diff --git a/.claude/skills/debug/SKILL.md b/.claude/skills/debug/SKILL.md deleted file mode 100644 index c42898cd708b..000000000000 --- a/.claude/skills/debug/SKILL.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -name: debug -description: Troubleshooting all sorts of failures, crashes, exceptions and errors using debug capabilities. Analyze accuracy, performance, or memory issues. Dump tensors and intermediate blobs. Serialize IRs, execution graphs. Enable verbose, logging. Profile execution. Compare layer outputs. Inspect, trace or dump transformations. Identify executed operations, nodes, primitives, kernels. ---- - -# Debug Skill - -## Prerequisites -Many debug env vars require the build flag `-DENABLE_DEBUG_CAPS=ON`. -Before suggesting env vars, verify the build was configured with this flag -(check CMakeCache.txt in the build dir). - -## Components - -| Component | Reference file to read | -|---------------------------|---------------------------------------| -| openvino_intel_cpu_plugin | @components/debug-intel-cpu-plugin.md | -| openvino_intel_gpu_plugin | @components/debug-intel-gpu-plugin.md | - -## Steps -1. Identify the component (or multiple components), then load its reference file -2. Use the instructions and recommendations for debugging diff --git a/.claude/skills/debug-matcher-pass/SKILL.md b/.claude/skills/ov-debug-matcher-pass/SKILL.md similarity index 79% rename from .claude/skills/debug-matcher-pass/SKILL.md rename to .claude/skills/ov-debug-matcher-pass/SKILL.md index bd2eeac25d86..7b8ae5efc8d6 100644 --- a/.claude/skills/debug-matcher-pass/SKILL.md +++ b/.claude/skills/ov-debug-matcher-pass/SKILL.md @@ -1,6 +1,7 @@ --- -name: debug-matcher-pass -description: Debug why an OpenVINO MatcherPass transformation is not firing. Use this skill immediately when a user says a transformation is "not applied", a "pass has no effect", a "matcher never triggers", a pattern "doesn't match", a "callback never fires", "WrapType predicate is too strict", a subgraph "not fused" despite the pass being registered, or they see "END: PATTERN DIDN'T MATCH" in matcher logs. Also trigger when a MatcherPass works on one model but silently skips another, when the user wants to add a reproducer test for a transformation that should fire but doesn't, or when they suspect an opset version mismatch preventing a match. Do NOT trigger for: writing a new MatcherPass from scratch, debugging a pass that fires but produces wrong numerical results, crashes in pass registration, or general questions about what MatcherPass is. +name: ov-debug-matcher-pass +description: > + Debug why an OpenVINO MatcherPass transformation is not firing. Use this skill immediately when a user says a transformation is "not applied", a "pass has no effect", a "matcher never triggers", a pattern "doesn't match", a "callback never fires", "WrapType predicate is too strict", a subgraph "not fused" despite the pass being registered, or they see "END: PATTERN DIDN'T MATCH" in matcher logs. Also trigger when a MatcherPass works on one model but silently skips another, when the user wants to add a reproducer test for a transformation that should fire but doesn't, or when they suspect an opset version mismatch preventing a match. Do NOT trigger for: writing a new MatcherPass from scratch, debugging a pass that fires but produces wrong numerical results, crashes in pass registration, or general questions about what MatcherPass is. --- # Debug MatcherPass Skill @@ -15,6 +16,8 @@ Produce two deliverables before finishing: The only filesystem change should be the test file edit. Do not create additional output files. +**Early exit — all passes fired:** If Step 2a shows `CALLBACK SUCCEDED > 0` for every pass under investigation, skip Steps 3–6. Post a lightweight confirmation summary (use the "all fired" variant in the Diagnosis Report Template) and do not write a reproducer test. There is no bug to reproduce. + --- ## Step 0: Gather Prerequisites @@ -36,20 +39,20 @@ Ask the user only if the transformation name or run command is missing. The buil ## Step 1: Verify Debug Build -The matcher logging macros are compiled in only when `ENABLE_OPENVINO_DEBUG=ON`. Check whether the current build already has it: +The matcher logging macros are compiled in only when `ENABLE_DEBUG_CAPS=ON`. Check whether the current build already has it: ```bash -grep -i "ENABLE_OPENVINO_DEBUG" build/*/CMakeCache.txt +grep -i "ENABLE_DEBUG_CAPS" build/*/CMakeCache.txt ``` If the flag is absent or set to `OFF`, reconfigure CMake using the existing build directory and build type from Step 0: ```bash -cmake -B -DENABLE_OPENVINO_DEBUG=ON +cmake -B -DENABLE_DEBUG_CAPS=ON cmake --build --parallel ``` -> **Note:** Logging also works in `Release` builds as long as `ENABLE_OPENVINO_DEBUG=ON` is set at configure time. +> **Note:** Logging also works in `Release` builds as long as `ENABLE_DEBUG_CAPS=ON` is set at configure time. --- @@ -68,6 +71,42 @@ Additional env vars: - **`OV_MATCHERS_TO_LOG`** — comma-separated list of matcher names to filter (omit to log all matchers, which produces very large output). - **`OV_VERBOSE_LOGGING=true`** — prints additional node details (element type, shape, attributes); use when the basic log does not identify the failure clearly. +### Wait for the command to complete + +Matcher logs are interleaved with normal output. If the command is long-running (e.g., model compilation on GPU), **wait for it to finish before analyzing**. Parsing a partial log will produce incorrect statistics (e.g., undercounting successful callbacks). Verify completion by checking for the final your_run_command output line or the process exit code. + +--- + +## Step 2a: Collect Per-Pass Statistics + +Before diving into detailed log analysis, run a quick tally to build an overview of which passes fired and how many times. This is especially important for **pipeline cascade** investigations with multiple passes. + +Use this Python snippet (adjust the `pass_names` list): + +```bash +python3 -c " +import re, sys +pass_names = ['PassA', 'PassB'] # <-- replace with actual pass names +with open('matcher.log') as f: + content = f.read() +for p in pass_names: + s = len(re.findall(rf'\[{p}\] END: PATTERN MATCHED, CALLBACK SUCCEDED', content)) + m = len(re.findall(rf'\[{p}\] END: PATTERN MATCHED', content)) + d = len(re.findall(rf'\[{p}\] END: PATTERN DIDN.T MATCH', content)) + print(f'{p}: CALLBACK SUCCEDED={s} MATCHED={m} DIDN\'T MATCH={d}') +" +``` + +Interpret the output: +- **CALLBACK SUCCEDED > 0** → pass fired and transformed nodes. +- **MATCHED > 0 but SUCCEDED = 0** → pattern matched but the callback returned `false` (check callback logic). +- **DIDN'T MATCH > 0 and MATCHED = 0** → pattern was attempted but never matched any node (proceed to Step 3 for root cause). +- **All counts = 0** → pass name not found in the log at all (not registered, or the pass is a `GraphRewrite` wrapper — log the inner `MatcherPass` names instead). + +> **Note on `GraphRewrite` wrappers:** If a pass is a `GraphRewrite` that calls `add_matcher()`, the matcher log will use the inner pass names, not the wrapper name. Check the header for inner pass names and use those in `OV_MATCHERS_TO_LOG`. + +For pipeline cascades, use the tally to quickly classify each pass as ✅ fired / ❌ root cause / ❌ downstream casualty before spending time on detailed log analysis. + --- ## Step 3: Analyze the Log — Identify Root Cause @@ -128,12 +167,13 @@ Work inward through the nested blocks to find the deepest `}` labeled with a fai 7. **Attribute value mismatch** — Pattern constrains an attribute (e.g., `group == 1`, `axis == 0`) that doesn't match the actual node. 8. **Transformation was already applied** — Graph was modified by a symmetric or overlapping pass earlier; the target op no longer exists. 9. **Wrong opset version** — Pattern uses `opset::OpX` but the frontend or a prior pass has already replaced it with a different version or a decomposed form. +10. **Argument count mismatch** — The graph node has a different number of inputs than the pattern expects (e.g., an op with shared-expert inputs has 23 inputs but the pattern only covers the 11-input or 13-input variant). Common when ops have multiple configurations (with/without bias, with/without shared experts). ### 3e. No output at all from OV_MATCHER_LOGGING If the log file is empty or contains no matcher output even though the flag is set: -- Confirm `ENABLE_OPENVINO_DEBUG=ON` in `CMakeCache.txt` for the binary you are actually running (not a different build directory). +- Confirm `ENABLE_DEBUG_CAPS=ON` in `CMakeCache.txt` for the binary you are actually running (not a different build directory). - Confirm the env var is exported in the same shell context as the run command: `export OV_MATCHER_LOGGING=true` or prefix it inline. - If calling through Python or a launcher script, the env var must survive into the child process — use `os.environ` or prefix the full command. - Verify you are running the freshly rebuilt binary, not a cached one from a different `bin/` location. @@ -280,11 +320,11 @@ See [references/example-diagnosis-report.md](references/example-diagnosis-report ## Summary of passes -| Pass | Result | -|---|---| -| `PassA` | ✅ Fired (`CALLBACK SUCCEEDED`) | -| `PassB` | ❌ Did not fire — root cause | -| `PassC` | ❌ Did not fire — downstream: PassB never produced its expected input | +| Pass | Result | Callbacks | Matches | +|---|---|---|---| +| `PassA` | ✅ Fired | 40 | 40 | +| `PassB` | ❌ Did not fire — root cause | 0 | 0 | +| `PassC` | ❌ Did not fire — downstream casualty | 0 | 0 | **Root cause:** **Log evidence:** `` @@ -297,6 +337,25 @@ Test name: `` Status before fix: PASS (transformation did not fire — model unchanged, matches auto-cloned ref; confirms bug reproduced) ``` +### All-fired variant (early exit) + +When all passes fired successfully (Step 2a), use this shorter template instead: + +``` +## MatcherPass Diagnosis: + +## Summary of passes +| Pass | Result | Callbacks | Matches | +|---|---|---|---| +| `PassA` | ✅ Fired | | | +| `PassB` | ✅ Fired | | | + +**All transformations fired successfully.** No issues found. + +## Reproducer Test +Not needed — no bug to reproduce. +``` + --- ## References diff --git a/.claude/skills/debug-matcher-pass/references/example-diagnosis-report.md b/.claude/skills/ov-debug-matcher-pass/references/example-diagnosis-report.md similarity index 100% rename from .claude/skills/debug-matcher-pass/references/example-diagnosis-report.md rename to .claude/skills/ov-debug-matcher-pass/references/example-diagnosis-report.md diff --git a/.claude/skills/ov-debug/SKILL.md b/.claude/skills/ov-debug/SKILL.md new file mode 100644 index 000000000000..e195afbd1ce8 --- /dev/null +++ b/.claude/skills/ov-debug/SKILL.md @@ -0,0 +1,23 @@ +--- +name: ov-debug +description: Troubleshooting all sorts of failures, crashes, exceptions and errors using debug capabilities. Analyze accuracy, performance, model compilation, or memory issues. Dump tensors and intermediate blobs. Serialize and visualize IRs, execution graphs. Enable verbose, logging. Profile execution. Compare layer outputs. Inspect, trace or dump transformations. Identify executed operations, nodes, primitives, kernels. +--- + +# Debug Skill + +## Prerequisites +Build flags that enable debug capabilities (check CMakeCache.txt in the build dir): +- `-DENABLE_DEBUG_CAPS=ON` — CPU/GPU plugin debug env vars, transformation matcher logging + +## Components + +| Component | Reference file to read | Routing hints | +|---------------------------|---------------------------------------|-------------------------------------------------------------------------------| +| openvino_intel_cpu_plugin | @components/debug-intel-cpu-plugin.md | CPU: inference issues, wrong results, slow inference, tensor dumps, execution graphs | +| openvino_intel_gpu_plugin | @components/debug-intel-gpu-plugin.md | GPU: inference issues, wrong results, slow inference, tensor dumps, execution graphs | +| transformations | @components/debug-transformations.md | transformation not applied, pass not firing, slow compilation, graph inspection | + +## Steps +1. Match the user's symptom to the routing hints above to identify the component(s) +2. Load the component's reference file +3. Follow the instructions and recommendations for debugging diff --git a/.claude/skills/debug/components/debug-intel-cpu-plugin.md b/.claude/skills/ov-debug/components/debug-intel-cpu-plugin.md similarity index 100% rename from .claude/skills/debug/components/debug-intel-cpu-plugin.md rename to .claude/skills/ov-debug/components/debug-intel-cpu-plugin.md diff --git a/.claude/skills/debug/components/debug-intel-gpu-plugin.md b/.claude/skills/ov-debug/components/debug-intel-gpu-plugin.md similarity index 100% rename from .claude/skills/debug/components/debug-intel-gpu-plugin.md rename to .claude/skills/ov-debug/components/debug-intel-gpu-plugin.md diff --git a/.claude/skills/ov-debug/components/debug-transformations.md b/.claude/skills/ov-debug/components/debug-transformations.md new file mode 100644 index 000000000000..b8c1128152c6 --- /dev/null +++ b/.claude/skills/ov-debug/components/debug-transformations.md @@ -0,0 +1,14 @@ +The debug and troubleshooting of OpenVINO transformations can be performed using various debug capabilities activated via environment variables. + +# Reference +Read `src/common/transformations/docs/debug_capabilities/README.md` — use the "When to use" guidance to match the observed problem to the right capability. + +# Handoff +If the symptom is specifically a MatcherPass not firing (transformation not applied, pattern not matching, callback never called), hand off to the `/ov-debug-matcher-pass` skill — it provides a deeper automated diagnosis workflow with matcher log analysis and reproducer test generation. + +# Steps +1. Read the debug capabilities README +2. Match the observed symptom to the relevant "When to use" entries +3. If the issue is a MatcherPass not firing — invoke `/ov-debug-matcher-pass` instead of continuing +4. Read the linked detail doc for the chosen capability +5. Use suitable environment variables diff --git a/.claude/skills/ov-ensure-coding-style/SKILL.md b/.claude/skills/ov-ensure-coding-style/SKILL.md new file mode 100644 index 000000000000..fe1d67bf1951 --- /dev/null +++ b/.claude/skills/ov-ensure-coding-style/SKILL.md @@ -0,0 +1,28 @@ +--- +name: ov-ensure-coding-style +description: Detect and fix clang-format, clang-tidy, and copyright header violations in an OpenVINO C++ codebase. Use when the user complains about code style or formatting, asks to clean up changes, fix linting, add a copyright header, or when a style check or linting CI job is failing. Do not use for build errors, compilation failures, linker errors, test failures, runtime crashes, accuracy issues, or CMake config problems. +--- + +# Apply Code Standards + +Iteratively detect and fix all clang-format, clang-tidy, and copyright violations introduced by the current branch's changes. + +## Step 1: Identify Affected Files and Build Targets + +**Before running any commands**, check whether the upstream reference is already confirmed in the conversation. If it is not, stop and ask: + +> "I'll diff against `upstream/master`. Let me know if you use a different upstream." + +Do not proceed until the user replies. Use the confirmed reference in all subsequent steps. Once the upstream branch is known, you must fetch and find a merge-base commit , against which the diff should be collected. Then run: + +```bash +git diff --name-only | grep -v '^thirdparty' | tee /tmp/changed_files.txt +``` + +Map the changed files to CMake targets. If the mapping is not obvious, ask the user: + +> "Which CMake targets cover the files you changed? (e.g., `openvino_intel_cpu_plugin`)" + +## Step 2: Check, Fix, Repeat + +**Follow the fix order and tool instructions from [coding_style.md](../../../docs/dev/coding_style.md) exactly — do not rearrange or skip steps.** Read that file before proceeding. Repeat the full cycle until all checks pass. diff --git a/.claude/skills/ov-update-pytorch-version/SKILL.md b/.claude/skills/ov-update-pytorch-version/SKILL.md new file mode 100644 index 000000000000..6ff283c89b2e --- /dev/null +++ b/.claude/skills/ov-update-pytorch-version/SKILL.md @@ -0,0 +1,208 @@ +--- +name: ov-update-pytorch-version +description: Upgrade the PyTorch version used by OpenVINO tests (torch / torchvision / torchaudio) and resolve fallout — missing operator translators, new functionalized `*_copy` aten ops, decomposition changes, FX-only tests failing in TorchScript mode, and accuracy regressions caused by stricter typing. Use when the user asks to "bump torch", "update pytorch to X.Y", "upgrade torch tests", or when pytorch_tests / model_hub pytorch tests fail after a torch version change. Do not use for: enabling a single new PyTorch operator unrelated to a version bump, GenAI / Optimum upgrades, or plugin-level numerical bugs unrelated to the frontend. +--- + +# Update PyTorch Version in OpenVINO + +End-to-end procedure for bumping the PyTorch version used by OpenVINO layer tests and the PyTorch frontend, and fixing the regressions that typically follow. + +## Step 0: Confirm Scope + +Before editing anything, confirm with the user: + +- Target versions for `torch`, `torchvision`: + - `torchvision` minor = torch minor **+ 15** — e.g. torch `2.9.0` ↔ torchvision `0.24.0`, torch `2.12.0` ↔ torchvision `0.27.0`. Always confirm against the official release matrix. + - `torchaudio` is **not** pinned by [tests/requirements_pytorch](../../../tests/requirements_pytorch): it is unused by these tests and PyTorch removed it from the official installation instructions starting with the 2.8 release. **Do not re-add it.** If a future task genuinely needs torchaudio, note that it is in a maintenance phase and its latest release can lag torch by one or more minors (e.g. torch `2.12.0` ↔ torchaudio `2.11.0`), so `torchaudio==` may not exist — look up the latest available wheel at `https://download.pytorch.org/whl/cpu/torchaudio/`. Recent torchaudio wheels carry **no** `Requires-Dist: torch` pin, so a slightly older torchaudio installs cleanly alongside a newer torch. +- Whether `tests/model_hub_tests/pytorch/envs/compile_gptq.txt` (pinned at `torch==2.3.1`, `torchaudio==2.3.1` for auto-gptq) should be touched. **Default: leave it alone.** +- Both the default TorchScript path **and** `PYTORCH_TRACING_MODE=EXPORT` (FX) path must be validated. Skip the FX run only if the user explicitly opts out. + +## Step 1: Update Version Pins + +Edit the following files. Keep `~=` style in `constraints.txt`, exact `==` in `requirements_*`. `A.B` is the torchvision version (torch minor + 15): + +- [tests/constraints.txt](../../../tests/constraints.txt) — `torch~=X.Y.0`, `torchvision~=A.B.0` (constraints.txt does not pin torchaudio) +- [tests/requirements_pytorch](../../../tests/requirements_pytorch) — `torch==X.Y.0`, `torchvision==A.B.0` (no torchaudio — see Step 0) +- `tests/model_hub_tests/pytorch/envs/*.txt` — bump in lockstep **except** `compile_gptq.txt` unless the user confirms. + +Install into the active environment: + +```bash +pip3 install --index-url https://download.pytorch.org/whl/cpu --upgrade \ + torch==X.Y.0 torchvision==A.B.0 +``` + +## Step 2: Run the Layer Tests in Parallel + +Always test against a freshly built `openvino_pytorch_frontend`. A site-wide install (e.g. under `~/.local`) can shadow the dev build — explicitly point at the build output. Set `OV_REPO` to the OpenVINO checkout root and `OV_BUILD_BIN` to its build artifact dir (typically `$OV_REPO/bin/intel64/Release`): + +```bash +cd "$OV_REPO/tests/layer_tests" +export PYTHONPATH="$OV_BUILD_BIN/python" +export LD_LIBRARY_PATH="$OV_BUILD_BIN" +export TEST_DEVICE=CPU TEST_PRECISION=FP32 +``` + +The pytorch layer tests use two pytest markers to gate which path a test runs on: + +- `-m precommit` — TorchScript path (default tracing mode). Use this to validate the TS frontend. +- `-m precommit_torch_export` — `torch.export` / FX path. Requires `PYTORCH_TRACING_MODE=EXPORT`; the test runner reads that env var to switch the test class behaviour. + +Both runs are required after a version bump: + +```bash +# TorchScript path +python3 -m pytest pytorch_tests/ -m precommit -n auto --tb=line -q \ + 2>&1 | tee /tmp/pt_run_ts.log | tail -5 + +# torch.export (FX) path +PYTORCH_TRACING_MODE=EXPORT \ +python3 -m pytest pytorch_tests/ -m precommit_torch_export -n auto --tb=line -q \ + 2>&1 | tee /tmp/pt_run_fx.log | tail -5 +``` + +Collect unique failing test files from either log: + +```bash +grep -E "^FAILED" /tmp/pt_run_ts.log /tmp/pt_run_fx.log | sed 's/\[.*//' | sort -u +``` + +Run one failing test verbosely to see the real traceback before grouping fixes (add `PYTORCH_TRACING_MODE=EXPORT` for FX-side reproduction): + +```bash +python3 -m pytest pytorch_tests/test_.py -k -x --tb=long +``` + +## Step 3: Triage Failures + +Sort failures into these buckets. The first three are the common ones for any minor torch bump. + +> **Rebuild after any C++ change.** Buckets A and B both modify the PyTorch frontend; re-run before re-testing: +> +> ```bash +> cmake --build "$OV_REPO/build" --target openvino_pytorch_frontend -j$(nproc) +> ``` + +### Bucket A — Newly-emitted aten ops with no translator + +Each torch release tends to lower more ops to new aten variants. The `*_copy` family from functionalization (e.g. `aten::select_copy`, `aten::view_copy`, `aten::squeeze_copy`, `aten::expand_copy`, `aten::permute_copy`, `aten::as_strided_copy`, `aten::split_with_sizes_copy`, `aten::unsqueeze_copy`) is one common pattern, but the bucket also covers any other freshly-introduced op (renames, new overloads, ops promoted out of decomposition, etc.). Symptom: log line like `No translator found for aten::`. + +Decision tree: + +1. **Semantically equivalent to an existing op** (typical for `_copy` variants — OV graphs are functional, so the copy is a no-op) → alias to the existing translator in the **TorchScript** table (`aten::*` keys) of [src/frontends/pytorch/src/op_table.cpp](../../../src/frontends/pytorch/src/op_table.cpp): + + ```cpp + {"aten::select_copy", op::quantizable_op}, + {"aten::view_copy", op::quantizable_op}, + {"aten::split_with_sizes_copy", op::translate_split_with_sizes}, + ``` + +2. **Different signature but same underlying op** → write a thin wrapper that adapts inputs/attrs and dispatches to the existing translator. + +3. **Genuinely new behavior** → implement a new translator under `src/frontends/pytorch/src/op/` and register it in both tables if the FX path also emits it. + +Add an FX-table entry (`aten..default` / specific overload) only if the FX path also reports the op missing. + +**Ensure the alias is actually covered by a TorchScript test.** Many existing `*_copy` layer tests are marked only `@pytest.mark.precommit_fx_backend` (and sometimes `precommit_torch_export`), so the TS table entry you just added is never exercised by the TorchScript precommit job. After adding a TS alias, find the matching test class (e.g. `TestSelectCopy`, `TestViewCopy`) and add `@pytest.mark.nightly` + `@pytest.mark.precommit` so the new registration is validated. If no test exists for the op, add one. Reviewers (including Copilot) flag missing TS coverage for new TS registrations. + +### Bucket B — Translator only registered for FX + +Symptom: TS test fails with "No translator found for `aten::`" while an `*_fx` translator already exists. + +Fix: register the existing `translate__fx` (or write a thin TS wrapper) in the TS section of `op_table.cpp`, and extend the translator body to accept the TS calling convention. Use `num_inputs_check(context, min, max)` and branch on input count for the TS dtype/layout/device tail. Example: see `translate_scalar_tensor_fx` in [src/frontends/pytorch/src/op/scalar_tensor.cpp](../../../src/frontends/pytorch/src/op/scalar_tensor.cpp). + +As in Bucket A, add `@pytest.mark.nightly` + `@pytest.mark.precommit` to the corresponding test so the TS path is exercised. If you added a new TS-only code branch (e.g. a TS dtype tail), add a parametrized case that hits it — extending the existing test, not duplicating it. + +### Bucket C — FX-only test code reached in TorchScript mode + +This bucket only applies when a test ends up executed in the TS path while its body relies on FX-only constructs (`torch.cond`, `torch.while_loop`, `torch.ops.aten.*`, `torch.ops.quantized_decomposed.*`, etc.). It happens when: + +1. The test class carries both `@pytest.mark.precommit` and `@pytest.mark.precommit_torch_export` but the body works only on one path, **or** +2. The suite is run without `-m` (e.g. ad-hoc full-suite reproduction). + +If the test is genuinely FX-only, drop the `precommit` marker so `-m precommit` skips it cleanly. If both backends are intended, add a body-level guard: + +```python +if not (self.use_torch_export() or self.use_torch_compile_backend()): + pytest.skip("FX-only test") +``` + +For parametrize-time skipping, the helpers in [tests/layer_tests/pytorch_tests/pytorch_layer_test_class.py](../../../tests/layer_tests/pytorch_tests/pytorch_layer_test_class.py) are `skip_if_export(*params, reason=...)` (skips on the `torch.export` path), `skip_if_fx(*params, reason=...)` (skips on the `torch.compile`/FX path), and `skip_check(*params, reason=...)` (skips on whichever non-TS path is active). There is **no** `skip_if_ts` helper — to skip the TorchScript path, gate inside the test body with `if not (self.use_torch_export() or self.use_torch_compile_backend()): pytest.skip(...)`. + +### Bucket D — Stricter typing / changed default kwargs + +Examples observed in past bumps: + +- `aten::hardtanh_` is now emitted instead of `aten::hardtanh` when `inplace=True` — make the expected op-kind dynamic in the test. +- Literal `[0, 1.0]` rejected as mixed int/float — change to `[0.0, 1.0]`. +- New kwargs default-on (e.g. `scaled_dot_product_attention(enable_gqa=...)`) — pass the value explicitly so traced graphs match across versions. + +### Bucket E — Accuracy regressions + +Before assuming a translator bug, check whether the same input pattern triggers a known plugin issue (e.g. CPU `Multiply(x, Constant(inf))` followed by `IsInf` with dynamic shape returns all-False due to a Mul+IsInf fusion). If reproducible without the PT FE, work around in the test input and note the underlying ticket in the commit message — do not paper over real frontend regressions. + +## Step 4: Re-validate + +Re-run both marker scopes from Step 2 until each ends with `0 failed`: + +```bash +python3 -m pytest pytorch_tests/ -m precommit -n auto --tb=line -q 2>&1 | tail -5 +PYTORCH_TRACING_MODE=EXPORT \ +python3 -m pytest pytorch_tests/ -m precommit_torch_export -n auto --tb=line -q 2>&1 | tail -5 +``` + +Expect a handful of new skipped/xfailed tests after Bucket C fixes — that is normal. FX-path failures usually point to missing FX-table entries or decomposition changes, not TS aliasing. + +## Step 5: Check Model Hub Tests + +Model hub tests load real (downloaded) models and prepare inputs using torchvision/torch helpers directly in the test file. These helpers can break when the new torchvision version removes or renames an API. + +**Where to look**: `tests/model_hub_tests/pytorch/test_torchvision_models.py` and similar files in that directory. Focus on `load_model()` — it typically contains model-specific input preparation branches. + +**Common failure pattern**: + +``` +ImportError: cannot import name '' from 'torchvision.' +``` + +A helper function inside the test file imports an API (e.g. `torchvision.io.read_video`, `torchvision.transforms.*`) that was removed in the new version. The function is called from inside a `load_model()` branch for a specific model (e.g. optical-flow models like RAFT). + +**Fix**: + +1. Identify what input shape the removed pipeline produced. Check the original code for resize/transform dimensions applied to the input. +2. Replace the helper call with a `torch.randn(...)` tensor of the same shape. +3. Remove the now-unused imports and the helper function itself. + +Example (optical-flow model input after a 520×960 resize+transform pipeline): + +```python +# before +frames = get_video() +self.example = prepare_frames_for_raft(model_name, [frames[100], frames[150]], ...) + +# after +self.example = (torch.randn(2, 3, 520, 960), torch.randn(2, 3, 520, 960)) +self.inputs = (torch.randn(2, 3, 520, 960), torch.randn(2, 3, 520, 960)) +``` + +Also check that any imports only needed by the removed helper are cleaned up (e.g. `import tempfile`, `import torchvision.transforms.functional as F`, `get_model_weights`). + +**Verify** by checking the torchvision changelog for removed APIs whenever tests in this directory fail after a version bump. + +## Step 6: Commit + +One commit on a feature branch (project convention: `/pt_fe/`, e.g. `mvafin/pt_fe/torch_2_12_upgrade`) covering: requirements, `op_table.cpp` additions, any translator extensions, and test fixes. List the new translators and the broken-test categories in the commit body — reviewers use it to map CI changes to user-visible behavior. + +The pre-commit hook runs `clang-format` and may amend `src/frontends/pytorch/src/op/*.cpp`; re-stage and recommit if it does. + +## Pitfalls + +- **Model hub test helpers** (in `tests/model_hub_tests/pytorch/`) sometimes import torchvision/torch APIs that are removed in a new release. The failure surfaces as an `ImportError` inside a subprocess (via `multiprocessing_run`), so the traceback points into the helper, not the test parametrize line. Always check the torchvision changelog for removed APIs when these tests fail after a bump. Replace removed API calls with equivalent `torch.randn(...)` inputs of the same shape. +- **Do not** alias a new aten op to an existing translator without confirming semantic equivalence (signature, dtype/broadcast rules, attribute defaults). Aliasing is only safe when the new op is a behavioral no-op or true synonym of the existing one. +- **Do not** rely on `id(node)` when walking ov.Node graphs in Python; wrappers are recreated. Use `get_friendly_name()`. +- Test markers are filterable but not enforced — a class marked only `precommit_torch_export` still runs under TS if pytest is invoked without `-m`. Use marker-filtered invocations from Step 2; reserve inline `pytest.skip` for genuinely dual-marked tests. +- New TS op_table aliases/registrations are easy to leave untested: their tests are frequently marked `precommit_fx_backend` only. Always add `precommit` + `nightly` so the TorchScript job covers them (see Buckets A/B). +- `pytest -n auto` requires `pytest-xdist` (in `tests/requirements_pytorch`); confirm it is installed in the active env. +- `torchvision` minor = torch minor **+ 15** (`torch 2.9` ↔ `torchvision 0.24`, `torch 2.12` ↔ `torchvision 0.27`). Double-check the official release matrix before pinning. +- In `tests/requirements_pytorch`, place `--extra-index-url https://download.pytorch.org/whl/cpu` **before** the `torch==` and `torchvision==` lines so the PyTorch CPU index is available when pip resolves those packages. The line order matters for some pip-based tooling even though pip applies the option globally once parsed. +- `torchaudio` is intentionally **not** in `requirements_pytorch` (unused; dropped from PyTorch's official install since 2.8). Do not re-add it. If you ever must, note it is in a maintenance phase and lags torch (`torch 2.12.0` ↔ `torchaudio 2.11.0`), so pinning `torchaudio==` blindly causes a CI install failure (`No matching distribution found for torchaudio==X.Y.0`) — look up the latest available wheel. diff --git a/.github/actions/cache/package-lock.json b/.github/actions/cache/package-lock.json index ea0f322b36e8..037fbc3da425 100644 --- a/.github/actions/cache/package-lock.json +++ b/.github/actions/cache/package-lock.json @@ -23,7 +23,7 @@ "eslint-plugin-github": "^6.0.0", "eslint-plugin-jest": "^27.9.0", "fs": "0.0.1-security", - "jest": "^29.7.0", + "jest": "^30.3.0", "prettier": "^3.8.3" }, "engines": { @@ -65,19 +65,6 @@ "integrity": "sha512-nRBchcMM+QK1pdjO7/idu86rbJI5YHUKCvKs0KxnSYbVe3F51UfGxuZX4Qy/fWlp6l7gWFwIkrOzN+oUK03kfw==", "license": "MIT" }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "dev": true, - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@babel/code-frame": { "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", @@ -104,21 +91,22 @@ } }, "node_modules/@babel/core": { - "version": "7.26.10", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.10.tgz", - "integrity": "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==", - "dev": true, - "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.26.2", - "@babel/generator": "^7.26.10", - "@babel/helper-compilation-targets": "^7.26.5", - "@babel/helper-module-transforms": "^7.26.0", - "@babel/helpers": "^7.26.10", - "@babel/parser": "^7.26.10", - "@babel/template": "^7.26.9", - "@babel/traverse": "^7.26.10", - "@babel/types": "^7.26.10", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -430,13 +418,14 @@ } }, "node_modules/@babel/helpers": { - "version": "7.26.10", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.10.tgz", - "integrity": "sha512-UPYc3SauzZ3JGgj87GgZ89JVdC5dj0AoetR5Bw6wj4niittNyFh6+eOGonYvJ1ao6B8lEa3Q3klS7ADZ53bc5g==", + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/template": "^7.26.9", - "@babel/types": "^7.26.10" + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" }, "engines": { "node": ">=6.9.0" @@ -662,12 +651,13 @@ } }, "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.25.9.tgz", - "integrity": "sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", + "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -779,12 +769,13 @@ } }, "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.9.tgz", - "integrity": "sha512-hjMgRy5hb8uJJjUcdWunWVcoi9bGpJp8p5Ol1229PoN6aytsLwNMgmdftO23wnCLMfVmTwZDWMPNq/D1SY60JQ==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz", + "integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1816,7 +1807,42 @@ "version": "0.2.3", "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", - "dev": true + "dev": true, + "license": "MIT" + }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } }, "node_modules/@eslint-community/eslint-utils": { "version": "4.9.1", @@ -1981,6 +2007,102 @@ "deprecated": "Use @eslint/object-schema instead", "dev": true }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/@isaacs/fs-minipass": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", @@ -1997,6 +2119,7 @@ "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", "dev": true, + "license": "ISC", "dependencies": { "camelcase": "^5.3.1", "find-up": "^4.1.0", @@ -2013,6 +2136,7 @@ "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", "dev": true, + "license": "MIT", "dependencies": { "sprintf-js": "~1.0.2" } @@ -2022,6 +2146,7 @@ "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", "dev": true, + "license": "MIT", "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" @@ -2035,6 +2160,7 @@ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", "dev": true, + "license": "MIT", "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" @@ -2048,6 +2174,7 @@ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", "dev": true, + "license": "MIT", "dependencies": { "p-locate": "^4.1.0" }, @@ -2060,6 +2187,7 @@ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", "dev": true, + "license": "MIT", "dependencies": { "p-try": "^2.0.0" }, @@ -2075,6 +2203,7 @@ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", "dev": true, + "license": "MIT", "dependencies": { "p-limit": "^2.2.0" }, @@ -2087,73 +2216,76 @@ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/@istanbuljs/schema": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", + "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/@jest/console": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", - "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.3.0.tgz", + "integrity": "sha512-PAwCvFJ4696XP2qZj+LAn1BWjZaJ6RjG6c7/lkMaUJnkyMS34ucuIsfqYvfskVNvUI27R/u4P1HMYFnlVXG/Ww==", "dev": true, + "license": "MIT", "dependencies": { - "@jest/types": "^29.6.3", + "@jest/types": "30.3.0", "@types/node": "*", - "chalk": "^4.0.0", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0", + "chalk": "^4.1.2", + "jest-message-util": "30.3.0", + "jest-util": "30.3.0", "slash": "^3.0.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/core": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", - "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.3.0.tgz", + "integrity": "sha512-U5mVPsBxLSO6xYbf+tgkymLx+iAhvZX43/xI1+ej2ZOPnPdkdO1CzDmFKh2mZBn2s4XZixszHeQnzp1gm/DIxw==", "dev": true, + "license": "MIT", "dependencies": { - "@jest/console": "^29.7.0", - "@jest/reporters": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", + "@jest/console": "30.3.0", + "@jest/pattern": "30.0.1", + "@jest/reporters": "30.3.0", + "@jest/test-result": "30.3.0", + "@jest/transform": "30.3.0", + "@jest/types": "30.3.0", "@types/node": "*", - "ansi-escapes": "^4.2.1", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.9", - "jest-changed-files": "^29.7.0", - "jest-config": "^29.7.0", - "jest-haste-map": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-regex-util": "^29.6.3", - "jest-resolve": "^29.7.0", - "jest-resolve-dependencies": "^29.7.0", - "jest-runner": "^29.7.0", - "jest-runtime": "^29.7.0", - "jest-snapshot": "^29.7.0", - "jest-util": "^29.7.0", - "jest-validate": "^29.7.0", - "jest-watcher": "^29.7.0", - "micromatch": "^4.0.4", - "pretty-format": "^29.7.0", - "slash": "^3.0.0", - "strip-ansi": "^6.0.0" + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "exit-x": "^0.2.2", + "graceful-fs": "^4.2.11", + "jest-changed-files": "30.3.0", + "jest-config": "30.3.0", + "jest-haste-map": "30.3.0", + "jest-message-util": "30.3.0", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.3.0", + "jest-resolve-dependencies": "30.3.0", + "jest-runner": "30.3.0", + "jest-runtime": "30.3.0", + "jest-snapshot": "30.3.0", + "jest-util": "30.3.0", + "jest-validate": "30.3.0", + "jest-watcher": "30.3.0", + "pretty-format": "30.3.0", + "slash": "^3.0.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" }, "peerDependencies": { "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" @@ -2164,111 +2296,150 @@ } } }, + "node_modules/@jest/diff-sequences": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.3.0.tgz", + "integrity": "sha512-cG51MVnLq1ecVUaQ3fr6YuuAOitHK1S4WUJHnsPFE/quQr33ADUx1FfrTCpMCRxvy0Yr9BThKpDjSlcTi91tMA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, "node_modules/@jest/environment": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", - "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.3.0.tgz", + "integrity": "sha512-SlLSF4Be735yQXyh2+mctBOzNDx5s5uLv88/j8Qn1wH679PDcwy67+YdADn8NJnGjzlXtN62asGH/T4vWOkfaw==", "dev": true, + "license": "MIT", "dependencies": { - "@jest/fake-timers": "^29.7.0", - "@jest/types": "^29.6.3", + "@jest/fake-timers": "30.3.0", + "@jest/types": "30.3.0", "@types/node": "*", - "jest-mock": "^29.7.0" + "jest-mock": "30.3.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/expect": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", - "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.3.0.tgz", + "integrity": "sha512-76Nlh4xJxk2D/9URCn3wFi98d2hb19uWE1idLsTt2ywhvdOldbw3S570hBgn25P4ICUZ/cBjybrBex2g17IDbg==", "dev": true, + "license": "MIT", "dependencies": { - "expect": "^29.7.0", - "jest-snapshot": "^29.7.0" + "expect": "30.3.0", + "jest-snapshot": "30.3.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/expect-utils": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", - "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.3.0.tgz", + "integrity": "sha512-j0+W5iQQ8hBh7tHZkTQv3q2Fh/M7Je72cIsYqC4OaktgtO7v1So9UTjp6uPBHIaB6beoF/RRsCgMJKvti0wADA==", "dev": true, + "license": "MIT", "dependencies": { - "jest-get-type": "^29.6.3" + "@jest/get-type": "30.1.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/fake-timers": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", - "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.3.0.tgz", + "integrity": "sha512-WUQDs8SOP9URStX1DzhD425CqbN/HxUYCTwVrT8sTVBfMvFqYt/s61EK5T05qnHu0po6RitXIvP9otZxYDzTGQ==", "dev": true, + "license": "MIT", "dependencies": { - "@jest/types": "^29.6.3", - "@sinonjs/fake-timers": "^10.0.2", + "@jest/types": "30.3.0", + "@sinonjs/fake-timers": "^15.0.0", "@types/node": "*", - "jest-message-util": "^29.7.0", - "jest-mock": "^29.7.0", - "jest-util": "^29.7.0" + "jest-message-util": "30.3.0", + "jest-mock": "30.3.0", + "jest-util": "30.3.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/get-type": { + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.1.0.tgz", + "integrity": "sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/globals": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", - "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.3.0.tgz", + "integrity": "sha512-+owLCBBdfpgL3HU+BD5etr1SvbXpSitJK0is1kiYjJxAAJggYMRQz5hSdd5pq1sSggfxPbw2ld71pt4x5wwViA==", "dev": true, + "license": "MIT", "dependencies": { - "@jest/environment": "^29.7.0", - "@jest/expect": "^29.7.0", - "@jest/types": "^29.6.3", - "jest-mock": "^29.7.0" + "@jest/environment": "30.3.0", + "@jest/expect": "30.3.0", + "@jest/types": "30.3.0", + "jest-mock": "30.3.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/pattern": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz", + "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-regex-util": "30.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/reporters": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", - "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.3.0.tgz", + "integrity": "sha512-a09z89S+PkQnL055bVj8+pe2Caed2PBOaczHcXCykW5ngxX9EWx/1uAwncxc/HiU0oZqfwseMjyhxgRjS49qPw==", "dev": true, + "license": "MIT", "dependencies": { "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "@jridgewell/trace-mapping": "^0.3.18", + "@jest/console": "30.3.0", + "@jest/test-result": "30.3.0", + "@jest/transform": "30.3.0", + "@jest/types": "30.3.0", + "@jridgewell/trace-mapping": "^0.3.25", "@types/node": "*", - "chalk": "^4.0.0", - "collect-v8-coverage": "^1.0.0", - "exit": "^0.1.2", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", + "chalk": "^4.1.2", + "collect-v8-coverage": "^1.0.2", + "exit-x": "^0.2.2", + "glob": "^10.5.0", + "graceful-fs": "^4.2.11", "istanbul-lib-coverage": "^3.0.0", "istanbul-lib-instrument": "^6.0.0", "istanbul-lib-report": "^3.0.0", - "istanbul-lib-source-maps": "^4.0.0", + "istanbul-lib-source-maps": "^5.0.0", "istanbul-reports": "^3.1.3", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0", - "jest-worker": "^29.7.0", + "jest-message-util": "30.3.0", + "jest-util": "30.3.0", + "jest-worker": "30.3.0", "slash": "^3.0.0", - "string-length": "^4.0.1", - "strip-ansi": "^6.0.0", + "string-length": "^4.0.2", "v8-to-istanbul": "^9.0.1" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" }, "peerDependencies": { "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" @@ -2279,103 +2450,173 @@ } } }, + "node_modules/@jest/reporters/node_modules/brace-expansion": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@jest/reporters/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@jest/reporters/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@jest/schemas": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", - "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", "dev": true, + "license": "MIT", "dependencies": { - "@sinclair/typebox": "^0.27.8" + "@sinclair/typebox": "^0.34.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/snapshot-utils": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.3.0.tgz", + "integrity": "sha512-ORbRN9sf5PP82v3FXNSwmO1OTDR2vzR2YTaR+E3VkSBZ8zadQE6IqYdYEeFH1NIkeB2HIGdF02dapb6K0Mj05g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.3.0", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "natural-compare": "^1.4.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/source-map": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", - "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-30.0.1.tgz", + "integrity": "sha512-MIRWMUUR3sdbP36oyNyhbThLHyJ2eEDClPCiHVbrYAe5g3CHRArIVpBw7cdSB5fr+ofSfIb2Tnsw8iEHL0PYQg==", "dev": true, + "license": "MIT", "dependencies": { - "@jridgewell/trace-mapping": "^0.3.18", - "callsites": "^3.0.0", - "graceful-fs": "^4.2.9" + "@jridgewell/trace-mapping": "^0.3.25", + "callsites": "^3.1.0", + "graceful-fs": "^4.2.11" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/test-result": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", - "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.3.0.tgz", + "integrity": "sha512-e/52nJGuD74AKTSe0P4y5wFRlaXP0qmrS17rqOMHeSwm278VyNyXE3gFO/4DTGF9w+65ra3lo3VKj0LBrzmgdQ==", "dev": true, + "license": "MIT", "dependencies": { - "@jest/console": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "collect-v8-coverage": "^1.0.0" + "@jest/console": "30.3.0", + "@jest/types": "30.3.0", + "@types/istanbul-lib-coverage": "^2.0.6", + "collect-v8-coverage": "^1.0.2" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/test-sequencer": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", - "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.3.0.tgz", + "integrity": "sha512-dgbWy9b8QDlQeRZcv7LNF+/jFiiYHTKho1xirauZ7kVwY7avjFF6uTT0RqlgudB5OuIPagFdVtfFMosjVbk1eA==", "dev": true, + "license": "MIT", "dependencies": { - "@jest/test-result": "^29.7.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", + "@jest/test-result": "30.3.0", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.3.0", "slash": "^3.0.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/transform": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", - "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.3.0.tgz", + "integrity": "sha512-TLKY33fSLVd/lKB2YI1pH69ijyUblO/BQvCj566YvnwuzoTNr648iE0j22vRvVNk2HsPwByPxATg3MleS3gf5A==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/core": "^7.11.6", - "@jest/types": "^29.6.3", - "@jridgewell/trace-mapping": "^0.3.18", - "babel-plugin-istanbul": "^6.1.1", - "chalk": "^4.0.0", + "@babel/core": "^7.27.4", + "@jest/types": "30.3.0", + "@jridgewell/trace-mapping": "^0.3.25", + "babel-plugin-istanbul": "^7.0.1", + "chalk": "^4.1.2", "convert-source-map": "^2.0.0", "fast-json-stable-stringify": "^2.1.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "jest-regex-util": "^29.6.3", - "jest-util": "^29.7.0", - "micromatch": "^4.0.4", - "pirates": "^4.0.4", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.3.0", + "jest-regex-util": "30.0.1", + "jest-util": "30.3.0", + "pirates": "^4.0.7", "slash": "^3.0.0", - "write-file-atomic": "^4.0.2" + "write-file-atomic": "^5.0.1" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/types": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", - "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.3.0.tgz", + "integrity": "sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw==", "dev": true, + "license": "MIT", "dependencies": { - "@jest/schemas": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jridgewell/gen-mapping": { @@ -2389,6 +2630,17 @@ "@jridgewell/trace-mapping": "^0.3.24" } }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -2415,12 +2667,25 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { - "version": "5.1.1-v1", - "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", - "integrity": "sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==", + "node_modules/@napi-rs/wasm-runtime": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", + "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", "dev": true, - "dependencies": { + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.10.0" + } + }, + "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { + "version": "5.1.1-v1", + "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", + "integrity": "sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==", + "dev": true, + "dependencies": { "eslint-scope": "5.1.1" } }, @@ -2459,6 +2724,17 @@ "node": ">= 8" } }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@pkgr/core": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.2.tgz", @@ -2478,27 +2754,41 @@ "dev": true }, "node_modules/@sinclair/typebox": { - "version": "0.27.8", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", - "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", - "dev": true + "version": "0.34.49", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.49.tgz", + "integrity": "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==", + "dev": true, + "license": "MIT" }, "node_modules/@sinonjs/commons": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "type-detect": "4.0.8" } }, "node_modules/@sinonjs/fake-timers": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", - "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "version": "15.3.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-15.3.2.tgz", + "integrity": "sha512-mrn35Jl2pCpns+mE3HaZa1yPN5EYCRgiMI+135COjr2hr8Cls9DXqIZ57vZe2cz7y2XVSq92tcs6kGQcT1J8Rw==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "@sinonjs/commons": "^3.0.0" + "@sinonjs/commons": "^3.0.1" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" } }, "node_modules/@types/babel__core": { @@ -2542,26 +2832,19 @@ "@babel/types": "^7.20.7" } }, - "node_modules/@types/graceful-fs": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", - "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/istanbul-lib-report": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", "dev": true, + "license": "MIT", "dependencies": { "@types/istanbul-lib-coverage": "*" } @@ -2571,6 +2854,7 @@ "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", "dev": true, + "license": "MIT", "dependencies": { "@types/istanbul-lib-report": "*" } @@ -2588,12 +2872,13 @@ "dev": true }, "node_modules/@types/node": { - "version": "22.13.11", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.11.tgz", - "integrity": "sha512-iEUCUJoU0i3VnrCmgoWCXttklWcvoCIx4jzcP22fioIVSdTmjgoEvmAO/QPw6TcS9k5FrNgn4w7q5lGOd1CT5g==", + "version": "25.6.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz", + "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", "dev": true, + "license": "MIT", "dependencies": { - "undici-types": "~6.20.0" + "undici-types": "~7.19.0" } }, "node_modules/@types/semver": { @@ -2606,13 +2891,15 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/yargs": { - "version": "17.0.33", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", - "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", "dev": true, + "license": "MIT", "dependencies": { "@types/yargs-parser": "*" } @@ -2621,7 +2908,8 @@ "version": "21.0.3", "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@typescript-eslint/project-service": { "version": "8.58.2", @@ -2682,6 +2970,275 @@ "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", "dev": true }, + "node_modules/@unrs/resolver-binding-android-arm-eabi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", + "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-android-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", + "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz", + "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", + "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz", + "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz", + "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz", + "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz", + "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz", + "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz", + "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz", + "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz", + "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz", + "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz", + "integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz", + "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", + "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^0.2.11" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", + "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", + "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", + "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@vercel/ncc": { "version": "0.38.3", "resolved": "https://registry.npmjs.org/@vercel/ncc/-/ncc-0.38.3.tgz", @@ -2734,6 +3291,7 @@ "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", "dev": true, + "license": "MIT", "dependencies": { "type-fest": "^0.21.3" }, @@ -2749,6 +3307,7 @@ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", "dev": true, + "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=10" }, @@ -2785,6 +3344,7 @@ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", "dev": true, + "license": "ISC", "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" @@ -2980,89 +3540,45 @@ } }, "node_modules/babel-jest": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", - "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", - "dev": true, - "dependencies": { - "@jest/transform": "^29.7.0", - "@types/babel__core": "^7.1.14", - "babel-plugin-istanbul": "^6.1.1", - "babel-preset-jest": "^29.6.3", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "slash": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.8.0" - } - }, - "node_modules/babel-jest/node_modules/babel-plugin-jest-hoist": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", - "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.3.3", - "@babel/types": "^7.3.3", - "@types/babel__core": "^7.1.14", - "@types/babel__traverse": "^7.0.6" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/babel-jest/node_modules/babel-preset-jest": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", - "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.3.0.tgz", + "integrity": "sha512-gRpauEU2KRrCox5Z296aeVHR4jQ98BCnu0IO332D/xpHNOsIH/bgSRk9k6GbKIbBw8vFeN6ctuu6tV8WOyVfYQ==", "dev": true, "license": "MIT", "dependencies": { - "babel-plugin-jest-hoist": "^29.6.3", - "babel-preset-current-node-syntax": "^1.0.0" + "@jest/transform": "30.3.0", + "@types/babel__core": "^7.20.5", + "babel-plugin-istanbul": "^7.0.1", + "babel-preset-jest": "30.3.0", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "slash": "^3.0.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" }, "peerDependencies": { - "@babel/core": "^7.0.0" + "@babel/core": "^7.11.0 || ^8.0.0-0" } }, "node_modules/babel-plugin-istanbul": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", - "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.1.tgz", + "integrity": "sha512-D8Z6Qm8jCvVXtIRkBnqNHX0zJ37rQcFJ9u8WOS6tkYOsRdHBzypCstaxWiu5ZIlqQtviRYbgnRLSoCEvjqcqbA==", "dev": true, + "license": "BSD-3-Clause", + "workspaces": [ + "test/babel-8" + ], "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", "@istanbuljs/load-nyc-config": "^1.0.0", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-instrument": "^5.0.4", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-instrument": "^6.0.2", "test-exclude": "^6.0.0" }, "engines": { - "node": ">=8" - } - }, - "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", - "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", - "dev": true, - "dependencies": { - "@babel/core": "^7.12.3", - "@babel/parser": "^7.14.7", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=8" + "node": ">=12" } }, "node_modules/babel-plugin-jest-hoist": { @@ -3244,6 +3760,7 @@ "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", "dev": true, + "license": "Apache-2.0", "dependencies": { "node-int64": "^0.4.0" } @@ -3252,7 +3769,8 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/call-bind": { "version": "1.0.8", @@ -3315,6 +3833,7 @@ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -3361,6 +3880,7 @@ "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" } @@ -3374,9 +3894,9 @@ } }, "node_modules/ci-info": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", - "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", "dev": true, "funding": [ { @@ -3384,21 +3904,24 @@ "url": "https://github.com/sponsors/sibiraj-s" } ], + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/cjs-module-lexer": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", - "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", - "dev": true + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.2.0.tgz", + "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", + "dev": true, + "license": "MIT" }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", "dev": true, + "license": "ISC", "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", @@ -3413,16 +3936,18 @@ "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", "dev": true, + "license": "MIT", "engines": { "iojs": ">= 1.0.0", "node": ">= 0.12.0" } }, "node_modules/collect-v8-coverage": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", - "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", - "dev": true + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", + "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", + "dev": true, + "license": "MIT" }, "node_modules/color-convert": { "version": "2.0.1", @@ -3468,27 +3993,6 @@ "url": "https://opencollective.com/core-js" } }, - "node_modules/create-jest": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", - "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", - "dev": true, - "dependencies": { - "@jest/types": "^29.6.3", - "chalk": "^4.0.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.9", - "jest-config": "^29.7.0", - "jest-util": "^29.7.0", - "prompts": "^2.0.1" - }, - "bin": { - "create-jest": "bin/create-jest.js" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -3579,10 +4083,11 @@ } }, "node_modules/dedent": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", - "integrity": "sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==", + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", + "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", "dev": true, + "license": "MIT", "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, @@ -3603,6 +4108,7 @@ "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -3646,19 +4152,11 @@ "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, - "node_modules/diff-sequences": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", - "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", - "dev": true, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -3697,6 +4195,13 @@ "node": ">= 0.4" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, "node_modules/electron-to-chromium": { "version": "1.5.336", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.336.tgz", @@ -3709,6 +4214,7 @@ "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -3723,10 +4229,11 @@ "dev": true }, "node_modules/error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", "dev": true, + "license": "MIT", "dependencies": { "is-arrayish": "^0.2.1" } @@ -4872,6 +5379,7 @@ "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", "dev": true, + "license": "BSD-2-Clause", "bin": { "esparse": "bin/esparse.js", "esvalidate": "bin/esvalidate.js" @@ -4945,6 +5453,7 @@ "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", "dev": true, + "license": "MIT", "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", @@ -4963,29 +5472,32 @@ "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, - "node_modules/exit": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", - "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "node_modules/exit-x": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/exit-x/-/exit-x-0.2.2.tgz", + "integrity": "sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.8.0" } }, "node_modules/expect": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", - "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-30.3.0.tgz", + "integrity": "sha512-1zQrciTiQfRdo7qJM1uG4navm8DayFa2TgCSRlzUyNkhcJ6XUZF3hjnpkyr3VhAqPH7i/9GkG7Tv5abz6fqz0Q==", "dev": true, + "license": "MIT", "dependencies": { - "@jest/expect-utils": "^29.7.0", - "jest-get-type": "^29.6.3", - "jest-matcher-utils": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0" + "@jest/expect-utils": "30.3.0", + "@jest/get-type": "30.1.0", + "jest-matcher-utils": "30.3.0", + "jest-message-util": "30.3.0", + "jest-mock": "30.3.0", + "jest-util": "30.3.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/fast-deep-equal": { @@ -5054,6 +5566,7 @@ "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", "dev": true, + "license": "Apache-2.0", "dependencies": { "bser": "2.1.1" } @@ -5127,10 +5640,40 @@ "is-callable": "^1.2.7" }, "engines": { - "node": ">= 0.4" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/fs": { @@ -5151,6 +5694,7 @@ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, "hasInstallScript": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -5211,6 +5755,7 @@ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true, + "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" } @@ -5244,6 +5789,7 @@ "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=8.0.0" } @@ -5266,6 +5812,7 @@ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -5375,7 +5922,8 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/graphemer": { "version": "1.4.0", @@ -5474,13 +6022,15 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", "dev": true, + "license": "Apache-2.0", "engines": { "node": ">=10.17.0" } @@ -5515,6 +6065,7 @@ "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", "dev": true, + "license": "MIT", "dependencies": { "pkg-dir": "^4.2.0", "resolve-cwd": "^3.0.0" @@ -5590,7 +6141,8 @@ "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/is-async-function": { "version": "2.1.1", @@ -5731,6 +6283,7 @@ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -5740,6 +6293,7 @@ "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -5870,6 +6424,7 @@ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" }, @@ -5985,6 +6540,7 @@ "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=8" } @@ -5994,6 +6550,7 @@ "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "@babel/core": "^7.23.9", "@babel/parser": "^7.23.9", @@ -6006,10 +6563,11 @@ } }, "node_modules/istanbul-lib-instrument/node_modules/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -6022,6 +6580,7 @@ "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "istanbul-lib-coverage": "^3.0.0", "make-dir": "^4.0.0", @@ -6032,24 +6591,26 @@ } }, "node_modules/istanbul-lib-source-maps": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", - "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0", - "source-map": "^0.6.1" + "istanbul-lib-coverage": "^3.0.0" }, "engines": { "node": ">=10" } }, "node_modules/istanbul-reports": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", - "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "html-escaper": "^2.0.0", "istanbul-lib-report": "^3.0.0" @@ -6058,22 +6619,39 @@ "node": ">=8" } }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/jest": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", - "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-30.3.0.tgz", + "integrity": "sha512-AkXIIFcaazymvey2i/+F94XRnM6TsVLZDhBMLsd1Sf/W0wzsvvpjeyUrCZD6HGG4SDYPgDJDBKeiJTBb10WzMg==", "dev": true, + "license": "MIT", "dependencies": { - "@jest/core": "^29.7.0", - "@jest/types": "^29.6.3", - "import-local": "^3.0.2", - "jest-cli": "^29.7.0" + "@jest/core": "30.3.0", + "@jest/types": "30.3.0", + "import-local": "^3.2.0", + "jest-cli": "30.3.0" }, "bin": { "jest": "bin/jest.js" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" }, "peerDependencies": { "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" @@ -6085,249 +6663,364 @@ } }, "node_modules/jest-changed-files": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", - "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.3.0.tgz", + "integrity": "sha512-B/7Cny6cV5At6M25EWDgf9S617lHivamL8vl6KEpJqkStauzcG4e+WPfDgMMF+H4FVH4A2PLRyvgDJan4441QA==", "dev": true, + "license": "MIT", "dependencies": { - "execa": "^5.0.0", - "jest-util": "^29.7.0", + "execa": "^5.1.1", + "jest-util": "30.3.0", "p-limit": "^3.1.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-circus": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", - "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.3.0.tgz", + "integrity": "sha512-PyXq5szeSfR/4f1lYqCmmQjh0vqDkURUYi9N6whnHjlRz4IUQfMcXkGLeEoiJtxtyPqgUaUUfyQlApXWBSN1RA==", "dev": true, + "license": "MIT", "dependencies": { - "@jest/environment": "^29.7.0", - "@jest/expect": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/types": "^29.6.3", + "@jest/environment": "30.3.0", + "@jest/expect": "30.3.0", + "@jest/test-result": "30.3.0", + "@jest/types": "30.3.0", "@types/node": "*", - "chalk": "^4.0.0", + "chalk": "^4.1.2", "co": "^4.6.0", - "dedent": "^1.0.0", - "is-generator-fn": "^2.0.0", - "jest-each": "^29.7.0", - "jest-matcher-utils": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-runtime": "^29.7.0", - "jest-snapshot": "^29.7.0", - "jest-util": "^29.7.0", + "dedent": "^1.6.0", + "is-generator-fn": "^2.1.0", + "jest-each": "30.3.0", + "jest-matcher-utils": "30.3.0", + "jest-message-util": "30.3.0", + "jest-runtime": "30.3.0", + "jest-snapshot": "30.3.0", + "jest-util": "30.3.0", "p-limit": "^3.1.0", - "pretty-format": "^29.7.0", - "pure-rand": "^6.0.0", + "pretty-format": "30.3.0", + "pure-rand": "^7.0.0", "slash": "^3.0.0", - "stack-utils": "^2.0.3" + "stack-utils": "^2.0.6" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/jest-config": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", - "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "node_modules/jest-cli": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.3.0.tgz", + "integrity": "sha512-l6Tqx+j1fDXJEW5bqYykDQQ7mQg+9mhWXtnj+tQZrTWYHyHoi6Be8HPumDSA+UiX2/2buEgjA58iJzdj146uCw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/core": "^7.11.6", - "@jest/test-sequencer": "^29.7.0", - "@jest/types": "^29.6.3", - "babel-jest": "^29.7.0", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "deepmerge": "^4.2.2", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "jest-circus": "^29.7.0", - "jest-environment-node": "^29.7.0", - "jest-get-type": "^29.6.3", - "jest-regex-util": "^29.6.3", - "jest-resolve": "^29.7.0", - "jest-runner": "^29.7.0", - "jest-util": "^29.7.0", - "jest-validate": "^29.7.0", - "micromatch": "^4.0.4", + "@jest/core": "30.3.0", + "@jest/test-result": "30.3.0", + "@jest/types": "30.3.0", + "chalk": "^4.1.2", + "exit-x": "^0.2.2", + "import-local": "^3.2.0", + "jest-config": "30.3.0", + "jest-util": "30.3.0", + "jest-validate": "30.3.0", + "yargs": "^17.7.2" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.3.0.tgz", + "integrity": "sha512-WPMAkMAtNDY9P/oKObtsRG/6KTrhtgPJoBTmk20uDn4Uy6/3EJnnaZJre/FMT1KVRx8cve1r7/FlMIOfRVWL4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@jest/get-type": "30.1.0", + "@jest/pattern": "30.0.1", + "@jest/test-sequencer": "30.3.0", + "@jest/types": "30.3.0", + "babel-jest": "30.3.0", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "deepmerge": "^4.3.1", + "glob": "^10.5.0", + "graceful-fs": "^4.2.11", + "jest-circus": "30.3.0", + "jest-docblock": "30.2.0", + "jest-environment-node": "30.3.0", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.3.0", + "jest-runner": "30.3.0", + "jest-util": "30.3.0", + "jest-validate": "30.3.0", "parse-json": "^5.2.0", - "pretty-format": "^29.7.0", + "pretty-format": "30.3.0", "slash": "^3.0.0", "strip-json-comments": "^3.1.1" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" }, "peerDependencies": { "@types/node": "*", + "esbuild-register": ">=3.4.0", "ts-node": ">=9.0.0" }, "peerDependenciesMeta": { "@types/node": { "optional": true }, + "esbuild-register": { + "optional": true + }, "ts-node": { "optional": true } } }, + "node_modules/jest-config/node_modules/brace-expansion": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/jest-config/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest-config/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/jest-diff": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", - "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.3.0.tgz", + "integrity": "sha512-n3q4PDQjS4LrKxfWB3Z5KNk1XjXtZTBwQp71OP0Jo03Z6V60x++K5L8k6ZrW8MY8pOFylZvHM0zsjS1RqlHJZQ==", "dev": true, + "license": "MIT", "dependencies": { - "chalk": "^4.0.0", - "diff-sequences": "^29.6.3", - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" + "@jest/diff-sequences": "30.3.0", + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "pretty-format": "30.3.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-docblock": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", - "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-30.2.0.tgz", + "integrity": "sha512-tR/FFgZKS1CXluOQzZvNH3+0z9jXr3ldGSD8bhyuxvlVUwbeLOGynkunvlTMxchC5urrKndYiwCFC0DLVjpOCA==", "dev": true, + "license": "MIT", "dependencies": { - "detect-newline": "^3.0.0" + "detect-newline": "^3.1.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-each": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", - "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.3.0.tgz", + "integrity": "sha512-V8eMndg/aZ+3LnCJgSm13IxS5XSBM22QSZc9BtPK8Dek6pm+hfUNfwBdvsB3d342bo1q7wnSkC38zjX259qZNA==", "dev": true, + "license": "MIT", "dependencies": { - "@jest/types": "^29.6.3", - "chalk": "^4.0.0", - "jest-get-type": "^29.6.3", - "jest-util": "^29.7.0", - "pretty-format": "^29.7.0" + "@jest/get-type": "30.1.0", + "@jest/types": "30.3.0", + "chalk": "^4.1.2", + "jest-util": "30.3.0", + "pretty-format": "30.3.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-environment-node": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", - "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.3.0.tgz", + "integrity": "sha512-4i6HItw/JSiJVsC5q0hnKIe/hbYfZLVG9YJ/0pU9Hz2n/9qZe3Rhn5s5CUZA5ORZlcdT/vmAXRMyONXJwPrmYQ==", "dev": true, + "license": "MIT", "dependencies": { - "@jest/environment": "^29.7.0", - "@jest/fake-timers": "^29.7.0", - "@jest/types": "^29.6.3", + "@jest/environment": "30.3.0", + "@jest/fake-timers": "30.3.0", + "@jest/types": "30.3.0", "@types/node": "*", - "jest-mock": "^29.7.0", - "jest-util": "^29.7.0" + "jest-mock": "30.3.0", + "jest-util": "30.3.0", + "jest-validate": "30.3.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-get-type": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", - "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", - "dev": true, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-haste-map": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", - "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.3.0.tgz", + "integrity": "sha512-mMi2oqG4KRU0R9QEtscl87JzMXfUhbKaFqOxmjb2CKcbHcUGFrJCBWHmnTiUqi6JcnzoBlO4rWfpdl2k/RfLCA==", "dev": true, + "license": "MIT", "dependencies": { - "@jest/types": "^29.6.3", - "@types/graceful-fs": "^4.1.3", + "@jest/types": "30.3.0", "@types/node": "*", - "anymatch": "^3.0.3", - "fb-watchman": "^2.0.0", - "graceful-fs": "^4.2.9", - "jest-regex-util": "^29.6.3", - "jest-util": "^29.7.0", - "jest-worker": "^29.7.0", - "micromatch": "^4.0.4", + "anymatch": "^3.1.3", + "fb-watchman": "^2.0.2", + "graceful-fs": "^4.2.11", + "jest-regex-util": "30.0.1", + "jest-util": "30.3.0", + "jest-worker": "30.3.0", + "picomatch": "^4.0.3", "walker": "^1.0.8" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" }, "optionalDependencies": { - "fsevents": "^2.3.2" + "fsevents": "^2.3.3" + } + }, + "node_modules/jest-haste-map/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/jest-leak-detector": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", - "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.3.0.tgz", + "integrity": "sha512-cuKmUUGIjfXZAiGJ7TbEMx0bcqNdPPI6P1V+7aF+m/FUJqFDxkFR4JqkTu8ZOiU5AaX/x0hZ20KaaIPXQzbMGQ==", "dev": true, + "license": "MIT", "dependencies": { - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" + "@jest/get-type": "30.1.0", + "pretty-format": "30.3.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-matcher-utils": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", - "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.3.0.tgz", + "integrity": "sha512-HEtc9uFQgaUHkC7nLSlQL3Tph4Pjxt/yiPvkIrrDCt9jhoLIgxaubo1G+CFOnmHYMxHwwdaSN7mkIFs6ZK8OhA==", "dev": true, + "license": "MIT", "dependencies": { - "chalk": "^4.0.0", - "jest-diff": "^29.7.0", - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "jest-diff": "30.3.0", + "pretty-format": "30.3.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-message-util": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", - "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.3.0.tgz", + "integrity": "sha512-Z/j4Bo+4ySJ+JPJN3b2Qbl9hDq3VrXmnjjGEWD/x0BCXeOXPTV1iZYYzl2X8c1MaCOL+ewMyNBcm88sboE6YWw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.12.13", - "@jest/types": "^29.6.3", - "@types/stack-utils": "^2.0.0", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "micromatch": "^4.0.4", - "pretty-format": "^29.7.0", + "@babel/code-frame": "^7.27.1", + "@jest/types": "30.3.0", + "@types/stack-utils": "^2.0.3", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.3", + "pretty-format": "30.3.0", "slash": "^3.0.0", - "stack-utils": "^2.0.3" + "stack-utils": "^2.0.6" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-message-util/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/jest-mock": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", - "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.3.0.tgz", + "integrity": "sha512-OTzICK8CpE+t4ndhKrwlIdbM6Pn8j00lvmSmq5ejiO+KxukbLjgOflKWMn3KE34EZdQm5RqTuKj+5RIEniYhog==", "dev": true, + "license": "MIT", "dependencies": { - "@jest/types": "^29.6.3", + "@jest/types": "30.3.0", "@types/node": "*", - "jest-util": "^29.7.0" + "jest-util": "30.3.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-pnp-resolver": { @@ -6335,6 +7028,7 @@ "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" }, @@ -6348,110 +7042,163 @@ } }, "node_modules/jest-regex-util": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", - "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", + "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", "dev": true, + "license": "MIT", "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-resolve": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", - "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.3.0.tgz", + "integrity": "sha512-NRtTAHQlpd15F9rUR36jqwelbrDV/dY4vzNte3S2kxCKUJRYNd5/6nTSbYiak1VX5g8IoFF23Uj5TURkUW8O5g==", "dev": true, + "license": "MIT", "dependencies": { - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "jest-pnp-resolver": "^1.2.2", - "jest-util": "^29.7.0", - "jest-validate": "^29.7.0", - "resolve": "^1.20.0", - "resolve.exports": "^2.0.0", - "slash": "^3.0.0" + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.3.0", + "jest-pnp-resolver": "^1.2.3", + "jest-util": "30.3.0", + "jest-validate": "30.3.0", + "slash": "^3.0.0", + "unrs-resolver": "^1.7.11" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-resolve-dependencies": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", - "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.3.0.tgz", + "integrity": "sha512-9ev8s3YN6Hsyz9LV75XUwkCVFlwPbaFn6Wp75qnI0wzAINYWY8Fb3+6y59Rwd3QaS3kKXffHXsZMziMavfz/nw==", "dev": true, + "license": "MIT", "dependencies": { - "jest-regex-util": "^29.6.3", - "jest-snapshot": "^29.7.0" + "jest-regex-util": "30.0.1", + "jest-snapshot": "30.3.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-runner": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", - "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.3.0.tgz", + "integrity": "sha512-gDv6C9LGKWDPLia9TSzZwf4h3kMQCqyTpq+95PODnTRDO0g9os48XIYYkS6D236vjpBir2fF63YmJFtqkS5Duw==", "dev": true, + "license": "MIT", "dependencies": { - "@jest/console": "^29.7.0", - "@jest/environment": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", + "@jest/console": "30.3.0", + "@jest/environment": "30.3.0", + "@jest/test-result": "30.3.0", + "@jest/transform": "30.3.0", + "@jest/types": "30.3.0", "@types/node": "*", - "chalk": "^4.0.0", + "chalk": "^4.1.2", "emittery": "^0.13.1", - "graceful-fs": "^4.2.9", - "jest-docblock": "^29.7.0", - "jest-environment-node": "^29.7.0", - "jest-haste-map": "^29.7.0", - "jest-leak-detector": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-resolve": "^29.7.0", - "jest-runtime": "^29.7.0", - "jest-util": "^29.7.0", - "jest-watcher": "^29.7.0", - "jest-worker": "^29.7.0", + "exit-x": "^0.2.2", + "graceful-fs": "^4.2.11", + "jest-docblock": "30.2.0", + "jest-environment-node": "30.3.0", + "jest-haste-map": "30.3.0", + "jest-leak-detector": "30.3.0", + "jest-message-util": "30.3.0", + "jest-resolve": "30.3.0", + "jest-runtime": "30.3.0", + "jest-util": "30.3.0", + "jest-watcher": "30.3.0", + "jest-worker": "30.3.0", "p-limit": "^3.1.0", "source-map-support": "0.5.13" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-runtime": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", - "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", - "dev": true, - "dependencies": { - "@jest/environment": "^29.7.0", - "@jest/fake-timers": "^29.7.0", - "@jest/globals": "^29.7.0", - "@jest/source-map": "^29.6.3", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.3.0.tgz", + "integrity": "sha512-CgC+hIBJbuh78HEffkhNKcbXAytQViplcl8xupqeIWyKQF50kCQA8J7GeJCkjisC6hpnC9Muf8jV5RdtdFbGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.3.0", + "@jest/fake-timers": "30.3.0", + "@jest/globals": "30.3.0", + "@jest/source-map": "30.0.1", + "@jest/test-result": "30.3.0", + "@jest/transform": "30.3.0", + "@jest/types": "30.3.0", "@types/node": "*", - "chalk": "^4.0.0", - "cjs-module-lexer": "^1.0.0", - "collect-v8-coverage": "^1.0.0", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-mock": "^29.7.0", - "jest-regex-util": "^29.6.3", - "jest-resolve": "^29.7.0", - "jest-snapshot": "^29.7.0", - "jest-util": "^29.7.0", + "chalk": "^4.1.2", + "cjs-module-lexer": "^2.1.0", + "collect-v8-coverage": "^1.0.2", + "glob": "^10.5.0", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.3.0", + "jest-message-util": "30.3.0", + "jest-mock": "30.3.0", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.3.0", + "jest-snapshot": "30.3.0", + "jest-util": "30.3.0", "slash": "^3.0.0", "strip-bom": "^4.0.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-runtime/node_modules/brace-expansion": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/jest-runtime/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest-runtime/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/jest-runtime/node_modules/strip-bom": { @@ -6459,46 +7206,63 @@ "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/jest-snapshot": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", - "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", - "dev": true, - "dependencies": { - "@babel/core": "^7.11.6", - "@babel/generator": "^7.7.2", - "@babel/plugin-syntax-jsx": "^7.7.2", - "@babel/plugin-syntax-typescript": "^7.7.2", - "@babel/types": "^7.3.3", - "@jest/expect-utils": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "babel-preset-current-node-syntax": "^1.0.0", - "chalk": "^4.0.0", - "expect": "^29.7.0", - "graceful-fs": "^4.2.9", - "jest-diff": "^29.7.0", - "jest-get-type": "^29.6.3", - "jest-matcher-utils": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0", - "natural-compare": "^1.4.0", - "pretty-format": "^29.7.0", - "semver": "^7.5.3" + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.3.0.tgz", + "integrity": "sha512-f14c7atpb4O2DeNhwcvS810Y63wEn8O1HqK/luJ4F6M4NjvxmAKQwBUWjbExUtMxWJQ0wVgmCKymeJK6NZMnfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@babel/generator": "^7.27.5", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.27.1", + "@babel/types": "^7.27.3", + "@jest/expect-utils": "30.3.0", + "@jest/get-type": "30.1.0", + "@jest/snapshot-utils": "30.3.0", + "@jest/transform": "30.3.0", + "@jest/types": "30.3.0", + "babel-preset-current-node-syntax": "^1.2.0", + "chalk": "^4.1.2", + "expect": "30.3.0", + "graceful-fs": "^4.2.11", + "jest-diff": "30.3.0", + "jest-matcher-utils": "30.3.0", + "jest-message-util": "30.3.0", + "jest-util": "30.3.0", + "pretty-format": "30.3.0", + "semver": "^7.7.2", + "synckit": "^0.11.8" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/@pkgr/core": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", + "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" } }, "node_modules/jest-snapshot/node_modules/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -6506,38 +7270,69 @@ "node": ">=10" } }, + "node_modules/jest-snapshot/node_modules/synckit": { + "version": "0.11.12", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.12.tgz", + "integrity": "sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.2.9" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, "node_modules/jest-util": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", - "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.3.0.tgz", + "integrity": "sha512-/jZDa00a3Sz7rdyu55NLrQCIrbyIkbBxareejQI315f/i8HjYN+ZWsDLLpoQSiUIEIyZF/R8fDg3BmB8AtHttg==", "dev": true, + "license": "MIT", "dependencies": { - "@jest/types": "^29.6.3", + "@jest/types": "30.3.0", "@types/node": "*", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "graceful-fs": "^4.2.9", - "picomatch": "^2.2.3" + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.3" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-util/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/jest-validate": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", - "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.3.0.tgz", + "integrity": "sha512-I/xzC8h5G+SHCb2P2gWkJYrNiTbeL47KvKeW5EzplkyxzBRBw1ssSHlI/jXec0ukH2q7x2zAWQm7015iusg62Q==", "dev": true, + "license": "MIT", "dependencies": { - "@jest/types": "^29.6.3", - "camelcase": "^6.2.0", - "chalk": "^4.0.0", - "jest-get-type": "^29.6.3", + "@jest/get-type": "30.1.0", + "@jest/types": "30.3.0", + "camelcase": "^6.3.0", + "chalk": "^4.1.2", "leven": "^3.1.0", - "pretty-format": "^29.7.0" + "pretty-format": "30.3.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-validate/node_modules/camelcase": { @@ -6545,6 +7340,7 @@ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -6553,37 +7349,40 @@ } }, "node_modules/jest-watcher": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", - "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.3.0.tgz", + "integrity": "sha512-PJ1d9ThtTR8aMiBWUdcownq9mDdLXsQzJayTk4kmaBRHKvwNQn+ANveuhEBUyNI2hR1TVhvQ8D5kHubbzBHR/w==", "dev": true, + "license": "MIT", "dependencies": { - "@jest/test-result": "^29.7.0", - "@jest/types": "^29.6.3", + "@jest/test-result": "30.3.0", + "@jest/types": "30.3.0", "@types/node": "*", - "ansi-escapes": "^4.2.1", - "chalk": "^4.0.0", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", "emittery": "^0.13.1", - "jest-util": "^29.7.0", - "string-length": "^4.0.1" + "jest-util": "30.3.0", + "string-length": "^4.0.2" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-worker": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", - "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.3.0.tgz", + "integrity": "sha512-DrCKkaQwHexjRUFTmPzs7sHQe0TSj9nvDALKGdwmK5mW9v7j90BudWirKAJHt3QQ9Dhrg1F7DogPzhChppkJpQ==", "dev": true, + "license": "MIT", "dependencies": { "@types/node": "*", - "jest-util": "^29.7.0", + "@ungap/structured-clone": "^1.3.0", + "jest-util": "30.3.0", "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" + "supports-color": "^8.1.1" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-worker/node_modules/supports-color": { @@ -6591,6 +7390,7 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, + "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -6601,39 +7401,6 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/jest/node_modules/jest-cli": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", - "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", - "dev": true, - "dependencies": { - "@jest/core": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/types": "^29.6.3", - "chalk": "^4.0.0", - "create-jest": "^29.7.0", - "exit": "^0.1.2", - "import-local": "^3.0.2", - "jest-config": "^29.7.0", - "jest-util": "^29.7.0", - "jest-validate": "^29.7.0", - "yargs": "^17.3.1" - }, - "bin": { - "jest": "bin/jest.js" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -6674,7 +7441,8 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/json-schema-traverse": { "version": "0.4.1", @@ -6724,15 +7492,6 @@ "json-buffer": "3.0.1" } }, - "node_modules/kleur": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", - "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/language-subtag-registry": { "version": "0.3.23", "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", @@ -6756,6 +7515,7 @@ "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -6777,7 +7537,8 @@ "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/locate-path": { "version": "6.0.0", @@ -6851,6 +7612,7 @@ "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", "dev": true, + "license": "MIT", "dependencies": { "semver": "^7.5.3" }, @@ -6862,10 +7624,11 @@ } }, "node_modules/make-dir/node_modules/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -6878,6 +7641,7 @@ "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "tmpl": "1.0.5" } @@ -6895,7 +7659,8 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/merge2": { "version": "1.4.1", @@ -6924,6 +7689,7 @@ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -6974,6 +7740,22 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true }, + "node_modules/napi-postinstall": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", + "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==", + "dev": true, + "license": "MIT", + "bin": { + "napi-postinstall": "lib/cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/napi-postinstall" + } + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -6984,7 +7766,8 @@ "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/node-releases": { "version": "2.0.37", @@ -6998,6 +7781,7 @@ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -7007,6 +7791,7 @@ "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^3.0.0" }, @@ -7119,6 +7904,7 @@ "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", "dev": true, + "license": "MIT", "dependencies": { "mimic-fn": "^2.1.0" }, @@ -7198,10 +7984,18 @@ "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -7219,6 +8013,7 @@ "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", "dev": true, + "license": "MIT", "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", @@ -7265,6 +8060,30 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -7293,10 +8112,11 @@ } }, "node_modules/pirates": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", - "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", "dev": true, + "license": "MIT", "engines": { "node": ">= 6" } @@ -7306,6 +8126,7 @@ "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", "dev": true, + "license": "MIT", "dependencies": { "find-up": "^4.0.0" }, @@ -7318,6 +8139,7 @@ "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", "dev": true, + "license": "MIT", "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" @@ -7331,6 +8153,7 @@ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", "dev": true, + "license": "MIT", "dependencies": { "p-locate": "^4.1.0" }, @@ -7343,6 +8166,7 @@ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", "dev": true, + "license": "MIT", "dependencies": { "p-try": "^2.0.0" }, @@ -7358,6 +8182,7 @@ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", "dev": true, + "license": "MIT", "dependencies": { "p-limit": "^2.2.0" }, @@ -7412,17 +8237,18 @@ } }, "node_modules/pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz", + "integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==", "dev": true, + "license": "MIT", "dependencies": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/pretty-format/node_modules/ansi-styles": { @@ -7430,6 +8256,7 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -7437,19 +8264,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/prompts": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", - "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", - "dev": true, - "dependencies": { - "kleur": "^3.0.3", - "sisteransi": "^1.0.5" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -7460,9 +8274,9 @@ } }, "node_modules/pure-rand": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", - "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz", + "integrity": "sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==", "dev": true, "funding": [ { @@ -7473,7 +8287,8 @@ "type": "opencollective", "url": "https://opencollective.com/fast-check" } - ] + ], + "license": "MIT" }, "node_modules/queue-microtask": { "version": "1.2.3", @@ -7499,7 +8314,8 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", @@ -7606,6 +8422,7 @@ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -7637,6 +8454,7 @@ "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", "dev": true, + "license": "MIT", "dependencies": { "resolve-from": "^5.0.0" }, @@ -7649,6 +8467,7 @@ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -7662,15 +8481,6 @@ "node": ">=4" } }, - "node_modules/resolve.exports": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", - "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", - "dev": true, - "engines": { - "node": ">=10" - } - }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -7924,13 +8734,8 @@ "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true - }, - "node_modules/sisteransi": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", - "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/slash": { "version": "3.0.0", @@ -7946,6 +8751,7 @@ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } @@ -7955,6 +8761,7 @@ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", "dev": true, + "license": "MIT", "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -7964,13 +8771,15 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "dev": true + "dev": true, + "license": "BSD-3-Clause" }, "node_modules/stack-utils": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", "dev": true, + "license": "MIT", "dependencies": { "escape-string-regexp": "^2.0.0" }, @@ -7983,6 +8792,7 @@ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -7992,6 +8802,7 @@ "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", "dev": true, + "license": "MIT", "dependencies": { "char-regex": "^1.0.2", "strip-ansi": "^6.0.0" @@ -8005,6 +8816,23 @@ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -8014,11 +8842,19 @@ "node": ">=8" } }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, "node_modules/string-width/node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/string.prototype.includes": { "version": "2.0.1", @@ -8102,6 +8938,20 @@ "node": ">=8" } }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", @@ -8116,6 +8966,7 @@ "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -8202,6 +9053,7 @@ "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", "dev": true, + "license": "ISC", "dependencies": { "@istanbuljs/schema": "^0.1.2", "glob": "^7.1.4", @@ -8269,7 +9121,8 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", - "dev": true + "dev": true, + "license": "BSD-3-Clause" }, "node_modules/to-regex-range": { "version": "5.0.1", @@ -8360,6 +9213,7 @@ "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } @@ -8785,10 +9639,11 @@ } }, "node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "dev": true + "version": "7.19.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", + "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", + "dev": true, + "license": "MIT" }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.1", @@ -8834,6 +9689,41 @@ "node": ">=4" } }, + "node_modules/unrs-resolver": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", + "integrity": "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "napi-postinstall": "^0.3.0" + }, + "funding": { + "url": "https://opencollective.com/unrs-resolver" + }, + "optionalDependencies": { + "@unrs/resolver-binding-android-arm-eabi": "1.11.1", + "@unrs/resolver-binding-android-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-x64": "1.11.1", + "@unrs/resolver-binding-freebsd-x64": "1.11.1", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", + "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-musl": "1.11.1", + "@unrs/resolver-binding-wasm32-wasi": "1.11.1", + "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", + "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", + "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" + } + }, "node_modules/update-browserslist-db": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", @@ -8879,6 +9769,7 @@ "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", "dev": true, + "license": "ISC", "dependencies": { "@jridgewell/trace-mapping": "^0.3.12", "@types/istanbul-lib-coverage": "^2.0.1", @@ -8893,6 +9784,7 @@ "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", "dev": true, + "license": "Apache-2.0", "dependencies": { "makeerror": "1.0.12" } @@ -9011,6 +9903,26 @@ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -9030,16 +9942,30 @@ "dev": true }, "node_modules/write-file-atomic": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", - "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", + "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", "dev": true, + "license": "ISC", "dependencies": { "imurmurhash": "^0.1.4", - "signal-exit": "^3.0.7" + "signal-exit": "^4.0.1" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/write-file-atomic/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/y18n": { @@ -9047,6 +9973,7 @@ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "dev": true, + "license": "ISC", "engines": { "node": ">=10" } @@ -9064,6 +9991,7 @@ "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", "dev": true, + "license": "MIT", "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", @@ -9082,6 +10010,7 @@ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", "dev": true, + "license": "ISC", "engines": { "node": ">=12" } diff --git a/.github/actions/cache/package.json b/.github/actions/cache/package.json index aa2035ba92ff..02a3bddf36fe 100644 --- a/.github/actions/cache/package.json +++ b/.github/actions/cache/package.json @@ -67,7 +67,7 @@ "eslint-plugin-github": "^6.0.0", "eslint-plugin-jest": "^27.9.0", "fs": "0.0.1-security", - "jest": "^29.7.0", + "jest": "^30.3.0", "prettier": "^3.8.3" }, "license": "Apache-2.0" diff --git a/.github/actions/common/constants.py b/.github/actions/common/constants.py index 9c544c1a2214..ccacc8059457 100644 --- a/.github/actions/common/constants.py +++ b/.github/actions/common/constants.py @@ -21,6 +21,7 @@ class EventType(Enum): 'public_windows_vs2019_release', 'public_windows_vs2019_debug', 'public_windows_vs2022_release', + 'public_windows_vs2022_levelzero_release', 'public_windows_vs2022_debug', 'public_manylinux_2_28_x86_64_release', 'public_macos_x86_64_release', diff --git a/.github/actions/coverage_toolkit/action.yml b/.github/actions/coverage_toolkit/action.yml new file mode 100644 index 000000000000..9f31a2476915 --- /dev/null +++ b/.github/actions/coverage_toolkit/action.yml @@ -0,0 +1,530 @@ +name: Coverage Toolkit +description: Run OpenVINO coverage toolkit script steps from reusable workflows. + +inputs: + command: + description: One of install-requirements, validate-config, run-suite, collect-native, collect-results, render-summary, merge-durations, resolve-uploads, resolve-upload-files. + required: true + workspace: + description: Repository workspace that owns the checked-out sources and coverage artifacts. + required: false + default: '' + config-dir: + description: Directory containing tests_cpp.yml, tests_python.yml, and tests_js.yml. + required: false + default: '' + suite: + description: 'Coverage suite. run-suite supports cpp, python, and js; reporting commands accept any suite defined in .github/coverage.' + required: false + default: '' + profile: + description: Test profile, usually cpu or gpu. + required: false + default: cpu + lane: + description: Coverage lane name used in summaries and artifact metadata. + required: false + default: '' + test-names: + description: Comma-separated test scenario names selected for the suite. + required: false + default: '' + artifact-name: + description: Coverage artifact name for collect-results. Defaults to the suite reporting artifact_name_template. + required: false + default: '' + artifact-dir: + description: Directory where collect-results writes the artifact payload. Defaults to RUNNER_TEMP/artifact-name. + required: false + default: '' + extra-cpp-coverage-dirs: + description: Additional native coverage build directories, separated by the OS path separator. + required: false + default: '' + selected-lanes: + description: Active lanes for render-summary. + required: false + default: '' + selection: + description: Human-readable selection label for render-summary. Defaults to selected-lanes. + required: false + default: '' + summary-file: + description: Summary file path for render-summary. Defaults to GITHUB_STEP_SUMMARY. + required: false + default: '' + duration-output: + description: Merged duration CSV path for merge-durations. + required: false + default: '' + uploads-output-file: + description: Output file for resolve-uploads. Defaults to GITHUB_OUTPUT. + required: false + default: '' + install-requirements: + description: Install coverage toolkit Python requirements before running the command. + required: false + default: 'false' + build-type: + description: CMake build type passed to coverage.py. + required: false + default: '' + python-package: + description: Python package name used for coverage source detection. + required: false + default: '' + python-installed-package: + description: Installed Python package to import when it differs from python-package. + required: false + default: '' + python-source-dir: + description: Workspace-relative or absolute Python source directory for coverage XML rewriting. + required: false + default: '' + python-path-alias: + description: Coverage.py [paths] alias and package prefix stripped from Python XML filenames. + required: false + default: '' + python-coverage-omit: + description: Newline-separated Python coverage omit patterns. + required: false + default: '' + python-tests-dir: + description: Workspace-relative or absolute Python tests directory exposed as TESTS_DIR. + required: false + default: '' + python-layer-tests-dir: + description: Workspace-relative or absolute layer tests directory exposed as WORKSPACE_LAYER_TESTS_DIR. + required: false + default: '' + python-runtime-extra-paths: + description: Extra runtime library paths for Python tests, separated by the OS path separator or newlines. + required: false + default: '' + python-wheel-lib-dir: + description: Runtime library path for installed OpenVINO wheel libraries. + required: false + default: '' + js-dir: + description: Workspace-relative or absolute JavaScript package directory. + required: false + default: '' + model-path: + description: Workspace-relative or absolute C++ test model path used for __MODEL_PATH__ replacement. + required: false + default: '' + artifact-group: + description: Artifact group directory under workspace/artifacts for resolve-upload-files. + required: false + default: '' + artifact-names-json: + description: JSON array of artifact names for resolve-upload-files. + required: false + default: '' + upload-key: + description: Dynamic Codecov upload key for resolve-upload-files. + required: false + default: '' + upload-file: + description: Coverage file name inside each downloaded artifact for resolve-upload-files. + required: false + default: '' +outputs: + dir: + description: Artifact directory produced by collect-results. + value: ${{ steps.collect_results.outputs.dir }} + artifact-dir: + description: Artifact directory produced by collect-results. + value: ${{ steps.collect_results.outputs['artifact-dir'] }} + artifact-name: + description: Artifact name produced by collect-results. + value: ${{ steps.collect_results.outputs['artifact-name'] }} + upload-files-json: + description: JSON object mapping Codecov upload keys to resolved file paths. + value: ${{ steps.resolve_uploads.outputs['upload-files-json'] || steps.resolve_upload_files.outputs['upload-files-json'] }} + upload-matrix-json: + description: JSON array used as the dynamic Codecov upload matrix. + value: ${{ steps.resolve_uploads.outputs['upload-matrix-json'] }} + files: + description: Comma-separated coverage files resolved by resolve-upload-files. + value: ${{ steps.resolve_upload_files.outputs.files }} + +runs: + using: composite + steps: + - name: Prepare coverage toolkit environment + id: prepare + shell: bash + env: + ACTION_PATH: ${{ github.action_path }} + INPUT_COMMAND: ${{ inputs.command }} + INPUT_WORKSPACE: ${{ inputs.workspace }} + INPUT_CONFIG_DIR: ${{ inputs.config-dir }} + INPUT_SUITE: ${{ inputs.suite }} + INPUT_PROFILE: ${{ inputs.profile }} + INPUT_LANE: ${{ inputs.lane }} + INPUT_TEST_NAMES: ${{ inputs.test-names }} + INPUT_ARTIFACT_NAME: ${{ inputs.artifact-name }} + INPUT_ARTIFACT_DIR: ${{ inputs.artifact-dir }} + INPUT_EXTRA_CPP_COVERAGE_DIRS: ${{ inputs.extra-cpp-coverage-dirs }} + INPUT_SELECTED_LANES: ${{ inputs.selected-lanes }} + INPUT_SELECTION: ${{ inputs.selection }} + INPUT_SUMMARY_FILE: ${{ inputs.summary-file }} + INPUT_DURATION_OUTPUT: ${{ inputs.duration-output }} + INPUT_UPLOADS_OUTPUT_FILE: ${{ inputs.uploads-output-file }} + INPUT_INSTALL_REQUIREMENTS: ${{ inputs.install-requirements }} + INPUT_BUILD_TYPE: ${{ inputs.build-type }} + INPUT_PYTHON_PACKAGE: ${{ inputs.python-package }} + INPUT_PYTHON_INSTALLED_PACKAGE: ${{ inputs.python-installed-package }} + INPUT_PYTHON_SOURCE_DIR: ${{ inputs.python-source-dir }} + INPUT_PYTHON_PATH_ALIAS: ${{ inputs.python-path-alias }} + INPUT_PYTHON_COVERAGE_OMIT: ${{ inputs.python-coverage-omit }} + INPUT_PYTHON_TESTS_DIR: ${{ inputs.python-tests-dir }} + INPUT_PYTHON_LAYER_TESTS_DIR: ${{ inputs.python-layer-tests-dir }} + INPUT_PYTHON_RUNTIME_EXTRA_PATHS: ${{ inputs.python-runtime-extra-paths }} + INPUT_PYTHON_WHEEL_LIB_DIR: ${{ inputs.python-wheel-lib-dir }} + INPUT_JS_DIR: ${{ inputs.js-dir }} + INPUT_MODEL_PATH: ${{ inputs.model-path }} + INPUT_ARTIFACT_GROUP: ${{ inputs.artifact-group }} + INPUT_ARTIFACT_NAMES_JSON: ${{ inputs.artifact-names-json }} + INPUT_UPLOAD_KEY: ${{ inputs.upload-key }} + INPUT_UPLOAD_FILE: ${{ inputs.upload-file }} + run: | + set -euo pipefail + + env_file="$(mktemp "${RUNNER_TEMP:-/tmp}/coverage-toolkit-env.XXXXXX")" + scripts_dir="${ACTION_PATH}" + coverage_py="${scripts_dir}/coverage.py" + reports_py="${scripts_dir}/ci_reports.py" + requirements_file="${scripts_dir}/requirements-ci.txt" + + workspace="${INPUT_WORKSPACE:-${GITHUB_WORKSPACE}}" + profile="${INPUT_PROFILE:-cpu}" + + write_var() { + printf '%s=' "$1" >> "${env_file}" + printf '%q\n' "$2" >> "${env_file}" + } + + write_export() { + printf 'export %s=' "$1" >> "${env_file}" + printf '%q\n' "$2" >> "${env_file}" + } + + write_array() { + local name="$1" + shift + printf '%s=(' "${name}" >> "${env_file}" + local value + for value in "$@"; do + printf ' %q' "${value}" >> "${env_file}" + done + printf ' )\n' >> "${env_file}" + } + + write_var scripts_dir "${scripts_dir}" + write_var coverage_py "${coverage_py}" + write_var reports_py "${reports_py}" + write_var requirements_file "${requirements_file}" + write_var workspace "${workspace}" + write_var profile "${profile}" + write_var input_suite "${INPUT_SUITE}" + write_var input_lane "${INPUT_LANE}" + write_var input_test_names "${INPUT_TEST_NAMES}" + write_var input_artifact_name "${INPUT_ARTIFACT_NAME}" + write_var input_artifact_dir "${INPUT_ARTIFACT_DIR}" + write_var input_selected_lanes "${INPUT_SELECTED_LANES}" + write_var input_selection "${INPUT_SELECTION}" + write_var input_summary_file "${INPUT_SUMMARY_FILE}" + write_var input_duration_output "${INPUT_DURATION_OUTPUT}" + write_var input_uploads_output_file "${INPUT_UPLOADS_OUTPUT_FILE}" + write_var input_artifact_group "${INPUT_ARTIFACT_GROUP}" + write_var input_artifact_names_json "${INPUT_ARTIFACT_NAMES_JSON}" + write_var input_upload_key "${INPUT_UPLOAD_KEY}" + write_var input_upload_file "${INPUT_UPLOAD_FILE}" + + write_export OV_WORKSPACE "${workspace}" + write_export TEST_PROFILE "${profile}" + + if [[ -n "${INPUT_CONFIG_DIR}" ]]; then + write_export COVERAGE_CONFIG_DIR "${INPUT_CONFIG_DIR}" + fi + if [[ -n "${INPUT_BUILD_TYPE}" ]]; then + write_export CMAKE_BUILD_TYPE "${INPUT_BUILD_TYPE}" + fi + if [[ -n "${INPUT_EXTRA_CPP_COVERAGE_DIRS}" ]]; then + write_export EXTRA_CPP_COVERAGE_DIRS "${INPUT_EXTRA_CPP_COVERAGE_DIRS}" + fi + if [[ -n "${INPUT_PYTHON_PACKAGE}" ]]; then + write_export COVERAGE_PYTHON_PACKAGE "${INPUT_PYTHON_PACKAGE}" + fi + if [[ -n "${INPUT_PYTHON_INSTALLED_PACKAGE}" ]]; then + write_export COVERAGE_PYTHON_INSTALLED_PACKAGE "${INPUT_PYTHON_INSTALLED_PACKAGE}" + fi + if [[ -n "${INPUT_PYTHON_SOURCE_DIR}" ]]; then + write_export COVERAGE_PYTHON_SOURCE_DIR "${INPUT_PYTHON_SOURCE_DIR}" + fi + if [[ -n "${INPUT_PYTHON_PATH_ALIAS}" ]]; then + write_export COVERAGE_PYTHON_PATH_ALIAS "${INPUT_PYTHON_PATH_ALIAS}" + fi + if [[ -n "${INPUT_PYTHON_COVERAGE_OMIT}" ]]; then + write_export COVERAGE_PYTHON_OMIT "${INPUT_PYTHON_COVERAGE_OMIT}" + fi + if [[ -n "${INPUT_PYTHON_TESTS_DIR}" ]]; then + write_export COVERAGE_PYTHON_TESTS_DIR "${INPUT_PYTHON_TESTS_DIR}" + fi + if [[ -n "${INPUT_PYTHON_LAYER_TESTS_DIR}" ]]; then + write_export COVERAGE_PYTHON_LAYER_TESTS_DIR "${INPUT_PYTHON_LAYER_TESTS_DIR}" + fi + if [[ -n "${INPUT_PYTHON_RUNTIME_EXTRA_PATHS}" ]]; then + write_export COVERAGE_PYTHON_RUNTIME_EXTRA_PATHS "${INPUT_PYTHON_RUNTIME_EXTRA_PATHS}" + fi + if [[ -n "${INPUT_PYTHON_WHEEL_LIB_DIR}" ]]; then + write_export COVERAGE_PYTHON_WHEEL_LIB_DIR "${INPUT_PYTHON_WHEEL_LIB_DIR}" + fi + if [[ -n "${INPUT_JS_DIR}" ]]; then + write_export JS_DIR "${INPUT_JS_DIR}" + fi + if [[ -n "${INPUT_MODEL_PATH}" ]]; then + write_export MODEL_PATH "${INPUT_MODEL_PATH}" + fi + + coverage_common=(--workspace "${workspace}" --profile "${profile}") + if [[ -n "${INPUT_CONFIG_DIR}" ]]; then + coverage_common+=(--config-dir "${INPUT_CONFIG_DIR}") + fi + if [[ -n "${INPUT_BUILD_TYPE}" ]]; then + coverage_common+=(--build-type "${INPUT_BUILD_TYPE}") + fi + + validate_common=(--workspace "${workspace}") + if [[ -n "${INPUT_CONFIG_DIR}" ]]; then + validate_common+=(--config-dir "${INPUT_CONFIG_DIR}") + fi + + reports_common=() + if [[ -n "${INPUT_CONFIG_DIR}" ]]; then + reports_common+=(--config-dir "${INPUT_CONFIG_DIR}") + fi + + selection_env="" + case "${INPUT_SUITE}" in + cpp) + selection_env="CXX_TEST_NAMES" + ;; + python) + selection_env="PY_TEST_NAMES" + ;; + js) + selection_env="JS_TEST_NAMES" + ;; + "") + ;; + esac + + if [[ -n "${INPUT_TEST_NAMES}" && -n "${selection_env}" ]]; then + write_export "${selection_env}" "${INPUT_TEST_NAMES}" + fi + + write_array coverage_common "${coverage_common[@]}" + write_array validate_common "${validate_common[@]}" + write_array reports_common "${reports_common[@]}" + + echo "env_file=${env_file}" >> "${GITHUB_OUTPUT}" + + - name: Reject unsupported run-suite suite + if: ${{ inputs.command == 'run-suite' && inputs.suite != '' && inputs.suite != 'cpp' && inputs.suite != 'python' && inputs.suite != 'js' }} + shell: bash + env: + INPUT_SUITE: ${{ inputs.suite }} + run: | + echo "Unsupported coverage suite for run-suite: ${INPUT_SUITE}" >&2 + exit 1 + + - name: Install coverage toolkit requirements + if: ${{ inputs.command == 'install-requirements' || inputs.install-requirements == 'true' }} + shell: bash + env: + COVERAGE_TOOLKIT_ENV_FILE: ${{ steps.prepare.outputs.env_file }} + run: | + set -euo pipefail + source "${COVERAGE_TOOLKIT_ENV_FILE}" + python3 -m pip install -r "${requirements_file}" + + - name: Validate coverage config + if: ${{ inputs.command == 'validate-config' }} + shell: bash + env: + COVERAGE_TOOLKIT_ENV_FILE: ${{ steps.prepare.outputs.env_file }} + run: | + set -euo pipefail + source "${COVERAGE_TOOLKIT_ENV_FILE}" + python3 "${coverage_py}" validate-config "${validate_common[@]}" + + - name: Require run-suite suite + if: ${{ inputs.command == 'run-suite' && inputs.suite == '' }} + shell: bash + run: | + echo "run-suite requires suite=cpp, suite=python, or suite=js" >&2 + exit 1 + + - name: Run suite (C++) + if: ${{ inputs.command == 'run-suite' && inputs.suite == 'cpp' }} + shell: bash + env: + COVERAGE_TOOLKIT_ENV_FILE: ${{ steps.prepare.outputs.env_file }} + run: | + set -euo pipefail + source "${COVERAGE_TOOLKIT_ENV_FILE}" + python3 "${coverage_py}" step "${coverage_common[@]}" run-cpp-tests + + - name: Run suite (Python) + if: ${{ inputs.command == 'run-suite' && inputs.suite == 'python' }} + shell: bash + env: + COVERAGE_TOOLKIT_ENV_FILE: ${{ steps.prepare.outputs.env_file }} + run: | + set -euo pipefail + source "${COVERAGE_TOOLKIT_ENV_FILE}" + python3 "${coverage_py}" step "${coverage_common[@]}" run-python-tests + + - name: Run suite (JavaScript) + if: ${{ inputs.command == 'run-suite' && inputs.suite == 'js' }} + shell: bash + env: + COVERAGE_TOOLKIT_ENV_FILE: ${{ steps.prepare.outputs.env_file }} + run: | + set -euo pipefail + source "${COVERAGE_TOOLKIT_ENV_FILE}" + python3 "${coverage_py}" step "${coverage_common[@]}" run-js-tests + + - name: Collect native coverage + if: ${{ inputs.command == 'collect-native' }} + shell: bash + env: + COVERAGE_TOOLKIT_ENV_FILE: ${{ steps.prepare.outputs.env_file }} + run: | + set -euo pipefail + source "${COVERAGE_TOOLKIT_ENV_FILE}" + python3 "${coverage_py}" step "${coverage_common[@]}" collect-cpp-coverage + + - name: Require collect-results suite + if: ${{ inputs.command == 'collect-results' && inputs.suite == '' }} + shell: bash + run: | + echo "collect-results requires suite" >&2 + exit 1 + + - name: Collect suite results + id: collect_results + if: ${{ inputs.command == 'collect-results' && inputs.suite != '' }} + shell: bash + env: + COVERAGE_TOOLKIT_ENV_FILE: ${{ steps.prepare.outputs.env_file }} + run: | + set -euo pipefail + source "${COVERAGE_TOOLKIT_ENV_FILE}" + lane="${input_lane:-${profile}}" + collect_args=( + --workspace "${workspace}" + --suite "${input_suite}" + --profile "${profile}" + --lane "${lane}" + --test-names "${input_test_names}" + --outputs-file "${GITHUB_OUTPUT}" + ) + if [[ -n "${input_artifact_name}" ]]; then + collect_args+=(--artifact-name "${input_artifact_name}") + fi + if [[ -n "${input_artifact_dir}" ]]; then + collect_args+=(--artifact-dir "${input_artifact_dir}") + fi + python3 "${reports_py}" collect-suite-results "${reports_common[@]}" \ + "${collect_args[@]}" + + - name: Render summary + if: ${{ inputs.command == 'render-summary' }} + shell: bash + env: + COVERAGE_TOOLKIT_ENV_FILE: ${{ steps.prepare.outputs.env_file }} + run: | + set -euo pipefail + source "${COVERAGE_TOOLKIT_ENV_FILE}" + summary_file="${input_summary_file:-${GITHUB_STEP_SUMMARY:-}}" + if [[ -z "${summary_file}" ]]; then + echo "render-summary requires summary-file or GITHUB_STEP_SUMMARY" >&2 + exit 1 + fi + selection="${input_selection:-${input_selected_lanes:-all}}" + selected_lanes="${input_selected_lanes:-${selection}}" + python3 "${reports_py}" render-summary "${reports_common[@]}" \ + --workspace "${workspace}" \ + --summary-file "${summary_file}" \ + --selection "${selection}" \ + --selected-lanes "${selected_lanes}" + + - name: Merge durations + if: ${{ inputs.command == 'merge-durations' }} + shell: bash + env: + COVERAGE_TOOLKIT_ENV_FILE: ${{ steps.prepare.outputs.env_file }} + run: | + set -euo pipefail + source "${COVERAGE_TOOLKIT_ENV_FILE}" + duration_output="${input_duration_output:-${workspace}/coverage-test-durations-all.csv}" + python3 "${reports_py}" merge-durations "${reports_common[@]}" \ + --workspace "${workspace}" \ + --output "${duration_output}" + + - name: Resolve uploads + id: resolve_uploads + if: ${{ inputs.command == 'resolve-uploads' }} + shell: bash + env: + COVERAGE_TOOLKIT_ENV_FILE: ${{ steps.prepare.outputs.env_file }} + run: | + set -euo pipefail + source "${COVERAGE_TOOLKIT_ENV_FILE}" + uploads_output_file="${input_uploads_output_file:-${GITHUB_OUTPUT:-}}" + if [[ -z "${uploads_output_file}" ]]; then + echo "resolve-uploads requires uploads-output-file or GITHUB_OUTPUT" >&2 + exit 1 + fi + python3 "${reports_py}" resolve-uploads "${reports_common[@]}" \ + --workspace "${workspace}" \ + --output-file "${uploads_output_file}" + + - name: Resolve upload files + id: resolve_upload_files + if: ${{ inputs.command == 'resolve-upload-files' }} + shell: bash + env: + COVERAGE_TOOLKIT_ENV_FILE: ${{ steps.prepare.outputs.env_file }} + run: | + set -euo pipefail + source "${COVERAGE_TOOLKIT_ENV_FILE}" + uploads_output_file="${input_uploads_output_file:-${GITHUB_OUTPUT:-}}" + if [[ -z "${uploads_output_file}" ]]; then + echo "resolve-upload-files requires uploads-output-file or GITHUB_OUTPUT" >&2 + exit 1 + fi + if [[ -z "${input_artifact_group}" || -z "${input_upload_file}" ]]; then + echo "resolve-upload-files requires artifact-group and upload-file" >&2 + exit 1 + fi + python3 "${reports_py}" resolve-upload-files \ + --workspace "${workspace}" \ + --artifact-group "${input_artifact_group}" \ + --artifact-names-json "${input_artifact_names_json}" \ + --upload-key "${input_upload_key}" \ + --file "${input_upload_file}" \ + --output-file "${uploads_output_file}" + + - name: Reject unsupported command + if: ${{ !contains(fromJSON('["install-requirements","validate-config","run-suite","collect-native","collect-results","render-summary","merge-durations","resolve-uploads","resolve-upload-files"]'), inputs.command) }} + shell: bash + env: + INPUT_COMMAND: ${{ inputs.command }} + run: | + echo "Unsupported coverage toolkit command: ${INPUT_COMMAND}" >&2 + exit 1 diff --git a/.github/actions/coverage_toolkit/ci_reports.py b/.github/actions/coverage_toolkit/ci_reports.py new file mode 100644 index 000000000000..e5393795312a --- /dev/null +++ b/.github/actions/coverage_toolkit/ci_reports.py @@ -0,0 +1,847 @@ +# Copyright (C) 2018-2026 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import argparse +import csv +from fnmatch import fnmatchcase +import json +import os +from pathlib import Path +import shutil +import sys +from typing import Any + + +SCRIPT_DIR = Path(__file__).resolve().parent +if str(SCRIPT_DIR) not in sys.path: + sys.path.insert(0, str(SCRIPT_DIR)) + +from coverage import get_config_dir, resolve_workspace_path + + +METADATA_FILE = "coverage-artifact-metadata.json" +REQUIRED_REPORTING_FIELDS = ( + "label", + "stats_file", + "duration_file", + "coverage_file", +) +DEFAULT_COUNTER_KEYS = { + "cpp": { + "total": "CXX_TESTS_TOTAL", + "executed": "CXX_TESTS_EXECUTED", + "passed": "CXX_TESTS_PASSED", + "failed": "CXX_TESTS_FAILED", + "skipped": "CXX_TESTS_SKIPPED", + "not_run": "CXX_TESTS_NOT_RUN", + }, + "python": { + "total": "PY_TESTS_TOTAL", + "executed": None, + "passed": "PY_TESTS_PASSED", + "failed": "PY_TESTS_FAILED", + "skipped": "PY_TESTS_SKIPPED", + "not_run": "PY_TESTS_NOT_RUN", + }, + "js": { + "total": "JS_TESTS_TOTAL", + "executed": None, + "passed": "JS_TESTS_PASSED", + "failed": "JS_TESTS_FAILED", + "skipped": "JS_TESTS_SKIPPED", + "not_run": "JS_TESTS_NOT_RUN", + }, +} + + +def _apply_common_env(args: argparse.Namespace) -> None: + config_dir = getattr(args, "config_dir", None) + if config_dir: + os.environ["COVERAGE_CONFIG_DIR"] = str(resolve_workspace_path(config_dir, workspace=args.workspace.resolve())) + + +def _load_yaml(path: Path) -> dict[str, Any]: + try: + import yaml # type: ignore[import-not-found] + except ModuleNotFoundError as exc: + raise RuntimeError("PyYAML is required to read coverage reporting configs") from exc + + data = yaml.safe_load(path.read_text(encoding="utf-8")) or {} + if not isinstance(data, dict): + raise ValueError(f"Invalid YAML root in {path}") + return data + + +def _as_list(value: Any, *, field_name: str, suite: str) -> list[Any]: + if value is None: + return [] + if not isinstance(value, list): + raise ValueError(f"Suite {suite!r} reporting field {field_name!r} must be a list") + return value + + +def _normalize_debug_dirs(value: Any, *, suite: str) -> list[dict[str, str]]: + debug_dirs: list[dict[str, str]] = [] + for item in _as_list(value, field_name="debug_dirs", suite=suite): + if not isinstance(item, dict): + raise ValueError(f"Suite {suite!r} has an invalid debug_dirs entry") + src = str(item.get("src", "")).strip() + dest = str(item.get("dest", "")).strip() + if not src or not dest: + raise ValueError(f"Suite {suite!r} debug_dirs entries require src and dest") + debug_dirs.append({"src": src, "dest": dest}) + return debug_dirs + + +def _normalize_uploads(value: Any, *, suite: str) -> list[dict[str, Any]]: + uploads: list[dict[str, Any]] = [] + for item in _as_list(value, field_name="uploads", suite=suite): + if not isinstance(item, dict): + raise ValueError(f"Suite {suite!r} has an invalid uploads entry") + upload = dict(item) + if not str(upload.get("file", "")).strip(): + raise ValueError(f"Suite {suite!r} uploads entries require file") + if not str(upload.get("flag", upload.get("flag_template", ""))).strip(): + raise ValueError(f"Suite {suite!r} uploads entries require flag or flag_template") + uploads.append(upload) + return uploads + + +def _normalize_suite_def(suite: str, reporting: dict[str, Any], *, config_file: Path | None = None) -> dict[str, Any]: + missing = [field for field in REQUIRED_REPORTING_FIELDS if not str(reporting.get(field, "")).strip()] + if missing: + raise ValueError(f"Suite {suite!r} reporting is missing required field(s): {', '.join(missing)}") + + counter_keys = DEFAULT_COUNTER_KEYS.get(suite) + if counter_keys is None: + raise ValueError(f"Suite {suite!r} does not have default reporting counter keys") + + suite_def: dict[str, Any] = { + "suite": suite, + "label": str(reporting["label"]).strip(), + "artifact_group": str(reporting.get("artifact_group", reporting.get("artifacts_dir", suite))).strip() or suite, + "artifact_name_template": str(reporting.get("artifact_name_template", "coverage-{suite}-{lane}")).strip() + or "coverage-{suite}-{lane}", + "stats_file": str(reporting["stats_file"]).strip(), + "duration_file": str(reporting["duration_file"]).strip(), + "coverage_file": str(reporting["coverage_file"]).strip(), + "selection_env": str(reporting.get("selection_env", "")).strip(), + "extra_files": [str(item) for item in _as_list(reporting.get("extra_files"), field_name="extra_files", suite=suite)], + "debug_dirs": _normalize_debug_dirs(reporting.get("debug_dirs"), suite=suite), + "uploads": _normalize_uploads(reporting.get("uploads"), suite=suite), + "total_key": str(counter_keys["total"]), + "executed_key": str(counter_keys["executed"]) if counter_keys["executed"] else None, + "passed_key": str(counter_keys["passed"]), + "failed_key": str(counter_keys["failed"]), + "skipped_key": str(counter_keys["skipped"]), + "not_run_key": str(counter_keys["not_run"]), + } + + tests_file = str(reporting.get("tests_file", "")).strip() + if tests_file: + suite_def["tests_file"] = tests_file + elif config_file is not None: + suite_def["tests_file"] = str(config_file) + return suite_def + + +def _load_suite_defs() -> dict[str, dict[str, Any]]: + suite_defs: dict[str, dict[str, Any]] = {} + config_dir = get_config_dir() + for path in sorted(config_dir.glob("tests_*.yml")): + data = _load_yaml(path) + suite = str(data.get("suite", "")).strip() + if not suite: + raise ValueError(f"Coverage config {path} is missing suite") + reporting = data.get("reporting") + if not isinstance(reporting, dict): + raise ValueError(f"Coverage config {path} is missing reporting section") + suite_defs[suite] = _normalize_suite_def(suite, reporting, config_file=path) + if not suite_defs: + raise ValueError(f"No coverage reporting suite definitions were found in {config_dir}") + return suite_defs + + +def _get_suite_def(suite: str) -> dict[str, Any]: + suite_defs = _load_suite_defs() + if suite not in suite_defs: + available = ", ".join(sorted(suite_defs)) + raise ValueError(f"Suite {suite!r} is not defined in coverage reporting configs. Available suites: {available}") + return suite_defs[suite] + + +def _read_json_file(path: Path) -> dict[str, object]: + if not path.is_file(): + return {} + return json.loads(path.read_text(encoding="utf-8")) + + +def _read_env_file(path: Path) -> dict[str, str]: + values: dict[str, str] = {} + if not path.is_file(): + return values + for raw_line in path.read_text(encoding="utf-8").splitlines(): + if "=" not in raw_line: + continue + key, value = raw_line.split("=", 1) + values[key.strip()] = value.strip() + return values + + +def _to_int(values: dict[str, str], key: str | None) -> int: + if not key: + return 0 + try: + return int(values.get(key, "0").strip()) + except ValueError: + return 0 + + +def _load_tests_file_names(path: Path, profile: str) -> list[str]: + if not path.is_file(): + raise FileNotFoundError(f"Suite tests file does not exist: {path}") + + try: + import yaml # type: ignore[import-not-found] + except ModuleNotFoundError as exc: + raise RuntimeError("PyYAML is required to read suite tests files") from exc + + data = yaml.safe_load(path.read_text(encoding="utf-8")) or {} + tests = data.get("tests", []) + if not isinstance(tests, list): + raise ValueError(f"Suite tests file {path} must contain a 'tests' list") + + names: list[str] = [] + for item in tests: + if not isinstance(item, dict): + continue + profiles = item.get("profiles", []) + if profiles and profile not in {str(profile_item) for profile_item in profiles}: + continue + name = str(item.get("name", "")).strip() + if name: + names.append(name) + return names + + +def _load_test_names(*, workspace: Path, suite_def: dict[str, Any], profile: str) -> list[str]: + test_names = suite_def.get("test_names") + if isinstance(test_names, list): + return [str(name).strip() for name in test_names if str(name).strip()] + + tests_file = str(suite_def.get("tests_file", "")).strip() + if tests_file: + return _load_tests_file_names(resolve_workspace_path(tests_file, workspace=workspace), profile) + + return [] + + +def _selected_test_names(*, suite_def: dict[str, Any], test_names: str) -> list[str]: + raw_value = test_names.strip() + if not raw_value: + selection_env = str(suite_def.get("selection_env", "")).strip() + raw_value = os.environ.get(selection_env, "").strip() if selection_env else "" + if not raw_value: + return [] + return [name.strip() for name in raw_value.split(",") if name.strip()] + + +def collect_suite_results( + *, + workspace: Path, + suite: str, + profile: str, + lane: str, + test_names: str, + artifact_name: str, + artifact_dir: Path | None, + outputs_file: Path | None, +) -> tuple[str, Path]: + suite_def = _get_suite_def(suite) + lane = lane or profile + artifact_name = artifact_name or _format_template( + str(suite_def["artifact_name_template"]), + _template_context(suite_def=suite_def, profile=profile, lane=lane), + ) + if artifact_dir is None: + runner_temp = os.environ.get("RUNNER_TEMP", "").strip() + artifact_dir = Path(runner_temp or workspace) / artifact_name + artifact_dir.mkdir(parents=True, exist_ok=True) + + tests = _load_test_names(workspace=workspace, suite_def=suite_def, profile=profile) + selected_tests = _selected_test_names(suite_def=suite_def, test_names=test_names) + if selected_tests: + selected_set = set(selected_tests) + tests = [test for test in tests if test in selected_set] + total = len(tests) + + metadata = { + "suite": suite, + "profile": profile, + "lane": lane, + "artifact_name": artifact_name, + "artifact_group": suite_def["artifact_group"], + } + (artifact_dir / METADATA_FILE).write_text(json.dumps(metadata, separators=(",", ":")) + "\n", encoding="utf-8") + + duration_path = artifact_dir / str(suite_def["duration_file"]) + with duration_path.open("w", encoding="utf-8", newline="") as handle: + writer = csv.writer(handle) + writer.writerow(["test_name", "status", "duration_seconds", "duration_minutes"]) + for test_name in tests: + writer.writerow([test_name, "not_run", "0.000", "0.000"]) + + stats_lines = [f"{suite_def['total_key']}={total}"] + if suite_def["executed_key"]: + stats_lines.append(f"{suite_def['executed_key']}=0") + stats_lines.extend( + [ + f"{suite_def['passed_key']}=0", + f"{suite_def['failed_key']}=0", + f"{suite_def['skipped_key']}=0", + f"{suite_def['not_run_key']}={total}", + ] + ) + (artifact_dir / str(suite_def["stats_file"])).write_text("\n".join(stats_lines) + "\n", encoding="utf-8") + + files_to_copy = [ + str(suite_def["duration_file"]), + str(suite_def["stats_file"]), + str(suite_def["coverage_file"]), + *[str(name) for name in suite_def.get("extra_files", [])], + ] + for filename in files_to_copy: + source = workspace / filename + if source.is_file(): + shutil.copy2(source, artifact_dir / filename) + + for debug_dir in suite_def.get("debug_dirs", []): + source = resolve_workspace_path(str(debug_dir["src"]), workspace=workspace) + if source.is_dir(): + destination = artifact_dir / str(debug_dir["dest"]) + shutil.rmtree(destination, ignore_errors=True) + shutil.copytree(source, destination) + + if outputs_file is not None: + with outputs_file.open("a", encoding="utf-8") as handle: + handle.write(f"dir={artifact_dir}\n") + handle.write(f"artifact-dir={artifact_dir}\n") + handle.write(f"artifact-name={artifact_name}\n") + + return artifact_name, artifact_dir + + +def _collect_artifacts(*, workspace: Path, suite_key: str) -> list[dict[str, object]]: + suite_def = _get_suite_def(suite_key) + root = workspace / "artifacts" / str(suite_def["artifact_group"]) + if not root.exists(): + return [] + + artifacts: list[dict[str, object]] = [] + for metadata_path in sorted(root.rglob(METADATA_FILE)): + metadata = _read_json_file(metadata_path) + if not metadata or str(metadata.get("suite", "")).strip() != suite_key: + continue + artifacts.append( + { + "artifact_dir": metadata_path.parent, + "artifact_name": str(metadata.get("artifact_name", metadata_path.parent.name)).strip(), + "artifact_group": str(metadata.get("artifact_group", suite_def["artifact_group"])).strip(), + "lane": str(metadata.get("lane", "")).strip() or "-", + "profile": str(metadata.get("profile", "")).strip(), + "suite": suite_key, + } + ) + return artifacts + + +def _template_context( + *, + suite_def: dict[str, Any], + profile: str = "", + lane: str = "", + artifact_name: str = "", + extra: dict[str, Any] | None = None, +) -> dict[str, str]: + context = { + "suite": str(suite_def["suite"]), + "label": str(suite_def["label"]), + "artifact_group": str(suite_def["artifact_group"]), + "profile": profile, + "lane": lane, + "artifact_name": artifact_name, + } + if extra: + context.update({str(key): str(value) for key, value in extra.items()}) + return context + + +def _artifact_context(suite_def: dict[str, Any], artifact: dict[str, object]) -> dict[str, str]: + return _template_context( + suite_def=suite_def, + profile=str(artifact.get("profile", "")), + lane=str(artifact.get("lane", "")), + artifact_name=str(artifact.get("artifact_name", "")), + ) + + +def _format_template(template: str, context: dict[str, str]) -> str: + try: + return template.format(**context) + except KeyError as exc: + available = ", ".join(sorted(context)) + raise ValueError(f"Unknown template variable {exc!s}; available variables: {available}") from exc + + +def _format_upload_value(upload: dict[str, Any], *, field: str, template_field: str, context: dict[str, str], default: str = "") -> str: + template = str(upload.get(template_field, upload.get(field, default))).strip() + return _format_template(template, context) if template else "" + + +def _artifact_upload_flags(suite_def: dict[str, Any], artifact: dict[str, object]) -> str: + flags: list[str] = [] + artifact_name = str(artifact.get("artifact_name", "")) + context = _artifact_context(suite_def, artifact) + for upload in suite_def.get("uploads", []): + pattern = str(upload.get("artifact_pattern", "")).strip() + if pattern and not _artifact_name_matches(artifact_name, _format_template(pattern, context)): + continue + flag = _format_upload_value(upload, field="flag", template_field="flag_template", context=context) + if flag and flag not in flags: + flags.append(flag) + return ", ".join(f"`{flag}`" for flag in flags) if flags else "-" + + +def render_summary(*, workspace: Path, summary_file: Path, selection: str, selected_lanes: str) -> None: + rows: list[dict[str, object]] = [] + overall = {"total": 0, "executed": 0, "passed": 0, "failed": 0, "skipped": 0, "not_run": 0} + suite_defs = _load_suite_defs() + + for suite_key, suite_def in suite_defs.items(): + for artifact in _collect_artifacts(workspace=workspace, suite_key=suite_key): + artifact_dir = Path(artifact["artifact_dir"]) + lane = str(artifact["lane"]) + stats = _read_env_file(artifact_dir / str(suite_def["stats_file"])) + total = _to_int(stats, str(suite_def["total_key"])) + passed = _to_int(stats, str(suite_def["passed_key"])) + failed = _to_int(stats, str(suite_def["failed_key"])) + skipped = _to_int(stats, str(suite_def["skipped_key"])) + not_run = _to_int(stats, str(suite_def["not_run_key"])) + executed = _to_int(stats, suite_def["executed_key"]) if suite_def["executed_key"] else max(0, total - skipped - not_run) + coverage_file = artifact_dir / str(suite_def["coverage_file"]) + report_size = coverage_file.stat().st_size if coverage_file.is_file() else 0 + + rows.append( + { + "lane": lane, + "suite": suite_def["label"], + "total": total, + "executed": executed, + "passed": passed, + "failed": failed, + "skipped": skipped, + "not_run": not_run, + "report_size": report_size, + "flags": _artifact_upload_flags(suite_def, artifact), + } + ) + overall["total"] += total + overall["executed"] += executed + overall["passed"] += passed + overall["failed"] += failed + overall["skipped"] += skipped + overall["not_run"] += not_run + + pass_rate = (overall["passed"] * 100.0) / overall["executed"] if overall["executed"] else 0.0 + + lines = [ + "## Coverage Summary", + "", + f"**Selection:** `{selection}`", + "", + f"**Active lanes:** `{selected_lanes}`", + "", + f"**Executed pass rate:** `{pass_rate:.1f}%`", + "", + "### Overall", + "| Metric | Value |", + "| --- | ---: |", + f"| Total test units | {overall['total']} |", + f"| Executed | {overall['executed']} |", + f"| Passed | {overall['passed']} |", + f"| Failed | {overall['failed']} |", + f"| Skipped | {overall['skipped']} |", + f"| Not run | {overall['not_run']} |", + "", + "### By Lane And Suite", + "| Lane | Suite | Total | Executed | Passed | Failed | Skipped | Not run | Report Size (bytes) | Codecov Flags |", + "| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | --- |", + ] + + if rows: + for row in sorted(rows, key=lambda item: (str(item["lane"]), str(item["suite"]))): + lines.append( + f"| {row['lane']} | {row['suite']} | {row['total']} | {row['executed']} | " + f"{row['passed']} | {row['failed']} | {row['skipped']} | {row['not_run']} | {row['report_size']} | {row['flags']} |" + ) + else: + lines.append("| - | - | 0 | 0 | 0 | 0 | 0 | 0 | 0 | - |") + + lines.extend(["", "Merged duration artifact: `coverage-test-durations` (`coverage-test-durations-all.csv`)", ""]) + summary_file.write_text("\n".join(lines), encoding="utf-8") + + +def merge_durations(*, workspace: Path, output: Path) -> None: + rows: list[dict[str, str]] = [] + suite_defs = _load_suite_defs() + for suite_key, suite_def in suite_defs.items(): + for artifact in _collect_artifacts(workspace=workspace, suite_key=suite_key): + artifact_dir = Path(artifact["artifact_dir"]) + report = artifact_dir / str(suite_def["duration_file"]) + if not report.is_file(): + continue + artifact_name = str(artifact["artifact_name"]) + lane = str(artifact["lane"]) + with report.open(encoding="utf-8", newline="") as handle: + reader = csv.DictReader(handle) + for row in reader: + rows.append( + { + "suite": suite_key, + "lane": lane, + "artifact": artifact_name, + "test_name": row.get("test_name", ""), + "status": row.get("status", ""), + "duration_seconds": row.get("duration_seconds", "0"), + "duration_minutes": row.get("duration_minutes", "0"), + } + ) + + rows.sort(key=lambda row: (-float(row["duration_seconds"] or 0), row["suite"], row["lane"], row["test_name"])) + with output.open("w", encoding="utf-8", newline="") as handle: + writer = csv.DictWriter( + handle, + fieldnames=["suite", "lane", "artifact", "test_name", "status", "duration_seconds", "duration_minutes"], + ) + writer.writeheader() + writer.writerows(rows) + + print(f"Wrote {len(rows)} duration row(s) to {output}") + + +def _artifact_name_matches(artifact_name: str, pattern: str) -> bool: + return fnmatchcase(artifact_name, pattern) + + +def _workspace_relative(path: Path, workspace: Path) -> str: + try: + return path.resolve().relative_to(workspace.resolve()).as_posix() + except ValueError: + return path.resolve().as_posix() + + +def _parse_artifact_names(value: str) -> list[str]: + raw_value = value.strip() + if not raw_value: + return [] + try: + parsed = json.loads(raw_value) + except json.JSONDecodeError: + parsed = None + if isinstance(parsed, list): + return [str(item).strip() for item in parsed if str(item).strip()] + return [item.strip() for item in raw_value.replace("\n", ",").split(",") if item.strip()] + + +def _safe_relative_file(file_name: str) -> Path: + relative_file = Path(file_name) + if relative_file.is_absolute() or ".." in relative_file.parts: + raise ValueError(f"Upload file must be a relative path inside the artifact: {file_name}") + return relative_file + + +def _append_unique_path(paths: list[Path], candidate: Path) -> None: + if candidate.is_file() and candidate not in paths: + paths.append(candidate) + + +def resolve_upload_files( + *, + workspace: Path, + artifact_group: str, + artifact_names: str, + file_name: str, + upload_key: str, + output_file: Path, +) -> None: + root = workspace / "artifacts" / artifact_group + if not root.is_dir(): + raise FileNotFoundError(f"Coverage artifact group directory does not exist: {root}") + + relative_file = _safe_relative_file(file_name) + files: list[Path] = [] + parsed_artifact_names = _parse_artifact_names(artifact_names) + + for artifact_name in parsed_artifact_names: + _append_unique_path(files, root / artifact_name / relative_file) + + if not files: + _append_unique_path(files, root / relative_file) + + if not files: + artifact_name_set = set(parsed_artifact_names) + for candidate in sorted(root.rglob(relative_file.as_posix())): + if not candidate.is_file(): + continue + if artifact_name_set and candidate.parent.name not in artifact_name_set: + relative_parts = candidate.relative_to(root).parts + artifact_root = relative_parts[0] if len(relative_parts) > len(relative_file.parts) else "" + if artifact_root not in artifact_name_set: + continue + _append_unique_path(files, candidate) + + if not files: + expected = ", ".join(parsed_artifact_names) if parsed_artifact_names else "any downloaded artifact" + raise FileNotFoundError(f"Could not find {file_name!r} for {expected} under {root}") + + files_csv = ",".join(_workspace_relative(path, workspace) for path in files) + upload_files = {upload_key: files_csv} if upload_key else {} + + print(f"Resolved Codecov upload files: {files_csv}") + with output_file.open("a", encoding="utf-8") as handle: + handle.write(f"files={files_csv}\n") + handle.write(f"upload-files-json={json.dumps(upload_files, sort_keys=True)}\n") + + +def _upload_key(upload: dict[str, Any], context: dict[str, str], *, file_name: str) -> str: + context = {**context, "file": file_name} + raw_key = _format_upload_value( + upload, + field="key", + template_field="key_template", + context=context, + default="{suite}_{lane}_{file}", + ) + return "".join(character if character.isalnum() else "_" for character in raw_key).strip("_") + + +def _upload_entry( + *, + suite_def: dict[str, Any], + upload: dict[str, Any], + artifacts: list[dict[str, object]], + workspace: Path, + context: dict[str, str], + artifact_pattern: str, +) -> dict[str, Any] | None: + file_name = _format_upload_value(upload, field="file", template_field="file_template", context=context) + if not file_name: + raise ValueError(f"Upload entry for suite {suite_def['suite']!r} resolved to an empty file name") + + files: list[str] = [] + artifact_names: list[str] = [] + for artifact in artifacts: + artifact_name = str(artifact["artifact_name"]) + if not _artifact_name_matches(artifact_name, artifact_pattern): + continue + candidate = Path(artifact["artifact_dir"]) / file_name + if candidate.is_file(): + files.append(_workspace_relative(candidate, workspace)) + artifact_names.append(artifact_name) + + if not files: + return None + + flag = _format_upload_value(upload, field="flag", template_field="flag_template", context=context) + name = _format_upload_value( + upload, + field="name", + template_field="name_template", + context=context, + default="{label}, {lane}", + ) + + return { + "key": _upload_key(upload, context, file_name=file_name), + "name": name, + "suite": suite_def["suite"], + "artifact_group": suite_def["artifact_group"], + "artifact_pattern": artifact_pattern, + "artifact_names": artifact_names, + "file": file_name, + "files": files, + "files_csv": ",".join(files), + "flag": flag, + } + + +def _build_upload_matrix(*, workspace: Path) -> list[dict[str, Any]]: + matrix: list[dict[str, Any]] = [] + suite_defs = _load_suite_defs() + for suite_key, suite_def in suite_defs.items(): + artifacts = _collect_artifacts(workspace=workspace, suite_key=suite_key) + if not artifacts: + continue + + for upload in suite_def.get("uploads", []): + artifact_pattern_template = str(upload.get("artifact_pattern", "")).strip() + if artifact_pattern_template: + context = _template_context( + suite_def=suite_def, + profile=str(upload.get("profile", "")), + lane=str(upload.get("lane", "")), + artifact_name=str(upload.get("artifact_name", "")), + extra={key: value for key, value in upload.items() if isinstance(value, (str, int, float))}, + ) + artifact_pattern = _format_template(artifact_pattern_template, context) + entry = _upload_entry( + suite_def=suite_def, + upload=upload, + artifacts=artifacts, + workspace=workspace, + context=context, + artifact_pattern=artifact_pattern, + ) + if entry: + matrix.append(entry) + continue + + for artifact in artifacts: + context = _artifact_context(suite_def, artifact) + artifact_pattern = str(artifact["artifact_name"]) + entry = _upload_entry( + suite_def=suite_def, + upload=upload, + artifacts=[artifact], + workspace=workspace, + context=context, + artifact_pattern=artifact_pattern, + ) + if entry: + matrix.append(entry) + return matrix + + +def resolve_uploads(*, workspace: Path, output_file: Path) -> None: + upload_matrix = _build_upload_matrix(workspace=workspace) + upload_files = {str(entry["key"]): ",".join(entry["files"]) for entry in upload_matrix} + output_lines = [ + f"upload-files-json={json.dumps(upload_files, sort_keys=True)}", + f"upload-matrix-json={json.dumps(upload_matrix, sort_keys=True)}", + ] + + for entry in upload_matrix: + print(f"{entry['name']}: {','.join(entry['files'])}") + if not upload_matrix: + print("No Codecov upload files were resolved") + + with output_file.open("a", encoding="utf-8") as handle: + handle.write("\n".join(output_lines) + "\n") + + +def _parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Coverage CI report helpers") + subparsers = parser.add_subparsers(dest="command", required=True) + common = argparse.ArgumentParser(add_help=False) + common.add_argument("--config-dir", default=os.environ.get("COVERAGE_CONFIG_DIR")) + + collect = subparsers.add_parser( + "collect-suite-results", + parents=[common], + help="Prepare one suite artifact with fallback files and real outputs", + ) + collect.add_argument("--workspace", type=Path, required=True) + collect.add_argument("--suite", required=True) + collect.add_argument("--profile", required=True) + collect.add_argument("--lane", required=True) + collect.add_argument("--test-names", default="") + collect.add_argument("--artifact-name", default="") + collect.add_argument("--artifact-dir", type=Path) + collect.add_argument("--outputs-file", type=Path) + + render = subparsers.add_parser( + "render-summary", + parents=[common], + help="Render the aggregated GitHub summary from downloaded artifacts", + ) + render.add_argument("--workspace", type=Path, required=True) + render.add_argument("--summary-file", type=Path, required=True) + render.add_argument("--selection", required=True) + render.add_argument("--selected-lanes", required=True) + + merge = subparsers.add_parser( + "merge-durations", + parents=[common], + help="Merge suite duration CSV files from downloaded artifacts", + ) + merge.add_argument("--workspace", type=Path, required=True) + merge.add_argument("--output", type=Path, required=True) + + uploads = subparsers.add_parser( + "resolve-uploads", + parents=[common], + help="Resolve Codecov upload files from downloaded artifacts", + ) + uploads.add_argument("--workspace", type=Path, required=True) + uploads.add_argument("--output-file", type=Path, required=True) + + upload_files = subparsers.add_parser( + "resolve-upload-files", + help="Resolve one Codecov upload row from artifacts downloaded in the current job", + ) + upload_files.add_argument("--workspace", type=Path, required=True) + upload_files.add_argument("--artifact-group", required=True) + upload_files.add_argument("--artifact-names-json", default="") + upload_files.add_argument("--upload-key", default="") + upload_files.add_argument("--file", required=True) + upload_files.add_argument("--output-file", type=Path, required=True) + + return parser.parse_args() + + +def main() -> int: + args = _parse_args() + _apply_common_env(args) + if args.command == "collect-suite-results": + collect_suite_results( + workspace=args.workspace.resolve(), + suite=args.suite, + profile=args.profile, + lane=args.lane, + test_names=args.test_names, + artifact_name=args.artifact_name, + artifact_dir=args.artifact_dir.resolve() if args.artifact_dir else None, + outputs_file=args.outputs_file.resolve() if args.outputs_file else None, + ) + return 0 + if args.command == "render-summary": + render_summary( + workspace=args.workspace.resolve(), + summary_file=args.summary_file.resolve(), + selection=args.selection, + selected_lanes=args.selected_lanes, + ) + return 0 + if args.command == "merge-durations": + merge_durations(workspace=args.workspace.resolve(), output=args.output.resolve()) + return 0 + if args.command == "resolve-uploads": + resolve_uploads(workspace=args.workspace.resolve(), output_file=args.output_file.resolve()) + return 0 + if args.command == "resolve-upload-files": + resolve_upload_files( + workspace=args.workspace.resolve(), + artifact_group=args.artifact_group, + artifact_names=args.artifact_names_json, + file_name=args.file, + upload_key=args.upload_key, + output_file=args.output_file.resolve(), + ) + return 0 + raise ValueError(f"Unsupported command: {args.command}") + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/.github/scripts/coverage/collect_cpp_coverage.py b/.github/actions/coverage_toolkit/collect_cpp_coverage.py similarity index 95% rename from .github/scripts/coverage/collect_cpp_coverage.py rename to .github/actions/coverage_toolkit/collect_cpp_coverage.py index 678b39ffb125..f03de1eb2487 100644 --- a/.github/scripts/coverage/collect_cpp_coverage.py +++ b/.github/actions/coverage_toolkit/collect_cpp_coverage.py @@ -199,17 +199,17 @@ def _classify_unwanted_source(rel_path: Path) -> str | None: return None -def _classify_profile_unwanted_source(rel_path: Path, *, run_gpu_tests: bool, run_npu_tests: bool) -> str | None: +def _classify_profile_unwanted_source(rel_path: Path, *, run_gpu_tests: bool) -> str | None: """Return profile-specific exclusion reasons for source paths.""" rel = rel_path.as_posix() if not run_gpu_tests and rel.startswith("src/plugins/intel_gpu/"): return "gpu-plugin-disabled" - if not run_npu_tests and rel.startswith("src/plugins/intel_npu/"): + if rel.startswith("src/plugins/intel_npu/"): return "npu-plugin-disabled" return None -def _prune_unwanted_gcda(root: Path, *, label: str, run_gpu_tests: bool, run_npu_tests: bool) -> None: +def _prune_unwanted_gcda(root: Path, *, label: str, run_gpu_tests: bool) -> None: """Delete gcda files that are known to be excluded from the final report. This reduces lcov capture time by removing whole classes of files that we @@ -224,7 +224,7 @@ def _prune_unwanted_gcda(root: Path, *, label: str, run_gpu_tests: bool, run_npu rel = gcda.relative_to(root) reason = _classify_unwanted_gcda(rel) if reason is None: - reason = _classify_profile_unwanted_source(rel, run_gpu_tests=run_gpu_tests, run_npu_tests=run_npu_tests) + reason = _classify_profile_unwanted_source(rel, run_gpu_tests=run_gpu_tests) if reason is None: continue try: @@ -292,7 +292,6 @@ def _normalize_and_filter_tracefile( workspace: Path, debug_dir: Path, run_gpu_tests: bool, - run_npu_tests: bool, ) -> dict[str, int]: """Normalize ``SF:`` paths and drop records that should not be uploaded.""" stats: dict[str, int] = { @@ -342,7 +341,6 @@ def flush_record(lines: list[str]) -> None: exclude_reason = _classify_profile_unwanted_source( rel_source, run_gpu_tests=run_gpu_tests, - run_npu_tests=run_npu_tests, ) if exclude_reason is not None: stats["dropped_excluded"] += 1 @@ -417,6 +415,14 @@ def _tool_version_report() -> str: return "\n".join(lines).rstrip() + "\n" +def _extra_coverage_dirs() -> list[Path]: + """Return additional native build directories requested by the caller.""" + raw_dirs = os.environ.get("EXTRA_CPP_COVERAGE_DIRS", "").strip() + if not raw_dirs: + return [] + return [Path(item) for item in raw_dirs.split(os.pathsep) if item.strip()] + + def _tree_inventory_report( *, root: Path, @@ -669,14 +675,12 @@ def run(ctx: CoverageContext) -> None: ctx.paths.build_dir, label="main build", run_gpu_tests=ctx.run_gpu_tests, - run_npu_tests=ctx.run_npu_tests, ) if ctx.paths.build_js_dir.exists(): _prune_unwanted_gcda( ctx.paths.build_js_dir, label="js build", run_gpu_tests=ctx.run_gpu_tests, - run_npu_tests=ctx.run_npu_tests, ) _prefilter_incompatible_gcda(ctx.paths.build_dir, label="main build") @@ -753,6 +757,24 @@ def run(ctx: CoverageContext) -> None: if has_main_gcda and main_info is not None: tracefiles.append(main_info) + for index, extra_dir in enumerate(_extra_coverage_dirs(), start=1): + label = f"extra native capture {index}" + _prune_unwanted_gcda(extra_dir, label=label, run_gpu_tests=ctx.run_gpu_tests) + _prefilter_incompatible_gcda(extra_dir, label=label) + if not _has_gcda(extra_dir): + LOGGER.warning("No .gcda files found in %s, skipping %s", extra_dir, label) + continue + extra_info = trace_dir / f"extra-build-{index}.info" + _run_lcov_capture( + directory=extra_dir, + base_directory=src_dir, + output_file=extra_info, + label=label, + debug_dir=trace_dir, + branch_coverage=ctx.branch_coverage, + ) + tracefiles.append(extra_info) + _merge_tracefiles(tracefiles, merged_info, branch_coverage=ctx.branch_coverage) normalization_stats = _normalize_and_filter_tracefile( @@ -760,7 +782,6 @@ def run(ctx: CoverageContext) -> None: workspace=src_dir, debug_dir=trace_dir, run_gpu_tests=ctx.run_gpu_tests, - run_npu_tests=ctx.run_npu_tests, ) if not merged_info.exists() or merged_info.stat().st_size == 0: diff --git a/.github/scripts/coverage/coverage.py b/.github/actions/coverage_toolkit/coverage.py similarity index 78% rename from .github/scripts/coverage/coverage.py rename to .github/actions/coverage_toolkit/coverage.py index 7aec77902ad6..1e35a8937b6c 100644 --- a/.github/scripts/coverage/coverage.py +++ b/.github/actions/coverage_toolkit/coverage.py @@ -18,13 +18,14 @@ SCRIPT_DIR = Path(__file__).resolve().parent CONFIG_DIR = SCRIPT_DIR / "config" +CONFIG_DIR_ENV = "COVERAGE_CONFIG_DIR" if str(SCRIPT_DIR) not in sys.path: sys.path.insert(0, str(SCRIPT_DIR)) # Stable import alias used by step modules. sys.modules.setdefault("coverage_workflow", sys.modules[__name__]) -SUPPORTED_PROFILES = {"cpu", "gpu", "npu"} +SUPPORTED_PROFILES = {"cpu", "gpu"} def _build_logger() -> logging.Logger: @@ -44,11 +45,34 @@ def _build_logger() -> logging.Logger: LOGGER = _build_logger() +def resolve_workspace_path(value: str | Path, *, workspace: Path | None = None) -> Path: + """Resolve an absolute or workspace-relative path from an environment/config value.""" + raw = os.path.expandvars(str(value)).strip() + path = Path(raw).expanduser() + if path.is_absolute(): + return path.resolve() + + base = workspace + if base is None: + base = Path(os.environ.get("OV_WORKSPACE") or os.environ.get("GITHUB_WORKSPACE") or Path.cwd()) + return (base / path).resolve() + + +def get_config_dir() -> Path: + """Return the active coverage test config directory.""" + raw = os.environ.get(CONFIG_DIR_ENV, "").strip() + if raw: + return resolve_workspace_path(raw) + workspace = Path(os.environ.get("OV_WORKSPACE") or os.environ.get("GITHUB_WORKSPACE") or Path.cwd()).resolve() + repo_config_dir = workspace / ".github" / "coverage" + if repo_config_dir.is_dir(): + return repo_config_dir + return CONFIG_DIR + + @dataclass(frozen=True) class CppTestCase: name: str - enabled: bool - skip_reason: str binary: str mode: str args: str @@ -58,8 +82,6 @@ class CppTestCase: @dataclass(frozen=True) class PythonTestCase: name: str - enabled: bool - skip_reason: str kind: str target: str args: str @@ -70,8 +92,6 @@ class PythonTestCase: @dataclass(frozen=True) class JsTestCase: name: str - enabled: bool - skip_reason: str kind: str command: str @@ -87,14 +107,6 @@ class Paths: model_path: Path -@dataclass(frozen=True) -class ProfileFlags: - run_gpu_tests: bool - run_npu_tests: bool - gpu_flags: tuple[str, ...] - npu_flags: tuple[str, ...] - - @dataclass(frozen=True) class ConfigValidationIssue: suite: str @@ -225,29 +237,14 @@ def _repo_root(default: Path) -> Path: return default -def _profile_flags(profile: str) -> ProfileFlags: - """Resolve build flags and runtime switches for a test profile.""" - if profile == "cpu": - return ProfileFlags(False, False, ("-DENABLE_INTEL_GPU=OFF", "-DENABLE_ONEDNN_FOR_GPU=OFF"), ("-DENABLE_INTEL_NPU=OFF",)) - if profile == "gpu": - return ProfileFlags(True, False, ("-DENABLE_INTEL_GPU=ON", "-DENABLE_ONEDNN_FOR_GPU=ON"), ("-DENABLE_INTEL_NPU=OFF",)) - if profile == "npu": - return ProfileFlags(False, True, ("-DENABLE_INTEL_GPU=OFF", "-DENABLE_ONEDNN_FOR_GPU=OFF"), ("-DENABLE_INTEL_NPU=ON",)) - raise ValueError(f"Unsupported TEST_PROFILE: {profile}. Use one of: {', '.join(sorted(SUPPORTED_PROFILES))}") - - @dataclass class CoverageContext: """Runtime configuration shared by coverage workflow steps.""" workspace: Path - build_type: str branch_coverage: bool test_profile: str - cc: str - cxx: str paths: Paths - profile_flags: ProfileFlags io: GithubIO @classmethod @@ -274,9 +271,6 @@ def _bool_env(name: str, fallback: bool) -> bool: if test_profile not in SUPPORTED_PROFILES: raise ValueError(f"Unsupported TEST_PROFILE: {test_profile}. Use one of: {', '.join(sorted(SUPPORTED_PROFILES))}") - cc = os.environ.get("CC", "gcc") - cxx = os.environ.get("CXX", "g++") - paths = Paths( workspace=workspace, build_dir=Path(os.environ.get("BUILD_DIR", str(workspace / "build"))), @@ -287,51 +281,28 @@ def _bool_env(name: str, fallback: bool) -> bool: model_path=Path(os.environ.get("MODEL_PATH", str(workspace / "src" / "core" / "tests" / "models" / "ir" / "add_abc.xml"))), ) - profile_flags = _profile_flags(test_profile) - os.environ["OV_WORKSPACE"] = str(workspace) os.environ["CMAKE_BUILD_TYPE"] = build_type os.environ["ENABLE_BRANCH_COVERAGE"] = "true" if branch_coverage else "false" os.environ["TEST_PROFILE"] = test_profile - os.environ["RUN_GPU_TESTS"] = "true" if profile_flags.run_gpu_tests else "false" - os.environ["RUN_NPU_TESTS"] = "true" if profile_flags.run_npu_tests else "false" return cls( workspace=workspace, - build_type=build_type, branch_coverage=branch_coverage, test_profile=test_profile, - cc=cc, - cxx=cxx, paths=paths, - profile_flags=profile_flags, io=io, ) @property def run_gpu_tests(self) -> bool: - return self.profile_flags.run_gpu_tests - - @property - def run_npu_tests(self) -> bool: - return self.profile_flags.run_npu_tests - - @property - def gpu_flags(self) -> tuple[str, ...]: - return self.profile_flags.gpu_flags - - @property - def npu_flags(self) -> tuple[str, ...]: - return self.profile_flags.npu_flags + return self.test_profile == "gpu" def log_profile(self) -> None: - """Print the resolved profile and accelerator flags.""" + """Print the resolved profile.""" LOGGER.info("TEST_PROFILE=%s", self.test_profile) LOGGER.info("ENABLE_BRANCH_COVERAGE=%s", "true" if self.branch_coverage else "false") LOGGER.info("RUN_GPU_TESTS=%s", "true" if self.run_gpu_tests else "false") - LOGGER.info("RUN_NPU_TESTS=%s", "true" if self.run_npu_tests else "false") - LOGGER.info("GPU_FLAGS=%s", " ".join(self.gpu_flags)) - LOGGER.info("NPU_FLAGS=%s", " ".join(self.npu_flags)) def _as_text(value: Any) -> str: @@ -344,38 +315,18 @@ def _as_text(value: Any) -> str: def _resolve_profile_value(value: Any, profile: str) -> str: - """Pick a profile-specific config value when a mapping is provided.""" + """Pick an explicit profile-specific config value when a mapping is provided.""" if isinstance(value, dict): - if profile in value: - return _as_text(value[profile]) - return _as_text(value.get("default", "")) + return _as_text(value.get(profile, "")) return _as_text(value) -def _resolve_enabled(test: dict[str, Any], profile: str) -> tuple[bool, str]: - """Decide whether a configured test is enabled for the active profile.""" - skip_reason = _as_text(test.get("skip_reason", "")).strip() +def _configured_profiles(test: dict[str, Any]) -> tuple[str, ...]: + """Return the explicit profiles configured for a test.""" profiles = test.get("profiles") - - # Accelerator-specific profiles execute only tests explicitly marked for them. - if profile in {"gpu", "npu"} and profiles is None: - reason = _as_text(test.get("profile_skip_reason", "")).strip() - if not reason: - reason = f"{profile.upper()} profile is OFF" - return False, reason - - if profiles is not None: - normalized = [str(p) for p in profiles] - if profile not in normalized: - reason = _as_text(test.get("profile_skip_reason", "")).strip() - if not reason: - reason = f"{profile.upper()} profile is OFF" - return False, reason - - if skip_reason: - return False, skip_reason - - return True, "" + if not isinstance(profiles, list): + return () + return tuple(_as_text(profile).strip() for profile in profiles) def _load_yaml(path: Path) -> dict[str, Any]: @@ -412,12 +363,11 @@ def load_cpp_tests(path: Path, profile: str) -> list[CppTestCase]: loaded: list[CppTestCase] = [] for test in _load_tests(path, "cpp"): - enabled, reason = _resolve_enabled(test, profile) + if profile not in _configured_profiles(test): + continue loaded.append( CppTestCase( name=_as_text(test.get("name", "")).strip(), - enabled=enabled, - skip_reason=reason, binary=_as_text(test.get("binary", "")).strip(), mode=_as_text(test.get("mode", "gtest_single")).strip() or "gtest_single", args=_resolve_profile_value(test.get("args", ""), profile).strip(), @@ -434,12 +384,11 @@ def load_python_tests(path: Path, profile: str) -> list[PythonTestCase]: loaded: list[PythonTestCase] = [] for test in _load_tests(path, "python"): - enabled, reason = _resolve_enabled(test, profile) + if profile not in _configured_profiles(test): + continue loaded.append( PythonTestCase( name=_as_text(test.get("name", "")).strip(), - enabled=enabled, - skip_reason=reason, kind=_as_text(test.get("kind", "pytest")).strip() or "pytest", target=_resolve_profile_value(test.get("target", ""), profile).strip(), args=_resolve_profile_value(test.get("args", ""), profile).strip(), @@ -457,12 +406,11 @@ def load_js_tests(path: Path, profile: str) -> list[JsTestCase]: loaded: list[JsTestCase] = [] for test in _load_tests(path, "js"): - enabled, reason = _resolve_enabled(test, profile) + if profile not in _configured_profiles(test): + continue loaded.append( JsTestCase( name=_as_text(test.get("name", "")).strip(), - enabled=enabled, - skip_reason=reason, kind=_as_text(test.get("kind", "command")).strip() or "command", command=_resolve_profile_value(test.get("command", ""), profile).strip(), ) @@ -470,27 +418,83 @@ def load_js_tests(path: Path, profile: str) -> list[JsTestCase]: return loaded +def _profile_value_fields(suite: str) -> tuple[str, ...]: + """Return config fields that may contain per-profile mappings.""" + if suite == "cpp": + return ("args", "extra_env") + if suite == "python": + return ("target", "args", "env", "command") + if suite == "js": + return ("command",) + return () + + def validate_configs(config_dir: Path) -> list[ConfigValidationIssue]: """Validate coverage YAML configs and return any issues found.""" issues: list[ConfigValidationIssue] = [] - suites = { + known_suites = { "cpp": config_dir / "tests_cpp.yml", "python": config_dir / "tests_python.yml", "js": config_dir / "tests_js.yml", } + suites = {suite: path for suite, path in known_suites.items() if path.is_file()} + for path in sorted(config_dir.glob("tests_*.yml")): + if path not in known_suites.values(): + issues.append(ConfigValidationIssue("config", path.name, "unsupported coverage suite config file")) + + if not suites: + issues.append(ConfigValidationIssue("config", str(config_dir), "no coverage suite config files found")) + return issues + for suite, path in suites.items(): + data = _load_yaml(path) + reporting = data.get("reporting") + if not isinstance(reporting, dict): + issues.append(ConfigValidationIssue(suite, "", "missing 'reporting' section")) + else: + for field in ("label", "stats_file", "duration_file", "coverage_file"): + if not _as_text(reporting.get(field, "")).strip(): + issues.append(ConfigValidationIssue(suite, "", f"missing '{field}'")) + uploads = reporting.get("uploads") + if not isinstance(uploads, list) or not uploads: + issues.append(ConfigValidationIssue(suite, "", "'uploads' must be a non-empty list")) + else: + for idx, upload in enumerate(uploads): + if not isinstance(upload, dict): + issues.append(ConfigValidationIssue(suite, f"", "upload entry must be a mapping")) + continue + if not _as_text(upload.get("file", "")).strip(): + issues.append(ConfigValidationIssue(suite, f"", "missing 'file'")) + if not _as_text(upload.get("flag", upload.get("flag_template", ""))).strip(): + issues.append(ConfigValidationIssue(suite, f"", "missing 'flag' or 'flag_template'")) tests = _load_tests(path, suite) for idx, test in enumerate(tests): name = _as_text(test.get("name", f"")) + profiles = test.get("profiles") + expected_profiles = _configured_profiles(test) if not _as_text(test.get("name", "")).strip(): issues.append(ConfigValidationIssue(suite, name, "missing 'name'")) + if not isinstance(profiles, list) or not profiles: + issues.append(ConfigValidationIssue(suite, name, "'profiles' must be a non-empty list")) + else: + for profile_name in expected_profiles: + if profile_name not in SUPPORTED_PROFILES: + issues.append(ConfigValidationIssue(suite, name, f"unsupported profile '{profile_name}'")) + for field in _profile_value_fields(suite): + value = test.get(field) + if not isinstance(value, dict): + continue + mapped_profiles = {_as_text(profile).strip() for profile in value} + for profile_name in expected_profiles: + if profile_name in SUPPORTED_PROFILES and profile_name not in mapped_profiles: + issues.append(ConfigValidationIssue(suite, name, f"'{field}' missing '{profile_name}' value")) if suite == "cpp" and not _as_text(test.get("binary", "")).strip(): issues.append(ConfigValidationIssue(suite, name, "missing 'binary'")) if suite == "python": kind = _as_text(test.get("kind", "pytest")).strip() or "pytest" - if kind not in {"pytest", "pytest_if_dir", "command"}: + if kind not in {"pytest", "command"}: issues.append(ConfigValidationIssue(suite, name, f"unsupported kind '{kind}'")) if suite == "js": kind = _as_text(test.get("kind", "command")).strip() or "command" @@ -522,6 +526,11 @@ def _apply_common_env(args: argparse.Namespace) -> None: if build_type: os.environ["CMAKE_BUILD_TYPE"] = build_type + config_dir = getattr(args, "config_dir", None) + if config_dir: + os.environ[CONFIG_DIR_ENV] = str(resolve_workspace_path(config_dir)) + + def _load_context(args: argparse.Namespace) -> CoverageContext: """Create a coverage context for the current command.""" _apply_common_env(args) @@ -558,19 +567,16 @@ def _command_step(args: argparse.Namespace) -> int: def _command_list_tests(args: argparse.Namespace) -> int: """Print resolved tests for a suite/profile pair.""" _apply_common_env(args) - workspace = Path(os.environ.get("OV_WORKSPACE") or os.environ.get("GITHUB_WORKSPACE") or Path.cwd()).resolve() - config_dir = CONFIG_DIR + config_dir = get_config_dir() if args.suite == "cpp": tests = load_cpp_tests(config_dir / "tests_cpp.yml", args.profile) - print("name\tenabled\tskip_reason\tbinary\tmode\targs\textra_env") + print("name\tbinary\tmode\targs\textra_env") for t in tests: print( "\t".join( [ t.name, - "1" if t.enabled else "0", - t.skip_reason, t.binary, t.mode, t.args, @@ -580,14 +586,12 @@ def _command_list_tests(args: argparse.Namespace) -> int: ) elif args.suite == "python": tests = load_python_tests(config_dir / "tests_python.yml", args.profile) - print("name\tenabled\tskip_reason\tkind\ttarget\targs\tenv\tcommand") + print("name\tkind\ttarget\targs\tenv\tcommand") for t in tests: print( "\t".join( [ t.name, - "1" if t.enabled else "0", - t.skip_reason, t.kind, t.target, t.args, @@ -598,14 +602,12 @@ def _command_list_tests(args: argparse.Namespace) -> int: ) else: tests = load_js_tests(config_dir / "tests_js.yml", args.profile) - print("name\tenabled\tskip_reason\tkind\tcommand") + print("name\tkind\tcommand") for t in tests: print( "\t".join( [ t.name, - "1" if t.enabled else "0", - t.skip_reason, t.kind, t.command, ] @@ -618,8 +620,7 @@ def _command_list_tests(args: argparse.Namespace) -> int: def _command_validate_config(args: argparse.Namespace) -> int: """Validate the coverage YAML configuration files.""" _apply_common_env(args) - workspace = Path(os.environ.get("OV_WORKSPACE") or os.environ.get("GITHUB_WORKSPACE") or Path.cwd()).resolve() - issues = validate_configs(CONFIG_DIR) + issues = validate_configs(get_config_dir()) if issues: for issue in issues: @@ -636,6 +637,7 @@ def _add_common_options(parser: argparse.ArgumentParser, *, include_profile: boo parser.add_argument("--profile", choices=sorted(SUPPORTED_PROFILES), default=os.environ.get("TEST_PROFILE", "cpu")) parser.add_argument("--workspace", default=os.environ.get("OV_WORKSPACE") or os.environ.get("GITHUB_WORKSPACE")) parser.add_argument("--build-type", default=os.environ.get("CMAKE_BUILD_TYPE", "Release")) + parser.add_argument("--config-dir", default=os.environ.get(CONFIG_DIR_ENV)) def build_parser() -> argparse.ArgumentParser: @@ -652,10 +654,12 @@ def build_parser() -> argparse.ArgumentParser: list_parser.add_argument("--suite", required=True, choices=["cpp", "python", "js"]) list_parser.add_argument("--profile", choices=sorted(SUPPORTED_PROFILES), default=os.environ.get("TEST_PROFILE", "cpu")) list_parser.add_argument("--workspace", default=os.environ.get("OV_WORKSPACE") or os.environ.get("GITHUB_WORKSPACE")) + list_parser.add_argument("--config-dir", default=os.environ.get(CONFIG_DIR_ENV)) list_parser.set_defaults(func=_command_list_tests) validate_parser = subparsers.add_parser("validate-config", help="Validate YAML coverage test configs") validate_parser.add_argument("--workspace", default=os.environ.get("OV_WORKSPACE") or os.environ.get("GITHUB_WORKSPACE")) + validate_parser.add_argument("--config-dir", default=os.environ.get(CONFIG_DIR_ENV)) validate_parser.set_defaults(func=_command_validate_config) return parser diff --git a/.github/scripts/coverage/requirements-ci.txt b/.github/actions/coverage_toolkit/requirements-ci.txt similarity index 100% rename from .github/scripts/coverage/requirements-ci.txt rename to .github/actions/coverage_toolkit/requirements-ci.txt diff --git a/.github/scripts/coverage/run_tests.py b/.github/actions/coverage_toolkit/run_tests.py similarity index 83% rename from .github/scripts/coverage/run_tests.py rename to .github/actions/coverage_toolkit/run_tests.py index 86aec2128189..eb099cc985df 100644 --- a/.github/scripts/coverage/run_tests.py +++ b/.github/actions/coverage_toolkit/run_tests.py @@ -18,14 +18,26 @@ CppTestCase, LOGGER, env_from_assignments, + get_config_dir, load_cpp_tests, load_js_tests, load_python_tests, + resolve_workspace_path, run_cmd, ) SCRIPT_DIR = Path(__file__).resolve().parent -CONFIG_DIR = SCRIPT_DIR / "config" +DEFAULT_PYTHON_COVERAGE_OMIT = ( + "*/tests/*", + "*/thirdparty/*", + "*/docs/*", + "*/samples/*", + "*/tools/*", + "*/src/bindings/js/node/tests/*", + "*/src/bindings/python/tests/*", + "*.pb.cc", + "*.pb.h", +) @dataclass(frozen=True) @@ -222,7 +234,7 @@ def _append_summary( lines.extend(extra_lines) lines.extend( [ - f"{suite.label} test selection: {', '.join(selected_names) if selected_names else 'all enabled tests'}", + f"{suite.label} test selection: {', '.join(selected_names) if selected_names else 'all configured tests'}", f"{suite.label} tests executed: {executed}", f"{suite.label} tests passed: {passed}", f"{suite.label} tests failed: {len(failed)}", @@ -299,6 +311,8 @@ def _pycov_config( repo_source_dir: Path, workspace: Path, installed_dirs: tuple[Path, ...] = (), + path_alias: str = "openvino", + omit_patterns: tuple[str, ...] = DEFAULT_PYTHON_COVERAGE_OMIT, ) -> str: """Build the coverage.py config used by pytest-cov and coverage CLI.""" branch_line = "branch = True\n" if branch_coverage else "" @@ -316,22 +330,15 @@ def _pycov_config( if value not in path_entries: path_entries.append(value) path_entries_text = "\n ".join(path_entries) + omit_entries_text = "\n ".join(omit_patterns) return f"""[run] source = {source_value} {branch_line}omit = - */tests/* - */thirdparty/* - */docs/* - */samples/* - */tools/* - */src/bindings/js/node/tests/* - */src/bindings/python/tests/* - *.pb.cc - *.pb.h + {omit_entries_text} [paths] -openvino = +{path_alias} = {path_entries_text} """ @@ -429,7 +436,44 @@ def _run_logged_command( return completed.returncode -def _normalize_python_xml_filename(raw_path: str, *, repo_source_dir: Path, installed_dirs: tuple[Path, ...]) -> str: +def _env_text(name: str, default: str) -> str: + """Read a stripped environment value with a default.""" + raw = os.environ.get(name, "").strip() + return raw or default + + +def _env_path(name: str, default: Path, *, workspace: Path) -> Path: + """Read an absolute or workspace-relative path from the environment.""" + raw = os.environ.get(name, "").strip() + if not raw: + return default + return resolve_workspace_path(raw, workspace=workspace) + + +def _env_path_list(name: str, *, workspace: Path) -> list[Path]: + """Read a path list from the environment, accepting pathsep and newlines.""" + raw = os.environ.get(name, "").strip() + if not raw: + return [] + normalized = raw.replace("\n", os.pathsep) + return [resolve_workspace_path(item, workspace=workspace) for item in normalized.split(os.pathsep) if item.strip()] + + +def _env_patterns(name: str, default: tuple[str, ...]) -> tuple[str, ...]: + """Read newline-separated coverage patterns from the environment.""" + raw = os.environ.get(name, "").strip() + if not raw: + return default + return tuple(line.strip() for line in raw.splitlines() if line.strip()) + + +def _normalize_python_xml_filename( + raw_path: str, + *, + repo_source_dir: Path, + installed_dirs: tuple[Path, ...], + path_alias: str, +) -> str: """Map coverage.py XML filenames back to repo-relative Python source paths.""" raw = raw_path.strip() if not raw: @@ -438,8 +482,9 @@ def _normalize_python_xml_filename(raw_path: str, *, repo_source_dir: Path, inst path = Path(raw) if not path.is_absolute(): stripped = raw.removeprefix("./") - if stripped.startswith("openvino/"): - stripped = stripped[len("openvino/") :] + alias_prefix = f"{path_alias}/" + if stripped.startswith(alias_prefix): + stripped = stripped[len(alias_prefix) :] return Path(stripped).as_posix() try: @@ -459,12 +504,12 @@ def _normalize_python_xml_filename(raw_path: str, *, repo_source_dir: Path, inst continue idx = parts.index(marker) tail = parts[idx + 1 :] - if tail and tail[0] == "openvino": + if tail and tail[0] == path_alias: return Path(*tail[1:]).as_posix() - if "openvino" in parts: - openvino_indexes = [idx for idx, part in enumerate(parts) if part == "openvino"] - for idx in reversed(openvino_indexes): + if path_alias in parts: + alias_indexes = [idx for idx, part in enumerate(parts) if part == path_alias] + for idx in reversed(alias_indexes): tail = parts[idx + 1 :] if tail: return Path(*tail).as_posix() @@ -472,7 +517,14 @@ def _normalize_python_xml_filename(raw_path: str, *, repo_source_dir: Path, inst return raw -def _rewrite_python_coverage_xml(*, xml_path: Path, workspace: Path, repo_source_dir: Path, installed_dirs: tuple[Path, ...]) -> None: +def _rewrite_python_coverage_xml( + *, + xml_path: Path, + workspace: Path, + repo_source_dir: Path, + installed_dirs: tuple[Path, ...], + path_alias: str, +) -> None: """Rewrite Python XML coverage paths to the repo source layout expected by Codecov.""" if not xml_path.is_file(): return @@ -496,6 +548,7 @@ def _rewrite_python_coverage_xml(*, xml_path: Path, workspace: Path, repo_source filename, repo_source_dir=repo_source_dir, installed_dirs=installed_dirs, + path_alias=path_alias, ) tree.write(xml_path, encoding="utf-8", xml_declaration=True) @@ -535,7 +588,7 @@ def _run_cpp_test( if not os.access(exe, os.X_OK): return _TestRunResult(test.name, "skipped", f"{test.name} (binary not executable: {test.binary})") - env = env_from_assignments(test.extra_env) + env = env_from_assignments(_expand(test.extra_env)) existing_ld = env.get("LD_LIBRARY_PATH", "") if runtime_ld_library_path: env["LD_LIBRARY_PATH"] = f"{runtime_ld_library_path}:{existing_ld}" if existing_ld else runtime_ld_library_path @@ -630,7 +683,7 @@ def _copy_js_lcov(*, source: Path, target: Path, workspace: Path, branch_coverag def run_cpp(ctx: CoverageContext) -> None: """Execute configured C++ tests and record execution statistics.""" - config = CONFIG_DIR / "tests_cpp.yml" + config = get_config_dir() / "tests_cpp.yml" selected_names = _selected_test_names(CPP_SUITE.selection_env) tests = _filter_selected_tests(load_cpp_tests(config, ctx.test_profile), selected_names, suite_label=CPP_SUITE.label) @@ -639,16 +692,11 @@ def run_cpp(ctx: CoverageContext) -> None: shutil.rmtree(ctx.paths.build_dir / "gcov", ignore_errors=True) results_by_name: dict[str, _TestRunResult] = {} - enabled_tests: list[tuple[CppTestCase, str]] = [] - for test in tests: - if not test.enabled: - results_by_name[test.name] = _TestRunResult(test.name, "skipped", f"{test.name} ({test.skip_reason})") - continue - enabled_tests.append((test, test.args.replace("__MODEL_PATH__", str(ctx.paths.model_path)))) + configured_tests = [(test, test.args.replace("__MODEL_PATH__", str(ctx.paths.model_path))) for test in tests] - if enabled_tests: + if configured_tests: runtime_ld_library_path = _runtime_ld_library_path(ctx) - for result in _run_cpp_tests_serial(ctx, enabled_tests, runtime_ld_library_path=runtime_ld_library_path): + for result in _run_cpp_tests_serial(ctx, configured_tests, runtime_ld_library_path=runtime_ld_library_path): results_by_name[result.name] = result results = [results_by_name[test.name] for test in tests if test.name in results_by_name] @@ -661,60 +709,63 @@ def run_cpp(ctx: CoverageContext) -> None: extra_lines=[ f"Test profile: {ctx.test_profile}", f"GPU mode: {'true' if ctx.run_gpu_tests else 'false'}", - f"NPU mode: {'true' if ctx.run_npu_tests else 'false'}", "", ], - no_execution_warning=( - f"No C++ tests were executed (all skipped). Check restored binaries under: {ctx.paths.bin_dir}" - if results - else None - ), + no_execution_warning=f"No C++ tests were executed. Check restored binaries under: {ctx.paths.bin_dir}", failure_warning="One or more C++ tests failed; continuing to coverage generation.", ) def run_python(ctx: CoverageContext) -> None: """Execute configured Python tests and export coverage results.""" - config = CONFIG_DIR / "tests_python.yml" + config = get_config_dir() / "tests_python.yml" selected_names = _selected_test_names(PYTHON_SUITE.selection_env) tests = _filter_selected_tests(load_python_tests(config, ctx.test_profile), selected_names, suite_label=PYTHON_SUITE.label) results: list[_TestRunResult] = [] - if not any(test.enabled for test in tests): - for test in tests: - if not test.enabled: - results.append(_TestRunResult(test.name, "skipped", f"{test.name} ({test.skip_reason})")) + if not tests: _finalize_suite( ctx, PYTHON_SUITE, selected_names=selected_names, results=results, - no_execution_warning=f"No Python tests are enabled for TEST_PROFILE={ctx.test_profile}; skipping Python suite.", + no_execution_warning=f"No Python tests are configured for TEST_PROFILE={ctx.test_profile}; skipping Python suite.", ) return - tests_dir = ctx.paths.install_pkg_dir / "tests" - py_source_root = ctx.workspace / "src" / "bindings" / "python" / "src" - py_source_dir = py_source_root / "openvino" - src_py_tests = ctx.workspace / "src" / "bindings" / "python" / "tests" - onnx_py_tests = ctx.workspace / "src" / "frontends" / "onnx" / "tests" / "tests_python" - layer_tests = ctx.workspace / "tests" / "layer_tests" + python_package = _env_text("COVERAGE_PYTHON_PACKAGE", "openvino") + installed_package = _env_text("COVERAGE_PYTHON_INSTALLED_PACKAGE", python_package) + path_alias = _env_text("COVERAGE_PYTHON_PATH_ALIAS", python_package) + tests_dir = _env_path("COVERAGE_PYTHON_TESTS_DIR", ctx.paths.install_pkg_dir / "tests", workspace=ctx.workspace) + py_source_dir = _env_path( + "COVERAGE_PYTHON_SOURCE_DIR", + ctx.workspace / "src" / "bindings" / "python" / "src" / path_alias, + workspace=ctx.workspace, + ) + layer_tests = _env_path("COVERAGE_PYTHON_LAYER_TESTS_DIR", ctx.workspace / "tests" / "layer_tests", workspace=ctx.workspace) py_cov_config = ctx.workspace / ".python_coverage_ci.rc" python_coverage_debug_dir = ctx.workspace / ".tmp" / "python-coverage" pip_install_path = os.environ.get("PIP_INSTALL_PATH", "").strip() - installed_openvino_dir = _detect_installed_python_package_dir("openvino") - if installed_openvino_dir is None and pip_install_path: - fallback_dir = Path(pip_install_path) / "openvino" + installed_python_package_dir = _detect_installed_python_package_dir(installed_package) + if installed_python_package_dir is None and pip_install_path: + fallback_dir = Path(pip_install_path) / installed_package if fallback_dir.is_dir(): - installed_openvino_dir = fallback_dir + installed_python_package_dir = fallback_dir - py_cov_source_dir = installed_openvino_dir if installed_openvino_dir is not None else py_source_dir + py_cov_source_dir = installed_python_package_dir if installed_python_package_dir is not None else py_source_dir py_cov_source = str(py_cov_source_dir) - wheel_lib_dir = Path(pip_install_path) / "openvino" / "libs" if pip_install_path else None + wheel_lib_dir = ( + _env_path("COVERAGE_PYTHON_WHEEL_LIB_DIR", Path(pip_install_path) / "openvino" / "libs", workspace=ctx.workspace) + if pip_install_path + else None + ) runtime_extra_paths = [tests_dir] + if installed_python_package_dir is not None: + runtime_extra_paths.append(installed_python_package_dir) if wheel_lib_dir is not None: runtime_extra_paths.append(wheel_lib_dir) + runtime_extra_paths.extend(_env_path_list("COVERAGE_PYTHON_RUNTIME_EXTRA_PATHS", workspace=ctx.workspace)) os.environ["LD_LIBRARY_PATH"] = ( f"{_runtime_ld_library_path(ctx, extra_paths=tuple(runtime_extra_paths))}:{os.environ.get('LD_LIBRARY_PATH', '')}" @@ -727,10 +778,10 @@ def run_python(ctx: CoverageContext) -> None: os.environ["PYTHONPATH"] = ":".join(entry for entry in python_path_entries if entry).rstrip(":") os.environ["PYTHONSAFEPATH"] = "1" os.environ["TESTS_DIR"] = str(tests_dir) - os.environ["SRC_PY_TESTS_DIR"] = str(src_py_tests) - os.environ["ONNX_PY_TESTS_DIR"] = str(onnx_py_tests) os.environ["WORKSPACE_LAYER_TESTS_DIR"] = str(layer_tests) + os.environ["PY_COV_SOURCE"] = py_cov_source os.environ["PY_COV_CONFIG"] = str(py_cov_config) + os.environ["PYTEST_COVERAGE_ARGS"] = f"--cov={shlex.quote(py_cov_source)} --cov-config={shlex.quote(str(py_cov_config))} --cov-append" py_cov_config.write_text( _pycov_config( @@ -738,7 +789,9 @@ def run_python(ctx: CoverageContext) -> None: runtime_source_dir=py_cov_source_dir, repo_source_dir=py_source_dir, workspace=ctx.workspace, - installed_dirs=tuple(path for path in (installed_openvino_dir,) if path is not None), + installed_dirs=tuple(path for path in (installed_python_package_dir,) if path is not None), + path_alias=path_alias, + omit_patterns=_env_patterns("COVERAGE_PYTHON_OMIT", DEFAULT_PYTHON_COVERAGE_OMIT), ), encoding="utf-8", ) @@ -746,10 +799,6 @@ def run_python(ctx: CoverageContext) -> None: run_cmd(["python3", "-m", "coverage", "erase"]) for test in tests: - if not test.enabled: - results.append(_TestRunResult(test.name, "skipped", f"{test.name} ({test.skip_reason})")) - continue - target = _expand(test.target) args = _expand(test.args) command = _expand(test.command) @@ -758,13 +807,6 @@ def run_python(ctx: CoverageContext) -> None: if test.kind == "pytest": cmd = _python_pytest_command(target, args, py_cov_source=py_cov_source, py_cov_config=py_cov_config) rc, duration_seconds = _timed_run(f"Python test: {test.name}", cmd, env=env) - elif test.kind == "pytest_if_dir": - if not Path(target).is_dir(): - LOGGER.warning("Skipping Python test group '%s' (missing: %s)", test.name, target) - results.append(_TestRunResult(test.name, "skipped", f"{test.name} (missing path)")) - continue - cmd = _python_pytest_command(target, args, py_cov_source=py_cov_source, py_cov_config=py_cov_config) - rc, duration_seconds = _timed_run(f"Python test: {test.name}", cmd, env=env) elif test.kind == "command": rc, duration_seconds = _timed_run( f"Python test: {test.name}", @@ -817,12 +859,13 @@ def run_python(ctx: CoverageContext) -> None: f"see {xml_log} and {debug_log}" ) - installed_dirs = tuple(path for path in (installed_openvino_dir,) if path is not None) + installed_dirs = tuple(path for path in (installed_python_package_dir,) if path is not None) _rewrite_python_coverage_xml( xml_path=xml_path, workspace=ctx.workspace, repo_source_dir=py_source_dir, installed_dirs=installed_dirs, + path_alias=path_alias, ) _finalize_suite( @@ -839,21 +882,18 @@ def run_js(ctx: CoverageContext) -> None: if shutil.which("node") is None or shutil.which("npm") is None: raise RuntimeError("Node.js/npm are not available in the coverage runtime environment.") - config = CONFIG_DIR / "tests_js.yml" + config = get_config_dir() / "tests_js.yml" selected_names = _selected_test_names(JS_SUITE.selection_env) tests = _filter_selected_tests(load_js_tests(config, ctx.test_profile), selected_names, suite_label=JS_SUITE.label) results: list[_TestRunResult] = [] - if not any(test.enabled for test in tests): - for test in tests: - if not test.enabled: - results.append(_TestRunResult(test.name, "skipped", f"{test.name} ({test.skip_reason})")) + if not tests: _finalize_suite( ctx, JS_SUITE, selected_names=selected_names, results=results, - no_execution_warning=f"No JS tests are enabled for TEST_PROFILE={ctx.test_profile}; skipping JS suite.", + no_execution_warning=f"No JS tests are configured for TEST_PROFILE={ctx.test_profile}; skipping JS suite.", ) return @@ -863,9 +903,6 @@ def run_js(ctx: CoverageContext) -> None: ).rstrip(":") for test in tests: - if not test.enabled: - results.append(_TestRunResult(test.name, "skipped", f"{test.name} ({test.skip_reason})")) - continue if test.kind != "command": results.append(_TestRunResult(test.name, "skipped", f"{test.name} (unknown kind: {test.kind})")) continue diff --git a/.github/actions/setup_python/action.yml b/.github/actions/setup_python/action.yml index 6a8b446ef998..aadd6a7e26bd 100644 --- a/.github/actions/setup_python/action.yml +++ b/.github/actions/setup_python/action.yml @@ -65,10 +65,3 @@ runs: $pipVersion = python3 -c "import pip; print(pip.__version__)" Write-Host "Using pip version: $pipVersion" "PIP_CACHE_DIR=${{ inputs.pip-cache-path }}/$pipVersion" >> $env:GITHUB_ENV - - # See PR #34185 for details, this is just for debug purposes and will be reverted - - if: ${{ runner.os == 'Windows' }} - name: Set PIP_INSTALL_COMMAND variable for Windows - shell: pwsh - run: | - "PIP_INSTALL_COMMAND=python3 c:/pip_diag.py install" >> $env:GITHUB_ENV diff --git a/.github/actions/wait-for-check-completion/package-lock.json b/.github/actions/wait-for-check-completion/package-lock.json index fbd00989f7dc..ec41b9cc8ab6 100644 --- a/.github/actions/wait-for-check-completion/package-lock.json +++ b/.github/actions/wait-for-check-completion/package-lock.json @@ -9,8 +9,8 @@ "version": "1.0.0", "license": "Apache-2.0", "dependencies": { - "@actions/core": "^3.0.0", - "@actions/github": "^6.0.0" + "@actions/core": "^3.0.1", + "@actions/github": "^9.1.1" }, "devDependencies": { "@vercel/ncc": "^0.38.1", @@ -18,9 +18,9 @@ } }, "node_modules/@actions/core": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@actions/core/-/core-3.0.0.tgz", - "integrity": "sha512-zYt6cz+ivnTmiT/ksRVriMBOiuoUpDCJJlZ5KPl2/FRdvwU3f7MPh9qftvbkXJThragzUZieit2nyHUyw53Seg==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@actions/core/-/core-3.0.1.tgz", + "integrity": "sha512-a6d/Nwahm9fliVGRhdhofo40HjHQasUPusmc7vBfyky+7Z+P2A1J68zyFVaNcEclc/Se+eO595oAr5nwEIoIUA==", "license": "MIT", "dependencies": { "@actions/exec": "^3.0.0", @@ -37,15 +37,6 @@ "undici": "^6.23.0" } }, - "node_modules/@actions/core/node_modules/undici": { - "version": "6.25.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.25.0.tgz", - "integrity": "sha512-ZgpWDC5gmNiuY9CnLVXEH8rl50xhRCuLNA97fAUnKi8RRuV4E6KG31pDTsLVUKnohJE0I3XDrTeEydAXRw47xg==", - "license": "MIT", - "engines": { - "node": ">=18.17" - } - }, "node_modules/@actions/exec": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@actions/exec/-/exec-3.0.0.tgz", @@ -56,26 +47,28 @@ } }, "node_modules/@actions/github": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/@actions/github/-/github-6.0.1.tgz", - "integrity": "sha512-xbZVcaqD4XnQAe35qSQqskb3SqIAfRyLBrHMd/8TuL7hJSz2QtbDwnNM8zWx4zO5l2fnGtseNE3MbEvD7BxVMw==", + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/@actions/github/-/github-9.1.1.tgz", + "integrity": "sha512-tL5JbYOBZHc0ngEnCsaDcryUizIUIlQyIMwy1Wkx93H5HzbBJ7TbiPx2PnFjBwZW0Vh05JmfFZhecE6gglYegA==", + "license": "MIT", "dependencies": { - "@actions/http-client": "^2.2.0", - "@octokit/core": "^5.0.1", - "@octokit/plugin-paginate-rest": "^9.2.2", - "@octokit/plugin-rest-endpoint-methods": "^10.4.0", - "@octokit/request": "^8.4.1", - "@octokit/request-error": "^5.1.1", - "undici": "^5.28.5" + "@actions/http-client": "^3.0.2", + "@octokit/core": "^7.0.6", + "@octokit/plugin-paginate-rest": "^14.0.0", + "@octokit/plugin-rest-endpoint-methods": "^17.0.0", + "@octokit/request": "^10.0.7", + "@octokit/request-error": "^7.1.0", + "undici": "^6.23.0" } }, "node_modules/@actions/http-client": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.2.3.tgz", - "integrity": "sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-3.0.2.tgz", + "integrity": "sha512-JP38FYYpyqvUsz+Igqlc/JG6YO9PaKuvqjM3iGvaLqFnJ7TFmcLyy2IDrY0bI0qCQug8E9K+elv5ZNfw62ZJzA==", + "license": "MIT", "dependencies": { "tunnel": "^0.0.6", - "undici": "^5.25.4" + "undici": "^6.23.0" } }, "node_modules/@actions/io": { @@ -545,14 +538,6 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, - "node_modules/@fastify/busboy": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", - "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", - "engines": { - "node": ">=14" - } - }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -902,147 +887,131 @@ } }, "node_modules/@octokit/auth-token": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-4.0.0.tgz", - "integrity": "sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-6.0.0.tgz", + "integrity": "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w==", + "license": "MIT", "engines": { - "node": ">= 18" + "node": ">= 20" } }, "node_modules/@octokit/core": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.2.2.tgz", - "integrity": "sha512-/g2d4sW9nUDJOMz3mabVQvOGhVa4e/BN/Um7yca9Bb2XTzPPnfTWHWQg+IsEYO7M3Vx+EXvaM/I2pJWIMun1bg==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-7.0.6.tgz", + "integrity": "sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q==", + "license": "MIT", "dependencies": { - "@octokit/auth-token": "^4.0.0", - "@octokit/graphql": "^7.1.0", - "@octokit/request": "^8.4.1", - "@octokit/request-error": "^5.1.1", - "@octokit/types": "^13.0.0", - "before-after-hook": "^2.2.0", - "universal-user-agent": "^6.0.0" + "@octokit/auth-token": "^6.0.0", + "@octokit/graphql": "^9.0.3", + "@octokit/request": "^10.0.6", + "@octokit/request-error": "^7.0.2", + "@octokit/types": "^16.0.0", + "before-after-hook": "^4.0.0", + "universal-user-agent": "^7.0.0" }, "engines": { - "node": ">= 18" + "node": ">= 20" } }, "node_modules/@octokit/endpoint": { - "version": "9.0.6", - "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-9.0.6.tgz", - "integrity": "sha512-H1fNTMA57HbkFESSt3Y9+FBICv+0jFceJFPWDePYlR/iMGrwM5ph+Dd4XRQs+8X+PUFURLQgX9ChPfhJ/1uNQw==", + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-11.0.3.tgz", + "integrity": "sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag==", + "license": "MIT", "dependencies": { - "@octokit/types": "^13.1.0", - "universal-user-agent": "^6.0.0" + "@octokit/types": "^16.0.0", + "universal-user-agent": "^7.0.2" }, "engines": { - "node": ">= 18" + "node": ">= 20" } }, "node_modules/@octokit/graphql": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-7.1.1.tgz", - "integrity": "sha512-3mkDltSfcDUoa176nlGoA32RGjeWjl3K7F/BwHwRMJUW/IteSa4bnSV8p2ThNkcIcZU2umkZWxwETSSCJf2Q7g==", + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-9.0.3.tgz", + "integrity": "sha512-grAEuupr/C1rALFnXTv6ZQhFuL1D8G5y8CN04RgrO4FIPMrtm+mcZzFG7dcBm+nq+1ppNixu+Jd78aeJOYxlGA==", + "license": "MIT", "dependencies": { - "@octokit/request": "^8.4.1", - "@octokit/types": "^13.0.0", - "universal-user-agent": "^6.0.0" + "@octokit/request": "^10.0.6", + "@octokit/types": "^16.0.0", + "universal-user-agent": "^7.0.0" }, "engines": { - "node": ">= 18" + "node": ">= 20" } }, "node_modules/@octokit/openapi-types": { - "version": "24.2.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", - "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==" + "version": "27.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-27.0.0.tgz", + "integrity": "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA==", + "license": "MIT" }, "node_modules/@octokit/plugin-paginate-rest": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-9.2.2.tgz", - "integrity": "sha512-u3KYkGF7GcZnSD/3UP0S7K5XUFT2FkOQdcfXZGZQPGv3lm4F2Xbf71lvjldr8c1H3nNbF+33cLEkWYbokGWqiQ==", + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-14.0.0.tgz", + "integrity": "sha512-fNVRE7ufJiAA3XUrha2omTA39M6IXIc6GIZLvlbsm8QOQCYvpq/LkMNGyFlB1d8hTDzsAXa3OKtybdMAYsV/fw==", + "license": "MIT", "dependencies": { - "@octokit/types": "^12.6.0" + "@octokit/types": "^16.0.0" }, "engines": { - "node": ">= 18" + "node": ">= 20" }, "peerDependencies": { - "@octokit/core": "5" - } - }, - "node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/openapi-types": { - "version": "20.0.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-20.0.0.tgz", - "integrity": "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA==" - }, - "node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/types": { - "version": "12.6.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.6.0.tgz", - "integrity": "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw==", - "dependencies": { - "@octokit/openapi-types": "^20.0.0" + "@octokit/core": ">=6" } }, "node_modules/@octokit/plugin-rest-endpoint-methods": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-10.4.1.tgz", - "integrity": "sha512-xV1b+ceKV9KytQe3zCVqjg+8GTGfDYwaT1ATU5isiUyVtlVAO3HNdzpS4sr4GBx4hxQ46s7ITtZrAsxG22+rVg==", + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-17.0.0.tgz", + "integrity": "sha512-B5yCyIlOJFPqUUeiD0cnBJwWJO8lkJs5d8+ze9QDP6SvfiXSz1BF+91+0MeI1d2yxgOhU/O+CvtiZ9jSkHhFAw==", + "license": "MIT", "dependencies": { - "@octokit/types": "^12.6.0" + "@octokit/types": "^16.0.0" }, "engines": { - "node": ">= 18" + "node": ">= 20" }, "peerDependencies": { - "@octokit/core": "5" - } - }, - "node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/openapi-types": { - "version": "20.0.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-20.0.0.tgz", - "integrity": "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA==" - }, - "node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/types": { - "version": "12.6.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.6.0.tgz", - "integrity": "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw==", - "dependencies": { - "@octokit/openapi-types": "^20.0.0" + "@octokit/core": ">=6" } }, "node_modules/@octokit/request": { - "version": "8.4.1", - "resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.4.1.tgz", - "integrity": "sha512-qnB2+SY3hkCmBxZsR/MPCybNmbJe4KAlfWErXq+rBKkQJlbjdJeS85VI9r8UqeLYLvnAenU8Q1okM/0MBsAGXw==", + "version": "10.0.8", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.8.tgz", + "integrity": "sha512-SJZNwY9pur9Agf7l87ywFi14W+Hd9Jg6Ifivsd33+/bGUQIjNujdFiXII2/qSlN2ybqUHfp5xpekMEjIBTjlSw==", + "license": "MIT", "dependencies": { - "@octokit/endpoint": "^9.0.6", - "@octokit/request-error": "^5.1.1", - "@octokit/types": "^13.1.0", - "universal-user-agent": "^6.0.0" + "@octokit/endpoint": "^11.0.3", + "@octokit/request-error": "^7.0.2", + "@octokit/types": "^16.0.0", + "fast-content-type-parse": "^3.0.0", + "json-with-bigint": "^3.5.3", + "universal-user-agent": "^7.0.2" }, "engines": { - "node": ">= 18" + "node": ">= 20" } }, "node_modules/@octokit/request-error": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-5.1.1.tgz", - "integrity": "sha512-v9iyEQJH6ZntoENr9/yXxjuezh4My67CBSu9r6Ve/05Iu5gNgnisNWOsoJHTP6k0Rr0+HQIpnH+kyammu90q/g==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-7.1.0.tgz", + "integrity": "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw==", + "license": "MIT", "dependencies": { - "@octokit/types": "^13.1.0", - "deprecation": "^2.0.0", - "once": "^1.4.0" + "@octokit/types": "^16.0.0" }, "engines": { - "node": ">= 18" + "node": ">= 20" } }, "node_modules/@octokit/types": { - "version": "13.10.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", - "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-16.0.0.tgz", + "integrity": "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg==", + "license": "MIT", "dependencies": { - "@octokit/openapi-types": "^24.2.0" + "@octokit/openapi-types": "^27.0.0" } }, "node_modules/@sinclair/typebox": { @@ -1369,9 +1338,10 @@ } }, "node_modules/before-after-hook": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", - "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==" + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-4.0.0.tgz", + "integrity": "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==", + "license": "Apache-2.0" }, "node_modules/brace-expansion": { "version": "1.1.13", @@ -1663,11 +1633,6 @@ "node": ">=0.10.0" } }, - "node_modules/deprecation": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", - "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==" - }, "node_modules/detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -1798,6 +1763,22 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/fast-content-type-parse": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-3.0.0.tgz", + "integrity": "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -2763,6 +2744,12 @@ "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", "dev": true }, + "node_modules/json-with-bigint": { + "version": "3.5.8", + "resolved": "https://registry.npmjs.org/json-with-bigint/-/json-with-bigint-3.5.8.tgz", + "integrity": "sha512-eq/4KP6K34kwa7TcFdtvnftvHCD9KvHOGGICWwMFc4dOOKF5t4iYqnfLK8otCRCRv06FXOzGGyqE8h8ElMvvdw==", + "license": "MIT" + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -2946,6 +2933,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, "dependencies": { "wrappy": "1" } @@ -3470,14 +3458,12 @@ } }, "node_modules/undici": { - "version": "5.29.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz", - "integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==", - "dependencies": { - "@fastify/busboy": "^2.0.0" - }, + "version": "6.25.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.25.0.tgz", + "integrity": "sha512-ZgpWDC5gmNiuY9CnLVXEH8rl50xhRCuLNA97fAUnKi8RRuV4E6KG31pDTsLVUKnohJE0I3XDrTeEydAXRw47xg==", + "license": "MIT", "engines": { - "node": ">=14.0" + "node": ">=18.17" } }, "node_modules/undici-types": { @@ -3487,9 +3473,10 @@ "dev": true }, "node_modules/universal-user-agent": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz", - "integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==" + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.3.tgz", + "integrity": "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==", + "license": "ISC" }, "node_modules/update-browserslist-db": { "version": "1.1.3", @@ -3579,7 +3566,8 @@ "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true }, "node_modules/write-file-atomic": { "version": "4.0.2", diff --git a/.github/actions/wait-for-check-completion/package.json b/.github/actions/wait-for-check-completion/package.json index dd8e7c3de51f..74eec8e37b83 100644 --- a/.github/actions/wait-for-check-completion/package.json +++ b/.github/actions/wait-for-check-completion/package.json @@ -23,8 +23,8 @@ "author": "OpenVINO Developers", "license": "Apache-2.0", "dependencies": { - "@actions/core": "^3.0.0", - "@actions/github": "^6.0.0" + "@actions/core": "^3.0.1", + "@actions/github": "^9.1.1" }, "devDependencies": { "@vercel/ncc": "^0.38.1", diff --git a/.github/agents-prototype/analyze-and-convert.agent.md b/.github/agents-prototype/analyze-and-convert.agent.md new file mode 100644 index 000000000000..a65729f0009b --- /dev/null +++ b/.github/agents-prototype/analyze-and-convert.agent.md @@ -0,0 +1,180 @@ +--- +name: Analyze and Convert Agent +model: claude-sonnet-4.6 +description: Attempts HuggingFace model conversion to OpenVINO IR using a multi-strategy matrix, deeply probes model properties and requirements, classifies failures with full traceback analysis, and produces a structured diagnostic report with precise routing signals for the orchestrator. +--- +# Analyze and Convert Agent + +## Role + +Active conversion agent and diagnostic producer. Sits between the fast +first-attempt **Deployer** and the specialist coding agents. + +When Deployer fails (or on initial investigation), this agent: +1. **Probes** the model — collects architecture profile, parameter count, + special flags, and tokenizer info without downloading weights. +2. **Attempts conversion** — runs a systematic strategy matrix + (fp16 / int8 / int4 × stable / git-HEAD × with/without trust_remote_code). +3. **Classifies every failure** — maps tracebacks to the 11-class error taxonomy + and extracts routing signals. +4. **Builds a conversion report** — posts it to the tracking issue and emits a + machine-readable `agent-complete` marker with full context for the orchestrator. + +This agent does NOT write code, create PRs, or fix libraries. +Its sole purpose is **maximum diagnostic fidelity** to enable the right specialist +to succeed on the first attempt. + +--- + +## Called by + +- **Common Orchestrator** — after Deployer fails +- Can also run standalone for diagnosis of any model + +--- + +## Skills + +Execute in strict order. Each skill produces files consumed by the next. + +This agent follows the **[`skills/analyze-and-convert/SKILL.md`](skills/analyze-and-convert/SKILL.md)** workflow. +SKILL.md lists all step files with their purpose and execution order. + +--- + +## Inputs + +| Input | Source | Description | +|-------|--------|-------------| +| `model_id` | Orchestrator prompt | HuggingFace model ID (`org/name`) | +| `error_log` | Optional — from Deployer | Error log from previous attempt to seed strategy selection | +| `pass_num` | Env `PASS_NUM` | Pipeline pass number for manifest entries | + +--- + +## Outputs + +All outputs are written to `agent-results/analyze-and-convert/`. + +| File | Description | +|------|-------------| +| `model_profile.json` | Model architecture fingerprint | +| `conversion_attempts.json` | All strategy attempts with full stdout/stderr | +| `routing_signals.json` | Machine-readable signals for orchestrator | +| `conversion_report.md` | Human-readable full report (also posted to GitHub issue) | +| `ov_model_*/` | IR files from the first successful strategy (if any) | + +--- + +## Execution + +### Step 0 — Setup + +```bash +# Work from repo root +cd +mkdir -p agent-results/analyze-and-convert +cd agent-results/analyze-and-convert + +# Minimal venv (probe-model only needs transformers + requests) +python -m venv venv-probe +source venv-probe/bin/activate # Linux/macOS +# venv-probe\Scripts\activate # Windows +pip install -q transformers requests +``` + +### Step 1 — Probe + +Invoke `skills/analyze-and-convert/probe-model.md`. + +Produces: `model_profile.json` + +### Step 2 — Convert + +Invoke `skills/analyze-and-convert/try-conversion.md`. + +Strategy selection is guided by `model_profile.json`: +- If `optimum_supported == false` → still attempt with git-HEAD (optimum may have + unreleased support) before declaring unsupported +- If `trust_remote_code_required == true` → all strategies include `--trust-remote-code` +- If `estimated_params_b > 7` → add int4 AWQ strategy +- If Deployer's error_log mentions a specific strategy failure → skip equivalent + strategy to avoid repeating known failures + +Produces: `conversion_attempts.json`, `ov_model_*/` (if any success) + +### Step 3 — Classify (on failure or partial success) + +Skip this step only if conversion succeeded **and** inference check passed. + +Invoke `skills/analyze-and-convert/classify-failure.md`. + +Produces: `routing_signals.json`, `error_excerpts.json` + +### Step 4 — Report + +Always execute, regardless of outcome. + +Invoke `skills/analyze-and-convert/build-report.md`. + +Produces: `conversion_report.md`, saves to `agent-results/`, emits `agent-complete` marker. + +--- + +## Routing Output + +This agent always ends by printing an `agent-complete` marker (from `build-report`): + +``` + +``` + +### Success path (`status: success`) +- Conversion + inference OK → `next_agent: wwb` + +### Partial path (`status: partial`) +- IR produced but inference failed → `next_agent: enable-operator` +- With `error_class: inference_runtime_error` + +### Failure paths (`status: failed`) + +| `error_class` | `next_agent` | Key `next_context` signals | +|---------------|-------------|---------------------------| +| `optimum_unsupported_arch` | `optimum-intel` | `requires_optimum_new_arch=true` | +| `optimum_export_bug` | `optimum-intel` | full error excerpt | +| `missing_model_dependency` | `optimum-intel` | missing package name | +| `unknown_arch_transformers_too_old` | `optimum-intel` | `requires_transformers_upgrade=true`, `transformers_override` | +| `missing_conversion_rule` | `enable-operator` | op name from traceback | +| `frontend_error` | `enable-operator` | error excerpt | +| `ir_validation_error` | `enable-operator` | IR validation error | +| `inference_runtime_error` | `enable-operator` | runtime exception | +| `genai_unsupported` | `openvino-genai` | pipeline error detail | +| `tokenizer_error` | `openvino-tokenizers` | tokenizer error detail | +| `unknown` | `optimum-intel` | full error log | + +--- + +## Constraints + +- Does **not** create branches, open PRs, or modify any library code. +- Does **not** download model weights (probing uses config only). +- Each conversion strategy runs in an **isolated venv** — never pollutes the + base environment. +- Stops strategy matrix on first successful conversion — does not exhaustively + try all strategies if an early one works. +- Posts **one** GitHub issue comment (from `build-report`). + +## Creating Pull Requests + +When your work is complete and all tests pass, follow the +[`submit-draft-pr`](skills/submit-draft-pr/SKILL.md) skill — it handles branch +naming, existing-PR deduplication, fork creation, and `gh pr create`. +Skip silently if `gh` is unavailable, not authenticated, or the command fails. \ No newline at end of file diff --git a/.github/agents-prototype/core-opspec.agent.md b/.github/agents-prototype/core-opspec.agent.md new file mode 100644 index 000000000000..0a806484f87e --- /dev/null +++ b/.github/agents-prototype/core-opspec.agent.md @@ -0,0 +1,239 @@ +--- +name: Core Opset Agent +description: OpenVINO Core operation specialist. Implements missing or incomplete operation specifications in the OpenVINO opset — C++ class definition, shape inference, opset registration, reference kernel, and RST documentation. Invoked by OV Orchestrator after the FE Agent escalates with status=escalate_to_core, or directly when a core op is missing. On completion unlocks the parallel Transformation, CPU, and GPU agents. +model: claude-sonnet-4.6 +--- +# Core OpSpec Agent + +## Role + +OpenVINO Core operation specialist. Handles missing or incomplete operation +specifications and implementations in the OpenVINO core. + +**Pipeline position:** Priority 2 — called after the FE Agent either fails or +explicitly escalates because a new Core op is needed. + +## Output + +Write all logs, results, and patches to `agent-results/core-opspec/`. + +## Called by + +- **OV Orchestrator** (priority 2 — after FE Agent) +- Triggered when FE Agent returns `status=escalate_to_core` + +--- + +## Environment + +| Item | Notes | +|---|---| +| **OpenVINO repository** | Current working directory — run from the `openvinotoolkit/openvino` repository root | +| **Skills** | `.github/agents-prototype/skills/` — relative to the repository root | + +### Python Package Bootstrap + +Follow **[`skills/python-bootstrap/SKILL.md`](skills/python-bootstrap/SKILL.md) — Path A** (no source build). + +--- + +## Skills + +This agent follows the **[`skills/add-core-op/SKILL.md`](skills/add-core-op/SKILL.md)** workflow. +SKILL.md lists all step files with their purpose and execution order. + +## Code Quality + +Before writing any code, read [`.github/copilot-instructions.md`](.github/copilot-instructions.md) +and apply its conventions. Key points for this agent: +- Op class in `ov::op::vN` namespace; registered in the matching opset header +- Filenames: `snake_case`; class names: `CamelCase` +- `OPENVINO_OP` macro required; `visit_attributes` must cover all attributes +- Run `clang-format -i ` (config: `src/.clang-format`) before committing +- Every new op must include `type_prop`, `visitors`, and `op_reference` tests + +> **Step 0 guard:** Before Step 1, check whether the target opset exists: +> ```bash +> ls ./src/core/include/openvino/opsets/opsetX.hpp 2>/dev/null \ +> && echo "opset exists — skip step 0" \ +> || echo "opset missing — run step0-opset-init first" +> ``` + +--- + +## Execution Model + +### Entry: FE escalation vs direct classification + +The agent can be triggered in two ways: + +1. **FE escalation** (primary path): FE Agent returns `status=escalate_to_core` + with a structured `escalation_payload` containing the PyTorch/TF/ONNX op spec. + Use this payload as the primary source for Analysis skill — the framework + semantics, inputs, outputs, and attributes are already captured. + +2. **Direct classification** (fallback): OV Orchestrator classifies the error + directly as `core_op` without a prior FE escalation (e.g. the error comes + from an ONNX model that bypasses the FE entirely). + +### Step-by-step + +1. Receive `error_context` from OV Orchestrator (op name, error log, and + optionally the `escalation_payload` from FE Agent). + +1.5. **(Conditional) Run Opset Init** skill (`core-opset-initialization`): + - Check whether the target opset (e.g. `opset17`) already has + `opsetX.hpp` in `./src/core/include/openvino/opsets/`. + - If the file is **absent** → run `skills/add-core-op/step0-opset-init.md` + to create all scaffolding files before proceeding. + - If the file is **present** → skip this step entirely. + +2. Run **Analysis** skill (`core_op_analysis`): + - Use `escalation_payload` as starting point when available (avoids + redundant research — the FE Agent already checked framework docs). + - Derive the PyTorch/ONNX/TF op spec from the payload's `framework_spec_url`. + - If `decomposable=yes` → report back to OV Orchestrator (defer to + Transformation Agent); stop. + - If `decomposable=no` → continue to Implementation. + +3. Run **Implementation** skill (`core_op_implementation`): + - Create `.hpp`, `.cpp`, reference kernel, shape inference. + - Register in opset table (latest opset only — never modify older tables). + +4. Run **Testing** skill (`core_op_testing`): + - `type_prop`, `visitors`, opset count, conformance, `op_reference`. + - Fix and re-run if tests fail. + +5. Run **Specification** skill (`core_op_specification`): + - Create `.rst` documentation following OV op spec conventions. + +6. **Write op spec to agent-results/** (artifact for parallel agents): + ```bash + mkdir -p agent-results/core-opspec + cp op_spec_.json agent-results/core-opspec/ + python3 -c " + import json, os + state = {} + try: + with open('agent-results/pipeline_state.json') as f: + state = json.load(f) + except FileNotFoundError: + pass + state.setdefault('ov_orchestrator', {})['op_spec_path'] = 'agent-results/core-opspec/op_spec_.json' + state['ov_orchestrator']['op_spec_ready'] = True + os.makedirs('agent-results', exist_ok=True) + with open('agent-results/pipeline_state.json', 'w') as f: + json.dump(state, f, indent=2) + " + ``` + Writing to `agent-results/` is the trigger signal — Transformation, CPU, and GPU agents + read from `agent-results/core-opspec/` and `agent-results/pipeline_state.json`. + +7. Generate `git format-patch` for all core changes and save to `agent-results/core-opspec/`. + +8. Report `success` + patch to OV Orchestrator with `op_spec_ready=true`. + +--- + +## Parallel Agent Unlock + +Once the Core OpSpec Agent completes Step 5 (Specification) and posts the op +spec comment, the following agents are **unblocked to run in parallel**: + +| Agent | What they use from Core spec | +|-------|------------------------------| +| **Transformation Agent** | Op signature + decomposable sub-patterns from spec | +| **CPU Agent** | Op signature + reference kernel from `.hpp` for CPU implementation | +| **GPU Agent** | Op signature + math semantics for OpenCL kernel sketch | + +These agents consume the spec artifact from `agent-results/core-opspec/` — they do +not need to wait for Core implementation tests to pass; the spec document alone +is sufficient for them to begin. + +--- + +## Source Repository + +- Reference code: current working directory (the OpenVINO repository root) +- **Do NOT build OpenVINO** — compilation takes too long on a local machine. +- Produce `git format-patch` files; save to `agent-results/core-opspec/`. + +## Key References + +- OpenVINO operations: https://docs.openvino.ai/2025/documentation/openvino-ir-format/operation-sets.html +- Existing ops for style alignment: `openvino/src/core/include/openvino/op/` + +## Constraints + +- Reports only to OV Orchestrator - does not call other agents directly. +- Op specs must follow the OpenVINO operation set conventions exactly. +- Register new ops only in the **latest** opset — never modify older opset tables. +- Do not break compatibility of existing ops. +- Always write the op spec to `agent-results/core-opspec/` before reporting `success` + so parallel agents can start without delay. + +--- + +## PR Creation + +**`pr_mode: delegated_to_orchestrator`** (invoked by Enable Operator Agent): do **not** create a +PR. Write patches to the result JSON only. The orchestrator creates one central draft PR in Phase 7. + +**Standalone invocation** (no `pr_mode` set): follow the [`submit-draft-pr`](skills/submit-draft-pr/SKILL.md) +skill — it handles branch naming, existing-PR deduplication, fork creation, and `gh pr create`. +Skip silently if `gh` is unavailable, not authenticated, or the command fails. + +--- + +## Checkpoint Protocol + +You are given a **120-minute session** (GitHub Actions timeout). Post a checkpoint +comment to the tracking issue **after completing each numbered step**, not only +when done or escalating. + +This allows: +- A human to see real-time progress without downloading anything. +- A re-triggered session to resume exactly where this one left off. + +### Checkpoint comment format + +Write a checkpoint to `agent-results/core-opspec/checkpoints.md` with this structure after every step: + +```markdown +## ⏱ Checkpoint — Step complete () + +| Field | Value | +|---|---| +| **Step completed** | `` | +| **Outcome** | `success` \| `failed` \| `partial` | +| **Key finding** | `` | +| **Next step** | `` | + + +``` + +### Re-trigger resume + +When invoked on an issue that already has checkpoint comments from a previous +run, read them first and: +1. Find the last `` marker and its `step` value. +2. Resume from the step immediately after the last completed one. +3. Do not repeat already-completed steps. +4. State explicitly: `Resuming after previous session — continuing from Step `. + +--- + +## Job Communication Protocol + +When your work is complete — regardless of outcome — post a comment to the +tracking issue containing **exactly** this marker on its own line: + + + +- `agent`: `"core_opspec_agent"` (fixed) +- `status`: `"success"` | `"failed"` +- `op_spec_ready`: **CRITICAL** — set to `"true"` once the op spec is published to the tracking issue; this value unlocks the Transformation/CPU/GPU parallel gate in the parent workflow + +Place your full Markdown report above or below this marker. +The polling job reads **only** this marker to forward outputs to the orchestrator. + diff --git a/.github/agents-prototype/cpu.agent.md b/.github/agents-prototype/cpu.agent.md new file mode 100644 index 000000000000..a12303c5ad3b --- /dev/null +++ b/.github/agents-prototype/cpu.agent.md @@ -0,0 +1,203 @@ +--- +name: CPU Plugin Agent +description: OpenVINO Intel CPU plugin specialist. Implements ISA-aware CPU kernels for new operations: node registration, JIT executors (AVX2/AVX-512/AMX), oneDNN-backed paths, and functional tests. Runs in parallel with the Transformation and GPU agents after Core OpSpec publishes the op spec. +model: claude-sonnet-4.6 +--- +# CPU Agent + +## Role + +CPU plugin specialist. Handles CPU-specific operation enablement, node +implementation, ISA-aware optimization (JIT kernels), and testing for the +OpenVINO CPU backend. + +## Output + +Write all logs, results, and patches to `agent-results/cpu/`. + +## Called by + +- **OV Orchestrator** (priority 3 — parallel with Transformation and GPU, after Core OpSpec) + +--- + +## Environment + +| Item | Notes | +|---|---| +| **OpenVINO repository** | Current working directory — run from the `openvinotoolkit/openvino` repository root | +| **Skills** | `.github/agents-prototype/skills/` — relative to the repository root | + +### Python Package Bootstrap + +Follow **[`skills/python-bootstrap/SKILL.md`](skills/python-bootstrap/SKILL.md) — Path A** (no source build). +Do **not** `pip install openvino` — use the locally built package to avoid +having two conflicting OpenVINO installations in the same environment. + +--- + +## Skills + +This agent follows the **[`skills/add-cpu-op/SKILL.md`](skills/add-cpu-op/SKILL.md)** workflow. +SKILL.md lists all step files with their purpose and execution order. + +## Execution Model + +1. Receive `error_context` from OV Orchestrator (contains op name, error log, + opset version, op specification). +2. Run **Op Analysis** skill: + - Locate core op class and reference implementation. + - Determine the CPU implementation approach (reference-only, JIT-optimized, + oneDNN-backed, or executor-based). + - Identify inputs/outputs, attributes, supported precisions, and layout + requirements. + - Determine target ISA levels (AVX2, AVX-512). +3. Run **Node Implementation** skill: + - Create node header and source following the `Node` base class pattern. + - Register the node in `cpu_types.h`, `cpu_types.cpp`, and `nodes_factory.cpp`. + - Implement `getSupportedDescriptors()`, `created()`, `isSupportedOperation`, `initSupportedPrimitiveDescriptors`, + `createPrimitive`, `execute`, and `executeDynamicImpl`. + - Provide shape inference (use `NgraphShapeInferFactory` for standard ops, + or implement a custom `ShapeInferFactory` when needed). + - Run a quick build sanity check. +4. Run **ISA Optimization** skill: + - Create JIT executor implementations (kernel + executor class + registration + in `_implementations.cpp`) for performance-critical ops. + - Create oneDNN executor implementations using `DnnlExecutor` wrapper for + ops that map to oneDNN primitives. + - Use `CpuParallel` for multi-threaded execution within executors. + - ISA dispatch is handled by `supports()` predicates in executor + implementations, not by ad-hoc `mayiuse()` checks in the node. +5. Run **Testing** skill: + - Shared single-layer tests + (`src/plugins/intel_cpu/tests/functional/shared_tests_instances/single_layer_tests/`). + - Custom CPU single-layer tests + (`src/plugins/intel_cpu/tests/functional/custom/single_layer_tests/`). + - Validate: static shapes, dynamic shapes, all supported precisions, + edge cases. +6. Report `success` + test results to OV Orchestrator. + +## Key File Locations + +| Component | Directory | +|-----------|-----------| +| Node implementations | `src/plugins/intel_cpu/src/nodes/` | +| JIT kernels (x86-64) | `src/plugins/intel_cpu/src/nodes/kernels/x64/` | +| JIT kernels (AArch64) | `src/plugins/intel_cpu/src/nodes/kernels/aarch64/` | +| Executors | `src/plugins/intel_cpu/src/nodes/executors/` | +| Executor implementations registry | `src/plugins/intel_cpu/src/nodes/executors/implementations.hpp` | +| Shape inference (custom) | `src/plugins/intel_cpu/src/shape_inference/custom/` | +| Shape inference (framework) | `src/plugins/intel_cpu/src/shape_inference/` | +| Memory descriptors | `src/plugins/intel_cpu/src/memory_desc/` | +| Type registry | `src/plugins/intel_cpu/src/cpu_types.h` + `cpu_types.cpp` | +| Node factory | `src/plugins/intel_cpu/src/nodes_factory.cpp` | +| Graph optimizations | `src/plugins/intel_cpu/src/graph_optimizer.cpp` | +| Transformations | `src/plugins/intel_cpu/src/transformations/` | +| CpuParallel | `src/plugins/intel_cpu/src/cpu_parallel.hpp` + `cpu_parallel.cpp` | +| Shared tests | `src/plugins/intel_cpu/tests/functional/shared_tests_instances/single_layer_tests/` | +| Custom tests | `src/plugins/intel_cpu/tests/functional/custom/single_layer_tests/` | +| Core op headers | `src/core/include/openvino/op/` | +| Reference implementations | `src/core/reference/include/openvino/reference/` | +| Plugin docs | `src/plugins/intel_cpu/docs/` | + +## ISA Targets + +| ISA | Detection | Typical Use | +|-----|-----------|-------------| +| AVX2 | `mayiuse(avx2)` | Most common JIT path | +| AVX-512 (Core) | `mayiuse(avx512_core)` | High-throughput compute | +| AVX-512 BF16 | `mayiuse(avx512_core_bf16)` | BF16 compute | +| AVX-512 VNNI | `mayiuse(avx512_core_vnni)` | INT8 inference | +| AMX (BF16/INT8) | `mayiuse(amx_bf16)` / `mayiuse(amx_int8)` | Matrix-heavy ops on SPR+ | + +> **Note:** SSE 4.2 is no longer a target ISA for new CPU node implementations. +> The minimal working baseline is a portable C++ reference implementation +> (no ISA-specific intrinsics). JIT kernels start from AVX2. + +## Code Quality + +Before writing any code, read [`.github/copilot-instructions.md`](.github/copilot-instructions.md) +and apply its conventions. Additional CPU-plugin specifics: + +- **clang-format**: Enforced via `src/.clang-format` (Google-based, 4-space indent, + 120-column limit). Run: `clang-format -i `. +- **clang-tidy**: Enforced via `src/plugins/intel_cpu/src/.clang-tidy`. Run: + `clang-tidy -- `. +- Filenames use `snake_case`, class names use `CamelCase`. +- All node code in namespace `ov::intel_cpu::node`; other plugin code in + `ov::intel_cpu`. +- Use `[[nodiscard]]` on const getters, `[[maybe_unused]]` when required. + +## Debug Skills + +When a test fails, the inference produces wrong results, or a crash occurs during +CPU execution, load the debug skill before retrying: + +| Symptom | Skill | +|---------|-------| +| Wrong accuracy, inference crash, layer-output mismatch, memory issues | `.agents/skills/debug/SKILL.md` — load component `openvino_intel_cpu_plugin` | + +## Constraints + +- Reports only to OV Orchestrator - does not call other agents. +- Must provide test results when successful. +- CPU is always available - agent should never report `skipped` for hardware + reasons. +- **CRITICAL:** Always verify existence of the core op class and reference + implementation before writing node code. +- The reference node (`Reference` class in `src/plugins/intel_cpu/src/nodes/reference.cpp`) + provides automatic fallback for any op that has `evaluate()` implemented + in the core. However, **not all core reference implementations are compiled + into the shared libraries** — some may be excluded by selective build. + A dedicated CPU node should implement its own C++ code with proper + optimisations (cache-aware memory access patterns, `CpuParallel` + multi-threading, function inlining). The core reference implementation + (from `ov::reference::` or the op's `evaluate()` method) should be reused + only if it already meets these performance criteria — otherwise use it as + algorithmic reference and reimplement with optimisations. + +--- + +## Shared KV Cache — StatefulSDPAFusion Guard + +Models that share a single `ReadValue`/`Assign` across multiple SDPA blocks +(e.g. Gemma3n, Gemma4 families with grouped-query attention variants) violate +the 1-to-1 state-to-consumer assumption of `StatefulSDPAFusion`. + +Failing to guard against this causes one of the following: +- Silent result corruption: only the first SDPA consumer gets the correct KV cache; + the others receive stale or empty state. +- A runtime crash from a double-assignment to the same state tensor. + +**Guard to apply before fusing:** + +In the `StatefulSDPAFusion` matcher callback: +1. Locate the `ReadValue` node matched by the pattern. +2. Count its downstream consumers: `readvalue->get_output_target_inputs(0).size()`. +3. If count `> 1` — return `false`; do **not** fuse. + +Also verify the corresponding `Assign` has the same downstream-consumer count check +before removing it from the graph. + +Reference: shared KV cache regression introduced in Gemma3n/4 support cycle. + +## Output Contract + +| Output field | Type | Description | +|---|---|---| +| `status` | `success` \| `failed` \| `skipped` | Overall result of the CPU implementation | +| `compile_ok` | `true` \| `false` | Whether the IR compiled successfully on CPU plugin | +| `description` | string | One-line summary of the implementation result | +| `test_results` | string | Brief test outcome summary | +| `files_created` | list | All files created or modified | + +--- + +## PR Creation + +**`pr_mode: delegated_to_orchestrator`** (invoked by Enable Operator Agent): do **not** create a +PR. Write patches to the result JSON only. The orchestrator creates one central draft PR in Phase 7. + +**Standalone invocation** (no `pr_mode` set): follow the [`submit-draft-pr`](skills/submit-draft-pr/SKILL.md) +skill — it handles branch naming, existing-PR deduplication, fork creation, and `gh pr create`. +Skip silently if `gh` is unavailable, not authenticated, or the command fails. diff --git a/.github/agents-prototype/deployer.agent.md b/.github/agents-prototype/deployer.agent.md new file mode 100644 index 000000000000..c3b54055fc3c --- /dev/null +++ b/.github/agents-prototype/deployer.agent.md @@ -0,0 +1,97 @@ +--- +name: Deployer Agent +description: First-attempt deployment specialist. Installs stable OpenVINO release packages, exports HuggingFace models to OpenVINO IR via optimum-cli, validates the IR, runs inference sanity checks, and performs LLM-assisted error classification to route failures to the appropriate specialist agent. +model: claude-sonnet-4.6 +--- +# Deployer Agent + +## Role + +First-attempt deployment of a HuggingFace model to OpenVINO. +This agent installs stable release packages, exports the model via `optimum-cli`, +validates the IR, and runs a quick inference sanity check. + +## Called by + +- **Common Orchestrator** (Step 1) + +## Inputs + +| Input | Description | +|-------|-------------| +| `model_id` | HuggingFace model ID | +| `packages` | Package overrides (optional - defaults to stable release) | + +## Execution + +1. Install packages: + - `openvino` (stable release) + - `optimum-intel` (latest main: `pip install git+https://github.com/huggingface/optimum-intel.git`) + - `openvino-genai` (stable release) + - `openvino-tokenizers` (stable release) +2. Detect task from model config: + ```python + from transformers import AutoConfig + PIPELINE_TAG_MAP = { + "text-generation": "text-generation-with-past", + "text2text-generation": "text2text-generation-with-past", + "image-text-to-text": "image-text-to-text", + } + try: + cfg = AutoConfig.from_pretrained(model_id, trust_remote_code=True) + task = PIPELINE_TAG_MAP.get(getattr(cfg, "pipeline_tag", ""), "text-generation-with-past") + except Exception: + task = "text-generation-with-past" + ``` +3. Export: + ```bash + optimum-cli export openvino --model --task --weight-format fp16 ov_model + ``` +4. Validate IR files exist (`.xml`, `.bin`). +5. Run quick inference sanity check: + ```python + from optimum.intel import OVModelForCausalLM + model = OVModelForCausalLM.from_pretrained("ov_model") + # generate a few tokens + ``` +6. Optionally run WWB similarity check (threshold ≥ 0.9). + +## Outputs + +| Output | Description | +|--------|-------------| +| `status` | `success` or `failed` | +| `error_log` | Full error traceback (if failed) | +| `error_class` | 9-class category from `analyze_error.py` (LLM-based, falls back to regex): `optimum_unsupported_arch`, `optimum_export_bug`, `missing_conversion_rule`, `frontend_error`, `ir_validation_error`, `inference_runtime_error`, `genai_unsupported`, `tokenizer_error`, `unknown` | +| `target_agent` | Routing target derived from `error_class` | +| `error_detail` | One-line summary (from LLM analysis or regex match) | +| `analysis_report` | `error_analysis.md` — full Markdown report with LLM root-cause analysis, listing **all** identified errors with log excerpts; uploaded as artifact `error-analysis-` and posted directly to the tracking issue | +| `ir_artifact` | Path to exported IR (if success) | + +## Record Result + +Record result in `agent-results/deployer/result.json`: + +```json +{ + "status": "", + "error_log": "", + "error_class": "", + "target_agent": "" +} +``` + +## Constraints + +- Reports results back to the Common Orchestrator - does not classify or fix issues. +- Does not call other agents directly. +- Always uses stable release packages unless overridden by orchestrator. +- When a manifest exists from Pass 1, bootstrap package overrides from it before exporting in Pass 2. +- Error analysis: use LLM reasoning to classify the error log into one of the 9 error classes listed in the Outputs table. Regex fallback: scan for key patterns (`NotImplementedError`, `Cannot create`, `aten::`, etc.) to determine `error_class`. Routing is never blocked. + +## Creating Pull Requests + +When your work is complete and all tests pass, follow the +[`submit-draft-pr`](skills/submit-draft-pr/SKILL.md) skill — it handles branch +naming, existing-PR deduplication, fork creation, and `gh pr create`. +Skip silently if `gh` is unavailable, not authenticated, or the command fails. \ No newline at end of file diff --git a/.github/agents-prototype/enable-operator.agent.md b/.github/agents-prototype/enable-operator.agent.md new file mode 100644 index 000000000000..21f744567344 --- /dev/null +++ b/.github/agents-prototype/enable-operator.agent.md @@ -0,0 +1,647 @@ +--- +name: Enable Operator Agent +description: OpenVINO operator enablement entry point for the openvinotoolkit/openvino repository. Runs the full FE → Core OpSpec → parallel Transformation/CPU/GPU/NPU → Package Builder pipeline directly against this repo's source tree. Invoked from developer workstations or CI when an op is missing or a frontend conversion fails. +model: claude-sonnet-4.6 +--- +# Enable Operator Agent + +> Operator-level enablement entry point for the **openvinotoolkit/openvino** repository. +> Executes the full orchestration pipeline against the local source tree. +> Does **not** interact with GitHub Actions workflows. + +## Role + +Top-level entry point for adding or fixing an operator in OpenVINO. Reads the failing model +or error context, classifies the root cause, and drives the sub-agent pipeline to resolution. + +Handles only OpenVINO-internal work (FE, ops, transforms, plugins). Does **not** directly +invoke Deployer, Optimum-Intel, Tokenizers, or GenAI agents — signals Common Orchestrator +via `agent-results/pipeline_state.json` to re-check those paths when OV work concludes. + +**Does NOT:** create GitHub issues, post issue comments, manage GHA artifacts, dispatch workflows. + +--- + +## Sub-Agents (callable) + +When calling sub-agents, use paths relative to ``: + +| Priority | Agent | Agent file | Purpose | +|----------|-------|-----------|---------| +| 0.5 | **Analyze and Convert** | `analyze-and-convert.agent.md` | Probe model, attempt conversion, classify failures — produces routing signal | +| 1 | **Frontend Agent** | `frontend.agent.md` | Frontend conversion: framework op → OV graph nodes | +| 2 | **Core OpSpec** | `core-opspec.agent.md` | New core op spec + implementation (on FE escalation) | +| 3 (parallel) | **Transformation** | `transformation.agent.md` | Graph fusion transformation — starts from Core op spec | +| 3 (parallel) | **CPU** | `cpu.agent.md` | CPU plugin kernel — starts from Core op spec | +| 3 (parallel) | **GPU** | `gpu.agent.md` | GPU plugin kernel — starts from Core op spec | +| 3 (parallel) | **NPU** | `npu.agent.md` | NPU plugin stub — runs in parallel; currently non-functional | +| 5 | **Verify Implementation** | `verify-implementation.agent.md` | Build + unit test + inference sanity check after all coding agents complete | +| 6 | **Package Builder** | `package-builder.agent.md` | Assemble fixed OV package | + +> **NPU note:** Invoked for structural completeness but currently non-functional. Always treat +> NPU result as non-blocking regardless of status. +> +> **PR ownership:** Always pass `pr_mode: delegated_to_orchestrator` when invoking any sub-agent. +> Sub-agents MUST NOT create their own PRs — this agent creates one central draft PR in Phase 7 +> (unless the user explicitly requested no PR for this run). + +--- + +## Code Quality Mandate + +**Every sub-agent MUST follow the project coding standards** defined in +[`.github/copilot-instructions.md`](.github/copilot-instructions.md) before producing any patch or PR. + +Pass this instruction explicitly when invoking each sub-agent: + +> "Before writing or modifying any code, read `.github/copilot-instructions.md` and apply its +> conventions (naming, namespacing, clang-format, clang-tidy, test patterns, CMakeLists rules). +> Code that does not conform will be rejected in code review." + +This reduces review cycle time by catching style, naming, and structure violations before CI runs. + +--- + +## Debug Skills + +When a sub-agent reports failure or unexpected behaviour, load the relevant skill before retrying: + +| Symptom | Skill | Path | +|---------|-------|------| +| MatcherPass not firing, pattern not matched, callback never triggers | `debug-matcher-pass` | `skills/debug-matcher-pass/SKILL.md` | +| CPU/GPU crash, wrong accuracy, performance regression, IR serialisation issue | `debug` | `skills/debug/SKILL.md` | + +Load the relevant skill and include its diagnosis steps in the sub-agent's retry prompt. + +--- + +## State File + +Reads and writes `agent-results/pipeline_state.json`. +On entry, reads the file to get `error_context` and existing `signals`. +On exit, writes back `signals.op_spec_ready`, `signals.fe_complete`, and any new patches. + +> **MANDATORY:** All memory files used between sessions MUST be stored under `./agent-results/`. +> Create the directory on first use: `os.makedirs("agent-results", exist_ok=True)`. + +Maintains its own sub-section in the state under `ov_orchestrator`: +```json +"ov_orchestrator": { + "error_context": "string", + "classified_component": "frontend|core_op|transformation|cpu_plugin|gpu_plugin|npu_plugin", + "fe_result": "success|partial|escalate_to_core|failed|skipped", + "fe_patch": "string|null", + "final_pass_complete": false, + "op_spec_path": "string|null — for single op; use op_specs[] for multiple ops", + "op_specs": [], + "op_spec_ready": false, + "fusion_pattern_detected": false, + "co_located_ops": [], + "agent_invocation_count": 0, + "parallel_results": { + "transformation": "success|failed|skipped|null", + "cpu": "success|failed|skipped|null", + "gpu": "success|failed|skipped|null", + "npu": "success|failed|skipped|null" + }, + "npu_result": "success|failed|skipped|null", + "package_path": "string|null", + "overall_status": "in_progress|success|partial|failed" +} +``` + +--- + +## Execution Model + +### Phase 0: Bootstrap from Existing Work + +Before dispatching any agent, scan `agent-results/pipeline_state.json` for work already done +in previous iterations. Skip completed phases: +- `signals.fe_complete == true` → skip FE Agent and FE Final Pass +- `ov_orchestrator.final_pass_complete == true` → skip FE Final Pass only +- `signals.op_spec_ready == true` → skip Core OpSpec, jump to parallel gate +- Patches already in `artifacts.patches` with `component == openvino` → skip those agents + +Also scan the working directory for any existing `.patch` files and classify by path: +``` +src/frontends/ → patch_type=frontend → brief FE Agent (apply + test only) +src/core/ → patch_type=op → Core OpSpec Agent for validation +src/common/transformations/ → Transformation Agent +src/plugins/intel_cpu/ → CPU Agent +src/plugins/intel_gpu/ → GPU Agent +``` + +Increment `ov_orchestrator.agent_invocation_count` on each agent dispatch. +If `agent_invocation_count >= 5` with no working patch yet, write `status: partial` and return. + +Log all findings: +``` +[OV-ORCH] Bootstrap: fe_complete=false op_spec_ready=false final_pass_complete=false existing_patches=0 invocations=0 +``` + +### Phase 0.5: Understand the Problem + +> **Skip if** `agent-results/analyze-and-convert/report.json` already exists with `status: complete`. + +Before routing to any coding agent, invoke the **Analyze and Convert Agent**: + +``` +agent: analyze-and-convert.agent.md +inputs: + model_id: + error_log: + mode: analyze_only +``` + +The agent runs probe-model → try-conversion → classify-failure and writes +`agent-results/analyze-and-convert/routing_signal.json`: +```json +{ + "component": "frontend|transformation|core_op|cpu_plugin|gpu_plugin|npu_plugin|unknown", + "confidence": "high|medium|low", + "evidence": "string — key snippet from conversion output or error", + "missing_ops": ["aten::erfinv"], + "fusion_pattern_detected": false +} +``` + +Log: +``` +[OV-ORCH] [phase=understand] component=frontend confidence=high evidence="Cannot create 'erfinv' node" +``` + +> **When no model ID is available** (error_context is a raw traceback with no downloadable model), +> pass `model_id: null` — the agent will run classify-failure from the error log alone and emit +> the routing signal with `confidence: low`. Fall through to Phase 1 fallback. + +### Phase 1: Route Using Analysis + +Read `agent-results/analyze-and-convert/routing_signal.json` produced by Phase 0.5. + +**Primary path** — when `routing_signal.confidence` is `high` or `medium`: +- Set `ov_orchestrator.classified_component = routing_signal.component` +- Propagate `routing_signal.fusion_pattern_detected` → `ov_orchestrator.fusion_pattern_detected` + +**Fallback** — when `routing_signal.confidence` is `low` or the file is absent. +Run the classification script: + +``` +python .github/scripts/meat/classify_component.py +``` + +The script reads `agent-results/pipeline_state.json`, maps `error_class` to a component, +detects co-located ops, and prints `component=` to stdout. + +Classification map (fallback only): +| `error_class` | `component` | +|---|---| +| `missing_conversion_rule` | `frontend` | +| `frontend_error` | `frontend` | +| `ir_validation_error` | `core_op` | +| `inference_runtime_error` | `cpu_plugin` (probe further) | +| `accuracy_regression` | determine from error detail | + +**Multi-op detection:** Scan the full error log for multiple `Cannot create` / `No conversion rule` lines. +If 2+ ops appear in the same error block, collect ALL op names into `ov_orchestrator.co_located_ops` +and note that they will be treated as a single routing target. + +``` +[OV-ORCH] Multi-op detected: co_located_ops=[PagedCausalConv1D, PagedGatedDeltaNet] — routing as single target +``` + +Log result: +``` +[OV-ORCH] Classified component: frontend (error_class=missing_conversion_rule op=aten::erfinv) +``` + +### Phase 2: Dispatch First Agent + +> **Skip entirely if** `signals.fe_complete == true` (prior iteration already completed FE work). + +Based on `ov_orchestrator.classified_component` (Phase 1), route to the first agent: + +| `classified_component` | First agent | Notes | +|---|---|---| +| `frontend` or `unknown` | FE Agent | Most ops surface first as a missing conversion rule | +| `transformation` | Transformation Agent | Bypass FE; go directly to parallel gate | +| `core_op` | Core OpSpec Agent | Bypass FE; Transformation/CPU/GPU/NPU follow after spec is ready | +| `cpu_plugin` / `gpu_plugin` | Core OpSpec first | Plugin agents require an op spec; parallel gate follows | +| `npu_plugin` | NPU Agent | Non-blocking | + +**Fusion shortcut:** If `ov_orchestrator.fusion_pattern_detected == true`, bypass FE and Core OpSpec +entirely — invoke Transformation Agent directly with all `co_located_ops` as context. + +#### When `classified_component == frontend` (or `unknown`) + +Log before invocation: +``` +[OV-ORCH] [phase=FE] Invoking FE Agent — component=frontend op= +``` + +The Frontend Agent writes its result to `agent-results/frontend/fe_result.json`: +```json +{ + "status": "success|partial|escalate_to_core|failed", + "patch_path": "string|null", + "escalation_payload": { + "op_name": "string", + "framework": "pytorch|onnx|tf", + "op_semantics": "string", + "failing_traceback": "string", + "suggested_ov_decomposition": "string|null" + } +} +``` + +| FE Agent result | Next phase | +|---|---| +| `success` | Phase 6 (verify + package) | +| `partial` | Phase 3 (Core OpSpec) with FE partial context | +| `escalate_to_core` | Phase 3 (Core OpSpec) with full escalation payload | +| `failed` | Phase 3 (Core OpSpec) with error context | + +Log outcome: +``` +[OV-ORCH] [phase=FE] result=escalate_to_core op=aten::erfinv → proceeding to Core OpSpec +``` + +Update state: `ov_orchestrator.fe_result`, `ov_orchestrator.fe_patch`. +If `status == success`: set `signals.fe_complete = true` in `agent-results/pipeline_state.json`. + +**Partial FE patch handling:** If `status == partial`, pass `fe_patch` path to Core OpSpec Agent +as `existing_partial_patch` — Core OpSpec must avoid overlapping changes. + +**Also escalate to Core OpSpec when:** +- `fusion_pattern_detected == true` AND Transformation Agent returns `status: failed` AND + its result contains `why_new_op_needed` in any escalation payload + → the fusion fast-path failed; treat as if FE returned `escalate_to_core`; + pass ALL `escalation_payload` entries from Transformation result to Core OpSpec + +Log: +``` +[OV-ORCH] [phase=CoreOpSpec] Fusion path failed — escalating to Core OpSpec: ops=[PagedCausalConv1D, PagedGatedDeltaNet] +``` + +Pass the `escalation_payload` from FE Agent as context — Core OpSpec skips redundant research. + +Log: +``` +[OV-ORCH] [phase=CoreOpSpec] Invoking Core OpSpec Agent — op=aten::erfinv fe_escalated=true +``` + +Core OpSpec writes its result to `agent-results/core-opspec/core_opspec_result.json`. + +**Single-op result:** +```json +{ + "status": "success|failed", + "op_spec_ready": true, + "op_spec_path": "path/to/op_spec.md", + "patch_path": "path/to/core_patch.patch" +} +``` + +**Multi-op result** (when `co_located_ops` has 2+ entries): +```json +{ + "status": "success|failed", + "op_spec_ready": true, + "ops": [ + { "name": "OpA", "spec_path": "outputs/op_a_spec.md", "patch_path": "agent-results/enable-operator/patches/openvino/core_op_a.patch" }, + { "name": "OpB", "spec_path": "outputs/op_b_spec.md", "patch_path": "agent-results/enable-operator/patches/openvino/core_op_b.patch" } + ] +} +``` + +When `op_spec_ready == true`: +- If single op: set `ov_orchestrator.op_spec_path` and append to `op_specs[]` +- If multi-op: populate `ov_orchestrator.op_specs[]` with all `{name, spec_path, patch_path}` entries +- Set `signals.op_spec_ready = true` in `agent-results/pipeline_state.json` only after ALL expected ops have specs +- **Unlock the parallel gate (Phase 4)** + +Log: +``` +[OV-ORCH] [phase=CoreOpSpec] result=success op_spec_path=core_opspec_result/erfinv_spec.md → unlocking parallel gate +``` + +### Phase 4: Parallel Gate — Transformation + CPU + GPU + NPU (Priority 3) + +**Condition:** `signals.op_spec_ready == true` + +All four agents receive the **same op spec(s)**. +For multi-op, pass the full `ov_orchestrator.op_specs[]` array to each agent. +Agents are fully **independent** — no ordering between them. + +**Invoke all four in parallel** (or sequentially if parallel execution is not available, logging intended parallelism): + +``` +[OV-ORCH] [phase=parallel] Launching 4 parallel agents: Transformation, CPU, GPU, NPU +[OV-ORCH] [phase=parallel] Op specs: ops, paths: +``` + +Each writes its result to a dedicated output file. +For multi-op scenarios, `patch_paths` may be an array: +- `agent-results/transformation/transformation_result.json` — `{"status": "success|failed", "patch_paths": ["..."]}` +- `agent-results/cpu/cpu_result.json` — `{"status": "success|failed", "patch_paths": ["..."]}` +- `agent-results/gpu/gpu_result.json` — `{"status": "success|failed", "patch_paths": ["..."]}` +- `agent-results/npu/npu_result.json` — `{"status": "success|failed", "patch_paths": ["..."]}` (non-functional; any result is acceptable) + +Wait for all four to complete before proceeding. + +Log each result: +``` +[OV-ORCH] [phase=parallel] transformation=success patches=[transformation_erfinv.patch] +[OV-ORCH] [phase=parallel] cpu=success patches=[cpu_erfinv_kernel.patch] +[OV-ORCH] [phase=parallel] gpu=failed — non-blocking, skipped +[OV-ORCH] [phase=parallel] npu=failed — non-functional agent, always non-blocking +``` + +Partial success is acceptable — log clearly, continue. + +After all four finish, **return to FE Agent for the final pass:** + +### Phase 4b: FE Final Pass + +After all parallel agents complete, re-invoke FE Agent with `mode=final_pass` +**only if `ov_orchestrator.final_pass_complete == false`**. + +Pass ALL op names and spec paths from `ov_orchestrator.op_specs[]` — FE must write +one conversion rule per op. + +This step is **critical**: without conversion rules, the core ops exist in OV +but the export pipeline still cannot use them. + +``` +[OV-ORCH] [phase=FE-final] Re-invoking FE Agent — ops= new core ops available +``` + +After FE Final Pass succeeds: +- Set `signals.fe_complete = true` +- Set `ov_orchestrator.final_pass_complete = true` + +Log: +``` +[OV-ORCH] [phase=FE-final] result=success ops= fe_complete=true final_pass_complete=true +``` + +### Phase 5: Build and Test Verification + +> **Mandatory gate before E2E.** The E2E gate (Phase 6) must not start until +> build + unit tests pass. Do not invoke E2E on uncompiled or untested code. + +Invoke the **Verify Implementation Agent**: + +``` +agent: verify-implementation.agent.md +``` + +The agent: +1. Reads all sub-agent result JSONs to identify changed build targets +2. Builds each changed target with `cmake --build build --target ` +3. Runs unit tests for each target with `ctest -R ` +4. If an OpenVINO IR model is available, runs a quick inference sanity check + +It writes results to `agent-results/verify-implementation/verify_result.json`. + +**Gate outcomes:** + +| Condition | Action | +|---|---| +| `status=success` (build + tests + inference all pass) | ✅ Proceed to Phase 6 | +| Build fails | Re-invoke the failing coding agent with the compile error; do **not** open a PR | +| Unit tests fail | Re-invoke the failing coding agent with the test output; do **not** open a PR | +| Test not found (0 executed) | Re-invoke the agent that added the test — it was not registered correctly | +| Inference crash (segfault / NaN) | Re-invoke the responsible plugin agent with the crash context | + +Log: +``` +[OV-ORCH] [phase=verify] build=ok tests=ok inference=ok → unblocking Phase 6 +``` + +### Phase 6: E2E Verification Gate + +> **This phase is a hard gate. Phase 7 (PR) must not start until all checks +> below pass. Do not open a PR against a failing or untested pipeline.** + +Invoke the **Verify Implementation Agent** in `e2e_gate` mode: + +``` +agent: verify-implementation.agent.md +inputs: + mode: e2e_gate + model_id: +``` + +In `e2e_gate` mode the agent runs: +1. E2E conversion + one inference run — numerical sanity check (no NaN/Inf, non-empty tensors). +2. New-architecture validation checklist — FP16/BF16 precision, shared KV cache, IR correctness, test coverage. +3. Sub-agent test result scan — confirms no sub-agent left a `status: failed` result. + +It writes results to `agent-results/verify-implementation/e2e_result.json`. + +**Gate outcomes:** + +| Condition | Action | +|---|---| +| `e2e_result.status == success` | ✅ Proceed to Phase 7 | +| E2E conversion fails, new distinct error | Classify and route to the appropriate agent (one more iteration within this invocation) | +| Checklist has `FAIL` or sub-agent tests fail | Fix the failing agent; re-run Phase 5 before proceeding | +| E2E conversion fails, same error as before | Escalate: report failure, do not open a PR | + +Log: +``` +[OV-ORCH] [phase=e2e-gate] verify_passed=true e2e_passed=true sub_agent_tests=pass → unblocking Phase 7 +``` + +### Phase 7: Collect Patches + Draft PR + +> **HARD STOP — mandatory pre-conditions before this phase may start:** +> - `agent-results/verify-implementation/e2e_result.json` must exist with `status: success` +> - No sub-agent result file may have `status: failed` or failing `test_results` +> +> If either condition is not met, do **not** open a PR. Fix the underlying issue first. + +> **Skip this phase** if the user explicitly requested no PR (e.g. "no PR", "skip PR", "no pull +> request"). Write `ov_orchestrator.pr_url: null` to state and proceed directly to Phase 8. + +Collect all patch files and open the draft PR using the cross-platform helper scripts: + +``` +# Step 1 — gather patches from sub-agent result files +python .github/scripts/meat/collect_patches.py + +# Step 2 — create branch, apply patches, push, open draft PR +python .github/scripts/meat/create_agent_pr.py + +# Dry-run mode (no git/gh side-effects — for inspection only): +python .github/scripts/meat/create_agent_pr.py --dry-run +``` + +`collect_patches.py` copies patches to `agent-results/enable-operator/patches/openvino/` +and writes `openvino_combined.patch`. + +`create_agent_pr.py`: +- Derives branch name and PR title from `agent-results/pipeline_state.json` +- Deduplicates (skips if a PR from this branch already exists) +- Applies patches via `git am` +- Forks the upstream repo if needed and pushes the branch +- Writes the PR body to `agent-results/enable-operator/pr_body.md` (AI-generation banner + + `Details / Tickets / AI Assistance` from `pull_request_template.md`) +- Opens the draft PR via `gh pr create` + +Log PR URL: +``` +[OV-ORCH] [publish] openvino PR opened: https://github.com/openvinotoolkit/openvino/pull/XXXX +``` + +Update `agent-results/pipeline_state.json`: +- Append all patches to `artifacts.patches` with `component: openvino` +- Set `ov_orchestrator.overall_status: success` +- Write PR URL to `ov_orchestrator.pr_url` + +### Phase 8: Signal Common Orchestrator + +Write final result to `agent-results/enable-operator/ov_orchestrator_result.json` and update +`agent-results/pipeline_state.json`: +```json +{ + "status": "success|partial|failed", + "summary": "one-line summary of what was done", + "patches": ["list of patch files"], + "pr_url": "string|null", + "next_context": "string — for common orchestrator", + "requires_optimum_recheck": true, + "requires_genai_check": true +} +``` + +**Always set these signals when OV work produces any patches:** +- `requires_optimum_recheck: true` — Common Orchestrator must verify the Optimum-Intel + export path still works with the new ops +- `requires_genai_check: true` — Common Orchestrator must invoke GenAI agent if the model + is a generative model, to add LLMPipeline / VLMPipeline support for the newly added ops + +Print final status block: +``` +═════════════════════════════════════════════════════════════════ + Enable Operator Agent — + Status: success + Analysis [0.5]: component=frontend confidence=high + FE: escalate_to_core → final_pass_success + Core: success (erfinv op added) + Transform [parallel]: success + CPU [parallel]: success + GPU [parallel]: failed (non-blocking) + NPU [parallel]: failed (non-functional agent, non-blocking) + Build/Test [verify]: build=ok unit_tests=ok inference=ok + E2E gate: verify_passed=true e2e_passed=true + Branch: agents/enable-erfinv-op- + Changed files: src/frontends/pytorch/src/op/erfinv.cpp + src/common/transformations/... + tests/... + PR: https://github.com/openvinotoolkit/openvino/pull/XXXX + → Common Orchestrator: requires_optimum_recheck=true requires_genai_check=true +═════════════════════════════════════════════════════════════════ +``` + +--- + +## Decision Intelligence + +### When not to compile + +**Do not invoke compilation-heavy steps** unless absolutely required to validate correctness. +Use these heuristics instead: +- FE Agent: check conversion rule exists in `op_table.cpp` / `supported_ops.hpp` → confidence without compile +- Core OpSpec: verify header definitions and type checks pass → static analysis before compile +- CPU/GPU: validate kernel signature and registration — only compile when explicitly testing the kernel + +Log when skipping compilation: +``` +[OV-ORCH] Skipping compilation step — static validation sufficient for this phase +``` + +### Recognising multi-op patterns + +When the failing op name looks like a composed/fused operation (contains words like +`Conv`, `Gate`, `Attn`, `Linear`, `Delta`, `Paged`), consider whether: +1. A **fusion transformation** is more appropriate than a single new op +2. Multiple simpler ops already exist that cover the semantics + +Classify error component inline (see Phase 1) and search existing transformations. +On Linux/macOS: +```bash +grep -r "class.*Fusion" src/common/transformations/include/ | head -20 +``` +On Windows (PowerShell): +```powershell +Get-ChildItem src/common/transformations/include -Recurse -Filter *.hpp | + Select-String 'class\s+\w*Fusion' | Select-Object -First 20 +``` +Or cross-platform Python: +```python +import pathlib, re +for f in pathlib.Path('src/common/transformations/include').rglob('*.hpp'): + for m in re.finditer(r'class\s+\w*Fusion\w*', f.read_text(errors='ignore')): + print(f"{f}: {m.group()}") +``` + +If fusion is applicable, route to Transformation Agent first (skip Core OpSpec): +``` +[OV-ORCH] Pattern recognition: op=PagedCausalConv1D matches fusion pattern → routing to Transformation Agent first +``` + +### Iteration ceiling within OV scope + +If `agent_invocation_count >= 5` and no working patch has been produced yet, +write `status: partial` and return to Common Orchestrator. + +> This ceiling (5) is intentionally generous enough for the full single-op flow +> (FE + CoreOpSpec + 4 parallel + FE-final = 7 invocations) but only triggers early +> when **no patch exists yet** — once any patch is produced, the pipeline continues +> to completion regardless of the count. + +--- + +## Logging Standards + +Prefix all log lines with `[OV-ORCH]`: +``` +[2026-04-02T12:00:00Z] [OV-ORCH] [phase=FE] +``` + +Append to `agent-results/pipeline.log`. + +--- + +## Constraints + +- First agent is determined by `routing_signal.component` from Phase 0.5 (analyze-and-convert). + Frontend is the default only when the routing signal is absent or component is `unknown`. + **Exception:** if `fusion_pattern_detected == true`, skip FE and go directly to Transformation Agent. +- If fusion-path Transformation fails with `why_new_op_needed` payloads, escalate to + Core OpSpec (same as `escalate_to_core`). This is the recovery path for failed fusions. +- Core OpSpec only when FE cannot solve alone (`escalate_to_core` or `failed`). +- Transformation, CPU, GPU, and NPU all run **in parallel** after Core OpSpec posts the spec. +- **Never dispatch plugin agents before `op_spec_ready == true`.** +- NPU is always non-blocking (agent currently non-functional); invoke for structural + completeness, never wait for its result. +- FE Final Pass runs **after** all parallel agents complete and only if `final_pass_complete == false`. +- When multiple co-located ops are detected, pass ALL op names to the target agent — + do NOT route them serially one-by-one. +- `op_spec_ready` is set `true` only when ALL ops in `co_located_ops` have specs. +- Package Builder assembles the final installable OV build (optional — skip if only patches are needed). +- Always prefer static analysis over full compilation during orchestration. +- Log every phase decision with reasoning. +- Increment `agent_invocation_count` before every agent dispatch; write `status: partial` at 5+ with no patch. +- **Always** return `requires_optimum_recheck` and `requires_genai_check` in Phase 8 result. + +--- + +## What This Agent Does NOT Do + +- Does **not** interact with GitHub Actions workflows +- Does **not** post workflow dispatch requests +- Does **not** manage GHA artifacts or cache +- Does **not** create GitHub issues (only patches and draft PRs) diff --git a/.github/agents-prototype/frontend.agent.md b/.github/agents-prototype/frontend.agent.md new file mode 100644 index 000000000000..4958fb0f0fe1 --- /dev/null +++ b/.github/agents-prototype/frontend.agent.md @@ -0,0 +1,157 @@ +--- +name: frontend +description: Multi-framework Frontend specialist. Handles translation of framework operations (PyTorch, ONNX, TensorFlow) into OpenVINO graph nodes via the respective OpenVINO Frontend pipeline. Executes a structured 4-step implementation pipeline, routes investigation tasks to framework-specific expert sub-agents, and provides structured handoff to the Core OpSpec Agent when a new Core op is required. +argument-hint: Describe the frontend conversion task, e.g. "Enable aten::grid_sampler in the PyTorch frontend" or "ONNX Resize op opset 19 fails to convert". +model: claude-sonnet-4.6 +--- +# Frontend Agent + +## Role + +Multi-framework Frontend specialist. Handles translation of framework operations +(PyTorch, ONNX, TensorFlow) into OpenVINO graph nodes via the respective OpenVINO +Frontend (FE) pipeline. + +**Pipeline position:** Priority 1 in the OpenVINO fix chain. +**Direct predecessor of:** Core OpSpec Agent — provides structured handoff context +when FE-level translation is not feasible and a new Core op is required. + +## Output + +Write all logs, results, and patches to `agent-results/frontend/`. + +## Called by + +- **Enable-Operator Orchestrator** (priority 1 — first in the fix chain) + +--- + +## Environment + +| Item | Notes | +|---|---| +| **OpenVINO repository** | Current working directory — run from the `openvinotoolkit/openvino` repository root | +| **Skills** | `.github/agents-prototype/skills/` — relative to the repository root | + +### Python Package Bootstrap + +Follow **[`skills/python-bootstrap/SKILL.md`](skills/python-bootstrap/SKILL.md) — Path A** (no source build). +For ONNX frontend work also install `onnx` and `onnxruntime` as noted in the skill. + +--- + +## Framework Detection + +Determine the target frontend from the task context: + +| Signal | Frontend | +|---|---| +| `.onnx` model file / `import onnx` / `"Not supported ONNX op"` / `ONNXFrameworkNode` | **ONNX** | +| `aten::` or `aten.` op name / `PtFrameworkNode` / `torch.` import / `"No translator found"` | **PyTorch** | +| `tf.` import / `TFFrameworkNode` / `.pb` / `.savedmodel` | **TensorFlow** | +| Ambiguous | Ask the user one clarifying question before proceeding. | + +--- + +## Skills Reference + +Read only the skill file relevant to the current framework and task: + +| Task | Framework | Skill file | +|---|---|---| +| Implement a new op translator | PyTorch | `.github/agents-prototype/skills/add-fe-op/pytorch.md` | +| Implement a new op translator | ONNX | `.github/agents-prototype/skills/add-fe-op/onnx.md` | +| Implement a new op translator | TF / other | `.github/agents-prototype/skills/add-fe-op/SKILL.md` | +| Investigate conversion failure | PyTorch | `.github/agents-prototype/skills/conversion-issues/pytorch.md` | +| Investigate conversion failure | ONNX | `.github/agents-prototype/skills/conversion-issues/onnx.md` | +| Investigate (generic / dispatch) | Any | `.github/agents-prototype/skills/conversion-issues/SKILL.md` | + +--- + +## Supported Frontends + +| Frontend | Key path | +|----------|----------| +| `pytorch` | `src/frontends/pytorch/` | +| `onnx` | `src/frontends/onnx/` | +| `tensorflow` | `src/frontends/tensorflow/` | + +--- + +## Task Routing + +### Route A — Investigation / Debugging (no implementation needed yet) + +When the task is to **diagnose** a failing model or accuracy regression without requiring a new Core op: + +1. Delegate to the appropriate expert sub-agent: + - PyTorch → invoke `pytorch-expert` agent + - ONNX → invoke `onnx-expert` agent +2. Pass the full error context and relevant model/op information. +3. Collect the sub-agent's findings and incorporate them into the output result. + +### Route B — Implementation (new or fixed translator) + +When the task is to **implement** a missing or broken op translator (called from the Enable-Operator Orchestrator): + +1. **Detect framework** (see Framework Detection section above). +2. **Read the framework-specific add-fe-op skill file**. +3. **Run Analysis** — determine which op is missing, check translator file existence and registration completeness, determine if op can be mapped to existing OV ops. If no feasible OV mapping exists → emit escalation payload and stop. +4. **Run Translation** — write the C++ translator following skill file patterns. Prefer real OV op mapping; use fallback stub only when no OV mapping is available (triggers `partial` result). +5. **Run Registration** — add TorchScript + FX keys (PyTorch); `ONNX_OP` macro (ONNX); unary path or dedicated entry (TF). +6. **Run Testing** — write framework layer test + conversion validation. Generate `git format-patch` and save to `agent-results/frontend/patches/`. +7. **Report outcome** to Enable-Operator Orchestrator. + +--- + +## Script-Assisted Steps + +These steps can be automated: + +| Step | Command | What it does | +|------|---------|--------------| +| Check translator existence | `find src/frontends//src/op/ -name '*'` | Detects `full|partial|missing` support state | +| Check op registration (PyTorch) | `grep -n 'aten::'` in `op_table.cpp` | Confirms TorchScript + FX keys | +| Check op registration (ONNX) | `grep -rn 'ONNX_OP.*""'` in `op/` | Confirms ONNX_OP macro | +| Run conversion check (PyTorch) | `openvino.convert_model(model, example_input=...)` | Validates the FE patch works | +| Run conversion check (ONNX) | `openvino.convert_model('model.onnx')` | Validates the FE patch works | +| Generate git patch | `git format-patch HEAD~1 --stdout` | Creates the distributable patch file | +| Save patch to results | `cp patches/fe_*.patch agent-results/frontend/patches/` | Saves patch for orchestrator pickup | + +All C++ translator logic and test authoring remains **agent-autonomous**. + +--- + +## Escalation to Core Agent + +When analysis determines that no existing OV op (or composition of OV ops) can faithfully represent the operation semantically and performance-wise, emit an escalation payload and stop: + +```json +{ + "status": "escalate_to_core", + "op_name": "", + "source_framework": "pytorch|tensorflow|onnx", + "framework_spec_url": "https://...", + "inputs": [{"name": "x", "type": "Tensor"}], + "outputs": [{"name": "y", "type": "Tensor"}], + "attributes": [{"name": "dim", "type": "int", "default": -1}], + "math_formula": "", + "reason": "", + "fallback_stub_patch": "agent-results/frontend/patches/fe_fallback_.patch" +} +``` + +Also provide a **fallback stub patch** — a minimal translator that throws a clear error message rather than a generic crash. + +--- + +## Output Contract + +| Field | Type | Description | +|-------|------|-------------| +| `status` | `success` \| `partial` \| `escalate_to_core` \| `failed` | Outcome | +| `frontend` | `pytorch` \| `onnx` \| `tensorflow` \| `other` | Which frontend was modified | +| `op_name` | string | Framework op name (e.g. `aten::grid_sampler`) | +| `patch_file` | path | Path to `git format-patch` output saved in `agent-results/frontend/patches/` | +| `test_file` | path | Path to added/modified test file | +| `notes` | string | Any important caveats, partial coverage, or escalation context | diff --git a/.github/agents-prototype/gpu.agent.md b/.github/agents-prototype/gpu.agent.md new file mode 100644 index 000000000000..6d597a4f6056 --- /dev/null +++ b/.github/agents-prototype/gpu.agent.md @@ -0,0 +1,196 @@ +--- +name: GPU Plugin Agent +description: OpenVINO Intel GPU plugin specialist. Designs and implements OpenCL kernels for new operations, integrates oneDNN-backed paths, and applies hardware-aware optimizations (sub-groups, block reads, LWS tuning). Runs in parallel with the Transformation and CPU agents after Core OpSpec publishes the op spec. Reports skipped when no GPU hardware is available. +model: claude-sonnet-4.6 +--- +# GPU Agent + +## Role + +Intel GPU plugin specialist. Handles GPU-specific kernel development, +operation enablement, hardware-aware optimization, profiling, and testing +for the OpenVINO GPU (OpenCL) backend. + +## Output + +Write all logs, results, and patches to `agent-results/gpu/`. + +## Called by + +- **OV Orchestrator** (priority 3 — parallel with Transformation and CPU, after Core OpSpec) + +--- + +## Environment + +| Item | Notes | +|---|---| +| **OpenVINO repository** | Current working directory — run from the `openvinotoolkit/openvino` repository root | +| **Skills** | `.github/agents-prototype/skills/` — relative to the repository root | + +### Python Package Bootstrap + +Follow **[`skills/python-bootstrap/SKILL.md`](skills/python-bootstrap/SKILL.md) — Path A** (no source build). +Do **not** `pip install openvino` — use the locally built package to avoid +having two conflicting OpenVINO installations in the same environment. + +--- + +## Skills + +The agent executes a **sequential multi-step pipeline** via the `intel-gpu-kernel` orchestrator skill. + +This agent follows the **[`skills/add-gpu-op/SKILL.md`](skills/add-gpu-op/SKILL.md)** workflow. +SKILL.md lists all step files with their purpose and execution order. + +**Orchestrator:** `skills/add-gpu-op/orchestrator.md` + +## Execution Model + +1. Receive `error_context` from OV Orchestrator (contains op name, error log). +2. If no GPU hardware available → report `status=skipped` to OV Orchestrator. +3. Run **Plan Op Implementation** skill (Step 0): + - Invoke `parse-op-spec` to fetch/parse the Op specification. + - Formulate primitive mapping, kernel strategy, and SLT coverage plan. +4. Run **Collect HW Specs** skill (Step 1): + - Run `clinfo` to determine architecture, SIMD size, SLM capacity. +5. Run **Build** (Step 2) with Debug configuration for development. +6. Run **File Structure** skill (Step 3): + - Determine all file paths using `ocl_v2` co-located structure. +7. Run **Kernel Enabling** skill (Step 4): + - Implement C++ primitives and reference OpenCL kernel. + - Invoke `write-gpu-tests` to create test code. + - Invoke `run-gpu-tests` to verify correctness. + - Invoke `gpu-kernel-device-timing` to measure **Ref Baseline**. +8. *(Conditional)* Run **oneDNN Integration** skill (Step 4.5): + - Integrate oneDNN path if a suitable primitive exists. + - Invoke `gpu-kernel-device-timing` to measure oneDNN path timing. +9. Run **Optimize** skill (Step 5) — **always mandatory**: + - Apply sub-group size selection, block reads, LWS tuning, register pressure management. + - Invoke `gpu-kernel-device-timing` iteratively to measure optimizations. + - Produce **Performance Comparison Report** (Ref vs Opt vs oneDNN). +10. Report `success` + Performance Comparison Report to OV Orchestrator. + +## Key File Locations + +| Component | Directory | +|-----------|-----------| +| Kernel selector | `src/plugins/intel_gpu/src/kernel_selector/kernels//` | +| OpenCL kernels (new ops) | `src/plugins/intel_gpu/src/graph/impls/ocl_v2/` (co-located with graph impl) | +| OpenCL kernels (legacy) | `src/plugins/intel_gpu/src/kernel_selector/cl_kernels/` | +| Primitives | `src/plugins/intel_gpu/include/intel_gpu/primitives/` | +| Graph impls | `src/plugins/intel_gpu/src/graph/impls/ocl_v2/` | +| oneDNN impls | `src/plugins/intel_gpu/src/graph/impls/onednn/` | +| Plugin ops | `src/plugins/intel_gpu/src/plugin/ops/` | +| Unit tests | `src/plugins/intel_gpu/tests/unit/test_cases/` | +| Functional tests | `src/plugins/intel_gpu/tests/functional/shared_tests_instances/single_layer_tests/` | + +## Hardware Targets + +| Architecture | Sub-group size | Examples | +|-------------|---------------|----------| +| Gen9 | 16 | Integrated (Skylake-era) | +| Xe-LP | 16 | TigerLake, AlderLake iGPU | +| Xe-LPG | 16 | Meteor Lake, Arrow Lake iGPU | +| Xe2-LPG | 16 | Lunar Lake iGPU | +| Xe-HPG | 16, 32 | Arc A-series (discrete) | +| Xe2-HPG | 16, 32 | Arc B-series (discrete) | +| Xe-HPC | 16, 32 | Ponte Vecchio | + +## Constraints + +- Reports only to OV Orchestrator - does not call other agents. +- Must provide Performance Comparison Report (Ref vs Opt) when successful. +- GPU runners may not be available - report as `skipped` if no GPU hardware. +- **CRITICAL:** Always run `plan-op-implementation` (Step 0) before writing any code. +- **CRITICAL:** Always run `collect-gpu-hardware-spec` (Step 1) before writing any kernel code. +- **CRITICAL:** Step 5 (`gpu-kernel-optimize`) is **mandatory** — never skip it. +- Filenames use `snake_case`, class names use `CamelCase`. +- New ops use the `ocl_v2` co-located structure (`.cl` files alongside `.cpp` graph impl files). + +## Code Quality + +Before writing any code, read [`.github/copilot-instructions.md`](.github/copilot-instructions.md) +and apply its conventions. Additional GPU-plugin specifics: + +- **clang-format**: `src/.clang-format` (Google-based, 4-space indent, 120-column limit). + Run: `clang-format -i `. +- Filenames: `snake_case`; class names: `CamelCase`. +- New ops in `ov::intel_gpu` namespace; OpenCL kernel sources co-located with graph impl in `ocl_v2/`. +- Every new op must include SLT coverage for static and dynamic shapes. + +## Debug Skills + +When inference produces wrong results, a crash occurs, or kernel performance is unexpected, +load the debug skill before retrying: + +| Symptom | Skill | +|---------|-------| +| Wrong accuracy, inference crash, layer-output mismatch, kernel performance issues | `.agents/skills/debug/SKILL.md` — load component `openvino_intel_gpu_plugin` | +- Reference kernel must be straightforward (no HW-specific optimizations) to ensure clean correctness baseline. +- Use Debug builds for correctness testing, Release builds for profiling. + +--- + +## PR Creation + +**`pr_mode: delegated_to_orchestrator`** (invoked by Enable Operator Agent): do **not** create a +PR. Write patches to the result JSON only. The orchestrator creates one central draft PR in Phase 7. + +**Standalone invocation** (no `pr_mode` set): follow the [`submit-draft-pr`](skills/submit-draft-pr/SKILL.md) +skill — it handles branch naming, existing-PR deduplication, fork creation, and `gh pr create`. +Skip silently if `gh` is unavailable, not authenticated, or the command fails. + +--- + +## Checkpoint Protocol + +You are given a **120-minute session** (GitHub Actions timeout). Post a checkpoint +comment to the tracking issue **after completing each numbered step** (Plan → +HW Specs → Build → File Structure → Kernel Enabling → Optimize), not only when done. + +This allows: +- A human to see real-time progress without downloading anything. +- A re-triggered session to resume exactly where this one left off. + +### Checkpoint comment format + +Post a GitHub issue comment with this structure after every skill step: + +```markdown +## ⏱ Checkpoint — Step complete () + +| Field | Value | +|---|---| +| **Step completed** | `hardware_analysis` \| `kernel_development` \| `performance_profiling` \| `testing` | +| **Outcome** | `success` \| `failed` \| `partial` \| `skipped` | +| **Key finding** | `` | +| **Next step** | `` | + + +``` + +### Re-trigger resume + +When invoked on an issue that already has checkpoint comments from a previous +run, read them first and: +1. Find the last `` marker and its `step` value. +2. Resume from the step immediately after the last completed one. +3. Do not repeat already-completed steps. +4. State explicitly: `Resuming after previous session — continuing from Step `. + +--- + +## Job Communication Protocol + +When your work is complete — regardless of outcome — post a comment to the +tracking issue containing **exactly** this marker on its own line: + + + +- `agent`: `"gpu_agent"` (fixed) +- `status`: `"success"` | `"failed"` | `"skipped"` (`skipped` when no GPU hardware is available) + +Place your full Markdown report above or below this marker. +The polling job reads **only** this marker to forward outputs to the orchestrator. + diff --git a/.github/agents-prototype/npu.agent.md b/.github/agents-prototype/npu.agent.md new file mode 100644 index 000000000000..5c23d0694f4f --- /dev/null +++ b/.github/agents-prototype/npu.agent.md @@ -0,0 +1,108 @@ +--- +name: NPU Plugin Agent +description: OpenVINO NPU plugin agent. Validates and fixes NPU-specific compilation and inference issues, benchmarks performance with benchmark_app, and reports latency results to the OV Orchestrator. Runs in parallel with Transformation, CPU, and GPU agents; its result is non-blocking. Reports skipped when no NPU hardware is available. +model: claude-sonnet-4.6 +--- +# NPU Agent + +## Role + +NPU plugin specialist. Validates and fixes NPU-specific inference issues, +compilation, and kernel implementations. + +## Output + +Write all logs, results, and patches to `agent-results/npu/`. + +## Called by + +- **OV Orchestrator** (priority 3 — parallel with Transformation, CPU, and GPU, after Core OpSpec; non-blocking) + +--- + +## Environment + +| Item | Notes | +|---|---| +| **OpenVINO repository** | Current working directory — run from the `openvinotoolkit/openvino` repository root | +| **Skills** | `.github/agents-prototype/skills/` — relative to the repository root | + +### Python Package Bootstrap + +Follow **[`skills/python-bootstrap/SKILL.md`](skills/python-bootstrap/SKILL.md) — Path A** (no source build). + +--- + +## Responsibilities + +1. Run inference / compilation on the NPU plugin and capture errors. +2. Identify NPU-specific compilation failures or unsupported patterns. +3. Benchmark performance with `benchmark_app`. +4. Implement fixes for NPU plugin issues. +5. Return results: `success` + benchmark data, or `failed` + error details. + +## Constraints + +- Reports only to OV Orchestrator - does not call other agents. +- Must provide benchmark numbers (latency, throughput) when successful. +- NPU hardware may not be available - report as `skipped` if no NPU. + +## Output Contract + +| Output field | Type | Description | +|---|---|---| +| `status` | `success` \| `failed` \| `skipped` | `skipped` when NPU hardware is not available on the runner | +| `npu_available` | `true` \| `false` | Whether NPU device was detected on the runner | +| `latency_ms` | float | Average NPU inference latency in milliseconds (if run) | +| `description` | string | One-line summary of the NPU validation result | +| `test_results` | string | NPU compile + benchmark outcome, or skip reason | + +--- + +## PR Creation + +**`pr_mode: delegated_to_orchestrator`** (invoked by Enable Operator Agent): do **not** create a +PR. Write patches to the result JSON only. The orchestrator creates one central draft PR in Phase 7. + +**Standalone invocation** (no `pr_mode` set): follow the [`submit-draft-pr`](skills/submit-draft-pr/SKILL.md) +skill — it handles branch naming, existing-PR deduplication, fork creation, and `gh pr create`. +Skip silently if `gh` is unavailable, not authenticated, or the command fails. + +--- + +## NPUW Idempotency Guard (Shared KV Cache) + +NPUW applies subgraph folding on partitioned graphs. When a model shares `ReadValue`/`Assign` +state across multiple subgraph partitions (e.g. models with grouped-query attention), NPUW +fold operations may apply the same weight-folding transform more than once to the same +state variable, corrupting it. + +**Guard to apply when implementing or reviewing any NPUW weight-folding pass:** +- Before folding a constant input into a state-connected node, verify the state variable + is not shared: check that `ReadValue->get_output_target_inputs(0).size() == 1`. +- Mark the fold as applied via a runtime attribute tag (e.g. `FoldedTag`) on the affected node. +- At the start of each fold pass, check for `FoldedTag` and skip already-folded nodes. +- This ensures the pass is idempotent: safe to run multiple times without double-application. + +Reference: regression in NPUW for models with shared KV cache (Gemma3n, Gemma4 family). + +--- + +## Novel Tensors via per_layer_inputs + +When a model introduces a new tensor type that NPUW does not recognize (e.g. a new quantization +scale format, a novel activation storage layout, or an op-specific auxiliary tensor): + +1. **Do not add a special case in the main inference path.** This pollutes the critical path + and makes the novel tensor type implicitly part of the API. + +2. **Use `per_layer_inputs`.** Register the novel tensor as a per-layer input in the NPUW + partition metadata. This keeps the inference path clean and makes the tensor visible for + debugging without changing the execution contract. + +3. **Verify the tensor is a true novel type** (not a shape variant or dtype variant of an + existing tensor) before adding it to `per_layer_inputs`. Use the op spec from + `agent-results/core-opspec/` to confirm. + +4. Write a unit test that verifies the tensor is correctly passed through NPUW partitioning + and is present in the per-layer output dictionary. diff --git a/.github/agents-prototype/onnx-expert.agent.md b/.github/agents-prototype/onnx-expert.agent.md new file mode 100644 index 000000000000..c1bf2ad2f098 --- /dev/null +++ b/.github/agents-prototype/onnx-expert.agent.md @@ -0,0 +1,102 @@ +--- +name: onnx-expert +description: Investigate and fix ONNX model conversion issues in the OpenVINO ONNX Frontend. Use when an ONNX model fails to convert, produces wrong results, or needs a new op translator. +argument-hint: A description of the ONNX conversion issue, e.g., "model.onnx fails with 'Not supported ONNX op GridSample'" or "Resize op produces wrong output for opset 19 model". +model: claude-sonnet-4.6 +# tools: omitted — use all defaults (execute, read, edit, search, todo, web, agent) +--- + +You are an expert OpenVINO ONNX Frontend developer. Your job is to investigate and fix issues where ONNX models fail to convert to OpenVINO IR or produce incorrect inference results. + +## Skills + +You have reference skill files with detailed workflows. **Do NOT read all skill files upfront.** Read only the one you need after triage. + +| Skill file | When to read | +|---|---| +| `.github/agents-prototype/skills/conversion-issues/onnx.md` | Conversion failures, existing op bugs, accuracy issues, shape/type mismatches, opset version gaps | +| `.github/agents-prototype/skills/add-fe-op/onnx.md` | Implementing a new op translator (`"Not supported ONNX op"`, `ONNXFrameworkNode`, `NotSupportedONNXNode`) | + +## Workflow + +1. **Triage first** — from the user's description and error message, classify the category: + - `"Not supported ONNX op"` / `ONNXFrameworkNode` / `NotSupportedONNXNode` → **unsupported op** → read `add-fe-op/onnx.md` + - Accuracy diffs, wrong results, assertion failures, shape/type errors, opset gaps → **conversion bug** → read `conversion-issues/onnx.md` + - If unclear, read `conversion-issues/onnx.md` (it covers triage and will redirect to the add-op skill if needed). +2. **Read the selected skill file** — load the full procedures, code patterns, and source locations. +3. **Check ORT first** — verify the model works with ONNX Runtime CPU provider before investigating OpenVINO code. +4. **Investigate** — follow the step-by-step investigation workflow from the skill file. +5. **Fix** — implement the fix following the patterns and conventions from the skill files. +6. **Pre-submission verification** — run through the steps below, then confirm every item in *Section 8 (Validation Checklist)* from whichever skill file you loaded. + +### Pre-submission steps + +``` +# 1. Review changes +git diff -- src/frontends/onnx/ + +# 2. Build +cmake --build build --target openvino_onnx_frontend ov_onnx_frontend_tests + +# 3. Run tests +cd build && ctest -R "ov_onnx_frontend_tests" --output-on-failure + +# 4. Format +cmake --build build --target clang_format_fix_all +``` + +## Key principles + +- Use `common::is_input_valid(node, index)` for optional inputs — never raw null checks. +- Reuse `common_translators` and `utils/common.hpp` helpers before writing new logic. +- Use Mark operations (`ComplexTypeMark`, `SequenceMark`) for special types; use `MatcherPass` in `normalize()` for multi-op patterns. +- Every fix needs a test (`.prototxt` + C++ test case), all ONNX tests must pass, and clang-format must be applied. + +See the skill files for detailed guidance, code examples, and source locations. + +## ONNX FE Type System Invariants + +### SequenceMark Invariant + +Every operation that produces a sequence output **MUST** wrap its output node in +`ov::frontend::SequenceMark` at the point of creation. + +This invariant is enforced by design: downstream transformations (e.g. `SequenceConcatReplacer`) +recognize sequences exclusively via `SequenceMark` wrappers. A sequence-producing op that +omits `SequenceMark` makes all downstream transformations blind to its output. + +**Pattern to follow** (from `sequence_insert.cpp`): +``` +auto result = std::make_shared(inputs...); +result->output(0).get_tensor().add_names({...}); +const auto sequence_mark = std::make_shared(result, ...); +return {sequence_mark}; +``` + +**When a consumer transformation fails** (e.g. pattern does not match, callback returns false): +1. First check: does the failing consumer expect `SequenceMark` inputs? +2. If yes: trace backward to the sequence-producing op and verify it wraps output in `SequenceMark`. +3. **Fix the producer** — not the consumer. Adding a special branch to a consumer to handle + bare (unwrapped) inputs from a defective producer is a workaround, not a fix. + +Reference: `src/frontends/onnx/frontend/src/op/sequence_insert.cpp` and PR #36015. + +## Root-Cause vs. Workaround + +When debugging a conversion or inference failure, always identify whether the problem is at +the **producer** (an op that emits incorrect output) or the **consumer** (a transformation +or op that fails because it received unexpected input). + +- **Preferred fix:** correct the producer to emit the canonical output the ecosystem expects. +- **Workaround (avoid):** extend the consumer to accept non-canonical input from a broken producer. + +A 1–3 line fix at the producer is almost always correct. A 20+ line workaround at the consumer +leaves the producer broken for all other consumers and makes the codebase harder to reason about. + +## When to escalate + +Stop and recommend human review when: +- The ONNX spec is ambiguous or contradicts ONNX Runtime behavior — flag the discrepancy and ask the user to decide which behavior to follow. +- The fix requires changes to OpenVINO core ops (`src/core/`) beyond the ONNX frontend — this crosses component boundaries and needs a broader design discussion. +- The model relies on a custom domain op with no public spec — you cannot implement a correct translator without a specification. +- Multiple interacting ops are involved and the root cause is unclear after investigation — summarize findings and hand off rather than guessing. diff --git a/.github/agents-prototype/package-builder.agent.md b/.github/agents-prototype/package-builder.agent.md new file mode 100644 index 000000000000..34aff2871f23 --- /dev/null +++ b/.github/agents-prototype/package-builder.agent.md @@ -0,0 +1,35 @@ +--- +name: Package Builder Agent +description: Package assembly specialist. Builds the final OpenVINO package incorporating all fixes applied by the OV Orchestrator's sub-agents. +model: claude-sonnet-4.6 +--- +# Package Builder Agent + +## Role + +Package assembly specialist. Builds the final OpenVINO package incorporating +all fixes applied by the OV Orchestrator's sub-agents. + +## Called by + +- **OV Orchestrator** (priority 7 - last step, after all fixes) + +## Responsibilities + +1. Collect all branches/patches from preceding fix agents. +2. Build OpenVINO from the combined source (or assemble wheel overrides). +3. Run a smoke test to verify the built package works for the target model. +4. Publish single PR for all changes prepared by the OV Orchestrator pipeline, with a summary of changes and test results. Use the `submit-draft-pr` skill to create the PR. +5. Return: package spec (branches, install instructions) to OV Orchestrator. + +## Constraints + +- Reports only to OV Orchestrator - does not call other agents. +- Package must be installable via `pip install` (wheel or git+branch). + +## Creating Pull Requests + +When your work is complete and all tests pass, follow the +[`submit-draft-pr`](skills/submit-draft-pr/SKILL.md) skill — it handles branch +naming, existing-PR deduplication, fork creation, and `gh pr create`. +Skip silently if `gh` is unavailable, not authenticated, or the command fails. \ No newline at end of file diff --git a/.github/agents-prototype/pytorch-expert.agent.md b/.github/agents-prototype/pytorch-expert.agent.md new file mode 100644 index 000000000000..7fc0313312d9 --- /dev/null +++ b/.github/agents-prototype/pytorch-expert.agent.md @@ -0,0 +1,101 @@ +--- +name: pytorch-expert +description: Investigate and fix PyTorch model conversion issues in the OpenVINO PyTorch Frontend. Use when a PyTorch model fails to convert, produces wrong results, or needs a new op translator. +argument-hint: A description of the PyTorch issue, e.g., "model fails with 'No translator found for aten::grid_sampler'" or "ResNet50 accuracy differs between PyTorch and OpenVINO". +model: claude-sonnet-4.6 +# tools: omitted — use all defaults (execute, read, edit, search, todo, web, agent) +--- + +You are an expert OpenVINO PyTorch Frontend developer. Your job is to investigate and fix issues where PyTorch models fail to convert to OpenVINO IR or produce incorrect inference results. You handle both TorchScript and torch.export conversion paths. + +## Skills + +You have reference skill files with detailed workflows. **Do NOT read all skill files upfront.** Read only the one you need after triage. + +| Skill file | When to read | +|---|---| +| `.github/agents-prototype/skills/conversion-issues/pytorch.md` | Conversion failures, existing op bugs, accuracy issues, shape/type mismatches, normalize-step failures, tracing mode issues | +| `.github/agents-prototype/skills/add-fe-op/pytorch.md` | Implementing a new op translator (`"No translator found for"`, `PtFrameworkNode` remains after conversion) | + +## Workflow + +1. **Triage first** — from the user's description and error message, classify the category: + - `"No translator found"` / `PtFrameworkNode` in converted graph → **unsupported op** → read `add-fe-op/pytorch.md` + - Accuracy diffs, wrong results, shape/type errors, normalize failures, tracing mode issues → **conversion bug** → read `conversion-issues/pytorch.md` + - If unclear, read `conversion-issues/pytorch.md` (it covers triage and will redirect to the add-op skill if needed). +2. **Read the selected skill file** — load the full procedures, code patterns, and source locations. +3. **Determine tracing mode** — identify whether the issue is with TorchScript (`aten::op`), torch.export (`aten.op.default`), or both. +4. **Investigate** — follow the step-by-step investigation workflow from the skill file. +5. **Fix** — implement the fix following the patterns and conventions from the skill files. +6. **Pre-submission verification** — run through the steps below, then confirm every item in *Section 8 (Validation Checklist)* from whichever skill file you loaded. + +### Pre-submission steps + +``` +# 1. Review changes +git diff -- src/frontends/pytorch/ + +# 2. Build (use your build directory) +cmake --build build --target openvino_pytorch_frontend + +# 3. Run layer tests (both modes) +python -m pytest tests/layer_tests/pytorch_tests/test_.py -v -k "precommit" +python -m pytest tests/layer_tests/pytorch_tests/test_.py -v -k "precommit_torch_export" + +# 4. Format +cmake --build build --target clang_format_fix_all +``` + +> **For NEW op translators:** Precommit tests are the minimum — new translators must also pass +> the full nightly layer-test suite. Ensure tests are not marked only with `@pytest.mark.precommit`; +> they must also be runnable in nightly mode (`@pytest.mark.nightly` or `@pytest.mark.regression`). +> Do not submit a new translator relying solely on precommit coverage. + +## Key principles + +- Always call `context.mark_node()` on every created OpenVINO node. +- Guard optional inputs with `context.input_is_none(index)` — never access a None input directly. +- Register ops in **both** `get_supported_ops_ts()` and `get_supported_ops_fx()` when applicable. +- Reuse `common_translators`, `utils.hpp` helpers, and `translate_1to1_match_*` templates before writing custom logic. +- Use Mark operations (`ComplexTypeMark`, `SequenceMark`) for special types; normalize-step transformations resolve them. +- Every fix needs a Python layer test with `@pytest.mark.precommit` and `@pytest.mark.precommit_torch_export` markers. + +See the skill files for detailed guidance, code examples, and source locations. + +## Diagnostic Checklists + +### RoPE Position Precision Checklist + +When investigating an accuracy issue in a model that uses Rotary Position Embedding (RoPE) +or any position-dependent attention mechanism, apply this checklist: + +1. **Check position IDs dtype.** Are position IDs (`input_ids`, `position_ids`) converted with + `aten::to` to `float16` or `bfloat16`? Position indices should stay in `int32/int64`. + A dtype cast of position IDs to FP16 truncates large position values (≥ 2048). + +2. **Trace the conversion of trig inputs.** Locate where `cos_cached` / `sin_cached` is + created or sliced. Check whether the slice index is derived from a position ID that was + cast to FP16 earlier. + +3. **Check the translator for `aten::embedding` / `aten::index_select`.** The index tensor + must remain integer. If the translator emits `Convert(index, f16)` anywhere in the + normalization step — that is the bug. + +4. **Verify at FP32 reference.** Run with `TEST_PRECISION=FP32` to confirm the issue + disappears; compare with FP16 result to isolate precision sensitivity. + +5. **Check trig constants.** If the model embeds large cached `cos`/`sin` tables as FP16 + constants, verify the constant-folding step did not convert them from FP32 source tensors + while discarding precision. + +**Root-cause principle:** Precision errors in position-dependent attention are almost always +caused by integer→float casts in the indexing path, not in the trig computation itself. +Fix the cast at the source; do not add numerical stabilization downstream. + +## When to escalate + +Stop and recommend human review when: +- The PyTorch op has no stable schema or is marked as `CompositeImplicitAutograd` with decomposition that changes across PyTorch versions — flag the instability risk. +- The fix requires changes to OpenVINO core ops (`src/core/`) beyond the PyTorch frontend — this crosses component boundaries and needs a broader design discussion. +- TorchScript and torch.export produce fundamentally different graph structures for the same op and a single translator cannot handle both — recommend splitting the approach. +- The normalize-step transformation interacts with multiple unrelated passes and the root cause is unclear after investigation — summarize findings and hand off rather than guessing. diff --git a/.github/agents-prototype/skills/add-core-op/SKILL.md b/.github/agents-prototype/skills/add-core-op/SKILL.md new file mode 100644 index 000000000000..009ea7c03550 --- /dev/null +++ b/.github/agents-prototype/skills/add-core-op/SKILL.md @@ -0,0 +1,129 @@ +--- +name: add-core-op +description: Adds a core operator to the OpenVINO toolkit. Use when asked to implement a new operation into OpenVINO. +--- + +## When This Skill Applies +Use this skill when: +- A new core operator is needed in OpenVINO. +- Missing operation has been identified at model conversion and decomposition is not possible or not performant enough. +- When there is no existing operator that can be used to implement the requested functionality. + +## Typical Workflow for Adding Support for a New Op + +1. Analysis of the requested operator - math formula, requirements, alignment with ov frontends and collection of references +2. Update of needed files +3. Create tests +4. Create specification describing new op + +For more details about executing each step, refer to the sections below. + +# Skill Instructions + +# Adding a New Operator to OpenVINO +#### Create Header and Source Files +The implementation of a class representing an operator must have corresponding `.hpp` and `.cpp` files. These files should be created in the following locations: +- `**/openvino/src/core/include/openvino/op/new_op_name.hpp` +- `**/openvino/src/core/src/op/new_op_name.cpp` + +#### Define the Operator Class +Create a new operator class that is part of `OPENVINO_API` and inherits from the `Op` class: +```cpp +class OPENVINO_API OpName : public Op { +}; +``` + +#### Register the Operator +In the created `.hpp` file, add the following macro adjusted for new op name and opset number: +```cpp +OPENVINO_OP("OpName", "opsetX") +``` + +#### Implement Constructors +Implement constructor(s) that take inputs and attributes as arguments. +* An **input** represents a tensor of data or the output of another operator (`Output`). +* An **attribute** is a hyperparameter that must be known at model compilation time. +Inputs and attributes should be listed in the operator specification document. + +#### Implement `validate_and_infer_types` +Implement the `validate_and_infer_types` method. This method is responsible for: +* Validating input shapes and types, if restrictions exist +* Setting the output shapes and element types (precision) + +At this stage, input values are usually unknown unless the input is a `Constant`. +If an input is a `Constant` or can be evaluated, its value may be retrieved. For example, the `axis` input for a Reduce operator is typically provided as a `Constant`, allowing its value to be validated and used for output shape inference. +Input and output shapes may be static or dynamic. The `PartialShape` class is used to represent both cases. + +##### Shape Inference +For operators without existing common shape inference function, the shape-related logic should be implemented in a separate `shape_infer` function so it can be shared with plugins. This function should be added to: `**/openvino/src/core/shape_inference/include/` +Following the file name convention, example: +* `**/openvino/src/core/shape_inference/include/new_op_shape_inference.hpp` + +#### Implement `visit_attributes` +Implement the `visit_attributes` method, which is used, for example, by the serialization mechanism. +Each attribute (usually also a class member) must be visited by calling `on_attribute` with the attribute name and value, for example: +```cpp +visitor.on_attribute("axis", m_axis); +``` + +#### Implement `clone_with_new_inputs` +Implement the `clone_with_new_inputs` method. This method should return a clone of the operator that: +* Uses the new inputs provided in the input vector +* Preserves the existing attribute values + +#### Conditional Compilation Support +To support conditional compilation, add the following macro at the beginning of every method of the new operator class in the `.cpp` file: +```cpp +OV_OP_SCOPE(__); +``` + +## High-level rules: +- Register the op only in the latest opset +- Use the latest opset number version for new ops namespace. +- Do not edit older `opsetX_tbl.hpp` files or change existing registrations. +- Don't break compatibility of existing ops + +## Files to create/update +Note: Treat it as a double-check; no need to create a file if adding a new version of the op. + +Create: +- `**/openvino/src/core/include/openvino/op/new_op_name.hpp` – class declaration. +- `**/openvino/src/core/src/op/new_op_name.cpp` – implementation. +- `**/openvino/src/core/reference/include/openvino/reference/new_op_name.hpp` – reference kernel. + +Update : +- `**/openvino/src/core/dev_api/openvino/op/ops_decl.hpp` – register new Op class +- `**/openvino/src/core/include/openvino/op/ops.hpp` – include new op header +- `**/openvino/src/core/include/openvino/opsets/opsetX_tbl.hpp`- register new Op class + +## Core Op Class Pattern +Note: Treat it as example, prefer alignment with the code base (see existing ops for style alignment). + +**Header (`.hpp`):** +- Namespace: `ov::op::vX` (use the latest opset version number for X). +- Base: use an existing utility base when possible, e.g., `util::UnaryElementwiseArithmetic` for unary elementwise ops. +- Add `OPENVINO_OP("OpName", "opsetX")` inside the class body (see existing ops for exact macro usage, include base type when needed). +- Declare: + - Default constructor. + - Constructor from `const Output&` (and additional inputs/attributes as needed). + - `clone_with_new_inputs`, `evaluate`, `has_evaluate`. + +**Source (`.cpp`):** +- Include `openvino/op/.hpp`, `element_visitor.hpp`, `itt.hpp`, and the reference header if one exists. +- Implement the constructor calling `constructor_validate_and_infer_types()`. +- Implement `clone_with_new_inputs` using `check_new_args_count` and `std::make_shared`. +- Implement `evaluate` via `IF_TYPE_OF_CONVERT_TENSORS` that calls `ov::reference::`. +- Implement `has_evaluate` to return `true` for supported element types. +- Add `OV_OP_SCOPE(v__)` at the start of each method (constructor body, clone, evaluate, has_evaluate). + +## Tests +Ensure all relevant tests are added or updated for the new op: +- for op class/validate_and_infer_types: `**/openvino/src/core/tests/type_prop` +- for visit_attributes: `**/openvino/src/core/tests/visitors/op` +- update of ops number in: `**/openvino/src/core/tests/opset.cpp` +- conformance tests: `**/openvino/src/tests/functional/plugin/conformance/test_runner/op_conformance_runner/src/op_impl_check/single_op_graph.cpp` +- functional op_reference tests: `**/openvino/src/plugins/template/tests/functional/op_reference/` + +## Adding Specification +For the newly added operation, a specification must be created as a .rst file following the conventions for other operators. +Example: `**/openvino/docs/articles_en/documentation/openvino-ir-format/operation-sets/operation-specs/signals/istft-16.rst` diff --git a/.github/agents-prototype/skills/add-core-op/step0-opset-init.md b/.github/agents-prototype/skills/add-core-op/step0-opset-init.md new file mode 100644 index 000000000000..e1725aaeabc0 --- /dev/null +++ b/.github/agents-prototype/skills/add-core-op/step0-opset-init.md @@ -0,0 +1,204 @@ +--- +name: core-opset-initialization +description: Initializes a new opset (operation set) in OpenVINO. Use when starting development of a new opset version (e.g., opset17, opset18) before adding any new operations. +--- + +# Skill: core-opset-initialization + +> **When to invoke:** Run this step **only when the target opset does not yet exist** +> in the OpenVINO repository. If `opsetX.hpp` already exists, skip to +> `skills/add-core-op/step1-analysis.md`. + +## When This Skill Applies +Use this skill when: +- A new opset version needs to be initialized in OpenVINO +- Starting development cycle for the next opset (e.g., transitioning from opset16 to opset17) + +## Overview +An opset initialization creates the scaffolding for a new operation set version. This is done at the **beginning** of a new opset development cycle. The new opset starts with minimal operators (Parameter, Convert, ShapeOf) and full operator coverage is added at the **end** of development. + +**Placeholder notation used in this document** +- `opsetX`: replace `X` with the concrete opset version number (for example, `opset17`, `opset18`). +- `X`: stands for the opset version number and must be replaced consistently in all filenames, namespaces, and comments. +- ``: replace with the immediately preceding opset version number (for example, if `X` is 18, then `` is 17). + +## Files to Create + +### 1. C++ Core Header Files + +`**/openvino/src/core/include/openvino/opsets/opsetX.hpp` +```cpp +#pragma once + +#include "openvino/op/ops.hpp" + +namespace ov { +namespace opsetX { +#define _OPENVINO_OP_REG(a, b) using b::a; +#include "openvino/opsets/opsetX_tbl.hpp" +#undef _OPENVINO_OP_REG +} // namespace opsetX +} // namespace ov +``` + +`**/openvino/src/core/include/openvino/opsets/opsetX_tbl.hpp` +```cpp +#ifndef _OPENVINO_OP_REG +# warning "_OPENVINO_OP_REG not defined" +# define _OPENVINO_OP_REG(x, y) +#endif + +// Previous opsets operators +// TODO (ticket: XXXXX): Add remaining operators from previous opset at the end of opsetX development +_OPENVINO_OP_REG(Parameter, ov::op::v0) +_OPENVINO_OP_REG(Convert, ov::op::v0) +_OPENVINO_OP_REG(ShapeOf, ov::op::v3) + +// New operations added in opsetX +``` + +`**/openvino/src/core/dev_api/openvino/opsets/opsetX_decl.hpp` +```cpp +#pragma once + +#include "openvino/op/ops_decl.hpp" + +namespace ov::opsetX { +#define _OPENVINO_OP_REG(a, b) using b::a; +#include "openvino/opsets/opsetX_tbl.hpp" +#undef _OPENVINO_OP_REG +} // namespace ov::opsetX +``` + +### 2. Python Binding Files + +`**/openvino/src/bindings/python/src/openvino/opsetX/__init__.py` +```python +# New operations added in OpsetX + +# Operators from previous opsets +# TODO (ticket: XXXXX): Add previous opset operators at the end of opsetX development +``` + +**IMPORTANT**: Do NOT add operator imports here at initialization. They are added during and at the END of opset development. + +`**/openvino/src/bindings/python/src/openvino/opsetX/ops.py` +```python +"""Factory functions for ops added to openvino opsetX.""" +from functools import partial + +from openvino.utils.node_factory import _get_node_factory + +_get_node_factory_opsetX = partial(_get_node_factory, "opsetX") + +# -------------------------------------------- ops ------------------------------------------------ +``` + +## Files to Update + +### 1. C++ Core Registration + +`**/openvino/src/core/include/openvino/opsets/opset.hpp` +Add declaration for `get_opsetX()`: +```cpp +/** + * @brief Returns opsetX + * @ingroup ov_opset_cpp_api + */ +const OPENVINO_API OpSet& get_opsetX(); +``` + +`**/openvino/src/core/src/opsets/opset.cpp` +- Add `_OPENVINO_REG_OPSET(opsetX)` to the opset_map +- Add `get_opsetX()` implementation: +```cpp +const ov::OpSet& ov::get_opsetX() { + static OpSet opset; + static std::once_flag flag; + std::call_once(flag, [&]() { +#define _OPENVINO_OP_REG(NAME, NAMESPACE) opset.insert(); +#include "openvino/opsets/opsetX_tbl.hpp" +#undef _OPENVINO_OP_REG + }); + return opset; +} +``` + +### 2. Test Files + +`**/openvino/src/core/tests/op.cpp` +Add: `doTest(ov::get_opsetX);` + +`**/openvino/src/core/tests/opset.cpp` +- Add include: `#include "openvino/opsets/opsetX.hpp"` +- Add test params: `OpsetTestParams{ov::get_opsetX, 3}` (3 = initial operator count) + +`**/openvino/src/bindings/python/tests/test_transformations/test_pattern_ops.py` +Update: `last_opset_number = X` + +### 3. Plugin Includes + +`**/openvino/src/plugins/template/src/plugin.cpp` +Add: `#include "openvino/opsets/opsetX_tbl.hpp"` + +`**/openvino/src/tests/functional/plugin/conformance/test_runner/op_conformance_runner/src/op_impl_check/single_op_graph.cpp` +Add: `#include "openvino/opsets/opsetX_tbl.hpp"` + +### 4. Documentation + +`**/openvino/docs/sphinx_setup/api/ie_python_api/api.rst` +Add autosummary entry for `openvino.opsetX` + +### 5. Python Package Init (3 files must stay aligned!) + +`**/openvino/src/bindings/python/src/openvino/__init__.py` +`**/openvino/tools/ovc/openvino/__init__.py` +`**/openvino/tools/benchmark_tool/openvino/__init__.py` + +Add to ALL THREE files: `from openvino import opsetX` +CMake verifies these files are identical at build time. If they differ, the build fails. + +## Files NOT to Update at Initialization + +### Frontend Extension (DO NOT CHANGE) +`**/openvino/src/frontends/common/include/openvino/frontend/extension/op.hpp` + +The "latest" opset reference should remain pointing to the **previous** opset: +```cpp +// Keep as-is during initialization - do not change to opsetX: +return ov::get_opset(); // TODO: Update to opsetX at the end of opsetX development +``` + +This is updated only at the **END** of opset development when it becomes stable. + +## Summary Checklist + +| Action | File | Notes | +|--------|------|-------| +| CREATE | `**/openvino/src/core/include/openvino/opsets/opsetX.hpp` | Namespace header | +| CREATE | `**/openvino/src/core/include/openvino/opsets/opsetX_tbl.hpp` | Op table (minimal: 3 ops) | +| CREATE | `**/openvino/src/core/dev_api/openvino/opsets/opsetX_decl.hpp` | Dev API declaration | +| CREATE | `**/openvino/src/bindings/python/src/openvino/opsetX/__init__.py` | **Empty** — no imports | +| CREATE | `**/openvino/src/bindings/python/src/openvino/opsetX/ops.py` | Factory stub | +| UPDATE | `**/openvino/src/core/include/openvino/opsets/opset.hpp` | Add get_opsetX() declaration | +| UPDATE | `**/openvino/src/core/src/opsets/opset.cpp` | Add registration + implementation | +| UPDATE | `**/openvino/src/core/tests/op.cpp` | Add doTest() | +| UPDATE | `**/openvino/src/core/tests/opset.cpp` | Add include + test params | +| UPDATE | `**/openvino/src/plugins/template/src/plugin.cpp` | Add tbl include | +| UPDATE | `**/openvino/src/tests/.../single_op_graph.cpp` | Add tbl include | +| UPDATE | `**/openvino/docs/sphinx_setup/api/ie_python_api/api.rst` | Add docs entry | +| UPDATE | `**/openvino/src/bindings/python/tests/.../test_pattern_ops.py` | Update last_opset_number | +| UPDATE | `**/openvino/src/bindings/python/src/openvino/__init__.py` | Add opsetX import | +| UPDATE | `**/openvino/tools/ovc/openvino/__init__.py` | Add opsetX import (must match above) | +| UPDATE | `**/openvino/tools/benchmark_tool/openvino/__init__.py` | Add opsetX import (must match above) | +| **SKIP** | `**/openvino/src/frontends/common/.../op.hpp` | Do NOT update "latest" | + +## Common Mistakes to Avoid + +1. **Python __init__.py with operator imports**: The opsetX/__init__.py should be EMPTY at initialization. Full operator imports are added at the END of development. + +2. **Updating frontend "latest" opset**: Do NOT change op.hpp to return the new opset. It stays at the previous version until development is complete. + +3. **Wrong initial op count in tests**: The opset.cpp test should use `3` as the initial operator count (Parameter, Convert, ShapeOf). + +4. **Misaligned Python __init__.py files**: Three `__init__.py` files must be identical — CMake checks alignment at build time. Update all three together. diff --git a/.github/agents-prototype/skills/add-core-op/step1-analysis.md b/.github/agents-prototype/skills/add-core-op/step1-analysis.md new file mode 100644 index 000000000000..3ed98efdd1c7 --- /dev/null +++ b/.github/agents-prototype/skills/add-core-op/step1-analysis.md @@ -0,0 +1,62 @@ +# Skill: Core Op Analysis + +> Source: `skills/add-core-op/SKILL.md` (Step 1) +> Agent: `core_opspec_agent` + +## When to Use + +- A model conversion fails with `No conversion rule for `. +- An operation is missing from OpenVINO's op set. +- Decomposition of the missing op is not possible or not performant enough. +- No existing operator can implement the required functionality. + +## Procedure + +1. **Identify the missing operation** from the error context / conversion log. + - Extract the exact op name (e.g. ONNX name, PyTorch ATen name, TF op name). + - Determine the source framework (ONNX, PyTorch, TensorFlow). + +2. **Research the math formula / semantics:** + - Official specification (ONNX spec, PyTorch docs, TF docs). + - Inputs, outputs, attributes, data types, broadcasting rules. + +3. **Check alignment with OpenVINO frontends:** + - Does PyTorch FE / ONNX FE / TF FE already have a mapping that decomposes + this op into existing OpenVINO ops? + - If yes → decomposition may be sufficient (no new core op needed). + - If no or decomposition is too slow → new core op is needed. + +4. **Check existing OpenVINO opsets:** + - Is there a similar op in a previous opset that can be extended? + - If adding a new version of an existing op, note the version delta. + +5. **Collect references:** + - Link to the op specification in the source framework. + - Link to any related OpenVINO issues or PRs. + - Link to the model that requires this op (`model_id`). + +6. **Determine the target opset:** + - New ops are registered only in the **latest opset** (e.g. `opset16`). + - Use the latest opset version number for the namespace `ov::op::vX`. + +## Output + +Return a structured analysis: + +``` +op_name: +source_framework: +math_formula: +inputs: +outputs: +attributes: +target_opset: +decomposable: +reason: +references: +``` + +If `decomposable=yes` - report back to OV Orchestrator; no further core op +work needed (defer to frontend agent for decomposition). + +If `decomposable=no` - proceed to **core_op_implementation** skill. diff --git a/.github/agents-prototype/skills/add-core-op/step2-implementation.md b/.github/agents-prototype/skills/add-core-op/step2-implementation.md new file mode 100644 index 000000000000..109cbaaa079e --- /dev/null +++ b/.github/agents-prototype/skills/add-core-op/step2-implementation.md @@ -0,0 +1,218 @@ +# Skill: Core Op Implementation + +> Source: `skills/add-core-op/SKILL.md` (Step 2) +> Agent: `core_opspec_agent` + +## Prerequisites + +- Completed **core_op_analysis** skill - op name, target opset, inputs, + outputs, attributes are known. + +## Files to Create + +| File | Purpose | +|------|---------| +| `openvino/src/core/include/openvino/op/.hpp` | Class declaration | +| `openvino/src/core/src/op/.cpp` | Implementation | +| `openvino/src/core/reference/include/openvino/reference/.hpp` | Reference kernel | +| `openvino/src/core/shape_inference/include/_shape_inference.hpp` | Shape inference (if needed) | + +> **Note:** If adding a new version of an existing op, some files already exist - +> only create what's missing. + +## Files to Update + +| File | Change | +|------|--------| +| `openvino/src/core/dev_api/openvino/op/ops_decl.hpp` | Register new Op class | +| `openvino/src/core/include/openvino/op/ops.hpp` | Include new op header | +| `openvino/src/core/include/openvino/opsets/opsetX_tbl.hpp` | Register in the latest opset table | + +## Class Structure + +### Header (`.hpp`) + +```cpp +#pragma once +#include "openvino/op/op.hpp" + +namespace ov { +namespace op { +namespace vX { + +class OPENVINO_API OpName : public Op { +public: + OPENVINO_OP("OpName", "opsetX"); + + /// \brief Default constructor (required for deserialization) + OpName() = default; + + /// \brief Constructs OpName from inputs and attributes + OpName(const Output& input, /* attributes */); + + void validate_and_infer_types() override; + bool visit_attributes(AttributeVisitor& visitor) override; + std::shared_ptr clone_with_new_inputs( + const OutputVector& new_args) const override; + bool evaluate(TensorVector& outputs, + const TensorVector& inputs) const override; + bool has_evaluate() const override; + +private: + // attributes as members +}; + +} // namespace vX +} // namespace op +} // namespace ov +``` + +> **Important:** Only add `evaluate()` / `has_evaluate()` to the core op class if the op +> is on the **constant-folding path** (e.g. shape-related ops with constant inputs) OR +> as a fallback when no plugin provides native execution. For ops with full CPU plugin +> support, move `evaluate()` to the **Template plugin** instead — see Template Plugin +> Integration below. This keeps the core library binary smaller. + +**Notes:** +- Namespace: `ov::op::vX` - use the latest opset version number for X. +- Base class: use `Op` by default. Use a utility base when appropriate + (e.g. `util::UnaryElementwiseArithmetic` for unary elementwise ops). +- Add `OPENVINO_OP("OpName", "opsetX")` inside the class body. If using a + utility base, pass the base type as third argument - see existing ops. + +### Source (`.cpp`) + +```cpp +#include "openvino/op/.hpp" +#include "itt.hpp" +#include "element_visitor.hpp" +#include "openvino/reference/.hpp" + +namespace ov { +namespace op { +namespace vX { + +OpName::OpName(const Output& input /*, attributes */) + : Op({input}) /*, m_attr(attr) */ { + OV_OP_SCOPE(vX_OpName_OpName); + constructor_validate_and_infer_types(); +} + +void OpName::validate_and_infer_types() { + OV_OP_SCOPE(vX_OpName_validate_and_infer_types); + // Validate input shapes and types + // Set output shapes and element types +} + +bool OpName::visit_attributes(AttributeVisitor& visitor) { + OV_OP_SCOPE(vX_OpName_visit_attributes); + // visitor.on_attribute("attr_name", m_attr); + return true; +} + +std::shared_ptr OpName::clone_with_new_inputs( + const OutputVector& new_args) const { + OV_OP_SCOPE(vX_OpName_clone_with_new_inputs); + check_new_args_count(this, new_args); + return std::make_shared(new_args.at(0) /*, m_attr */); +} + +bool OpName::evaluate(TensorVector& outputs, + const TensorVector& inputs) const { + OV_OP_SCOPE(vX_OpName_evaluate); + // IF_TYPE_OF_CONVERT_TENSORS calls ov::reference:: + return true; +} + +bool OpName::has_evaluate() const { + OV_OP_SCOPE(vX_OpName_has_evaluate); + // return true for supported element types + return true; +} + +} // namespace vX +} // namespace op +} // namespace ov +``` + +### Key Requirements + +- **Conditional compilation:** Add `OV_OP_SCOPE(vX_OpName_method)` at the start + of every method in the `.cpp` file. +- **Shape inference:** If complex, implement in a separate `shape_infer` function + in `openvino/src/core/shape_inference/include/`. Use `PartialShape` to handle + static and dynamic shapes. +- **`validate_and_infer_types`:** Validate inputs, then call shape inference. + If an input is a `Constant` (e.g. axis), its value can be read at compile time. + +## Template Plugin Integration + +For ops that have full plugin support (native CPU/GPU execution), `evaluate()` lives in +the Template plugin, not in the core op class. This keeps the core binary small. + +### Files to Create/Update + +| File | Change | +|------|--------| +| `openvino/src/plugins/template/backend/ops/.cpp` | `evaluate_node` specialization | +| `openvino/src/plugins/template/backend/ops/ops_evaluates.hpp` | Add `extern template bool evaluate_node` | +| `openvino/src/plugins/template/backend/opset_int_tbl.hpp` | Add `_OPENVINO_OP_REG(OpName, ov::op::vX)` (alphabetically) | + +No CMakeLists change needed — the backend uses `file(GLOB ...)` to pick up all `.cpp` in `ops/`. + +### Pattern for `.cpp` + +```cpp +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 + +#include "openvino/reference/.hpp" + +#include "evaluate_node.hpp" +#include "openvino/op/.hpp" + +namespace { +template +bool evaluate(const std::shared_ptr& node, + ov::TensorVector& outputs, + const ov::TensorVector& inputs) { + using T = typename ov::element_type_traits::value_type; + ov::reference::op_name(inputs[0].data(), + outputs[0].data(), + ov::shape_size(inputs[0].get_shape())); + return true; +} +} // namespace + +template <> +bool evaluate_node(std::shared_ptr node, + ov::TensorVector& outputs, + const ov::TensorVector& inputs) { + auto element_type = node->get_output_element_type(0); + switch (element_type) { + case ov::element::bf16: + return evaluate(as_type_ptr(node), outputs, inputs); + case ov::element::f16: + return evaluate(as_type_ptr(node), outputs, inputs); + case ov::element::f32: + return evaluate(as_type_ptr(node), outputs, inputs); + case ov::element::f64: + return evaluate(as_type_ptr(node), outputs, inputs); + default: + OPENVINO_THROW("Unhandled data type ", element_type.get_type_name(), + " in evaluate_node"); + } +} +``` + +## High-Level Rules + +- Register the op **only** in the latest opset. +- Do **not** edit older `opsetX_tbl.hpp` files or change existing registrations. +- Do **not** break compatibility of existing ops. +- Align style with existing ops in the codebase. + +## Output + +- Branch or patch with all created/updated files. +- Proceed to **core_op_testing** skill. diff --git a/.github/agents-prototype/skills/add-core-op/step3-testing.md b/.github/agents-prototype/skills/add-core-op/step3-testing.md new file mode 100644 index 000000000000..01c446418fdc --- /dev/null +++ b/.github/agents-prototype/skills/add-core-op/step3-testing.md @@ -0,0 +1,104 @@ +# Skill: Core Op Testing + +> Source: `skills/add-core-op/SKILL.md` (Step 3) +> Agent: `core_opspec_agent` + +## Prerequisites + +- Completed **core_op_implementation** skill - op class is implemented, + files created and registered. + +## Test Categories + +### 1. Type Propagation Tests (`type_prop`) + +**Location:** `openvino/src/core/tests/type_prop/.cpp` + +Tests for `validate_and_infer_types`: +- Correct output shape inference for valid inputs. +- Correct output element type inference. +- Static and dynamic input shapes. +- Partial shapes (unknown dimensions). +- Invalid input shapes → expect validation error. +- Invalid input types → expect validation error. +- Constant inputs (e.g. axis) → verify value is used. +- Interval-bounded dimensions: `PartialShape{Dimension(2,5), Dimension(1,4), 3}` — verify bounds are preserved. +- Fully-dynamic rank: `PartialShape::dynamic()` — output must also be rank-dynamic. +- Dimension symbol propagation: set symbols on input with `set_shape_symbols()`, verify output has same symbols via `get_shape_symbols()`. +- Scalar input: `Shape{}` (rank-0 tensor). +- **Each scenario should be a separate `TEST` — do not bundle multiple shape cases in one test function.** + +### 2. Visitor / Serialization Tests + +**Location:** `openvino/src/core/tests/visitors/op/.cpp` + +Tests for `visit_attributes`: +- All attributes are visited (serialized/deserialized correctly). +- Round-trip: create op → serialize → deserialize → compare attributes. + +### 3. Opset Count Update + +**Location:** `openvino/src/core/tests/opset.cpp` + +- Update the expected op count for the target opset. +- The test verifies the total number of ops registered in each opset table. + +### 4. Conformance Tests + +**Location:** `openvino/src/tests/functional/plugin/conformance/test_runner/op_conformance_runner/src/op_impl_check/single_op_graph.cpp` + +The conformance framework calls `OpImplCheckTest.checkPluginImplementation` for every registered op. +It looks up a model-building factory per op type. **If no entry is found, the model is `nullptr` +and the test fails with "Target model is empty!".** + +For unary elementwise ops (those that inherit from `UnaryElementwiseArithmetic`), add one `else if` +branch to the `generateUnaryEltwise()` function — alphabetically by op name: + +```cpp +} else if (ov::is_type(node)) { + eltwiseNode = std::make_shared(param); +} else if (ov::is_type(node)) { // ← add this + eltwiseNode = std::make_shared(param); +} else if (ov::is_type(node)) { +``` + +For ops that are **not** unary eltwise, add a dedicated `generate(const std::shared_ptr&)` overload instead (see other examples in the same file). + +**Build target (requires `ENABLE_TESTS=ON` in cmake):** + +```bash +cmake --preset RelWithDebInfo -DENABLE_TESTS=ON +cmake --build build/RelWithDebInfo --target ov_op_conformance_tests -j$(nproc) +``` + +### 5. Reference Implementation Tests (`op_reference`) + +**Location:** `openvino/src/plugins/template/tests/functional/op_reference/.cpp` + +- Test the `evaluate()` method against known input/output pairs. +- Cover all supported element types. +- Cover edge cases (empty tensors, scalar inputs, large shapes). +- Use the reference kernel from `openvino/reference/.hpp`. +- For domain-restricted ops (e.g. erfinv on (-1,1)): add boundary cases (e.g. x=±1 → ±inf) AND out-of-domain cases (e.g. |x|>1 → NaN). + +## Execution + +```bash +# Build with tests enabled +cmake --build build --target _test + +# Run type_prop tests +./bin/ov_core_unit_tests --gtest_filter="**" + +# Run visitor tests +./bin/ov_core_unit_tests --gtest_filter="*visitor**" + +# Run reference tests +./bin/ov_template_func_tests --gtest_filter="**" +``` + +## Output + +- All tests pass → proceed to **core_op_specification** skill. +- Test failures → fix implementation, re-run. Report issues to OV Orchestrator + if the fix requires changes outside core op scope. diff --git a/.github/agents-prototype/skills/add-core-op/step4-specification.md b/.github/agents-prototype/skills/add-core-op/step4-specification.md new file mode 100644 index 000000000000..2784ac096e4d --- /dev/null +++ b/.github/agents-prototype/skills/add-core-op/step4-specification.md @@ -0,0 +1,108 @@ +# Skill: Core Op Specification + +> Source: `skills/add-core-op/SKILL.md` (Step 4) +> Agent: `core_opspec_agent` + +## Prerequisites + +- Completed **core_op_testing** skill - op is implemented and all tests pass. + +## Specification File + +**Location:** +`openvino/docs/articles_en/documentation/openvino-ir-format/operation-sets/operation-specs//-.rst` + +**Example:** +`openvino/docs/articles_en/documentation/openvino-ir-format/operation-sets/operation-specs/signals/istft-16.rst` + +## Categories + +Place the `.rst` file in the appropriate category subdirectory: + +| Category | Operations | +|----------|-----------| +| `arithmetic` | Add, Subtract, Multiply, etc. | +| `comparison` | Equal, Greater, Less, etc. | +| `convolution` | Convolution, GroupConvolution, etc. | +| `detection` | DetectionOutput, NMS, etc. | +| `image` | Interpolate, etc. | +| `infrastructure` | Constant, Parameter, Result, etc. | +| `logical` | LogicalAnd, LogicalOr, etc. | +| `matrix` | MatMul, Einsum, etc. | +| `movement` | Gather, Scatter, Reshape, etc. | +| `normalization` | BatchNorm, LRN, MVN, etc. | +| `pooling` | MaxPool, AvgPool, etc. | +| `quantization` | FakeQuantize, etc. | +| `reduction` | ReduceMax, ReduceSum, etc. | +| `sequence` | LSTMSequence, GRUSequence, etc. | +| `signals` | DFT, IDFT, STFT, ISTFT, etc. | +| `sort` | TopK, NonMaxSuppression, etc. | +| `type` | Convert, ConvertLike, etc. | + +## RST Structure + +Follow the existing spec conventions. Typical structure: + +```rst +.. meta:: + :description: Learn about the OpName-X operation. + +.. _openvino_docs_ops__OpName_X: + +OpName - opset X +================ + +.. csv-table:: + :header: "Attribute", "Description" + + "Version", "opset X" + "Category", "" + "Brief", "" + +Description +----------- + + + +Attributes +---------- + + + +Inputs +------ + +
+ +Outputs +------- + +
+ +Types +----- + + + +Detailed Description +-------------------- + + + +Examples +-------- + + +``` + +## Validation + +- Spec covers all inputs, outputs, and attributes from the implementation. +- Math formula matches the `evaluate()` reference kernel. +- Supported types match `has_evaluate()`. +- Category matches the operation's purpose. + +## Output + +- Completed `.rst` specification file. +- All 4 steps done → report `success` + branch/patch to OV Orchestrator. diff --git a/.github/agents-prototype/skills/add-cpu-op/SKILL.md b/.github/agents-prototype/skills/add-cpu-op/SKILL.md new file mode 100644 index 000000000000..c46a4e70ce38 --- /dev/null +++ b/.github/agents-prototype/skills/add-cpu-op/SKILL.md @@ -0,0 +1,21 @@ +--- +name: add-cpu-op +description: Add a new operation to the OpenVINO CPU plugin — node registration, JIT/oneDNN executors (AVX2/AVX-512/AMX), and functional tests. +--- + +# Skill: Add CPU Op + +## When to use +When the Core OpSpec agent has produced a new op spec and you need to implement +CPU plugin support: node class, executor strategy, and single-layer tests. + +## Steps + +Execute in order — each step produces artifacts consumed by the next. + +| Step | File | Purpose | +|---|---|---| +| 1 | [step1-analysis.md](step1-analysis.md) | Read op spec, determine implementation strategy (reference / JIT / oneDNN), identify ISA targets and precision requirements | +| 2 | [step2-implementation.md](step2-implementation.md) | Create node class (header + source), register in factory, hook up shape inference | +| 3 | [step3-optimization.md](step3-optimization.md) | Write JIT executor (AVX2/AVX-512/AMX) or oneDNN-backed path; CpuParallel integration | +| 4 | [step4-testing.md](step4-testing.md) | Shared single-layer tests, custom CPU tests, dynamic shapes | diff --git a/.github/agents-prototype/skills/add-cpu-op/step1-analysis.md b/.github/agents-prototype/skills/add-cpu-op/step1-analysis.md new file mode 100644 index 000000000000..29dc61ba9d03 --- /dev/null +++ b/.github/agents-prototype/skills/add-cpu-op/step1-analysis.md @@ -0,0 +1,161 @@ +# Skill: CPU Op Analysis + +> Agent: `cpu_agent` — Step 1 of 4 + +## When to Use + +- A new operation needs to be enabled in the CPU plugin. +- An existing CPU node needs to be updated (new opset version, missing precision, + dynamic shape support). +- CPU inference fails with "not implemented" or unsupported-operation errors. + +## Prerequisites + +- Op name and opset version are known (from error context or task description). +- The OpenVINO repository is checked out and buildable. + +## Procedure + +### Step 1: Locate the Core Op Definition + +Find the core op class to understand the operation's contract: + +```bash +# Find the op header +find src/core/include/openvino/op/ -iname "**" + +# Check which opset it belongs to +grep -r "" src/core/include/openvino/opsets/ +``` + +Read the op header to extract: +- **Inputs**: count, element types, shape constraints. +- **Outputs**: count, element types, shape inference rules. +- **Attributes**: names, types, defaults. +- **Base class**: `Op`, `util::UnaryElementwiseArithmetic`, etc. + +### Step 2: Locate the Reference Implementation + +The reference implementation may reside in one of two places: + +**a) Standalone reference function:** +```bash +find src/core/reference/include/openvino/reference/ -iname "**" +``` + +**b) Inside the op's `evaluate()` method:** +Check the op header or source — many ops implement `evaluate()` directly, +which contains the reference algorithm. This is the method called by the +`Reference` fallback node. + +```bash +grep -n "evaluate" src/core/include/openvino/op/.hpp +grep -rn "evaluate" src/core/src/op/.cpp +``` + +Read the reference implementation (whichever location) to understand: +- Algorithm complexity (element-wise, reduction, convolution-like, etc.). +- Data type handling (templated on element type or generic). +- Whether it supports dynamic output shapes. + +### Step 3: Check if the Op Already Has a CPU Node + +```bash +# Check if a node exists +find src/plugins/intel_cpu/src/nodes/ -iname "**" + +# Check Type enum +grep -n "\|" src/plugins/intel_cpu/src/cpu_types.h + +# Check factory registration +grep -n "\|" src/plugins/intel_cpu/src/nodes_factory.cpp +``` + +If a dedicated node **already exists**, determine what needs updating: +- Missing precision support? +- Missing dynamic shape support? +- Performance issue requiring JIT optimisation? + +If **no dedicated node exists**, the op currently runs through the `Reference` +fallback node (if it has `evaluate()` implemented). However, not all reference +implementations are compiled into the shared libraries — some may be excluded +by selective build. A dedicated CPU node should implement its own optimised C++ +(cache-aware memory access, `CpuParallel` parallelisation, function inlining). +The core reference (from `ov::reference::` or the op's `evaluate()` method) +should be reused only if it already meets these performance criteria. + +### Step 4: Determine Implementation Strategy + +Choose one of these approaches based on the op characteristics: + +| Strategy | When to Use | Example Ops | +|----------|-------------|-------------| +| **Reference-only** | Simple ops where `Reference` fallback is sufficient; op is rarely used or not perf-critical | `Eye`, `Bucketize` | +| **Portable C++ with CpuParallel** | Trivially parallelisable, no SIMD benefit, moderate perf needs. Directly in the node's `execute()` method. | `SegmentMax`, `SearchSorted` | +| **Executor-based** | **Default for any op more complex than portable C++.** Must be used whenever JIT kernels, oneDNN primitives, ISA-specific paths (x64/ARM/RISC-V), or multiple backend implementations are involved. | `FullyConnected`, `Convolution`, `Eltwise`, `Interpolate`, `Reduce`, `Pooling` | + +> **Key rule:** The executor-based approach is the standard architecture for new +> CPU nodes. Even JIT-optimised and oneDNN-backed ops use the executor framework +> — the JIT kernel becomes an `ExecutorType::Jit` implementation, the oneDNN +> primitive becomes an `ExecutorType::Dnnl` implementation, and a portable C++ +> reference becomes an `ExecutorType::Reference` fallback. Study +> `eltwise_implementations.cpp`, `fullyconnected_implementations.cpp`, and +> `convolution_implementations.cpp` as canonical examples. + +Decision criteria: +1. **Is the op trivially simple and not perf-critical?** → Portable C++ in `execute()` with `CpuParallel` may suffice (no executor needed). +2. **Does the op benefit from JIT / SIMD / oneDNN / vendor libraries?** → Must use executor framework. JIT kernels, oneDNN primitives, and ACL/MLAS are registered as separate `ExecutorImplementation` entries. +3. **Does the op support multiple ISA or platform targets?** → Must use executor framework so it can automatically choose between JIT/Dnnl/ACL/Reference. +4. **Is dynamic output shape data-dependent?** → Needs custom `needShapeInfer()`. +5. **Do output shapes depend on the operation results?** → Use `InternalDynShapeInferFactory()` and update shape in `execute()`. + +### Step 5: Identify Supported Precisions and Layouts + +Standard precision support matrix: + +| Precision | Common Support | Notes | +|-----------|---------------|-------| +| `f32` | Always | Baseline precision | +| `bf16` | If ISA >= AVX-512 BF16 | Check `mayiuse(avx512_core_bf16)` | +| `f16` | If ISA >= AVX-512 FP16 | Check `mayiuse(avx512_core_fp16)` | +| `i8` / `u8` | For quantized ops | INT8 path | +| `i32` / `i64` | For index/integer ops | Index types | + +Standard layout types for `addSupportedPrimDesc`: + +| Layout | Constant | Use Case | +|--------|----------|----------| +| Planar (NCHW) | `LayoutType::ncsp` | Default, always supported | +| Channels-last (NHWC) | `LayoutType::nspc` | Conv, Pooling adjacency | + +### Step 6: Check for Existing Transformations + +```bash +grep -r "" src/plugins/intel_cpu/src/transformations/ +``` + +Some ops are decomposed or fused by CPU-specific transformations before reaching +the node layer. Understand these to avoid duplicating logic. + +## Output + +Return an analysis summary: + +``` +op_name: +opset_version: +core_op_class: +reference_impl: +existing_cpu_node: +strategy: +inputs: +outputs: +attributes: +precisions: +layouts: +dynamic_shapes: +target_isa: +transformations: +``` + +Proceed to **cpu_op_implementation** skill. diff --git a/.github/agents-prototype/skills/add-cpu-op/step2-implementation.md b/.github/agents-prototype/skills/add-cpu-op/step2-implementation.md new file mode 100644 index 000000000000..8c8d9bbe2b86 --- /dev/null +++ b/.github/agents-prototype/skills/add-cpu-op/step2-implementation.md @@ -0,0 +1,792 @@ +# Skill: CPU Op Implementation + +> Agent: `cpu_agent` — Step 2 of 4 + +## Prerequisites + +- Completed **cpu_op_analysis** skill — op name, strategy, precisions, layouts, + and shape inference approach are determined. +- Core op class exists in `src/core/include/openvino/op/`. +- Reference implementation exists in `src/core/reference/include/openvino/reference/` + and/or inside the core op's `evaluate()` method. + +## Fast Path: Eltwise Node (Unary Elementwise Ops) + +If the new op is a **simple unary elementwise** op (one input tensor → one output tensor, element-by-element, no attributes), the fastest path is to wire it through the **existing `Eltwise` node** instead of creating a new CPU node class. This avoids writing node boilerplate while still getting JIT emitter support and eltwise fusion chains. + +### Files to Update (Eltwise path) + +| File | Change | +|------|--------| +| `src/plugins/intel_cpu/src/cpu_types.h` | Add `EltwiseOpName` to the `Algorithm` enum | +| `src/plugins/intel_cpu/src/cpu_types.cpp` | Add string name mapping in `algToString` | +| `src/plugins/intel_cpu/src/nodes/eltwise.cpp` | Map `ov::op::vX::OpName` → `Algorithm::EltwiseOpName` in `getAlgorithmFor`; add 1-input count entry | +| `src/plugins/intel_cpu/src/nodes/executors/ref/eltwise.cpp` | Add scalar reference case in the `switch` statement | +| `src/plugins/intel_cpu/src/post_ops.hpp` | Add `OpName` to `ActivationPostOp::Type` enum | +| `src/plugins/intel_cpu/src/post_ops.cpp` | Add bidirectional mapping `EltwiseOpName` ↔ `ActivationPostOp::Type::OpName` | +| `src/plugins/intel_cpu/src/nodes/kernels/x64/jit_uni_eltwise_generic.cpp` | Register new emitter class | +| `src/plugins/intel_cpu/src/nodes/kernels/aarch64/jit_uni_eltwise_generic.cpp` | Register new emitter class | +| `src/plugins/intel_cpu/src/nodes/kernels/riscv64/jit_uni_eltwise_generic.cpp` | Register new emitter class | +| `src/plugins/intel_cpu/src/emitters/snippets/x64/cpu_generator.cpp` | Register for snippets JIT | +| `src/plugins/intel_cpu/src/emitters/snippets/aarch64/cpu_generator.cpp` | Register for snippets JIT | +| `src/plugins/intel_cpu/src/emitters/snippets/riscv64/cpu_generator.cpp` | Register for snippets JIT | +| `src/plugins/intel_cpu/src/nodes/executors/jit/eltwise.cpp` | Remove op from the JIT exclusion list (if present) | + +### JIT Emitter Files to Create + +| File | Change | +|------|--------| +| `src/plugins/intel_cpu/src/emitters/plugin/x64/jit_eltwise_emitters.hpp` | Declare `jit__emitter` class | +| `src/plugins/intel_cpu/src/emitters/plugin/x64/jit_eltwise_emitters.cpp` | Implement emitter + register table entries | +| `src/plugins/intel_cpu/src/emitters/plugin/aarch64/jit_eltwise_emitters.hpp` | Same for aarch64 | +| `src/plugins/intel_cpu/src/emitters/plugin/aarch64/jit_eltwise_emitters.cpp` | Same for aarch64 | +| `src/plugins/intel_cpu/src/emitters/plugin/riscv64/jit_eltwise_emitters.hpp` | Same for riscv64 | +| `src/plugins/intel_cpu/src/emitters/plugin/riscv64/jit_eltwise_emitters.cpp` | Same for riscv64 | + +### Checking oneDNN Post-Op Support + +Before implementing a JIT emitter, check if oneDNN already supports the op as a post-op. If it does, the `post_ops.hpp/cpp` wiring alone may be sufficient for post-op fusion chains. + +## File Structure + +All files follow **`snake_case`** for filenames, **`CamelCase`** for class names. +The build system uses `file(GLOB_RECURSE)` — new files under `src/` are +automatically picked up by CMake. No CMakeLists.txt edits needed for source files. + +### Files to Create + +| File | Purpose | +|------|---------| +| `src/plugins/intel_cpu/src/nodes/.h` | Node class header | +| `src/plugins/intel_cpu/src/nodes/.cpp` | Node class implementation | + +### Files to Create (Executor-based ops — any op more complex than portable C++) + +| File | Purpose | +|------|---------| +| `src/plugins/intel_cpu/src/nodes/executors/_config.hpp` | `OpNameAttrs` struct + `OpNameConfig` alias | +| `src/plugins/intel_cpu/src/nodes/executors/_implementations.cpp` | `getImplementations()` specialisation | +| `src/plugins/intel_cpu/src/nodes/executors/implementations.hpp` | (update) Add `getImplementations()` declaration | + +### Files to Update + +| File | Change | +|------|--------| +| `src/plugins/intel_cpu/src/cpu_types.h` | Add entry to `Type` enum | +| `src/plugins/intel_cpu/src/cpu_types.cpp` | Add string-to-Type mapping + `CASE` macro | +| `src/plugins/intel_cpu/src/nodes_factory.cpp` | Register node via `INTEL_CPU_NODE` macro | + +### Optional Files (if needed) + +| File | When | +|------|------| +| `src/plugins/intel_cpu/src/shape_inference/custom/.hpp` | Custom shape inference factory | +| `src/plugins/intel_cpu/src/shape_inference/custom/.cpp` | Custom shape inference implementation | +| `src/plugins/intel_cpu/src/nodes/kernels/x64/_kernel.hpp` | JIT kernel header (Step 3) | +| `src/plugins/intel_cpu/src/nodes/kernels/x64/_kernel.cpp` | JIT kernel implementation (Step 3) | + +## Step-by-Step Implementation + +### 1. Add Type Enum Entry + +In `src/plugins/intel_cpu/src/cpu_types.h`, add the new type to the `Type` enum: + +```cpp +enum class Type : uint8_t { + // ... existing entries ... + OpName, // <-- Add alphabetically or near related ops +}; +``` + +### 2. Add String-to-Type Mapping + +In `src/plugins/intel_cpu/src/cpu_types.cpp`, add the mapping in TWO places: + +**a) In the `type_to_name_tbl` map (op type string → Type enum):** + +```cpp +{"OpName", Type::OpName}, +``` + +**b) In the `type_to_str()` switch (Type enum → debug string), add a CASE:** + +```cpp +CASE(OpName); +``` + +> **Note:** The string key in `type_to_name_tbl` must match the op's +> `get_type_name()` return value exactly. Check the core op's `OPENVINO_OP("...")` +> macro to find the correct string. + +### 3. Create the Node Header + +Create `src/plugins/intel_cpu/src/nodes/.h`: + +```cpp +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#pragma once + +#include +#include +#include + +#include "graph_context.h" +#include "node.h" +#include "openvino/core/node.hpp" + +namespace ov::intel_cpu::node { + +class OpName : public Node { +public: + OpName(const std::shared_ptr& op, const GraphContext::CPtr& context); + + static bool isSupportedOperation(const std::shared_ptr& op, + std::string& errorMessage) noexcept; + void getSupportedDescriptors() override; + void initSupportedPrimitiveDescriptors() override; + void execute(const dnnl::stream& strm) override; + [[nodiscard]] bool created() const override; + [[nodiscard]] bool needPrepareParams() const override; + void executeDynamicImpl(const dnnl::stream& strm) override; + +private: + // Op-specific attributes + // Template helpers for type dispatch +}; + +} // namespace ov::intel_cpu::node +``` + +**Key decisions for the header:** + +| Question | If yes | If no | +|----------|--------|-------| +| Does the op need ISA-specific or multi-backend paths? | Use the Executor framework (`ExecutorFactory` + `ExecutorPtr`) — see section below | Use direct `execute()` with `OV_SWITCH` type dispatch | +| Does the op need custom shape inference? | Add custom `ShapeInferFactory` | Use `NgraphShapeInferFactory` | +| Does the op need `prepareParams()`? | Override `needPrepareParams` → `true` | Override → `false` | +| Does the op data-dependent output shape? | Override `needShapeInfer()` | Don't override | + +### 4. Create the Node Source + +Create `src/plugins/intel_cpu/src/nodes/.cpp`: + +```cpp +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include ".h" + +#include +#include +#include +#include + +#include "cpu_types.h" +#include "graph_context.h" +#include "memory_desc/cpu_memory_desc.h" +#include "node.h" +#include "onednn/iml_type_mapper.h" +#include "openvino/core/except.hpp" +#include "openvino/core/node.hpp" +#include "openvino/core/type.hpp" +#include "openvino/core/type/element_type.hpp" +#include "openvino/op/.hpp" // Core op header +#include "openvino/reference/.hpp" // Reference implementation +#include "selective_build.h" +#include "shape_inference/shape_inference_cpu.hpp" + +namespace ov::intel_cpu::node { + +// ═══════════════════════════════════════════════════════════════════ +// Constructor +// ═══════════════════════════════════════════════════════════════════ +OpName::OpName(const std::shared_ptr& op, const GraphContext::CPtr& context) + : Node(op, context, NgraphShapeInferFactory(op)) { + std::string errorMessage; + if (!isSupportedOperation(op, errorMessage)) { + OPENVINO_THROW_NOT_IMPLEMENTED(errorMessage); + } + // Extract attributes from the core op: + // const auto typed_op = ov::as_type_ptr(op); + // m_attribute = typed_op->get_attribute(); +} + +// ═══════════════════════════════════════════════════════════════════ +// Supported Operation Check +// ═══════════════════════════════════════════════════════════════════ +bool OpName::isSupportedOperation(const std::shared_ptr& op, + std::string& errorMessage) noexcept { + try { + if (!ov::is_type(op)) { + errorMessage = "Only opsetX OpName operation is supported"; + return false; + } + // Add additional checks: shape constraints, attribute values, etc. + } catch (...) { + return false; + } + return true; +} + +// ═══════════════════════════════════════════════════════════════════ +// Descriptor Setup +// ═══════════════════════════════════════════════════════════════════ +void OpName::getSupportedDescriptors() { + // Validation is already done in the core op. + // Add CPU-specific validation here if needed. +} + +void OpName::initSupportedPrimitiveDescriptors() { + if (!supportedPrimitiveDescriptors.empty()) { + return; + } + + auto inputPrecision = getOriginalInputPrecisionAtPort(0); + auto outputPrecision = getOriginalOutputPrecisionAtPort(0); + + // Constrain to supported precisions + if (none_of(inputPrecision, + ov::element::f32, + ov::element::bf16, // requires avx512_core_bf16 + ov::element::f16, + ov::element::i32)) { + inputPrecision = ov::element::f32; + } + + // Register supported primitive descriptors with layout+precision + addSupportedPrimDesc( + {{LayoutType::ncsp, inputPrecision}}, // inputs + {{LayoutType::ncsp, outputPrecision}}, // outputs + impl_desc_type::ref); // implementation type +} + +// ═══════════════════════════════════════════════════════════════════ +// Execution +// ═══════════════════════════════════════════════════════════════════ +bool OpName::created() const { + return getType() == Type::OpName; +} + +bool OpName::needPrepareParams() const { + return false; +} + +void OpName::executeDynamicImpl(const dnnl::stream& strm) { + execute(strm); +} + +void OpName::execute([[maybe_unused]] const dnnl::stream& strm) { + // Option A: Call the reference implementation directly + // ov::reference::op_name(getSrcDataAtPortAs(0), + // getDstDataAtPortAs(0), + // ov::Shape{getSrcMemoryAtPort(0)->getStaticDims()}, + // /* attributes */); + + // Option B: Use OV_SWITCH for type dispatch + // (see SearchSorted or SegmentMax for pattern) +} + +} // namespace ov::intel_cpu::node +``` + +### 5. Register in Node Factory + +In `src/plugins/intel_cpu/src/nodes_factory.cpp`: + +**a) Add the include:** + +```cpp +#include "nodes/.h" +``` + +**b) Add the registration macro in `NodesFactory::NodesFactory()`:** + +```cpp +INTEL_CPU_NODE(OpName, Type::OpName); +``` + +> **Architecture-specific registration:** If the node is only supported on +> x86-64 (e.g., has JIT kernels), wrap both the include and registration in: +> ```cpp +> #if defined(OPENVINO_ARCH_X86_64) +> INTEL_CPU_NODE(OpName, Type::OpName); +> #endif +> ``` + +### 6. Shape Inference + +**Standard ops** — Use `NgraphShapeInferFactory(op)` in the constructor (default). +This delegates to the core op's `validate_and_infer_types()`. + +**Custom shape inference** — When the core shape inference is insufficient or when +output shapes depend on input data at runtime: + +Create `src/plugins/intel_cpu/src/shape_inference/custom/.hpp`: + +```cpp +#pragma once + +#include +#include + +#include "openvino/core/node.hpp" +#include "shape_inference/shape_inference_cpu.hpp" + +namespace ov::intel_cpu::node { + +class OpNameShapeInferFactory : public ShapeInferFactory { +public: + explicit OpNameShapeInferFactory(std::shared_ptr op) : m_op(std::move(op)) {} + [[nodiscard]] ShapeInferPtr makeShapeInfer() const override; + +private: + std::shared_ptr m_op; +}; + +} // namespace ov::intel_cpu::node +``` + +Then in the node constructor, use `OpNameShapeInferFactory(op)` instead of +`NgraphShapeInferFactory(op)`. + +### 7. Dynamic Shapes Support + +Dynamic shapes are a **mandatory** requirement for new CPU node implementations. + +Key methods to implement: + +| Method | Purpose | +|--------|---------| +| `executeDynamicImpl(strm)` | Called during dynamic-shape execution. Usually delegates to `execute(strm)`. | +| `needPrepareParams()` | Return `true` if internal state (e.g., JIT kernel) must be rebuilt when shapes change. | +| `needShapeInfer()` | Override if output shape depends on input **data** (not just input shapes). | +| `createPrimitive()` | Called once after shapes are resolved. Allocate JIT kernels here (with caching). | + +**Pattern for data-dependent output shapes** (see `SegmentMax` for reference): + +```cpp +bool OpName::needShapeInfer() const { + if (inputShapesModified()) { + return true; + } + // Check if data that affects output shape has changed + // Compare against cached values + return false; +} +``` + +### 8. Build Verification + +```bash +cd build +cmake --build . --target ov_cpu_func_tests -j$(nproc) 2>&1 | tail -20 +# Or for a quicker check: +cmake --build . --target openvino_intel_cpu_plugin -j$(nproc) 2>&1 | tail -20 +``` + +Verify no compilation errors before proceeding. + +## Type Dispatch Pattern (OV_SWITCH — Mandatory for Conditional Compilation) + +For ops that need to handle multiple element types, **always** use the `OV_SWITCH` +macro for precision dispatch. This is **required** for the CPU plugin's +conditional compilation feature — `OV_SWITCH` allows the build system to +eliminate unused type specialisations at compile time, reducing binary size. + +**Do NOT** use manual `if/else` or `switch` chains on element type — they break +conditional compilation. + +```cpp +namespace { +struct OpNameContext { + OpName& node; +}; +} // namespace + +template +struct OpName::OpNameExecute { + void operator()(OpNameContext& ctx) { + ctx.node.executeImpl(); + } +}; + +void OpName::execute([[maybe_unused]] const dnnl::stream& strm) { + auto precision = getParentEdgeAt(0)->getMemory().getDesc().getPrecision(); + OpNameContext ctx = {*this}; + OV_SWITCH(intel_cpu, + OpNameExecute, + ctx, + precision, + OV_CASE(ov::element::f32, float), + OV_CASE(ov::element::bf16, ov::bfloat16), + OV_CASE(ov::element::f16, ov::float16), + OV_CASE(ov::element::i32, int32_t)) +} +``` + +## Executor Pattern (Standard Architecture for Non-Trivial Ops) + +The executor framework is the **standard architecture** for any CPU node that is +more complex than simple portable C++ code. This includes ops with: +- JIT kernels (these become `ExecutorType::Jit` implementations) +- oneDNN primitives (these become `ExecutorType::Dnnl` implementations) +- Multiple backend targets (x64/ARM/RISC-V) +- Any ISA-specific optimisation + +Study these canonical implementations in this order: +1. `eltwise_implementations.cpp` — simplest executor pattern (Jit + ACL + Reference) +2. `fullyconnected_implementations.cpp` — full-featured (MLAS, 1x1conv, ACL, KleidiAI, oneDNN) +3. `convolution_implementations.cpp` — layout-aware implementations with `MemoryFormatFilter` + +### Architecture Overview + +``` +Node (e.g. Eltwise, FullyConnected, Convolution) + ├── OpNameAttrs (op config struct: _config.hpp) + ├── MemoryArgs memory (maps ARG_SRC/ARG_DST/... → MemoryPtr) + ├── ExecutorFactoryPtr factory + │ ├── filters ExecutorImplementation list by: + │ │ ├── supports(config, memoryFormatFilter) — ISA, precision, layout + │ │ ├── createOptimalConfig(config) — preferred memory format + │ │ └── acceptsShape(attrs, memory) — shape-dependent heuristics + │ └── make(memory) → returns ExecutorPtr or VariableExecutor + └── ExecutorPtr executor + ├── update(memory) — called in prepareParams() + └── execute(memory) — called in execute() +``` + +### Step 1: Define the Attrs Struct + +Create `src/plugins/intel_cpu/src/nodes/executors/_config.hpp`: + +```cpp +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#pragma once + +#include "executor_config.hpp" +#include "post_ops.hpp" // if post-ops fusion is supported + +namespace ov::intel_cpu { + +struct OpNameAttrs { + // Op-specific attributes needed by all implementations. + // Examples: float epsilon; int axis; bool keep_dims; + PostOps postOps; // include if the op supports post-op fusion +}; + +using OpNameConfig = executor::Config; + +} // namespace ov::intel_cpu +``` + +### Step 2: Register the `getImplementations` Specialisation + +**a) Declare in `implementations.hpp`** — add alongside existing declarations: + +```cpp +// OpName +template <> +const std::vector>& getImplementations(); +``` + +**b) Define in `_implementations.cpp`:** + +The file registers all implementation variants in **priority order** (highest +first). Each entry uses an `OV_CPU_INSTANCE_*` macro that conditionally compiles +the implementation based on the target architecture: + +| Macro | Active When | +|-------|-------------| +| `OV_CPU_INSTANCE_COMMON(...)` | Always (portable code) | +| `OV_CPU_INSTANCE_X64(...)` | x86-64 only | +| `OV_CPU_INSTANCE_DNNL_X64(...)` | x86-64 with oneDNN | +| `OV_CPU_INSTANCE_DNNL(...)` | Any arch with oneDNN | +| `OV_CPU_INSTANCE_ACL(...)` | AArch64 with ARM Compute Library | +| `OV_CPU_INSTANCE_KLEIDIAI(...)` | AArch64 with KleidiAI | +| `OV_CPU_INSTANCE_MLAS_X64(...)` | x86-64 with MLAS | +| `OV_CPU_INSTANCE_RISCV64(...)` | RISC-V 64 only | + +Each `ExecutorImplementation` has 6 fields: +1. **name** — unique human-readable string (e.g. `"op_name_jit_ncsp"`) +2. **ExecutorType** — `Jit`, `Dnnl`, `Acl`, `Reference`, etc. +3. **OperationType** — `OperationType::OpName` +4. **supports** — lambda returning `bool`. Use `VERIFY(condition, MESSAGE)` macro + for debuggable rejection. Can take `(const Config&)` or + `(const Config&, const MemoryFormatFilter&)`. +5. **createOptimalConfig** — lambda returning `std::optional`. Return + `{}` (nullopt) if the current memory config is already acceptable. Use + `createOptimalConfigCommon()` helper with `TypeMapping` and `LayoutConfig`. +6. **acceptsShape** — lambda `(const Attrs&, const MemoryArgs&) -> bool`. Pass + `AcceptsAnyShape` (or `{}`) for shape-agnostic implementations. +7. **create** — lambda returning `ExecutorPtr`. Use `CreateDefault{}` + for simple cases or a custom lambda for complex wiring (e.g., `DnnlExecutor`). + +```cpp +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "nodes/executors/executor_implementation.hpp" +#include "nodes/executors/implementations.hpp" +#include "nodes/executors/implementation_utils.hpp" +#include "nodes/executors/debug_messages.hpp" +#include "nodes/executors/_config.hpp" +#include "nodes/executors/precision_translation.hpp" +#include "nodes/executors/type_mask.hpp" +#include "utils/arch_macros.h" + +// Include the actual executor headers: +#include "nodes/executors/jit/_jit.hpp" // JIT executor (x64) +// #include "nodes/executors/acl/_acl.hpp" // ACL executor (ARM) +#include "nodes/executors/ref/_ref.hpp" // Reference executor + +#if defined(OPENVINO_ARCH_X86_64) +# include "cpu/x64/cpu_isa_traits.hpp" +#endif + +namespace ov::intel_cpu { + +using namespace ov::element; +using namespace TypeMaskAlias; +using namespace executor; + +using LayoutConfig = std::vector; + +// TypeMapping defines precision transformations per implementation. +// Each row: {input_precisions..., output_precisions...} → {transform_functions...} +// Transform helpers: bypass() = keep as-is, just() = force to T, use() = match arg N +static const TypeMapping opNameTypeMapping { + // {src, dst} pt + {{_f32, _f32}, {bypass(), bypass()}}, + {{_bf16, _bf16 | _f32}, {bypass(), bypass()}}, + {{_any, _any}, {just(), just()}}, // fallback +}; + +static const MappingNotation opNameMappingNotation { + {ARG_SRC, 0}, + {ARG_DST, 1}, +}; + +// to keep OV_CPU_INSTANCE macros aligned +// clang-format off +template <> +const std::vector>& getImplementations() { + static const std::vector> implementations { + OV_CPU_INSTANCE_X64( + "op_name_jit_ncsp", + ExecutorType::Jit, + OperationType::OpName, + [](const OpNameConfig& config) -> bool { + VERIFY(dnnl::impl::cpu::x64::mayiuse( + dnnl::impl::cpu::x64::avx2), UNSUPPORTED_ISA); + // Add precision / attr checks as needed + return true; + }, + // createOptimalConfig + [](const OpNameConfig& config) -> std::optional { + return createOptimalConfigCommon(config, + opNameTypeMapping, + LayoutConfig{LayoutType::ncsp, LayoutType::ncsp}, + opNameMappingNotation); + }, + AcceptsAnyShape, + CreateDefault{} + ) + OV_CPU_INSTANCE_COMMON( + "op_name_ref_ncsp", + ExecutorType::Reference, + OperationType::OpName, + [](const OpNameConfig& config) -> bool { return true; }, + [](const OpNameConfig& config) -> std::optional { + return createOptimalConfigCommon(config, + opNameTypeMapping, + LayoutConfig{LayoutType::ncsp, LayoutType::ncsp}, + opNameMappingNotation); + }, + AcceptsAnyShape, + CreateDefault{} + ) + }; + return implementations; +} +// clang-format on + +} // namespace ov::intel_cpu +``` + +### Step 3: Implement the Concrete Executor Classes + +Each executor implements the `Executor` base class from `executor.hpp`: + +```cpp +class Executor { +public: + virtual bool update(const MemoryArgs& memory) = 0; // reshape / re-init + virtual void execute(const MemoryArgs& memory) = 0; // run computation + virtual impl_desc_type implType() const = 0; // report impl type + virtual ~Executor() = default; +}; +``` + +**Reference executor** (portable C++): + +```cpp +class RefOpNameExecutor : public Executor { +public: + RefOpNameExecutor(const OpNameAttrs& attrs, + const MemoryArgs& memory, + const ExecutorContext::CPtr& context); + bool update(const MemoryArgs& memory) override; + void execute(const MemoryArgs& memory) override; + impl_desc_type implType() const override { return impl_desc_type::ref; } +}; +``` + +**JIT executor** (wraps JIT kernel + CpuParallel): + +```cpp +class JitOpNameExecutor : public Executor { +public: + JitOpNameExecutor(const OpNameAttrs& attrs, + const MemoryArgs& memory, + const ExecutorContext::CPtr& context); + bool update(const MemoryArgs& memory) override; + void execute(const MemoryArgs& memory) override; + impl_desc_type implType() const override { return impl_desc_type::jit_avx2; } +private: + std::shared_ptr m_kernel; + // ... kernel compile params, context, etc. +}; +``` + +### Step 4: Wire the Node to Use ExecutorFactory + +In the **node header**, replace direct `execute()` logic with executor fields: + +```cpp +#include "nodes/executors/executor.hpp" +#include "nodes/executors/executor_factory.hpp" +#include "nodes/executors/_config.hpp" +#include "nodes/executors/memory_arguments.hpp" + +// Inside the class: +private: + OpNameAttrs m_attrs; + MemoryArgs m_memory; + ExecutorFactoryPtr m_factory; + ExecutorPtr m_executor = nullptr; +``` + +In **`initSupportedPrimitiveDescriptors()`** (follows `conv.cpp` pattern): + +```cpp +void OpName::initSupportedPrimitiveDescriptors() { + // 1. Populate m_attrs from the core op + // m_attrs.epsilon = ...; m_attrs.axis = ...; + // m_attrs.postOps = getPostOps(fusedWith); + + // 2. Build memory descriptors per port + const auto& creatorsMap = BlockedDescCreator::getCommonCreators(); + VecMemoryDescs srcDescs; + for (size_t i = 0; i < getOriginalInputsNumber(); i++) { + srcDescs.push_back(creatorsMap.at(LayoutType::ncsp)->createSharedDesc( + getOriginalInputPrecisionAtPort(i), getInputShapeAtPort(i))); + } + auto dstDesc = creatorsMap.at(LayoutType::ncsp)->createSharedDesc( + getOriginalOutputPrecisionAtPort(0), getOutputShapeAtPort(0)); + + MemoryDescArgs descs{ + {ARG_SRC, srcDescs[0]}, + {ARG_DST, dstDesc}, + }; + + // 3. Create executor factory — this filters & ranks implementations + auto executionContext = std::make_shared( + context, getImplPriority()); + m_factory = std::make_shared>( + m_attrs, executionContext, descs); + + // 4. Let factory decide optimal memory descriptors + const auto nodeDescList = m_factory->getProperMemoryDescriptors(descs); + for (const auto& nodeDescs : nodeDescList) { + NodeConfig nodeConfig; + nodeConfig.inConfs.resize(srcDescs.size()); + // ... populate from nodeDescs (see conv.cpp / fc.cpp pattern) ... + nodeConfig.outConfs.emplace_back(nodeDescs.at(ARG_DST)); + supportedPrimitiveDescriptors.emplace_back(nodeConfig, impl_desc_type::undef); + } +} +``` + +In **`prepareParams()`**: + +```cpp +void OpName::prepareParams() { + m_memory[ARG_SRC] = getSrcMemoryAtPort(0); + m_memory[ARG_DST] = getDstMemoryAtPort(0); + + if (!m_executor) { + m_executor = m_factory->make(m_memory); + } + m_executor->update(m_memory); +} +``` + +In **`execute()`**: + +```cpp +void OpName::execute([[maybe_unused]] const dnnl::stream& strm) { + m_executor->execute(m_memory); +} +``` + +### Key Executor Framework Utilities + +| Utility | Location | Purpose | +|---------|----------|---------| +| `VERIFY(cond, msg)` | `debug_messages.hpp` | Debug-logged rejection in `supports()` | +| `TypeMapping` / `MappingNotation` | `precision_translation.hpp` | Precision transform rules | +| `createOptimalConfigCommon()` | `implementation_utils.hpp` | Standard config optimisation | +| `AcceptsAnyShape` | `implementation_utils.hpp` | Shape-agnostic stub | +| `HasNoOptimalConfig` | `implementation_utils.hpp` | No config override stub | +| `SupportsAnyConfig` | `implementation_utils.hpp` | Unconditional support stub | +| `CreateDefault` | `implementation_utils.hpp` | Simple `make_shared` factory | +| `CreateDnnlDefault` | `implementation_utils.hpp` | `DnnlExecutor` wrapper factory | +| `OV_CPU_INSTANCE_*` macros | `utils/arch_macros.h` | Conditional compilation per arch | + +> **Key advantage:** The `ExecutorFactory` automatically handles runtime +> selection between JIT / oneDNN / ACL / Reference implementations based on +> hardware capabilities, precision matching, and memory format preferences. +> When multiple implementations match, `VariableExecutor` handles fallback. +> The `OV_CPU_INSTANCE_*` macros ensure that code for unavailable platforms is +> compiled away, supporting the conditional compilation infrastructure. + +## Code Quality Checklist + +Before proceeding to the next step, verify: + +- [ ] `clang-format` passes: code follows `src/.clang-format` rules + (Google style, 4-space indent, 120 col limit). +- [ ] `clang-tidy` passes: code follows `src/plugins/intel_cpu/src/.clang-tidy` + rules. +- [ ] Copyright header is present on all new files. +- [ ] SPDX license identifier: `Apache-2.0`. +- [ ] Namespace is `ov::intel_cpu::node`. +- [ ] `[[nodiscard]]` on const getter methods. +- [ ] `[[maybe_unused]]` on `const dnnl::stream& strm` in `execute()` when + the stream is not used. +- [ ] No raw `new` / `delete` — use smart pointers. +- [ ] No `using namespace std;` or similar broad using-directives. + +## Output + +- All source files created/updated per the file structure above. +- Build compiles without errors. +- Proceed to **cpu_op_optimization** skill. diff --git a/.github/agents-prototype/skills/add-cpu-op/step3-optimization.md b/.github/agents-prototype/skills/add-cpu-op/step3-optimization.md new file mode 100644 index 000000000000..94c068598037 --- /dev/null +++ b/.github/agents-prototype/skills/add-cpu-op/step3-optimization.md @@ -0,0 +1,344 @@ +# Skill: CPU Op Optimization + +> Agent: `cpu_agent` — Step 3 of 4 + +## Prerequisites + +- Completed **cpu_op_implementation** skill — node class compiles and executes + via reference implementation. +- ISA targets and implementation strategy determined in **cpu_op_analysis**. + +## When to Apply + +This step is **mandatory** for performance-critical ops (element-wise, reductions, +attention, normalization). It may be **skipped** for simple ops where the reference +implementation is sufficient (mark the Step 3 checkpoint or step-local status as +`skipped`; the final job `status` should still be `success` if implementation and +tests pass). + +Skip criteria: +- Op is rarely used in production models. +- Reference implementation already meets performance requirements. +- Op is pure data movement (handled efficiently by `Reorder` or `Reshape`). + +## Optimization Techniques + +All optimization techniques described below are used **inside executor +implementations**. The executor framework handles ISA dispatch, fallback +ordering, and precision matching — see `cpu_op_implementation` skill for +the full wiring pattern. This skill focuses on the code that goes **inside** +each executor class. + +### CpuParallel Multithreading + +The simplest and most common optimization. Use `CpuParallel` (available via +`context->getCpuParallel()`) to parallelise the execution loop inside any +executor — Reference, JIT, or oneDNN. + +**File:** `src/plugins/intel_cpu/src/cpu_parallel.hpp` + +**Available methods:** + +| Method | Signature | Use Case | +|--------|-----------|----------| +| `parallel_for` | `(D0, func(i))` | 1D iteration | +| `parallel_for2d` | `(D0, D1, func(i, j))` | 2D iteration | +| `parallel_for3d` | `(D0, D1, D2, func(i, j, k))` | 3D iteration (batch × spatial) | +| `parallel_for4d` | `(D0, D1, D2, D3, func(...))` | 4D (batch × channels × H × W) | +| `parallel_for5d` | `(D0, D1, D2, D3, D4, func(...))` | 5D tensors | +| `parallel_for6d` | `(D0, ..., D5, func(...))` | 6D tensors | +| `parallel_sum` | `(D0, init, func(i))` | Parallel reduction | +| `parallel_sum2d` | `(D0, D1, init, func(i, j))` | 2D parallel reduction | +| `parallel_sum3d` | `(D0, D1, D2, init, func(i, j, k))` | 3D parallel reduction | +| `parallel_simple` | `(nthr, func(ithr, nthr))` | Manual thread partitioning | + +**Example — element-wise op with CpuParallel inside a Reference executor:** + +```cpp +void RefOpNameExecutor::execute(const MemoryArgs& memory) { + auto* src = memory.at(ARG_SRC)->getDataAs(); + auto* dst = memory.at(ARG_DST)->getDataAs(); + auto total = memory.at(ARG_SRC)->getShape().getElementsCount(); + + m_context->getCpuParallel()->parallel_for(total, [&](size_t i) { + dst[i] = /* element-wise computation on */ src[i]; + }); +} +``` + +**Example — batch-parallelised reduction:** + +```cpp +const auto& shape = memory.at(ARG_SRC)->getStaticDims(); +auto batch = shape_size(shape) / shape.back(); +auto inner = shape.back(); + +m_context->getCpuParallel()->parallel_for(batch, [&](size_t b) { + auto* row_src = src + b * inner; + auto* row_dst = dst + b * inner; + // Per-row reduction + normalization + float sum = 0.0f; + for (size_t i = 0; i < inner; i++) { + sum += row_src[i] * row_src[i]; + } + float scale = 1.0f / std::sqrt(sum / inner + eps); + for (size_t i = 0; i < inner; i++) { + row_dst[i] = row_src[i] * scale; + } +}); +``` + +### Creating JIT Executor Implementations + +JIT kernels are **not** used directly from the node class. Instead, they are +wrapped inside an executor class that implements the `Executor` interface and +is registered as an `ExecutorType::Jit` entry in `_implementations.cpp`. + +The ISA dispatch (`mayiuse(avx512_core)` vs `mayiuse(avx2)`) happens in the +`supports()` predicate of the `ExecutorImplementation`, **not** in the node. + +**Study** `eltwise_implementations.cpp` — it shows Jit executors +(`eltwise_jit_ncsp`, `eltwise_jit_nspc`) alongside Reference executors, all +as `ExecutorImplementation` entries. + +#### Step 1: Write the JIT Kernel + +**Directory:** `src/plugins/intel_cpu/src/nodes/kernels/x64/` + +```cpp +// _kernel.hpp +#pragma once + +#include "nodes/kernels/x64/jit_kernel_base.hpp" + +namespace ov::intel_cpu::kernel { + +struct jit__compile_params { + ov::element::Type src_prc; + ov::element::Type dst_prc; + size_t data_size; + // Op-specific compile-time parameters +}; + +struct jit__call_args { + const uint8_t* src; + uint8_t* dst; + // Op-specific runtime arguments +}; + +template +class jit__kernel : public JitKernelBase { +public: + DECLARE_CPU_JIT_AUX_FUNCTIONS(jit__kernel) + + explicit jit__kernel(const jit__compile_params& jcp); + void generate() override; + +private: + jit__compile_params m_jcp; + // Register assignments, helper methods +}; + +} // namespace ov::intel_cpu::kernel +``` + +#### Step 2: Wrap the Kernel in a JIT Executor Class + +Place in `src/plugins/intel_cpu/src/nodes/executors/jit/_jit.hpp`: + +```cpp +#pragma once + +#include "nodes/executors/executor.hpp" +#include "nodes/executors/_config.hpp" +#include "nodes/executors/executor_context.hpp" + +namespace ov::intel_cpu { + +class JitOpNameExecutor : public Executor { +public: + JitOpNameExecutor(const OpNameAttrs& attrs, + const MemoryArgs& memory, + const ExecutorContext::CPtr& context); + + bool update(const MemoryArgs& memory) override; + void execute(const MemoryArgs& memory) override; + impl_desc_type implType() const override { return impl_desc_type::jit_avx2; } + +private: + OpNameAttrs m_attrs; + ExecutorContext::CPtr m_context; + std::shared_ptr m_kernel; + // ... cached shape info, working buffers +}; +``` + +The constructor creates / compiles the JIT kernel. `update()` re-compiles if +shapes changed. `execute()` sets up call args and invokes the kernel inside +a `CpuParallel` loop over the outer dimension. + +#### Step 3: Register in `_implementations.cpp` + +```cpp +OV_CPU_INSTANCE_X64( + "op_name_jit_ncsp", + ExecutorType::Jit, + OperationType::OpName, + [](const OpNameConfig& config) -> bool { + VERIFY(dnnl::impl::cpu::x64::mayiuse( + dnnl::impl::cpu::x64::avx2), UNSUPPORTED_ISA); + return true; + }, + [](const OpNameConfig& config) -> std::optional { + return createOptimalConfigCommon(config, + opNameTypeMapping, + LayoutConfig{LayoutType::ncsp, LayoutType::ncsp}, + opNameMappingNotation); + }, + AcceptsAnyShape, + CreateDefault{} + ) +``` + +For an AVX-512 variant, add a **second** entry before the AVX2 one (higher +priority first) with `mayiuse(avx512_core)` in the `supports` predicate and +a correspondingly wider implementation. + +**ISA-specific considerations:** + +| ISA | Vector Width | Key Features | +|-----|-------------|--------------| +| AVX2 | 256-bit (8×f32) | `ymm` registers, FMA, most common JIT baseline | +| AVX-512 (Core) | 512-bit (16×f32) | `zmm` registers, mask registers, wider SIMD | +| AVX-512 BF16 | 512-bit | `avx512_core_bf16` — required for BF16 compute | +| AVX-512 VNNI | 512-bit | `vpdpbusd` for INT8 dot products | +| AMX | Tile-based | `tdpbf16ps` / `tdpbssd` for matrix ops | + +> **Note:** SSE 4.2 is no longer a target ISA. The minimal baseline is portable +> C++ code. JIT kernels start from AVX2. + +**Reference JIT executor implementations to study:** + +| Op | Executor | Key Pattern | +|----|----------|-------------| +| Eltwise | `EltwiseStatefulExecutor` (Jit + Ref) | Element-wise, multiple layouts | +| RMSNorm | `nodes/kernels/x64/rms_kernel.hpp` | Reduction + normalize | +| RoPE | `nodes/kernels/x64/rope_kernel.hpp` | Element-wise with indexing | + +### Creating oneDNN Executor Implementations + +For ops that map to oneDNN primitives (convolutions, matmul, pooling, softmax, +layer normalization, etc.), create an `ExecutorType::Dnnl` implementation using +the `DnnlExecutor` wrapper. + +**Study** `convolution_implementations.cpp` — it registers multiple oneDNN +variants differentiated by memory layout (nspc, ncsp, nCsp16c, nCsp8c), each +as a separate `ExecutorImplementation`. + +#### DnnlExecutor Pattern + +Use the `CreateDnnlDefault` helper to create a standard +`DnnlExecutor` that wraps a oneDNN primitive: + +```cpp +OV_CPU_INSTANCE_DNNL_X64( + "op_name_dnnl_nspc", + ExecutorType::Dnnl, + OperationType::OpName, + [](const OpNameConfig& config, const MemoryFormatFilter& filter) -> bool { + VERIFY(filter.isSuitableMemoryFormatFilter( + MemoryFormatFilter::nspc), UNSUPPORTED_MEMORY_FORMAT); + return true; + }, + [](const OpNameConfig& config) -> std::optional { + return createOptimalConfigCommon(config, + opNameDnnlTypeMapping, + LayoutConfig{LayoutType::nspc, LayoutType::nspc}, + opNameMappingNotation); + }, + AcceptsAnyShape, + CreateDnnlDefault{} + ) +``` + +The `DnnlExecutor` template: +- Calls `Primitive::createDescriptors()` to build oneDNN primitive descriptors +- Manages primitive caching via `ExecutorContext` +- Implements `update()` / `execute()` around the oneDNN primitive execution + +**Note:** `CreateDnnlDefault` accepts optional flags: `{cacheWeights, fc3Das2D}`. + +#### `MemoryFormatFilter` + +oneDNN implementations typically require specific memory layouts. Use the +extended `supports` predicate form that takes `MemoryFormatFilter`: + +```cpp +[](const OpNameConfig& config, const MemoryFormatFilter& filter) -> bool { + VERIFY(filter.isSuitableMemoryFormatFilter( + MemoryFormatFilter::nspc), UNSUPPORTED_MEMORY_FORMAT); + // additional ISA / precision checks + return true; +} +``` + +## JIT Eltwise Emitters + +For ops routed through the `Eltwise` node (see "Fast Path" in [**cpu_op_implementation**](skills/add-cpu-op/step2-implementation.md)), JIT execution uses **eltwise emitter classes** rather than the executor framework. Each ISA has its own emitter base class. + +### Emitter Class Pattern + +```cpp +// Header (jit_eltwise_emitters.hpp) +class jit__emitter : public jit_emitter { +public: + jit__emitter(dnnl::impl::cpu::x64::jit_generator* host, + dnnl::impl::cpu::x64::cpu_isa_t host_isa, + ov::element::Type exec_prc = ov::element::f32); + + size_t get_inputs_count() const override { return 1; } + +private: + void emit_impl(const std::vector& in_vec_idxs, + const std::vector& out_vec_idxs) const override; + template + void emit_isa(const std::vector& in_vec_idxs, + const std::vector& out_vec_idxs) const; + void register_table_entries() override; + size_t aux_vecs_count() const override; // return count of needed aux vmm registers +}; +``` + +### Coefficient Tables + +Use `push_arg_entry_of("name", hex_value, true)` in `register_table_entries()`: + +```cpp +void jit__emitter::register_table_entries() { + // Float constants as hex IEEE 754 + push_arg_entry_of("coeff_a", 0x3f800000, true); // 1.0f + push_arg_entry_of("abs_mask", 0x7fffffff, true); // strip sign bit + push_arg_entry_of("pos_inf", 0x7f800000, true); // +inf + push_arg_entry_of("qnan", 0x7fc00000, true); // quiet NaN +} +// Access in emit_isa: table_val("coeff_a") +``` + +## Functional Verification + +After optimization, verify correctness before proceeding to full testing: + +```bash +# Quick functional check +cd build +cmake --build . --target ov_cpu_func_tests -j$(nproc) +./bin/intel64/Release/ov_cpu_func_tests --gtest_filter=** +``` + +## Output + +- Optimized implementation with ISA-specific paths. +- JIT kernels created (if applicable). +- `CpuParallel` integration for multi-threaded execution. +- Functional verification passes. +- Proceed to **cpu_op_testing** skill. diff --git a/.github/agents-prototype/skills/add-cpu-op/step4-testing.md b/.github/agents-prototype/skills/add-cpu-op/step4-testing.md new file mode 100644 index 000000000000..7ab72c734095 --- /dev/null +++ b/.github/agents-prototype/skills/add-cpu-op/step4-testing.md @@ -0,0 +1,330 @@ +# Skill: CPU Op Testing + +> Agent: `cpu_agent` — Step 4 of 4 + +## Prerequisites + +- Completed **cpu_op_implementation** — node compiles and has basic execution. +- Completed **cpu_op_optimization** (or skipped if reference-only). + +## Test Categories + +### 1. Shared Single-Layer Tests (Mandatory) + +**Directory:** `src/plugins/intel_cpu/tests/functional/shared_tests_instances/single_layer_tests/` + +These tests instantiate the shared OpenVINO layer tests that are common across +all plugins (CPU, GPU, Template). They ensure the CPU implementation produces +results consistent with the reference. + +Dynamic shape scenarios can also be tested via shared single-layer tests by +using `ov::test::static_shapes_to_test_representation` with dynamic `InputShape` +entries (e.g., `{{-1, -1}, {{1, 16}, {2, 32}}}`), not only static shapes. + +**File to create:** `.cpp` + +**Pattern:** + +```cpp +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "single_op_tests/.hpp" +#include "common_test_utils/test_constants.hpp" + +namespace { +using ov::test::OpNameLayerTest; + +const std::vector model_types = { + ov::element::f32, + ov::element::bf16, + ov::element::f16, +}; + +const std::vector> input_shapes_static = { + {{1, 16}}, + {{2, 32}}, + {{1, 3, 224, 224}}, + {{2, 64, 7, 7}}, +}; + +// Dynamic shapes can also be tested in shared tests using InputShape: +// const std::vector> input_shapes_dynamic = { +// {{{-1, -1}, {{1, 16}, {2, 32}}}}, +// {{{{1, 4}, {16, 64}}, {{1, 16}, {4, 64}}}}, +// }; + +const auto params = ::testing::Combine( + ::testing::ValuesIn( + ov::test::static_shapes_to_test_representation(input_shapes_static)), + ::testing::ValuesIn(model_types), + // Op-specific parameters + ::testing::Values(ov::test::utils::DEVICE_CPU) +); + +INSTANTIATE_TEST_SUITE_P(smoke_OpName, + OpNameLayerTest, + params, + OpNameLayerTest::getTestCaseName); + +} // namespace +``` + +**Test naming:** Use the `smoke_` prefix for basic functionality tests, +`nightly_` for exhaustive tests. + +### 2. Custom CPU Single-Layer Tests (Recommended) + +**Directory:** `src/plugins/intel_cpu/tests/functional/custom/single_layer_tests/` + +These tests verify CPU-specific implementation details that shared tests don't +cover: +- Specific memory layouts (channels-last). +- ISA-specific implementation selection (`jit_avx2`, `jit_avx512`). +- Dynamic shape scenarios. +- Edge cases specific to the CPU implementation. + +**File to create:** `.cpp` + +**Pattern:** + +```cpp +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "shared_test_classes/base/ov_subgraph.hpp" +#include "utils/cpu_test_utils.hpp" +#include "utils/filter_cpu_info.hpp" +#include "openvino/op/.hpp" + +using namespace CPUTestUtils; + +namespace ov { +namespace test { + +using OpNameLayerCPUTestParamSet = std::tuple< + InputShape, // Input shape (supports dynamic) + ElementType, // Input element type + // Op-specific attributes + CPUSpecificParams // CPU layout + implementation check +>; + +class OpNameLayerCPUTest + : public testing::WithParamInterface, + virtual public ov::test::SubgraphBaseTest, + public CPUTestsBase { +public: + static std::string getTestCaseName( + const testing::TestParamInfo& obj) { + const auto& [shapes, inType, /* attrs, */ cpuParams] = obj.param; + std::ostringstream results; + results << "IS=" << ov::test::utils::partialShape2str({shapes.first}) << "_"; + results << "TS="; + for (const auto& item : shapes.second) { + results << ov::test::utils::vec2str(item) << "_"; + } + results << "Prc=" << inType << "_"; + results << CPUTestsBase::getTestCaseName(cpuParams); + return results.str(); + } + +protected: + void SetUp() override { + const auto& [shapes, _inType, /* attrs, */ cpuParams] = this->GetParam(); + inType = _inType; + std::tie(inFmts, outFmts, priority, selectedType) = cpuParams; + if (selectedType.empty()) { + selectedType = getPrimitiveType(); + } + selectedType = makeSelectedTypeStr(selectedType, inType); + targetDevice = ov::test::utils::DEVICE_CPU; + init_input_shapes({shapes}); + + ov::ParameterVector params; + for (auto&& shape : inputDynamicShapes) { + params.push_back( + std::make_shared(inType, shape)); + } + // Build the op graph + auto op = std::make_shared( + params[0] /*, attributes */); + function = create_ov_model(inType, params, op, "OpName"); + } +}; + +TEST_P(OpNameLayerCPUTest, CompareWithRefs) { + run(); + CheckPluginRelatedResults(compiledModel, "OpName"); +} + +namespace { + +// ═══════════════════════════════════════════════════════════════════ +// CPU-Specific Parameters +// ═══════════════════════════════════════════════════════════════════ +const auto cpuParams_nchw = CPUSpecificParams{{nchw}, {nchw}, {}, {}}; +const auto cpuParams_nhwc = CPUSpecificParams{{nhwc}, {nhwc}, {}, {}}; + +// ═══════════════════════════════════════════════════════════════════ +// Static Shape Tests +// ═══════════════════════════════════════════════════════════════════ +const std::vector staticShapes4D = { + {{}, {{1, 16, 8, 8}}}, + {{}, {{2, 32, 4, 4}}}, + {{}, {{1, 64, 1, 1}}}, +}; + +INSTANTIATE_TEST_SUITE_P(smoke_OpName_Static, + OpNameLayerCPUTest, + ::testing::Combine( + ::testing::ValuesIn(staticShapes4D), + ::testing::Values(ElementType::f32, ElementType::bf16), + // Op-specific attribute values + ::testing::Values(cpuParams_nchw, cpuParams_nhwc)), + OpNameLayerCPUTest::getTestCaseName); + +// ═══════════════════════════════════════════════════════════════════ +// Dynamic Shape Tests +// ═══════════════════════════════════════════════════════════════════ +const std::vector dynamicShapes4D = { + // {PartialShape, list of concrete shapes to test} + {{-1, -1, -1, -1}, {{1, 16, 8, 8}, {2, 32, 4, 4}, {1, 64, 1, 1}}}, + {{{1, 4}, {16, 64}, {1, 16}, {1, 16}}, {{1, 16, 8, 8}, {4, 64, 16, 16}}}, +}; + +INSTANTIATE_TEST_SUITE_P(smoke_OpName_Dynamic, + OpNameLayerCPUTest, + ::testing::Combine( + ::testing::ValuesIn(dynamicShapes4D), + ::testing::Values(ElementType::f32), + // Op-specific attribute values + ::testing::Values(cpuParams_nchw)), + OpNameLayerCPUTest::getTestCaseName); + +} // namespace +} // namespace test +} // namespace ov +``` + +### Eltwise-Routed Ops: Use Activation Test Infrastructure + +For ops routed through the existing `Eltwise` node (see **cpu_op_implementation** Fast Path), +**do not create a separate custom test file**. Instead, extend the existing activation test infrastructure: + +**`src/plugins/intel_cpu/tests/functional/custom/single_layer_tests/classes/activation.cpp`:** +1. Add `{ActivationTypes::OpName, {{}}}` to `activationTypes()` (or `activationTypesDynamicMath()` for ref-only ops). +2. Add `OpName` to `getPrimitiveType()` jit lists for ARM64 and riscv64. +3. Add edge-case injection in `ActivationLayerCPUTest::generate_inputs()` for domain-bounded ops. + +**`src/plugins/intel_cpu/tests/functional/shared_tests_instances/single_layer_tests/activation.cpp`:** +- The op is already covered by the parameterized `ActivationLayerTest` and `ActivationLayerCPUTest` once added to the `activationTypes()` map — no new `INSTANTIATE_TEST_SUITE_P` needed. + +### 3. Edge Cases to Cover + +| Category | Test Cases | +|----------|-----------| +| **Shapes** | Scalar input, 1-element tensor, very large tensor, zero-dimension (empty) | +| **Precisions** | `f32`, `bf16`, `f16`, `i8`, `u8`, `i32`, `i64` (as applicable) | +| **Layouts** | Planar (`ncsp`), channels-last (`nspc`) | +| **Dynamic shapes** | Fully dynamic (`{-1, -1, ...}`), partially dynamic, shape-changing across inferences | +| **Rank variation** | 1D, 2D, 3D, 4D, 5D (as supported by the op) | +| **Attribute values** | All valid attribute combinations, especially boundary values | +| **Numerical stability** | Very small values (denormals), very large values, NaN, Inf | + +### 4. Verifying ISA Implementation Selection + +Custom CPU tests can verify that the correct implementation type is selected: + +```cpp +TEST_P(OpNameLayerCPUTest, CompareWithRefs) { + run(); + // Verify the correct CPU implementation was chosen + CheckPluginRelatedResults(compiledModel, "OpName"); +} +``` + +The `CheckPluginRelatedResults` function validates that `selectedType` +(e.g., `jit_avx512_FP32`) matches the expected implementation based on the +current hardware. + +## Build and Run Tests + +### Build + +```bash +cd build + +# Build shared functional tests +cmake --build . --target ov_cpu_func_tests -j$(nproc) + +# Or build only the CPU plugin library +cmake --build . --target openvino_intel_cpu_plugin -j$(nproc) +``` + +### Run Shared Tests + +```bash +./bin/intel64/Release/ov_cpu_func_tests \ + --gtest_filter=*smoke*OpName* +``` + +### Run Custom CPU Tests + +```bash +./bin/intel64/Release/ov_cpu_func_tests \ + --gtest_filter=*OpNameLayerCPUTest* +``` + +### Run All Tests for the Op + +```bash +./bin/intel64/Release/ov_cpu_func_tests \ + --gtest_filter=*OpName* +``` + +### Run with Verbose Output + +```bash +./bin/intel64/Release/ov_cpu_func_tests \ + --gtest_filter=*OpName* \ + --gtest_print_time=1 +``` + +## Debug Capabilities + +The CPU plugin provides debug features documented in +`src/plugins/intel_cpu/docs/debug_capabilities/`: + +```bash +# Print execution graph with performance counters +export OV_CPU_DEBUG_LOG=1 + +# Dump node execution details +export OV_CPU_EXEC_GRAPH_INFO=1 +``` + +## Test Failure Troubleshooting + +| Symptom | Likely Cause | Fix | +|---------|-------------|-----| +| Test not found | Test not registered in CMake or wrong filter | Check test file is under correct directory | +| Accuracy mismatch | Incorrect precision handling | Verify data type dispatch and precision conversion | +| Segfault | Memory stride/offset calculation error | Check `getStrides()`, `getStaticDims()` usage | +| Shape mismatch | Incorrect shape inference | Verify `NgraphShapeInferFactory` or custom shape inference | +| Wrong impl selected | `initSupportedPrimitiveDescriptors` ordering | First matching descriptor is selected — order matters | +| Dynamic shape failure | Missing `executeDynamicImpl` | Ensure `executeDynamicImpl` delegates to `execute` | + +## Output + +- All shared single-layer tests pass. +- Custom CPU single-layer tests pass (static + dynamic shapes). +- Report `success` + list of files created to OV Orchestrator. + +### Files Created in This Step + +| File | Purpose | +|------|---------| +| `src/plugins/intel_cpu/tests/functional/shared_tests_instances/single_layer_tests/.cpp` | Shared test instantiation | +| `src/plugins/intel_cpu/tests/functional/custom/single_layer_tests/.cpp` | Custom CPU-specific tests | diff --git a/.github/agents-prototype/skills/add-fe-op/SKILL.md b/.github/agents-prototype/skills/add-fe-op/SKILL.md new file mode 100644 index 000000000000..b5532f1b732d --- /dev/null +++ b/.github/agents-prototype/skills/add-fe-op/SKILL.md @@ -0,0 +1,97 @@ +--- +name: add-fe-op +description: Adds a new operation to OpenVINO Frontend pipelines with translator updates, registration, and tests. +--- + +# Agent Skill: Add New Operation to OpenVINO Frontend + +## Goal +Enable a new operation in an OpenVINO Frontend (FE) pipeline — translator creation, registration, conversion validation, and tests. + +## Framework-Specific Workflows + +Each frontend has its own detailed workflow. Read the one matching the target framework: + +| Frontend | Skill file | What it covers | +|---|---|---| +| **PyTorch** | [pytorch.md](pytorch.md) | NodeContext API, `op_table.cpp` registration (TorchScript + FX/Export), `translate_1to1_match_*` wrappers, Python layer tests, Mark operations | +| **ONNX** | [onnx.md](onnx.md) | `ov::frontend::onnx::Node` API, `ONNX_OP` macro registration with `OPSET_SINCE`/`OPSET_RANGE`, `.prototxt` test models, C++ GTest cases, normalize-step transformations | + +## Related Skills (investigation & debugging) + +| Frontend | Skill file | When to use | +|---|---|---| +| ONNX | [conversion-issues/onnx.md](../conversion-issues/onnx.md) | Conversion failures, accuracy bugs, shape/type mismatches, opset version gaps | +| PyTorch | [conversion-issues/pytorch.md](../conversion-issues/pytorch.md) | Conversion failures, accuracy bugs, tracing mode issues, normalize-step failures | + +## Notes + +- Keep this skill instruction-only in markdown. + +--- + +## Instruction Workflow (TensorFlow and generic fallback) + +The following sections apply to frontends **not** covered by the framework-specific files above (e.g., TensorFlow), or as a general reference when no framework-specific skill is available. + +### 1) Check current support state +- Verify whether translator file already exists. +- Verify whether op is already registered in frontend mapping table. +- If both exist and conversion is known to pass, skip scaffolding. +- If only partial state exists, repair only missing parts. + +### 2) Add translator logic +- Preferred path: emit real OV conversion logic when the operation maps to an OpenVINO op or a set of operations that will provide reasonable performance. +- If the same operation already exists in another frontend, extract/reuse common translation logic instead of duplicating it. +- For simple 1:1 ops, prefer existing helper translation patterns/utilities already used in FE codebase. +- Fallback path: emit safe placeholder translator only when real mapping is unavailable. +- Never claim full support when fallback translator is used. + +### 3) Register operation in frontend tables +- PyTorch: + - Add converter declaration in op table. + - Add TorchScript key registration (for example `aten::op`). + - Add FX key registration (for example `aten.op.default`). +- TensorFlow: + - For supported unary ops, prefer generic unary registration path. + - For unsupported cases, register dedicated translator function. +- ONNX: + - Ensure `ONNX_OP` macro registration exists in translator file. + +### 4) Add tests +- Add frontend smoke test file under `tests/frontend` (not available for PyTorch). +- Add framework layer test under `tests/layer_tests` where supported. +- Ensure test naming follows existing suite conventions. + +### 5) Build prerequisites for validation +- Ensure frontend changes are available to runtime before conversion checks. +- Build frontend targets from source or use an existing OpenVINO build/package that already includes your FE changes. +- If neither is true, do not report conversion status as passed; mark validation as blocked. + +### 6) Validate conversion and finalize +- run conversion check from generated model. +- if automated validation is unavailable, mark as skipped with explicit reason. +- Confirm no duplicate registrations are introduced. +- Confirm rerun is idempotent. +- Report one of three states: fully enabled, partially repaired, or scaffolded with fallback. + +## Translation Recommendations + +- Prefer runtime shape computation over conversion-time shape extraction. + - Use `ShapeOf`-based graph logic for shape-dependent behavior. + - Avoid relying on `get_shape()` / fully static shape reads during FE conversion. + - `get_partial_shape()` is acceptable for compile-time rank-only decisions. + +- Handle data types according to framework semantics. + - If PyTorch op behavior allows mixed input types, preserve this in translation. + - Do not over-constrain translators to a single dtype when mixed-type execution is valid. + +- Avoid constants tied to compile-time element type queries. + - Do not create `Constant` values from `get_element_type()` when type can be dynamic at conversion time. + - Prefer runtime type-safe construction paths that remain valid for dynamic element types. + +## Notes + +- Keep this skill instruction-only in markdown. +- Prefer minimal, root-cause updates over broad refactors. +- Do not mark operation as supported unless FE conversion produces OV graph nodes (not framework fallback nodes). diff --git a/.github/agents-prototype/skills/add-fe-op/onnx.md b/.github/agents-prototype/skills/add-fe-op/onnx.md new file mode 100644 index 000000000000..e5159c1e7049 --- /dev/null +++ b/.github/agents-prototype/skills/add-fe-op/onnx.md @@ -0,0 +1,301 @@ +# Skill: Add a New Operation Translator to the OpenVINO ONNX Frontend + +--- + +## 1. Prerequisites + +Before writing any code: + +- [ ] Read the [ONNX spec for the op](https://onnx.ai/onnx/operators/) — inputs, outputs, attributes, type constraints, and behavioral notes. +- [ ] Check the [ONNX Operator Changelog](https://github.com/onnx/onnx/blob/main/docs/Changelog.md) for all opset versions that introduced or modified the op. +- [ ] Identify the matching OpenVINO op(s) in `src/core/include/openvino/op/`. If none exists, use a composition of existing ops or escalate to the Core OpSpec Agent. +- [ ] Search `src/frontends/common_translators/` — a shared translator may already exist or be partially implemented. +- [ ] Check if other frontends (PyTorch, TF) already have a translator for the same operation; it may be reusable or a model to follow. + +--- + +## 2. Key Source Locations + +| Path | Description | +|---|---| +| `src/frontends/onnx/frontend/src/op/` | Per-op translator files — create your new `.cpp` file here. `ONNX_OP` registration goes at the bottom of this file. | +| `src/frontends/onnx/frontend/src/core/operator_set.hpp` | `ONNX_OP` and `ONNX_OP_M` macros. | +| `src/frontends/onnx/frontend/src/version_range.hpp` | `OPSET_SINCE`, `OPSET_RANGE`, `OPSET_IN` helpers. | +| `src/frontends/onnx/frontend/src/core/node.hpp` | `ov::frontend::onnx::Node` — the context object passed to each translator. | +| `src/frontends/onnx/frontend/src/utils/common.hpp` | Shared helpers: `is_input_valid`, `handle_opset6_binary_op`. **Always check here.** | +| `src/frontends/onnx/frontend/CMakeLists.txt` | Add new `.cpp` file here under `SOURCES`. | +| `src/frontends/onnx/tests/onnx_import.in.cpp` | Main import/inference tests. Add test cases here. | +| `src/frontends/onnx/tests/models/` | `.prototxt` test model definitions. Add test models here. | + +--- + +## 3. `ov::frontend::onnx::Node` API Reference + +`Node` is the context object every translator receives. + +```cpp +#include "core/node.hpp" + +// Get all op inputs as OpenVINO outputs: +ov::OutputVector inputs = node.get_ov_inputs(); + +// Get a specific input: +auto x = node.get_ov_inputs()[0]; + +// Get an attribute with a default value: +int64_t axis = node.get_attribute_value("axis", 0); +std::vector pads = node.get_attribute_value>("pads", {0, 0}); +std::string auto_pad = node.get_attribute_value("auto_pad", "NOTSET"); + +// Check if an attribute exists: +bool has_pads = node.has_attribute("pads"); + +// Check if an optional input is present (not "missing"): +#include "utils/common.hpp" +bool valid = common::is_input_valid(node, 2); +``` + +--- + +## 4. Registration Macros + +The `ONNX_OP` macro is placed **at the bottom of the per-op `.cpp` file** (not in `ops_bridge.cpp`). Look at existing op files such as `src/frontends/onnx/frontend/src/op/sequence_insert.cpp` or `src/frontends/onnx/frontend/src/op/resize.cpp` as reference. + +**Example** (adapt op name and version range to your op): + +``` +// Single version range: +ONNX_OP("GridSample", OPSET_SINCE(16), ai_onnx::opset_16::grid_sample); + +// Multiple ranges when behavior changed between opsets: +ONNX_OP("Resize", OPSET_RANGE(10, 10), ai_onnx::opset_10::resize); +ONNX_OP("Resize", OPSET_RANGE(11, 17), ai_onnx::opset_11::resize); +ONNX_OP("Resize", OPSET_SINCE(18), ai_onnx::opset_18::resize); + +// Specific individual opset versions (when there are gaps): +ONNX_OP("Cast", OPSET_IN({1, 6, 9, 13, 19}), ai_onnx::opset_13::cast); +``` + +--- + +## 5. Step-by-Step Implementation + +### 5a. Create the translator file + +ONNX op translators consist of **a single `.cpp` file only** — no corresponding `.hpp` is needed. + +Read an existing op as your template. Good references: +- `src/frontends/onnx/frontend/src/op/sequence_insert.cpp` (simple, recent pattern) +- `src/frontends/onnx/frontend/src/op/resize.cpp` (multi-opset-version example) + +Adapt the chosen template to your op. Do not copy-paste blindly — understand each section before writing. + +### 5b. Handling multiple opset versions + +When an op was added in opset 9 and modified in opset 13: + +```cpp +// ---- opset_9 namespace ---- +namespace opset_9 { +ov::OutputVector my_op(const ov::frontend::onnx::Node& node) { + // opset 9 implementation +} +} // namespace opset_9 + +// ---- opset_13 namespace ---- +namespace opset_13 { +ov::OutputVector my_op(const ov::frontend::onnx::Node& node) { + // opset 13 implementation (new behavior/attributes) +} +} // namespace opset_13 +``` + +```cpp +ONNX_OP("MyOp", OPSET_RANGE(9, 12), ai_onnx::opset_9::my_op); +ONNX_OP("MyOp", OPSET_SINCE(13), ai_onnx::opset_13::my_op); + +### 5c. Handling simple binary/elementwise ops + +```cpp +#include "utils/common.hpp" + +ov::OutputVector add(const ov::frontend::onnx::Node& node) { + return common::handle_opset6_binary_op(node); +} +``` + +### 5d. Using shared helpers + +```cpp +#include "utils/common.hpp" + +// Check optional input before accessing it: +const auto inputs = node.get_ov_inputs(); +if (common::is_input_valid(node, 1)) { + auto bias = inputs[1]; + // ... use bias ... +} +``` + +### 5e. Checking and using `common_translators` + +```bash +grep -rn '' src/frontends/common_translators/include/common_translators.hpp +``` + +If found, use it directly: + +```cpp +#include "common_translators.hpp" + +ov::OutputVector my_op(const ov::frontend::onnx::Node& node) { + return common_translators::translate_(node.get_ov_inputs(), attributes...); +} +``` + +### 5f. Mark operations for special types + +For ops that process complex or quantized tensors: + +```cpp +#include "openvino/frontend/complex_type_mark.hpp" + +// In normalize-step MatcherPass (if needed): +auto complex_mark = std::make_shared(input, input_et); +``` + +### 5g. Normalize-step transformations + +For ops that require post-translation graph rewrites (e.g., resolving Mark operations or merging multi-op patterns): + +1. Create a `MatcherPass` in `src/frontends/onnx/frontend/src/transforms/` (or `src/frontends/common_translators/` for shared transforms). +2. Register in `FrontEnd::normalize()` in `frontend.cpp`. + +### 5h. Update `CMakeLists.txt` + +In `src/frontends/onnx/frontend/CMakeLists.txt`: +```cmake +set(SOURCES + ... + src/op/.cpp + ... +) +``` + +## 6. Adding Tests + +### 6a. Create a `.prototxt` test model + +In `src/frontends/onnx/tests/models/.prototxt`: + +```protobuf +ir_version: 7 +graph { + node { + op_type: "GridSample" + attribute { + name: "mode" + s: "bilinear" + type: STRING + } + input: "X" + input: "grid" + output: "Y" + } + name: "test_grid_sample" + input { + name: "X" + type { tensor_type { elem_type: 1 shape { dim { dim_value: 1 } ... }}} + } + output { + name: "Y" + type { tensor_type { elem_type: 1 shape { ... }}} + } +} +opset_import { version: 16 } +``` + +### 6b. Add C++ test cases + +In `src/frontends/onnx/tests/onnx_import.in.cpp`: + +```cpp +NGRAPH_TEST(${BACKEND_NAME}, onnx_model_) { + const auto function = onnx_import::import_onnx_model( + file_util::path_join(CommonTestUtils::getExecutionDirectory(), + SERIALIZED_ZOO, "onnx/.prototxt")); + + auto test_case = test::TestCase(function, s_device); + test_case.add_input({...}); // input values + test_case.add_expected_output(Shape{...}, {...}); // expected output + test_case.run(); +} +``` + +### 6c. Add multiple test scenarios + +Cover all important cases: +- Basic case with default attributes +- Non-default attribute values +- Edge cases (empty tensor, scalar, dynamic rank) +- Multiple opset versions if behavior differs + +--- + +## 7. Build and Test + +```bash +# Build the ONNX frontend: +cmake --build build --target openvino_onnx_frontend -j$(nproc) + +# Build and run tests: +cmake --build build --target ov_onnx_frontend_tests -j$(nproc) +cd build && ctest -R "ov_onnx_frontend_tests" --output-on-failure -j$(nproc) + +# Run a specific test: +ctest -R "onnx_model_" -V + +# Apply clang-format: +cmake --build build --target clang_format_fix_all -j$(nproc) +``` + +--- + +## 8. Validation Checklist + +- [ ] **ONNX spec consulted** — all attributes, input/output optionality, type constraints covered. +- [ ] **All opset versions covered** — `ONNX_OP` registrations span the correct version ranges. +- [ ] **Optional inputs guarded** — `common::is_input_valid(node, N)` used for all optional inputs. +- [ ] **Default attribute values** — per ONNX spec for each opset version. +- [ ] **CMakeLists.txt updated** — new `.cpp` added to `SOURCES`. +- [ ] **`ONNX_OP` registration added** — macro at bottom of the new `.cpp` translator file (not in `ops_bridge.cpp`). +- [ ] **Test model added** — `.prototxt` covers basic case and edge cases. +- [ ] **C++ test added** — at least one test in `onnx_import.in.cpp`. +- [ ] **All ONNX tests pass** — `ctest -R ov_onnx_frontend_tests` green. +- [ ] **clang-format applied** — no formatting diffs. +- [ ] **Supported ops doc updated** — `src/frontends/onnx/docs/supported_ops.md` (if maintained). + +--- + +## 9. Common Pitfalls + +| Pitfall | Explanation | +|---|---| +| **Wrong opset range** | `OPSET_SINCE(11)` instead of `OPSET_RANGE(11, 17)` when the op changed at opset 18. | +| **Attribute type mismatch** | `get_attribute_value()` vs `get_attribute_value()` — wrong type silently returns a default value. | +| **Not checking common_translators** | Many ops are already in `src/frontends/common_translators/`. Check before writing a new one. | +| **Forgetting `is_input_valid()`** | Accessing `inputs[2]` when the input is optional and absent causes a crash. | +| **Missing `#include`** | Each OpenVINO op class needs its header (e.g., `#include "openvino/op/add.hpp"`). | +| **Broadcasting edge cases** | ONNX uses Numpy-style broadcasting; verify with scalar and 0-rank tensor inputs. | +| **Not handling empty attribute list** | Some attributes may have list type with empty default; `get_attribute_value>("axis", {})`. | +| **Mark operations missing** | For complex/quantized/sequence types, the normalize-step won't resolve correctly without the corresponding Mark operation. | + +--- + +## 10. Reference Links + +- [ONNX Operator Specifications](https://onnx.ai/onnx/operators/) +- [ONNX Operator Changelog](https://github.com/onnx/onnx/blob/main/docs/Changelog.md) +- [OpenVINO Available Operations](https://docs.openvino.ai/latest/openvino_docs_ops_opset.html) +- [OpenVINO ONNX Frontend README](src/frontends/onnx/README.md) +- [OpenVINO ONNX Supported Ops](src/frontends/onnx/docs/supported_ops.md) diff --git a/.github/agents-prototype/skills/add-fe-op/pytorch.md b/.github/agents-prototype/skills/add-fe-op/pytorch.md new file mode 100644 index 000000000000..a60068c35e00 --- /dev/null +++ b/.github/agents-prototype/skills/add-fe-op/pytorch.md @@ -0,0 +1,337 @@ +# Skill: Add a New Operation Translator to the OpenVINO PyTorch Frontend + +--- + +## 1. Prerequisites + +Before writing any code: + +- [ ] Read the [PyTorch documentation](https://pytorch.org/docs/stable/) for the op — function signature, parameter types, and behavioral notes. +- [ ] Check the [PyTorch ATen operator schema](https://github.com/pytorch/pytorch/blob/main/aten/src/ATen/native/native_functions.yaml) for all overloads. +- [ ] Identify whether the op needs to be supported in **TorchScript** (`aten::op`), **torch.export/FX** (`aten.op.overload`), or both. +- [ ] Identify the matching OpenVINO op(s) in `src/core/include/openvino/op/`. If none exists, escalate to the Core OpSpec Agent. +- [ ] Search `src/frontends/common_translators/` — a shared implementation may already exist. +- [ ] Check if the op can be expressed using existing `translate_1to1_match_*` wrappers. + +--- + +## 2. Key Source Locations + +| Path | Description | +|---|---| +| `src/frontends/pytorch/src/op/` | Per-op translator files — create your new file here. | +| `src/frontends/pytorch/src/op_table.cpp` | Operator registry — `get_supported_ops_ts()` and `get_supported_ops_fx()`. Add registration here. | +| `src/frontends/pytorch/include/openvino/frontend/pytorch/node_context.hpp` | `NodeContext` — the context object each translator receives. | +| `src/frontends/pytorch/src/utils.hpp` | Shared helpers: `num_inputs_check`, `make_optional_bias`, `get_shape_rank`, `normalize_axis`, `numel`, `convert_dtype`, `concat_list_construct`. **Always check here first.** | +| `src/frontends/pytorch/src/pt_framework_node.hpp` | `PtFrameworkNode` — placeholder for unconverted ops. | +| `src/frontends/pytorch/src/transforms/` | Normalize-step transformations — `MatcherPass` subclasses that run after initial translation. | +| `src/frontends/pytorch/CMakeLists.txt` | Add new `.cpp` files here. | +| `tests/layer_tests/pytorch_tests/` | Python layer tests — create `test_.py` here. | +| `tests/layer_tests/pytorch_tests/pytorch_layer_test_class.py` | `PytorchLayerTest` base class. | + +--- + +## 3. `NodeContext` API Reference + +`NodeContext` is the context object every translator receives. + +```cpp +#include "openvino/frontend/pytorch/node_context.hpp" + +// Get an input by index: +ov::Output x = context.get_input(0); + +// Get a constant input (already a tensor, reads the value): +int64_t dim = context.const_input(1); +double alpha = context.const_input(2); +std::vector shape = context.const_input>(3); + +// Check if an optional input is None: +bool is_none = context.input_is_none(2); + +// Mark a created node (MANDATORY for every created OV node): +auto add = context.mark_node(std::make_shared(x, y)); + +// Get the number of inputs: +size_t n = context.get_input_size(); +``` + +--- + +## 4. Registration in `op_table.cpp` + +### Define converter function + +```cpp +// In the OP_CONVERTER macro (or standalone function): +OP_CONVERTER(translate_) { + // context is NodeContext& + num_inputs_check(context, 1, 3); + const auto x = context.get_input(0); + // ... + return {context.mark_node(std::make_shared(x))}; +} +``` + +### Register in `get_supported_ops_ts()` (TorchScript) + +```cpp +{"aten::", ov::frontend::pytorch::op::translate_}, +``` + +### Register in `get_supported_ops_fx()` (torch.export / FX) + +```cpp +{"aten..default", ov::frontend::pytorch::op::translate_}, +{"aten..Tensor", ov::frontend::pytorch::op::translate_}, +``` + +### 1:1 mapping wrappers (no custom logic needed) + +```cpp +// Single input → single output, same type: +{"aten::relu", op::translate_1to1_match_1_inputs}, +{"aten.relu.default", op::translate_1to1_match_1_inputs}, + +// Two inputs, type-aligned: +{"aten::add.Tensor", op::translate_1to1_match_2_inputs_align_types}, + +// Inplace variant: +{"aten::relu_", op::inplace_op>}, +``` + +--- + +## 5. Step-by-Step Implementation + +### 5a. Create the translator file + +Use the template below as your starting point, then read the matching example to adapt it +to your op's specific inputs and attribute handling: + +```cpp +// src/frontends/pytorch/src/op/.cpp +// Starting template — adapt inputs, attribute reads, and OV op to your case. + +#include "openvino/frontend/pytorch/node_context.hpp" +#include "openvino/op/.hpp" +#include "utils.hpp" + +namespace ov::frontend::pytorch::op { + +OutputVector translate_(const NodeContext& context) { + // Minimum 1 input, maximum 3 — adjust for your op: + num_inputs_check(context, 1, 3); + + const auto x = context.get_input(0); + + // Read constant scalar attribute (if needed): + // const auto alpha = context.const_input(1); + + // Read constant integer attribute (if needed): + // const auto dim = context.const_input(2); + + // Guard optional input (if needed): + // if (!context.input_is_none(1)) { ... } + + auto result = std::make_shared(x /*, other inputs */); + return {context.mark_node(result)}; +} + +} // namespace ov::frontend::pytorch::op +``` + +> `context.mark_node(...)` is **mandatory** on every created OV node — do not omit it. + +Real implementations to read before adapting your code: +- `src/frontends/pytorch/src/op/upsample.cpp` — optional inputs, resize semantics +- `src/frontends/pytorch/src/op/layer_norm.cpp` — shared helpers (`normalize_axis`, `make_optional_bias`) +- `src/frontends/pytorch/src/op/linear.cpp` — attribute translation, bias handling + +After creating the file, declare the translator in `src/frontends/pytorch/src/op/op.hpp`. + +### 5b. 1:1 mapping (no custom translator needed) + +When the PyTorch op maps directly to a single OV op with the same input/output arity and no attribute translation: + +```cpp +// In op_table.cpp, no new .cpp file needed: +{"aten::abs", op::translate_1to1_match_1_inputs}, +{"aten.abs.default", op::translate_1to1_match_1_inputs}, +``` + +### 5c. Multi-input with type alignment + +```cpp +OutputVector translate_add(const NodeContext& context) { + num_inputs_check(context, 2, 3); + auto x = context.get_input(0); + auto y = context.get_input(1); + align_eltwise_input_types(context, x, y, false, false); + return {context.mark_node(std::make_shared(x, y))}; +} +``` + +### 5d. Using shared helpers + +```cpp +#include "utils.hpp" + +OutputVector translate_layer_norm(const NodeContext& context) { + num_inputs_check(context, 2, 3); + auto x = context.get_input(0); + auto axes = context.const_input>(1); + + // Utility: normalize axis values to positive range + const auto rank = get_shape_rank(context, x); + axes = normalize_axis(axes, rank); + + // Utility: make optional bias (returns Constant(0) if input is None) + auto bias = make_optional_bias(context, x, 2); + + return { /* ... */ }; +} +``` + +### 5e. Normalize-step transformations + +For ops that need graph-level rewriting after translation: + +```cpp +// In src/frontends/pytorch/src/transforms/.cpp: +class MyTransform : public ov::pass::MatcherPass { +public: + MyTransform() { + auto pattern = ov::pass::pattern::wrap_type(); + auto callback = [](ov::pass::pattern::Matcher& m) -> bool { + // Pattern match + transform logic + return true; + }; + auto m = std::make_shared(pattern, "MyTransform"); + register_matcher(m, callback); + } +}; + +// Register in src/frontends/pytorch/src/frontend.cpp, in FrontEnd::normalize(): +manager.register_pass(); +``` + +### 5f. Update `CMakeLists.txt` + +In `src/frontends/pytorch/CMakeLists.txt`: +```cmake +set(SOURCES + ... + src/op/.cpp + ... +) +``` + +--- + +## 6. Adding Tests + +### 6a. Create a Python layer test + +In `tests/layer_tests/pytorch_tests/test_.py`: + +```python +# Copyright (C) 2018-2026 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +import pytest +import torch +import numpy as np +from pytorch_layer_test_class import PytorchLayerTest + + +class TestMyOp(PytorchLayerTest): + def _prepare_input(self): + return (np.random.randn(1, 3, 224, 224).astype(np.float32),) + + def create_model(self, dim, keepdim): + class MyModel(torch.nn.Module): + def __init__(self, dim, keepdim): + super().__init__() + self.dim = dim + self.keepdim = keepdim + + def forward(self, x): + return torch.my_op(x, dim=self.dim, keepdim=self.keepdim) + + return MyModel(dim, keepdim), None, "my_op" + + @pytest.mark.nightly + @pytest.mark.precommit + @pytest.mark.parametrize("dim", [0, 1, -1]) + @pytest.mark.parametrize("keepdim", [True, False]) + def test_my_op(self, dim, keepdim, ie_device, precision, ir_version): + self._test(*self.create_model(dim, keepdim), ie_device, precision, ir_version) + + @pytest.mark.nightly + @pytest.mark.precommit_torch_export + @pytest.mark.parametrize("dim", [0, 1]) + def test_my_op_torch_export(self, dim, ie_device, precision, ir_version): + self._test(*self.create_model(dim, True), ie_device, precision, ir_version, + trace_model=True) +``` + +--- + +## 7. Build and Test + +```bash +# Build the PyTorch frontend: +cmake --build build --target openvino_pytorch_frontend -j$(nproc) + +# Run layer tests (TorchScript mode): +TEST_DEVICE=CPU TEST_PRECISION=FP32 python3 -m pytest tests/layer_tests/pytorch_tests/test_.py -v -k "precommit" + +# Run with torch.export mode: +TEST_DEVICE=CPU TEST_PRECISION=FP32 PYTORCH_TRACING_MODE=EXPORT python3 -m pytest tests/layer_tests/pytorch_tests/test_.py -v -k "precommit_torch_export" + +# Apply clang-format: +cmake --build build --target clang_format_fix_all -j$(nproc) +``` + +--- + +## 8. Validation Checklist + +- [ ] **PyTorch schema consulted** — all inputs (including optional), constants, and attributes covered. +- [ ] **TorchScript mapping added** — registered in `get_supported_ops_ts()`. +- [ ] **FX/Export mapping added** — registered in `get_supported_ops_fx()` for all overload variants. +- [ ] **Inplace variant handled** — registered with `inplace_op<>` if applicable. +- [ ] **`mark_node()` called** — on every `std::make_shared<...>()` in the translator. +- [ ] **Optional inputs checked** — `input_is_none()` guard before each optional access. +- [ ] **`num_inputs_check()` called** — validates expected input count. +- [ ] **CMakeLists.txt updated** — new `.cpp` file added. +- [ ] **Layer test added** — `test_.py` with `@pytest.mark.precommit` and `@pytest.mark.precommit_torch_export`. +- [ ] **Both tracing modes pass** — TorchScript and Export. +- [ ] **clang-format applied** — no formatting diffs. + +--- + +## 9. Common Pitfalls + +| Pitfall | Explanation | +|---|---| +| **Forgetting `mark_node()`** | Every `std::make_shared<...>()` must be wrapped with `context.mark_node()`. | +| **Not checking `input_is_none()`** | Optional inputs (e.g., bias, weight) may be None. Always guard before access. | +| **TorchScript vs FX name mismatch** | `aten::op_name` vs `aten.op_name.overload` — missing FX mapping causes silent `PtFrameworkNode` under `torch.export`. | +| **Missing inplace wrapper** | `aten::relu_` must use `inplace_op>`, not a separate translator. | +| **Wrong `const_input()` type** | Using `const_input()` for a float attribute silently returns a wrong value. | +| **Not using `num_inputs_check()`** | Missing this guard leads to hard-to-debug out-of-bounds crashes when the model has fewer inputs. | +| **Not checking `common_translators`** | Check `src/frontends/common_translators/` before writing a new translator. | +| **Dynamic shape assumptions** | Translators must not assume static dimensions. Use `get_shape_rank()` and dynamic-safe OV ops. | +| **Missing pytest marks** | `@pytest.mark.precommit` and `@pytest.mark.precommit_torch_export` are required for CI gating. | + +--- + +## 10. Reference Links + +- [PyTorch Operator Documentation](https://pytorch.org/docs/stable/torch.html) +- [PyTorch ATen native_functions.yaml](https://github.com/pytorch/pytorch/blob/main/aten/src/ATen/native/native_functions.yaml) +- [torch.export Reference](https://pytorch.org/docs/stable/export.html) +- [OpenVINO Available Operations](https://docs.openvino.ai/latest/openvino_docs_ops_opset.html) +- [OpenVINO PyTorch Frontend README](src/frontends/pytorch/README.md) diff --git a/.github/agents-prototype/skills/add-fe-op/step1-analysis.md b/.github/agents-prototype/skills/add-fe-op/step1-analysis.md new file mode 100644 index 000000000000..2eee7cf8510a --- /dev/null +++ b/.github/agents-prototype/skills/add-fe-op/step1-analysis.md @@ -0,0 +1,134 @@ +# Skill: FE Op Analysis + +> Source: `skills/add-fe-op/SKILL.md` (Step 1) +> Agent: `fe_agent` + +## When to Use + +- Model conversion fails with a missing translator or unsupported op error from a FE. +- An operation is absent from the FE op mapping table. +- Partial support exists (translator file present but registration missing, or vice versa). + +## Procedure + +### 1. Identify the operation and source framework + +From the error context / conversion log, extract: + +- Exact op name: + - PyTorch: `aten::op_name` (TorchScript) or `aten.op_name.default` (FX) + - TensorFlow: `tf.nn.op_name` or raw op string (e.g. `GatherNd`) + - ONNX: op string (e.g. `ScatterND`, versioned by opset) +- Source framework: `pytorch`, `tensorflow`, or `onnx` +- Version/variant where applicable (ONNX opset, TorchScript vs FX namespace) + +### 2. Check current support state + +Browse or clone `https://github.com/openvinotoolkit/openvino` and run: + +**PyTorch:** +```bash +# Translator file +ls src/frontends/pytorch/src/op/.cpp + +# TorchScript key +grep -n 'aten::' src/frontends/pytorch/src/op_table.cpp + +# FX key +grep -n 'aten\.' src/frontends/pytorch/src/op_table.cpp +``` + +**TensorFlow:** +```bash +ls src/frontends/tensorflow/src/op/.cpp +grep -n '' src/frontends/tensorflow/src/op_table.cpp +``` + +**ONNX:** +```bash +ls src/frontends/onnx/src/ops/.cpp +grep -rn 'ONNX_OP.*""' src/frontends/onnx/src/ops/ +``` + +Support states: + +| State | Meaning | +|-------|---------| +| `full` | Translator file + registration both present → skip scaffolding | +| `partial` | One piece missing → repair only that part | +| `missing` | Nothing exists → full scaffold needed | + +### 3. Research the op semantics + +Consult official documentation for: + +- Math formula / semantics +- Inputs, outputs, attributes +- Data types and broadcasting rules +- Edge cases (empty tensors, negative indices, etc.) + +| Framework | Reference | +|-----------|-----------| +| PyTorch | https://pytorch.org/docs/stable/generated/torch..html | +| TensorFlow | https://www.tensorflow.org/api_docs/python/tf/ | +| ONNX | https://onnx.ai/onnx/operators/ | + +### 4. Check for equivalent OV core op + +Search existing OV ops: + +```bash +grep -r '\|' \ + openvino/src/core/include/openvino/op/ \ + openvino/src/core/include/openvino/opsets/ +``` + +Decision table: + +| Situation | Action | +|-----------|--------| +| 1:1 match with existing OV op | Real translation — proceed | +| Composition of existing OV ops produces correct semantics | Real translation — proceed | +| No match; decomposing is too slow or inaccurate | **Escalate to Core Agent** | +| Operation is clearly decomposable via graph transform | Decomposable — flag for Transformation Agent | + +### 5. Check cross-frontend reuse + +If the same op exists in another frontend, reuse its logic rather than duplicating: + +```bash +grep -rn '' src/frontends/ --include="*.cpp" +``` + +If a working implementation found in another FE, extract shared translation +logic into a common utility header instead of copy-pasting. + +## Output + +Return a structured analysis block: + +``` +op_name: +source_framework: +support_state: +translator_file: +registration: + torchscript_key: # PyTorch only + fx_key: # PyTorch only + onnx_op_macro: # ONNX only + tf_table_entry: # TF only +math_formula: +inputs: [, ...] +outputs: [, ...] +attributes: [, ...] +ov_equivalent: +cross_fe_reuse: +action: +reason: +``` + +### Action routing + +- `action=translate` or `action=repair` → proceed to **fe_op_translation** skill. +- `action=escalate_to_core` → fill the full escalation payload (see `fe_agent.md`) + and stop. Do not proceed to Translation skill. diff --git a/.github/agents-prototype/skills/add-fe-op/step2-translation.md b/.github/agents-prototype/skills/add-fe-op/step2-translation.md new file mode 100644 index 000000000000..31425bab117c --- /dev/null +++ b/.github/agents-prototype/skills/add-fe-op/step2-translation.md @@ -0,0 +1,176 @@ +# Skill: FE Op Translation + +> Source: `skills/add-fe-op/SKILL.md` (Step 2 + Translation Recommendations) +> Agent: `fe_agent` + +## Prerequisites + +- Completed **fe_op_analysis** with `action=translate` or `action=repair`. +- Op name, source framework, support state, inputs/outputs/attributes are known. +- OV equivalent op (or a composition path) identified. + +--- + +## Translation paths by frontend + +### PyTorch + +Translator file: `src/frontends/pytorch/src/op/.cpp` + +```cpp +// Minimal PyTorch FE translator template +#include "openvino/frontend/pytorch/node_context.hpp" +#include "openvino/op/.hpp" +#include "utils.hpp" + +namespace ov { +namespace frontend { +namespace pytorch { +namespace op { + +using namespace ov::op; + +OutputVector translate_(const NodeContext& context) { + // Validate input count + num_inputs_check(context, , context.get_input_size()); + + // Fetch inputs + auto x = context.get_input(0); + // auto attr = context.const_input(1); // for attribute-valued inputs + + // Build OV subgraph + auto result = std::make_shared(x /*, attrs */); + return {result}; +} + +} // namespace op +} // namespace pytorch +} // namespace frontend +} // namespace ov +``` + +### TensorFlow + +Translator file: `src/frontends/tensorflow/src/op/.cpp` + +Preferred patterns: + +- **Unary elementwise**: use the generic unary registration path (see `unary_op.cpp`). +- **Complex**: write a dedicated translator function. + +```cpp +#include "common_op_table.hpp" +#include "openvino/op/.hpp" + +namespace ov { +namespace frontend { +namespace tensorflow { +namespace op { + +OutputVector translate_(const NodeContext& context) { + default_op_checks(context, 1, {""}); + auto x = context.get_input(0); + auto result = std::make_shared(x); + set_node_name(context.get_name(), result); + return {result->output(0)}; +} + +} // namespace op +} // namespace tensorflow +} // namespace frontend +} // namespace ov +``` + +### ONNX + +Translator file: `src/frontends/onnx/src/ops/.cpp` + +```cpp +#include "core/null_node.hpp" +#include "openvino/frontend/onnx/extension/conversion.hpp" +#include "openvino/op/.hpp" + +namespace ngraph { +namespace onnx_import { +namespace op { +namespace set_ { + +ov::OutputVector (const ov::frontend::onnx::Node& node) { + const auto inputs = node.get_ov_inputs(); + auto x = inputs.at(0); + // auto attr_val = node.get_attribute_value("attr_name", default_val); + auto result = std::make_shared(x); + return {result}; +} + +} // namespace set_ +} // namespace op +} // namespace onnx_import +} // namespace ngraph +``` + +--- + +## Translation Recommendations + +### Shape handling + +- **Prefer runtime ShapeOf-based graph logic** for shape-dependent behavior. +- Avoid `get_shape()` (fully static read at conversion time). +- `get_partial_shape()` is acceptable for compile-time rank-only decisions. + +```cpp +// Preferred: runtime shape computation +auto shape_node = std::make_shared(x, element::i64); +// Avoid: +// auto static_shape = x.get_shape(); // will fail for dynamic input +``` + +### Data type handling + +- If the framework op allows mixed input types, preserve this in translation — do + not over-constrain to a single dtype. +- Do not create `Constant` values from `get_element_type()` when the element type + can be dynamic at conversion time. + +```cpp +// Preferred: type-safe runtime path +// Avoid: +// auto type_const = Constant::create(element::i64, {1}, {x.get_element_type()}); +``` + +### Reuse and simplification + +- For simple 1:1 ops, check `utils.hpp` and `utils_quantize.hpp` for helpers + before writing new code. +- If the same op already has a translator in another frontend (detected in + `fe_op_analysis`), extract the shared logic into a common utility header + rather than duplicating. + +--- + +## Fallback Translator + +When a real OV mapping is unavailable: + +```cpp +OutputVector translate__stub(const NodeContext& context) { + // FALLBACK: no OV op mapping — op is not yet supported. + // The FE Agent will escalate to Core Agent for a new core op. + OPENVINO_THROW("No FE translation for op: "); +} +``` + +A fallback stub: +- Keeps the model from crashing on unrelated unsupported ops. +- Does **not** count as support. +- Must be reported as `partial`, never `success`. +- Always triggers the FE→Core escalation path. + +--- + +## Output + +- Modified or created translator `.cpp` file (ready for commit/patch). +- Notes for the Registration skill: which keys / macros need adding. +- Translation complexity label: `real_translation` or `fallback_stub`. diff --git a/.github/agents-prototype/skills/add-fe-op/step3-registration.md b/.github/agents-prototype/skills/add-fe-op/step3-registration.md new file mode 100644 index 000000000000..c065d8fb40b2 --- /dev/null +++ b/.github/agents-prototype/skills/add-fe-op/step3-registration.md @@ -0,0 +1,31 @@ +# Skill: FE Op Registration — Verification Checklist + +> For registration instructions see the per-frontend skill files: +> - **PyTorch**: [pytorch.md](pytorch.md) §4 and §5 (op_table.cpp, TorchScript key, FX key) +> - **ONNX**: [onnx.md](onnx.md) §4 (ONNX_OP macro in per-op `.cpp`) +> - **TensorFlow / generic**: [SKILL.md](SKILL.md) §3 + +Use this checklist to confirm registration is complete before proceeding to testing. + +--- + +## Verification Checklist + +**PyTorch:** +- [ ] `OP_CONVERTER(translate_)` declaration present in `op_table.cpp` +- [ ] TorchScript key `aten::` registered +- [ ] FX key `aten..default` registered +- [ ] All overload variants covered (`aten..Tensor`, `aten..Scalar`, …) +- [ ] `CMakeLists.txt` updated to include new `.cpp` + +**TensorFlow:** +- [ ] `op_table.cpp` entry present (unary path or dedicated) +- [ ] `CMakeLists.txt` updated + +**ONNX:** +- [ ] `ONNX_OP` macro present in translator `.cpp` file (not in `ops_bridge.cpp`) +- [ ] Opset range is accurate (cross-check with ONNX spec) +- [ ] `CMakeLists.txt` updated + +**All frontends:** +- [ ] No duplicate registrations introduced diff --git a/.github/agents-prototype/skills/add-fe-op/step4-testing.md b/.github/agents-prototype/skills/add-fe-op/step4-testing.md new file mode 100644 index 000000000000..e04d45c391f6 --- /dev/null +++ b/.github/agents-prototype/skills/add-fe-op/step4-testing.md @@ -0,0 +1,74 @@ +# Skill: FE Op Testing + +> Source: `skills/add-fe-op/SKILL.md` (Steps 4, 5, 6) + +## Prerequisites + +- Completed **fe_op_registration** — translator file and registration entries are in place. + +--- + +## Writing Tests + +Follow the test patterns described in the per-frontend skill files: +- **PyTorch**: [pytorch.md](pytorch.md) §5 — `PytorchLayerTest` class structure, TorchScript and FX paths, `@pytest.mark.nightly` / `@pytest.mark.precommit` +- **ONNX**: [onnx.md](onnx.md) §6 — `.prototxt` test model and C++ GTest pattern +- **TensorFlow / generic**: follow naming conventions in `tests/layer_tests/tensorflow_tests/` + +Look at tests for a similar existing op in the same directory as the starting point. + +--- + +## Conversion Validation + +After writing translator and registration, run the end-to-end conversion check using the +**`verify-conversion`** skill: + +> Read and follow: `skills/verify-conversion/SKILL.md` + +The skill auto-detects the conversion path (optimum-intel / ovc / convert_model) and +runs a numerical sanity check. Report the outcome (`validation=pass|fail|blocked`) back +to the FE Agent before generating the patch. + +--- + +## Git Patch Generation + +After translator, registration, and test files are written, generate a patch file. +Run: + +``` +python .github/scripts/meat/generate_patch.py --component fe_ --op +``` + +Place the resulting `.patch` file in `agent-results/frontend/patches/`. + +--- + +## Save Patch and Report + +Write the patch path and validation status to the FE Agent output file: + +``` +python .github/scripts/meat/record_result.py --agent frontend --op --patch --validation +``` + +--- + +## Reporting States + +| State | Meaning | +|-------|---------| +| `success` | Real translation in place, conversion produces OV graph nodes, tests written | +| `partial` | Fallback stub in place — model does not crash, but no real OV graph yet | +| `blocked` | Validation could not run (installed OV does not contain FE patch) | +| `failed` | Translator or test produced errors | + +--- + +## Output + +- Test files created and ready for commit. +- Git patch file: `patches/fe__.patch` +- Patch available in `agent-results/pytorch-fe/patches/`. +- Validation status reported back to FE Agent. diff --git a/.github/agents-prototype/skills/add-fusion-transformation/SKILL.md b/.github/agents-prototype/skills/add-fusion-transformation/SKILL.md new file mode 100644 index 000000000000..e49e5b0548e6 --- /dev/null +++ b/.github/agents-prototype/skills/add-fusion-transformation/SKILL.md @@ -0,0 +1,141 @@ +--- +name: add-fusion-transformation +description: Adds a new OpenVINO fusion transformation (subgraph to one or several operations) and corresponding tests. +--- + +# Agent Skill: Add New Fusion Transformation in OpenVINO + +## Goal +Provide an instruction-only workflow to implement a new OpenVINO graph fusion transformation that replaces a matched subgraph with one operation (or a compact operation sequence), including regression tests. + +Expected outcome: + +- New transformation pass implementation +- Pass registration in the appropriate pipeline +- Positive and negative transformation tests +- Functional coverage when behavior is user-visible + +--- + +## Scope + +This workflow applies to OpenVINO transformation development in `openvino/` for: + +- **Fusion transformations** — pattern-based graph fusion that replaces a matched subgraph with: + - a single fused op, or + - several ops preserving semantics with lower runtime overhead + - _Purpose: optimization (reduced dispatch overhead, better hardware mapping)_ + +- **Decomposition transformations** — pattern-based replacement that expands a complex op into a + sequence of simpler, already-supported ops: + - _Purpose: **enablement prerequisite** — makes an unsupported op runnable on all backends + without adding a new core op. Use when the semantics can be faithfully expressed via existing ops._ + - Decomposition is the first thing to evaluate when a new op is needed. If the op is + decomposable, no new core op class is necessary. + +The workflow covers: + +1. Pattern design and safety guards +2. Pass implementation and registration +3. Runtime correctness constraints (shape/type/friendly name/rt_info) +4. Unit and functional regression tests + +--- + +## Architecture + +Model Graph → Pattern Matcher Pass → Subgraph Replacement → Validation/Inference Tests + +--- + +## Expected Repository Layout + +openvino/ +├─ src/common/transformations/ +│ ├─ include/transformations/ +│ │ └─ /.hpp +│ ├─ src/transformations/ +│ │ └─ /.cpp +│ ├─ src/transformations/common_optimizations/ +│ │ └─ common_optimizations.cpp (or relevant pipeline file) +│ └─ tests/ +│ ├─ common_optimizations/ +│ │ └─ .cpp +│ └─ utils/ +└─ tests/ + └─ layer_tests/ or functional/ (if user-visible behavior/perf contract is affected) + +--- + +## Instruction Workflow + +### 1) Confirm the fusion does not already exist +- Search for equivalent matcher patterns and passes. +- If similar fusion exists, extend it instead of creating a duplicate pass. +- Reuse existing helper utilities and pattern predicates where possible. + +### 2) Define fusion contract before coding +- Specify exact matched subgraph topology and constraints. +- Specify replacement graph (single op or multi-op compact form). +- Document semantic invariants: + - output values + - element types + - ranks/dynamic-shape compatibility + - runtime info / friendly names +- Identify explicit no-fuse conditions (negative guards). + +### 3) Implement matcher pass +- Add pass class declaration in header and implementation in source. +- Prefer `ov::pass::MatcherPass` for local rewrite; use `GraphRewrite` only when grouping multiple related matchers. +- Make predicates strict enough to avoid unsafe rewrites. +- Preserve node names and `rt_info` where expected. +- Register and copy runtime info for replacement nodes. +- Avoid hidden behavior changes; if assumptions are violated, do not fuse. + +### 4) Register pass in the right optimization pipeline +- Add the pass to the appropriate transformation pipeline file. +- Keep registration order consistent with dependencies (producer fusions before consumer fusions when required). +- Do not widen pass scope beyond the intended domain. + +### 5) Add transformation unit tests +- Add tests under transformations test suite with: + - positive case(s): pattern is fused + - negative case(s): pattern must remain unchanged when guards fail + - dynamic-shape/type coverage where relevant +- Compare transformed function against expected reference graph. +- Ensure tests validate both structure and semantics. + +### 6) Add functional/layer tests when externally observable +- If fusion changes user-visible behavior/performance-sensitive execution path, add/extend functional tests. +- Cover representative input shapes/dtypes and backend-relevant conditions. +- Keep tests minimal and deterministic. + +### 7) Validate and finalize +- Ensure pass is deterministic and idempotent. +- Verify no duplicated matcher registration. +- Confirm no fallback behavior silently changes model semantics. +- Report one of: + - fully implemented + covered, + - partially implemented (missing functional coverage), + - blocked with explicit reason. + +--- + +## Fusion Design Recommendations + +- Prefer root-cause optimization: fuse only when the resulting graph is strictly safer/faster or equivalent. +- Favor existing OpenVINO ops over introducing new custom ops unless required. +- Keep pattern predicates robust for partial/dynamic shapes. +- Avoid expensive checks in hot transformation loops when cheaper predicates exist. +- Reuse canonical helper utilities for constants, shape checks, and node replacement. + +--- + +## Notes + +- Keep this skill instruction-only in markdown. +- Do not claim fusion support without both positive and negative tests. +- Avoid broad refactoring unrelated to the fusion objective. +- Preserve compatibility with existing passes and test baselines. + +``` \ No newline at end of file diff --git a/.github/agents-prototype/skills/add-fusion-transformation/step1-analysis.md b/.github/agents-prototype/skills/add-fusion-transformation/step1-analysis.md new file mode 100644 index 000000000000..5f6dae6ba6e2 --- /dev/null +++ b/.github/agents-prototype/skills/add-fusion-transformation/step1-analysis.md @@ -0,0 +1,132 @@ +# Skill: OpenVINO Transformation Analysis + +## Purpose + +Analyse an OpenVINO IR sub-graph to identify the optimal fusion pattern and +transformation type before any implementation work begins. Produces a structured +`transformation_analysis.md` that the implementation skill consumes. + +## When to invoke + +- Transformation Agent Step 1 +- Any time you need to decide *which* `ov::pass` class to use and *why* + +--- + +## Steps + +### Step 1: Load Op Spec and Existing Graph + +Obtain the op spec from `agent-results/core-opspec/` (written by Core OpSpec Agent). +If not available, find the op specification in the OpenVINO documentation directory: +`docs/articles_en/documentation/openvino-ir-format/operation-sets/` + +Export the target model to IR and locate the sub-graph visually using the +OpenVINO Model Explorer or by reading the XML file directly. Focus on +identifying: +- Which node types appear before/after the target op +- Constant vs. dynamic inputs on each node +- Shape and dtype at each port +- Output consumer count for each node + +### Step 2: Draw the Sub-Graph Pattern + +Produce a textual node diagram (**this is an example structure — adapt to your actual sub-graph**): + +``` +Parameter(input) ──► MatMul ──────────────► Add ──► Result + ▲ ▲ +Constant(weights) ─────┘ Constant(bias) ──┘ +``` + +For each node record: +- Op type and version (e.g. `MatMul v0`) +- Input sources: parameter (dynamic), constant (static), intermediate (op output) +- Output consumers (does anything else use this output?) +- Shape and dtype at each port + +### Step 3: Identify Constant Constraints + +A `MatcherPass` can only reliably fuse when certain inputs are `Constant` nodes +(otherwise the fusion is not safe at compile time). List each input and whether +it must be constant for the fusion to be valid. + +If non-constant inputs must be treated as runtime values, the transformation may +need to be a `FunctionPass` or use symbolic shape analysis. + +### Step 4: Classify Transformation Type + +Apply this decision tree: + +``` +Is the fusion local (fixed sub-graph topology)? + YES → Does it need to run before any other pass touches these nodes? + YES → MatcherPass registered early in common_optimizations + NO → MatcherPass registered at the appropriate priority slot + NO → Does the pass need consumer information (backward)? + YES → BackwardGraphRewrite + NO → FunctionPass (iterate over all nodes manually) +``` + +| Type | When | Examples in codebase | +|---|---|---| +| `MatcherPass` | Local topology, pattern of 2-10 ops | `FuseMHATokensSplit`, `FuseConvolutionSimpleLeakyRelu` | +| `FunctionPass` | Multi-pattern or graph-wide | `ConstantFolding`, `AlignMixedFP32FP16Types` | +| `BackwardGraphRewrite` | Consumer-first needed | `ConvertPrecision` (when checking all consumers) | +| `GraphRewrite` | Forward multi-pattern | Wraps multiple `MatcherPass` instances | + +### Step 5: Find a Template to Follow + +Search for the syntactically closest existing transformation in `src/common/transformations/` +by subgraph depth and operator types. For example, if your pattern fuses two ops, look for +existing two-op fusions; if it decomposes a complex op, look for similar decompositions. + +Search the headers directory to find candidates: + +``` +dir src\common\transformations\include\transformations\common_optimizations\ +``` + +Read the closest match in full — it is your implementation template. +Do not copy-paste; understand each section before adapting it to your pattern. + +### Step 6: Output `transformation_analysis.md` + +Write your analysis result to `agent-results/transformation/transformation_analysis.md`. +Include at minimum: +- ASCII diagram of the matched sub-graph +- Chosen transformation type and rationale +- Constant constraints for each input +- Template reference (path to the transformation used as template) +- Registration target (which pipeline file and position) + +**Example output structure** (adapt content to your specific transformation): + +``` +## Transformation Analysis + +Target pattern: [ASCII diagram] + +Transformation type: MatcherPass + +Rationale: Fixed topology of 3 ops with constant weights and bias. +MatcherPass is optimal — fires once per match with O(n) complexity. + +Constant constraints: +- weights: MUST be Constant +- bias: MUST be Constant + +Template reference: +src/common/transformations/src/transformations/common_optimizations/fuse_u4_weights_zero_point.cpp + +Registration target: +src/common/transformations/src/transformations/common_optimizations/common_optimizations.cpp +``` + +--- + +## Output + +`transformation_analysis.md` written to `artifacts/` or posted as issue comment. +Contains everything required by `openvino_transformation_implementation` to start +coding without further research. diff --git a/.github/agents-prototype/skills/add-fusion-transformation/step2-implementation.md b/.github/agents-prototype/skills/add-fusion-transformation/step2-implementation.md new file mode 100644 index 000000000000..ae5c9ce5e4b4 --- /dev/null +++ b/.github/agents-prototype/skills/add-fusion-transformation/step2-implementation.md @@ -0,0 +1,157 @@ +# Skill: OpenVINO Transformation Implementation + +## Purpose + +Implement a single OpenVINO `MatcherPass` / `FunctionPass` / `BackwardGraphRewrite` +from the `transformation_analysis.md` produced by the analysis skill. +Covers: header, source, CMake registration, pass-pipeline registration. + +## When to invoke + +- Transformation Agent Step 2 (after analysis is complete) +- When a `patch_type=transformation` git patch exists and needs refinement + +--- + +## Steps + +### Step 1: Create Header File + +Location: `src/common/transformations/include/transformations//.hpp` + +Read an existing transformation header as your template. Good references: +- `src/common/transformations/include/transformations/common_optimizations/fuse_u4_weights_zero_point.hpp` +- `src/common/transformations/include/transformations/common_optimizations/matmul_multiply_fusion.hpp` + +Every header must include: +- `OPENVINO_RTTI("ClassName", "0")` macro +- `TRANSFORMATIONS_API` export macro +- A doxygen comment with before/after ASCII diagram + +Do not copy-paste the template file directly; read and adapt the structure. + +### Step 2: Create Source File + +Location: `src/common/transformations/src//.cpp` + +Use the template below as your starting point, then read the closest existing transformation to +adapt the pattern to your sub-graph: + +```cpp +// Starting template — adapt node types, input count, and guards to your sub-graph. + +::() { + MATCHER_SCOPE(); + + // Describe the pattern bottom-up (root last): + auto input = pattern::any_input(); + auto weights = pattern::wrap_type(); + auto root_op = pattern::wrap_type({input, weights}); + + ov::matcher_pass_callback callback = [=](pattern::Matcher& m) { + const auto& pmap = m.get_pattern_map(); + const auto& root = pmap.at(root_op); + + // Guard: skip if node is shared (multiple consumers) + if (root->get_output_target_inputs(0).size() != 1) + return false; + + auto fused = std::make_shared( + pmap.at(input), pmap.at(weights)); + ov::copy_runtime_info({root}, fused); + fused->set_friendly_name(root->get_friendly_name()); + ov::replace_node(root, fused); + return true; + }; + + auto m = std::make_shared(root_op, matcher_name); + register_matcher(m, callback); +} +``` + +Key implementation patterns to follow from the template: + +- Use `MATCHER_SCOPE(ClassName)` at the start of the constructor +- Use `pattern::wrap_type({...})` for node matching +- Use `pattern::consumers_count(N)` to guard on consumer count +- Call `ov::copy_runtime_info({old_nodes...}, new_node)` before replacement +- Call `fused->set_friendly_name(root->get_friendly_name())` +- Call `ov::replace_node(old_root, new_node)` as the last step +- Return `true` from callback on successful replacement + +Real transformations to read before adapting your code: +- `src/common/transformations/src/transformations/common_optimizations/fuse_u4_weights_zero_point.cpp` +- `src/common/transformations/src/transformations/common_optimizations/matmul_multiply_fusion.cpp` + +For `FunctionPass` (full-graph traversal), read: +- `src/common/transformations/src/transformations/common_optimizations/align_mixed_fp32_fp16_types.cpp` + +### Step 3: Update CMakeLists.txt + +The CMake build system picks up any `.cpp` file listed in the `LIBRARY_SRC` variable. +Add your new `.cpp` to `src/common/transformations/CMakeLists.txt` in the `LIBRARY_SRC` set. +Add the corresponding `.hpp` to `PUBLIC_HEADERS`. + +**Do not add a new `add_library` or `add_test_target` call** — the existing targets +already compile everything in `LIBRARY_SRC`. + +### Step 4: Register in the Pass Pipeline + +**For general (all-backend) use:** +```cpp +// src/common/transformations/src/transformations/common_optimizations/common_optimizations.cpp +// Find the block of ADD_MATCHER calls. Insert in priority order. +ADD_MATCHER(manager, FuseMyOp) +``` + +**For backend-specific use (CPU only):** +```cpp +// src/plugins/intel_cpu/src/graph_optimizer.cpp +ApplyCommonOptimizations::run_on_model(model) { + ... + manager.register_pass(); + ... +} +``` + +**With pass config (opt-in by consumer):** +```cpp +manager.register_pass(); +// Disabled by default; backend enables explicitly: +pass_config->enable(); +``` + +### Step 5: Self-Check + +After writing implementation and tests, verify the transformation fires correctly. +Run the cross-platform validation script: + +``` +python .github/scripts/meat/validate_transformation.py --pass-name +``` + +The script loads the model at `agent-results/analyze-and-convert/ov_model_*/openvino_model.xml`, +applies the transformation, and reports whether the fused op appears in the resulting graph. + +--- + +## Common Pitfalls + +| Problem | Fix | +|---|---| +| Pass fires but output shape is wrong | Call `fused->validate_and_infer_types()` after replacing | +| Pattern matches but replacement crashes | Ensure all `shared_ptr` casts are checked with `ov::as_type_ptr` | +| Pass fires on non-constant input | Add `pattern::wrap_type()` with a predicate check in callback | +| Pass does not fire at all | Verify node consumer count (`consumers_count`) and output port indices | +| Accuracy drops after fusion | Check that the fused op handles transposed inputs the same way as the original sub-graph | + +--- + +## Output + +Modified files: +- `src/common/transformations/include/transformations//.hpp` +- `src/common/transformations/src//.cpp` +- `src/common/transformations/CMakeLists.txt` +- `src/common/transformations/src/transformations/common_optimizations/common_optimizations.cpp` + (or backend-specific file) diff --git a/.github/agents-prototype/skills/add-fusion-transformation/workflow.md b/.github/agents-prototype/skills/add-fusion-transformation/workflow.md new file mode 100644 index 000000000000..b44f8fae476e --- /dev/null +++ b/.github/agents-prototype/skills/add-fusion-transformation/workflow.md @@ -0,0 +1,230 @@ +# Skill: Add OpenVINO Fusion Transformation (End-to-End) + +> **Upstream skill spec:** `skills/add-fusion-transformation/SKILL.md` +> This file is the Copilot-ready, agent-executable version of that specification. + +## Purpose + +End-to-end workflow for implementing a new **pattern-based graph fusion or decomposition +transformation** in OpenVINO. Covers: problem framing → design → implementation → +registration → unit tests → patch generation. + +Based on the OpenVINO transformation API patterns in `src/common/transformations/`. + +--- + +## Architecture + +``` +Model Graph → Pattern Matcher Pass → Subgraph Replacement → Validation/Inference Tests +``` + +--- + +## Repository Layout + +``` +openvino/ +├─ src/common/transformations/ +│ ├─ include/transformations/ +│ │ └─ /.hpp +│ ├─ src/transformations/ +│ │ └─ /.cpp +│ ├─ src/transformations/common_optimizations/ +│ │ └─ common_optimizations.cpp (or relevant pipeline file) +│ └─ tests/ +│ ├─ common_optimizations/ +│ │ └─ .cpp +│ └─ utils/ +└─ tests/ + └─ layer_tests/ or functional/ (if user-visible behavior/perf contract is affected) +``` + +--- + +## When to invoke + +- Transformation Agent, whenever a new fusion pass needs to be created from scratch +- `patch_type=transformation` shortcut path (incoming patch already exists — apply, + validate, then follow Steps 4–6 for testing and registration) + +--- + +## Steps + +### Step 1: Confirm the fusion does not already exist + +- Search for equivalent matcher patterns and passes (`grep -r "MatcherPass\|register_pass" src/common/transformations/`). +- If a similar fusion exists, extend it instead of creating a duplicate pass. +- Reuse existing helper utilities and pattern predicates where possible. + +--- + +### Step 2: Define fusion contract and understand the problem + +Before writing any code: + +1. **Read the op spec** (from Core OpSpec Agent or issue comment) to confirm: + - What does the new fused op do? + - What sub-graph does it replace? + - Are there accuracy or performance requirements? + +2. **Confirm the fusion makes sense at graph level:** + - The pattern must be composable (inputs/outputs align exactly) + - No side-effecting ops in the middle of the pattern + - The replacement preserves dtype and broadcast semantics + +3. **Specify semantic invariants** documented before coding: + - Output values, element types, ranks/dynamic-shape compatibility + - Runtime info / friendly names preserved + - Explicit no-fuse conditions (negative guards) + +4. **Choose the pass type** using `openvino_transformation_analysis` skill. + +### Step 3: Design the Fusion Pattern + +Produce a before/after diagram: + +``` +BEFORE: + x ──► MatMul(transB=true) ──► Add ──► gate1 + ▲ ▲ + W ───────┘ bias ────────┘ + +AFTER: + x ──► FusedLinear(W, bias) ──► gate1 +``` + +Write down: +- Matching conditions (e.g., `transB=true` required, `bias` must be 1-D Constant) +- Output consumer constraints (e.g., `Add` must have exactly one consumer) +- Edge case exclusions (e.g., no fusion when weights are FP16 quantised) + +### Step 4: Implement Header and Source + +Follow **`openvino_transformation_implementation`** skill exactly. + +Checklist after writing: +- [ ] `OPENVINO_RTTI("ClassName", "0")` present +- [ ] `MATCHER_SCOPE` macro used (sets `matcher_name`) +- [ ] `ov::copy_runtime_info` called before `ov::replace_node` +- [ ] `friendly_name` copied from the root node of the replaced sub-graph +- [ ] No raw `delete` or manual output re-wiring + +### Step 5: Register the Pass + +Determine registration location from the analysis output. + +For **common_optimizations**, find the section that groups related passes and insert +your pass alphabetically/logically within that group. + +The CMake build system picks up any new `.cpp` in `src/common/transformations/src/` automatically +— **do NOT manually edit `CMakeLists.txt`** unless you are adding a new subdirectory. + +Verify the registration order does not break existing passes: +- Constant folding must run BEFORE pattern-matching passes that require constant inputs +- Layout-changing passes must run AFTER all arithmetic fusions + +### Step 6: Write Unit Tests + +Location: `src/common/transformations/tests//` + +Required test file: `test_.cpp` + +Read the closest existing transformation test as your template. Good references: +- `src/common/transformations/tests/common_optimizations/fuse_u4_weights_zero_point.cpp` +- `src/common/transformations/tests/common_optimizations/matmul_multiply_fusion_tests.cpp` + +Each test file must include at minimum: +- **One positive test** (`TransformationTestsF`) that verifies the pattern fires and produces the expected replacement graph. +- **One negative test** that verifies the pattern does NOT fire when a guard condition fails (e.g., non-constant weights, multi-consumer output). +- **One output-type-preservation test** for FP16/BF16 inputs if the fusion touches precision. + +**Functional / layer tests (when externally observable):** +If the fusion changes user-visible behavior or a performance-sensitive execution path, +add/extend functional tests under `tests/layer_tests/` or `tests/functional/`. +Keep tests minimal and deterministic. + +### Step 7: Build, Verify and Generate Patch + +```bash +# Build only the transformations library and its tests +cmake --build build/ --target ov_transformations_func_tests -j$(nproc) + +# Run only this test +./build/src/common/transformations/tests/ov_transformations_func_tests \ + --gtest_filter="*FuseMyOp*" +``` + +All three tests must pass: +- `FuseMyOp_Basic` → PASSED +- `FuseMyOp_Negative_DynamicWeights` → PASSED +- `FuseMyOp_OutputTypePreserved` → PASSED + +**Validate and finalize before generating the patch:** +- Ensure the pass is deterministic and idempotent. +- Verify no duplicated matcher registration. +- Confirm no fallback behavior silently changes model semantics. + +#### Generate the patch + +```bash +git add src/common/transformations/ +git commit -m "transformations: add FuseMyOp MatcherPass + +Fuses MatMul(transB=true) + Add(constant_bias) into a single +MyFusedOp to reduce kernel dispatch overhead. + +Refs: #" + +git format-patch HEAD~1 --stdout > \ + transformation-fuse-my-op-${GITHUB_RUN_ID}.patch +``` + +The patch is available in `agent-results/transformation/` for collection by the orchestrator. + +--- + +## Fusion Design Recommendations + +- Prefer root-cause optimization: fuse only when the resulting graph is strictly safer/faster or equivalent. +- Favor existing OpenVINO ops over introducing new custom ops unless required. +- Keep pattern predicates robust for partial/dynamic shapes. +- Avoid expensive checks in hot transformation loops when cheaper predicates exist. +- Reuse canonical helper utilities for constants, shape checks, and node replacement. +- Prefer `ov::pass::MatcherPass` for local rewrite; use `GraphRewrite` only when grouping multiple related matchers. +- Avoid hidden behavior changes; if assumptions are violated, do not fuse. + +--- + +## Design Checklist (before declaring success) + +- [ ] Pattern fires exactly once per matching sub-graph (not multiple times) +- [ ] Pattern does NOT fire on known negative cases (non-constant inputs, wrong consumer count) +- [ ] Friendly name is preserved on the fused node +- [ ] RT info propagated with `copy_runtime_info` +- [ ] Output type and shape inferred correctly after fusion +- [ ] Pass registered at the correct position in the pipeline +- [ ] Both header and source files added to CMakeLists.txt +- [ ] At least 3 unit tests written (positive, negative, type-preservation) +- [ ] Patch generated and saved to agent-results/transformation/ + +--- + +## Notes + +- Do not claim fusion support without both positive and negative tests. +- Avoid broad refactoring unrelated to the fusion objective. +- Preserve compatibility with existing passes and test baselines. +- Only touches `src/common/transformations/` (general) or the specific plugin path + if the transformation is backend-exclusive — never both in the same implementation. + +--- + +## Key References + +- Transformation API docs: https://docs.openvino.ai/2025/documentation/openvino-extensibility/transformation-api.html +- Existing fusion examples: + - `src/common/transformations/src/transformations/common_optimizations/fuse_u4_weights_zero_point.cpp` + - `src/common/transformations/src/transformations/common_optimizations/matmul_multiply_fusion.cpp` +- Test utilities: `src/common/transformations/tests/utils/compare_graphs.hpp` diff --git a/.github/agents-prototype/skills/add-gpu-op/SKILL.md b/.github/agents-prototype/skills/add-gpu-op/SKILL.md new file mode 100644 index 000000000000..b231b12d5d79 --- /dev/null +++ b/.github/agents-prototype/skills/add-gpu-op/SKILL.md @@ -0,0 +1,33 @@ +--- +name: add-gpu-op +description: Add a new operation to the OpenVINO GPU plugin — OpenCL kernel design, oneDNN-backed paths, sub-group/LWS tuning, and functional tests. +--- + +# Skill: Add GPU Op + +## When to use +When the Core OpSpec agent has produced a new op spec and you need to implement +GPU plugin support: kernel implementation, registration, and testing. + +## Steps + +Execute in order — each step produces artifacts consumed by the next. + +| Step | File | Purpose | +|---|---|---| +| 0a | [step0-plan.md](step0-plan.md) | Read op spec, build implementation plan, decide kernel vs oneDNN path | +| 0b | [step0-parse-spec.md](step0-parse-spec.md) | Parse op spec JSON/MD into GPU-readable format | +| 1 | [step1-hardware-analysis.md](step1-hardware-analysis.md) | Identify hardware constraints, sub-group size, memory layout requirements | +| 2 | [step2-file-structure.md](step2-file-structure.md) | Create kernel and primitive files, register in factory | +| 3a | [step3-kernel-development.md](step3-kernel-development.md) | Write OpenCL kernel (blocked reads, sub-groups, LWS tuning) | +| 3b | [step3-write-tests.md](step3-write-tests.md) | Write layer tests | +| 3c | [step3-run-tests.md](step3-run-tests.md) | Run and verify tests | +| 3d | [step3-profiling.md](step3-profiling.md) | Profile and tune kernel | +| 4 | [step4-onednn-integration.md](step4-onednn-integration.md) | Add oneDNN-backed primitive path where applicable | +| 5 | [step5-optimize.md](step5-optimize.md) | Final performance optimizations | + +## Entry point for orchestrated use + +When invoked by the GPU Agent or Enable Operator Agent, start from +[orchestrator.md](orchestrator.md) — it selects the relevant subset of steps +based on the op type and available hardware. diff --git a/.github/agents-prototype/skills/add-gpu-op/opset-migration.md b/.github/agents-prototype/skills/add-gpu-op/opset-migration.md new file mode 100644 index 000000000000..b455e62e6ed1 --- /dev/null +++ b/.github/agents-prototype/skills/add-gpu-op/opset-migration.md @@ -0,0 +1,179 @@ +--- +name: gpu-opset-migration +description: Opset version update and migration strategy for OpenVINO GPU plugin operations +--- + +# Purpose + +Guide the process of updating GPU plugin operations when OpenVINO introduces new Opset versions. Ensures backward compatibility, proper spec analysis, and correct handling of new data types (bf16, fp16). + +# When to Use + +Use this skill when: +- A new OpenVINO Opset version is released and existing GPU ops need updating +- Migrating an operation from an older Opset version to a newer one +- Adding support for new data types introduced in a new Opset + +```mermaid +flowchart TD + A[New Opset Released] --> B[Analyze Spec Changes] + B --> C{Breaking Changes?} + C -->|Yes| D[Plan Migration] + C -->|No| E[Version Bump Only] + D --> F[Implement Changes] + F --> G[Verify Backward Compat] + E --> G + G --> H[Test All Versions] + H --> I[Done] +``` + +# Procedure + +1. **Step 1: Spec Analysis** — Compare old and new Opset versions for the target Op +2. **Step 2: Backward Compatibility** — Ensure legacy behavior is preserved +3. **Step 3: Data Type Support** — Add new data type support if required +4. **Step 4: Implementation** — Update the GPU plugin code +5. **Step 5: Verification** — Test both old and new Opset versions + +--- + +# Prerequisites Check + +Verify you have access to the OpenVINO Opset specifications: + +**Windows (PowerShell):** +```powershell +# Check if OpenVINO source is available with opset definitions +Test-Path "src\core\include\openvino\op\ops.hpp" +``` + +**Ubuntu:** +```bash +# Check if OpenVINO source is available with opset definitions +test -f src/core/include/openvino/op/ops.hpp && echo "OK" || echo "MISSING" +``` + +- **If successful:** Proceed to "Quick Start - Main Steps" +- **If failed:** Clone or checkout the OpenVINO repository first + +--- + +# Quick Start + +## Installation (Prerequisites Check failed) + +Ensure the OpenVINO source tree is available. Clone from: +```bash +git clone https://github.com/openvinotoolkit/openvino.git +``` + +--- + +## Main Steps (Prerequisites Check passed) + +### Step 1: Spec Analysis + +Compare Opset versions to identify changes. Invoke `parse-op-spec` for both the old and new Opset version specs to produce structured summaries, then compare: + +1. **Locate the Op specification** in: + - `src/core/include/openvino/op/` — C++ Op definitions + - OpenVINO Opset documentation: https://docs.openvino.ai/latest/openvino_docs_ops_opset.html + +2. **Compare versions** — Identify: + - New attributes or parameters added + - Changed behavior or semantics + - New input/output specifications + - Deprecated features + +3. **Document changes** in a comparison table: + +| Aspect | Old Version | New Version | Impact | +|---|---|---|---| +| Attributes | ... | ... | ... | +| Inputs/Outputs | ... | ... | ... | +| Behavior | ... | ... | ... | + +### Step 2: Backward Compatibility + +**Critical rule:** Legacy behavior must be preserved. + +- If the new version adds optional parameters, set defaults matching the old version's behavior +- If semantics changed, maintain a version check in the implementation +- Support both old and new Opset versions simultaneously + +**Pattern:** +```cpp +// Example: Version-aware implementation +if (op_version >= 14) { + // New behavior +} else { + // Legacy behavior (must match old opset exactly) +} +``` + +### Step 3: Data Type Support + +Check `clinfo` extensions for hardware data type support: + +| Extension | Data Type | Notes | +|---|---|---| +| `cl_khr_fp16` | fp16 (half) | Most Intel GPUs support this | +| `cl_intel_bfloat16_conversions` | bf16 | Xe-HPG (Arc) and newer | +| `cl_khr_fp64` | fp64 (double) | Rarely needed; not on all GPUs | + +**Windows (PowerShell):** +```powershell +clinfo | Select-String -Pattern "cl_khr_fp16|cl_intel_bfloat16|cl_khr_fp64" +``` + +**Ubuntu:** +```bash +clinfo | grep -E "cl_khr_fp16|cl_intel_bfloat16|cl_khr_fp64" +``` + +If a new Opset requires `bf16`/`fp16`: +- Add data type support to the kernel selector +- Update JitConstants with appropriate type definitions +- Add test cases for new data types + +### Step 4: Implementation + +Update the following files (see `gpu-op-file-structure` skill for paths): + +1. **Op translation** (`src/plugins/intel_gpu/src/plugin/ops/.cpp`): + - Update `CreateOp` to handle new Opset version + - Map new attributes to primitive parameters + +2. **Primitive** (`src/plugins/intel_gpu/include/intel_gpu/primitives/.hpp`): + - Add new fields if the Opset adds parameters + +3. **Kernel implementation** (kernel selector files): + - Update JitConstants for new parameters + - Modify OpenCL kernel if operation logic changed + +### Step 5: Verification + +Invoke `write-gpu-tests` to add test cases for the new Opset version and data types, then invoke `run-gpu-tests` to execute: + +1. **New version tests:** Run functional tests filtered to the target op +2. **Backward compatibility:** Run functional tests also for the old Opset version filter (`**v*`) + +Both must pass before the migration is considered complete. + +--- + +# Troubleshooting + +- **Old tests fail after migration**: Check that default values match the old Opset behavior +- **New data type not supported**: Verify hardware support via `clinfo` extensions +- **Type mismatch errors**: Ensure kernel selector handles all supported data types +- **Missing Op in new Opset**: Check if the Op was renamed or merged with another Op +- **Backward compat breaks**: Review all code paths — version checks must cover all branches + +--- + +# References + +- Related skills: `gpu-kernel-enabling`, `gpu-op-file-structure`, `collect-gpu-hardware-spec`, `write-gpu-tests`, `run-gpu-tests` +- OpenVINO Opset documentation: https://docs.openvino.ai/latest/openvino_docs_ops_opset.html +- Op definitions: `src/core/include/openvino/op/` diff --git a/.github/agents-prototype/skills/add-gpu-op/orchestrator.md b/.github/agents-prototype/skills/add-gpu-op/orchestrator.md new file mode 100644 index 000000000000..ad63479a9900 --- /dev/null +++ b/.github/agents-prototype/skills/add-gpu-op/orchestrator.md @@ -0,0 +1,170 @@ +--- +name: intel-gpu-kernel +description: Entry point for OpenVINO GPU plugin kernel implementation — guides the full development workflow from hardware spec collection through kernel enabling, profiling, and optimization by routing to specialized sub-skills +--- + +# Purpose + +Orchestrate the full development workflow for Intel OpenVINO GPU plugin kernel implementation, covering both custom OpenCL kernels and oneDNN primitive integration, by routing to specialized sub-skills for each phase. + +# When to Use + +Use this skill as the **entry point** for any Intel OpenVINO GPU plugin kernel implementation task. It will guide you through the complete workflow by referencing the appropriate sub-skill at each stage. + +**CRITICAL**: Before generating any kernel code, you must collect the target hardware specifications via `clinfo` (see `collect-gpu-hardware-spec` skill). + +# Procedure + +Follow this workflow in order. Each step links to a dedicated skill with detailed procedures. + +1. **Step 0:** Analyze the Op spec and create an implementation plan (`plan-op-implementation` with `parse-op-spec`) +2. **Step 1:** Collect target GPU hardware specs via clinfo (`collect-gpu-hardware-spec`) +3. **Step 2:** Build the OpenVINO tree with GPU enabled (`build-openvino`) +4. **Step 3:** Determine file locations and naming conventions (`gpu-op-file-structure`) +5. **Step 4:** Implement C++ primitives, create ref kernel, write and run tests (`gpu-kernel-enabling` → `write-gpu-tests` → `run-gpu-tests`) +6. **Step 4.5:** *(Conditional)* Integrate oneDNN primitive if available (`gpu-integrate-onednn-primitive`) +7. **Step 5:** *(Mandatory)* Apply hardware-aware optimizations and produce Performance Comparison Report (`gpu-kernel-optimize`) + +> `gpu-kernel-device-timing` is used as a utility within Steps 4, 4.5, and 5. + +--- + +# Prerequisites Check + +This is a routing skill — it delegates work to sub-skills. Verify you are in the OpenVINO source tree: + +**Windows (PowerShell):** +```powershell +# Verify OpenVINO GPU plugin source tree is available +Test-Path "src\plugins\intel_gpu\src\kernel_selector\kernels" +``` + +**Ubuntu:** +```bash +# Verify OpenVINO GPU plugin source tree is available +test -d src/plugins/intel_gpu/src/kernel_selector/kernels && echo "OK" || echo "MISSING" +``` + +- **If successful:** Proceed to "Quick Start - Main Steps" +- **If failed:** Ensure you are in the OpenVINO source root directory + +--- + +# Quick Start + +## Installation (Prerequisites Check failed) + +Clone or navigate to the OpenVINO source repository: +```bash +git clone https://github.com/openvinotoolkit/openvino.git +cd openvino +``` + +--- + +## Main Steps (Prerequisites Check passed) + +Follow the Development Workflow below. Each step routes to a dedicated sub-skill. + +# Development Workflow + +```mermaid +flowchart TD + Z[New Op Request] --> A["Step 0: plan-op-implementation\n(with parse-op-spec)"] + A --> B[Step 1: collect-gpu-hardware-spec] + B --> C[Step 2: build-openvino] + C --> D[Step 3: gpu-op-file-structure] + D --> E["Step 4: gpu-kernel-enabling\n→ write-gpu-tests → run-gpu-tests"] + E --> E1[Ref Baseline: gpu-kernel-device-timing] + E1 --> F{oneDNN primitive available?} + F -->|Yes| G[Step 4.5: gpu-integrate-onednn-primitive] + F -->|No| J + G --> J + J["Step 5: gpu-kernel-optimize (MANDATORY)"] --> J1[Opt Measurement: gpu-kernel-device-timing] + J1 --> K[Performance Comparison Report] + K --> L[Done] + + subgraph Existing Op + N[Existing Op Needs New Opset Support?] --> M[gpu-opset-migration] + end + + style E1 fill:#e0e0ff,stroke:#888 + style J1 fill:#e0e0ff,stroke:#888 +``` + +> **Note:** `gpu-kernel-device-timing` is a utility sub-skill used within Steps 4 and 5 to measure kernel device time via clintercept. It is not a standalone step — invoke it whenever you need a performance baseline or need to verify optimization progress. + +## Step 0: Plan Op Implementation +**Skill:** `plan-op-implementation` + +Before any specs are collected or code is written, analyze the OpenVINO operation specification (often provided via docs URL) to map inputs, outputs, attributes, supported data types, and formulate a Single Layer Test (SLT) and kernel strategy. + +## Step 1: Collect Hardware Specs +**Skill:** `collect-gpu-hardware-spec` + +Before writing any code, collect GPU hardware specs via `clinfo`. This determines SIMD size, sub-group preferences, SLM capacity, and tuning parameters. + +## Step 2: Build the GPU Plugin +**Skill:** `build-openvino` + +Build the OpenVINO tree with Intel GPU enabled. In the GPU kernel workflow, use Debug builds for development and functional verification, Release builds for profiling and optimization, and Python/Wheel builds only when packaging or Python integration is required. + +## Step 3: File Structure & Naming +**Skill:** `gpu-op-file-structure` + +Determine the exact file locations and naming conventions for the new operation. New ops use the `ocl_v2` co-located structure where `.cl` kernel files sit alongside `.cpp` graph impl files. All files must follow `snake_case` for filenames and `CamelCase` for class names across directories (kernel selector, `ocl_v2` graph impls with co-located CL kernels, primitives, plugin ops, tests). + +## Step 4: Kernel Enabling & Verification +**Skills:** `gpu-kernel-enabling` → `write-gpu-tests` → `run-gpu-tests` + +Implement C++ primitives and create the reference OpenCL kernel (`gpu-kernel-enabling`), then write test code (`write-gpu-tests`), and finally execute tests to verify correctness (`run-gpu-tests`). After the ref kernel passes tests, measure the **Ref Baseline** via `gpu-kernel-device-timing`, then proceed to the optional oneDNN integration (Step 4.5). + +## Step 4.5: oneDNN Primitive Integration (Conditional) +**Skill:** `gpu-integrate-onednn-primitive` + +If the target operation has an available oneDNN primitive, integrate the oneDNN-based path alongside the OpenCL kernel. This step runs **after** the Ref Baseline has been measured via `gpu-kernel-device-timing`. After integration, use `gpu-kernel-device-timing` again to profile the oneDNN path for comparison. + +## Step 5: Hardware-Aware Optimization +**Skill:** `gpu-kernel-optimize` + +This step is **mandatory** — it produces the Performance Comparison Report (Ref vs Opt). Apply optimizations: sub-group size selection, block reads (fsv16), local work size tuning, and register pressure management. Create optimized kernel variants. Use `gpu-kernel-device-timing` iteratively during this step to measure each optimization iteration and verify improvement. + +## Opset Migration (When Needed) +**Skill:** `gpu-opset-migration` + +Use this branch when you are updating an existing GPU operation for a newer OpenVINO Opset rather than enabling a brand-new operation. This migration path is separate from the main new-op flow above and focuses on backward compatibility, spec deltas, and new data type support. + +--- + +# Sub-Skills Reference + +| Skill | Purpose | When | +|---|---|---| +| `parse-op-spec` | Fetch and parse Op specification into structured summary | Called by `plan-op-implementation` in Step 0 | +| `plan-op-implementation` | Analyze Op spec and formulate primitive/kernel/test plan | Always first, before writing code | +| `collect-gpu-hardware-spec` | Collect GPU specs via clinfo | After planning, before optimizing | +| `build-openvino` | Build OpenVINO with the required GPU-oriented configuration | Before development/testing | +| `gpu-op-file-structure` | Files to implement for new operation | When creating new op files | +| `gpu-kernel-enabling` | Implement C++ primitives & create ref OpenCL kernel | Initial kernel enabling phase | +| `write-gpu-tests` | Create SLT and unit test code for GPU ops | During enabling, migration, oneDNN integration | +| `run-gpu-tests` | Execute GPU tests and dump kernel sources | After writing tests — verification utility | +| `gpu-integrate-onednn-primitive` | Integrate oneDNN-based primitive | When oneDNN supports the op | +| `gpu-kernel-device-timing` | Measure device timing with clintercept | During Steps 4, 4.5, and 5 — baseline and optimization iterations | +| `gpu-kernel-optimize` | Hardware-aware kernel optimization | After ref kernel baseline is measured | +| `gpu-opset-migration` | Opset version migration | When updating to new Opset | + +--- + +# Troubleshooting + +- **Sub-skill not found by agent**: Ensure all sub-skill files exist under `skills/add-gpu-op/` and each has a valid YAML frontmatter with the correct skill name +- **Wrong step order**: Always follow the numbered steps in sequence — skipping `plan-op-implementation` or `collect-gpu-hardware-spec` leads to incorrect kernel code +- **Opset migration vs new op confusion**: If the Op already exists and only needs a version bump, use `gpu-opset-migration` instead of the full new-op flow +- **Performance loop not converging**: If Step 5 optimization iterations do not improve timing, review the kernel strategy in the original `plan-op-implementation` output + +--- + +# References + +- Skills directory: `skills/add-gpu-op/` +- Repository guidelines: `.github/agents-prototype/gpu.agent.md` diff --git a/.github/agents-prototype/skills/add-gpu-op/step0-parse-spec.md b/.github/agents-prototype/skills/add-gpu-op/step0-parse-spec.md new file mode 100644 index 000000000000..3a399874b3b8 --- /dev/null +++ b/.github/agents-prototype/skills/add-gpu-op/step0-parse-spec.md @@ -0,0 +1,159 @@ +--- +name: parse-op-spec +description: Fetch and parse an OpenVINO operation specification from URL or text, extracting inputs, outputs, attributes, and data types into a structured summary +--- + +# Purpose + +Parse an Operation (Op) specification from a URL or pasted text and produce a structured summary of its inputs, outputs, attributes, and data types. This is a pure **extraction** utility — it does not make implementation decisions. The caller (e.g., `plan-op-implementation`, `gpu-opset-migration`) uses the extracted data for planning. + +# When to Use + +Use this skill whenever an Op specification needs to be read and structured: +- As the first step of `plan-op-implementation` — parse a new Op spec +- During `gpu-opset-migration` — parse the new Opset version spec for comparison with the old version + +```mermaid +flowchart TD + A[URL or pasted text] --> B[Fetch / Read spec] + B --> C[Extract Op Name & Version] + C --> D[Extract Inputs] + C --> E[Extract Outputs] + C --> F[Extract Attributes] + D --> G[Map Data Types to OV precisions] + E --> G + F --> G + G --> H[Return structured summary] +``` + +# Procedure + +1. **Step 1: Acquire Specification** — Fetch URL or read pasted text +2. **Step 2: Extract Elements** — Parse inputs, outputs, attributes, data types +3. **Step 3: Map to OpenVINO Precisions** — Convert framework types to OV types +4. **Step 4: Produce Structured Summary** — Return standardized output to the caller + +--- + +# Prerequisites Check + +This skill requires an Op specification as input. No tools needed. + +**Windows (PowerShell):** +```powershell +Write-Host "Ready: Provide an Op spec URL or paste the specification text." +``` + +**Ubuntu:** +```bash +echo "Ready: Provide an Op spec URL or paste the specification text." +``` + +- **If Op spec is provided:** Proceed to "Quick Start - Main Steps" +- **If no Op spec:** Ask the user for the specification URL or text + +--- + +# Quick Start + +## Installation (Prerequisites Check failed) + +No installation required. Ask the user to provide: +- An OpenVINO docs URL (e.g., `https://docs.openvino.ai/.../operation-specs/...`) +- A PyTorch/TensorFlow/ONNX docs URL +- Or directly pasted specification text + +--- + +## Main Steps (Prerequisites Check passed) + +### Step 1: Acquire Specification + +- **If URL provided:** Fetch the webpage content. +- **If text provided:** Read directly. +- Note that framework documentation (PyTorch, TensorFlow, ONNX) may be structured differently than OpenVINO IR specifications. + +### Step 2: Extract Key Elements + +Parse the specification and extract: + +1. **Operation Name** — Name and version if applicable (e.g., `ScatterNDUpdate-15`) +2. **Inputs** — For each input tensor: + - Name, rank/shape constraints (1D, 2D, etc.) + - Required data types + - Whether optional +3. **Outputs** — For each output tensor: + - Name, rank/shape, deduced types +4. **Attributes** — Static parameters: + - Name, type, default value, valid range + - Note: some frameworks treat attributes as inputs (or vice versa) +5. **Broadcasting rules** — If mentioned in the spec + +### Step 3: Map Data Types to OpenVINO Precisions + +Convert framework-specific or generic types to supported OpenVINO precisions: + +| Framework Type | OpenVINO Precision | +|---|---| +| float / float32 | FP32 | +| float16 / half | FP16 | +| bfloat16 | BF16 | +| int / int32 | I32 | +| int64 / long | I64 | +| int8 | I8 | +| uint8 | U8 | +| bool | BOOL | + +### Step 4: Produce Structured Summary + +Return the parsed data in this format: + +```markdown +## Op Spec Summary: + +**Version:** +**Source:** + +### Inputs +| # | Name | Shape Constraint | Data Types | Optional | +|---|------|-----------------|------------|----------| +| 1 | ... | ... | ... | No | +| 2 | ... | ... | ... | Yes | + +### Outputs +| # | Name | Shape | Data Types | +|---|------|-------|------------| +| 1 | ... | ... | ... | + +### Attributes +| Name | Type | Default | Description | +|------|------|---------|-------------| +| ... | ... | ... | ... | + +### Data Type Mapping +| Role | Supported Precisions | +|------|---------------------| +| Data (T) | FP32, FP16, I32, I64 | +| Index (T_IDX) | I32, I64 | + +### Broadcasting + +``` + +This summary is consumed by the caller — do not make implementation decisions here. + +--- + +# Troubleshooting + +- **Op spec URL returns 404 or empty page**: Try the nightly docs URL variant or ask the user to paste the spec text directly +- **Framework spec lacks explicit input/output types**: Infer types from usage context and document assumptions clearly +- **Ambiguous attribute vs input distinction**: List both interpretations and let the caller decide + +--- + +# References + +- Related skills: `plan-op-implementation`, `gpu-opset-migration` +- OpenVINO Op specs: https://docs.openvino.ai/latest/openvino_docs_ops_opset.html +- ONNX Op specs: https://onnx.ai/onnx/operators/ diff --git a/.github/agents-prototype/skills/add-gpu-op/step0-plan.md b/.github/agents-prototype/skills/add-gpu-op/step0-plan.md new file mode 100644 index 000000000000..c7ee20a33722 --- /dev/null +++ b/.github/agents-prototype/skills/add-gpu-op/step0-plan.md @@ -0,0 +1,138 @@ +--- +name: plan-op-implementation +description: Analyze OpenVINO runtime operation specifications (e.g., from docs URL) and create an implementation plan for the GPU plugin including primitive mapping, data types, and testing strategy. +--- + +# Purpose + +Analyze a new Operation (Op) specification and formulate a comprehensive implementation plan for the OpenVINO GPU plugin before any code is written or hardware metrics are gathered. + +# When to Use + +Use this skill as the **first step** (Step 0) in the `intel-gpu-kernel` workflow whenever a new GPU Op implementation is requested. The user provides an Op spec URL (OpenVINO, PyTorch, TensorFlow, or ONNX docs) or pasted text. + +```mermaid +flowchart TD + A[Receive Op Spec URL or Text] --> B[Invoke parse-op-spec] + B --> C[Formulate Primitive Mapping] + C --> D[Outline Kernel Strategy] + D --> E[Plan SLT Coverage] + E --> F[Present Plan to User] + F --> G{User Approves?} + G -->|Yes| H[Proceed to intel-gpu-kernel Step 1] + G -->|No| I[Revise Plan] + I --> F +``` + +# Procedure + +Follow these steps exactly to plan the operation implementation. + +1. **Step 1: Analyze the Operation Specification** — Invoke `parse-op-spec` to fetch and extract inputs/outputs/attributes/types +2. **Step 2: Formulate the Primitive Mapping Plan** — Design cldnn primitive, shape inference, precision support +3. **Step 3: Outline Kernel Design Strategy** — Map to OpenCL work-items, identify dependencies, check oneDNN +4. **Step 4: Plan Single Layer Test (SLT) Coverage** — Define test parameters, corner cases, reference impl +5. **Step 5: Deliver the Implementation Plan** — Present summary and wait for user approval + +--- + +# Prerequisites Check + +This skill requires an Op specification as input. No build tools are needed. + +**Windows (PowerShell):** +```powershell +# Verify the user has provided an Op spec URL or text +# No tool installation needed — this is a planning-only skill +Write-Host "Ready: Provide an Op spec URL or paste the specification text." +``` + +**Ubuntu:** +```bash +# Verify the user has provided an Op spec URL or text +# No tool installation needed — this is a planning-only skill +echo "Ready: Provide an Op spec URL or paste the specification text." +``` + +- **If Op spec is provided:** Proceed to "Quick Start - Main Steps" +- **If no Op spec:** Ask the user for the specification URL or text + +--- + +# Quick Start + +## Installation (Prerequisites Check failed) + +No installation required. Ask the user to provide the Operation Specification: +- An OpenVINO docs URL (e.g., `https://docs.openvino.ai/.../operation-specs/...`) +- A PyTorch/TensorFlow/ONNX docs URL +- Or directly pasted specification text + +--- + +## Main Steps (Prerequisites Check passed) + +## Step 1: Analyze the Operation Specification + +Invoke the `parse-op-spec` skill to fetch/read the spec and produce a structured summary of the Op (name, inputs, outputs, attributes, data type mapping). + +Review the returned summary and verify completeness before proceeding to Step 2. + +## Step 2: Formulate the Primitive Mapping Plan + +Plan how this operation maps to the OpenVINO GPU plugin (cldnn) graph structure. + +1. **Primitive Creation:** What will the `cldnn::primitive` struct look like? What fields does it need to store the operation attributes? +2. **Shape Infer Logic:** How will the output shape be calculated from the inputs? Are there dynamic shapes to consider? + - **Static Shape:** Output dimensions are fully determined at graph compilation time. + - **Dynamic Shape:** If output size depends on data values at runtime (e.g., the number of filled rows), `shape_infer` must return `ov::PartialShape::dynamic()` for that dimension. Document the runtime mechanism needed to determine the actual output size (e.g., a counting pass, `set_output_shape`, or shape-of sub-graph). +3. **Data Type / Precision Support:** Determine which layout (e.g., `bfyx`, `b_fs_yx_fsv16`) and precisions (e.g., `f32`, `f16`, `i32`) must be supported in the custom OpenCL kernel. Usually, coordinate types (`T_IDX`) must support `i32`/`i64` and data types (`T`) must support floating-point + integer types. + +## Step 3: Outline Kernel Design Strategy + +1. **Work Items and Execution Space:** How will the problem be mapped to OpenCL work-items? (e.g., 1 work-item per output element? 1 work-item per row?). +2. **Dependencies:** Are there dependencies between elements that would prevent naive parallelization? If sequential dependencies exist, propose a concrete GPU-friendly strategy: + - **Two-pass approach:** Pass 1 counts/scans, Pass 2 scatters results (recommended for most cases). + - **Parallel prefix-sum (scan):** When cumulative counts determine output positions. + - **Atomic operations:** When contention is low and output order is unimportant. + - Document the chosen strategy and why it fits the operation. +3. **Existing implementations:** Check if oneDNN provides an existing optimized primitive for this type of operation (e.g. convolution, matmul). If so, suggest integrating via `gpu-integrate-onednn-primitive` in later steps. + +## Step 4: Plan Single Layer Test (SLT) Coverage + +Define the scope for the functional tests (`SingleLayerTests`): +1. **Test Parameters:** What parameters (precisions, input shapes, attributes) need to be iterated over? +2. **Corner Cases:** Identify potential edge cases from the spec (e.g., empty rows, empty tensors, negative numbers, Out-of-Bounds handling). +3. **Reference Implementation:** A reference implementation (e.g., in `ov::reference`) must exist or be created to check against. + +## Step 5: Deliver the Implementation Plan to User + +Present the summary to the user using the following format: +1. **Summary of Op Spec** (Name, Inputs, Outputs, Attributes). +2. **GPU Plugin Mapping** (Primitive struct design, precision/layout support). +3. **Proposed Kernel Strategy** (GWS/LWS mapping idea). +4. **SLT Test Plan** (Parameter sweep and corner cases). + +**WAIT FOR APPROVAL**: Ask the user to review the plan. Do not proceed to `intel-gpu-kernel` (Step 1) without user confirmation. + +--- + +# Troubleshooting + +- **Op spec URL returns 404 or empty page**: Try the nightly docs URL variant or ask the user to paste the spec text directly +- **Framework spec lacks explicit input/output types**: Infer types from usage context and document assumptions in the plan +- **Dynamic output shape detected but unclear mechanism**: Default to two-pass approach (count pass + scatter pass) and note it in the plan for user review +- **No reference implementation exists in `ov::reference`**: Flag this in the plan — a custom reference must be written during the enabling phase +- **oneDNN availability uncertain**: List the operation as "check during Step 4.5" and proceed with OpenCL-first strategy + +--- + +# Next Steps + +Once the plan is approved, the `intel-gpu-kernel` orchestrator routes you through the remaining steps. Proceed to **Step 1: `collect-gpu-hardware-spec`**. + +--- + +# References + +- Related skills: `parse-op-spec`, `intel-gpu-kernel`, `gpu-integrate-onednn-primitive` diff --git a/.github/agents-prototype/skills/add-gpu-op/step1-hardware-analysis.md b/.github/agents-prototype/skills/add-gpu-op/step1-hardware-analysis.md new file mode 100644 index 000000000000..7d9688ef74a3 --- /dev/null +++ b/.github/agents-prototype/skills/add-gpu-op/step1-hardware-analysis.md @@ -0,0 +1,133 @@ +--- +name: collect-gpu-hardware-spec +description: Collect target Intel GPU hardware specifications via clinfo for kernel tuning parameter reference +--- + +# Purpose + +Collect the target Intel GPU hardware specifications via `clinfo` for use by other GPU skills. This is a mandatory first step before writing any OpenCL kernel code. + +# When to Use + +Use this skill **before** any GPU kernel development or optimization task. It provides the hardware context that all other GPU skills depend on. + +```mermaid +flowchart TD + A[Start GPU Work] --> B[Run clinfo] + B --> C[Record Device Name, EUs, Sub-group Size, SLM, Max Work Group Size] + C --> D[Proceed to Kernel Development] +``` + +# Procedure + +Follow these steps in order: + +1. **Step 1: Acquire Hardware Specs** — Run `clinfo` and collect key parameters. If system has both iGPU and dGPU, collect specs for both. +2. **Step 2: Identify Architecture** — Map Device Name to architecture family using the table below. +3. **Step 3: Record Baseline** — Save the collected parameters for use by `gpu-kernel-enabling` and `gpu-kernel-optimize` skills. + +--- + +# Prerequisites Check + +Verify that `clinfo` is available on the system: + +**Windows (PowerShell):** +```powershell +# Check if clinfo is available +clinfo --version +``` + +**Ubuntu:** +```bash +# Check if clinfo is available +clinfo --version +``` + +- **If successful:** Proceed to "Quick Start - Main Steps" +- **If failed:** Follow "Quick Start - Installation" below + +--- + +# Quick Start + +## Installation (Prerequisites Check failed) + +**Windows (PowerShell):** +```powershell +# clinfo is typically bundled with Intel GPU drivers +# Ensure Intel GPU drivers are installed +# Or install via package manager if available +winget install --id Intel.clinfo --source winget +``` + +**Ubuntu:** +```bash +# Install clinfo +sudo apt-get update +sudo apt-get install -y clinfo +``` + +--- + +## Main Steps (Prerequisites Check passed) + +### Step 1: Collect Hardware Specs + +**Windows (PowerShell):** +```powershell +# Collect GPU hardware specifications +clinfo | Select-String -Pattern "Device Name|Max compute units|Max work group size|Max sub-groups|Max sub-group size|Local memory type|Local memory size|Global memory cache size" +``` + +**Ubuntu:** +```bash +# Collect GPU hardware specifications +clinfo | grep -E "Device Name|Max compute units|Max work group size|Max sub-groups|Max sub-group size|Local memory type|Local memory size|Global memory cache size" +``` + +### Step 2: Identify Architecture + +Classify the GPU based on Device Name: + +| Device Name Pattern | Type | Architecture | +|---|---|---| +| HD Graphics, UHD Graphics | iGPU | Gen9 | +| Iris Xe (TigerLake/AlderLake/RaptorLake) | iGPU | Xe-LP | +| Intel Graphics (Meteor Lake / Arrow Lake) | iGPU | Xe-LPG | +| Intel Graphics (Lunar Lake) | iGPU | Xe2-LPG | +| Intel Graphics (Panther Lake) | iGPU | Xe3 | +| Arc A-series (A370M, A580, A770, etc.) | dGPU | Xe-HPG | +| Arc B-series (B570, B580) | dGPU | Xe2-HPG | +| Data Center GPU Max (Ponte Vecchio) | dGPU | Xe-HPC | + +**If system has both iGPU and dGPU:** Collect specs for both and verify/validate/optimize on both devices. + +### Step 3: Record Baseline + +From the `clinfo` output, record these values. Tuning decisions (SIMD size, LWS, SLM usage) are made by the `gpu-kernel-optimize` skill based on these numbers. + +| Parameter | What It Determines | +|---|---| +| **Device Name** | iGPU vs dGPU, architecture family | +| **Max compute units (EUs)** | Parallelism level needed | +| **Max sub-group size** | SIMD width (8, 16, or 32) | +| **Local memory size (SLM)** | Max tile size for blocked algorithms | +| **Max work group size** | Upper bound for local work size | + +--- + +# Troubleshooting + +- **"clinfo: command not found"**: Install Intel GPU compute runtime or clinfo package +- **No GPU devices listed**: Ensure Intel GPU drivers and OpenCL runtime are installed +- **Multiple devices shown**: Record specs for each device; use `--device_suffix` in tests to target specific GPU +- **Missing sub-group info**: Older drivers may not report sub-group details; assume SIMD 16 as default + +--- + +# References + +- Related skills: `gpu-kernel-enabling`, `gpu-kernel-optimize`, `build-openvino` +- Intel OpenCL documentation: https://www.intel.com/content/www/us/en/developer/tools/opencl/overview.html +- **Next Step:** Proceed to `build-openvino` (Step 2 of `intel-gpu-kernel` workflow) \ No newline at end of file diff --git a/.github/agents-prototype/skills/add-gpu-op/step2-file-structure.md b/.github/agents-prototype/skills/add-gpu-op/step2-file-structure.md new file mode 100644 index 000000000000..668a2dffe7ab --- /dev/null +++ b/.github/agents-prototype/skills/add-gpu-op/step2-file-structure.md @@ -0,0 +1,217 @@ +--- +name: gpu-op-file-structure +description: Files to implement for a new OpenVINO GPU plugin operation — naming conventions, directory locations, and required components +--- + +# Purpose + +Define the exact file locations, naming conventions, and directory structure for implementing a new operation in the OpenVINO GPU plugin. This is the authoritative reference for where every file must be placed. + +# When to Use + +Use this skill whenever creating or modifying files for a GPU plugin operation. The architecture and code inside these files should be directed by the design established in the `plan-op-implementation` skill. Reference this structure from other GPU skills (`gpu-kernel-enabling`, `gpu-kernel-optimize`, etc.) to ensure files are placed correctly. + +```mermaid +flowchart TD + A[New Op: OpName] --> B[snake_case for files] + B --> C[CamelCase for classes] + C --> D[Create files in 6 directories] + D --> E1[1. Kernel Selector] + D --> E2[2. CL Kernels] + D --> E3[3. Primitives] + D --> E4[4. Graph Impls] + D --> E5[5. Plugin Ops] + D --> E6[6. Tests] +``` + +# Procedure + +1. **Step 1: Determine Op Name** — Convert to `snake_case` for files, `CamelCase` for classes +2. **Step 2: Create All Required Files** — In the 6 directories listed below +3. **Step 3: Verify Structure** — Ensure all files are in the correct locations + +--- + +# Prerequisites Check + +Verify the OpenVINO GPU plugin source tree is available: + +**Windows (PowerShell):** +```powershell +Test-Path "src\plugins\intel_gpu\src\kernel_selector\kernels" +``` + +**Ubuntu:** +```bash +test -d src/plugins/intel_gpu/src/kernel_selector/kernels && echo "OK" || echo "MISSING" +``` + +- **If successful:** Proceed to "Quick Start - Main Steps" +- **If failed:** Ensure you are in the OpenVINO source root directory + +--- + +# Quick Start + +## Installation (Prerequisites Check failed) + +Navigate to the OpenVINO source root directory: +```bash +cd /path/to/openvino +``` + +--- + +## Main Steps (Prerequisites Check passed) + +### Naming Conventions + +| Item | Convention | Example (Op: FillEmptyRows) | +|---|---|---| +| File names | `snake_case` | `fill_empty_rows` | +| Class names | `CamelCase` | `FillEmptyRows` | +| Kernel names | `snake_case` | `fill_empty_rows_ref` | +| Directory names | `snake_case` | `fill_empty_rows/` | + +--- + +### 1. Kernel Selector Layer (Host Logic) + +Defines parameters and selects the best kernel implementation. + +**Directory:** `src/plugins/intel_gpu/src/kernel_selector/kernels//` + +**Files to create:** + +| File | Purpose | +|---|---| +| `_kernel_selector.h` | Kernel selector class declaration | +| `_kernel_selector.cpp` | Logic to select between Ref, Opt, or layout-specific kernels | +| `_kernel_base.h` | Base kernel class with parameter/JitConstant definitions | +| `_kernel_base.cpp` | Base kernel implementation | +| `_kernel_ref.h` | Reference kernel binder declaration | +| `_kernel_ref.cpp` | Reference kernel binder implementation | +| `_kernel_opt.h` | *(Optional)* Optimized kernel binder declaration | +| `_kernel_opt.cpp` | *(Optional)* Optimized kernel binder implementation | + +--- + +### 2. OpenCL Kernel Source (Device Code) + +Contains the actual OpenCL C code that runs on the GPU. + +**Directory (ocl_v2):** `src/plugins/intel_gpu/src/graph/impls/ocl_v2/` — co-located with the Graph Impl `.cpp` file. + +**Directory (legacy kernel_selector):** `src/plugins/intel_gpu/src/kernel_selector/cl_kernels/` — used by older ops that have not migrated to ocl_v2. + +**Files to create (in the same directory as the Graph Impl):** + +| File | Purpose | +|---|---| +| `_ref.cl` | Reference OpenCL kernel (clean, no HW-specific optimizations) | +| `_opt.cl` | *(Optional)* General optimized OpenCL kernel | +| `_.cl` | *(Optional)* Layout-specific optimized kernel (e.g., `_bfyx`, `_fsv16`) | + +> **New ops should use the `ocl_v2` co-located structure.** Place `.cl` files next to the `.cpp` Graph Impl file. + +--- + +### 3. Primitive Definition (Internal API) + +Defines the structure used internally by the GPU plugin. + +**Directory:** `src/plugins/intel_gpu/include/intel_gpu/primitives/` + +**Files to create:** + +| File | Purpose | +|---|---| +| `.hpp` | Primitive structure inheriting from `primitive_base` | + +--- + +### 4. Implementation Logic (Graph Registration) + +Registers the primitive to the OpenCL backend with layout validation and kernel selection. + +**Directory:** `src/plugins/intel_gpu/src/graph/impls/ocl_v2/` + +**Files to create:** + +| File | Purpose | +|---|---| +| `.cpp` | `create_` implementation and layout validation | + +--- + +### 5. Operation Translation (Plugin Layer) + +Converts the OpenVINO Core Op (`ov::op::vX`) to the GPU Plugin Primitive. + +**Directory:** `src/plugins/intel_gpu/src/plugin/ops/` + +**Files to create:** + +| File | Purpose | +|---|---| +| `.cpp` | `CreateOp` function using `ProgramBuilder` | + +--- + +### 6. Verification & Tests + +**Functional Single Layer Tests (Shared):** + +**Directory:** `src/plugins/intel_gpu/tests/functional/shared_tests_instances/single_layer_tests/` + +| File | Purpose | +|---|---| +| `.cpp` | Instantiation of shared OpenVINO layer tests | + +**Unit Tests (Internal):** + +**Directory:** `src/plugins/intel_gpu/tests/unit/test_cases/` + +| File | Purpose | +|---|---| +| `_gpu_test.cpp` | gtest cases for edge cases, memory layouts, specific scenarios | + +--- + +### Complete Example: `FillEmptyRows` + +Given: "Implement `FillEmptyRows`" + +| Component | File Path | +|---|---| +| **Kernel Selector** | `src/plugins/intel_gpu/src/kernel_selector/kernels/fill_empty_rows/fill_empty_rows_kernel_selector.h` | +| | `src/plugins/intel_gpu/src/kernel_selector/kernels/fill_empty_rows/fill_empty_rows_kernel_selector.cpp` | +| | `src/plugins/intel_gpu/src/kernel_selector/kernels/fill_empty_rows/fill_empty_rows_kernel_base.h` | +| | `src/plugins/intel_gpu/src/kernel_selector/kernels/fill_empty_rows/fill_empty_rows_kernel_base.cpp` | +| | `src/plugins/intel_gpu/src/kernel_selector/kernels/fill_empty_rows/fill_empty_rows_kernel_ref.h` | +| | `src/plugins/intel_gpu/src/kernel_selector/kernels/fill_empty_rows/fill_empty_rows_kernel_ref.cpp` | +| **Graph Impl + CL Kernel** | `src/plugins/intel_gpu/src/graph/impls/ocl_v2/fill_empty_rows.cpp` | +| | `src/plugins/intel_gpu/src/graph/impls/ocl_v2/fill_empty_rows_ref.cl` | +| **Primitive** | `src/plugins/intel_gpu/include/intel_gpu/primitives/fill_empty_rows.hpp` | +| **Plugin Ops** | `src/plugins/intel_gpu/src/plugin/ops/fill_empty_rows.cpp` | +| **Shared Tests** | `src/plugins/intel_gpu/tests/functional/shared_tests_instances/single_layer_tests/fill_empty_rows.cpp` | +| **Unit Tests** | `src/plugins/intel_gpu/tests/unit/test_cases/fill_empty_rows_gpu_test.cpp` | + +--- + +# Troubleshooting + +- **Kernel not found at runtime**: Verify the `.cl` file is co-located in `ocl_v2/` (or in `cl_kernels/` for legacy ops) and registered in CMakeLists.txt +- **Selector not picking the kernel**: Check that the kernel selector `.cpp` properly adds the kernel implementation +- **Primitive not registered**: Ensure `.cpp` in `graph/impls/ocl_v2/` registers the factory +- **Tests not discovered**: Verify test file is added to the appropriate CMakeLists.txt +- **Name mismatch errors**: Double-check `snake_case` for files and `CamelCase` for classes + +--- + +# References + +- Related skills: `gpu-kernel-enabling`, `gpu-kernel-optimize`, `gpu-integrate-onednn-primitive` +- Previous name: `gpu-file-structure` (renamed per review: this skill is about files-to-implement-for-new-operation) +- OpenVINO GPU plugin source: `src/plugins/intel_gpu/` +- **Next Step:** Proceed to `gpu-kernel-enabling` (Step 4 of `intel-gpu-kernel` workflow) diff --git a/.github/agents-prototype/skills/add-gpu-op/step3-kernel-development.md b/.github/agents-prototype/skills/add-gpu-op/step3-kernel-development.md new file mode 100644 index 000000000000..eb7b1dbef76d --- /dev/null +++ b/.github/agents-prototype/skills/add-gpu-op/step3-kernel-development.md @@ -0,0 +1,164 @@ +--- +name: gpu-kernel-enabling +description: Initial OpenCL kernel creation, debugging, and functional verification for OpenVINO GPU plugin operations +--- + +# Purpose + +Guide the development of new OpenCL kernels for the OpenVINO GPU plugin. This covers creating reference kernels from OpenVINO core reference implementations, dumping kernel sources for inspection, and running functional verification tests. + +# When to Use + +Use this skill when implementing a new GPU operation (Op) that requires an OpenCL kernel, or when debugging an existing kernel. + +```mermaid +flowchart TD + A[New Op Request] --> B[Run collect-gpu-hardware-spec] + B --> C[Create Reference Kernel] + C --> D[Dump & Inspect Kernel Source] + D --> E[Run Functional Tests] + E -->|Pass| F[Proceed to Profiling or Optimization] + E -->|Fail| G[Debug & Fix] + G --> D +``` + +# Procedure + +1. **Step 1: Hardware Context** — Run `collect-gpu-hardware-spec` skill first (if not done) +2. **Step 2: Create Reference Kernel** — Write a clean baseline OpenCL kernel +3. **Step 3: Dump & Inspect** — Verify macro substitution and kernel correctness +4. **Step 4: Functional Verification** — Run unit and functional tests + +--- + +# Prerequisites Check + +Verify the GPU plugin is built (see `build-openvino` skill): + +**Windows (PowerShell):** +```powershell +# Check Debug build exists +Test-Path ".\build\bin\intel64\Debug\ov_gpu_unit_tests.exe" +``` + +**Ubuntu:** +```bash +# Check Debug build exists +test -f ./build/bin/intel64/Debug/ov_gpu_unit_tests && echo "OK" || echo "MISSING" +``` + +- **If successful:** Proceed to "Quick Start - Main Steps" +- **If failed:** Run `build-openvino` in Debug mode with tests enabled first + +--- + +# Quick Start + +## Installation (Prerequisites Check failed) + +Build the GPU plugin first using the `build-openvino` skill in Debug mode. + +--- + +## Main Steps (Prerequisites Check passed) + +### Step 1: Create Reference Kernel + +**Principle:** The reference kernel must be a clean, correct baseline — no hardware-specific optimizations (no sub-group usage, no local memory tiling). + +**Source for logic reference:** +- OpenVINO core reference implementations are in: + `src/core/reference/include/openvino/reference/` +- Use the C++ reference as a **logic reference only**. The OpenCL code must be written from scratch for correct GPU execution. + +**Reference kernel guidelines:** +- Straightforward implementation matching the op specification +- No `intel_sub_group_block_read` or sub-group functions +- No shared local memory (SLM) tiling +- Standard global memory reads/writes only +- Clear, readable code that serves as a correctness baseline + +**File location:** See `gpu-op-file-structure` skill for exact paths. +- Reference kernel → `src/plugins/intel_gpu/src/graph/impls/ocl_v2/_ref.cl` + +### Step 2: Kernel Source Dump & Inspection + +After building, dump the compiled kernel source to verify macro substitution. + +**Windows (PowerShell):** +```powershell +# Enable kernel dumping +$env:OV_GPU_DUMP_SOURCES_PATH = "" + +# Run a test to trigger kernel compilation +.\build\bin\intel64\Debug\ov_gpu_unit_tests.exe --gtest_filter=*TargetOpName* + +# Check dumped .cl files in the working directory +Get-ChildItem -Filter "*.cl" | Sort-Object LastWriteTime -Descending | Select-Object -First 5 +``` + +**Ubuntu:** +```bash +# Enable kernel dumping +export OV_GPU_DUMP_SOURCES_PATH="" + +# Run a test to trigger kernel compilation +./build/bin/intel64/Debug/ov_gpu_unit_tests --gtest_filter=*TargetOpName* + +# Check dumped .cl files +ls -lt *.cl | head -5 +``` + +**Verification checklist:** +- [ ] Macros from `clinfo` are correctly substituted (e.g., `#define SIMD_SIZE 16`) +- [ ] Data types match expected precision (float, half, int) +- [ ] No undefined macros or compilation errors in the dumped source + +### Step 3: Functional Verification + +Run unit tests and functional tests to verify kernel correctness. + +**Windows (PowerShell):** +```powershell +# Unit tests +.\build\bin\intel64\Debug\ov_gpu_unit_tests.exe --gtest_filter=*TargetOpName* --device_suffix=0 + +# Functional tests +.\build\bin\intel64\Debug\ov_gpu_func_tests.exe --gtest_filter=*TargetOpName* --device_suffix=0 +``` + +**Ubuntu:** +```bash +# Unit tests +./build/bin/intel64/Debug/ov_gpu_unit_tests --gtest_filter=*TargetOpName* --device_suffix=0 + +# Functional tests +./build/bin/intel64/Debug/ov_gpu_func_tests --gtest_filter=*TargetOpName* --device_suffix=0 +``` + +**Parameters:** +- `--gtest_filter=*TargetOpName*` — Filter to the specific operation being developed +- `--device_suffix=0` — GPU.0 (first GPU); use `1` for GPU.1 if testing on second device + +**Success criteria:** +- All unit tests pass +- All functional tests pass +- No memory errors (optional: run with address sanitizer) + +--- + +# Troubleshooting + +- **Kernel dump produces empty files**: Ensure `OV_GPU_DUMP_SOURCES_PATH` is set +- **Tests not found for new op**: Ensure test files are created (see `gpu-op-file-structure` skill) +- **Macro substitution errors**: Check that JitConstants in the kernel_base are correctly defined +- **Test crashes**: Run in Debug mode; check kernel source dump for compilation errors +- **device_suffix not working**: Verify GPU device numbering with `clinfo` (device order may vary) +- **Runtime errors**: Check OpenVINO verbose output with `OV_VERBOSE=all` environment variable + +--- + +# References + +- Related skills: `collect-gpu-hardware-spec`, `build-openvino`, `gpu-op-file-structure`, `gpu-kernel-device-timing` +- OpenVINO reference implementations: `src/core/reference/include/openvino/reference/` \ No newline at end of file diff --git a/.github/agents-prototype/skills/add-gpu-op/step3-profiling.md b/.github/agents-prototype/skills/add-gpu-op/step3-profiling.md new file mode 100644 index 000000000000..5d67d5265598 --- /dev/null +++ b/.github/agents-prototype/skills/add-gpu-op/step3-profiling.md @@ -0,0 +1,213 @@ +--- +name: gpu-kernel-device-timing +description: Measure OpenVINO GPU kernel device timing using clintercept and report metrics — reusable utility for baseline measurement and optimization verification +--- + +# Purpose + +Measure OpenCL kernel device timing using the Intel OpenCL Intercept Layer (clintercept) and produce a structured timing report. This is a pure **measurement and analysis** utility — it does not decide whether to optimize or what to optimize. The caller (e.g., `gpu-kernel-enabling`, `gpu-kernel-optimize`) interprets the results. + +# When to Use + +Use this skill whenever you need GPU kernel timing data: +- After ref kernel passes tests → measure **Ref Baseline** +- After opt kernel passes tests → measure **Opt Performance** +- After oneDNN integration → measure **oneDNN path** timing +- During optimization iterations → measure each iteration + +```mermaid +flowchart TD + A[Kernel passes run-gpu-tests] --> B[Profile with clintercept] + B --> C[Collect DeviceTotalTime] + C --> D[Correlate with HW specs] + D --> E[Return timing report to caller] +``` + +# Procedure + +1. **Step 1: Ensure Release Build** — Verify Release binaries exist (see `build-openvino`) +2. **Step 2: Run Profiling** — Collect kernel execution metrics via clintercept +3. **Step 3: Analyze Metrics** — Extract DeviceTotalTime, Call Count, Average Time +4. **Step 4: Correlate with Hardware** — Cross-reference with `collect-gpu-hardware-spec` data +5. **Step 5: Produce Timing Report** — Return structured results to the caller + +--- + +# Prerequisites Check + +Verify clintercept is available: + +**Windows (PowerShell):** +```powershell +# Check if clintercept is available +clintercept --version 2>&1 | Select-Object -First 3 +``` + +**Ubuntu:** +```bash +# Check if clintercept is available +clintercept --version 2>&1 | head -3 +``` + +Also verify Release build exists: + +**Windows (PowerShell):** +```powershell +Test-Path ".\build\bin\intel64\Release\ov_gpu_unit_tests.exe" +``` + +**Ubuntu:** +```bash +test -f ./build/bin/intel64/Release/ov_gpu_unit_tests && echo "OK" || echo "MISSING" +``` + +- **If successful:** Proceed to "Quick Start - Main Steps" +- **If failed:** Install clintercept and/or build in Release mode + +--- + +# Quick Start + +## Installation (Prerequisites Check failed) + +**Install Intel OpenCL Intercept Layer:** + +**Windows (PowerShell):** +```powershell +# Clone and build clintercept +git clone https://github.com/intel/opencl-intercept-layer.git +Push-Location opencl-intercept-layer +mkdir build -Force; Push-Location build +cmake .. +cmake --build . --config Release +Pop-Location; Pop-Location +# Add to PATH or use full path +``` + +**Ubuntu:** +```bash +# Clone and build clintercept +git clone https://github.com/intel/opencl-intercept-layer.git +cd opencl-intercept-layer +mkdir build && cd build +cmake .. +make -j$(nproc) +# Add to PATH or use full path +``` + +**Build Release binaries** by running `build-openvino` with a Release configuration and tests enabled. + +--- + +## Main Steps (Prerequisites Check passed) + +### Step 1: Basic Profiling + +Run clintercept with device timing enabled. Use `benchmark_app` or a dedicated performance test with sufficient iterations for reliable measurement (unit tests typically do not have enough iterations): + +**Windows (PowerShell):** +```powershell +# Device timing with benchmark_app +clintercept -d -t -- benchmark_app -m model.xml -d GPU -niter 100 + +# Or with a specific test (ensure sufficient iterations) +clintercept -d -t -- .\build\bin\intel64\Release\ov_gpu_func_tests.exe --gtest_filter=*TargetOpName* +``` + +**Ubuntu:** +```bash +# Device timing with benchmark_app +clintercept -d -t -- benchmark_app -m model.xml -d GPU -niter 100 + +# Or with a specific test (ensure sufficient iterations) +clintercept -d -t -- ./build/bin/intel64/Release/ov_gpu_func_tests --gtest_filter=*TargetOpName* +``` + +**Flags:** +- `-d` — Device timing (GPU-side timing) +- `-t` — Host timing (CPU-side timing) + +### Step 2: Analyze Key Metrics + +Focus on these metrics from the clintercept output: + +| Metric | What It Tells You | +|---|---| +| **DeviceTotalTime** | Total GPU execution time for the kernel | +| **HostTotalTime** | Total CPU-side overhead (enqueue, sync) | +| **Kernel Name** | Which kernel is being profiled | +| **Call Count** | How many times the kernel was dispatched | +| **Average Time** | Per-invocation GPU time | + +**Analysis questions:** +1. Is `DeviceTotalTime` dominated by one kernel or spread across many? +2. Is the kernel memory-bound (high bandwidth utilization) or compute-bound? +3. Is there excessive host-device synchronization overhead? + +### Step 3: Correlate with Hardware Specs + +Cross-reference profiling results with `collect-gpu-hardware-spec` data: + +| Observation | Hardware Check | Action | +|---|---|---| +| High DeviceTotalTime | Check EU count | Need more parallelism? | +| Low occupancy | Check Max work group size | Work-group too small? | +| Memory bottleneck | Check bandwidth, cache | Use block reads, SLM? | +| Suboptimal SIMD | Check Max sub-group size | Wrong SIMD width? | + +### Step 4: Produce Timing Report + +Return structured timing data to the caller. The caller decides what to do with it. + +```markdown +## Kernel Timing Report: + +| Metric | Value | +|------------------|-----------| +| Kernel Name | | +| DeviceTotalTime | X.XX ms | +| HostTotalTime | X.XX ms | +| Call Count | N | +| Average Time | X.XX ms | +| Hardware | | +| Build | Release | +| Test/App Used | | +``` + +This report is used by: +- `gpu-kernel-enabling` → as **Ref Baseline** +- `gpu-kernel-optimize` → as **Opt measurement** (compared against Ref Baseline) +- `gpu-integrate-onednn-primitive` → as **oneDNN path** timing + +### Step 5: Advanced Profiling (Optional) + +For deeper analysis: + +**Windows (PowerShell):** +```powershell +# Call logging (shows API call sequence) +clintercept --call-logging -- .\build\bin\intel64\Release\ov_gpu_func_tests.exe --gtest_filter=*TargetOpName* +``` + +**Ubuntu:** +```bash +# Call logging (shows API call sequence) +clintercept --call-logging -- ./build/bin/intel64/Release/ov_gpu_func_tests --gtest_filter=*TargetOpName* +``` + +--- + +# Troubleshooting + +- **clintercept not found**: Build from source or add to PATH +- **No GPU timing data**: Ensure `-d` flag is used and GPU drivers support profiling +- **Profiling overhead too high**: Reduce test iterations; profile only the target kernel +- **Results inconsistent**: Run multiple times and average; ensure no other GPU-intensive applications are running +- **Zero DeviceTotalTime reported**: Driver may not support device timing; try updating GPU drivers + +--- + +# References + +- Related skills: `build-openvino`, `run-gpu-tests`, `gpu-kernel-enabling`, `gpu-kernel-optimize`, `gpu-integrate-onednn-primitive`, `collect-gpu-hardware-spec` +- Intel OpenCL Intercept Layer: https://github.com/intel/opencl-intercept-layer diff --git a/.github/agents-prototype/skills/add-gpu-op/step3-run-tests.md b/.github/agents-prototype/skills/add-gpu-op/step3-run-tests.md new file mode 100644 index 000000000000..0dfb55ed6358 --- /dev/null +++ b/.github/agents-prototype/skills/add-gpu-op/step3-run-tests.md @@ -0,0 +1,155 @@ +--- +name: run-gpu-tests +description: Execute GPU plugin tests (unit, functional) and dump kernel sources for verification — reusable utility across all GPU workflow skills +--- + +# Purpose + +Execute pre-written GPU plugin tests and dump kernel sources for inspection. This is a pure **execution and verification** utility — it does not create test code (see `write-gpu-tests` for that). Designed to be called from any GPU workflow skill that needs to run tests. + +# When to Use + +Use this skill whenever you need to **run** existing GPU tests or **dump** kernel sources: +- After writing tests in `gpu-kernel-enabling` flow +- After modifying code in `gpu-opset-migration` flow +- After integrating oneDNN in `gpu-integrate-onednn-primitive` flow +- After optimization in `gpu-kernel-optimize` flow + +```mermaid +flowchart TD + A[Tests written / Code changed] --> B{What to verify?} + B -->|Correctness| C[Run Unit Tests] + B -->|Correctness| D[Run Functional Tests] + B -->|Kernel inspection| E[Dump Kernel Sources] + C --> F[Report Results] + D --> F + E --> F +``` + +# Procedure + +1. **Step 1: Select Mode** — Determine which verification to run (unit, functional, dump, or all) +2. **Step 2: Execute** — Run the selected tests or dump command +3. **Step 3: Report** — Summarize results (pass/fail count, dump file locations) + +--- + +# Prerequisites Check + +Verify that GPU test binaries exist: + +**Windows (PowerShell):** +```powershell +# Check Debug build (for correctness testing) +Test-Path ".\build\bin\intel64\Debug\ov_gpu_unit_tests.exe" +``` + +**Ubuntu:** +```bash +# Check Debug build (for correctness testing) +test -f ./build/bin/intel64/Debug/ov_gpu_unit_tests && echo "OK" || echo "MISSING" +``` + +- **If successful:** Proceed to "Quick Start - Main Steps" +- **If failed:** Run `build-openvino` with Debug configuration and tests enabled + +--- + +# Quick Start + +## Installation (Prerequisites Check failed) + +Build the GPU plugin first by running `build-openvino` with a Debug configuration and tests enabled. + +--- + +## Main Steps (Prerequisites Check passed) + +### Step 1: Select Mode + +Choose which verification to perform based on the caller's needs: + +| Mode | When to Use | Binary | +|---|---|---| +| `unit` | Verify internal GPU primitive logic | `ov_gpu_unit_tests` | +| `functional` | Verify end-to-end Op correctness via SLT | `ov_gpu_func_tests` | +| `dump` | Inspect compiled OpenCL kernel sources | `ov_gpu_unit_tests` + env var | +| `all` | Full verification (dump + unit + functional) | All of the above | + +### Step 2: Run Unit Tests + +**Windows (PowerShell):** +```powershell +.\build\bin\intel64\Debug\ov_gpu_unit_tests.exe --gtest_filter=** --device_suffix=0 +``` + +**Ubuntu:** +```bash +./build/bin/intel64/Debug/ov_gpu_unit_tests --gtest_filter=** --device_suffix=0 +``` + +### Step 3: Run Functional Tests + +**Windows (PowerShell):** +```powershell +.\build\bin\intel64\Debug\ov_gpu_func_tests.exe --gtest_filter=** --device_suffix=0 +``` + +**Ubuntu:** +```bash +./build/bin/intel64/Debug/ov_gpu_func_tests --gtest_filter=** --device_suffix=0 +``` + +### Step 4: Dump Kernel Sources + +**Windows (PowerShell):** +```powershell +$env:OV_GPU_DUMP_SOURCES_PATH = ".\gpu_dump" +.\build\bin\intel64\Debug\ov_gpu_unit_tests.exe --gtest_filter=** +Get-ChildItem -Path .\gpu_dump -Filter "*.cl" | Sort-Object LastWriteTime -Descending | Select-Object -First 5 +``` + +**Ubuntu:** +```bash +export OV_GPU_DUMP_SOURCES_PATH="./gpu_dump" +./build/bin/intel64/Debug/ov_gpu_unit_tests --gtest_filter=** +ls -lt ./gpu_dump/*.cl | head -5 +``` + +**Dump verification checklist:** +- [ ] Macros correctly substituted (e.g., `#define SIMD_SIZE 16`) +- [ ] Data types match expected precision (float, half, int) +- [ ] No undefined macros or compilation errors in dumped source + +### Step 5: Report Results + +Summarize test results for the caller: + +``` +## Test Results: +- Unit tests: X passed / Y total +- Functional tests: X passed / Y total +- Kernel dump: Verified (gpu_dump/.cl) +``` + +**Parameters reference:** +- `--gtest_filter=**` — Filter to the specific operation +- `--device_suffix=0` — GPU.0 (first GPU); use `1` for GPU.1 +- `OV_GPU_DUMP_SOURCES_PATH` — Directory for kernel source dump output + +--- + +# Troubleshooting + +- **Kernel dump produces empty files**: Ensure `OV_GPU_DUMP_SOURCES_PATH` points to a valid writable directory +- **Tests not discovered**: Verify test files exist and are listed in `CMakeLists.txt` (see `write-gpu-tests`) +- **device_suffix not working**: Check GPU device numbering with `clinfo` +- **Test crashes on startup**: Run in Debug mode; check kernel dump for compilation errors +- **Timeout during functional tests**: Some complex ops need longer; ensure sufficient system resources + +--- + +# References + +- Related skills: `write-gpu-tests`, `gpu-kernel-enabling`, `gpu-kernel-optimize`, `gpu-integrate-onednn-primitive`, `gpu-opset-migration`, `build-openvino` +- Test code creation: Use `write-gpu-tests` skill to create the test source files before running this skill \ No newline at end of file diff --git a/.github/agents-prototype/skills/add-gpu-op/step3-write-tests.md b/.github/agents-prototype/skills/add-gpu-op/step3-write-tests.md new file mode 100644 index 000000000000..e5afff22e319 --- /dev/null +++ b/.github/agents-prototype/skills/add-gpu-op/step3-write-tests.md @@ -0,0 +1,219 @@ +--- +name: write-gpu-tests +description: Create Single Layer Tests (SLT) and unit tests for OpenVINO GPU plugin operations — reusable across new op enabling, opset migration, and oneDNN integration +--- + +# Purpose + +Write functional and unit test code for OpenVINO GPU plugin operations. This skill creates the test source files that are later executed by the `run-gpu-tests` skill. It is designed to be reusable across multiple workflows: new op enabling, opset migration, and oneDNN integration. + +# When to Use + +Use this skill whenever test code needs to be **created or updated** for a GPU plugin operation: +- During new op enabling (`gpu-kernel-enabling`) — write tests from scratch +- During opset migration (`gpu-opset-migration`) — add test cases for new version/data types +- During oneDNN integration (`gpu-integrate-onednn-primitive`) — add backend-specific test cases + +```mermaid +flowchart TD + A[plan-op-implementation output] --> B[Determine test scope] + B --> C[Write SLT instantiation] + B --> D[Write unit test cases] + C --> E[Verify file placement] + D --> E + E --> F[run-gpu-tests to execute] +``` + +# Procedure + +1. **Step 1: Determine Test Scope** — Identify parameters, shapes, corner cases from the plan +2. **Step 2: Write Single Layer Tests (SLT)** — Create shared functional test instantiation +3. **Step 3: Write Unit Tests** — Create gtest cases for edge cases and layout-specific scenarios +4. **Step 4: Verify File Placement** — Ensure files are in the correct directories + +--- + +# Prerequisites Check + +Verify the OpenVINO GPU plugin source tree is available: + +**Windows (PowerShell):** +```powershell +Test-Path "src\plugins\intel_gpu\tests\functional\shared_tests_instances\single_layer_tests" +``` + +**Ubuntu:** +```bash +test -d src/plugins/intel_gpu/tests/functional/shared_tests_instances/single_layer_tests && echo "OK" || echo "MISSING" +``` + +- **If successful:** Proceed to "Quick Start - Main Steps" +- **If failed:** Ensure you are in the OpenVINO source root directory + +--- + +# Quick Start + +## Installation (Prerequisites Check failed) + +Navigate to the OpenVINO source root directory: +```bash +cd /path/to/openvino +``` + +--- + +## Main Steps (Prerequisites Check passed) + +### Step 1: Determine Test Scope + +Review the `plan-op-implementation` output (Step 4: SLT Coverage) to identify: + +1. **Test parameters to sweep:** + - Precisions: typically `{f32, f16, i32, i64}` for data, `{i32, i64}` for indices + - Input shapes: small (1×1), medium (batch×channel×H×W), large stress tests + - Attributes: all valid combinations from the Op spec + +2. **Corner cases to cover:** + - Empty tensors (zero-length dimensions) + - Scalar inputs + - Maximum rank tensors + - Boundary values (INT_MAX, negative indices) + - Broadcasting edge cases (if applicable) + +3. **Reference implementation availability:** + - Check if `ov::reference::` exists in `src/core/reference/include/openvino/reference/` + - If missing, flag it — a custom reference must be written + +### Step 2: Write Single Layer Tests (SLT) + +**Directory:** `src/plugins/intel_gpu/tests/functional/shared_tests_instances/single_layer_tests/` +**File:** `.cpp` + +**Reference pattern:** Before writing, read an existing SLT file for a similar operation to follow the established pattern. + +| Op Type | Good Reference SLT | +|---|---| +| Element-wise | `eltwise.cpp` | +| Reduction | `reduce_ops.cpp` | +| Scatter/Gather | `scatter_nd_update.cpp` | +| Shape manipulation | `reshape.cpp` | + +**SLT structure:** +```cpp +// 1. Include the shared test definition header +#include ".hpp" + +// 2. Define the test namespace +namespace { +using namespace ov::test; + +// 3. Define parameter combinations +const std::vector dataPrecisions = { + ov::element::f32, + ov::element::f16, + ov::element::i32, +}; + +const std::vector> inputShapes = { + {{2, 3}}, // small + {{4, 8, 16}}, // medium + {{2, 4, 8, 16}}, // 4D +}; + +// 4. Instantiate the test suite +INSTANTIATE_TEST_SUITE_P( + smoke__GPU, + LayerTest, + ::testing::Combine( + ::testing::ValuesIn(dataPrecisions), + ::testing::ValuesIn(inputShapes), + // ... additional parameters + ::testing::Values(ov::test::utils::DEVICE_GPU) + ), + LayerTest::getTestCaseName +); + +} // namespace +``` + +**Naming conventions:** +- Test suite prefix: `smoke__GPU` for basic coverage +- Extended tests: `extended__GPU` for exhaustive sweeps + +### Step 3: Write Unit Tests + +**Directory:** `src/plugins/intel_gpu/tests/unit/test_cases/` +**File:** `_gpu_test.cpp` + +Unit tests cover scenarios that SLTs may not reach: +- Specific memory layouts (`bfyx`, `b_fs_yx_fsv16`, `b_fs_yx_fsv32`) +- Dynamic shape behavior +- Error handling (invalid inputs) +- Edge cases unique to the GPU implementation + +**Unit test structure:** +```cpp +#include "test_utils.h" +// Include primitive header +#include "intel_gpu/primitives/.hpp" + +using namespace cldnn; +using namespace ::tests; + +TEST(op_name_gpu, basic_2d_f32) { + auto& engine = get_test_engine(); + + // 1. Create input memory + auto input = engine.allocate_memory({ data_types::f32, format::bfyx, { 1, 1, 4, 4 } }); + set_values(input, { /* test data */ }); + + // 2. Build topology + topology topo; + topo.add(input_layout("input", input->get_layout())); + topo.add(("output", input_info("input") /*, attributes */)); + + // 3. Execute and validate + network net(engine, topo); + net.set_input_data("input", input); + auto outputs = net.execute(); + + auto output_mem = outputs.at("output").get_memory(); + cldnn::mem_lock output_ptr(output_mem, get_test_stream()); + + // 4. Check results + std::vector expected = { /* expected values */ }; + for (size_t i = 0; i < expected.size(); ++i) { + ASSERT_NEAR(output_ptr[i], expected[i], 1e-5f); + } +} +``` + +### Step 4: Verify File Placement + +Confirm test files are placed correctly per `gpu-op-file-structure`: + +| Test Type | Path | +|---|---| +| SLT (shared functional) | `src/plugins/intel_gpu/tests/functional/shared_tests_instances/single_layer_tests/.cpp` | +| Unit tests | `src/plugins/intel_gpu/tests/unit/test_cases/_gpu_test.cpp` | + +Ensure the files are included in the appropriate `CMakeLists.txt` so they are compiled during build. + +--- + +# Troubleshooting + +- **Shared test definition header not found**: Check if the Op has a shared test class in `src/tests/functional/shared_test_classes/include/`. If not, it must be created first. +- **Test not discovered after build**: Verify the file is listed in the corresponding `CMakeLists.txt` +- **Reference implementation missing**: If `ov::reference::` does not exist, create one or use a custom evaluator in the test +- **Parameter combination explosion**: Use `smoke_` prefix for essential combinations only; save exhaustive sweeps for `extended_` suites + +--- + +# References + +- Related skills: `gpu-kernel-enabling`, `gpu-opset-migration`, `gpu-integrate-onednn-primitive`, `run-gpu-tests`, `gpu-op-file-structure` +- Test execution: Use `run-gpu-tests` skill to run the tests created by this skill +- OpenVINO shared test infrastructure: `src/tests/functional/shared_test_classes/` +- GPU unit test utilities: `src/plugins/intel_gpu/tests/unit/test_utils/` diff --git a/.github/agents-prototype/skills/add-gpu-op/step4-onednn-integration.md b/.github/agents-prototype/skills/add-gpu-op/step4-onednn-integration.md new file mode 100644 index 000000000000..e0395126a323 --- /dev/null +++ b/.github/agents-prototype/skills/add-gpu-op/step4-onednn-integration.md @@ -0,0 +1,185 @@ +--- +name: gpu-integrate-onednn-primitive +description: Integrate oneDNN primitive into OpenVINO GPU plugin when an existing oneDNN kernel is available for the operation +--- + +# Purpose + +Guide the implementation of a oneDNN-based primitive for the OpenVINO GPU plugin. When a requested operation already has a high-performance oneDNN primitive available, it should be leveraged instead of (or in addition to) writing a custom OpenCL kernel. oneDNN provides optimized implementations for many common deep learning operations on Intel GPUs. + +# When to Use + +Use this skill **after** creating and profiling the reference OpenCL kernel (`gpu-kernel-enabling` + `gpu-kernel-device-timing`), once you decide to integrate a oneDNN primitive for the operation. After integration, use `gpu-kernel-device-timing` to collect timings for both OpenCL and oneDNN paths. + +```mermaid +flowchart TD + A[Op Implementation Request] --> B[Implement & Verify Reference OpenCL Kernel
(gpu-kernel-enabling)] + B --> C[Profile Reference OpenCL Kernel
(gpu-kernel-device-timing)] + C --> D{oneDNN primitive available
& beneficial?} + D -->|Yes| E[Integrate oneDNN Primitive
(this skill)] + D -->|No| F[Keep OpenCL-only Path] + E --> G[Add Kernel Selection & Verify
(OpenCL + oneDNN)] + F --> G + G --> H[Collect Timing for Both Paths
(gpu-kernel-device-timing)] +``` + +# Procedure + +1. **Step 1: Review Op Plan Strategy** — Check if oneDNN integration was recommended in the `plan-op-implementation` strategy for the target Op +2. **Step 2: Create oneDNN Implementation** — Write the oneDNN-based implementation +3. **Step 3: Register in Graph** — Register the oneDNN primitive in the GPU plugin graph +4. **Step 4: Add Kernel Selection Logic** — Enable selection between OpenCL and oneDNN implementations +5. **Step 5: Verify** — Run tests to ensure correctness + +--- + +# Prerequisites Check + +Verify that oneDNN is available in the OpenVINO build: + +**Windows (PowerShell):** +```powershell +# Check whether bundled oneDNN artifacts are present in the build tree +Test-Path ".\build\third-party-programs\onednn" -ErrorAction SilentlyContinue + +# Or check for built dnnl artifacts +Get-ChildItem -Path .\build -Recurse -Include "dnnl*","*onednn*" -ErrorAction SilentlyContinue | Select-Object -First 5 +``` + +**Ubuntu:** +```bash +# Check whether bundled oneDNN artifacts are present in the build tree +test -d ./build/third-party-programs/onednn && echo "OK" || echo "Check build tree" + +# Or check if dnnl/oneDNN libraries exist +find ./build -name "libdnnl*" -o -name "dnnl*" 2>/dev/null | head -5 +``` + +- **If successful:** Proceed to "Quick Start - Main Steps" +- **If failed:** Rebuild using the canonical `build-openvino` workflow and verify bundled oneDNN artifacts are generated + +--- + +# Quick Start + +## Installation (Prerequisites Check failed) + +oneDNN is typically bundled as a third-party dependency in OpenVINO GPU builds. Use the `build-openvino` skill in Debug mode with tests enabled rather than relying on an ad-hoc oneDNN-specific CMake flag. + +--- + +## Main Steps (Prerequisites Check passed) + +### Step 1: Review Op Plan Strategy + +Examine the `plan-op-implementation` results. Before implementing, verify that this operation was identified as a candidate for oneDNN integration. + +**Common oneDNN-supported operations:** +- Convolution (`dnnl::convolution_forward`) +- MatMul / Inner Product (`dnnl::matmul`, `dnnl::inner_product_forward`) +- Pooling (`dnnl::pooling_forward`) +- Batch Normalization (`dnnl::batch_normalization_forward`) +- Eltwise (ReLU, GELU, etc.) (`dnnl::eltwise_forward`) +- Softmax (`dnnl::softmax_forward`) +- Reduction (`dnnl::reduction`) +- Reorder / Reformat (`dnnl::reorder`) +- Binary operations (`dnnl::binary`) + +**Check the oneDNN API reference:** +- https://oneapi-src.github.io/oneDNN/ +- Look for the primitive in `dnnl::primitive_kind` + +**Existing oneDNN implementations in GPU plugin:** +``` +src/plugins/intel_gpu/src/graph/impls/onednn/ +``` + +Review existing implementations for patterns and conventions. + +### Step 2: Create oneDNN Implementation + +**Directory:** `src/plugins/intel_gpu/src/graph/impls/onednn/` +**File:** `_onednn.cpp` + +**Implementation pattern:** + +```cpp +// Key components for a oneDNN implementation: + +// 1. Create oneDNN primitive descriptor +// - Map OpenVINO tensor descriptors to oneDNN memory descriptors +// - Configure the primitive attributes (post-ops, scales, etc.) + +// 2. Create primitive +// - Instantiate the oneDNN primitive from the descriptor +// - Handle memory format selection + +// 3. Execute +// - Map GPU plugin memory to oneDNN memory objects +// - Execute the primitive +// - Handle output memory reorder if needed +``` + +**Key considerations:** +- Map `cldnn` layout formats to oneDNN memory formats +- Handle data type conversions (fp32, fp16, bf16, int8) +- Support post-operation fusion when applicable +- Ensure proper memory lifecycle management + +### Step 3: Register in GPU Plugin Graph + +Register the oneDNN implementation so the GPU plugin can discover and use it: + +```cpp +// Registration pattern (simplified): +// 1. Implement the typed_primitive_onednn_impl class +// 2. Register the implementation factory +// 3. Add layout and data type validation +``` + +### Step 4: Add Kernel Selection Logic + +The GPU plugin needs logic to choose between OpenCL and oneDNN implementations. + +**Selection criteria:** +- Hardware support (oneDNN may only support certain GPU architectures) +- Data type availability (oneDNN may support int8 quantization that OpenCL ref doesn't) +- Layout compatibility (oneDNN may prefer specific blocked layouts) +- Performance characteristics (oneDNN is often faster for standard ops) + +**File to modify:** Kernel selector or implementation factory registration. + +### Step 5: Functional Verification + +If oneDNN-specific test cases are needed (e.g., backend selection, int8 quantization paths), invoke `write-gpu-tests` to add them first. + +Then invoke `run-gpu-tests` skill to verify the oneDNN integration: +- Run unit tests and functional tests filtered to the target op +- Verify both OpenCL and oneDNN paths produce correct results + +**Verification checklist:** +- [ ] All existing tests still pass (no regression) +- [ ] oneDNN path is selected when expected +- [ ] Results match the reference OpenCL kernel within acceptable tolerance +- [ ] Performance is equal or better than OpenCL reference (verify via `gpu-kernel-device-timing`) + +--- + +# Troubleshooting + +- **oneDNN primitive not found**: Verify the op is supported in the oneDNN version bundled with OpenVINO +- **Memory format mismatch**: Ensure proper mapping between `cldnn` layouts and `dnnl::memory::format_tag` +- **Accuracy regression**: Check data type handling, especially for fp16/bf16 operations +- **oneDNN path not selected**: Verify the implementation factory registration and selection priority +- **Build errors**: Verify the canonical `build-openvino` workflow completed successfully and that bundled dnnl/oneDNN artifacts were generated in the build tree +- **Runtime errors**: Check oneDNN verbose output with `ONEDNN_VERBOSE=1` environment variable + +--- + +# References + +- Related skills: `gpu-kernel-enabling`, `gpu-kernel-device-timing`, `run-gpu-tests`, `write-gpu-tests`, `gpu-op-file-structure` +- Canonical build workflow: `build-openvino` +- oneDNN API reference: https://oneapi-src.github.io/oneDNN/ +- Existing oneDNN implementations: `src/plugins/intel_gpu/src/graph/impls/onednn/` +- oneDNN GPU support: https://oneapi-src.github.io/oneDNN/dev_guide_understanding_memory_formats.html diff --git a/.github/agents-prototype/skills/add-gpu-op/step5-optimize.md b/.github/agents-prototype/skills/add-gpu-op/step5-optimize.md new file mode 100644 index 000000000000..b79dd482b318 --- /dev/null +++ b/.github/agents-prototype/skills/add-gpu-op/step5-optimize.md @@ -0,0 +1,254 @@ +--- +name: gpu-kernel-optimize +description: Hardware-aware optimization techniques for Intel GPU OpenCL kernels including sub-groups, memory layout, LWS tuning, and register pressure +--- + +# Purpose + +Apply hardware-aware optimizations to OpenCL kernels for Intel GPUs. This skill covers sub-group size selection, memory layout with block reads, local work size (LWS) tuning, and register pressure management — all based on hardware data from `collect-gpu-hardware-spec`. + +# When to Use + +Use this skill as **Step 5 (mandatory)** of the `intel-gpu-kernel` workflow to create an optimized kernel and produce a Performance Comparison Report. The optimizations here are guided by profiling data from `gpu-kernel-device-timing` and hardware specifications from `collect-gpu-hardware-spec`. + +```mermaid +flowchart TD + A[Profiling Shows Bottleneck] --> B[Review HW Specs] + B --> C{Bottleneck Type?} + C -->|SIMD Utilization| D[Sub-group Size Tuning] + C -->|Memory Bandwidth| E[Block Reads & Layout] + C -->|Occupancy| F[LWS Tuning] + C -->|Register Spilling| G[Register Pressure Mgmt] + D --> H[Create Optimized Kernel] + E --> H + F --> H + G --> H + H --> I[Re-profile to Verify] +``` + +# Procedure + +1. **Step 1: Review Hardware Baseline** — Ensure `collect-gpu-hardware-spec` data is available +2. **Step 2: Identify Bottleneck** — Use profiling data to determine optimization target +3. **Step 3: Apply Optimization** — Choose and implement the appropriate technique +4. **Step 4: Create Optimized Kernel** — Write `_opt.cl` with hardware-specific tuning +5. **Step 5: Re-profile** — Verify performance improvement + +--- + +# Prerequisites Check + +Verify you have hardware analysis data and profiling results: + +**Windows (PowerShell):** +```powershell +# Verify clinfo is available for hardware data +clinfo | Select-String "Device Name" + +# Verify Release build exists for profiling +Test-Path ".\build\bin\intel64\Release\ov_gpu_unit_tests.exe" +``` + +**Ubuntu:** +```bash +# Verify clinfo is available for hardware data +clinfo | grep "Device Name" + +# Verify Release build exists for profiling +test -f ./build/bin/intel64/Release/ov_gpu_unit_tests && echo "OK" || echo "MISSING" +``` + +- **If successful:** Proceed to "Quick Start - Main Steps" +- **If failed:** Run `collect-gpu-hardware-spec`, then run `build-openvino` with a Release configuration and tests enabled + +--- + +# Quick Start + +## Installation (Prerequisites Check failed) + +1. Run `collect-gpu-hardware-spec` skill to collect hardware specs +2. Run `build-openvino` with a Release configuration and tests enabled +3. Run `gpu-kernel-device-timing` skill to identify bottlenecks + +--- + +## Main Steps (Prerequisites Check passed) + +### Step 1: Prepare Profiling Test Case + +Before applying any optimization, prepare a dedicated test case for performance measurement. Unit tests typically do not have enough iterations for reliable profiling. + +**Recommended approach:** +- Use `benchmark_app` with a model that exercises the target kernel +- Or create a dedicated performance test with sufficient iterations (e.g., 100+ runs) +- Record baseline DeviceTotalTime using `gpu-kernel-device-timing` skill + +### Step 2–3: Identify Bottleneck & Apply Optimization + +### A. Dynamic Sub-group Size Selection + +Sub-group size directly affects SIMD width and performance. + +**Selection rules based on `Max sub-group size` from clinfo:** + +| Max Sub-group Size | Recommended SIMD | Notes | +|---|---|---| +| 16 or 32 | 16 | Use 16 as default for all architectures to keep codebase simple and portable | +| 8 | 8 | Rare; fallback only for legacy hardware | + +**Implementation:** +```c +// In OpenCL kernel — set sub-group size based on hardware +__attribute__((intel_reqd_sub_group_size(SIMD_SIZE))) +__kernel void my_kernel(...) { + // SIMD_SIZE is defined via JitConstants based on clinfo data + ... +} +``` + +**In kernel selector (JitConstants):** +```cpp +// Define SIMD_SIZE based on hardware detection +jit.AddConstant(MakeJitConstant("SIMD_SIZE", GetSimdSize(params))); +``` + +### B. Memory Layout & Block Reads (fsv16) + +Maximize memory bandwidth using Intel sub-group block reads. + +**Rules:** +- Use `intel_sub_group_block_read` — reads `4 bytes × SIMD_SIZE` in one operation +- Requires pointers aligned to at least 16 bytes +- Align to 128 bytes (cache line) when possible for optimal prefetching + +**Implementation:** +```c +// Block read pattern — much faster than scalar global memory access +uint raw = intel_sub_group_block_read((__global uint*)(input + offset)); +float value = as_float(raw); + +// For fp16 data: +ushort raw_h = intel_sub_group_block_read_us((__global ushort*)(input + offset)); +half value_h = as_half(raw_h); +``` + +**Alignment requirements:** + +| Minimum Alignment | Optimal Alignment | +|---|---| +| 16 bytes | 128 bytes (cache line) | + +### C. Local Work Size (LWS) Tuning + +Optimize work-group size for maximum GPU occupancy. + +**Rules:** +1. LWS must be a multiple of `SIMD_SIZE` +2. `Ideal LWS = min(Max work group size, SLM_Constraint)` +3. General preference: LWS of 256 works well across most Intel GPU architectures + +**Implementation in kernel selector:** +```cpp +// Set LWS based on hardware +CommonDispatchData dispatchData; +dispatchData.gws = { global_x, global_y, global_z }; +dispatchData.lws = { lws_x, 1, 1 }; // lws_x = multiple of SIMD_SIZE +``` + +**SLM constraint calculation:** +``` +Max LWS from SLM = SLM_size / per_workitem_SLM_usage +Actual LWS = min(Max_work_group_size, Max_LWS_from_SLM) +Actual LWS = round_down_to_multiple(Actual_LWS, SIMD_SIZE) +``` + +### D. Register Pressure Management + +Manage register usage to avoid spilling, especially on smaller GPUs. + +**Warning signs:** +- Kernel has many local variables +- Complex control flow with deep nesting +- Multiple large arrays in private memory + +**Strategies:** + +| Strategy | When to Apply | Impact | +|---|---|---| +| Use `half` (fp16) precision | `cl_khr_fp16` supported, acceptable accuracy | Halves register usage | +| Reduce loop unrolling | High register pressure | Fewer registers, slightly slower | +| Split large kernels | Many variables in single kernel | Better register allocation | +| Use SLM instead of registers | Data reuse across work-items | Trades register for SLM | + +**Implementation — fp16 precision:** +```c +// Use half precision to reduce register pressure +#ifdef cl_khr_fp16 +#pragma OPENCL EXTENSION cl_khr_fp16 : enable +// Use half instead of float where acceptable +half value = convert_half(input_float); +#endif +``` + +### Step 4: Create Optimized Kernel + +After applying optimizations, create the optimized kernel file: + +**File:** `src/plugins/intel_gpu/src/graph/impls/ocl_v2/_opt.cl` + +The optimized kernel should: +- Include all hardware-specific optimizations identified above +- Be conditionally selected in the kernel selector based on hardware capabilities +- Coexist with the reference kernel (`_ref.cl`) as a fallback + +**Kernel selector update:** +```cpp +// In kernel selector — choose optimized kernel when hardware supports it +if (SupportsOptimizedKernel(params)) { + implementations.push_back(std::make_unique()); +} +implementations.push_back(std::make_unique()); // Always fallback +``` + +### Step 5: Verify & Re-profile + +After creating the optimized kernel: + +1. **Verify correctness:** Invoke `run-gpu-tests` skill to run unit and functional tests, ensuring the optimized kernel produces correct results. + +2. **Measure performance:** Invoke `gpu-kernel-device-timing` skill to collect Opt kernel timing. + +3. **Produce Performance Comparison Report:** + +```markdown +## Performance Comparison Report: + +| Kernel Variant | DeviceTotalTime | Speedup vs Ref | +|----------------------|-----------------|----------------| +| Ref (`_ref.cl`) | X.XX ms | 1.00× (baseline) | +| Opt (`_opt.cl`) | Y.YY ms | X.XX / Y.YY = N.NN× | +| oneDNN (if applicable)| Z.ZZ ms | X.XX / Z.ZZ = N.NN× | + +- Test shape: [batch, channels, height, width] +- Hardware: Intel (from collect-gpu-hardware-spec) +- Optimizations applied: +``` + +--- + +# Troubleshooting + +- **No performance improvement**: Bottleneck may be elsewhere (host-side, memory transfer); re-analyze profiling data +- **Kernel compilation fails with sub-group**: Verify `SIMD_SIZE` matches hardware support from `clinfo` +- **Block read crashes**: Check pointer alignment — must be aligned to at least 16 bytes +- **Register spilling warnings**: Reduce variable count, use `half` precision, or split kernel +- **LWS rejected by driver**: Ensure LWS divides GWS evenly and does not exceed `Max work group size` + +--- + +# References + +- Related skills: `collect-gpu-hardware-spec`, `gpu-kernel-device-timing`, `run-gpu-tests`, `gpu-kernel-enabling`, `gpu-op-file-structure` +- Intel GPU optimization guide: https://www.intel.com/content/www/us/en/developer/articles/guide/opencl-developers-guide.html +- Sub-group functions: https://registry.khronos.org/OpenCL/extensions/intel/cl_intel_subgroups.html diff --git a/.github/agents-prototype/skills/analyze-and-convert/SKILL.md b/.github/agents-prototype/skills/analyze-and-convert/SKILL.md new file mode 100644 index 000000000000..379b7af13c0a --- /dev/null +++ b/.github/agents-prototype/skills/analyze-and-convert/SKILL.md @@ -0,0 +1,22 @@ +--- +name: analyze-and-convert +description: Analyze a HuggingFace model and attempt OpenVINO conversion — probe properties, run strategy matrix, classify failures, and produce a structured routing report. +--- + +# Skill: Analyze and Convert + +## When to use +When you have a model ID (or local path) and need to determine whether it converts +to OpenVINO IR, identify why it fails, and emit routing signals for the next agent +in the pipeline. + +## Steps + +Execute in strict order — each step produces files consumed by the next. + +| Step | File | Purpose | +|---|---|---| +| 1 | [probe-model.md](probe-model.md) | Gather model profile (architecture, task, op types) without downloading weights | +| 2 | [try-conversion.md](try-conversion.md) | Run conversion strategy matrix (optimum-cli / ovc / convert_model), capture all outputs | +| 3 | [classify-failure.md](classify-failure.md) | Map errors to taxonomy, extract `missing_op` / `shape_inference` / `accuracy` signals (skip on full success) | +| 4 | [build-report.md](build-report.md) | Assemble structured report, emit `` marker for orchestrator | diff --git a/.github/agents-prototype/skills/analyze-and-convert/build-report.md b/.github/agents-prototype/skills/analyze-and-convert/build-report.md new file mode 100644 index 000000000000..051a1037086d --- /dev/null +++ b/.github/agents-prototype/skills/analyze-and-convert/build-report.md @@ -0,0 +1,75 @@ +--- +name: build-report +description: Combine all gathered artifacts into a structured conversion_report.md and a machine-readable agent-complete marker. Posts the report to the GitHub issue and writes the manifest entry. +--- + +# Skill: Build Report + +**Trigger:** Final step of the `analyze-and-convert` workflow. Always runs — +regardless of whether conversion succeeded or failed. + +Inputs: `model_profile.json`, `conversion_attempts.json`, `routing_signals.json`, +`error_excerpts.json` (all produced by previous skills). + +--- + +## Step 1 — Load all artifacts + +Run `build_report.py` to load artifacts and compute status: + +```bash +python build_report.py +``` + +Script: [build_report.py](./build_report.py) + +--- + +## Step 2 — Write conversion_report.md + +Script: [build_report.py](./build_report.py) — generates `conversion_report.md` with: + +- Model profile table +- Conversion attempts summary +- Successful strategy details (if applicable) +- Failure details and error excerpts (if applicable) +- Routing signals summary +- Recommended next step and agent routing + +--- + +## Step 3 — Post to GitHub Issue (if gh CLI available) + +Script: [build_report.py](./build_report.py) — posts `conversion_report.md` as a PR comment using `gh pr comment` if available. + +--- + +## Step 4 — Record Result + +Script: [build_report.py](./build_report.py) — writes `analyze_and_convert_result.json` with: + +- Agent name and status (success/partial/failed) +- Model ID and entry type +- Error classification and target agent routing + +--- + +## Step 5 — Write agent-complete Marker + +Script: [build_report.py](./build_report.py) — prints standard `` marker (HTML comment) with: + +- `agent`: "analyze-and-convert" +- `status`: "success" | "partial" | "failed" +- `next_agent`: target agent for routing (wwb, optimum-intel, etc.) +- `error_class`: error classification (none, oom_suspected, custom_ops, etc.) +- `next_context`: filtered signals for next agent +- `model_id`: original model identifier + +--- + +## Output + +| File | Contents | +|------|----------| +| `conversion_report.md` | Full human-readable report — saved to agent-results/ | +| `agent-complete` marker (stdout) | Machine-readable routing block for `dispatcher.yml` | diff --git a/.github/agents-prototype/skills/analyze-and-convert/classify-failure.md b/.github/agents-prototype/skills/analyze-and-convert/classify-failure.md new file mode 100644 index 000000000000..a287bb09d97f --- /dev/null +++ b/.github/agents-prototype/skills/analyze-and-convert/classify-failure.md @@ -0,0 +1,294 @@ +--- +name: classify-failure +description: Classify each failed conversion attempt into the 9-class error taxonomy and extract routing signals for the orchestrator. Maps tracebacks to root causes, identifies specific components, and produces a machine-readable signal block. +--- + +# Skill: Classify Failure + +**Trigger:** Called after `try-conversion` when no strategy succeeded, or after +a successful conversion that fails inference. Reads `conversion_attempts.json` +and `model_profile.json`. + +--- + +## Error Taxonomy + +Map every failure traceback to exactly one class: + +| Class | Key signatures | First specialist | +|-------|---------------|-----------------| +| `optimum_unsupported_arch` | `KeyError` from `TasksManager`, `model_type` not registered | Optimum-Intel | +| `optimum_export_bug` | Exception inside `optimum/exporters/`, wrongly-shaped dummy inputs, `TypeError` in model config | Optimum-Intel | +| `missing_model_dependency` | `ModuleNotFoundError`, `ImportError: requires package`, `pip install` hint in error | Optimum-Intel | +| `missing_conversion_rule` | `NotImplemented` in PyTorch FE, `aten::*` op not covered, `ConversionError: No rule` | OV Orchestrator (FE Agent) | +| `frontend_error` | Exception inside `openvino/frontend/`, segfault during tracing, IR parse error | OV Orchestrator (FE Agent) | +| `ir_validation_error` | Shape inference failure, `ngraph::validate_and_infer_types`, invalid IR structure | OV Orchestrator | +| `inference_runtime_error` | Exception from OV runtime, plugin error, `ov::Exception` during infer | OV Orchestrator | +| `genai_unsupported` | `ValueError` from `openvino_genai`, missing chat template, pipeline construction error | GenAI | +| `tokenizer_error` | Exception from `openvino_tokenizers`, tokenizer conversion failure, SentencePiece error | Tokenizers | +| `unknown_arch_transformers_too_old` | `model_type` absent from installed transformers but visible on HF Hub | Optimum-Intel (transformers upgrade first) | +| `unknown` | None of the above match | Optimum-Intel (fallback) | + +--- + +## Step 1 — Load Artifacts + +```python +import json, re + +with open("conversion_attempts.json") as f: + attempts = json.load(f) + +with open("model_profile.json") as f: + profile = json.load(f) + +# Collect all error text from failed attempts +all_errors = [] +for a in attempts: + if not a["success"]: + combined = (a.get("stderr", "") + "\n" + a.get("stdout", "")).strip() + all_errors.append({"id": a["id"], "text": combined}) +``` + +--- + +## Step 2 — Pattern Matching + +```python +def classify_error(text: str) -> str: + t = text.lower() + + # Order matters — most specific first + if re.search(r"modulenotfounderror|importerror.*requires.*package|no module named", t): + return "missing_model_dependency" + + if re.search(r"keyerror.*taskmanager|model_type.*not.*support|no configuration class", t): + # Distinguish: is model_type missing from transformers or from optimum? + if re.search(r"no.*class.*for.*model_type|transformers.*doesn.t.*know", t): + return "unknown_arch_transformers_too_old" + return "optimum_unsupported_arch" + + if re.search(r"optimum[/\\]exporters|dummy_inputs|onnx_config|ModelPatcher|" + r"from_pretrained.*export|openvino_config", t): + return "optimum_export_bug" + + if re.search(r"notimplemented.*aten::|no rule.*op|conversion.*failed.*aten|" + r"pytorch_frontend|pt_frontend|torchscript.*error", t): + return "missing_conversion_rule" + + if re.search(r"openvino[/\\]frontend|ir.*parse|frontend.*error|" + r"segmentation fault|core dumped", t): + return "frontend_error" + + if re.search(r"validate_and_infer_types|ngraph.*shape|ir.*invalid|" + r"shape inference failed|opset.*mismatch", t): + return "ir_validation_error" + + if re.search(r"ov::exception|infer.*request|plugin.*error|" + r"openvino.*runtime.*error|inference engine", t): + return "inference_runtime_error" + + if re.search(r"openvino_genai|chat.*template.*missing|pipeline.*construct", t): + return "genai_unsupported" + + if re.search(r"openvino_tokenizers|sentencepiece|tokenizer.*convert|" + r"tiktoken.*error|fast.*tokenizer", t): + return "tokenizer_error" + + return "unknown" + + +# Classify each failed attempt +for err in all_errors: + err["error_class"] = classify_error(err["text"]) + # Extract first meaningful traceback line + lines = err["text"].splitlines() + tb_lines = [l for l in lines if + "Error" in l or "Exception" in l or "raise " in l] + err["key_line"] = tb_lines[0].strip() if tb_lines else lines[-1].strip() if lines else "" + +# Dominant class = class of the last (most sophisticated) attempt +dominant_class = all_errors[-1]["error_class"] if all_errors else "unknown" +print(f"Dominant error class: {dominant_class}") +``` + +--- + +## Step 2.5 — Search Git History for Similar Fixes + +Before generating routing signals, search the repository's git log for recent commits that +solved the same class of problem. This gives the downstream coding agent a prior-art hint, +preventing repeated workarounds for problems that already have a canonical fix. + +```python +import subprocess + +def search_prior_art(error_class: str, op_name: str) -> list: + """Return up to 10 recent commit one-liners relevant to this error class.""" + term_map = { + "missing_conversion_rule": [op_name, "add op translator", "add converter", "frontend: add"], + "accuracy_regression": [op_name, "fp16 overflow", "precision fix", "accuracy", "fp16"], + "frontend_error": [op_name, "frontend: fix", "normalize", "conversion fix"], + "inference_runtime_error": [op_name, "cpu: fix", "plugin fix", "inference"], + "ir_validation_error": [op_name, "shape inference", "type prop", "core: fix"], + } + terms = term_map.get(error_class, [op_name]) + seen, results = set(), [] + for term in terms[:4]: + try: + out = subprocess.check_output( + ["git", "log", "--oneline", "-15", f"--grep={term}"], + text=True, stderr=subprocess.DEVNULL, + ) + for line in out.strip().splitlines(): + if line and line not in seen: + seen.add(line) + results.append({"term": term, "commit": line}) + except (subprocess.CalledProcessError, FileNotFoundError): + pass + return results[:10] + +op_name = profile.get("missing_ops", [""])[0] if profile.get("missing_ops") else "" +prior_art = search_prior_art(dominant_class, op_name) + +with open("prior_art.json", "w") as f: + json.dump({"error_class": dominant_class, "op": op_name, "commits": prior_art}, f, indent=2) + +if prior_art: + print("Prior art found:") + for entry in prior_art: + print(f" [{entry['term']}] {entry['commit']}") +else: + print("No prior art found in git history — proceeding without hints.") +``` + +The resulting `prior_art.json` is included in the routing report and passed to the downstream +coding agent as context. The agent should use it to confirm the canonical fix pattern rather +than implementing from scratch. +``` + +--- + +## Step 3 — Extract Routing Signals + +```python +def extract_signals(attempts, profile, dominant_class): + signals = { + "error_class": dominant_class, + "requires_optimum_new_arch": False, + "requires_transformers_upgrade": False, + "transformers_override": "", + "requires_tokenizer_check": False, + "trust_remote_code_required": profile["trust_remote_code_required"], + "is_vlm": profile["is_vlm"], + "custom_ops_suspected": False, + "oom_suspected": False, + "target_agent": "", # filled below + } + + all_text = " ".join( + a.get("stderr", "") + a.get("stdout", "") for a in attempts + ).lower() + + # Optimum arch signals + if dominant_class in ("optimum_unsupported_arch", "unknown_arch_transformers_too_old"): + signals["requires_optimum_new_arch"] = True + + if dominant_class == "unknown_arch_transformers_too_old": + signals["requires_transformers_upgrade"] = True + signals["transformers_override"] = ( + "git+https://github.com/huggingface/transformers.git" + ) + + # Custom ops / recurrent patterns in config + ssm_keys = [k for k in profile.get("special_config_keys", []) + if any(w in k.lower() for w in + ["ssm", "mamba", "rwkv", "recurrent", "conv", "delta"])] + if ssm_keys: + signals["custom_ops_suspected"] = True + + # OOM signals + if re.search(r"out of memory|oom|killed|memory error|cannot allocate", all_text): + signals["oom_suspected"] = True + + # VLM implies tokenizer check after export + if profile["is_vlm"]: + signals["requires_tokenizer_check"] = True + + # Determine target agent + routing = { + "optimum_unsupported_arch": "optimum-intel", + "optimum_export_bug": "optimum-intel", + "missing_model_dependency": "optimum-intel", + "unknown_arch_transformers_too_old": "optimum-intel", + "unknown": "optimum-intel", + "missing_conversion_rule": "enable-operator", + "frontend_error": "enable-operator", + "ir_validation_error": "enable-operator", + "inference_runtime_error": "enable-operator", + "genai_unsupported": "openvino-genai", + "tokenizer_error": "openvino-tokenizers", + } + signals["target_agent"] = routing.get(dominant_class, "optimum-intel") + + return signals + + +signals = extract_signals(attempts, profile, dominant_class) +print("Routing signals:", json.dumps(signals, indent=2)) + +with open("routing_signals.json", "w") as f: + json.dump(signals, f, indent=2) +``` + +--- + +## Step 4 — Extract the Key Error Excerpt + +For each failed attempt, extract the most diagnostic ~20 lines to include in +the report — the traceback tail plus context lines before the final exception. + +```python +def extract_excerpt(text: str, max_lines: int = 20) -> str: + lines = text.splitlines() + # Find last traceback block + tb_start = max( + (i for i, l in enumerate(lines) if "Traceback (most recent call last)" in l), + default=0 + ) + excerpt = lines[tb_start:] + return "\n".join(excerpt[-max_lines:]) + + +excerpts = {} +for err in all_errors: + excerpts[err["id"]] = extract_excerpt(err["text"]) + +with open("error_excerpts.json", "w") as f: + json.dump(excerpts, f, indent=2) + +print(f"Excerpts saved for {len(excerpts)} failed attempt(s).") +``` + +--- + +## Output + +| File | Contents | +|------|----------| +| `routing_signals.json` | Machine-readable signals for orchestrator routing | +| `error_excerpts.json` | Key traceback excerpts per attempt | +| `prior_art.json` | Recent git commits that fixed similar problems (empty list if none found) | + +Key signal fields consumed by orchestrator: + +| Signal | Meaning | +|--------|---------| +| `error_class` | 11-value taxonomy class | +| `target_agent` | Recommended first specialist | +| `requires_optimum_new_arch` | Optimum-Intel must add full model support | +| `requires_transformers_upgrade` | Transformers version is the root cause | +| `transformers_override` | Pip install string for git-HEAD transformers | +| `custom_ops_suspected` | SSM/recurrent ops detected — likely needs `_ov_ops.py` | +| `is_vlm` | Vision-language model — extra routing needed | +| `oom_suspected` | OOM hit — suggest int4 or sharded export | diff --git a/.github/agents-prototype/skills/analyze-and-convert/probe-model.md b/.github/agents-prototype/skills/analyze-and-convert/probe-model.md new file mode 100644 index 000000000000..dbeee918e6d7 --- /dev/null +++ b/.github/agents-prototype/skills/analyze-and-convert/probe-model.md @@ -0,0 +1,200 @@ +--- +name: probe-model +description: Probe a HuggingFace model to collect its full profile — architecture class, parameter count, special requirements, tokenizer type, and known issues — before attempting conversion. +--- + +# Skill: Probe Model + +**Trigger:** Always the first step in the `analyze-and-convert` workflow. +Produces `model_profile.json` and `probe_report.md`, consumed by all subsequent skills. + +--- + +## Step 1 — Fetch HuggingFace Metadata + +```python +import json, requests + +MODEL_ID = "" +HF_API = "https://huggingface.co/api/models" + +# Model card / metadata +resp = requests.get(f"{HF_API}/{MODEL_ID}", timeout=30) +meta = resp.json() if resp.ok else {} + +print("pipeline_tag :", meta.get("pipeline_tag")) +print("library_name :", meta.get("library_name")) +print("tags :", meta.get("tags", [])) +print("card_data :", json.dumps(meta.get("cardData", {}), indent=2)) +``` + +## Step 2 — Load and Inspect Config + +```python +from transformers import AutoConfig + +cfg = AutoConfig.from_pretrained(MODEL_ID, trust_remote_code=False) +cfg_dict = cfg.to_dict() + +print("model_type :", cfg.model_type) +print("architectures :", cfg.architectures) +print("hidden_size :", getattr(cfg, "hidden_size", "N/A")) +print("num_hidden_layers :", getattr(cfg, "num_hidden_layers", "N/A")) +print("num_attention_heads :", getattr(cfg, "num_attention_heads", "N/A")) +print("num_key_value_heads :", getattr(cfg, "num_key_value_heads", "N/A")) +print("vocab_size :", getattr(cfg, "vocab_size", "N/A")) +print("max_position_embeds :", getattr(cfg, "max_position_embeddings", "N/A")) + +# Detect special architecture markers +special_keys = [k for k in cfg_dict if any(kw in k.lower() for kw in [ + "layer_type", "ssm", "mamba", "rwkv", "moe", "expert", "conv", + "recurrent", "delta", "vision", "image", "video", "mm_", "vlm", + "audio", "cross_attn", "sliding", "hybrid", "flash", "rope", + "alibi", "custom_code", +])] +for k in special_keys: + print(f" {k}: {cfg_dict[k]}") +``` + +## Step 3 — Estimate Parameter Count + +```python +from transformers import AutoConfig + +# Rough estimation from config (no weights download needed) +def estimate_params(cfg): + h = getattr(cfg, "hidden_size", 0) + layers = getattr(cfg, "num_hidden_layers", 0) + vocab = getattr(cfg, "vocab_size", 0) + intermediate = getattr(cfg, "intermediate_size", h * 4) + heads = getattr(cfg, "num_attention_heads", 1) + kv_heads = getattr(cfg, "num_key_value_heads", heads) + + # attn (Q + K + V + O) + attn = h * h + (kv_heads * (h // heads)) * 2 * h + h * h + # ffn (gate + up + down for LLaMA-style, or 2-linear for BERT-style) + ffn = h * intermediate * 3 if hasattr(cfg, "mlp_bias") else h * intermediate * 2 + # embeddings + embed = vocab * h * 2 # input + output (tied) + per_layer = attn + ffn + total = layers * per_layer + embed + return total + +cfg = AutoConfig.from_pretrained(MODEL_ID, trust_remote_code=False) +est = estimate_params(cfg) +print(f"Estimated params: ~{est/1e9:.1f}B") +``` + +## Step 4 — Check trust_remote_code Requirement + +```python +# Try loading without trust_remote_code first +try: + from transformers import AutoConfig + AutoConfig.from_pretrained(MODEL_ID, trust_remote_code=False) + trust_remote_code_required = False +except Exception as e: + if "trust_remote_code" in str(e) or "custom code" in str(e).lower(): + trust_remote_code_required = True + else: + trust_remote_code_required = False # different error +print("trust_remote_code required:", trust_remote_code_required) +``` + +## Step 5 — Inspect Tokenizer Config + +```python +from transformers import AutoTokenizer +import json + +try: + tok = AutoTokenizer.from_pretrained(MODEL_ID, trust_remote_code=True) + print("tokenizer_class :", type(tok).__name__) + print("is_fast :", tok.is_fast) + print("vocab_size :", tok.vocab_size) + print("chat_template :", "present" if tok.chat_template else "absent") +except Exception as e: + print(f"Tokenizer load error: {e}") +``` + +## Step 6 — Detect Multimodal / VLM Signals + +```python +from transformers import AutoConfig + +cfg = AutoConfig.from_pretrained(MODEL_ID, trust_remote_code=True) + +vlm_signals = { + "vision_config": hasattr(cfg, "vision_config"), + "image_token_id": hasattr(cfg, "image_token_id"), + "video_token_id": hasattr(cfg, "video_token_id"), + "mm_projector": hasattr(cfg, "mm_projector_type"), + "audio_config": hasattr(cfg, "audio_config"), +} +is_vlm = any(vlm_signals.values()) +print("VLM signals:", vlm_signals) +print("Is VLM:", is_vlm) +``` + +## Step 7 — Check optimum-intel Support + +```python +from optimum.exporters.tasks import TasksManager + +model_type = cfg.model_type +try: + supported_tasks = TasksManager.get_supported_tasks_for_model_type( + model_type, "openvino" + ) + print(f"optimum-intel knows model_type='{model_type}': YES") + print("Supported tasks:", supported_tasks) + optimum_supported = True +except KeyError: + print(f"optimum-intel does NOT know model_type='{model_type}'") + optimum_supported = False +``` + +## Step 8 — Write model_profile.json + +```python +import json + +profile = { + "model_id": MODEL_ID, + "model_type": cfg.model_type, + "architectures": cfg.architectures, + "pipeline_tag": meta.get("pipeline_tag"), + "library_name": meta.get("library_name"), + "estimated_params_b": round(est / 1e9, 1), + "hidden_size": getattr(cfg, "hidden_size", None), + "num_layers": getattr(cfg, "num_hidden_layers", None), + "vocab_size": getattr(cfg, "vocab_size", None), + "trust_remote_code_required": trust_remote_code_required, + "is_vlm": is_vlm, + "vlm_signals": vlm_signals, + "optimum_supported": optimum_supported, + "special_config_keys": special_keys, + "hf_tags": meta.get("tags", []), +} + +with open("model_profile.json", "w") as f: + json.dump(profile, f, indent=2) + +print("Written: model_profile.json") +``` + +## Output + +| File | Contents | +|------|----------| +| `model_profile.json` | Structured model profile consumed by subsequent skills | + +Key fields used downstream: + +| Field | Consumed by | +|-------|-------------| +| `optimum_supported` | `try-conversion` — decides which export path to use | +| `trust_remote_code_required` | `try-conversion` — adds flag if needed | +| `is_vlm` | `classify-failure` — routes to GenAI/VLM specialists | +| `estimated_params_b` | `build-report` — included in diagnostics for orchestrator | +| `special_config_keys` | `classify-failure` — identifies SSM/MoE/recurrent patterns | diff --git a/.github/agents-prototype/skills/analyze-and-convert/try-conversion.md b/.github/agents-prototype/skills/analyze-and-convert/try-conversion.md new file mode 100644 index 000000000000..36486b96d8e1 --- /dev/null +++ b/.github/agents-prototype/skills/analyze-and-convert/try-conversion.md @@ -0,0 +1,281 @@ +--- +name: try-conversion +description: Attempt model export to OpenVINO IR using a systematic strategy matrix. Tries multiple weight formats, optimum-intel versions, and flag combinations. Records every attempt with full output. +--- + +# Skill: Try Conversion + +**Trigger:** Called after `probe-model` produces `model_profile.json`. +Uses the profile to select and execute the right strategy matrix. + +--- + +## Prerequisites + +```bash +# Isolated venv per conversion session +python -m venv venv-convert +source venv-convert/bin/activate # Linux/macOS +# or: venv-convert\Scripts\activate # Windows + +pip install -q openvino optimum[openvino] huggingface_hub +``` + +Load profile: +```python +import json +with open("model_profile.json") as f: + profile = json.load(f) + +MODEL_ID = profile["model_id"] +TRUST_RC = profile["trust_remote_code_required"] +IS_VLM = profile["is_vlm"] +OPTIMUM_SUPPORTED = profile["optimum_supported"] +``` + +--- + +## Step 1 — Determine Task + +```python +# Resolve pipeline_tag → optimum-cli export task +from transformers import AutoConfig + +PIPELINE_TAG_MAP = { + "text-generation": "text-generation-with-past", + "text2text-generation": "text2text-generation-with-past", + "image-text-to-text": "image-text-to-text", + "text-classification": "text-classification", + "token-classification": "token-classification", + "question-answering": "question-answering", + "feature-extraction": "feature-extraction", + "fill-mask": "fill-mask", + "text-to-image": "text-to-image", + "image-to-text": "image-to-text", + "automatic-speech-recognition": "automatic-speech-recognition", + "audio-classification": "audio-classification", + "zero-shot-image-classification": "zero-shot-image-classification", +} +try: + cfg = AutoConfig.from_pretrained(MODEL_ID, trust_remote_code=True) + pipeline_tag = getattr(cfg, "pipeline_tag", None) + if pipeline_tag is None: + model_type = getattr(cfg, "model_type", "") + task = "text2text-generation-with-past" if model_type in ("t5", "mt5", "bart", "mbart") else "text-generation-with-past" + else: + task = PIPELINE_TAG_MAP.get(pipeline_tag, pipeline_tag) +except Exception: + task = "text-generation-with-past" +print(f"Resolved task: {task}") +``` + +--- + +## Step 2 — Build Strategy Matrix + +Each strategy is a dict of `optimum-cli` flags to try. +Ordered from fastest/cheapest to most permissive. + +```python +strategies = [] + +# Base flags shared by all strategies +base = { + "--model": MODEL_ID, + "--task": task, + "--output": "ov_model", +} +if TRUST_RC: + base["--trust-remote-code"] = "" + +# Strategy A: fp16, stable optimum-intel +strategies.append({ + "id": "A-fp16-stable", + "weight_format": "fp16", + "optimum_version": "stable", # current installed + "extra_flags": [], + "description": "fp16, stable optimum-intel", +}) + +# Strategy B: int8 symmetric, stable (lighter on memory) +strategies.append({ + "id": "B-int8-stable", + "weight_format": "int8", + "optimum_version": "stable", + "extra_flags": [], + "description": "int8, stable optimum-intel", +}) + +# Strategy C: fp16, optimum-intel git-HEAD +strategies.append({ + "id": "C-fp16-gitHEAD", + "weight_format": "fp16", + "optimum_version": "git+https://github.com/huggingface/optimum-intel.git", + "extra_flags": [], + "description": "fp16, optimum-intel git-HEAD", +}) + +# Strategy D: int4 AWQ (if model is large — > 7B) +if profile.get("estimated_params_b", 0) > 7: + strategies.append({ + "id": "D-int4-awq-gitHEAD", + "weight_format": "int4", + "optimum_version": "git+https://github.com/huggingface/optimum-intel.git", + "extra_flags": ["--ratio", "1.0", "--group-size", "128"], + "description": "int4 AWQ, optimum-intel git-HEAD (large model)", + }) + +# Strategy E: transformers override (if git-HEAD also fails, try pinning to latest pre-release) +strategies.append({ + "id": "E-fp16-transformers-pre", + "weight_format": "fp16", + "optimum_version": "git+https://github.com/huggingface/optimum-intel.git", + "extra_flags": [], + "description": "fp16, git-HEAD + transformers pre-release", + "transformers_override": "git+https://github.com/huggingface/transformers.git", +}) +``` + +--- + +## Step 3 — Run Each Strategy + +```python +import os, subprocess, sys, time, shutil, json + +attempts = [] + +for s in strategies: + print(f"\n{'='*60}") + print(f"Strategy {s['id']}: {s['description']}") + print(f"{'='*60}") + + # Fresh output dir per attempt + out_dir = f"ov_model_{s['id']}" + if os.path.exists(out_dir): + shutil.rmtree(out_dir) + + # Install required optimum-intel version if different + if s["optimum_version"] != "stable": + subprocess.run( + [sys.executable, "-m", "pip", "install", "-q", s["optimum_version"]], + check=False + ) + + if s.get("transformers_override"): + subprocess.run( + [sys.executable, "-m", "pip", "install", "-q", s["transformers_override"]], + check=False + ) + + # Build command + cmd = ["optimum-cli", "export", "openvino", + "--model", MODEL_ID, + "--task", task, + "--weight-format", s["weight_format"], + "--output", out_dir] + + if TRUST_RC: + cmd.append("--trust-remote-code") + + cmd.extend(s.get("extra_flags", [])) + + # Run with full output capture + t0 = time.time() + proc = subprocess.run(cmd, capture_output=True, text=True) + elapsed = round(time.time() - t0, 1) + + attempt = { + "id": s["id"], + "description": s["description"], + "command": " ".join(cmd), + "returncode": proc.returncode, + "elapsed_s": elapsed, + "stdout": proc.stdout[-8000:], # cap to last 8 KB + "stderr": proc.stderr[-8000:], + "success": False, + "ir_files": [], + } + + if proc.returncode == 0: + # Verify IR files were actually produced + ir_files = [f for f in os.listdir(out_dir) + if f.endswith(".xml") or f.endswith(".bin")] + if ir_files: + attempt["success"] = True + attempt["ir_files"] = ir_files + attempt["ir_dir"] = out_dir + print(f" SUCCESS in {elapsed}s — IR files: {ir_files}") + else: + attempt["stderr"] += "\n[No IR files found in output dir despite exit 0]" + print(f" EXIT 0 but no IR files produced.") + else: + print(f" FAILED (rc={proc.returncode}) in {elapsed}s") + print(f" Last error: {(proc.stderr or proc.stdout)[-500:]}") + + attempts.append(attempt) + + # Stop on first success + if attempt["success"]: + print(f"\nFirst successful strategy: {s['id']}") + break + +# Persist results +with open("conversion_attempts.json", "w") as f: + json.dump(attempts, f, indent=2) + +print(f"\nAttempts recorded: {len(attempts)}") +print(f"Successful: {sum(1 for a in attempts if a['success'])}") +``` + +--- + +## Step 4 — Quick Inference Sanity Check (on success) + +Only runs when at least one strategy succeeded. + +```python +import json + +with open("conversion_attempts.json") as f: + attempts = json.load(f) + +winning = next((a for a in attempts if a["success"]), None) +if not winning: + print("No successful conversion — skipping inference check.") +else: + ir_dir = winning["ir_dir"] + print(f"Running inference check on: {ir_dir}") + try: + from optimum.intel import OVModelForCausalLM + from transformers import AutoTokenizer + + tok = AutoTokenizer.from_pretrained(MODEL_ID, trust_remote_code=TRUST_RC) + model = OVModelForCausalLM.from_pretrained(ir_dir, trust_remote_code=TRUST_RC) + + inputs = tok("Hello, world!", return_tensors="pt") + out = model.generate(**inputs, max_new_tokens=10) + generated = tok.decode(out[0], skip_special_tokens=True) + print(f"Inference OK: '{generated}'") + winning["inference_ok"] = True + winning["inference_sample"] = generated + except Exception as e: + print(f"Inference check failed: {e}") + winning["inference_ok"] = False + winning["inference_error"] = str(e) + + with open("conversion_attempts.json", "w") as f: + json.dump(attempts, f, indent=2) +``` + +--- + +## Output + +| File | Contents | +|------|----------| +| `conversion_attempts.json` | All strategy attempts with commands, full stdout/stderr, success flag, IR file list | +| `ov_model_/` | IR files from the first successful strategy (if any) | + +If **no strategy succeeded**, `conversion_attempts.json` contains the complete +failure evidence for `classify-failure` to analyse. diff --git a/.github/agents-prototype/skills/conversion-issues/SKILL.md b/.github/agents-prototype/skills/conversion-issues/SKILL.md new file mode 100644 index 000000000000..658f4c0e7c9c --- /dev/null +++ b/.github/agents-prototype/skills/conversion-issues/SKILL.md @@ -0,0 +1,31 @@ +--- +name: conversion-issues +description: Investigate and fix model conversion issues in OpenVINO Frontends (ONNX, PyTorch) — triage, debugging, accuracy comparison, and pre-submission verification. +--- + +# Agent Skill: Investigate and Fix Frontend Conversion Issues + +## Goal +Diagnose and fix issues where models fail to convert to OpenVINO IR or produce incorrect inference results through an OpenVINO frontend. + +## Framework-Specific Workflows + +Each frontend has its own detailed investigation workflow. Read the one matching the target framework: + +| Frontend | Skill file | What it covers | +|---|---|---| +| **ONNX** | [onnx.md](onnx.md) | Triage (unsupported op / conversion bug / shape-type / opset gap), ORT baseline comparison, translator debugging, `.prototxt` test models, C++ GTest, pre-submission checklist | +| **PyTorch** | [pytorch.md](pytorch.md) | Triage (unsupported op / tracing mode / inplace / normalize-step), TorchScript vs torch.export identification, layer test debugging, pre-submission checklist | + +## Related Skills (adding new ops) + +| Frontend | Skill file | When to use | +|---|---|---| +| ONNX | [add-fe-op/onnx.md](../add-fe-op/onnx.md) | Implementing a new ONNX op translator from scratch | +| PyTorch | [add-fe-op/pytorch.md](../add-fe-op/pytorch.md) | Implementing a new PyTorch op translator from scratch | + +## Notes + +- Always verify the model works with the framework's reference runtime (ONNX Runtime / PyTorch) before investigating OpenVINO code. +- Prefer minimal, root-cause fixes over broad refactors. +- Every fix needs a test and must pass the full frontend test suite. diff --git a/.github/agents-prototype/skills/conversion-issues/onnx.md b/.github/agents-prototype/skills/conversion-issues/onnx.md new file mode 100644 index 000000000000..ecc536bfba75 --- /dev/null +++ b/.github/agents-prototype/skills/conversion-issues/onnx.md @@ -0,0 +1,416 @@ +# Skill: Investigate and Fix ONNX Model Conversion Issues in OpenVINO + +--- + +## 1. Triage — Classify the Failure + +Before diving into code, determine which category the issue falls into: + +| Category | Symptoms | +|---|---| +| **Unsupported Op** | Error contains `"Not supported ONNX op"`, `ONNXFrameworkNode`, or `NotSupportedONNXNode`. The ONNX op has no translator registered. | +| **Op Conversion Bug** | Model converts but inference accuracy is wrong (large numerical diff), or an assertion like `CHECK_VALID_NODE` fires during conversion for a valid model. | +| **Shape/Type Mismatch** | Runtime errors such as `"Inconsistent element type"`, `"Shape mismatch"`, or partial-shape propagation failures. | +| **Opset Version Gap** | Error mentions an opset version, or the op was introduced/changed in a newer ONNX opset that OpenVINO hasn't implemented yet. | +| **External Data / Protobuf** | Errors about missing external weight files or protobuf parsing failures. | +| **Subgraph / Control Flow** | Errors inside `If`, `Loop`, `Scan` ops that carry sub-graphs. | + +### Quick diagnostic commands + +```bash +# Python: attempt conversion and print the error +python3 -c " +from openvino import Core +core = Core() +try: + model = core.read_model('model.onnx') + print('Conversion OK, outputs:', [o.any_name for o in model.outputs]) +except Exception as e: + print('ERROR:', e) +" + +# Or using convert_model: +python3 -c " +from openvino import convert_model +model = convert_model('model.onnx') +print([o.any_name for o in model.outputs]) +" + +# Inspect ONNX model metadata: +python3 -c " +import onnx +m = onnx.load('model.onnx') +print('Opset imports:', [(o.domain or 'ai.onnx', o.version) for o in m.opset_import]) +ops = set(n.op_type for n in m.graph.node) +print('Unique ops:', sorted(ops)) +" +``` + +--- + +## 2. Key Source Locations + +All paths are relative to the repository root. + +### ONNX Frontend Core + +| Path | Description | +|---|---| +| `src/frontends/onnx/frontend/src/op/` | **Per-op translators** — one `.cpp` file per ONNX operator. This is where most fixes go. | +| `src/frontends/onnx/frontend/src/op/com.microsoft/` | Translators for `com.microsoft` domain ops. | +| `src/frontends/onnx/frontend/src/op/org.openvinotoolkit/` | Translators for custom OpenVINO-specific ops. | +| `src/frontends/onnx/frontend/src/ops_bridge.cpp` | **Operator registry** — maps `(domain, op_name, opset_version)` → translator function. | +| `src/frontends/onnx/frontend/src/core/operator_set.hpp` | Defines `ONNX_OP` / `ONNX_OP_M` macros and `Operator` type alias. | +| `src/frontends/onnx/frontend/src/version_range.hpp` | `VersionRange`, `OPSET_RANGE`, `OPSET_SINCE`, `OPSET_IN` helpers. | +| `src/frontends/onnx/frontend/src/translate_session.cpp` | Main conversion loop. | +| `src/frontends/onnx/frontend/src/frontend.cpp` | Entry point for `FrontEnd::convert()`, `load()`, `convert_partially()`. | +| `src/frontends/onnx/frontend/src/exceptions.hpp` | `CHECK_VALID_NODE` macro and `OnnxNodeValidationFailure` exception. | +| `src/frontends/onnx/frontend/src/core/node.hpp` | `ov::frontend::onnx::Node` — context object each translator receives. | +| `src/frontends/onnx/frontend/src/utils/common.hpp` | Shared helpers (`is_input_valid`, `handle_opset6_binary_op`). **Always check here first.** | + +### Cross-Frontend Shared Libraries + +| Path | Description | +|---|---| +| `src/frontends/common_translators/include/common_translators.hpp` | **Reusable translators** shared across ONNX/PyTorch/TF frontends. | +| `src/frontends/common_translators/src/op/` | Implementation of common translators. | +| `src/frontends/common/include/openvino/frontend/complex_type_mark.hpp` | `ComplexTypeMark` — Mark node for complex number type propagation. | +| `src/frontends/common/include/openvino/frontend/sequence_mark.hpp` | `SequenceMark` — Mark node for sequence/list type propagation. | +| `src/frontends/common_translators/include/sequence_concat_replacer.hpp` | Normalize-step transformation example. | + +### ONNX Frontend Tests + +| Path | Description | +|---|---| +| `src/frontends/onnx/tests/onnx_import.in.cpp` | Main C++ import/inference tests (templated per backend). | +| `src/frontends/onnx/tests/onnx_import_com_microsoft.in.cpp` | Tests for `com.microsoft` domain. | +| `src/frontends/onnx/tests/onnx_import_controlflow.in.cpp` | Tests for ops with sub-graphs (`If`, `Loop`, `Scan`). | +| `src/frontends/onnx/tests/onnx_import_convpool.in.cpp` | Convolution and pooling op tests. | +| `src/frontends/onnx/tests/models/` | `.prototxt` test model definitions. | + +### Build Targets + +| Target | What it builds | +|---|---| +| `openvino_onnx_frontend` | The ONNX frontend shared library. | +| `ov_onnx_frontend_tests` | C++ unit/import tests. | + +--- + +## 3. Investigation Workflow + +### Step 0: Check if the model works with ONNX Runtime (CPU provider) + +```python +import numpy as np +import onnxruntime as ort +import onnx + +model = onnx.load("model.onnx") +opset_versions = [(o.domain or "ai.onnx", o.version) for o in model.opset_import] +print(f"Opset imports: {opset_versions}") + +try: + sess = ort.InferenceSession("model.onnx", providers=["CPUExecutionProvider"]) + inputs = {} + for inp in sess.get_inputs(): + shape = [d if isinstance(d, int) else 1 for d in inp.shape] + inputs[inp.name] = np.random.randn(*shape).astype(np.float32) + result = sess.run(None, inputs) + print(f"ORT inference OK, {len(result)} outputs") +except Exception as e: + print(f"ORT inference FAILED: {e}") +``` + +**Decision based on ORT result:** + +| ORT Result | Model Age | Action | +|---|---|---| +| **ORT fails** | Old model (opset ≤ 9) | Ask the user to consider not supporting this model. | +| **ORT fails** | Newer model (opset ≥ 13) | The model may be corrupted. Ask user to verify with `onnx.checker.check_model()`. | +| **ORT succeeds** | Any | Valid model — the issue is in OpenVINO's ONNX frontend. | + +### Step 1: Reproduce the failure + +```bash +python3 -c " +from openvino import Core +import numpy as np +core = Core() +model = core.read_model('model.onnx') +compiled = core.compile_model(model, 'CPU') +result = compiled(np.zeros((1, 3, 224, 224), dtype=np.float32)) +print('Output shapes:', {k: v.shape for k, v in result.items()}) +" +``` + +### Step 2: Identify the failing op + +```bash +python3 -c " +import onnx +m = onnx.load('model.onnx') +for n in m.graph.node: + print(f'{n.op_type} (domain={n.domain or \"ai.onnx\"})') +" | sort | uniq -c | sort -rn +``` + +If the error names the op, skip to Step 3. Otherwise use partial conversion: + +```bash +python3 -c " +from openvino.frontend import FrontEndManager +from openvino import serialize +fem = FrontEndManager() +fe = fem.load_by_framework('onnx') +model = fe.load('model.onnx') +ov_model = fe.convert_partially(model) +serialize(ov_model, '/tmp/partial.xml') +" +grep -i 'framework\|not.supported' /tmp/partial.xml +``` + +### Step 3: Check if the op has a translator + +```bash +# Example: looking for "Resize" +grep -rn 'ONNX_OP.*"Resize"' src/frontends/onnx/frontend/src/op/ +``` + +- No result → **unsupported op** → see [add-fe-op/onnx.md](../add-fe-op/onnx.md) +- Found → **existing translator** → go to Section 5 (Fixing an Existing Op) + +Check the registered opset range: + +```bash +grep -n 'ONNX_OP' src/frontends/onnx/frontend/src/op/resize.cpp +``` + +### Step 4: Examine the translator code + +```bash +cat src/frontends/onnx/frontend/src/op/.cpp +``` + +Cross-reference with the [ONNX operator spec](https://onnx.ai/onnx/operators/) to check: +- Are all attributes handled with correct defaults per the ONNX spec? +- Are optional inputs guarded with `common::is_input_valid(node, index)`? +- Are edge cases covered (empty tensors, scalar inputs, negative indices)? +- Does the opset version introduce new attributes or behavior changes? + +### Step 5: Check existing tests + +```bash +grep -rn '' src/frontends/onnx/tests/onnx_import*.in.cpp +grep -rn '' src/frontends/onnx/tests/models/*.prototxt | head -20 +``` + +--- + +## 4. Adding a New Op Translator + +For full implementation details, see [add-fe-op/onnx.md](../add-fe-op/onnx.md). + +--- + +## 5. Fixing an Existing Op Translator + +### Common fix patterns + +#### A. Missing attribute or default value + +```cpp +// BEFORE (crashes if attribute missing): +const auto axis = node.get_attribute_value("axis"); + +// AFTER (provide default per ONNX spec): +const auto axis = node.get_attribute_value("axis", 0); +``` + +#### B. Unhandled optional input + +```cpp +#include "utils/common.hpp" + +const auto inputs = node.get_ov_inputs(); +if (common::is_input_valid(node, 2)) { + const auto& bias = inputs[2]; + result = std::make_shared(result, bias); +} +``` + +#### C. Opset version upgrade (new attributes/behavior) + +Add a new opset namespace and register it: + +```cpp +namespace opset_18 { +ov::OutputVector op_name(const ov::frontend::onnx::Node& node) { + // Handle new opset-18 attributes/behavior +} +ONNX_OP("OpName", OPSET_SINCE(18), ai_onnx::opset_18::op_name); +} // namespace opset_18 +``` + +Update the old registration's upper bound: + +```cpp +// Change OPSET_SINCE(11) to OPSET_RANGE(11, 17): +ONNX_OP("OpName", OPSET_RANGE(11, 17), ai_onnx::opset_11::op_name); +``` + +#### D. Shape/Type mismatch + +Ensure the translator handles dynamic dimensions, different element types, broadcasting rules per ONNX spec, and scalar vs 1-D tensor differences. + +#### E. Numerical accuracy + +If inference works but results are wrong: +1. Isolate the failing op using `onnx.utils.extract_model()`. +2. Compare per-op outputs between ONNX Runtime and OpenVINO. +3. Check if OV op semantics differ from ONNX (padding conventions, rounding modes, epsilon placement). + +#### F. Multi-op patterns — normalize-step transformations + +When a fix requires matching and replacing a pattern of multiple ops: +1. Write an `ov::pass::MatcherPass` transformation. +2. Register it in `FrontEnd::normalize()` (in `frontend.cpp`) via the `pass::Manager`. +3. For cross-frontend transformations, place it in `src/frontends/common_translators/`. + +--- + +## 6. Testing the Fix + +```bash +# Build and run all ONNX frontend tests +cmake --build build --target ov_onnx_frontend_tests -j$(nproc) +cd build && ctest -R "ov_onnx_frontend_tests" --output-on-failure -j$(nproc) + +# Run a specific test by name pattern +ctest -R "onnx_model_" -V +``` + +### Validate with Python comparison + +```python +import numpy as np +import onnxruntime as ort +from openvino import Core + +sess = ort.InferenceSession("model.onnx") +ort_inputs = {"input": np.random.randn(1, 3, 224, 224).astype(np.float32)} +ort_out = sess.run(None, ort_inputs) + +core = Core() +model = core.read_model("model.onnx") +compiled = core.compile_model(model, "CPU") +ov_out = compiled(ort_inputs) + +for i, (ref, ov_val) in enumerate(zip(ort_out, ov_out.values())): + max_diff = np.abs(ref - ov_val).max() + print(f"Output {i}: max_diff={max_diff:.6e}, allclose={np.allclose(ref, ov_val, atol=1e-5, rtol=1e-5)}") +``` + +--- + +## 7. Debugging Techniques + +### Enable ONNX frontend verbose logging + +```bash +export OV_LOG_LEVEL=DEBUG +python3 -c "from openvino import Core; Core().read_model('model.onnx')" +``` + +### Inspect the model graph post-conversion + +```python +from openvino import Core, serialize + +core = Core() +model = core.read_model("model.onnx") +for op in model.get_ordered_ops(): + print(f"{op.get_type_name()} '{op.get_friendly_name()}' " + f"inputs={[str(i.get_partial_shape()) for i in op.inputs()]} " + f"outputs={[str(o.get_partial_shape()) for o in op.outputs()]}") +serialize(model, "/tmp/model.xml", "/tmp/model.bin") +``` + +### Use partial conversion to find unconverted ops + +```python +from openvino.frontend import FrontEndManager + +fem = FrontEndManager() +fe = fem.load_by_framework("onnx") +input_model = fe.load("model.onnx") +partial_model = fe.convert_partially(input_model) + +for op in partial_model.get_ordered_ops(): + if "FrameworkNode" in op.get_type_name(): + attrs = op.get_attrs() + print(f"UNCONVERTED: {attrs.get('ONNX_META_type', '?')} domain={attrs.get('ONNX_META_domain', '')}") +``` + +### Extract a minimal failing subgraph + +```python +import onnx + +model = onnx.load("model.onnx") +model = onnx.shape_inference.infer_shapes(model) +onnx.utils.extract_model("model.onnx", "submodel.onnx", + input_names=["input_tensor_name"], + output_names=["problematic_op_output_name"]) +``` + +### GDB debugging for C++ translator issues + +```bash +cmake --preset debug +cmake --build build --target ov_onnx_frontend_tests -j$(nproc) +gdb --args ./build/bin/ov_onnx_frontend_tests --gtest_filter="*onnx_model_test_name*" +# (gdb) break resize.cpp:65 +# (gdb) run +``` + +--- + +## 8. Checklist Before Submitting a Fix + +- [ ] **Root cause identified** — the specific ONNX spec requirement that was violated or missing. +- [ ] **Minimal code change** — only translator file(s) and test(s) are modified. +- [ ] **Test model added** — `.prototxt` in `src/frontends/onnx/tests/models/` exercising the fix. +- [ ] **C++ test added** — in the appropriate `onnx_import*.in.cpp` file. +- [ ] **Build succeeds** — `cmake --build build --target openvino_onnx_frontend ov_onnx_frontend_tests` completes. +- [ ] **All ONNX tests pass** — `ctest -R ov_onnx_frontend_tests` shows no regressions. +- [ ] **clang-format applied** — `cmake --build build --target clang_format_fix_all` run. +- [ ] **Supported ops doc** — verify `src/frontends/onnx/docs/supported_ops.md` reflects new op. +- [ ] **Opset range correct** — `ONNX_OP` covers the right version range per ONNX changelog. +- [ ] **Edge cases** — dynamic shapes, optional inputs, unusual dtypes, scalars handled. + +--- + +## 9. Common Pitfalls + +| Pitfall | Explanation | +|---|---| +| **Forgetting optional inputs** | Use `common::is_input_valid(node, index)` for all optional input checks. | +| **Wrong opset version range** | Use `OPSET_RANGE(1, 10)` + `OPSET_SINCE(11)` if spec changed behavior in opset 11. | +| **Attribute type mismatch** | Using the wrong `get_attribute_value()` silently returns a default. | +| **Broadcasting assumptions** | OV's Numpy-style broadcasting may differ from ONNX in edge cases. | +| **Not handling empty tensors** | Some ops may receive 0-element tensors. Ensure no division by size. | +| **Modifying the wrong opset function** | When fixing for opset 18, create a new function — don't modify the opset 11 one. | +| **Missing `#include`** | Each OpenVINO op needs its own include (e.g., `#include "openvino/op/add.hpp"`). | +| **Not checking common_translators** | Check `src/frontends/common_translators/` before writing a new translator. | +| **Not using Mark operations** | For complex, sequence, or quantized types, use `ComplexTypeMark`/`SequenceMark`. | + +--- + +## 10. Reference Links + +- [ONNX Operator Specifications](https://onnx.ai/onnx/operators/) +- [ONNX Operator Changelog](https://github.com/onnx/onnx/blob/main/docs/Changelog.md) +- [OpenVINO Available Operations](https://docs.openvino.ai/latest/openvino_docs_ops_opset.html) +- [OpenVINO ONNX Frontend README](src/frontends/onnx/README.md) +- [OpenVINO ONNX Supported Ops](src/frontends/onnx/docs/supported_ops.md) diff --git a/.github/agents-prototype/skills/conversion-issues/pytorch.md b/.github/agents-prototype/skills/conversion-issues/pytorch.md new file mode 100644 index 000000000000..b3ed213ce5df --- /dev/null +++ b/.github/agents-prototype/skills/conversion-issues/pytorch.md @@ -0,0 +1,359 @@ +# Skill: Investigate and Fix PyTorch Model Conversion Issues in OpenVINO + +--- + +## 1. Triage — Classify the Failure + +Before diving into code, determine which category the issue falls into: + +| Category | Symptoms | +|---|---| +| **Unsupported Op** | Error contains `"No translator found for"`, `PtFrameworkNode` remains in the converted graph, or a `torch.export` op is missing from `get_supported_ops_fx()`. | +| **Op Conversion Bug** | Model converts but inference accuracy is wrong (large numerical diff vs PyTorch), or `PYTORCH_OP_CONVERSION_CHECK` fires for a valid model. | +| **Shape/Type Mismatch** | Errors like `"Inconsistent element type"`, `"Shape mismatch"`, or partial-shape propagation failures after conversion. | +| **Tracing Mode Issue** | Model works with TorchScript but fails with `torch.export` (or vice versa). Op name mappings differ between `get_supported_ops_ts()` and `get_supported_ops_fx()`. | +| **Inplace Op** | Inplace variant (`aten::add_`, `aten.mul_.Tensor`) is missing or doesn't correctly wrap with `inplace_op<>`. | +| **Normalize-Step Failure** | Conversion succeeds but a post-conversion transformation in `FrontEnd::normalize()` crashes or produces wrong results. | +| **Complex / Sequence Type** | Errors related to `ComplexTypeMark` or `SequenceMark` — the Mark operation was not resolved during normalize. | + +### Quick diagnostic commands + +```bash +# Attempt conversion and print the error +python3 -c " +from openvino import convert_model +import torch + +model = ... # instantiate or load the model +example_input = torch.randn(1, 3, 224, 224) +try: + ov_model = convert_model(model, example_input=example_input) + print('Conversion OK, outputs:', [o.any_name for o in ov_model.outputs]) +except Exception as e: + print('ERROR:', e) +" + +# Inspect FX graph ops (torch.export path): +python3 -c " +import torch +from torch.export import export +model = ... +em = export(model, (torch.randn(1, 3, 224, 224),)) +for node in em.graph.nodes: + if node.op == 'call_function': + print(f'{node.target.__name__}') +" | sort | uniq -c | sort -rn +``` + +--- + +## 2. Key Source Locations + +All paths are relative to the repository root. + +### PyTorch Frontend Core + +| Path | Description | +|---|---| +| `src/frontends/pytorch/src/op/` | **Per-op translators** — one `.cpp` file per PyTorch operation. This is where most fixes go. | +| `src/frontends/pytorch/src/op_table.cpp` | **Operator registry** — `get_supported_ops_ts()` (TorchScript) and `get_supported_ops_fx()` (torch.export/FX). | +| `src/frontends/pytorch/src/translate_session.cpp` | Main conversion loop. | +| `src/frontends/pytorch/src/frontend.cpp` | Entry point for `FrontEnd::convert()`, `convert_partially()`, and `normalize()`. | +| `src/frontends/pytorch/src/utils.hpp` | **Shared helpers** (`num_inputs_check`, `make_optional_bias`, `get_shape_rank`, `normalize_axis`, `numel`, `convert_dtype`, `concat_list_construct`). **Always check here first.** | +| `src/frontends/pytorch/include/openvino/frontend/pytorch/node_context.hpp` | `NodeContext` class — `get_input()`, `const_input()`, `mark_node()`, `input_is_none()`. | +| `src/frontends/pytorch/src/pt_framework_node.hpp` | `PtFrameworkNode` — placeholder for unconverted ops. | +| `src/frontends/pytorch/src/transforms/` | **Normalize-step transformations** — graph passes that run after initial translation. | + +### Cross-Frontend Shared Libraries + +| Path | Description | +|---|---| +| `src/frontends/common_translators/include/common_translators.hpp` | **Reusable translators** shared across frontends. | +| `src/frontends/common/include/openvino/frontend/complex_type_mark.hpp` | `ComplexTypeMark` — Mark node for complex number type propagation. | +| `src/frontends/common/include/openvino/frontend/sequence_mark.hpp` | `SequenceMark` — Mark node for sequence/list type propagation. | + +### Tests + +| Path | Description | +|---|---| +| `tests/layer_tests/pytorch_tests/` | **Python layer tests** — one `test_.py` file per operation. Primary test suite. | +| `tests/layer_tests/pytorch_tests/pytorch_layer_test_class.py` | `PytorchLayerTest` base class. | +| `tests/model_hub_tests/pytorch/` | Model-level tests for HuggingFace Transformers, TorchVision, etc. | + +### Build Targets + +| Target | What it builds | +|---|---| +| `openvino_pytorch_frontend` | The PyTorch frontend shared library. | + +--- + +## 3. Investigation Workflow + +### Step 0: Reproduce the failure and identify the conversion path + +| Path | How to identify | Op name format | +|---|---|---| +| **TorchScript** (default) | `torch.jit.script()` or `torch.jit.trace()` | `aten::add`, `aten::conv2d` | +| **torch.export** | `PYTORCH_TRACING_MODE=EXPORT` | `aten.add.Tensor`, `aten.conv2d.default` | + +```bash +# Reproduce with TorchScript (default): +python3 -c " +from openvino import convert_model +import torch +model = ... +ov_model = convert_model(model, example_input=torch.randn(1, 3, 224, 224)) +" + +# Reproduce with torch.export: +PYTORCH_TRACING_MODE=EXPORT python3 -c " +from openvino import convert_model +import torch +model = ... +ov_model = convert_model(model, example_input=torch.randn(1, 3, 224, 224)) +" +``` + +### Step 1: Identify the failing op + +From the error message, extract the op name (e.g., `aten::grid_sampler` or `aten.grid_sampler_2d.default`). + +```bash +# For TorchScript — list ops in the scripted graph: +python3 -c " +import torch +model = ... +scripted = torch.jit.script(model) +for node in scripted.inlined_graph.nodes(): + print(node.kind()) +" | sort | uniq -c | sort -rn +``` + +### Step 2: Check if the op has a translator + +```bash +# TorchScript ops — search in get_supported_ops_ts(): +grep -n '"aten::"' src/frontends/pytorch/src/op_table.cpp + +# FX/Export ops — search in get_supported_ops_fx(): +grep -n '"aten.' src/frontends/pytorch/src/op_table.cpp + +# Search for the translator implementation: +grep -rn 'translate_' src/frontends/pytorch/src/op/ +``` + +- No translator → see [add-fe-op/pytorch.md](../add-fe-op/pytorch.md) +- Found → go to Section 5 (Fixing an Existing Op) + +### Step 3: Examine the translator code + +```bash +cat src/frontends/pytorch/src/op/.cpp +``` + +Cross-reference with [PyTorch docs](https://pytorch.org/docs/stable/) to check: +- Are all inputs handled (including optional ones via `input_is_none()`)? +- Is `num_inputs_check()` called to validate input count? +- Does the translator call `context.mark_node()` on every created node? + +### Step 4: Check existing tests + +```bash +ls tests/layer_tests/pytorch_tests/test_.py +grep -rn '' tests/layer_tests/pytorch_tests/ --include="*.py" | head -20 +``` + +--- + +## 4. Adding a New Op Translator + +For full implementation details, see [add-fe-op/pytorch.md](../add-fe-op/pytorch.md). + +--- + +## 5. Fixing an Existing Op Translator + +### Common fix patterns + +#### A. Missing or wrong input handling + +```cpp +// BEFORE (crashes if input is None): +auto bias = context.get_input(2); + +// AFTER (check for None inputs): +if (!context.input_is_none(2)) { + auto bias = context.get_input(2); + result = context.mark_node(std::make_shared(result, bias)); +} +``` + +#### B. Missing FX/Export variant + +1. Find the FX op name: +```bash +python3 -c " +import torch +from torch.export import export +em = export(model, (torch.randn(1, 3, 224, 224),)) +for node in em.graph.nodes: + if node.op == 'call_function': + print(node.target.__name__) +" +``` + +2. Add the FX mapping in `get_supported_ops_fx()` in `op_table.cpp`: +```cpp +{"aten.op_name.default", op::translate_op_name}, +``` + +#### C. Inplace op missing + +```cpp +// In get_supported_ops_ts(): +{"aten::relu_", op::inplace_op>}, +// In get_supported_ops_fx(): +{"aten.relu_.default", op::inplace_op>}, +``` + +#### D. Shape/Type mismatch + +- Use `align_eltwise_input_types` or `translate_1to1_match_2_inputs_align_types<>` for type alignment. +- Use `apply_dtype()` when the op needs a specific output dtype. + +#### E. Numerical accuracy + +```python +import torch, numpy as np +from openvino import convert_model, Core + +model = ... +example = torch.randn(1, 3, 224, 224) +with torch.no_grad(): + pt_out = model(example) + +ov_model = convert_model(model, example_input=example) +ov_out = Core().compile_model(ov_model, "CPU")(example.numpy()) + +ref = pt_out.detach().numpy() +abs_diff = np.abs(ref - list(ov_out.values())[0]) +print(f"max_diff={abs_diff.max():.6e}, allclose={np.allclose(ref, list(ov_out.values())[0], atol=1e-5)}") +``` + +**Common accuracy root causes:** + +| Cause | Fix | +|---|---| +| **Padding convention** | Check ceil_mode, auto-pad, floor/ceil rounding mode differences | +| **Epsilon placement** | Verify epsilon inside/outside sqrt matches PyTorch behavior | +| **Type promotion** | PyTorch may upcast to float64 implicitly; OV stays float32 | +| **Reduction axis** | Check 0-indexed vs negative indexing, keepdims defaults | + +#### F. Normalize-step transformation issues + +Transformations live in `src/frontends/pytorch/src/transforms/`. Temporarily disable passes to identify which one misbehaves. + +--- + +## 6. Testing + +```bash +# Run tests for a specific op: +TEST_DEVICE=CPU TEST_PRECISION=FP32 python3 -m pytest tests/layer_tests/pytorch_tests/test_.py -v -k "precommit" + +# Run with torch.export mode: +TEST_DEVICE=CPU TEST_PRECISION=FP32 PYTORCH_TRACING_MODE=EXPORT python3 -m pytest tests/layer_tests/pytorch_tests/test_.py -v -k "precommit_torch_export" +``` + +--- + +## 7. Debugging Techniques + +### Enable frontend verbose logging + +```bash +export OV_LOG_LEVEL=DEBUG +python3 -c "from openvino import convert_model; import torch; convert_model(model, example_input=torch.randn(1,3,224,224))" +``` + +### Inspect the converted graph + +```python +from openvino import convert_model, serialize +import torch + +model = ... +ov_model = convert_model(model, example_input=torch.randn(1, 3, 224, 224)) +for op in ov_model.get_ordered_ops(): + print(f"{op.get_type_name()} '{op.get_friendly_name()}'") +serialize(ov_model, "/tmp/model.xml", "/tmp/model.bin") +``` + +### Use partial conversion to find unconverted ops + +```python +from openvino.frontend import FrontEndManager +import torch + +fem = FrontEndManager() +fe = fem.load_by_framework("pytorch") +scripted = torch.jit.script(model) +from openvino.frontend.pytorch.ts_decoder import TorchScriptPythonDecoder +decoder = TorchScriptPythonDecoder(scripted) +input_model = fe.load(decoder) +partial_model = fe.convert_partially(input_model) + +for op in partial_model.get_ordered_ops(): + if "PtFrameworkNode" in op.get_type_name(): + print(f"UNCONVERTED: {op.get_friendly_name()}") +``` + +### GDB debugging + +```bash +cmake -DCMAKE_BUILD_TYPE=Debug ... +cmake --build build --target openvino_pytorch_frontend -j$(nproc) +gdb --args python3 -m pytest tests/layer_tests/pytorch_tests/test_where.py -v -k "test_where[zeros-bool]" -s +# (gdb) break where.cpp:25 +# (gdb) run +``` + +--- + +## 8. Checklist Before Submitting a Fix + +- [ ] **Root cause identified** — the specific PyTorch op behavior that was violated or missing. +- [ ] **Minimal code change** — only translator, op_table, and test files are modified. +- [ ] **Python layer test added/updated** — in `tests/layer_tests/pytorch_tests/test_.py`. +- [ ] **Both tracing modes covered** — TorchScript and FX/Export mappings present where applicable. +- [ ] **Inplace variant handled** — registered with `inplace_op<>`. +- [ ] **`mark_node()` called** — on every OpenVINO node created in the translator. +- [ ] **Build succeeds** — `cmake --build build --target openvino_pytorch_frontend` completes. +- [ ] **Layer tests pass** — both TorchScript and Export modes. +- [ ] **clang-format applied** — formatting changes committed. + +--- + +## 9. Common Pitfalls + +| Pitfall | Explanation | +|---|---| +| **Forgetting `mark_node()`** | Every `std::make_shared<...>()` in a translator must be wrapped in `context.mark_node()`. | +| **Not checking `input_is_none()`** | Optional inputs may be None. Always guard with `context.input_is_none(index)` first. | +| **TorchScript vs FX name mismatch** | TorchScript uses `aten::op_name`, FX uses `aten.op_name.overload`. Forgetting the FX mapping causes silent `PtFrameworkNode` under `torch.export`. | +| **Missing inplace wrapper** | Inplace ops need `op::inplace_op`. | +| **Wrong `const_input()` type** | Produces silent wrong values. Check the PyTorch schema for the exact parameter type. | +| **Not using `num_inputs_check()`** | Skipping this leads to hard-to-debug out-of-bounds crashes. | +| **Not checking `common_translators`** | Check `src/frontends/common_translators/` before writing a new translator. | +| **Dynamic shape assumptions** | Translators must not assume static dimensions. | +| **Missing pytest marks** | New layer tests need `@pytest.mark.precommit` and `@pytest.mark.precommit_torch_export` for CI. | + +--- + +## 10. Reference Links + +- [PyTorch Operator Documentation](https://pytorch.org/docs/stable/torch.html) +- [torch.export Reference](https://pytorch.org/docs/stable/export.html) +- [OpenVINO Available Operations](https://docs.openvino.ai/latest/openvino_docs_ops_opset.html) +- [OpenVINO PyTorch Frontend README](src/frontends/pytorch/README.md) diff --git a/.github/agents-prototype/skills/new-arch-validation-checklist.md b/.github/agents-prototype/skills/new-arch-validation-checklist.md new file mode 100644 index 000000000000..0c6d8bf06fdd --- /dev/null +++ b/.github/agents-prototype/skills/new-arch-validation-checklist.md @@ -0,0 +1,74 @@ +# New Architecture Validation Checklist + +This checklist is for validating enablement of models that introduce architecturally +novel patterns. Run through every category before submitting a patch or opening a PR. + +## How to Use + +For each section, mark each item as `PASS`, `FAIL`, or `N/A`. + +A PR must have **no FAIL items** before it can be merged. + +--- + +## 1. FP16 / BF16 Precision + +- [ ] **Integer indexing paths** — position IDs, sequence indices, attention mask indices are + kept in `int32` or `int64` throughout; no cast to FP16/BF16 on index tensors. +- [ ] **Scale factors** — any scalar multiplier absorbed into a weight matrix via + `MatMulMultiplyFusion` has `|scalar| ≤ 1.0` OR the weight dtype is FP32. If `|scalar| > 1.0` + AND weight dtype is FP16/BF16 — the fusion must be skipped for that instance. +- [ ] **Constant tables** — cached trig tables (`cos_cached`, `sin_cached`), embedding lookup + tables, and other large constant tensors are stored at the precision required by the op spec, + not silently downcast during constant folding. +- [ ] **Reference comparison** — model inference result with `FP32` and `FP16` differ by no + more than the expected numerical tolerance (≤ 1% max absolute difference on representative + inputs, or per model card specification). + +## 2. Shared KV Cache + +- [ ] **ReadValue fan-out** — for every `ReadValue` node that is part of a KV cache: + `readvalue->get_output_target_inputs(0).size() == 1`. If `> 1`, the state is shared and + any fusion or folding pass that assumes 1-to-1 state ownership MUST be skipped. +- [ ] **Assign pairing** — each `ReadValue` has exactly one corresponding `Assign` that writes + to the same state variable. If `> 1` Assigns write to the same state — this is a model + defect; file a bug before proceeding. +- [ ] **StatefulSDPAFusion guard** — the `StatefulSDPAFusion` pass is guarded against shared KV + cache; a shared `ReadValue` causes `return false` from the callback, not a fusion. +- [ ] **NPUW fold idempotency** — any weight-folding pass that touches state-connected nodes + is tagged (e.g. `FoldedTag`) after the first application and skipped on re-application. + +## 3. Novel Tensor Types + +- [ ] **ONNX SequenceMark invariant** — every op that produces a sequence output wraps it in + `ov::frontend::SequenceMark` at the point of creation in the translator. + No downstream transformation receives a bare sequence node without `SequenceMark`. +- [ ] **Mark operations** — `ComplexTypeMark`, `SequenceMark`, and similar wrappers are used + for all special tensor types; the normalize-step transformation resolves them before + plugin execution. +- [ ] **per_layer_inputs** — genuinely novel tensor types that cannot be expressed via existing + OV ops are registered as `per_layer_inputs` in NPUW partition metadata; they are NOT handled + by adding special cases to the main inference path. + +## 4. IR Correctness + +- [ ] **Op spec compliance** — every new op's output shapes and types match the published op spec + in `docs/articles_en/documentation/openvino-ir-format/operation-sets/`. +- [ ] **No `PtFrameworkNode` in converted graph** — the final OV IR contains no unresolved + framework nodes (ONNX: no `ONNXFrameworkNode`; PyTorch: no `PtFrameworkNode`). +- [ ] **clang-format** — all changed `.hpp` / `.cpp` files pass `clang-format` with + `src/.clang-format` config. +- [ ] **clang-tidy** — no new warnings or errors introduced in changed files. + +## 5. Test Coverage + +- [ ] **Positive test** — a test that exercises the exact new op/transformation and verifies + the expected output node appears in the resulting graph. +- [ ] **Negative test** — a test that exercises a non-matching input graph and verifies the + transformation does NOT fire (graph unchanged). +- [ ] **Nightly coverage** — new op translators have at least one test marked for nightly/regression + mode, not only `@pytest.mark.precommit`. +- [ ] **Accuracy regression test** — if the fix addresses an accuracy issue, a test that + directly reproduces the failing scenario is included in the patch. +- [ ] **No disabled tests** — no tests are skipped or disabled without a documented reason + (`pytest.mark.skip(reason=...)` or `GTEST_SKIP()` with a GitHub issue reference). diff --git a/.github/agents-prototype/skills/python-bootstrap/SKILL.md b/.github/agents-prototype/skills/python-bootstrap/SKILL.md new file mode 100644 index 000000000000..a833065dbb91 --- /dev/null +++ b/.github/agents-prototype/skills/python-bootstrap/SKILL.md @@ -0,0 +1,67 @@ +--- +name: python-bootstrap +description: Install Python dependencies before running verification or test steps. Choose the correct path depending on whether the agent builds OpenVINO from source or not. +--- + +# Skill: Python Package Bootstrap + +Install packages **only when they are actually needed** for a step. Do not +pre-install everything upfront. + +--- + +## Path A — No source build (core-opspec, transformation, frontend, npu) + +Use this path when the agent **does not** compile OpenVINO from source. +The `openvino` release wheel provides the runtime needed for tests and +conversion checks. + +```bash +pip install openvino optimum-intel torch \ + --extra-index-url https://download.pytorch.org/whl/cpu +``` + +For frontend agents that also need ONNX or TensorFlow: +```bash +pip install onnx onnxruntime +pip install tensorflow # only if testing TF frontend +``` + +--- + +## Path B — Source build (cpu, gpu) + +Use this path when the agent **builds OpenVINO from source**. + +> **Do NOT `pip install openvino`** when a source build is present in the +> same environment. Installing the release wheel alongside a dev build creates +> two `openvino` packages that shadow each other, producing confusing import +> errors and incorrect test results. + +Install only the packages that are **not** produced by the OV build: + +```bash +pip install torch \ + --extra-index-url https://download.pytorch.org/whl/cpu +pip install optimum-intel pytest +# Do NOT add 'openvino' here — use the build output instead +``` + +After building OpenVINO, add the build's Python bindings to the path: + +```bash +# Point Python at the locally built openvino package +export PYTHONPATH="/src/bindings/python:$PYTHONPATH" +# or install the wheel produced by the build +pip install /wheels/openvino-*.whl +``` + +--- + +## General rules + +- Install packages **on demand** per step, not all at once. +- If a package install fails, note it in `agent-results//session.md` + and continue — do not abort the entire pipeline over a missing optional dep. +- Never install from untrusted or unofficial indexes beyond the PyTorch + CPU wheel server listed above. diff --git a/.github/agents-prototype/skills/submit-draft-pr/SKILL.md b/.github/agents-prototype/skills/submit-draft-pr/SKILL.md new file mode 100644 index 000000000000..1921cdd07150 --- /dev/null +++ b/.github/agents-prototype/skills/submit-draft-pr/SKILL.md @@ -0,0 +1,162 @@ +--- +name: submit-draft-pr +description: Optionally create a draft PR to the upstream repo from a local source directory. Used by coding agents when a local source path is available in the context file. +--- + +# Skill: Submit Draft PR + +> **Optional.** Attempt this only when all of the following are true: +> - Your context file provides a local source path (e.g. `OpenVINO source code: /path/to/openvino`) +> - That path is accessible and is a git repository +> - `gh` CLI is available and authenticated (`gh auth status`) +> +> If any condition is not met — **skip silently** and log one line. Do not fail. + +--- + +## When to invoke + +After completing your main implementation work and writing results to +`agent-results//`, call this skill to open a **draft PR** from your +changes to the upstream repo. + +This is *in addition to* — never instead of — generating patch files and +writing results to `agent-results/`. + +--- + +## Steps + +### 1. Verify prerequisites + +```bash +# Is gh available and authenticated? +gh auth status || { echo "[draft-pr] gh not available — skipping"; exit 0; } + +# Is the source path a git repo? +[ -d "${SOURCE_PATH}/.git" ] || { echo "[draft-pr] ${SOURCE_PATH} is not a git repo — skipping"; exit 0; } +``` + +### 2. Choose a branch name + +Use a descriptive kebab-case branch name derived from the work done: + +``` +agent/add--op # core op or FE op +agent/add--transformation +agent/-export # optimum-intel / tokenizers +agent/- # general +``` + +### 3. Check for an existing PR + +Before creating a new PR, check whether one already exists from the same branch to avoid duplicates: + +```bash +BRANCH="agent/" +EXISTING=$(gh pr list \ + --repo openvinotoolkit/openvino \ + --head "$(gh api user -q .login):$BRANCH" \ + --json url -q '.[0].url' 2>/dev/null) +if [ -n "$EXISTING" ]; then + echo "[draft-pr] PR already exists: $EXISTING — skipping creation" + exit 0 +fi +``` + +If a PR exists, log the URL and stop — do **not** open a second one. + +### 4. Compose the PR body + +Every agent-generated PR **must** open with the following notice block (exact +wording, first lines of the body before any other content): + +```markdown +> [!IMPORTANT] +> **This PR was generated by a Copilot coding agent and requires human review.** +> A maintainer must inspect the changes, run the relevant tests locally, and +> convert this draft to "Ready for review" only after verifying correctness. +> Do **not** merge without human sign-off. +``` + +After the notice, follow the upstream `pull_request_template.md` structure: + +```markdown +> [!IMPORTANT] +> **This PR was generated by a Copilot coding agent and requires human review.** +> A maintainer must inspect the changes, run the relevant tests locally, and +> convert this draft to "Ready for review" only after verifying correctness. +> Do **not** merge without human sign-off. + +### Details: + - + - Agent: + - Model tested: + - Operator: + +### Tickets: + - + +### AI Assistance: + - AI assistance used: yes + - Generated by GitHub Copilot agent (``). Human validation required: + build verification, unit/functional tests, and inference sanity check. +``` + +Write this body to `agent-results//pr_body.md` before calling `gh pr create`. + +### 5. Create branch, commit, and open draft PR + +```bash +cd +BRANCH="fix/" +git checkout -b "$BRANCH" +git add -A +git commit -m "" +gh repo fork openvinotoolkit/openvino --clone=false 2>/dev/null || true +git remote add fork "$(gh repo view "$(gh api user -q .login)/openvino" --json sshUrl -q .sshUrl)" 2>/dev/null || true +git push fork "$BRANCH" +gh pr create --draft \ + --repo openvinotoolkit/openvino \ + --head "$(gh api user -q .login):$BRANCH" \ + --title "" \ + --body-file agent-results//pr_body.md +``` + +### 6. Log the result + +After the script runs, record the outcome in `agent-results//session.md`: + +``` +[draft-pr] PR opened: +``` + +or + +``` +[draft-pr] Skipped: +``` + +--- + +## Per-repo upstream table + +| Agent | Upstream repo | +|---|---| +| transformation, core-opspec, pytorch-fe, cpu, gpu, npu | `openvinotoolkit/openvino` | +| optimum-intel | `huggingface/optimum-intel` | +| openvino-tokenizers | `openvinotoolkit/openvino_tokenizers` | +| openvino-genai | `openvinotoolkit/openvino.genai` | + +Pass `--upstream ` only when auto-detection from the git remote is likely +to fail (e.g. the local clone uses an internal mirror URL). + +--- + +## Important constraints + +- Always create PRs as **drafts** — never ready-for-review. +- PR target is the **upstream** repo, not the fork. +- Do not push to `main`/`master` of any repo. +- If `git push --force-with-lease` fails (e.g. diverged history), skip and log — do not `--force`. +- Do not block the agent's completion on PR success — log the failure and continue. diff --git a/.github/agents-prototype/skills/verify-conversion/SKILL.md b/.github/agents-prototype/skills/verify-conversion/SKILL.md new file mode 100644 index 000000000000..52e7d37f7533 --- /dev/null +++ b/.github/agents-prototype/skills/verify-conversion/SKILL.md @@ -0,0 +1,216 @@ +--- +name: verify-conversion +description: E2E gate — verifies that applied patches produce a working, numerically sane end-to-end inference through the OpenVINO plugin. Handles HuggingFace/optimum-intel, native OV conversion (ovc/convert_model), and ONNX. Used by orchestrators as the mandatory gate before any PR is published. +--- + +# Skill: Verify Conversion (E2E Gate) + +> **This skill is a hard gate before PR publication.** +> A PR must **not** be opened until both steps below pass: +> 1. The model converts without error. +> 2. A real end-to-end inference through the OV plugin layer produces numerically +> sane output (no NaN/Inf, non-empty, correct shape). +> +> Conversion success alone is not sufficient — plugin-level issues (wrong kernel +> output, silent data corruption, incorrect type/shape inference at runtime) are +> only caught by an actual inference run. + +This is **not** a full strategy matrix run — use `try-conversion.md` skill for that. + +Goal: one conversion, one inference run, numerical sanity check, structured result. + +--- + +## Step 1 — Determine conversion path + +Read `agent-results/pipeline_state.json` (or context file) to identify: + +| Signal | Conversion path | +|---|---| +| `model_id` starts with `org/name` (HuggingFace ID) AND `optimum_supported=true` | **Path A** — `optimum-cli` | +| `model_id` is a local path with `.onnx` file | **Path B** — OV native (`ovc` / `convert_model`) | +| `model_id` is a local PyTorch model or `.pt` / `.pth` file | **Path B** — OV native (`convert_model`) | +| `model_id` is a local TF SavedModel or `.pb` file | **Path B** — OV native (`ovc`) | +| Unclear | Try Path A first, fall back to Path B on failure | + +--- + +## Path A — HuggingFace model via optimum-intel + +### Auto-detect task + +```python +from transformers import AutoConfig + +PIPELINE_TAG_MAP = { + "text-generation": "text-generation-with-past", + "text2text-generation": "text2text-generation-with-past", + "image-text-to-text": "image-text-to-text", + "text-classification": "text-classification", + "token-classification": "token-classification", + "question-answering": "question-answering", + "feature-extraction": "feature-extraction", + "fill-mask": "fill-mask", + "text-to-image": "text-to-image", + "image-to-text": "image-to-text", + "automatic-speech-recognition": "automatic-speech-recognition", + "audio-classification": "audio-classification", +} +try: + cfg = AutoConfig.from_pretrained(MODEL_ID, trust_remote_code=True) + pipeline_tag = getattr(cfg, "pipeline_tag", None) + model_type = getattr(cfg, "model_type", "") + if pipeline_tag: + task = PIPELINE_TAG_MAP.get(pipeline_tag, pipeline_tag) + elif model_type in ("t5", "mt5", "bart", "mbart"): + task = "text2text-generation-with-past" + else: + task = "text-generation-with-past" +except Exception: + task = "text-generation-with-past" +print(f"[verify] Resolved task: {task}") +``` + +### Export + +```bash +optimum-cli export openvino \ + --model "$MODEL_ID" \ + --task "$TASK" \ + --weight-format fp16 \ + ov_verify_check/ +``` + +If export fails with a timeout or OOM, retry with `--weight-format int4`. + +### Quick inference check + +```python +from optimum.intel import OVModelForCausalLM +from transformers import AutoTokenizer + +tok = AutoTokenizer.from_pretrained("ov_verify_check/", trust_remote_code=True) +model = OVModelForCausalLM.from_pretrained("ov_verify_check/", trust_remote_code=True) +inputs = tok("Hello", return_tensors="pt") +out = model.generate(**inputs, max_new_tokens=5) +print("[verify] Inference OK:", tok.decode(out[0])) +``` + +For non-causal models (classification, ASR, etc.), adapt the class and inputs accordingly. + +--- + +## Path B — Native OV conversion (`ovc` / `convert_model`) + +### ONNX model + +```python +import openvino as ov +import numpy as np +core = ov.Core() +model = core.read_model("path/to/model.onnx") +compiled = core.compile_model(model, "CPU") +# Run one inference pass +infer = compiled.create_infer_request() +for inp in compiled.inputs: + shape = [d if d > 0 else 1 for d in inp.partial_shape.get_min_shape()] + infer.set_tensor(inp, ov.Tensor(np.zeros(shape, dtype=inp.element_type.to_dtype()))) +infer.infer() +print("[verify] ONNX compile + inference OK") +``` + +### PyTorch model + +```python +import torch, openvino as ov + +# Load your torch model +# torch_model = ... + +example_input = torch.zeros(1, 3, 224, 224) # adjust shape +ov_model = ov.convert_model(torch_model, example_input=example_input) +compiled = ov.Core().compile_model(ov_model, "CPU") +result = list(compiled({0: example_input.numpy()}).values())[0] +print("[verify] PyTorch convert + inference OK, output shape:", result.shape) +``` + +### TF / generic via ovc + +```bash +ovc path/to/saved_model --output_model ov_verify_check/model.xml +python3 -c " +import openvino as ov, numpy as np +core = ov.Core() +m = core.read_model('ov_verify_check/model.xml') +cmp = core.compile_model(m, 'CPU') +infer = cmp.create_infer_request() +for inp in cmp.inputs: + shape = [d if d > 0 else 1 for d in inp.partial_shape.get_min_shape()] + infer.set_tensor(inp, ov.Tensor(np.zeros(shape, dtype=inp.element_type.to_dtype()))) +infer.infer() +print('[verify] ovc + compile + inference OK') +" +``` + +--- + +## Step 3 — E2E Numerical Sanity Check + +After inference completes, validate output quality through the plugin layer: + +```python +import numpy as np + +def check_output_sanity(outputs: dict, label: str) -> tuple[bool, str]: + """Returns (passed, reason). Checks all output tensors.""" + for name, arr in outputs.items(): + arr = np.asarray(arr) + if arr.size == 0: + return False, f"{label}: output '{name}' is empty (size=0)" + if np.isnan(arr).any(): + return False, f"{label}: output '{name}' contains NaN" + if np.isinf(arr).any(): + return False, f"{label}: output '{name}' contains Inf" + return True, "OK" + +# For HF/optimum path — check that generated tokens are non-empty +def check_lm_output(out_ids, tokenizer, label: str) -> tuple[bool, str]: + if out_ids is None or out_ids.shape[-1] == 0: + return False, f"{label}: generated token sequence is empty" + decoded = tokenizer.decode(out_ids[0], skip_special_tokens=True) + if not decoded.strip(): + return False, f"{label}: decoded output is blank" + return True, f"generated: '{decoded[:80]}'" +``` + +Apply the appropriate check based on conversion path: +- **Path A (HF/optimum)**: use `check_lm_output` on the generated token ids +- **Path B (native OV)**: use `check_output_sanity` on the infer request output tensors + +If the sanity check fails, set `e2e_passed = false` with the reason — do **not** +silently swallow the failure. + +--- + +## Result reporting + +Write the outcome to `agent-results//verify_result.json`: + +```json +{ + "verify_passed": true, + "e2e_passed": true, + "conversion_path": "optimum-cli | ovc | convert_model", + "task": "", + "e2e_detail": "", + "error": null +} +``` + +`verify_passed` is `true` only when **both** conversion and E2E inference pass. + +On failure, set `"verify_passed": false`, `"e2e_passed": false` (if inference +failed), and populate `"error"` with the specific failure reason. + +Do **not** abort the pipeline — the orchestrator decides whether to retry or +escalate. diff --git a/.github/agents-prototype/transformation.agent.md b/.github/agents-prototype/transformation.agent.md new file mode 100644 index 000000000000..f352e0cc733a --- /dev/null +++ b/.github/agents-prototype/transformation.agent.md @@ -0,0 +1,254 @@ +--- +name: Transformation Agent +description: Tier-2 OpenVINO graph transformation specialist. Implements pattern-based graph fusion transformations and custom graph rewrite rules in OpenVINO Core (src/common/transformations/). Invoked after Core OpSpec publishes the op spec — the transformation is what makes the new op composable with the rest of the graph optimization pipeline. +model: claude-sonnet-4.6 +--- +# Transformation Agent + +## Role + +Tier-2 OpenVINO graph transformation specialist. Implements **pattern-based graph +fusion transformations** and custom graph rewrite rules in OpenVINO Core +(`src/common/transformations/`). + +Invoked after Core OpSpec publishes the op spec — the transformation is what makes +the new op composable with the rest of the graph optimization pipeline. + +## Output + +Write all logs, results, and patches to `agent-results/transformation/`. + +## Called by + +- **OpenVINO Orchestrator** (Step 4 parallel gate — after Core OpSpec posts + `op_spec_ready=true`) +- **OpenVINO Orchestrator** (Step 0 shortcut — when a `patch_type=transformation` + git patch is found in the issue comments) + +--- + +## Environment + +| Item | Notes | +|---|---| +| **OpenVINO repository** | Current working directory — run from the `openvinotoolkit/openvino` repository root | +| **Skills** | `.github/agents-prototype/skills/` — relative to the repository root | + +### Python Package Bootstrap + +Follow **[`skills/python-bootstrap/SKILL.md`](skills/python-bootstrap/SKILL.md) — Path A** (no source build). + +--- + +## Skills + +This agent follows the **[`skills/add-fusion-transformation/SKILL.md`](skills/add-fusion-transformation/SKILL.md)** workflow. +SKILL.md lists all step files with their purpose and execution order. + +| Skill file | Purpose | +|---|---| +| `.github/agents-prototype/skills/debug-matcher-pass/SKILL.md` | **Debug only** — diagnose why a MatcherPass is not firing: pattern not matched, callback never triggers, opset version mismatch. Load this skill when a transformation silently produces no effect. | + +## Code Quality + +Before writing any code, read [`.github/copilot-instructions.md`](.github/copilot-instructions.md) +and apply its conventions. Key points for this agent: +- `MatcherPass` / `FunctionPass` classes in `ov::pass` namespace, `OPENVINO_RTTI` macro required +- Filenames: `snake_case`; class names: `CamelCase` +- Run `clang-format -i ` (config: `src/.clang-format`) before committing +- Every new pass must have positive, negative, and type-propagation unit tests in + `src/common/transformations/tests/` + +--- + +## Execution Model + +### Step 0: Bootstrap Manifest + +Read the op spec from Core OpSpec Agent output: + +``` +python .github/scripts/meat/read_op_spec.py +``` + +The script reads `agent-results/core-opspec/core_opspec_result.json` and prints the op spec path. +If a `patch_type=transformation` patch is available in `agent-results/`, apply it with `git apply` +and verify it compiles before proceeding. + +Follow `skills/add-fusion-transformation/SKILL.md` → step1-analysis: + +1. Read the op spec to understand the target op and its graph context. +2. Identify the **sub-graph pattern** to fuse/rewrite: + - Which OV ops participate? What are the constant vs runtime inputs? + - What is the target fused op (from Core OpSpec)? +3. Classify the transformation type: + +| Type | Use when | Base class | +|---|---|---| +| `MatcherPass` | Local pattern matching (fixed topology) | `ov::pass::MatcherPass` | +| `FunctionPass` | Whole-graph analysis or multi-pattern rewriting | `ov::pass::FunctionPass` | +| `BackwardGraphRewrite` | Needs consumer-first traversal | `ov::pass::BackwardGraphRewrite` | +| `GraphRewrite` | Forward traversal over all nodes | `ov::pass::GraphRewrite` | + +4. Output `transformation_analysis.md`: + - Sub-graph pattern (ASCII diagram or node list) + - Transformation type decision + rationale + - Constant-folding constraints + +### Step 2: Implement the Transformation + +Follow `skills/add-fusion-transformation/SKILL.md` → step2-implementation. + +File layout: +``` +src/common/transformations/include/transformations//.hpp +src/common/transformations/src//.cpp +``` + +Read the closest existing transformation source as your template (identified in +step1-analysis). Key implementation rules: +- Use `MATCHER_SCOPE(ClassName)` at the start of the constructor +- Use `pattern::wrap_type({...})` and `pattern::consumers_count(N)` for guards +- Call `ov::copy_runtime_info({old_nodes...}, new_node)` before replacement +- Call `fused->set_friendly_name(root->get_friendly_name())` +- Call `ov::replace_node(old_root, new_node)` as the last step — never delete nodes manually +- Return `true` from callback on successful replacement +- Preserve output element type + +### Step 3: Register in Pass Pipeline + +Find the registration location from `transformation_analysis.md` (output of step1-analysis). + +For **common_optimizations** (general, all backends): +Insert `ADD_MATCHER(manager, )` in `src/common/transformations/src/transformations/common_optimizations/common_optimizations.cpp`. + +For **CPU-exclusive** fusion: +Insert `manager.register_pass>();` in `src/plugins/intel_cpu/src/graph_optimizer.cpp`. + +Add the new `.cpp` to `LIBRARY_SRC` in `src/common/transformations/CMakeLists.txt`. +The CMake build picks up all files in `LIBRARY_SRC` — no new `add_library` call needed. + +### Step 4: Write Tests + +Location: `src/common/transformations/tests//` + +Required test cases: +1. **Positive** — pattern matches → replacement occurs +2. **Negative** — pattern does NOT match (e.g., non-constant weight) → graph unchanged +3. **Type-propagation** — output shape/type correct after fusion + +Read the closest existing transformation test as your template. See +`skills/add-fusion-transformation/workflow.md` → Step 6 for the test structure conventions. + +### Step 5: Record Patch + +Run the cross-platform patch generation script: + +``` +python .github/scripts/meat/generate_patch.py --component transformation --op +``` + +The patch is saved to `agent-results/transformation/` for the OV Orchestrator to collect. + +--- + +## Output Contract + +| Output field | Type | Description | +|---|---|---| +| `fix_applied` | `true` \| `false` | Whether a transformation fix was successfully applied | +| `status` | `success` \| `failed` | Whether the transformation was implemented successfully | +| `branch` | string | Fix branch name | +| `patch_file` | path | Path to the `git format-patch` output | +| `pass_name` | string | C++ class name of the new pass (e.g. `FuseGatedDeltaNetRecurrent`) | +| `pass_type` | string | One of: `MatcherPass`, `FunctionPass`, `BackwardGraphRewrite`, `GraphRewrite` | +| `pipeline_registration` | string | File and line where the pass is registered | +| `test_results` | string | Unit test outcome (pass/fail count) | +| `accuracy_ok` | `true` \| `false` | Post-transformation accuracy validation result | +| `agent_report` | Markdown file | Write the agent report directly to `agent-results/transformation/agent_report.md` | + +--- + +## Constraints + +- Reports only to OV Orchestrator — does not call other agents directly. +- Must provide test results when successful. +- Write all results to `agent-results/transformation/`; do not post to GitHub issues or create PRs unless invoked standalone. + +--- + +## PR Creation + +**`pr_mode: delegated_to_orchestrator`** (invoked by Enable Operator Agent): do **not** create a +PR. Write patches to the result JSON only. The orchestrator creates one central draft PR in Phase 7. + +**Standalone invocation** (no `pr_mode` set): follow the [`submit-draft-pr`](skills/submit-draft-pr/SKILL.md) +skill — it handles branch naming, existing-PR deduplication, fork creation, and `gh pr create`. +Skip silently if `gh` is unavailable, not authenticated, or the command fails. + +--- + +## Root Cause vs. Workaround + +Before implementing any fix, apply this decision gate: + +**When a transformation fails to match** (returns false / callback never fires): +1. Ask: "Is the input graph in the canonical form this transformation expects?" +2. If NOT — trace backward to find which **producer** is not emitting canonical output. +3. Fix the producer — not the transformation's matching logic. + +**Workaround anti-pattern** (do NOT do this): +- Adding extra branches to a transformation's callback to handle non-canonical inputs +- Example: `if (isa(input)) { /* special case */ }` in a consumer transformation + +**Root-cause fix** (preferred): +- Make the producer emit the correct output type/wrapper at its point of creation +- This is almost always a 1–3 line change vs. a 20+ line workaround +- It keeps all future consumers correct without modification + +If you are unsure which approach is correct, write your analysis in +`agent-results/transformation/questions.md` and report `status=blocked_needs_review` +before submitting a patch. + +--- + +## Shared Node / Idempotency Guard + +Before finalizing any match pattern, check for shared nodes: + +- Verify `node->get_output_target_inputs(0).size() == 1` for every node you plan to remove + or replace. If >1: the node is shared — fusing it may corrupt other consumers. +- For stateful patterns (`ReadValue`, `Assign`, state): verify the 1-to-1 state-to-consumer + assumption still holds. Models with shared KV cache (Gemma3n, Gemma4 families) violate this. +- Guard: `if (node->get_output_target_inputs(0).size() != 1) return false;` + +--- + +## Diagnostic Checklist: MatMulMultiplyFusion Overflow Guard + +When implementing or reviewing a `MatMul + Multiply(×scalar)` fusion that absorbs the +scalar into the weight matrix, apply this check before fusion: + +1. Is `|scalar| > 1` AND is the weight dtype `f16` or `bf16`? +2. If both — **skip the fusion for this instance**. + +**Why:** Absorbing a large scalar changes operation order: +- Original: `MatMul(large_input, weights) → small_output × scalar` (safe — magnitude reduced first) +- Fused: `MatMul(large_input, weights × scalar)` — intermediate values can overflow FP16 (max ≈ 65504) + +Add a guard in the fusion callback: +- Check the constant value of the scalar input +- If `abs(scalar) > 1.0 && (weight_element_type == f16 || weight_element_type == bf16)` → return false + +Reference: PR #35689 (Gemma4-26B-A4B-it-int4 accuracy regression from MatMulMultiplyFusion). + +## Constraints + +- Only touches `src/common/transformations/` (general) or the specific plugin path + if the transformation is backend-exclusive — never both in the same PR. +- Does not implement the core op itself — that is Core OpSpec Agent's responsibility. + If op headers are missing, signal `missing_op_spec=true` to the orchestrator. +- Always produces a negative test confirming the pass does not fire on non-matching graphs. +- Transformations must be deterministic and preserve model correctness. +- Every produced patch must update the manifest before returning. + diff --git a/.github/agents-prototype/verify-implementation.agent.md b/.github/agents-prototype/verify-implementation.agent.md new file mode 100644 index 000000000000..dd05911b94ca --- /dev/null +++ b/.github/agents-prototype/verify-implementation.agent.md @@ -0,0 +1,349 @@ +--- +name: Verify Implementation Agent +description: Build and test verification gate. Called by Enable Operator Agent after all coding agents complete. Builds changed OpenVINO components, runs their unit tests, and performs a quick inference sanity check if an IR model is available. Returns pass/fail with details before the E2E conversion gate. +model: claude-sonnet-4.6 +--- +# Verify Implementation Agent + +## Role + +Post-coding verification gate. Ensures that every change produced by the coding +agents (FE, Core OpSpec, Transformation, CPU, GPU) actually compiles, passes unit +tests, and does not crash at inference time. + +This agent does **not** add new code. It only builds, runs existing tests, and reports. + +## Output + +Write all results to `agent-results/verify-implementation/`. + +## Called by + +- **Enable Operator Agent** (Phase 5 — after all parallel agents and FE Final Pass complete, + before the E2E Verification Gate) + +--- + +## Environment + +| Item | Notes | +|---|---| +| **OpenVINO repository** | Current working directory — run from the `openvinotoolkit/openvino` repository root | +| **Build directory** | Use the existing `build/` directory — do **not** reconfigure CMake | + +--- + +## Execution Model + +### Step 1: Identify Changed Components + +Read all sub-agent result files to determine which build targets to check: + +```python +import json, os, glob + +result_files = glob.glob("agent-results/*/.*_result.json") + \ + glob.glob("agent-results/**/*_result.json", recursive=True) + +changed_components = set() +patch_files = [] + +for path in result_files: + try: + with open(path) as f: + data = json.load(f) + patches = data.get("patch_paths", []) or ([data["patch_path"]] if data.get("patch_path") else []) + patch_files.extend(patches) + except (json.JSONDecodeError, KeyError, FileNotFoundError): + continue + +# Map patch file paths to CMake build targets +target_map = { + "src/frontends/pytorch": "openvino_pytorch_frontend", + "src/frontends/onnx": "openvino_onnx_frontend", + "src/frontends/tensorflow": "openvino_tensorflow_frontend", + "src/common/transformations": "ov_transformations", + "src/core": "openvino", + "src/plugins/intel_cpu": "openvino_intel_cpu_plugin", + "src/plugins/intel_gpu": "openvino_intel_gpu_plugin", +} + +test_target_map = { + "src/frontends/pytorch": ("ov_pytorch_frontend_tests", "ov_pytorch_frontend_tests"), + "src/frontends/onnx": ("ov_onnx_frontend_tests", "ov_onnx_frontend_tests"), + "src/frontends/tensorflow": ("ov_tensorflow_frontend_tests", "ov_tensorflow_frontend_tests"), + "src/common/transformations": ("ov_transformations_tests", "ov_transformations_tests"), + "src/core": ("ov_core_unit_tests", "ov_core_unit_tests"), + "src/plugins/intel_cpu": ("ov_cpu_func_tests", "ov_cpu_func_tests"), +} + +for patch in patch_files: + for prefix, target in target_map.items(): + if prefix in str(patch): + changed_components.add(prefix) + +print("Changed components:", sorted(changed_components)) +``` + +If no patch files are found, default to checking all components that have +`agent-results//*_result.json` with `status=success`. + +Log: +``` +[VERIFY] Changed components: frontend/pytorch, common/transformations +``` + +### Step 2: Build Changed Targets + +For each changed component, run a focused build. Use the existing `build/` directory. + +```python +import subprocess, sys + +build_dir = "build" +build_results = {} + +for prefix in changed_components: + target = target_map.get(prefix) + if not target: + continue + + print(f"[VERIFY] Building {target}...") + result = subprocess.run( + [sys.executable, "-m", "cmake", "--build", build_dir, "--target", target], + capture_output=True, text=True, + ) + # cmake --build is not invoked via python -m; use direct call: + result = subprocess.run( + ["cmake", "--build", build_dir, "--target", target], + capture_output=True, text=True, + ) + build_results[prefix] = { + "target": target, + "returncode": result.returncode, + "ok": result.returncode == 0, + "stderr_tail": result.stderr.strip().splitlines()[-20:] if result.stderr else [], + } + status = "OK" if result.returncode == 0 else "FAILED" + print(f"[VERIFY] Build {target}: {status}") +``` + +A build failure is a **hard stop** — do not proceed to unit tests. Write the failure to +`agent-results/verify-implementation/build_results.json` and report `status=failed` to the orchestrator. + +### Step 3: Run Unit Tests + +For each successfully built component, run its unit tests: + +```python +import subprocess + +ctest_results = {} + +for prefix in changed_components: + if not build_results.get(prefix, {}).get("ok"): + continue + + test_target_name = test_target_map.get(prefix) + if not test_target_name: + continue + + build_target, ctest_pattern = test_target_name + print(f"[VERIFY] Running tests: {ctest_pattern}...") + + result = subprocess.run( + ["ctest", "--test-dir", build_dir, "-R", ctest_pattern, + "--output-on-failure", "--timeout", "300"], + capture_output=True, text=True, + ) + passed = result.returncode == 0 + # Extract summary line (e.g. "100% tests passed, 0 tests failed out of 42") + summary = next( + (l for l in result.stdout.splitlines() if "tests passed" in l or "tests failed" in l), + result.stdout.strip().splitlines()[-1] if result.stdout else "no output" + ) + ctest_results[prefix] = { + "returncode": result.returncode, + "ok": passed, + "summary": summary, + } + status = "PASS" if passed else "FAIL" + print(f"[VERIFY] Tests {ctest_pattern}: {status} — {summary}") +``` + +**Tests must be "aggressive"** — do not accept a passing run that skips the new +functionality. Verify that test names covering the newly changed op or pass are +actually executed. If you see `No tests were found` for a newly added test file, +that is a failure — the test was not registered. + +### Step 4: Inference Sanity Check + +If `agent-results/analyze-and-convert/ov_model_*/openvino_model.xml` exists, run a quick +inference through the locally built OpenVINO to catch segfaults and memory issues: + +```python +import glob, pathlib + +ir_candidates = sorted(glob.glob( + "agent-results/analyze-and-convert/ov_model_*/openvino_model.xml" +)) + +inference_result = {"skipped": True, "reason": "No IR model available"} + +if ir_candidates: + ir_path = ir_candidates[0] + print(f"[VERIFY] Running inference sanity check on {ir_path}...") + result = subprocess.run( + ["python", ".github/scripts/meat/quick_inference_check.py", "--ir", ir_path], + capture_output=True, text=True, timeout=120, + ) + inference_result = { + "skipped": False, + "ir_path": ir_path, + "returncode": result.returncode, + "ok": result.returncode == 0, + "output": result.stdout.strip().splitlines()[-10:], + "stderr_tail": result.stderr.strip().splitlines()[-10:] if result.stderr else [], + } + status = "PASS" if result.returncode == 0 else "FAIL" + print(f"[VERIFY] Inference check: {status}") +else: + print("[VERIFY] No IR model found — skipping inference check") +``` + +The inference check catches: +- Segfaults from incorrect pointer arithmetic in new CPU/GPU kernels +- Out-of-bounds memory access (reports non-zero exit or signal) +- Wrong output shape or NaN/Inf tensors (checked in `quick_inference_check.py`) + +A failed inference check is a hard stop even if unit tests pass. + +### Step 5: Write Report and Return + +```python +import json, datetime + +overall_ok = ( + all(r["ok"] for r in build_results.values()) and + all(r["ok"] for r in ctest_results.values()) and + (inference_result.get("skipped") or inference_result.get("ok", False)) +) + +report = { + "status": "success" if overall_ok else "failed", + "timestamp": datetime.datetime.utcnow().isoformat() + "Z", + "changed_components": sorted(changed_components), + "build_results": build_results, + "test_results": ctest_results, + "inference_result": inference_result, +} + +os.makedirs("agent-results/verify-implementation", exist_ok=True) +with open("agent-results/verify-implementation/verify_result.json", "w") as f: + json.dump(report, f, indent=2) + +print(f"[VERIFY] Overall: {'PASS' if overall_ok else 'FAIL'}") +print(json.dumps(report, indent=2)) +``` + +Log: +``` +[VERIFY] build=ok tests=ok inference=ok → overall=PASS +``` + +--- + +## Output Contract + +| Output field | Type | Description | +|---|---|---| +| `status` | `success` \| `failed` | Overall verification result | +| `changed_components` | list | Components that were verified | +| `build_results` | object | Per-component build outcome | +| `test_results` | object | Per-component test outcome with summary | +| `inference_result` | object | Inference sanity check result (or `skipped: true`) | + +Output file: `agent-results/verify-implementation/verify_result.json` + +--- + +## Failure Handling + +| Failure type | Action | +|---|---| +| Build fails | Report `status=failed`, include compiler error tail. **Do not run tests.** | +| Unit tests fail | Report `status=failed`, include ctest summary. **Do not run inference check.** | +| Test not found (0 tests executed) | Report `status=failed` — test was not registered correctly. | +| Inference segfault / non-zero exit | Report `status=failed` — this is a kernel or memory bug. | +| NaN/Inf in inference output | Report `status=failed` — this is a precision or initialization bug. | +| IR model not available | Report inference as `skipped` (not failed) — no IR to test against. | + +--- + +## E2E Gate Mode (`mode: e2e_gate`) + +When called with `mode: e2e_gate`, run Steps 1–5 (standard build + test verification) first, +then continue with these additional steps. + +### Step 6: E2E Conversion Verification + +Follow the **[`skills/verify-conversion/SKILL.md`](skills/verify-conversion/SKILL.md)** skill. + +The skill: +1. Auto-detects the correct conversion path (optimum-intel for HuggingFace models, + `ovc`/`convert_model` for local ONNX/PyTorch/TF). +2. Runs one E2E inference through the OV plugin layer. +3. Validates output sanity: no NaN/Inf, non-empty tensors, non-blank LM output. + +Writes `agent-results/verify-implementation/e2e_verify.json`. + +A failed E2E conversion is a **hard stop** — do not proceed to the checklist. + +### Step 7: New-Architecture Validation Checklist + +Follow the **[`skills/new-arch-validation-checklist.md`](skills/new-arch-validation-checklist.md)** skill. + +For each item, examine changed files (from `agent-results/*/files_created`) and mark +`PASS`, `FAIL`, or `N/A`. Log the result per category. + +Writes `agent-results/verify-implementation/checklist_result.json`. + +### Step 8: Sub-Agent Test Result Scan + +``` +python .github/scripts/meat/check_subagent_results.py +``` + +The script scans all sub-agent result JSONs for `status=failed` or failing `test_results`. +Exits with code 1 and prints details if any failure is found. + +### E2E Gate Output + +Write combined result to `agent-results/verify-implementation/e2e_result.json`: +```json +{ + "status": "success|failed", + "build_and_tests": "", + "e2e_verify": {"ok": true, "path": "e2e_verify.json"}, + "checklist": {"pass": 12, "fail": 0, "na": 3}, + "subagent_tests": "pass|fail" +} +``` + +| Condition | `e2e_result.status` | +|---|---| +| Build + tests + E2E + checklist + sub-agent scan all pass | `success` | +| Build or unit tests fail | `failed` (from Step 2/3; Steps 6–8 are not run) | +| E2E conversion fails | `failed` | +| Checklist has any `FAIL` | `failed` | +| Sub-agent scan exits non-zero | `failed` | + +--- + +## Constraints + +- Does **not** modify any source code. +- Does **not** create PRs or branches. +- Reports only to Enable Operator Agent (OV Orchestrator). +- If `build/` does not exist or CMake was not configured, report `status=failed` with reason + `"build directory not found"` — do not attempt to configure CMake. diff --git a/.github/aw/actions-lock.json b/.github/aw/actions-lock.json index 44a2fbcb8dff..8ba50b7c4fc2 100644 --- a/.github/aw/actions-lock.json +++ b/.github/aw/actions-lock.json @@ -5,6 +5,21 @@ "version": "v8", "sha": "ed597411d8f924073f98dfc5c65a23a2325f34cd" }, + "actions/github-script@v9.0.0": { + "repo": "actions/github-script", + "version": "v9.0.0", + "sha": "3a2844b7e9c422d3c10d287c895573f7108da1b3" + }, + "github/gh-aw-actions/setup-cli@v0.79.4": { + "repo": "github/gh-aw-actions/setup-cli", + "version": "v0.79.4", + "sha": "d059700c6a8ec3b5fd798b9ea60f5d048447b918" + }, + "github/gh-aw-actions/setup@v0.79.4": { + "repo": "github/gh-aw-actions/setup", + "version": "v0.79.4", + "sha": "d059700c6a8ec3b5fd798b9ea60f5d048447b918" + }, "github/gh-aw/actions/setup@v0.51.5": { "repo": "github/gh-aw/actions/setup", "version": "v0.51.5", diff --git a/.github/scripts/coverage/config/tests_cpp.yml b/.github/coverage/tests_cpp.yml similarity index 62% rename from .github/scripts/coverage/config/tests_cpp.yml rename to .github/coverage/tests_cpp.yml index 0d5891d8b8ae..ba6ea85498c4 100644 --- a/.github/scripts/coverage/config/tests_cpp.yml +++ b/.github/coverage/tests_cpp.yml @@ -1,36 +1,65 @@ schema_version: 1 suite: cpp +reporting: + label: C++ + artifact_group: cpp + artifact_name_template: coverage-{suite}-{lane} + selection_env: CXX_TEST_NAMES + stats_file: cpp-coverage-stats.env + duration_file: cpp-test-durations.csv + coverage_file: coverage.info + debug_dirs: + - src: .tmp/cpp-coverage-parts + dest: native-cpp-debug + uploads: + - name_template: C++ runtime, C++ suite, {lane} + file: coverage.info + flag_template: cpp-runtime-cpp-{lane} tests: - name: ov_api_conformance_tests binary: ov_api_conformance_tests - mode: gtest_single - args: "*mandatory*" + mode: raw + profiles: [cpu, gpu] + args: + cpu: "--device=CPU --gtest_filter=*mandatory*" + gpu: "--device=GPU --gtest_filter=*mandatory*" - name: ov_auto_batch_func_tests binary: ov_auto_batch_func_tests mode: gtest_single - args: "*smoke*" + profiles: [cpu, gpu] + args: + cpu: "*smoke*" + gpu: "*GPU*:*nightly_OVClassCompiledModelGetPropertyTest*:*nightly_OVClassCompiledModelGetIncorrectPropertyTest*:*nightly_Auto_Batch_OVClassCompileModelWithCorrectPropertiesAutoBatchingTest*" - name: ov_auto_batch_unit_tests binary: ov_auto_batch_unit_tests mode: gtest_single + profiles: [cpu] - name: ov_auto_func_tests binary: ov_auto_func_tests mode: gtest_single + profiles: [cpu] args: "*smoke*" - name: ov_auto_unit_tests binary: ov_auto_unit_tests mode: gtest_single + profiles: [cpu] - name: ov_capi_test binary: ov_capi_test mode: gtest_single + profiles: [cpu, gpu] + args: + cpu: "-*ov_core_gpu*:*intel_gpu*" + gpu: "*ov_core_gpu*:*intel_gpu*" - name: ov_conditional_compilation_tests binary: ov_conditional_compilation_tests mode: gtest_single + profiles: [cpu] - name: ov_core_unit_tests binary: ov_core_unit_tests @@ -39,45 +68,55 @@ tests: args: cpu: "-*IE_GPU*" gpu: "*IE_GPU*" - default: "" - name: ov_cpu_func_tests binary: ov_cpu_func_tests mode: gtest_single + profiles: [cpu] args: "*smoke*" - name: ov_cpu_unit_tests binary: ov_cpu_unit_tests mode: gtest_single + profiles: [cpu] - name: ov_cpu_unit_tests_vectorized binary: ov_cpu_unit_tests_vectorized mode: gtest_single + profiles: [cpu] - name: ov_hetero_func_tests binary: ov_hetero_func_tests mode: gtest_single - args: "*smoke*:-nightly*" + profiles: [cpu, gpu] + args: + cpu: "*smoke*:-nightly*" + gpu: "*GPU*:*nightly_HETERO_OVClassCompileModelWithCorrectPropertiesTest*/*/2:*nightly_HETERO_OVClassCompileModelWithCorrectPropertiesTest*/*/4:*nightly_HETERO_OVClassCompileModelWithCorrectSecondaryPropertiesTest*:*nightly_OVClassCompiledModelGetPropertyTest*:*nightly_OVClassCompiledModelGetIncorrectPropertyTest*" - name: ov_hetero_unit_tests binary: ov_hetero_unit_tests mode: gtest_single + profiles: [cpu] - name: ov_inference_functional_tests binary: ov_inference_functional_tests mode: gtest_single + profiles: [cpu] - name: ov_inference_unit_tests binary: ov_inference_unit_tests mode: gtest_single + profiles: [cpu] - name: ov_ir_frontend_tests binary: ov_ir_frontend_tests mode: gtest_single + profiles: [cpu] - name: ov_lp_transformations_tests binary: ov_lp_transformations_tests mode: gtest_single + profiles: [cpu] - name: ov_onnx_frontend_tests binary: ov_onnx_frontend_tests @@ -86,7 +125,6 @@ tests: args: cpu: "-*IE_GPU*" gpu: "*IE_GPU*" - default: "" - name: ov_onnx_frontend_tests (ONNX_ITERATOR=0) binary: ov_onnx_frontend_tests @@ -96,33 +134,40 @@ tests: args: cpu: "-*IE_GPU*" gpu: "*IE_GPU*" - default: "" - name: ov_op_conformance_tests binary: ov_op_conformance_tests mode: raw - args: "--device=TEMPLATE --gtest_filter=*OpImpl*" + profiles: [cpu, gpu] + args: + cpu: "--device=TEMPLATE --gtest_filter=*OpImpl*" + gpu: "--device=GPU --gtest_filter=*OpImpl*" - name: ov_proxy_plugin_tests binary: ov_proxy_plugin_tests mode: gtest_single + profiles: [cpu] - name: ov_snippets_func_tests binary: ov_snippets_func_tests mode: gtest_single + profiles: [cpu] - name: ov_subgraphs_dumper_tests binary: ov_subgraphs_dumper_tests mode: gtest_single + profiles: [cpu] - name: ov_template_func_tests binary: ov_template_func_tests mode: gtest_single + profiles: [cpu] args: "*smoke*" - name: ov_tensorflow_common_tests binary: ov_tensorflow_common_tests mode: gtest_single + profiles: [cpu] - name: ov_tensorflow_frontend_tests binary: ov_tensorflow_frontend_tests @@ -131,69 +176,50 @@ tests: args: cpu: "-*IE_GPU*" gpu: "*IE_GPU*" - default: "" - name: ov_tensorflow_lite_frontend_tests binary: ov_tensorflow_lite_frontend_tests mode: gtest_single + profiles: [cpu] - name: ov_transformations_tests binary: ov_transformations_tests mode: gtest_single + profiles: [cpu] - name: ov_util_tests binary: ov_util_tests mode: gtest_single + profiles: [cpu] - name: ov_paddle_tests binary: ov_paddle_tests mode: gtest_single - - - name: ov_npu_unit_tests - binary: ov_npu_unit_tests - mode: gtest_single - profiles: [npu] - profile_skip_reason: "NPU profile is OFF" - - - name: ov_npu_func_tests - binary: ov_npu_func_tests - mode: gtest_single - args: "*smoke*" - profiles: [npu] - profile_skip_reason: "NPU profile is OFF" - - - name: ov_nvidia_func_tests - binary: ov_nvidia_func_tests - mode: gtest_single - skip_reason: "unsupported in coverage workflow" + profiles: [cpu] - name: ov_gpu_unit_tests binary: ov_gpu_unit_tests mode: gtest_single profiles: [gpu] - profile_skip_reason: "GPU profile is OFF" - name: ov_gpu_func_tests binary: ov_gpu_func_tests mode: gtest_single args: "*smoke*" profiles: [gpu] - profile_skip_reason: "GPU profile is OFF" - name: test_inference_async - binary: test_inference_async + binary: memory_tests/test_inference_async mode: raw - profiles: [cpu, gpu, npu] + profiles: [cpu, gpu] args: + cpu: "__MODEL_PATH__ CPU" gpu: "__MODEL_PATH__ GPU" - npu: "__MODEL_PATH__ NPU" - default: "__MODEL_PATH__ CPU" - name: test_inference_sync - binary: test_inference_sync + binary: memory_tests/test_inference_sync mode: raw - profiles: [cpu, gpu, npu] + profiles: [cpu, gpu] args: + cpu: "__MODEL_PATH__ CPU" gpu: "__MODEL_PATH__ GPU" - npu: "__MODEL_PATH__ NPU" - default: "__MODEL_PATH__ CPU" diff --git a/.github/scripts/coverage/config/tests_js.yml b/.github/coverage/tests_js.yml similarity index 81% rename from .github/scripts/coverage/config/tests_js.yml rename to .github/coverage/tests_js.yml index 2f0d6d08578d..cfe0e5193f8e 100644 --- a/.github/scripts/coverage/config/tests_js.yml +++ b/.github/coverage/tests_js.yml @@ -1,60 +1,84 @@ schema_version: 1 suite: js +reporting: + label: JS + artifact_group: js + artifact_name_template: coverage-{suite}-{lane} + selection_env: JS_TEST_NAMES + stats_file: js-coverage-stats.env + duration_file: js-test-durations.csv + coverage_file: js-lcov.info + extra_files: + - coverage.info + uploads: + - name_template: JS bindings, {lane} + file: js-lcov.info + flag_template: nodejs-bindings-unit-e2e-{lane} + - name_template: C++ runtime, JS suite, {lane} + file: coverage.info + flag_template: cpp-runtime-js-{lane} tests: - name: node tests/setup.js (covered) kind: command + profiles: [cpu] command: "npx c8 --reporter=lcov --reporter=text --report-dir ${OV_WORKSPACE}/js-coverage --clean=true --exclude 'tests/**' --exclude 'thirdparty/**' node ./tests/setup.js" - name: node scripts/download-runtime.js --ignore-if-exists (covered) kind: command + profiles: [cpu] command: "npx c8 --reporter=lcov --reporter=text --report-dir ${OV_WORKSPACE}/js-coverage --clean=false --exclude 'tests/**' --exclude 'thirdparty/**' node ./scripts/download-runtime.js --ignore-if-exists" - name: node unit core.test.js kind: command + profiles: [cpu] command: "npx c8 --reporter=lcov --reporter=text --report-dir ${OV_WORKSPACE}/js-coverage --clean=false --exclude 'tests/**' --exclude 'thirdparty/**' node --test ./tests/unit/core.test.js" - name: node unit model.test.js kind: command + profiles: [cpu] command: "npx c8 --reporter=lcov --reporter=text --report-dir ${OV_WORKSPACE}/js-coverage --clean=false --exclude 'tests/**' --exclude 'thirdparty/**' node --test ./tests/unit/model.test.js" - name: node unit read_model.test.js kind: command + profiles: [cpu] command: "npx c8 --reporter=lcov --reporter=text --report-dir ${OV_WORKSPACE}/js-coverage --clean=false --exclude 'tests/**' --exclude 'thirdparty/**' node --test ./tests/unit/read_model.test.js" - name: node unit basic.test.js kind: command + profiles: [cpu] command: "npx c8 --reporter=lcov --reporter=text --report-dir ${OV_WORKSPACE}/js-coverage --clean=false --exclude 'tests/**' --exclude 'thirdparty/**' node --test ./tests/unit/basic.test.js" - name: node unit compiled_model.test.js kind: command + profiles: [cpu] command: "npx c8 --reporter=lcov --reporter=text --report-dir ${OV_WORKSPACE}/js-coverage --clean=false --exclude 'tests/**' --exclude 'thirdparty/**' node --test ./tests/unit/compiled_model.test.js" - name: node unit infer_request.test.js kind: command + profiles: [cpu] command: "npx c8 --reporter=lcov --reporter=text --report-dir ${OV_WORKSPACE}/js-coverage --clean=false --exclude 'tests/**' --exclude 'thirdparty/**' node --test ./tests/unit/infer_request.test.js" - name: node unit async_infer_queue.test.js kind: command + profiles: [cpu] command: "npx c8 --reporter=lcov --reporter=text --report-dir ${OV_WORKSPACE}/js-coverage --clean=false --exclude 'tests/**' --exclude 'thirdparty/**' node --test ./tests/unit/async_infer_queue.test.js" - name: node unit tensor.test.js kind: command + profiles: [cpu] command: "npx c8 --reporter=lcov --reporter=text --report-dir ${OV_WORKSPACE}/js-coverage --clean=false --exclude 'tests/**' --exclude 'thirdparty/**' node --test ./tests/unit/tensor.test.js" - name: node unit partial_shape.test.js kind: command + profiles: [cpu] command: "npx c8 --reporter=lcov --reporter=text --report-dir ${OV_WORKSPACE}/js-coverage --clean=false --exclude 'tests/**' --exclude 'thirdparty/**' node --test ./tests/unit/partial_shape.test.js" - name: node unit pre_post_processor.test.js kind: command + profiles: [cpu] command: "npx c8 --reporter=lcov --reporter=text --report-dir ${OV_WORKSPACE}/js-coverage --clean=false --exclude 'tests/**' --exclude 'thirdparty/**' node --test ./tests/unit/pre_post_processor.test.js" - name: npm run test:e2e kind: command + profiles: [cpu] command: "npx c8 --reporter=lcov --reporter=text --report-dir ${OV_WORKSPACE}/js-coverage --clean=false --exclude 'tests/**' --exclude 'thirdparty/**' npm run test:e2e --loglevel=silly" - - - name: js_gpu_compile_model_smoke - kind: command - profiles: [gpu] - profile_skip_reason: "GPU profile is OFF" - command: "npx c8 --reporter=lcov --reporter=text --report-dir ${OV_WORKSPACE}/js-coverage --clean=false --exclude 'tests/**' --exclude 'thirdparty/**' node -e \"const path=require('path'); const { addon: ov } = require('.'); const core = new ov.Core(); const devices = core.getAvailableDevices(); if (!devices.includes('GPU')) { throw new Error('GPU device is not available'); } const model = core.readModelSync(path.resolve('./tests/unit/test_models/add_model.xml')); const compiled = core.compileModelSync(model, 'GPU'); if (!compiled) { throw new Error('Failed to compile model on GPU'); } console.log('GPU JS smoke OK');\"" diff --git a/.github/coverage/tests_python.yml b/.github/coverage/tests_python.yml new file mode 100644 index 000000000000..f3158c50565f --- /dev/null +++ b/.github/coverage/tests_python.yml @@ -0,0 +1,171 @@ +schema_version: 1 +suite: python +reporting: + label: Python + artifact_group: python + artifact_name_template: coverage-{suite}-{lane} + selection_env: PY_TEST_NAMES + stats_file: python-coverage-stats.env + duration_file: python-test-durations.csv + coverage_file: python-coverage.xml + extra_files: + - coverage.info + debug_dirs: + - src: .tmp/python-coverage + dest: python-coverage-debug + uploads: + - name_template: Python API, {lane} + file: python-coverage.xml + flag_template: python-api-frontend-layer-ovc-{lane} + - name_template: C++ runtime, Python suite, {lane} + file: coverage.info + flag_template: cpp-runtime-python-{lane} +tests: + - name: pyopenvino + kind: pytest + profiles: [cpu, gpu] + target: "${TESTS_DIR}/pyopenvino" + env: + cpu: "TEST_DEVICE=CPU" + gpu: "TEST_DEVICE=GPU" + args: + cpu: "-sv -k 'not gpu and not cuda and not npu_'" + gpu: "-sv -k 'not cuda and not npu_'" + + - name: onnx_python + kind: pytest + profiles: [cpu, gpu] + target: "${TESTS_DIR}/onnx" + env: + cpu: "TEST_DEVICE=CPU" + gpu: "TEST_DEVICE=GPU" + args: + cpu: "-sv --backend=CPU -k 'not gpu and not cuda and not npu_ and not zoo' --ignore=${TESTS_DIR}/onnx/test_python/test_zoo_models.py --ignore=${TESTS_DIR}/onnx/tests/tests_python/test_zoo_models.py" + gpu: "-sv --backend=GPU -k 'not cuda and not npu_ and not zoo' --ignore=${TESTS_DIR}/onnx/test_python/test_zoo_models.py --ignore=${TESTS_DIR}/onnx/tests/tests_python/test_zoo_models.py" + + - name: onnx_python_legacy_iterator + kind: pytest + profiles: [cpu, gpu] + target: "${TESTS_DIR}/onnx" + env: + cpu: "TEST_DEVICE=CPU ONNX_ITERATOR=0" + gpu: "TEST_DEVICE=GPU ONNX_ITERATOR=0" + args: + cpu: "-sv --backend=CPU -k 'not gpu and not cuda and not npu_ and not zoo' --ignore=${TESTS_DIR}/onnx/test_python/test_zoo_models.py --ignore=${TESTS_DIR}/onnx/tests/tests_python/test_zoo_models.py" + gpu: "-sv --backend=GPU -k 'not cuda and not npu_ and not zoo' --ignore=${TESTS_DIR}/onnx/test_python/test_zoo_models.py --ignore=${TESTS_DIR}/onnx/tests/tests_python/test_zoo_models.py" + + - name: ovc_unit + kind: pytest + profiles: [cpu] + target: "${TESTS_DIR}/ovc/unit_tests" + args: "-sv -k 'not gpu and not cuda and not npu_'" + + - name: py_frontend + kind: pytest + profiles: [cpu] + target: "${TESTS_DIR}/layer_tests/py_frontend_tests" + args: "-sv -k 'not gpu and not cuda and not npu_'" + + - name: tensorflow_lite_layers + kind: pytest + profiles: [cpu, gpu] + target: "${TESTS_DIR}/layer_tests/tensorflow_lite_tests" + env: + cpu: "TEST_DEVICE=CPU TEST_PRECISION=FP16" + gpu: "TEST_DEVICE=GPU TEST_PRECISION=FP16" + args: + cpu: "-sv -k 'not gpu and not cuda and not npu_'" + gpu: "-sv -k 'not cuda and not npu_'" + + - name: tensorflow_layers + kind: pytest + profiles: [cpu, gpu] + target: "${TESTS_DIR}/layer_tests/tensorflow_tests" + env: + cpu: "TEST_DEVICE=CPU TEST_PRECISION=FP16" + gpu: "TEST_DEVICE=GPU TEST_PRECISION=FP16" + args: + cpu: "-sv -m precommit -k 'not gpu and not cuda and not npu_'" + gpu: "-sv -m precommit -k 'not cuda and not npu_'" + + - name: tensorflow2_keras_layers + kind: pytest + profiles: [cpu, gpu] + target: "${TESTS_DIR}/layer_tests/tensorflow2_keras_tests" + env: + cpu: "TEST_DEVICE=CPU TEST_PRECISION=" + gpu: "TEST_DEVICE=GPU TEST_PRECISION=FP16" + args: + cpu: "-sv -m precommit -k 'not gpu and not cuda and not npu_'" + gpu: "-sv -m precommit -k 'not cuda and not npu_'" + + - name: onnx_layers + kind: pytest + profiles: [cpu, gpu] + target: "${TESTS_DIR}/layer_tests/onnx_tests" + env: + cpu: "TEST_DEVICE=CPU TEST_PRECISION=FP16" + gpu: "TEST_DEVICE=GPU TEST_PRECISION=FP16" + args: + cpu: "-sv -k 'not gpu and not cuda and not npu_'" + gpu: "-sv -k 'not cuda and not npu_'" + + - name: pytorch_layers + kind: pytest + profiles: [cpu, gpu] + target: "${TESTS_DIR}/layer_tests/pytorch_tests" + env: + cpu: "TEST_DEVICE=CPU TEST_PRECISION=FP32" + gpu: "TEST_DEVICE=GPU TEST_PRECISION=FP32" + args: + cpu: "-sv -m precommit -k 'not gpu and not cuda and not npu_'" + gpu: "-sv -m precommit -k 'not cuda and not npu_'" + + - name: pytorch_layers_export + kind: pytest + profiles: [cpu, gpu] + target: "${TESTS_DIR}/layer_tests/pytorch_tests" + env: + cpu: "TEST_DEVICE=CPU TEST_PRECISION=FP32 PYTORCH_TRACING_MODE=EXPORT" + gpu: "TEST_DEVICE=GPU TEST_PRECISION=FP32 PYTORCH_TRACING_MODE=EXPORT" + args: + cpu: "-sv -m precommit_torch_export -k 'not gpu and not cuda and not npu_'" + gpu: "-sv -m precommit_torch_export -k 'not cuda and not npu_'" + + - name: pytorch_layers_fx_backend + kind: pytest + profiles: [cpu, gpu] + target: "${TESTS_DIR}/layer_tests/pytorch_tests" + env: + cpu: "TEST_DEVICE=CPU TEST_PRECISION=FP32 PYTORCH_TRACING_MODE=TORCHFX" + gpu: "TEST_DEVICE=GPU TEST_PRECISION=FP32 PYTORCH_TRACING_MODE=TORCHFX" + args: + cpu: "-sv -m precommit_fx_backend -k 'not gpu and not cuda and not npu_'" + gpu: "-sv -m precommit_fx_backend -k 'not cuda and not npu_'" + + - name: jax_layers_precommit + kind: pytest + profiles: [cpu, gpu] + target: "${TESTS_DIR}/layer_tests/jax_tests" + env: + cpu: "TEST_DEVICE=CPU TEST_PRECISION=FP16 JAX_TRACE_MODE=JAXPR" + gpu: "TEST_DEVICE=GPU TEST_PRECISION=FP16 JAX_TRACE_MODE=JAXPR" + args: + cpu: "-sv -m precommit_jax_fe -k 'not gpu and not cuda and not npu_'" + gpu: "-sv -m precommit_jax_fe -k 'not cuda and not npu_'" + + - name: ovc_python_api + kind: pytest + profiles: [cpu, gpu] + target: "${WORKSPACE_LAYER_TESTS_DIR}/ovc_python_api_tests" + env: + cpu: "TEST_DEVICE=CPU TEST_PRECISION=FP16" + gpu: "TEST_DEVICE=GPU TEST_PRECISION=FP16" + args: + cpu: "-sv -k 'not gpu and not cuda and not npu_'" + gpu: "-sv -k 'not cuda and not npu_'" + + - name: docs_python_snippets + kind: command + profiles: [cpu] + command: "PYTHONPATH=docs/articles_en/assets:${PYTHONPATH} python3 -m coverage run -a docs/articles_en/assets/snippets/main.py" diff --git a/.github/dockerfiles/docker_tag b/.github/dockerfiles/docker_tag index 7db0cfe5e9c3..262017a39e82 100644 --- a/.github/dockerfiles/docker_tag +++ b/.github/dockerfiles/docker_tag @@ -1 +1 @@ -pr-34202 +pr-36097 diff --git a/.github/dockerfiles/ov_build/debian_10_arm/Dockerfile b/.github/dockerfiles/ov_build/debian_10_arm/Dockerfile index 0da0734f5a5e..b8053bb48720 100644 --- a/.github/dockerfiles/ov_build/debian_10_arm/Dockerfile +++ b/.github/dockerfiles/ov_build/debian_10_arm/Dockerfile @@ -1,3 +1,6 @@ +# Copyright (C) 2018-2026 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + ARG REGISTRY="docker.io" FROM ${REGISTRY}/library/debian:10.13 @@ -60,9 +63,7 @@ RUN dpkg --add-architecture armhf && apt-get update && \ libavcodec-dev:armhf \ libavformat-dev:armhf \ libswscale-dev:armhf \ - libgstreamer1.0-dev:armhf \ libpython-dev:armhf \ - libgstreamer-plugins-base1.0-dev:armhf \ zlib1g-dev:armhf \ nlohmann-json-dev \ libgflags-dev:armhf \ @@ -250,4 +251,7 @@ RUN python3 -m pip install --upgrade pip==${PIP_VERSION} && \ RUN python3.11 -m venv venv ENV PATH="/venv/bin:$SCCACHE_HOME:$PATH" +# Install newer ninja (apt ships older version on debian 10 lacking --quiet) +RUN python3 -m pip install --no-cache-dir ninja==1.13.0 + ENV PIP_CACHE_DIR=/mount/caches/pip/linux/${PIP_VERSION} diff --git a/.github/dockerfiles/ov_build/fedora_29/Dockerfile b/.github/dockerfiles/ov_build/fedora_29/Dockerfile index f5dcab8c7c5e..5ad9396b2510 100644 --- a/.github/dockerfiles/ov_build/fedora_29/Dockerfile +++ b/.github/dockerfiles/ov_build/fedora_29/Dockerfile @@ -1,3 +1,6 @@ +# Copyright (C) 2018-2026 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + ARG REGISTRY="docker.io" FROM ${REGISTRY}/library/fedora:29 @@ -114,6 +117,9 @@ RUN curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py && \ python3.14t get-pip.py --no-cache-dir pip==${PIP_VERSION} && \ rm -f get-pip.py +# Install newer ninja (dnf ships older version lacking --quiet) +RUN python3 -m pip install --no-cache-dir ninja==1.13.0 + ENV PIP_CACHE_DIR=/mount/caches/pip/linux/${PIP_VERSION} # Install Node diff --git a/.github/dockerfiles/ov_build/manylinux_2_28/Dockerfile b/.github/dockerfiles/ov_build/manylinux_2_28/Dockerfile index b13120374237..10fb23da87b4 100644 --- a/.github/dockerfiles/ov_build/manylinux_2_28/Dockerfile +++ b/.github/dockerfiles/ov_build/manylinux_2_28/Dockerfile @@ -1,3 +1,6 @@ +# Copyright (C) 2018-2026 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + ARG REGISTRY="quay.io" FROM openvinogithubactions.azurecr.io/quayio/pypa/manylinux_2_28 @@ -18,3 +21,6 @@ RUN mkdir ${SCCACHE_HOME} && cd ${SCCACHE_HOME} && \ # To make python3 and pip binaries accessible ENV PATH="/opt/python/cp311-cp311/bin:$SCCACHE_HOME:$PATH" + +# Install newer ninja (yum ships older version lacking --quiet) +RUN python3 -m pip install --no-cache-dir ninja==1.13.0 diff --git a/.github/dockerfiles/ov_build/ubuntu_20_04_x64/Dockerfile b/.github/dockerfiles/ov_build/ubuntu_20_04_x64/Dockerfile index 7174da8858af..7c6fcca4648b 100644 --- a/.github/dockerfiles/ov_build/ubuntu_20_04_x64/Dockerfile +++ b/.github/dockerfiles/ov_build/ubuntu_20_04_x64/Dockerfile @@ -1,3 +1,6 @@ +# Copyright (C) 2018-2026 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + ARG REGISTRY="docker.io" FROM ${REGISTRY}/library/ubuntu:20.04 @@ -74,4 +77,7 @@ RUN curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py && \ RUN python3.10 -m venv venv ENV PATH="/venv/bin:$SCCACHE_HOME:$PATH" +# Install newer ninja (apt ships 1.10.0 on 20.04 which lacks --quiet) +RUN python3 -m pip install --no-cache-dir ninja==1.13.0 + ENV PIP_CACHE_DIR=/mount/caches/pip/linux/${PIP_VERSION} diff --git a/.github/dockerfiles/ov_build/ubuntu_22_04_android/Dockerfile b/.github/dockerfiles/ov_build/ubuntu_22_04_android/Dockerfile index ff86619ab034..604b00a275c7 100644 --- a/.github/dockerfiles/ov_build/ubuntu_22_04_android/Dockerfile +++ b/.github/dockerfiles/ov_build/ubuntu_22_04_android/Dockerfile @@ -1,3 +1,6 @@ +# Copyright (C) 2018-2026 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + ARG REGISTRY="docker.io" FROM ${REGISTRY}/library/ubuntu:22.04 @@ -24,7 +27,8 @@ RUN apt update && \ build-essential \ python3-pip && \ # vcpkg requires cmake 3.19 or later - python3 -m pip install -U pip cmake~=3.28.0 && \ + # ninja>=1.11 needed for --quiet support (apt ships 1.10.1 on 22.04) + python3 -m pip install -U pip cmake~=3.28.0 ninja==1.13.0 && \ # vcpkg's tool dependencies apt install curl zip unzip tar && \ # vcpkg 'python3' port dependencies diff --git a/.github/dockerfiles/ov_build/ubuntu_22_04_arm64/Dockerfile b/.github/dockerfiles/ov_build/ubuntu_22_04_arm64/Dockerfile index 5a7b94c9a1e0..2b15116b3ef1 100644 --- a/.github/dockerfiles/ov_build/ubuntu_22_04_arm64/Dockerfile +++ b/.github/dockerfiles/ov_build/ubuntu_22_04_arm64/Dockerfile @@ -1,3 +1,6 @@ +# Copyright (C) 2018-2026 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + ARG REGISTRY="docker.io" FROM ${REGISTRY}/library/ubuntu:22.04 @@ -87,8 +90,9 @@ RUN mkdir ${SCCACHE_HOME} && cd ${SCCACHE_HOME} && \ ENV PIP_VERSION="24.0" RUN curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py && \ curl https://bootstrap.pypa.io/pip/3.8/get-pip.py -o get-pip-3-8.py && \ + curl https://bootstrap.pypa.io/pip/3.9/get-pip.py -o get-pip-3-9.py && \ python3 get-pip-3-8.py --no-cache-dir pip==${PIP_VERSION} && \ - python3.9 get-pip.py --no-cache-dir pip==${PIP_VERSION} && \ + python3.9 get-pip-3-9.py --no-cache-dir pip==${PIP_VERSION} && \ python3.10 get-pip.py --no-cache-dir pip==${PIP_VERSION} && \ python3.11 get-pip.py --no-cache-dir pip==${PIP_VERSION} && \ python3.12 get-pip.py --no-cache-dir pip==${PIP_VERSION} && \ @@ -102,6 +106,9 @@ RUN curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py && \ RUN python3.11 -m venv venv ENV PATH="/venv/bin:$SCCACHE_HOME:$PATH" +# Install newer ninja (apt ships 1.10.1 on 22.04 which lacks --quiet) +RUN python3 -m pip install --no-cache-dir ninja==1.13.0 + ENV PIP_CACHE_DIR=/mount/caches/pip/linux/${PIP_VERSION} # ONNX Runtime, see https://github.com/microsoft/onnxruntime/issues/13197#issuecomment-1264542497 diff --git a/.github/dockerfiles/ov_build/ubuntu_22_04_arm64_cross_compilation/Dockerfile b/.github/dockerfiles/ov_build/ubuntu_22_04_arm64_cross_compilation/Dockerfile index 524de9fc22ad..c5cfa4a8bd74 100644 --- a/.github/dockerfiles/ov_build/ubuntu_22_04_arm64_cross_compilation/Dockerfile +++ b/.github/dockerfiles/ov_build/ubuntu_22_04_arm64_cross_compilation/Dockerfile @@ -1,3 +1,6 @@ +# Copyright (C) 2018-2026 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + ARG REGISTRY="docker.io" FROM ${REGISTRY}/library/ubuntu:22.04 @@ -127,6 +130,9 @@ ENV PIP_CACHE_DIR=/mount/caches/pip/linux/${PIP_VERSION} RUN python3.11 -m venv venv ENV PATH="/venv/bin:$PATH" +# Install newer ninja (apt ships 1.10.1 on 22.04 which lacks --quiet) +RUN python3 -m pip install --no-cache-dir ninja==1.13.0 + # Install Node ENV NODE_VERSION=21.7.3 ENV NVM_DIR=/.nvm diff --git a/.github/dockerfiles/ov_build/ubuntu_22_04_riscv/Dockerfile b/.github/dockerfiles/ov_build/ubuntu_22_04_riscv/Dockerfile index 113cd7af65fb..bdc502f27562 100644 --- a/.github/dockerfiles/ov_build/ubuntu_22_04_riscv/Dockerfile +++ b/.github/dockerfiles/ov_build/ubuntu_22_04_riscv/Dockerfile @@ -1,3 +1,6 @@ +# Copyright (C) 2018-2026 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + ARG REGISTRY="docker.io" FROM ${REGISTRY}/library/ubuntu:22.04 @@ -88,3 +91,6 @@ RUN curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py && \ python3 get-pip.py --no-cache-dir pip==${PIP_VERSION} && \ rm -f get-pip.py +# Install newer ninja (apt ships 1.10.1 on 22.04 which lacks --quiet) +RUN python3 -m pip install --no-cache-dir ninja==1.13.0 + diff --git a/.github/dockerfiles/ov_build/ubuntu_22_04_x64/Dockerfile b/.github/dockerfiles/ov_build/ubuntu_22_04_x64/Dockerfile index 2044483af2c1..c4ed856a569d 100644 --- a/.github/dockerfiles/ov_build/ubuntu_22_04_x64/Dockerfile +++ b/.github/dockerfiles/ov_build/ubuntu_22_04_x64/Dockerfile @@ -1,3 +1,6 @@ +# Copyright (C) 2018-2026 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + ARG REGISTRY="docker.io" FROM ${REGISTRY}/library/ubuntu:22.04 @@ -94,6 +97,9 @@ RUN curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py && \ RUN python3.11 -m venv venv ENV PATH="/venv/bin:$SCCACHE_HOME:$PATH" +# Install newer ninja (apt ships 1.10.1 on 22.04 which lacks --quiet) +RUN python3 -m pip install --no-cache-dir ninja==1.13.0 + ENV PIP_CACHE_DIR=/mount/caches/pip/linux/${PIP_VERSION} # ONNX Runtime, see https://github.com/microsoft/onnxruntime/issues/13197#issuecomment-1264542497 diff --git a/.github/dockerfiles/ov_build/ubuntu_22_04_x64_cc/Dockerfile b/.github/dockerfiles/ov_build/ubuntu_22_04_x64_cc/Dockerfile index dea3e8c26252..04dd201466c8 100644 --- a/.github/dockerfiles/ov_build/ubuntu_22_04_x64_cc/Dockerfile +++ b/.github/dockerfiles/ov_build/ubuntu_22_04_x64_cc/Dockerfile @@ -1,3 +1,6 @@ +# Copyright (C) 2018-2026 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + ARG REGISTRY="docker.io" FROM ${REGISTRY}/library/ubuntu:22.04 @@ -73,4 +76,7 @@ RUN curl https://bootstrap.pypa.io/pip/3.8/get-pip.py -o get-pip-3.8.py && \ RUN python3.11 -m venv venv ENV PATH="/venv/bin:$SCCACHE_HOME:$PATH" +# Install newer ninja (apt ships 1.10.1 on 22.04 which lacks --quiet) +RUN python3 -m pip install --no-cache-dir ninja==1.13.0 + ENV PIP_CACHE_DIR=/mount/caches/pip/linux/${PIP_VERSION} diff --git a/.github/dockerfiles/ov_build/ubuntu_22_04_x64_docker/Dockerfile b/.github/dockerfiles/ov_build/ubuntu_22_04_x64_docker/Dockerfile index 2d5bc1c87806..8a811933e28b 100644 --- a/.github/dockerfiles/ov_build/ubuntu_22_04_x64_docker/Dockerfile +++ b/.github/dockerfiles/ov_build/ubuntu_22_04_x64_docker/Dockerfile @@ -1,3 +1,6 @@ +# Copyright (C) 2018-2026 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + ARG REGISTRY="docker.io" FROM ${REGISTRY}/library/ubuntu:22.04 diff --git a/.github/dockerfiles/ov_build/ubuntu_22_04_x64_dpcpp/Dockerfile b/.github/dockerfiles/ov_build/ubuntu_22_04_x64_dpcpp/Dockerfile index 23111f59dd69..658a48ef2810 100644 --- a/.github/dockerfiles/ov_build/ubuntu_22_04_x64_dpcpp/Dockerfile +++ b/.github/dockerfiles/ov_build/ubuntu_22_04_x64_dpcpp/Dockerfile @@ -1,3 +1,6 @@ +# Copyright (C) 2018-2026 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + ARG REGISTRY="docker.io" FROM ${REGISTRY}/library/ubuntu:22.04 @@ -74,6 +77,9 @@ RUN curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py && \ RUN python3.11 -m venv venv ENV PATH="/venv/bin:$SCCACHE_HOME:$PATH" +# Install newer ninja (apt ships 1.10.1 on 22.04 which lacks --quiet) +RUN python3 -m pip install --no-cache-dir ninja==1.13.0 + ENV PIP_CACHE_DIR=/mount/caches/pip/linux/${PIP_VERSION} # OneAPI env diff --git a/.github/dockerfiles/ov_build/ubuntu_22_04_x64_nvidia/Dockerfile b/.github/dockerfiles/ov_build/ubuntu_22_04_x64_nvidia/Dockerfile index 8c6bdf43bda3..40c399eed38c 100644 --- a/.github/dockerfiles/ov_build/ubuntu_22_04_x64_nvidia/Dockerfile +++ b/.github/dockerfiles/ov_build/ubuntu_22_04_x64_nvidia/Dockerfile @@ -1,3 +1,6 @@ +# Copyright (C) 2018-2026 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + ARG REGISTRY="docker.io" FROM ${REGISTRY}/nvidia/cuda:11.8.0-runtime-ubuntu22.04 @@ -72,4 +75,7 @@ RUN curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py && \ RUN python3.11 -m venv venv ENV PATH="/venv/bin:$SCCACHE_HOME:$PATH" +# Install newer ninja (apt ships 1.10.1 on 22.04 which lacks --quiet) +RUN python3 -m pip install --no-cache-dir ninja==1.13.0 + ENV PIP_CACHE_DIR=/mount/caches/pip/linux/${PIP_VERSION} diff --git a/.github/dockerfiles/ov_build/ubuntu_24_04_x64/Dockerfile b/.github/dockerfiles/ov_build/ubuntu_24_04_x64/Dockerfile index 8d4899e6bdfd..27e42870f206 100644 --- a/.github/dockerfiles/ov_build/ubuntu_24_04_x64/Dockerfile +++ b/.github/dockerfiles/ov_build/ubuntu_24_04_x64/Dockerfile @@ -1,3 +1,6 @@ +# Copyright (C) 2018-2026 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + ARG REGISTRY="docker.io" FROM ${REGISTRY}/library/ubuntu:24.04 @@ -84,4 +87,7 @@ RUN curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py && \ python3.14 -m pip install --upgrade pip==${PIP_VERSION} && \ python3.14t get-pip.py --no-cache-dir pip==${PIP_VERSION} +# Install newer ninja (apt ships 1.10.1 on 22.04 / 1.11.1 on 24.04; pin for consistency) +RUN python3 -m pip install --no-cache-dir ninja==1.13.0 + ENV PIP_CACHE_DIR=/mount/caches/pip/linux/${PIP_VERSION} diff --git a/.github/dockerfiles/ov_build/webassembly/Dockerfile b/.github/dockerfiles/ov_build/webassembly/Dockerfile index 65717650e9a0..1299c4956a15 100644 --- a/.github/dockerfiles/ov_build/webassembly/Dockerfile +++ b/.github/dockerfiles/ov_build/webassembly/Dockerfile @@ -1,3 +1,6 @@ +# Copyright (C) 2018-2026 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + ARG REGISTRY="docker.io" FROM ${REGISTRY}/emscripten/emsdk:3.1.61 diff --git a/.github/dockerfiles/ov_test/debian_10_arm/Dockerfile b/.github/dockerfiles/ov_test/debian_10_arm/Dockerfile index 21252c0bbf70..5acd3c2772d1 100644 --- a/.github/dockerfiles/ov_test/debian_10_arm/Dockerfile +++ b/.github/dockerfiles/ov_test/debian_10_arm/Dockerfile @@ -1,3 +1,6 @@ +# Copyright (C) 2018-2026 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + ARG REGISTRY="docker.io" FROM ${REGISTRY}/library/debian:10.13 diff --git a/.github/dockerfiles/ov_test/debian_10_py310/Dockerfile b/.github/dockerfiles/ov_test/debian_10_py310/Dockerfile index a79ed2206f9e..1cef7c2eccee 100644 --- a/.github/dockerfiles/ov_test/debian_10_py310/Dockerfile +++ b/.github/dockerfiles/ov_test/debian_10_py310/Dockerfile @@ -1,3 +1,6 @@ +# Copyright (C) 2018-2026 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + ARG REGISTRY="docker.io" FROM ${REGISTRY}/library/debian:10.13 diff --git a/.github/dockerfiles/ov_test/fedora_33/Dockerfile b/.github/dockerfiles/ov_test/fedora_33/Dockerfile index 42e0ba78268f..090b6fd52efc 100644 --- a/.github/dockerfiles/ov_test/fedora_33/Dockerfile +++ b/.github/dockerfiles/ov_test/fedora_33/Dockerfile @@ -1,3 +1,6 @@ +# Copyright (C) 2018-2026 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + ARG REGISTRY="docker.io" FROM ${REGISTRY}/library/fedora:35 diff --git a/.github/dockerfiles/ov_test/ubuntu_22_04_android/Dockerfile b/.github/dockerfiles/ov_test/ubuntu_22_04_android/Dockerfile index 89b02e12155b..950694c3dcd1 100644 --- a/.github/dockerfiles/ov_test/ubuntu_22_04_android/Dockerfile +++ b/.github/dockerfiles/ov_test/ubuntu_22_04_android/Dockerfile @@ -1,3 +1,6 @@ +# Copyright (C) 2018-2026 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + ARG REGISTRY="docker.io" FROM ${REGISTRY}/library/ubuntu:22.04 diff --git a/.github/dockerfiles/ov_test/ubuntu_22_04_arm64/Dockerfile b/.github/dockerfiles/ov_test/ubuntu_22_04_arm64/Dockerfile index dcc40bac7c48..f3a06354dcfd 100644 --- a/.github/dockerfiles/ov_test/ubuntu_22_04_arm64/Dockerfile +++ b/.github/dockerfiles/ov_test/ubuntu_22_04_arm64/Dockerfile @@ -1,3 +1,6 @@ +# Copyright (C) 2018-2026 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + ARG REGISTRY="docker.io" FROM ${REGISTRY}/library/ubuntu:22.04 diff --git a/.github/dockerfiles/ov_test/ubuntu_22_04_riscv_xuantie/Dockerfile b/.github/dockerfiles/ov_test/ubuntu_22_04_riscv/Dockerfile similarity index 53% rename from .github/dockerfiles/ov_test/ubuntu_22_04_riscv_xuantie/Dockerfile rename to .github/dockerfiles/ov_test/ubuntu_22_04_riscv/Dockerfile index 26a32f0fcef0..da4f22e1131e 100644 --- a/.github/dockerfiles/ov_test/ubuntu_22_04_riscv_xuantie/Dockerfile +++ b/.github/dockerfiles/ov_test/ubuntu_22_04_riscv/Dockerfile @@ -1,3 +1,6 @@ +# Copyright (C) 2018-2026 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + ARG REGISTRY="docker.io" FROM ${REGISTRY}/library/ubuntu:22.04 @@ -26,6 +29,10 @@ RUN apt-get update && \ tzdata \ # parallel gzip pigz \ + # Python + python3 \ + python3-tomli \ + python3-venv \ # Compilers gcc \ g++ \ @@ -41,16 +48,17 @@ RUN apt-get update && \ && \ rm -rf /var/lib/apt/lists/* -# build xuintie qemu emulator only -ARG XUANTIE_VERSION="V2.10.2" -ARG XUANTIE_REPO="https://github.com/XUANTIE-RV/xuantie-gnu-toolchain" -ARG XUINTIE_PATH="/opt/riscv" -ARG XUINTIE_TMP_PATH="/tmp/xuantie" -ARG XUINTIE_SRC="/tmp/xuantie/src" - -RUN mkdir -p ${XUINTIE_TMP_PATH} && cd ${XUINTIE_TMP_PATH} && \ - git clone --branch ${XUANTIE_VERSION} --depth 1 ${XUANTIE_REPO} ${XUINTIE_SRC} && \ - cd ${XUINTIE_SRC} && git submodule update --init -- qemu && \ - cd ${XUINTIE_SRC}/qemu && ./configure --prefix=${XUINTIE_PATH} --interp-prefix=/usr/riscv64-linux-gnu --target-list=riscv64-linux-user && \ - make -j$(nproc) && make install && \ - rm -rf ${XUINTIE_TMP_PATH} +# build riscv-collab qemu emulator only +ARG RISCV_GNU_TOOLCHAIN_REF="2026.05.06" +ARG RISCV_GNU_TOOLCHAIN_REPO="https://github.com/riscv-collab/riscv-gnu-toolchain.git" +ARG RISCV_TOOLCHAIN_PATH="/opt/riscv" +ARG RISCV_TOOLCHAIN_TMP_PATH="/tmp/riscv-gnu-toolchain" +ARG RISCV_TOOLCHAIN_SRC="/tmp/riscv-gnu-toolchain/src" + +RUN mkdir -p ${RISCV_TOOLCHAIN_TMP_PATH} && cd ${RISCV_TOOLCHAIN_TMP_PATH} && \ + git clone --branch ${RISCV_GNU_TOOLCHAIN_REF} --depth 1 ${RISCV_GNU_TOOLCHAIN_REPO} ${RISCV_TOOLCHAIN_SRC} && \ + cd ${RISCV_TOOLCHAIN_SRC} && git submodule update --init -- qemu && \ + cd ${RISCV_TOOLCHAIN_SRC}/qemu && \ + ./configure --prefix=${RISCV_TOOLCHAIN_PATH} --interp-prefix=/usr/riscv64-linux-gnu --target-list=riscv64-linux-user && \ + make -j"$(nproc)" && make install && \ + rm -rf ${RISCV_TOOLCHAIN_TMP_PATH} diff --git a/.github/dockerfiles/ov_test/ubuntu_22_04_x64/Dockerfile b/.github/dockerfiles/ov_test/ubuntu_22_04_x64/Dockerfile index 9ea31664c584..bf3a588c0057 100644 --- a/.github/dockerfiles/ov_test/ubuntu_22_04_x64/Dockerfile +++ b/.github/dockerfiles/ov_test/ubuntu_22_04_x64/Dockerfile @@ -1,3 +1,6 @@ +# Copyright (C) 2018-2026 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + ARG REGISTRY="docker.io" FROM ${REGISTRY}/library/ubuntu:22.04 diff --git a/.github/dockerfiles/ov_test/ubuntu_22_04_x64_code_style/Dockerfile b/.github/dockerfiles/ov_test/ubuntu_22_04_x64_code_style/Dockerfile index e094015b4184..75e0db733a2d 100644 --- a/.github/dockerfiles/ov_test/ubuntu_22_04_x64_code_style/Dockerfile +++ b/.github/dockerfiles/ov_test/ubuntu_22_04_x64_code_style/Dockerfile @@ -1,3 +1,6 @@ +# Copyright (C) 2018-2026 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + ARG REGISTRY="docker.io" FROM ${REGISTRY}/library/ubuntu:22.04 diff --git a/.github/dockerfiles/ov_test/ubuntu_22_04_x64_igpu/Dockerfile b/.github/dockerfiles/ov_test/ubuntu_22_04_x64_igpu/Dockerfile new file mode 100644 index 000000000000..190fa446ade1 --- /dev/null +++ b/.github/dockerfiles/ov_test/ubuntu_22_04_x64_igpu/Dockerfile @@ -0,0 +1,87 @@ +ARG REGISTRY="docker.io" +FROM ${REGISTRY}/library/ubuntu:22.04 + +USER root + +# APT configuration +RUN echo 'Acquire::Retries "10";' > /etc/apt/apt.conf && \ + echo 'APT::Get::Assume-Yes "true";' >> /etc/apt/apt.conf && \ + echo 'APT::Get::Fix-Broken "true";' >> /etc/apt/apt.conf && \ + echo 'APT::Get::no-install-recommends "true";' >> /etc/apt/apt.conf + +ENV DEBIAN_FRONTEND="noninteractive" \ + TZ="Europe/London" + +RUN apt-get update && \ + apt-get install software-properties-common && \ + add-apt-repository --yes --no-update ppa:git-core/ppa && \ + add-apt-repository --yes --no-update ppa:deadsnakes/ppa && \ + apt-get update && \ + apt-get install \ + curl \ + git \ + ca-certificates \ + gpg-agent \ + tzdata \ + lcov \ + pigz \ + clang \ + python3.11-dev \ + python3.11-distutils \ + python3.11-venv \ + libhdf5-dev \ + xvfb \ + libgtk-3-0 \ + libgbm1 \ + libnss3 \ + wget \ + ffmpeg \ + libsm6 \ + libxext6 \ + libgl1 \ + libgl1-mesa-glx \ + libglib2.0-0 \ + clinfo \ + libegl-mesa0 \ + libegl1-mesa \ + libegl1-mesa-dev \ + libgl1-mesa-dev \ + libgl1-mesa-dri \ + libglapi-mesa \ + libgles2-mesa-dev \ + libglx-mesa0 \ + libxatracker2 \ + mesa-va-drivers \ + mesa-vdpau-drivers \ + mesa-vulkan-drivers \ + va-driver-all && \ + rm -rf /var/lib/apt/lists/* + +ADD scripts/install_dependencies/install_openvino_dependencies.sh /install_openvino_dependencies.sh +RUN chmod +x /install_openvino_dependencies.sh && \ + /install_openvino_dependencies.sh -c=core -c=dev -c=gpu -y && \ + rm -rf /var/lib/apt/lists/* + +RUN wget https://raw.githubusercontent.com/google/gtest-parallel/master/gtest_parallel.py -O /usr/local/bin/gtest_parallel.py && \ + chmod +x /usr/local/bin/gtest_parallel.py + +ENV PIP_VERSION="24.0" +RUN curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py && \ + python3 get-pip.py --no-cache-dir pip==${PIP_VERSION} && \ + python3.11 get-pip.py --no-cache-dir pip==${PIP_VERSION} && \ + rm -f get-pip.py + +# Install igpu drivers +RUN wget https://github.com/intel/intel-graphics-compiler/releases/download/igc-1.0.15985.7/intel-igc-core_1.0.15985.7_amd64.deb && \ + wget https://github.com/intel/intel-graphics-compiler/releases/download/igc-1.0.15985.7/intel-igc-opencl_1.0.15985.7_amd64.deb && \ + wget https://github.com/intel/compute-runtime/releases/download/24.05.28454.6/intel-level-zero-gpu_1.3.28454.6_amd64.deb && \ + wget https://github.com/intel/compute-runtime/releases/download/24.05.28454.6/intel-opencl-icd_24.05.28454.6_amd64.deb && \ + wget https://github.com/intel/compute-runtime/releases/download/24.05.28454.6/libigdgmm12_22.3.11_amd64.deb && \ + dpkg -i *.deb && \ + rm -f *.deb + +RUN python3.11 -m venv venv +ENV PATH="/venv/bin:$SCCACHE_HOME:$PATH" + +ENV PIP_CACHE_DIR=/mount/caches/pip/linux/${PIP_VERSION} +ENV PIP_INSTALL_PATH=/venv/lib/python3.11/site-packages diff --git a/.github/dockerfiles/ov_test/ubuntu_20_04_x64_py313/Dockerfile b/.github/dockerfiles/ov_test/ubuntu_22_04_x64_py313/Dockerfile similarity index 84% rename from .github/dockerfiles/ov_test/ubuntu_20_04_x64_py313/Dockerfile rename to .github/dockerfiles/ov_test/ubuntu_22_04_x64_py313/Dockerfile index 57d2bb7c2264..2285456e4e0d 100644 --- a/.github/dockerfiles/ov_test/ubuntu_20_04_x64_py313/Dockerfile +++ b/.github/dockerfiles/ov_test/ubuntu_22_04_x64_py313/Dockerfile @@ -1,5 +1,8 @@ +# Copyright (C) 2018-2026 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + ARG REGISTRY="docker.io" -FROM ${REGISTRY}/library/ubuntu:20.04 +FROM ${REGISTRY}/library/ubuntu:22.04 USER root @@ -39,13 +42,12 @@ RUN chmod +x /install_openvino_dependencies.sh && \ # Setup pip ENV PIP_VERSION="24.0" RUN curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py && \ - curl https://bootstrap.pypa.io/pip/3.8/get-pip.py -o get-pip-3-8.py && \ - python3 get-pip-3-8.py --no-cache-dir pip==${PIP_VERSION} && \ + python3 get-pip.py --no-cache-dir pip==${PIP_VERSION} && \ python3.13 get-pip.py --no-cache-dir pip==${PIP_VERSION} && \ rm -f get-pip.py -# Use Python 3.13 as default instead of Python 3.8 -# Using venv here 'cause other methods to switch the default Python on Ubuntu 20 break both system and wheels build +# Use Python 3.13 as default instead of Python 3.10 +# Using venv here 'cause other methods to switch the default Python on Ubuntu 22 break both system and wheels build RUN python3.13 -m venv venv ENV PATH="/venv/bin:$PATH" diff --git a/.github/dockerfiles/ov_test/ubuntu_22_04_x64_py314/Dockerfile b/.github/dockerfiles/ov_test/ubuntu_22_04_x64_py314/Dockerfile index 9a24fd23559d..149ad1a7184a 100644 --- a/.github/dockerfiles/ov_test/ubuntu_22_04_x64_py314/Dockerfile +++ b/.github/dockerfiles/ov_test/ubuntu_22_04_x64_py314/Dockerfile @@ -1,3 +1,6 @@ +# Copyright (C) 2018-2026 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + ARG REGISTRY="docker.io" FROM ${REGISTRY}/library/ubuntu:22.04 diff --git a/.github/dockerfiles/ov_test/ubuntu_22_04_x64_py314t/Dockerfile b/.github/dockerfiles/ov_test/ubuntu_22_04_x64_py314t/Dockerfile index c3ca5838273e..7549156b2ec1 100644 --- a/.github/dockerfiles/ov_test/ubuntu_22_04_x64_py314t/Dockerfile +++ b/.github/dockerfiles/ov_test/ubuntu_22_04_x64_py314t/Dockerfile @@ -1,3 +1,6 @@ +# Copyright (C) 2018-2026 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + ARG REGISTRY="docker.io" FROM ${REGISTRY}/library/ubuntu:22.04 diff --git a/.github/dockerfiles/ov_test/ubuntu_24_04_x64/Dockerfile b/.github/dockerfiles/ov_test/ubuntu_24_04_x64/Dockerfile index 630d7bed6e19..187df1b09bf4 100644 --- a/.github/dockerfiles/ov_test/ubuntu_24_04_x64/Dockerfile +++ b/.github/dockerfiles/ov_test/ubuntu_24_04_x64/Dockerfile @@ -1,3 +1,6 @@ +# Copyright (C) 2018-2026 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + ARG REGISTRY="docker.io" FROM ${REGISTRY}/library/ubuntu:24.04 diff --git a/.github/dockerfiles/ov_test/ubuntu_24_04_x64_code_style/Dockerfile b/.github/dockerfiles/ov_test/ubuntu_24_04_x64_code_style/Dockerfile index 65fbc30e098e..4a186fc747b8 100644 --- a/.github/dockerfiles/ov_test/ubuntu_24_04_x64_code_style/Dockerfile +++ b/.github/dockerfiles/ov_test/ubuntu_24_04_x64_code_style/Dockerfile @@ -1,3 +1,6 @@ +# Copyright (C) 2018-2026 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + ARG REGISTRY="docker.io" FROM ${REGISTRY}/library/ubuntu:24.04 diff --git a/.github/dockerfiles/ov_test/ubuntu_24_04_x64_dgpu/Dockerfile b/.github/dockerfiles/ov_test/ubuntu_24_04_x64_dgpu/Dockerfile new file mode 100644 index 000000000000..b78f7a921d50 --- /dev/null +++ b/.github/dockerfiles/ov_test/ubuntu_24_04_x64_dgpu/Dockerfile @@ -0,0 +1,64 @@ +ARG REGISTRY="docker.io" +FROM ${REGISTRY}/library/ubuntu:24.04 + +USER root + +# APT configuration +RUN echo 'Acquire::Retries "10";' > /etc/apt/apt.conf && \ + echo 'APT::Get::Assume-Yes "true";' >> /etc/apt/apt.conf && \ + echo 'APT::Get::Fix-Broken "true";' >> /etc/apt/apt.conf && \ + echo 'APT::Get::no-install-recommends "true";' >> /etc/apt/apt.conf + +ENV DEBIAN_FRONTEND="noninteractive" \ + TZ="Europe/London" + +RUN apt-get update && \ + apt-get install software-properties-common && \ + apt-get update && \ + apt-get install \ + curl \ + git \ + gpg-agent \ + tzdata \ + wget \ + clinfo \ + ca-certificates \ + gpg-agent \ + tzdata \ + # parallel gzip + pigz \ + # Samples + clang \ + # Python + python3-full \ + libhdf5-dev \ + && \ + rm -rf /var/lib/apt/lists/* + +# Install openvino dependencies +ADD scripts/install_dependencies/install_openvino_dependencies.sh /install_openvino_dependencies.sh +RUN chmod +x /install_openvino_dependencies.sh && \ + /install_openvino_dependencies.sh -c=core -c=dev -c=gpu -y && \ + rm -rf /var/lib/apt/lists/* + +RUN wget https://raw.githubusercontent.com/google/gtest-parallel/master/gtest_parallel.py -O /usr/local/bin/gtest_parallel.py && \ + chmod +x /usr/local/bin/gtest_parallel.py + +# Install dgpu drivers +RUN add-apt-repository -y ppa:kobuk-team/intel-graphics && \ + apt-get update && \ + apt-get install -y libze-intel-gpu1 libze1 intel-metrics-discovery intel-opencl-icd clinfo intel-gsc \ + intel-media-va-driver-non-free libmfx-gen1 libvpl2 libvpl-tools libva-glx2 va-driver-all vainfo \ + libze-dev intel-ocloc libze-intel-gpu-raytracing + +# Create a virtual environment for the system Python as Python in Ubuntu 24 complains about installing +# packages into the system Python +RUN python3 -m venv venv +ENV PATH="/venv/bin:$PATH" + +# Setup pip +ENV PIP_VERSION="24.0" +RUN /venv/bin/python3 -m pip install --upgrade pip==${PIP_VERSION} + +ENV PIP_CACHE_DIR=/mount/caches/pip/linux/${PIP_VERSION} +ENV PIP_INSTALL_PATH=/venv/lib/python3/site-packages diff --git a/.github/github_org_control/check_pr.py b/.github/github_org_control/check_pr.py index 25692e13f87c..ea9b6a5e17e3 100644 --- a/.github/github_org_control/check_pr.py +++ b/.github/github_org_control/check_pr.py @@ -30,11 +30,7 @@ class PrType(Enum): def get_pr_labels(pull): """Gets PR labels as set""" - pr_lables = set() - for label in pull.labels: - pr_lables.add(label.name) - return pr_lables - + return set([label.name for label in pull.labels]) def set_pr_labels(pull, labels): """Sets new PR labels (all previously set labels are removed)""" @@ -56,9 +52,9 @@ def add_pr_labels(pull, labels): def get_pr_type_by_labels(pull): """Gets PR type using labels""" - pr_lables = get_pr_labels(pull) + pr_labels = get_pr_labels(pull) pr_types = set(type.value for type in PrType) - pr_types_labels = pr_lables & pr_types + pr_types_labels = pr_labels & pr_types if not pr_types_labels: return None if len(pr_types_labels) > 1: @@ -68,7 +64,7 @@ def get_pr_type_by_labels(pull): def get_label_by_team_name_re(team_name): - """Generates label by PR reviwer team name using regular expressions""" + """Generates label by PR reviewer team name using regular expressions""" if "admins" in team_name: return "category: ci" re_compile_label = re.compile(rf"{Config().GITHUB_REPO}-(.+)-maintainers") @@ -79,17 +75,17 @@ def get_label_by_team_name_re(team_name): def get_label_by_team_name_map(team_name): - """Generates label by PR reviwer team name using config map""" + """Generates label by PR reviewer team name using config map""" return Config().TEAM_TO_LABEL.get(team_name) def get_category_labels(pull): - """Gets list of category labels by all PR reviwer teams""" + """Gets list of category labels by all PR reviewer teams""" labels = [] - pr_lables = get_pr_labels(pull) + pr_labels = get_pr_labels(pull) for reviewer_team in pull.get_review_requests()[1]: reviewer_label = get_label_by_team_name_map(reviewer_team.name) - if reviewer_label and reviewer_label not in pr_lables: + if reviewer_label and reviewer_label not in pr_labels: labels.append(reviewer_label) return labels @@ -171,7 +167,7 @@ def get_wrong_commits(pull): commit.raw_data["commit"]["verification"]["reason"], ) if pr_author_email != commit_author_email or pr_author_email != commit_committer_email: - print(" WARNING: Commit emails and GitHub PR author public email are differnt") + print(" WARNING: Commit emails and GitHub PR author public email are different") return wrong_commits diff --git a/.github/prompts/add-core-op.prompt.md b/.github/prompts/add-core-op.prompt.md new file mode 100644 index 000000000000..895324422859 --- /dev/null +++ b/.github/prompts/add-core-op.prompt.md @@ -0,0 +1,6 @@ +--- +mode: agent +description: Add a new operation to the OpenVINO Core opset — class definition, shape inference, registration, reference kernel, and documentation. +--- + +#file:../agents/skills/add-core-op/SKILL.md diff --git a/.github/prompts/add-cpu-op.prompt.md b/.github/prompts/add-cpu-op.prompt.md new file mode 100644 index 000000000000..954ee40ac2a9 --- /dev/null +++ b/.github/prompts/add-cpu-op.prompt.md @@ -0,0 +1,6 @@ +--- +mode: agent +description: Add a new operation to the OpenVINO CPU plugin — ISA-aware kernel (AVX2/AVX-512/AMX), oneDNN-backed paths, and functional tests. +--- + +#file:../agents/skills/add-cpu-op/SKILL.md diff --git a/.github/prompts/add-fe-op.prompt.md b/.github/prompts/add-fe-op.prompt.md new file mode 100644 index 000000000000..25688ca30d33 --- /dev/null +++ b/.github/prompts/add-fe-op.prompt.md @@ -0,0 +1,6 @@ +--- +mode: agent +description: Add a new operation to the OpenVINO Frontend — translate a framework op (ONNX/PyTorch/TF) into an OpenVINO graph node. +--- + +#file:../agents/skills/add-fe-op/SKILL.md diff --git a/.github/prompts/add-fusion-transformation.prompt.md b/.github/prompts/add-fusion-transformation.prompt.md new file mode 100644 index 000000000000..2693cfdd3909 --- /dev/null +++ b/.github/prompts/add-fusion-transformation.prompt.md @@ -0,0 +1,6 @@ +--- +mode: agent +description: Add a graph fusion transformation to OpenVINO Core transformations pipeline — MatcherPass/FunctionPass, registration, and tests. +--- + +#file:../agents/skills/add-fusion-transformation/SKILL.md diff --git a/.github/prompts/add-gpu-op.prompt.md b/.github/prompts/add-gpu-op.prompt.md new file mode 100644 index 000000000000..1efe4fef70d1 --- /dev/null +++ b/.github/prompts/add-gpu-op.prompt.md @@ -0,0 +1,6 @@ +--- +mode: agent +description: Add a new operation to the OpenVINO GPU plugin — OpenCL kernel design, oneDNN-backed paths, sub-group / LWS tuning, and tests. +--- + +#file:../agents/skills/add-gpu-op/SKILL.md diff --git a/.github/prompts/analyze-and-convert.prompt.md b/.github/prompts/analyze-and-convert.prompt.md new file mode 100644 index 000000000000..2dd8306bfeab --- /dev/null +++ b/.github/prompts/analyze-and-convert.prompt.md @@ -0,0 +1,6 @@ +--- +mode: agent +description: Analyze a model and attempt conversion to OpenVINO IR — probe properties, try conversion strategies, classify failures, and produce a structured report. +--- + +#file:../agents/skills/analyze-and-convert/SKILL.md diff --git a/.github/prompts/conversion-issues.prompt.md b/.github/prompts/conversion-issues.prompt.md new file mode 100644 index 000000000000..3428138f012f --- /dev/null +++ b/.github/prompts/conversion-issues.prompt.md @@ -0,0 +1,6 @@ +--- +mode: agent +description: Diagnose and fix model conversion issues — auto-detect failure type (unsupported op, shape inference, dtype mismatch) and route to the correct fix strategy. +--- + +#file:../agents/skills/conversion-issues/SKILL.md diff --git a/.github/prompts/new-arch-validation-checklist.prompt.md b/.github/prompts/new-arch-validation-checklist.prompt.md new file mode 100644 index 000000000000..777fa156cbbc --- /dev/null +++ b/.github/prompts/new-arch-validation-checklist.prompt.md @@ -0,0 +1,15 @@ +--- +mode: ask +description: Run the new-architecture validation checklist on the current patch or changed files. Use after implementing a new op or fixing an accuracy regression before submitting a PR. +--- + +Run the **new-architecture validation checklist** defined in +`.github/agents-prototype/skills/new-arch-validation-checklist.md`. + +1. Read the checklist file now. +2. For each item in the checklist, examine the current changes (`git diff HEAD` or the files + listed in `agent-results/*/files_created`) and determine whether the item is `PASS`, + `FAIL`, or `N/A`. +3. Report results in a markdown table with columns: `Category`, `Item`, `Status`, `Notes`. +4. For any `FAIL` item, provide a specific fix recommendation tied to the exact changed code. +5. Summarise: how many PASS / FAIL / N/A, and whether the patch is ready to submit. diff --git a/.github/prompts/python-bootstrap.prompt.md b/.github/prompts/python-bootstrap.prompt.md new file mode 100644 index 000000000000..9be2b667fe27 --- /dev/null +++ b/.github/prompts/python-bootstrap.prompt.md @@ -0,0 +1,6 @@ +--- +mode: agent +description: Bootstrap Python environment for OpenVINO development. Path A for release packages, Path B for source builds. +--- + +#file:../agents/skills/python-bootstrap/SKILL.md diff --git a/.github/prompts/submit-draft-pr.prompt.md b/.github/prompts/submit-draft-pr.prompt.md new file mode 100644 index 000000000000..4ec314e71d82 --- /dev/null +++ b/.github/prompts/submit-draft-pr.prompt.md @@ -0,0 +1,6 @@ +--- +mode: agent +description: Create a draft pull request with existing-PR deduplication. Centralized PR creation skill for all OpenVINO agents. +--- + +#file:../agents/skills/submit-draft-pr/SKILL.md diff --git a/.github/prompts/verify-conversion.prompt.md b/.github/prompts/verify-conversion.prompt.md new file mode 100644 index 000000000000..223f6fe744fb --- /dev/null +++ b/.github/prompts/verify-conversion.prompt.md @@ -0,0 +1,6 @@ +--- +mode: agent +description: E2E verification gate — convert a model and run inference to verify numerically sane output through the OV plugin layer. +--- + +#file:../agents/skills/verify-conversion/SKILL.md diff --git a/.github/scripts/coverage/ci_reports.py b/.github/scripts/coverage/ci_reports.py deleted file mode 100644 index 06f3e6e789c7..000000000000 --- a/.github/scripts/coverage/ci_reports.py +++ /dev/null @@ -1,365 +0,0 @@ -# Copyright (C) 2018-2026 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 - -from __future__ import annotations - -import argparse -import csv -import json -from pathlib import Path -import shutil -import sys - - -SCRIPT_DIR = Path(__file__).resolve().parent -CONFIG_DIR = SCRIPT_DIR / "config" -if str(SCRIPT_DIR) not in sys.path: - sys.path.insert(0, str(SCRIPT_DIR)) - -from coverage import load_cpp_tests, load_js_tests, load_python_tests - - -METADATA_FILE = "coverage-artifact-metadata.json" - - -SUITE_DEFS = { - "cpp": { - "label": "C++", - "artifacts_dir": "cpp", - "stats_file": "cpp-coverage-stats.env", - "duration_file": "cpp-test-durations.csv", - "coverage_file": "coverage.info", - "total_key": "CXX_TESTS_TOTAL", - "executed_key": "CXX_TESTS_EXECUTED", - "passed_key": "CXX_TESTS_PASSED", - "failed_key": "CXX_TESTS_FAILED", - "skipped_key": "CXX_TESTS_SKIPPED", - "not_run_key": "CXX_TESTS_NOT_RUN", - }, - "python": { - "label": "Python", - "artifacts_dir": "python", - "stats_file": "python-coverage-stats.env", - "duration_file": "python-test-durations.csv", - "coverage_file": "python-coverage.xml", - "extra_files": ["coverage.info"], - "total_key": "PY_TESTS_TOTAL", - "executed_key": None, - "passed_key": "PY_TESTS_PASSED", - "failed_key": "PY_TESTS_FAILED", - "skipped_key": "PY_TESTS_SKIPPED", - "not_run_key": "PY_TESTS_NOT_RUN", - }, - "js": { - "label": "JS", - "artifacts_dir": "js", - "stats_file": "js-coverage-stats.env", - "duration_file": "js-test-durations.csv", - "coverage_file": "js-lcov.info", - "extra_files": ["coverage.info"], - "total_key": "JS_TESTS_TOTAL", - "executed_key": None, - "passed_key": "JS_TESTS_PASSED", - "failed_key": "JS_TESTS_FAILED", - "skipped_key": "JS_TESTS_SKIPPED", - "not_run_key": "JS_TESTS_NOT_RUN", - }, -} -def _read_json_file(path: Path) -> dict[str, object]: - if not path.is_file(): - return {} - return json.loads(path.read_text(encoding="utf-8")) - - -def _read_env_file(path: Path) -> dict[str, str]: - values: dict[str, str] = {} - if not path.is_file(): - return values - for raw_line in path.read_text(encoding="utf-8").splitlines(): - if "=" not in raw_line: - continue - key, value = raw_line.split("=", 1) - values[key.strip()] = value.strip() - return values - - -def _to_int(values: dict[str, str], key: str | None) -> int: - if not key: - return 0 - try: - return int(values.get(key, "0").strip()) - except ValueError: - return 0 - - -def _load_test_names(*, workspace: Path, suite: str, profile: str) -> list[str]: - if suite == "cpp": - return [test.name for test in load_cpp_tests(CONFIG_DIR / "tests_cpp.yml", profile)] - if suite == "python": - return [test.name for test in load_python_tests(CONFIG_DIR / "tests_python.yml", profile)] - if suite == "js": - return [test.name for test in load_js_tests(CONFIG_DIR / "tests_js.yml", profile)] - raise ValueError(f"Unsupported suite: {suite}") - - -def collect_suite_results( - *, - workspace: Path, - suite: str, - profile: str, - lane: str, - artifact_name: str, - artifact_dir: Path, -) -> None: - suite_def = SUITE_DEFS[suite] - artifact_dir.mkdir(parents=True, exist_ok=True) - - tests = _load_test_names(workspace=workspace, suite=suite, profile=profile) - total = len(tests) - - metadata = {"suite": suite, "lane": lane, "artifact_name": artifact_name} - (artifact_dir / METADATA_FILE).write_text(json.dumps(metadata, separators=(",", ":")) + "\n", encoding="utf-8") - - duration_path = artifact_dir / str(suite_def["duration_file"]) - with duration_path.open("w", encoding="utf-8", newline="") as handle: - writer = csv.writer(handle) - writer.writerow(["test_name", "status", "duration_seconds", "duration_minutes"]) - for test_name in tests: - writer.writerow([test_name, "not_run", "0.000", "0.000"]) - - stats_lines = [f"{suite_def['total_key']}={total}"] - if suite_def["executed_key"]: - stats_lines.append(f"{suite_def['executed_key']}=0") - stats_lines.extend( - [ - f"{suite_def['passed_key']}=0", - f"{suite_def['failed_key']}=0", - f"{suite_def['skipped_key']}=0", - f"{suite_def['not_run_key']}={total}", - ] - ) - (artifact_dir / str(suite_def["stats_file"])).write_text("\n".join(stats_lines) + "\n", encoding="utf-8") - - files_to_copy = [ - str(suite_def["duration_file"]), - str(suite_def["stats_file"]), - str(suite_def["coverage_file"]), - *[str(name) for name in suite_def.get("extra_files", [])], - ] - for filename in files_to_copy: - source = workspace / filename - if source.is_file(): - shutil.copy2(source, artifact_dir / filename) - - debug_dir = workspace / ".tmp" / "cpp-coverage-parts" - if debug_dir.is_dir(): - destination = artifact_dir / "native-cpp-debug" - shutil.rmtree(destination, ignore_errors=True) - shutil.copytree(debug_dir, destination) - - python_debug_dir = workspace / ".tmp" / "python-coverage" - if suite == "python" and python_debug_dir.is_dir(): - destination = artifact_dir / "python-coverage-debug" - shutil.rmtree(destination, ignore_errors=True) - shutil.copytree(python_debug_dir, destination) - - -def _collect_artifacts(*, workspace: Path, suite_key: str) -> list[dict[str, object]]: - suite_def = SUITE_DEFS[suite_key] - root = workspace / "artifacts" / suite_def["artifacts_dir"] - if not root.exists(): - return [] - - artifacts: list[dict[str, object]] = [] - for metadata_path in sorted(root.rglob(METADATA_FILE)): - metadata = _read_json_file(metadata_path) - if not metadata or str(metadata.get("suite", "")).strip() != suite_key: - continue - artifacts.append( - { - "artifact_dir": metadata_path.parent, - "artifact_name": str(metadata.get("artifact_name", metadata_path.parent.name)).strip(), - "lane": str(metadata.get("lane", "")).strip() or "-", - } - ) - return artifacts - - -def _flag_patterns(suite: str, lane: str) -> str: - if suite == "cpp": - return f"`cpp-runtime-cpp-{lane}`" - if suite == "python": - return f"`python-api-frontend-layer-ovc-{lane}`, `cpp-runtime-python-{lane}`" - if suite == "js": - return f"`nodejs-bindings-unit-e2e-{lane}`, `cpp-runtime-js-{lane}`" - raise ValueError(f"Unsupported suite: {suite}") - - -def render_summary(*, workspace: Path, summary_file: Path, selection: str, selected_lanes: str) -> None: - rows: list[dict[str, object]] = [] - overall = {"total": 0, "executed": 0, "passed": 0, "failed": 0, "skipped": 0, "not_run": 0} - - for suite_key, suite_def in SUITE_DEFS.items(): - for artifact in _collect_artifacts(workspace=workspace, suite_key=suite_key): - artifact_dir = Path(artifact["artifact_dir"]) - lane = str(artifact["lane"]) - stats = _read_env_file(artifact_dir / str(suite_def["stats_file"])) - total = _to_int(stats, str(suite_def["total_key"])) - passed = _to_int(stats, str(suite_def["passed_key"])) - failed = _to_int(stats, str(suite_def["failed_key"])) - skipped = _to_int(stats, str(suite_def["skipped_key"])) - not_run = _to_int(stats, str(suite_def["not_run_key"])) - executed = _to_int(stats, suite_def["executed_key"]) if suite_def["executed_key"] else max(0, total - skipped - not_run) - coverage_file = artifact_dir / str(suite_def["coverage_file"]) - report_size = coverage_file.stat().st_size if coverage_file.is_file() else 0 - - rows.append( - { - "lane": lane, - "suite": suite_def["label"], - "total": total, - "executed": executed, - "passed": passed, - "failed": failed, - "skipped": skipped, - "not_run": not_run, - "report_size": report_size, - "flags": _flag_patterns(suite_key, lane), - } - ) - overall["total"] += total - overall["executed"] += executed - overall["passed"] += passed - overall["failed"] += failed - overall["skipped"] += skipped - overall["not_run"] += not_run - - pass_rate = (overall["passed"] * 100.0) / overall["executed"] if overall["executed"] else 0.0 - - lines = [ - "## Coverage Summary", - "", - f"**Selection:** `{selection}`", - "", - f"**Active lanes:** `{selected_lanes}`", - "", - f"**Executed pass rate:** `{pass_rate:.1f}%`", - "", - "### Overall", - "| Metric | Value |", - "| --- | ---: |", - f"| Total test units | {overall['total']} |", - f"| Executed | {overall['executed']} |", - f"| Passed | {overall['passed']} |", - f"| Failed | {overall['failed']} |", - f"| Skipped | {overall['skipped']} |", - f"| Not run | {overall['not_run']} |", - "", - "### By Lane And Suite", - "| Lane | Suite | Total | Executed | Passed | Failed | Skipped | Not run | Report Size (bytes) | Codecov Flags |", - "| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | --- |", - ] - - if rows: - for row in sorted(rows, key=lambda item: (str(item["lane"]), str(item["suite"]))): - lines.append( - f"| {row['lane']} | {row['suite']} | {row['total']} | {row['executed']} | " - f"{row['passed']} | {row['failed']} | {row['skipped']} | {row['not_run']} | {row['report_size']} | {row['flags']} |" - ) - else: - lines.append("| - | - | 0 | 0 | 0 | 0 | 0 | 0 | 0 | - |") - - lines.extend(["", "Merged duration artifact: `coverage-test-durations` (`coverage-test-durations-all.csv`)", ""]) - summary_file.write_text("\n".join(lines), encoding="utf-8") - - -def merge_durations(*, workspace: Path, output: Path) -> None: - rows: list[dict[str, str]] = [] - for suite_key, suite_def in SUITE_DEFS.items(): - for artifact in _collect_artifacts(workspace=workspace, suite_key=suite_key): - artifact_dir = Path(artifact["artifact_dir"]) - report = artifact_dir / str(suite_def["duration_file"]) - if not report.is_file(): - continue - artifact_name = str(artifact["artifact_name"]) - lane = str(artifact["lane"]) - with report.open(encoding="utf-8", newline="") as handle: - reader = csv.DictReader(handle) - for row in reader: - rows.append( - { - "suite": suite_key, - "lane": lane, - "artifact": artifact_name, - "test_name": row.get("test_name", ""), - "status": row.get("status", ""), - "duration_seconds": row.get("duration_seconds", "0"), - "duration_minutes": row.get("duration_minutes", "0"), - } - ) - - rows.sort(key=lambda row: (-float(row["duration_seconds"] or 0), row["suite"], row["lane"], row["test_name"])) - with output.open("w", encoding="utf-8", newline="") as handle: - writer = csv.DictWriter( - handle, - fieldnames=["suite", "lane", "artifact", "test_name", "status", "duration_seconds", "duration_minutes"], - ) - writer.writeheader() - writer.writerows(rows) - - print(f"Wrote {len(rows)} duration row(s) to {output}") - - -def _parse_args() -> argparse.Namespace: - parser = argparse.ArgumentParser(description="Coverage CI report helpers") - subparsers = parser.add_subparsers(dest="command", required=True) - - collect = subparsers.add_parser("collect-suite-results", help="Prepare one suite artifact with fallback files and real outputs") - collect.add_argument("--workspace", type=Path, required=True) - collect.add_argument("--suite", choices=sorted(SUITE_DEFS), required=True) - collect.add_argument("--profile", required=True) - collect.add_argument("--lane", required=True) - collect.add_argument("--artifact-name", required=True) - collect.add_argument("--artifact-dir", type=Path, required=True) - - render = subparsers.add_parser("render-summary", help="Render the aggregated GitHub summary from downloaded artifacts") - render.add_argument("--workspace", type=Path, required=True) - render.add_argument("--summary-file", type=Path, required=True) - render.add_argument("--selection", required=True) - render.add_argument("--selected-lanes", required=True) - - merge = subparsers.add_parser("merge-durations", help="Merge suite duration CSV files from downloaded artifacts") - merge.add_argument("--workspace", type=Path, required=True) - merge.add_argument("--output", type=Path, required=True) - - return parser.parse_args() - - -def main() -> int: - args = _parse_args() - if args.command == "collect-suite-results": - collect_suite_results( - workspace=args.workspace.resolve(), - suite=args.suite, - profile=args.profile, - lane=args.lane, - artifact_name=args.artifact_name, - artifact_dir=args.artifact_dir.resolve(), - ) - return 0 - if args.command == "render-summary": - render_summary( - workspace=args.workspace.resolve(), - summary_file=args.summary_file.resolve(), - selection=args.selection, - selected_lanes=args.selected_lanes, - ) - return 0 - if args.command == "merge-durations": - merge_durations(workspace=args.workspace.resolve(), output=args.output.resolve()) - return 0 - raise ValueError(f"Unsupported command: {args.command}") - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/.github/scripts/coverage/config/tests_python.yml b/.github/scripts/coverage/config/tests_python.yml deleted file mode 100644 index f50573ca1ad8..000000000000 --- a/.github/scripts/coverage/config/tests_python.yml +++ /dev/null @@ -1,138 +0,0 @@ -schema_version: 1 -suite: python -tests: - - name: pyopenvino - kind: pytest - target: "${TESTS_DIR}/pyopenvino" - args: "-sv -k 'not cuda' --ignore=${TESTS_DIR}/pyopenvino/tests/test_utils/test_utils.py" - - - name: onnx_python - kind: pytest - target: "${TESTS_DIR}/onnx" - args: "-sv -k 'not cuda' --ignore=${TESTS_DIR}/onnx/test_python/test_zoo_models.py" - - - name: ovc_unit - kind: pytest - target: "${TESTS_DIR}/ovc/unit_tests" - args: "-sv -k 'not cuda'" - - - name: py_frontend - kind: pytest_if_dir - target: "${TESTS_DIR}/layer_tests/py_frontend_tests" - args: "-sv -v" - - - name: tensorflow_lite_layers - kind: pytest_if_dir - target: "${TESTS_DIR}/layer_tests/tensorflow_lite_tests" - env: "TEST_DEVICE=CPU TEST_PRECISION=FP16" - args: "-sv -v" - - - name: tensorflow_layers - kind: pytest_if_dir - target: "${TESTS_DIR}/layer_tests/tensorflow_tests" - env: "TEST_DEVICE=CPU TEST_PRECISION=FP16" - args: "-sv -m precommit -v" - - - name: tensorflow2_keras_layers - kind: pytest_if_dir - target: "${TESTS_DIR}/layer_tests/tensorflow2_keras_tests" - env: "TEST_DEVICE=CPU" - args: "-sv -m precommit -v" - - - name: onnx_layers - kind: pytest_if_dir - profiles: [cpu, gpu, npu] - target: "${TESTS_DIR}/layer_tests/onnx_tests" - env: - cpu: "TEST_DEVICE=CPU TEST_PRECISION=FP16" - gpu: "TEST_DEVICE=GPU TEST_PRECISION=FP16" - npu: "TEST_DEVICE=NPU TEST_PRECISION=FP16" - default: "TEST_DEVICE=CPU TEST_PRECISION=FP16" - args: - cpu: "-sv -k 'not gpu and not cuda and not npu'" - gpu: "-sv -k 'not cuda and not npu'" - npu: "-sv -k 'not cuda and not gpu'" - default: "-sv -k 'not gpu and not cuda and not npu'" - - - name: pytorch_layers - kind: pytest_if_dir - target: "${TESTS_DIR}/layer_tests/pytorch_tests" - env: "TEST_DEVICE=CPU TEST_PRECISION=FP32" - args: "-sv -m precommit -v" - - - name: pytorch_layers_export - kind: pytest_if_dir - target: "${TESTS_DIR}/layer_tests/pytorch_tests" - env: "TEST_DEVICE=CPU TEST_PRECISION=FP32 PYTORCH_TRACING_MODE=EXPORT" - args: "-sv -m precommit_torch_export -v" - - - name: pytorch_layers_fx_backend - kind: pytest_if_dir - target: "${TESTS_DIR}/layer_tests/pytorch_tests" - env: "TEST_DEVICE=CPU TEST_PRECISION=FP32 PYTORCH_TRACING_MODE=TORCHFX" - args: "-sv -m precommit_fx_backend -v" - - - name: jax_layers_precommit - kind: pytest_if_dir - target: "${TESTS_DIR}/layer_tests/jax_tests" - env: "TEST_DEVICE=CPU TEST_PRECISION=FP16" - args: "-sv -m precommit_jax_fe -k 'not cuda'" - - - name: src_py_runtime - kind: pytest_if_dir - target: "${SRC_PY_TESTS_DIR}/test_runtime" - args: "-sv -k 'not gpu and not cuda and not npu'" - - - name: src_py_graph - kind: pytest_if_dir - target: "${SRC_PY_TESTS_DIR}/test_graph" - args: "-sv -k 'not gpu and not cuda and not npu'" - - - name: src_py_transformations - kind: pytest_if_dir - target: "${SRC_PY_TESTS_DIR}/test_transformations" - args: "-sv -k 'not gpu and not cuda and not npu'" - - - name: src_onnx_frontend_python - kind: pytest_if_dir - target: "${ONNX_PY_TESTS_DIR}" - args: "-sv -k 'not gpu and not cuda and not npu and not zoo' --ignore=${ONNX_PY_TESTS_DIR}/test_zoo_models.py" - - - name: src_py_runtime_strict - kind: pytest_if_dir - target: "${SRC_PY_TESTS_DIR}/test_runtime" - args: "-q --maxfail=1 -k 'not gpu and not cuda and not npu'" - - - name: src_py_utils - kind: pytest_if_dir - target: "${SRC_PY_TESTS_DIR}/test_utils" - args: "-sv -k 'not gpu and not cuda and not npu'" - - - name: src_py_package_versions - kind: pytest - target: "${SRC_PY_TESTS_DIR}/test_package_versions.py" - args: "-sv -k 'not cuda'" - - - name: src_py_runtime_remote_api_gpu - kind: pytest - profiles: [gpu] - profile_skip_reason: "GPU profile is OFF" - target: "${SRC_PY_TESTS_DIR}/test_runtime/test_remote_api.py" - env: "TEST_DEVICE=GPU TEST_PRECISION=FP16" - args: "-sv -k 'not cuda'" - - - name: tf_api_precommit_subset - kind: pytest_if_dir - target: "${WORKSPACE_LAYER_TESTS_DIR}/ovc_python_api_tests" - env: "TEST_DEVICE=CPU TEST_PRECISION=FP16" - args: "-sv -m precommit_tf_fe -k 'test_tf and not cuda' -v" - - - name: ovc_python_api_paddle_precommit - kind: pytest - target: "${WORKSPACE_LAYER_TESTS_DIR}/ovc_python_api_tests/test_paddle.py" - env: "TEST_DEVICE=CPU TEST_PRECISION=FP16" - args: "-sv -m precommit -k 'not cuda'" - - - name: ovc_cli_help - kind: command - command: "python3 -m coverage run -a -m openvino.tools.ovc --help" diff --git a/.github/scripts/meat/README.md b/.github/scripts/meat/README.md new file mode 100644 index 000000000000..b511e3b42912 --- /dev/null +++ b/.github/scripts/meat/README.md @@ -0,0 +1,268 @@ +# Agent scripts — local runner + +Scripts for running the OpenVINO coding agents from `.github/agents-prototype/` on your +local machine using the [GitHub Copilot CLI](https://docs.github.com/en/copilot/how-tos/copilot-cli/cli-getting-started). + +--- + +> [!WARNING] +> **Autonomous / unattended mode** +> +> These scripts invoke GitHub Copilot with `--no-ask-user` and `--autopilot`. +> The agent will **read, create, and modify files** in this repository +> **without asking for confirmation**. +> +> - Always run on a **clean branch** with no uncommitted work. +> - Review `agent-results/` after the run. +> - Apply generated patches with `git apply` and inspect them before committing. +> - **Do NOT blindly commit agent-generated changes.** + +> [!TIP] +> **Consider running the agent inside an isolated sandbox.** +> +> Autopilot mode gives the agent unrestricted access to your filesystem, shell, +> and network. Sandboxing limits the blast radius of unexpected agent behaviour +> (runaway file writes, unintended network calls, accidental credential exposure). +> +> Possible options (not an exhaustive list): +> - **Qwen Code sandbox** — sandboxing feature of the Qwen Code CLI that restricts +> filesystem and network access during agent runs (macOS Seatbelt or Docker/Podman): +> https://qwenlm.github.io/qwen-code-docs/en/users/features/sandbox/ +> - **gh-aw-firewall** — network firewall for agentic workflows that restricts +> outbound HTTP/HTTPS to an allowlist of domains via a Squid proxy inside Docker: +> https://github.com/github/gh-aw-firewall +> - **agent-sandbox** (Kubernetes SIGs) — Kubernetes CRD and controller for managing +> isolated, stateful singleton workloads, ideal for AI agent runtimes: +> https://github.com/kubernetes-sigs/agent-sandbox +> - **sandbox-runtime** (Anthropic) — lightweight OS-level sandboxing tool (no +> container required) that enforces filesystem and network restrictions using native +> OS primitives (macOS Seatbelt / Linux bubblewrap): +> https://github.com/anthropic-experimental/sandbox-runtime + +--- + +## Quick Start — Local + +### 1. Install prerequisites + +Run from the repo root: + +```bash +# Linux — must be run as root to install system packages (gh, copilot CLI) +sudo python .github/scripts/meat/install_copilot_env.py + +# macOS / Windows — no sudo needed +python .github/scripts/meat/install_copilot_env.py +``` + +This checks and installs: +- Python 3.10+ +- `gh` (GitHub CLI) +- `copilot` CLI +- Copilot authentication (opens browser login if needed) + +### 2. Write a context file + +Create a plain text (or Markdown) file describing the problem. +One operator or one model per file. + +**Operator enablement:** + +```markdown +Operator: aten::erfinv +Model: Qwen/Qwen3-0.6B +Error: No conversion rule for aten::erfinv + +erfinv computes the element-wise inverse error function. +PyTorch docs: https://pytorch.org/docs/stable/generated/torch.erfinv.html + +The op appears in the attention normalization path. +Single tensor in, single tensor out. dtype: float32/float16. +``` + +Both `Operator:` and `Model:` are important — the agents need a model to +reproduce and test the fix. + +### 3. Run + +**Full operator enablement pipeline** (FE → Core OpSpec → Transformation/CPU/GPU/NPU): + +```bash +python .github/scripts/meat/enable_operator.py my_op.md +``` + +**Run a single specialist agent:** + +```bash +# Triage — export + classify failure +python .github/scripts/meat/run_agent.py deployer my_context.md +python .github/scripts/meat/run_agent.py analyze-and-convert my_context.md + +# OpenVINO core pipeline +python .github/scripts/meat/run_agent.py frontend my_context.md +python .github/scripts/meat/run_agent.py core-opspec my_context.md +python .github/scripts/meat/run_agent.py transformation my_context.md +python .github/scripts/meat/run_agent.py cpu my_context.md +python .github/scripts/meat/run_agent.py gpu my_context.md + +# List all available agents +python .github/scripts/meat/run_agent.py --list +``` + +All output goes to `agent-results//` in the working directory: +- `session.md` — full agent session transcript +- `patches/` — generated `.patch` files ready to apply +- `pipeline_state.json` — shared state read/written by all agents + +### 4. Apply patches + +```bash +# Check first +git apply --check agent-results/enable-operator/patches/openvino/*.patch + +# Apply +git apply agent-results/enable-operator/patches/openvino/*.patch + +# If whitespace issues +git apply --whitespace=fix agent-results/enable-operator/patches/openvino/*.patch +``` + +--- + +## Agent pipeline map + +``` +enable_operator.py + │ (delegates to run_agent.py enable-operator) + │ + ├─ run_agent.py frontend ← missing_conversion_rule / frontend_error + ├─ run_agent.py core-opspec ← FE escalates (no existing OV op) + │ └─ (parallel) + │ ├─ run_agent.py transformation + │ ├─ run_agent.py cpu + │ └─ run_agent.py gpu + │ +run_agent.py deployer ← first-attempt export + classify +run_agent.py analyze-and-convert ← deep probe + strategy matrix + routing signals +``` + +--- + +## Tips & Tricks — Writing Effective Context + +The quality of the context file directly affects how well the agents perform. + +### Do + +- **Include the exact error message or traceback.** Agents parse these to make + routing decisions. A partial traceback is better than none. +- **Name the operator explicitly** (`aten::erfinv`, not "some math function"). +- **Link to documentation** (PyTorch op docs, paper, HF model card). +- **Mention the model ID** — agents use it to reproduce and test the fix. +- **State what you already tried** — saves the agent from repeating failed approaches. + +### Don't + +- Don't paste entire 10 000-line logs. Trim to the relevant section. +- Don't ask for multiple unrelated things in one context file. One operator + or one model per run. +- Don't include instructions about how agents should work — they already have + their own instructions. Focus on the *problem*, not the *process*. + +### Example: good context for a frontend issue + +```markdown +Model: Qwen/Qwen3-0.6B +Operator: aten::erfinv +Task: text-generation + +Export fails with: + RuntimeError: No conversion rule found for aten::erfinv + at openvino/frontend/pytorch/ts_decoder.py:287 + +erfinv computes the element-wise inverse error function. +PyTorch docs: https://pytorch.org/docs/stable/generated/torch.erfinv.html + +The op appears in the attention normalization path. Single tensor in, single +tensor out. dtype: float32/float16. +``` + +### Example: good context for a Core OpSpec escalation + +```markdown +Model: Qwen/Qwen3-0.6B +Operator: aten::erfinv + +FE agent escalated — no existing OV op covers erfinv semantics. +Escalation payload: + op_name: aten::erfinv + op_semantics: element-wise inverse error function, float tensor in/out + suggested_ov_decomposition: null +``` + +--- + +## Authentication + +The `copilot` CLI requires a GitHub account with an active Copilot subscription. + +`install_copilot_env.py` handles login automatically. To authenticate manually: + +```bash +copilot login +``` + +Or set an environment variable with a Personal Access Token (scopes: `read:user`, `copilot`): + +```bash +# Linux / macOS +export COPILOT_GITHUB_TOKEN=ghp_ + +# Windows PowerShell +$env:COPILOT_GITHUB_TOKEN = 'ghp_' +``` + +--- + +## Using skills from VS Code Chat + +Individual skills are also available as **slash commands** directly in the +VS Code Copilot Chat panel — no terminal or CLI required. + +Each skill maps to a `.prompt.md` file in [`.github/prompts/`](../prompts/). +VS Code discovers these automatically and exposes them as `/skill-name`. + +| Command | What it does | +|---|---| +| `/add-core-op` | Add a new op to the OpenVINO Core opset | +| `/add-fe-op` | Translate a framework op in a Frontend (ONNX/PyTorch/TF) | +| `/add-fusion-transformation` | Write a `MatcherPass`/`FunctionPass` fusion pass | +| `/add-cpu-op` | Implement a CPU plugin kernel (AVX2/AVX-512/AMX/oneDNN) | +| `/add-gpu-op` | Implement a GPU plugin OpenCL kernel | +| `/analyze-and-convert` | Probe a model and classify conversion failures | +| `/conversion-issues` | Diagnose and fix conversion errors | +| `/verify-conversion` | E2E gate — convert + run inference + sanity check | +| `/python-bootstrap` | Set up the Python environment (release or source build) | +| `/submit-draft-pr` | Create a draft PR with duplicate-PR guard | + +**How to use:** + +1. Open VS Code Copilot Chat (`Ctrl+Alt+I`). +2. Switch to **Agent** mode (dropdown next to the model selector). +3. Type `/` and select the skill from the autocomplete list. +4. Add your context after the command, e.g.: + +``` +/add-fe-op Model: Qwen/Qwen3-0.6B Operator: aten::erfinv +``` + +The skill instructions are loaded automatically — the agent follows the +step-by-step workflow defined in the corresponding skill file. + +--- + +## See also + +- Agent definitions: [`.github/agents-prototype/`](../agents-prototype/) +- Skill prompts: [`.github/prompts/`](../prompts/) +- Copilot CLI reference: https://docs.github.com/en/copilot/reference/copilot-cli-reference/cli-command-reference diff --git a/.github/scripts/meat/build_report.py b/.github/scripts/meat/build_report.py new file mode 100644 index 000000000000..18e6578eb001 --- /dev/null +++ b/.github/scripts/meat/build_report.py @@ -0,0 +1,298 @@ +# Copyright (C) 2018-2026 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +#!/usr/bin/env python3 +"""Build-report step for the analyze-and-convert agent. + +Loads all artifacts produced by previous skills and: + 1. Generates conversion_report.md (human-readable) + 2. Posts the report as a PR/issue comment via gh CLI (if available) + 3. Writes analyze_and_convert_result.json (machine-readable) + 4. Prints the marker on stdout + +Usage: + python .github/scripts/meat/build_report.py + +Expected input files (all in the current working directory): + model_profile.json + conversion_attempts.json + routing_signals.json + error_excerpts.json + +Output files: + agent-results/analyze-and-convert/conversion_report.md + agent-results/analyze-and-convert/analyze_and_convert_result.json +""" + +import json +import os +import pathlib +import subprocess +import sys +import textwrap + +# --------------------------------------------------------------------------- +# Paths +# --------------------------------------------------------------------------- + +RESULTS_DIR = pathlib.Path("agent-results") / "analyze-and-convert" + +INPUTS = { + "profile": pathlib.Path("model_profile.json"), + "attempts": pathlib.Path("conversion_attempts.json"), + "signals": pathlib.Path("routing_signals.json"), + "excerpts": pathlib.Path("error_excerpts.json"), +} + +REPORT_PATH = RESULTS_DIR / "conversion_report.md" +RESULT_JSON = RESULTS_DIR / "analyze_and_convert_result.json" + + +# --------------------------------------------------------------------------- +# Step 1 — Load artifacts +# --------------------------------------------------------------------------- + +def load_json(path: pathlib.Path, default): + if path.exists(): + try: + return json.loads(path.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + print(f"[build_report] WARNING: could not parse {path}: {exc}", file=sys.stderr) + else: + print(f"[build_report] WARNING: {path} not found — using default", file=sys.stderr) + return default + + +profile = load_json(INPUTS["profile"], {}) +attempts = load_json(INPUTS["attempts"], []) +signals = load_json(INPUTS["signals"], {}) +excerpts = load_json(INPUTS["excerpts"], {}) + +model_id = profile.get("model_id", "unknown") +error_class = signals.get("error_class", "unknown") +target_agent = signals.get("target_agent", "optimum-intel") + +# Determine overall status +successful = [a for a in attempts if a.get("success")] +if not successful: + status = "failed" +else: + winning = successful[-1] + if winning.get("inference_ok") is True: + status = "success" + elif "inference_ok" in winning: + # Exported OK but inference check failed + status = "partial" + else: + status = "success" + + +# --------------------------------------------------------------------------- +# Step 2 — Write conversion_report.md +# --------------------------------------------------------------------------- + +def md_table(headers: list, rows: list) -> str: + sep = " | ".join("---" for _ in headers) + header_row = " | ".join(headers) + lines = [f"| {header_row} |", f"| {sep} |"] + for row in rows: + lines.append("| " + " | ".join(str(c) for c in row) + " |") + return "\n".join(lines) + + +def build_report() -> str: + lines = [] + + lines.append(f"# Conversion Report: `{model_id}`\n") + lines.append(f"**Overall status:** `{status}` \n") + lines.append(f"**Error class:** `{error_class}` \n") + lines.append(f"**Recommended next agent:** `{target_agent}`\n") + + # --- Model profile --- + lines.append("\n## Model Profile\n") + if profile: + profile_rows = [ + [k, str(v)] + for k, v in profile.items() + if k != "special_config_keys" + ] + lines.append(md_table(["Key", "Value"], profile_rows)) + special_keys = profile.get("special_config_keys", []) + if special_keys: + lines.append(f"\n**Special config keys:** {', '.join(special_keys)}") + else: + lines.append("_No profile data available._") + + # --- Conversion attempts --- + lines.append("\n\n## Conversion Attempts\n") + if attempts: + attempt_rows = [ + [ + a.get("id", "?"), + "✅" if a.get("success") else "❌", + a.get("description", ""), + a.get("weight_format", ""), + a.get("optimum_version", ""), + ] + for a in attempts + ] + lines.append(md_table( + ["ID", "Result", "Description", "Weight Format", "Optimum Version"], + attempt_rows, + )) + else: + lines.append("_No conversion attempts recorded._") + + # --- Successful strategy --- + if successful: + lines.append("\n\n## Successful Strategy\n") + s = successful[-1] + lines.append(f"Strategy **{s.get('id')}** succeeded.\n") + extra = {k: v for k, v in s.items() + if k not in ("id", "success", "description", "weight_format", + "optimum_version", "stdout", "stderr")} + if extra: + lines.append(md_table(["Flag", "Value"], list(extra.items()))) + + # --- Failure details --- + if not successful: + lines.append("\n\n## Failure Details\n") + for a in attempts: + if not a.get("success"): + lines.append(f"### Attempt `{a.get('id', '?')}`\n") + excerpt = excerpts.get(a.get("id", ""), "") + if excerpt: + lines.append("```") + lines.append(textwrap.indent(excerpt, " ")) + lines.append("```\n") + else: + lines.append("_No error excerpt available._\n") + + # --- Routing signals --- + lines.append("\n## Routing Signals\n") + if signals: + signal_rows = [[k, str(v)] for k, v in signals.items()] + lines.append(md_table(["Signal", "Value"], signal_rows)) + else: + lines.append("_No routing signals available._") + + # --- Recommended next step --- + lines.append("\n\n## Recommended Next Step\n") + _next_step_map = { + "optimum-intel": "Open a follow-up issue against **Optimum-Intel**.", + "enable-operator": "Dispatch to **OV Orchestrator** (`enable-operator` agent).", + "openvino-genai": "Dispatch to **GenAI** team for pipeline support.", + "openvino-tokenizers": "Dispatch to **Tokenizers** team for tokenizer conversion fix.", + } + lines.append(_next_step_map.get(target_agent, f"Route to `{target_agent}`.")) + + return "\n".join(lines) + "\n" + + +RESULTS_DIR.mkdir(parents=True, exist_ok=True) +report_text = build_report() +REPORT_PATH.write_text(report_text, encoding="utf-8") +print(f"[build_report] Wrote {REPORT_PATH}") + + +# --------------------------------------------------------------------------- +# Step 3 — Post to GitHub PR/Issue (best-effort) +# --------------------------------------------------------------------------- + +def post_to_github(report_path: pathlib.Path) -> None: + pr_number = os.environ.get("PR_NUMBER", "") + issue_number = os.environ.get("ISSUE_NUMBER", "") + if not (pr_number or issue_number): + print("[build_report] No PR_NUMBER or ISSUE_NUMBER set — skipping gh comment") + return + + try: + result = subprocess.run( + ["gh", "--version"], + capture_output=True, + check=False, + ) + if result.returncode != 0: + raise FileNotFoundError + except FileNotFoundError: + print("[build_report] gh CLI not available — skipping comment post") + return + + body = report_path.read_text(encoding="utf-8") + if pr_number: + cmd = ["gh", "pr", "comment", pr_number, "--body", body] + else: + cmd = ["gh", "issue", "comment", issue_number, "--body", body] + + result = subprocess.run(cmd, capture_output=True, text=True, check=False) + if result.returncode == 0: + print("[build_report] Posted report to GitHub") + else: + print(f"[build_report] gh comment failed: {result.stderr.strip()}", file=sys.stderr) + + +post_to_github(REPORT_PATH) + + +# --------------------------------------------------------------------------- +# Step 4 — Write analyze_and_convert_result.json +# --------------------------------------------------------------------------- + +result_data = { + "agent": "analyze-and-convert", + "status": status, + "model_id": model_id, + "entry_type": profile.get("pipeline_tag", "unknown"), + "error_class": error_class, + "target_agent": target_agent, + "routing_signals": { + k: signals[k] + for k in ( + "requires_optimum_new_arch", + "requires_transformers_upgrade", + "transformers_override", + "requires_tokenizer_check", + "trust_remote_code_required", + "is_vlm", + "custom_ops_suspected", + "oom_suspected", + ) + if k in signals + }, +} + +RESULT_JSON.write_text(json.dumps(result_data, indent=2), encoding="utf-8") +print(f"[build_report] Wrote {RESULT_JSON}") + + +# --------------------------------------------------------------------------- +# Step 5 — Print agent-complete marker +# --------------------------------------------------------------------------- + +next_context = { + k: signals[k] + for k in ( + "requires_optimum_new_arch", + "requires_transformers_upgrade", + "transformers_override", + "requires_tokenizer_check", + "trust_remote_code_required", + "is_vlm", + "custom_ops_suspected", + "oom_suspected", + ) + if k in signals +} + +marker = { + "agent": "analyze-and-convert", + "status": status, + "next_agent": target_agent, + "error_class": error_class, + "model_id": model_id, + "next_context": next_context, +} + +print("") diff --git a/.github/scripts/meat/check_subagent_results.py b/.github/scripts/meat/check_subagent_results.py new file mode 100644 index 000000000000..485de1691e53 --- /dev/null +++ b/.github/scripts/meat/check_subagent_results.py @@ -0,0 +1,52 @@ +# Copyright (C) 2018-2026 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +#!/usr/bin/env python3 +"""Phase 6 helper for enable-operator agent. + +Scans sub-agent result JSON files for failures and failing test results. +Prints a summary and exits with code 1 if any failure is found, 0 otherwise. + +Usage: + python .github/scripts/meat/check_subagent_results.py +""" + +import json +import pathlib +import sys + +SUBAGENT_RESULTS = [ + "agent-results/frontend/fe_result.json", + "agent-results/core-opspec/core_opspec_result.json", + "agent-results/transformation/transformation_result.json", + "agent-results/cpu/cpu_result.json", + "agent-results/gpu/gpu_result.json", +] + +failures = [] + +for path_str in SUBAGENT_RESULTS: + p = pathlib.Path(path_str) + if not p.exists(): + continue + + try: + d = json.loads(p.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + failures.append(f"{path_str}: malformed JSON — {exc}") + continue + + if d.get("status") == "failed": + failures.append(f"{path_str}: status=failed") + + tr = d.get("test_results", "") + if tr and "FAILED" in str(tr).upper(): + failures.append(f"{path_str}: test_results={tr!r}") + +if failures: + print("[OV-ORCH] [phase=e2e-gate] SUB-AGENT TEST FAILURES:") + for f in failures: + print(f" {f}") + sys.exit(1) + +print("[OV-ORCH] [phase=e2e-gate] All sub-agent results: PASS") diff --git a/.github/scripts/meat/classify_component.py b/.github/scripts/meat/classify_component.py new file mode 100644 index 000000000000..7acd38c7e21d --- /dev/null +++ b/.github/scripts/meat/classify_component.py @@ -0,0 +1,46 @@ +# Copyright (C) 2018-2026 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +#!/usr/bin/env python3 +"""Phase 1 helper for enable-operator agent. + +Reads agent-results/pipeline_state.json, classifies the failing component +from error_context, and prints the result as 'component=' to stdout. + +Exit codes: + 0 — classification succeeded (result printed) + 0 — state file missing (prints 'component=frontend' as safe default) +""" + +import json +import pathlib +import sys + +CLASSIFICATION_MAP = { + "missing_conversion_rule": "frontend", + "frontend_error": "frontend", + "ir_validation_error": "core_op", + "inference_runtime_error": "cpu_plugin", + "accuracy_regression": "transformation", +} + +STATE = pathlib.Path("agent-results/pipeline_state.json") + +if not STATE.exists(): + print("component=frontend") + sys.exit(0) + +d = json.loads(STATE.read_text(encoding="utf-8")) +orch = d.get("ov_orchestrator", {}) +error_context = orch.get("error_context", "") + +error_class = error_context.split("/")[0].strip() if error_context else "unknown" +component = CLASSIFICATION_MAP.get(error_class, "frontend") + +# Multi-op detection: report co_located_ops when present +co_located = orch.get("co_located_ops", []) +if co_located: + print(f"[OV-ORCH] Multi-op detected: co_located_ops={co_located} — routing as single target") + +print(f"component={component}") +print(f"[OV-ORCH] Classified component: {component} (error_class={error_class})") diff --git a/.github/scripts/meat/classify_failure.py b/.github/scripts/meat/classify_failure.py new file mode 100644 index 000000000000..56cdfdbd69ca --- /dev/null +++ b/.github/scripts/meat/classify_failure.py @@ -0,0 +1,220 @@ +# Copyright (C) 2018-2026 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +#!/usr/bin/env python3 +"""Classify-failure step for the analyze-and-convert agent. + +Reads conversion_attempts.json and model_profile.json, classifies each +failure into the 11-class error taxonomy, extracts routing signals, and +writes error excerpts for use by build_report.py. + +Usage: + python .github/scripts/meat/classify_failure.py + +Input files (current working directory): + conversion_attempts.json + model_profile.json + +Output files: + routing_signals.json + error_excerpts.json +""" + +import json +import pathlib +import re +import sys + +# --------------------------------------------------------------------------- +# Load artifacts +# --------------------------------------------------------------------------- + +def load_json(path: str, default): + p = pathlib.Path(path) + if p.exists(): + try: + return json.loads(p.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + print(f"[classify_failure] WARNING: could not parse {path}: {exc}", file=sys.stderr) + else: + print(f"[classify_failure] WARNING: {path} not found — using default", file=sys.stderr) + return default + + +attempts = load_json("conversion_attempts.json", []) +profile = load_json("model_profile.json", {}) + +# Collect error text from failed attempts +all_errors = [] +for a in attempts: + if not a.get("success"): + combined = (a.get("stderr", "") + "\n" + a.get("stdout", "")).strip() + all_errors.append({"id": a["id"], "text": combined}) + + +# --------------------------------------------------------------------------- +# Step 2 — Pattern matching → 11-class taxonomy +# --------------------------------------------------------------------------- + +def classify_error(text: str) -> str: + t = text.lower() + + # Order matters — most specific first + if re.search(r"modulenotfounderror|importerror.*requires.*package|no module named", t): + return "missing_model_dependency" + + if re.search(r"keyerror.*taskmanager|model_type.*not.*support|no configuration class", t): + if re.search(r"no.*class.*for.*model_type|transformers.*doesn.t.*know", t): + return "unknown_arch_transformers_too_old" + return "optimum_unsupported_arch" + + if re.search( + r"optimum[/\\]exporters|dummy_inputs|onnx_config|ModelPatcher|" + r"from_pretrained.*export|openvino_config", + t, + ): + return "optimum_export_bug" + + if re.search( + r"notimplemented.*aten::|no rule.*op|conversion.*failed.*aten|" + r"pytorch_frontend|pt_frontend|torchscript.*error", + t, + ): + return "missing_conversion_rule" + + if re.search( + r"openvino[/\\]frontend|ir.*parse|frontend.*error|" + r"segmentation fault|core dumped", + t, + ): + return "frontend_error" + + if re.search( + r"validate_and_infer_types|ngraph.*shape|ir.*invalid|" + r"shape inference failed|opset.*mismatch", + t, + ): + return "ir_validation_error" + + if re.search( + r"ov::exception|infer.*request|plugin.*error|" + r"openvino.*runtime.*error|inference engine", + t, + ): + return "inference_runtime_error" + + if re.search(r"openvino_genai|chat.*template.*missing|pipeline.*construct", t): + return "genai_unsupported" + + if re.search( + r"openvino_tokenizers|sentencepiece|tokenizer.*convert|" + r"tiktoken.*error|fast.*tokenizer", + t, + ): + return "tokenizer_error" + + return "unknown" + + +for err in all_errors: + err["error_class"] = classify_error(err["text"]) + lines = err["text"].splitlines() + tb_lines = [l for l in lines if "Error" in l or "Exception" in l or "raise " in l] + err["key_line"] = tb_lines[0].strip() if tb_lines else (lines[-1].strip() if lines else "") + +dominant_class = all_errors[-1]["error_class"] if all_errors else "unknown" +print(f"[classify_failure] Dominant error class: {dominant_class}") + + +# --------------------------------------------------------------------------- +# Step 3 — Extract routing signals +# --------------------------------------------------------------------------- + +def extract_signals(attempts, profile, dominant_class): + signals = { + "error_class": dominant_class, + "requires_optimum_new_arch": False, + "requires_transformers_upgrade": False, + "transformers_override": "", + "requires_tokenizer_check": False, + "trust_remote_code_required": profile.get("trust_remote_code_required", False), + "is_vlm": profile.get("is_vlm", False), + "custom_ops_suspected": False, + "oom_suspected": False, + "target_agent": "", + } + + all_text = " ".join( + a.get("stderr", "") + a.get("stdout", "") for a in attempts + ).lower() + + if dominant_class in ("optimum_unsupported_arch", "unknown_arch_transformers_too_old"): + signals["requires_optimum_new_arch"] = True + + if dominant_class == "unknown_arch_transformers_too_old": + signals["requires_transformers_upgrade"] = True + signals["transformers_override"] = ( + "git+https://github.com/huggingface/transformers.git" + ) + + ssm_keys = [ + k for k in profile.get("special_config_keys", []) + if any(w in k.lower() for w in + ["ssm", "mamba", "rwkv", "recurrent", "conv", "delta"]) + ] + if ssm_keys: + signals["custom_ops_suspected"] = True + + if re.search(r"out of memory|oom|killed|memory error|cannot allocate", all_text): + signals["oom_suspected"] = True + + if profile.get("is_vlm"): + signals["requires_tokenizer_check"] = True + + routing = { + "optimum_unsupported_arch": "optimum-intel", + "optimum_export_bug": "optimum-intel", + "missing_model_dependency": "optimum-intel", + "unknown_arch_transformers_too_old": "optimum-intel", + "unknown": "optimum-intel", + "missing_conversion_rule": "enable-operator", + "frontend_error": "enable-operator", + "ir_validation_error": "enable-operator", + "inference_runtime_error": "enable-operator", + "genai_unsupported": "openvino-genai", + "tokenizer_error": "openvino-tokenizers", + } + signals["target_agent"] = routing.get(dominant_class, "optimum-intel") + + return signals + + +signals = extract_signals(attempts, profile, dominant_class) +print("[classify_failure] Routing signals:", json.dumps(signals, indent=2)) + +pathlib.Path("routing_signals.json").write_text( + json.dumps(signals, indent=2), encoding="utf-8" +) +print("[classify_failure] Written: routing_signals.json") + + +# --------------------------------------------------------------------------- +# Step 4 — Extract key error excerpts +# --------------------------------------------------------------------------- + +def extract_excerpt(text: str, max_lines: int = 20) -> str: + lines = text.splitlines() + tb_start = max( + (i for i, l in enumerate(lines) if "Traceback (most recent call last)" in l), + default=0, + ) + excerpt = lines[tb_start:] + return "\n".join(excerpt[-max_lines:]) + + +excerpts = {err["id"]: extract_excerpt(err["text"]) for err in all_errors} + +pathlib.Path("error_excerpts.json").write_text( + json.dumps(excerpts, indent=2), encoding="utf-8" +) +print(f"[classify_failure] Written: error_excerpts.json ({len(excerpts)} attempt(s))") diff --git a/.github/scripts/meat/collect_patches.py b/.github/scripts/meat/collect_patches.py new file mode 100644 index 000000000000..a3071175a395 --- /dev/null +++ b/.github/scripts/meat/collect_patches.py @@ -0,0 +1,81 @@ +# Copyright (C) 2018-2026 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +#!/usr/bin/env python3 +"""Phase 7 helper for enable-operator agent — patch collection. + +Reads patch paths from sub-agent result JSON files and copies them to +agent-results/enable-operator/patches/openvino/. +Produces a combined patch file for convenience. + +Usage: + python .github/scripts/meat/collect_patches.py + +Exit codes: + 0 — patches collected (or zero patches found — no-op) + 1 — at least one referenced patch file is missing +""" + +import json +import pathlib +import shutil +import sys + +RESULT_FILES = [ + "agent-results/frontend/fe_result.json", + "agent-results/core-opspec/core_opspec_result.json", + "agent-results/transformation/transformation_result.json", + "agent-results/cpu/cpu_result.json", + "agent-results/gpu/gpu_result.json", + "agent-results/npu/npu_result.json", +] + +OUT_DIR = pathlib.Path("agent-results/enable-operator/patches/openvino") +OUT_DIR.mkdir(parents=True, exist_ok=True) + +collected: list[pathlib.Path] = [] +missing: list[str] = [] + +for path_str in RESULT_FILES: + p = pathlib.Path(path_str) + if not p.exists(): + continue + + try: + d = json.loads(p.read_text(encoding="utf-8")) + except json.JSONDecodeError: + continue + + patch_paths = d.get("patch_paths", []) + if not patch_paths and d.get("patch_path"): + patch_paths = [d["patch_path"]] + + for pp in patch_paths: + if not pp: + continue + src = pathlib.Path(pp) + if src.is_file(): + dst = OUT_DIR / src.name + shutil.copy(src, dst) + collected.append(dst) + else: + missing.append(str(pp)) + print(f"[WARN] Patch not found: {pp} — skipping", file=sys.stderr) + +# A missing patch is always an error — check before the no-collected guard +if missing: + if not collected: + print("[WARN] No patches collected — nothing to publish") + sys.exit(1) + +if not collected: + print("[WARN] No patches collected — nothing to publish") + sys.exit(0) + +# Combine all patches into a single file (useful for review) +combined = pathlib.Path("agent-results/enable-operator/patches/openvino_combined.patch") +combined.write_text( + "\n".join(c.read_text(encoding="utf-8") for c in collected), + encoding="utf-8", +) +print(f"[OV-ORCH] Combined {len(collected)} patches into {combined}") diff --git a/.github/scripts/meat/create_agent_pr.py b/.github/scripts/meat/create_agent_pr.py new file mode 100644 index 000000000000..bd41bf677302 --- /dev/null +++ b/.github/scripts/meat/create_agent_pr.py @@ -0,0 +1,174 @@ +# Copyright (C) 2018-2026 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +#!/usr/bin/env python3 +"""Phase 7 helper for enable-operator agent — branch, apply patches, open draft PR. + +Reads agent-results/pipeline_state.json to derive op names, creates a branch, +applies all collected patches via 'git am', pushes to the user's fork, and opens +a draft PR against openvinotoolkit/openvino with the required AI-generation banner +and pull_request_template.md structure. + +Cross-platform: uses Python subprocess instead of bash. Requires git and gh CLI. + +Usage: + python .github/scripts/meat/create_agent_pr.py [--dry-run] + +Exit codes: + 0 — PR opened (or already exists, or dry-run) + 1 — unrecoverable error +""" + +import argparse +import json +import pathlib +import subprocess +import sys +import textwrap + + +def run(cmd: list[str], check: bool = True, capture: bool = True) -> subprocess.CompletedProcess: + """Run a command, optionally capturing output.""" + return subprocess.run( + cmd, + check=check, + text=True, + capture_output=capture, + ) + + +def gh_login() -> str: + return run(["gh", "api", "user", "-q", ".login"]).stdout.strip() + + +def check_existing_pr(login: str, branch: str) -> str | None: + result = run( + [ + "gh", "pr", "list", + "--repo", "openvinotoolkit/openvino", + "--head", f"{login}:{branch}", + "--json", "url", + "-q", ".[0].url", + ], + check=False, + ) + url = result.stdout.strip() + return url if url else None + + +def get_op_names(state: dict) -> tuple[list[str], str, str]: + """Return (ops list, slug for branch, title for PR).""" + orch = state.get("ov_orchestrator", {}) + ops = orch.get("co_located_ops", []) + if not ops: + ctx = orch.get("error_context", "unknown") + ops = [ctx.split("/")[-1]] + + slug = "-".join(o.lower().replace("::", "-").replace("_", "-") for o in ops) + title = ", ".join(ops) + return ops, slug, title + + +def compose_pr_body(ops: list[str], op_title: str) -> str: + return textwrap.dedent(f"""\ + > [!IMPORTANT] + > **This PR was generated by a Copilot coding agent and requires human review.** + > A maintainer must inspect the changes, run the relevant tests locally, and + > convert this draft to "Ready for review" only after verifying correctness. + > Do **not** merge without human sign-off. + + ### Details: + - Add operator support: {op_title} + - Agent: Enable Operator (`enable-operator.agent.md`) + - Ops added: {', '.join(ops)} + + ### Tickets: + - N/A + + ### AI Assistance: + - AI assistance used: yes + - Generated by GitHub Copilot agent (`enable-operator`). Human validation required: + build verification, unit/functional tests, and inference sanity check. + """) + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--dry-run", action="store_true", help="Print actions without executing git/gh commands") + args = parser.parse_args() + + state_path = pathlib.Path("agent-results/pipeline_state.json") + if not state_path.exists(): + print(f"[ERROR] State file not found: {state_path}", file=sys.stderr) + return 1 + + state = json.loads(state_path.read_text(encoding="utf-8")) + ops, slug, op_title = get_op_names(state) + branch = f"fix/add-{slug}-op" + + patches = sorted( + pathlib.Path("agent-results/enable-operator/patches/openvino").glob("*.patch") + ) + if not patches: + print("[WARN] No patches to apply — skipping PR creation") + return 0 + + # Check for existing PR to avoid duplicates + try: + login = gh_login() + except subprocess.CalledProcessError: + print("[ERROR] gh CLI not available or not authenticated", file=sys.stderr) + return 1 + + existing = check_existing_pr(login, branch) + if existing: + print(f"[draft-pr] PR already exists: {existing} — skipping creation") + return 0 + + if args.dry_run: + print(f"[dry-run] Would create branch: {branch}") + print(f"[dry-run] Would apply {len(patches)} patch(es)") + print(f"[dry-run] Would open PR: 'Add operator support: {op_title}'") + return 0 + + # Create branch and apply patches + run(["git", "checkout", "-b", branch]) + for patch in patches: + run(["git", "am", str(patch)]) + + # Ensure fork exists + run(["gh", "repo", "fork", "openvinotoolkit/openvino", "--clone=false"], check=False) + + # Add fork remote (ignore error if already present) + fork_ssh = run( + ["gh", "repo", "view", f"{login}/openvino", "--json", "sshUrl", "-q", ".sshUrl"] + ).stdout.strip() + run(["git", "remote", "add", "fork", fork_ssh], check=False) + run(["git", "push", "fork", branch]) + + # Write PR body + pr_body_path = pathlib.Path("agent-results/enable-operator/pr_body.md") + pr_body_path.write_text(compose_pr_body(ops, op_title), encoding="utf-8") + + # Open draft PR + result = run([ + "gh", "pr", "create", + "--repo", "openvinotoolkit/openvino", + "--head", f"{login}:{branch}", + "--title", f"Add operator support: {op_title}", + "--body-file", str(pr_body_path), + "--draft", + ]) + pr_url = result.stdout.strip() + print(f"[OV-ORCH] [publish] openvino PR opened: {pr_url}") + + # Update pipeline state + state.setdefault("ov_orchestrator", {})["pr_url"] = pr_url + state.setdefault("ov_orchestrator", {})["overall_status"] = "success" + state_path.write_text(json.dumps(state, indent=2), encoding="utf-8") + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.github/scripts/meat/enable_operator.py b/.github/scripts/meat/enable_operator.py new file mode 100644 index 000000000000..630c97545b6b --- /dev/null +++ b/.github/scripts/meat/enable_operator.py @@ -0,0 +1,46 @@ +# Copyright (C) 2018-2026 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +#!/usr/bin/env python3 +"""Entry point for operator-level enablement (OpenVINO core pipeline). + +Passes the context file to the `enable-operator` agent defined in +`.github/agents-prototype/enable-operator.agent.md`, which drives the full +FE → Core OpSpec → Transformation/CPU/GPU/NPU → Package Builder pipeline. + +Usage: + python .github/scripts/meat/enable_operator.py + +Run from the openvino repo root. + +The context file should contain: + - the operator name (e.g. ``aten::erfinv``), + - a model ID or local path to a model that uses the operator — the agents + will use this model to test and validate the implementation. + +Example context file: + + Operator: aten::erfinv + Model: Qwen/Qwen3-0.6B + Error: No conversion rule for aten::erfinv + + erfinv computes the element-wise inverse error function. + PyTorch docs: https://pytorch.org/docs/stable/generated/torch.erfinv.html + +See .github/scripts/meat/README.md for the full context file format. + +Shortcut for: run_agent.py enable-operator +""" + +import os +import subprocess +import sys + + +def main() -> None: + runner = os.path.join(os.path.dirname(os.path.abspath(__file__)), "run_agent.py") + sys.exit(subprocess.run([sys.executable, runner, "enable-operator"] + sys.argv[1:]).returncode) + + +if __name__ == "__main__": + main() diff --git a/.github/scripts/meat/install_copilot_env.py b/.github/scripts/meat/install_copilot_env.py new file mode 100644 index 000000000000..35149a4b8046 --- /dev/null +++ b/.github/scripts/meat/install_copilot_env.py @@ -0,0 +1,331 @@ +# Copyright (C) 2018-2026 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +#!/usr/bin/env python3 +"""Set up the environment needed to run agent scripts from .github/scripts/meat/. + +Checks and (where possible) performs: + 1. Python version requirement (3.10+) + 2. GitHub CLI (gh) installation + 3. copilot CLI installation + 4. copilot authentication + +Run from the openvino repo root: + python .github/scripts/meat/install_copilot_env.py +""" + +import json +import os +import platform +import shutil +import subprocess +import sys +from pathlib import Path +from typing import Optional + +REPO_ROOT = Path(__file__).parent.parent.parent +MIN_PYTHON = (3, 10) + + +# ────────────────────────────────────────────── +# Helpers +# ────────────────────────────────────────────── + +def ok(msg: str) -> None: + print(f" [OK] {msg}") + + +def info(msg: str) -> None: + print(f" [..] {msg}") + + +def warn(msg: str) -> None: + print(f" [!!] {msg}", file=sys.stderr) + + +def fail(msg: str) -> None: + print(f"\n [FAIL] {msg}\n", file=sys.stderr) + sys.exit(1) + + +def run(*args, **kwargs): + return subprocess.run(list(args), **kwargs) + + +# ────────────────────────────────────────────── +# Python +# ────────────────────────────────────────────── + +def check_python() -> None: + if sys.version_info < MIN_PYTHON: + fail(f"Python {MIN_PYTHON[0]}.{MIN_PYTHON[1]}+ required, found {sys.version.split()[0]}") + ok(f"Python {sys.version.split()[0]}") + + +# ────────────────────────────────────────────── +# GitHub CLI (gh) +# ────────────────────────────────────────────── + +def _gh_version() -> Optional[str]: + if not shutil.which("gh"): + return None + try: + r = run("gh", "--version", capture_output=True, text=True, timeout=10) + line = (r.stdout or "").splitlines() + return line[0].strip() if line else "unknown" + except Exception: + return None + + +def _try_install_gh() -> bool: + system = platform.system() + if system == "Windows": + if shutil.which("winget"): + print( + "\n winget will accept the GitHub CLI package agreement and the\n" + " winget source agreement on your behalf.\n" + " Proceed with automatic install? [y/N] ", + end="", flush=True, + ) + if input().strip().lower() != "y": + return False + info("Attempting: winget install --id GitHub.cli --silent …") + r = run("winget", "install", "--id", "GitHub.cli", "--silent", + "--accept-package-agreements", "--accept-source-agreements") + return r.returncode == 0 + elif system == "Darwin": + if shutil.which("brew"): + info("Attempting: brew install gh …") + r = run("brew", "install", "gh") + return r.returncode == 0 + else: # Linux + if shutil.which("apt-get"): + info("Attempting apt-based gh install …") + cmds = [ + ["mkdir", "-p", "/etc/apt/keyrings"], + ["bash", "-c", + "curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg" + " -o /etc/apt/keyrings/githubcli-archive-keyring.gpg"], + ["chmod", "go+r", "/etc/apt/keyrings/githubcli-archive-keyring.gpg"], + ["bash", "-c", + "echo \"deb [arch=$(dpkg --print-architecture)" + " signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg]" + " https://cli.github.com/packages stable main\"" + " | tee /etc/apt/sources.list.d/github-cli.list > /dev/null"], + ["apt-get", "update", "-qq"], + ["apt-get", "install", "-y", "--no-install-recommends", "gh"], + ] + for cmd in cmds: + if run(*cmd).returncode != 0: + return False + return True + if shutil.which("dnf"): + info("Attempting dnf-based gh install …") + cmds = [ + ["dnf", "config-manager", "--add-repo", + "https://cli.github.com/packages/rpm/gh-cli.repo"], + ["dnf", "install", "-y", "gh"], + ] + for cmd in cmds: + if run(*cmd).returncode != 0: + return False + return True + return False + + +def _print_gh_install_instructions() -> None: + system = platform.system() + print("\n GitHub CLI (gh) is not installed. Install it using one of the methods below,\n" + " then re-run this script.\n") + if system == "Windows": + print(" WinGet (recommended):") + print(" winget install --id GitHub.cli\n") + print(" Manual installer: https://cli.github.com/\n") + elif system == "Darwin": + print(" Homebrew:") + print(" brew install gh\n") + else: + print(" Debian/Ubuntu: https://github.com/cli/cli/blob/trunk/docs/install_linux.md\n") + print(" Fedora/RHEL:") + print(" sudo dnf config-manager --add-repo https://cli.github.com/packages/rpm/gh-cli.repo") + print(" sudo dnf install gh\n") + print(" Docs: https://cli.github.com/\n") + + +def check_gh() -> None: + version = _gh_version() + if version: + ok(f"GitHub CLI {version}") + return + + warn("GitHub CLI (gh) not found in PATH.") + info("Attempting automatic install …") + if _try_install_gh(): + version = _gh_version() + if version: + ok(f"GitHub CLI installed: {version}") + return + warn("Install command succeeded but 'gh' still not in PATH. " + "You may need to restart your shell.") + + _print_gh_install_instructions() + sys.exit(1) + + +# ────────────────────────────────────────────── +# copilot CLI +# ────────────────────────────────────────────── + +def _copilot_version() -> Optional[str]: + if not shutil.which("copilot"): + return None + try: + r = run("copilot", "version", capture_output=True, text=True, timeout=15) + line = (r.stdout or r.stderr or "").splitlines() + return line[0].strip() if line else "unknown" + except Exception: + return None + + +def _try_install_copilot() -> bool: + system = platform.system() + if system == "Windows": + if shutil.which("winget"): + print( + "\n winget will accept the GitHub Copilot package agreement and the\n" + " winget source agreement on your behalf.\n" + " Proceed with automatic install? [y/N] ", + end="", flush=True, + ) + if input().strip().lower() != "y": + return False + info("Attempting: winget install GitHub.Copilot --silent …") + r = run("winget", "install", "GitHub.Copilot", "--silent", + "--accept-package-agreements", "--accept-source-agreements") + return r.returncode == 0 + elif system in ("Darwin", "Linux"): + if shutil.which("brew"): + info("Attempting: brew install copilot-cli …") + r = run("brew", "install", "copilot-cli") + return r.returncode == 0 + return False + + +def _print_copilot_install_instructions() -> None: + system = platform.system() + print("\n copilot CLI is not installed. Install it using one of the methods below,\n" + " then re-run this script.\n") + if system == "Windows": + print(" WinGet:") + print(" winget install GitHub.Copilot\n") + else: + print(" Homebrew:") + print(" brew install copilot-cli\n") + print(" Docs: https://docs.github.com/en/copilot/how-tos/copilot-cli/cli-getting-started\n") + + +def check_copilot() -> None: + version = _copilot_version() + if version: + ok(f"copilot CLI {version}") + return + + warn("copilot CLI not found in PATH.") + info("Attempting automatic install …") + if _try_install_copilot(): + version = _copilot_version() + if version: + ok(f"copilot CLI installed: {version}") + return + warn("Install command succeeded but 'copilot' still not in PATH. " + "You may need to restart your shell.") + + _print_copilot_install_instructions() + sys.exit(1) + + +# ────────────────────────────────────────────── +# Authentication +# ────────────────────────────────────────────── + +def _token_in_env() -> Optional[str]: + for var in ("COPILOT_GITHUB_TOKEN", "GH_TOKEN", "GITHUB_TOKEN"): + if os.environ.get(var): + return var + return None + + +def _token_stored() -> bool: + cfg = Path.home() / ".copilot" / "config.json" + if not cfg.exists(): + return False + try: + data = json.loads(cfg.read_text(encoding="utf-8")) + return bool(data.get("token") or data.get("githubToken") or data.get("auth")) + except Exception: + return cfg.stat().st_size > 64 + + +def _print_token_hint() -> None: + print("\n Alternatively, set a Personal Access Token with 'copilot' scope:\n") + print(" Linux / macOS – add to ~/.bashrc or ~/.zshrc:") + print(" export COPILOT_GITHUB_TOKEN=ghp_\n") + print(" Windows PowerShell – add to $PROFILE:") + print(" $env:COPILOT_GITHUB_TOKEN = 'ghp_'\n") + print(" Token scopes needed: read:user, copilot\n") + print(" Create one at: https://github.com/settings/tokens\n") + + +def check_auth() -> None: + var = _token_in_env() + if var: + ok(f"Auth via ${var}") + return + + if _token_stored(): + ok("Auth: stored credentials found (~/.copilot/config.json)") + return + + warn("No authentication found.") + info("Starting 'copilot login' – follow the prompts in your browser.\n") + try: + result = run("copilot", "login") + if result.returncode != 0: + warn("'copilot login' exited with an error.") + _print_token_hint() + sys.exit(1) + ok("Authentication complete.") + except KeyboardInterrupt: + print() + warn("Login cancelled.") + _print_token_hint() + sys.exit(1) + except FileNotFoundError: + fail("'copilot' command not found – install it first (step above).") + + +# ────────────────────────────────────────────── +# Python deps +# ────────────────────────────────────────────── +# Entry point +# ────────────────────────────────────────────── + +def main() -> None: + print("\n=== OpenVINO agent scripts – Environment Setup ===\n") + + check_python() + check_gh() + check_copilot() + check_auth() + + print( + "\n All checks passed. You can now run agent scripts.\n" + " Example:\n" + " python .github/scripts/meat/enable_operator.py my_op.md\n" + "\n" + " See .github/scripts/meat/README.md for full usage.\n" + ) + + +if __name__ == "__main__": + main() diff --git a/.github/scripts/meat/probe_model.py b/.github/scripts/meat/probe_model.py new file mode 100644 index 000000000000..168844f3669c --- /dev/null +++ b/.github/scripts/meat/probe_model.py @@ -0,0 +1,252 @@ +# Copyright (C) 2018-2026 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +#!/usr/bin/env python3 +"""Probe-model step for the analyze-and-convert agent. + +First step in the analyze-and-convert workflow. Probes a HuggingFace model +to collect its full profile and writes model_profile.json consumed by all +subsequent skills. + +Usage: + python .github/scripts/meat/probe_model.py + + MODEL_ID env var is also accepted as a fallback. + +Output files (current working directory): + model_profile.json +""" + +import json +import os +import pathlib +import sys + +# --------------------------------------------------------------------------- +# Resolve MODEL_ID +# --------------------------------------------------------------------------- + +if len(sys.argv) >= 2: + MODEL_ID = sys.argv[1] +else: + MODEL_ID = os.environ.get("MODEL_ID", "") + +if not MODEL_ID: + print( + "Usage: probe_model.py OR set MODEL_ID env var", + file=sys.stderr, + ) + sys.exit(1) + +print(f"[probe_model] Probing model: {MODEL_ID}") + +# --------------------------------------------------------------------------- +# Step 1 — Fetch HuggingFace metadata +# --------------------------------------------------------------------------- + +try: + import requests # type: ignore + + HF_API = "https://huggingface.co/api/models" + resp = requests.get(f"{HF_API}/{MODEL_ID}", timeout=30) + meta = resp.json() if resp.ok else {} + if not resp.ok: + print(f"[probe_model] HF API returned {resp.status_code} — metadata will be empty", + file=sys.stderr) +except Exception as exc: # noqa: BLE001 + print(f"[probe_model] WARNING: could not fetch HF metadata: {exc}", file=sys.stderr) + meta = {} + +print("pipeline_tag :", meta.get("pipeline_tag")) +print("library_name :", meta.get("library_name")) +print("tags :", meta.get("tags", [])) + +# --------------------------------------------------------------------------- +# Step 2 — Load and inspect config +# --------------------------------------------------------------------------- + +try: + from transformers import AutoConfig # type: ignore + + cfg = AutoConfig.from_pretrained(MODEL_ID, trust_remote_code=False) + cfg_dict = cfg.to_dict() +except Exception as exc: # noqa: BLE001 + print(f"[probe_model] WARNING: AutoConfig failed (trust_remote_code=False): {exc}", + file=sys.stderr) + cfg = None + cfg_dict = {} + +if cfg is not None: + print("model_type :", cfg.model_type) + print("architectures :", cfg.architectures) + print("hidden_size :", getattr(cfg, "hidden_size", "N/A")) + print("num_hidden_layers :", getattr(cfg, "num_hidden_layers", "N/A")) + print("num_attention_heads :", getattr(cfg, "num_attention_heads", "N/A")) + print("num_key_value_heads :", getattr(cfg, "num_key_value_heads", "N/A")) + print("vocab_size :", getattr(cfg, "vocab_size", "N/A")) + print("max_position_embeds :", getattr(cfg, "max_position_embeddings", "N/A")) + +_SPECIAL_KEYWORDS = [ + "layer_type", "ssm", "mamba", "rwkv", "moe", "expert", "conv", + "recurrent", "delta", "vision", "image", "video", "mm_", "vlm", + "audio", "cross_attn", "sliding", "hybrid", "flash", "rope", + "alibi", "custom_code", +] +special_keys = [ + k for k in cfg_dict + if any(kw in k.lower() for kw in _SPECIAL_KEYWORDS) +] +for k in special_keys: + print(f" {k}: {cfg_dict[k]}") + +# --------------------------------------------------------------------------- +# Step 3 — Estimate parameter count +# --------------------------------------------------------------------------- + +def estimate_params(cfg) -> int: + h = getattr(cfg, "hidden_size", 0) + layers = getattr(cfg, "num_hidden_layers", 0) + vocab = getattr(cfg, "vocab_size", 0) + intermediate = getattr(cfg, "intermediate_size", h * 4) + heads = getattr(cfg, "num_attention_heads", 1) + kv_heads = getattr(cfg, "num_key_value_heads", heads) + + attn = h * h + (kv_heads * (h // heads if heads else 1)) * 2 * h + h * h + ffn = h * intermediate * 3 if hasattr(cfg, "mlp_bias") else h * intermediate * 2 + embed = vocab * h * 2 + per_layer = attn + ffn + return layers * per_layer + embed + + +est = estimate_params(cfg) if cfg is not None else 0 +print(f"[probe_model] Estimated params: ~{est / 1e9:.1f}B") + +# --------------------------------------------------------------------------- +# Step 4 — Check trust_remote_code requirement +# --------------------------------------------------------------------------- + +trust_remote_code_required = False +if cfg is None: + # AutoConfig already failed without trust_remote_code — check whether it + # succeeds with it to distinguish "needs trust_rc" from "broken model". + try: + from transformers import AutoConfig # type: ignore # noqa: F811 + + AutoConfig.from_pretrained(MODEL_ID, trust_remote_code=True) + trust_remote_code_required = True + # Re-load cfg with trust_remote_code for subsequent steps + cfg = AutoConfig.from_pretrained(MODEL_ID, trust_remote_code=True) + cfg_dict = cfg.to_dict() + est = estimate_params(cfg) + except Exception: # noqa: BLE001 + pass +else: + try: + from transformers import AutoConfig # type: ignore # noqa: F811 + + AutoConfig.from_pretrained(MODEL_ID, trust_remote_code=False) + except Exception as exc: # noqa: BLE001 + if "trust_remote_code" in str(exc) or "custom code" in str(exc).lower(): + trust_remote_code_required = True + +print("trust_remote_code required:", trust_remote_code_required) + +# --------------------------------------------------------------------------- +# Step 5 — Inspect tokenizer config +# --------------------------------------------------------------------------- + +tokenizer_info = {} +try: + from transformers import AutoTokenizer # type: ignore + + tok = AutoTokenizer.from_pretrained(MODEL_ID, trust_remote_code=True) + tokenizer_info = { + "tokenizer_class": type(tok).__name__, + "is_fast": tok.is_fast, + "vocab_size": tok.vocab_size, + "chat_template": "present" if tok.chat_template else "absent", + } + for k, v in tokenizer_info.items(): + print(f"{'tokenizer_' if 'tokenizer' not in k else ''}{k:<20}: {v}") +except Exception as exc: # noqa: BLE001 + print(f"[probe_model] WARNING: tokenizer load error: {exc}", file=sys.stderr) + +# --------------------------------------------------------------------------- +# Step 6 — Detect multimodal / VLM signals +# --------------------------------------------------------------------------- + +# cfg may have been re-loaded with trust_remote_code above; reload once more +# with trust_remote_code=True to get full config for VLM detection. +try: + from transformers import AutoConfig # type: ignore # noqa: F811 + + cfg_full = AutoConfig.from_pretrained(MODEL_ID, trust_remote_code=True) +except Exception: # noqa: BLE001 + cfg_full = cfg + +vlm_signals = {} +if cfg_full is not None: + vlm_signals = { + "vision_config": hasattr(cfg_full, "vision_config"), + "image_token_id": hasattr(cfg_full, "image_token_id"), + "video_token_id": hasattr(cfg_full, "video_token_id"), + "mm_projector": hasattr(cfg_full, "mm_projector_type"), + "audio_config": hasattr(cfg_full, "audio_config"), + } + +is_vlm = any(vlm_signals.values()) +print("VLM signals:", vlm_signals) +print("Is VLM:", is_vlm) + +# --------------------------------------------------------------------------- +# Step 7 — Check optimum-intel support +# --------------------------------------------------------------------------- + +optimum_supported = False +optimum_supported_tasks = [] +if cfg is not None: + model_type = getattr(cfg, "model_type", "") + try: + from optimum.exporters.tasks import TasksManager # type: ignore + + supported_tasks = TasksManager.get_supported_tasks_for_model_type( + model_type, "openvino" + ) + print(f"[probe_model] optimum-intel knows model_type='{model_type}': YES") + print("Supported tasks:", supported_tasks) + optimum_supported = True + optimum_supported_tasks = list(supported_tasks) + except KeyError: + print(f"[probe_model] optimum-intel does NOT know model_type='{model_type}'") + except ImportError: + print("[probe_model] WARNING: optimum not installed — skipping optimum check", + file=sys.stderr) + +# --------------------------------------------------------------------------- +# Step 8 — Write model_profile.json +# --------------------------------------------------------------------------- + +profile = { + "model_id": MODEL_ID, + "model_type": getattr(cfg, "model_type", None) if cfg else None, + "architectures": getattr(cfg, "architectures", None) if cfg else None, + "pipeline_tag": meta.get("pipeline_tag"), + "library_name": meta.get("library_name"), + "estimated_params_b": round(est / 1e9, 1), + "hidden_size": getattr(cfg, "hidden_size", None) if cfg else None, + "num_layers": getattr(cfg, "num_hidden_layers", None) if cfg else None, + "vocab_size": getattr(cfg, "vocab_size", None) if cfg else None, + "trust_remote_code_required": trust_remote_code_required, + "is_vlm": is_vlm, + "vlm_signals": vlm_signals, + "optimum_supported": optimum_supported, + "optimum_supported_tasks": optimum_supported_tasks, + "special_config_keys": special_keys, + "hf_tags": meta.get("tags", []), + **tokenizer_info, +} + +pathlib.Path("model_profile.json").write_text( + json.dumps(profile, indent=2), encoding="utf-8" +) +print("[probe_model] Written: model_profile.json") diff --git a/.github/scripts/meat/run_agent.py b/.github/scripts/meat/run_agent.py new file mode 100644 index 000000000000..58de781b6c05 --- /dev/null +++ b/.github/scripts/meat/run_agent.py @@ -0,0 +1,107 @@ +# Copyright (C) 2018-2026 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +#!/usr/bin/env python3 +"""Run any OpenVINO coding agent from .github/agents-prototype/. + +Usage: + python .github/scripts/meat/run_agent.py + python .github/scripts/meat/run_agent.py --list + +Run from the openvino repo root. + +Output goes to agent-results// in the working directory: + session.md — full agent session transcript + patches/ — generated .patch files ready to apply + pipeline_state.json — shared state read/written by all agents + +Copilot CLI reference: + https://docs.github.com/en/copilot/reference/copilot-cli-reference/cli-command-reference +""" + +import os +import subprocess +import sys + +AGENTS = [ + "enable-operator", + "frontend", + "core-opspec", + "transformation", + "cpu", + "gpu", + "deployer", + "analyze-and-convert", +] + +_WARN = ( + "\n" + " ╔══════════════════════════════════════════════════════════════════╗\n" + " ║ WARNING: AUTONOMOUS / UNATTENDED MODE ║\n" + " ║ ║\n" + " ║ This script runs GitHub Copilot with --no-ask-user and ║\n" + " ║ --autopilot. The agent will READ, CREATE, and MODIFY files ║\n" + " ║ in this repository WITHOUT asking for confirmation. ║\n" + " ║ ║\n" + " ║ Review agent-results/ after the run and apply patches with ║\n" + " ║ 'git apply' — do NOT blindly commit generated changes. ║\n" + " ╚══════════════════════════════════════════════════════════════════╝\n" +) + + +def main() -> None: + if len(sys.argv) == 2 and sys.argv[1] in ("--list", "-l"): + print("Available agents:") + for name in AGENTS: + print(f" {name}") + sys.exit(0) + + if len(sys.argv) != 3: + print( + f"Usage: {sys.argv[0]} \n" + f" {sys.argv[0]} --list", + file=sys.stderr, + ) + sys.exit(1) + + agent_name = sys.argv[1] + context_file_path = sys.argv[2] + agent_file = f".github/agents-prototype/{agent_name}.agent.md" + + if not os.path.isfile(context_file_path): + print(f"Error: context file not found: {context_file_path}", file=sys.stderr) + sys.exit(1) + + if not os.path.isfile(agent_file): + print( + f"Error: agent file not found: {agent_file}\n" + "Make sure you are running from the openvino repo root.\n" + f"Available agents: {', '.join(AGENTS)}", + file=sys.stderr, + ) + sys.exit(1) + + output_dir = f"agent-results/{agent_name}" + os.makedirs(output_dir, exist_ok=True) + + with open(context_file_path) as f: + prompt = f.read() + + print(_WARN, flush=True) + + cmd = [ + "copilot", + "--agent", agent_name, + "--share", f"{output_dir}/session.md", + "--no-ask-user", + "--autopilot", + "--stream", "on", + "--log-level", "all", + "-p", prompt, + ] + + sys.exit(subprocess.run(cmd, shell=(sys.platform == "win32")).returncode) + + +if __name__ == "__main__": + main() diff --git a/.github/scripts/meat/tests/conftest.py b/.github/scripts/meat/tests/conftest.py new file mode 100644 index 000000000000..c3a2b92488f8 --- /dev/null +++ b/.github/scripts/meat/tests/conftest.py @@ -0,0 +1,108 @@ +# Copyright (C) 2018-2026 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +"""Shared fixtures and helpers for meat-scripts test suite.""" + +import json +import os +import pathlib +import sys +import textwrap + +import pytest + +SCRIPTS_DIR = pathlib.Path(__file__).parent.parent + + +# --------------------------------------------------------------------------- +# Low-level helpers +# --------------------------------------------------------------------------- + +def run_script(script: pathlib.Path, cwd, *args, extra_env=None): + """Run a Python script as a subprocess, return CompletedProcess.""" + import subprocess + + env = os.environ.copy() + if extra_env: + env.update(extra_env) + return subprocess.run( + [sys.executable, str(script), *args], + capture_output=True, + text=True, + cwd=str(cwd), + env=env, + ) + + +def write_json(path: pathlib.Path, data) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(data, indent=2), encoding="utf-8") + + +# --------------------------------------------------------------------------- +# Cross-platform fake-command factory +# +# Creates a callable named `name` in `bin_dir` that runs a small Python +# script. On Windows a .bat wrapper is written; on POSIX a shell script. +# --------------------------------------------------------------------------- + +def _make_stub_impl(bin_dir: pathlib.Path, name: str, body: str) -> pathlib.Path: + """Write the Python implementation and a platform launcher.""" + impl = bin_dir / f"_stub_{name}.py" + impl.write_text(body, encoding="utf-8") + + if sys.platform == "win32": + launcher = bin_dir / f"{name}.bat" + launcher.write_text( + f'@"{sys.executable}" "{impl}" %*\r\n', + encoding="utf-8", + ) + else: + launcher = bin_dir / name + launcher.write_text( + f'#!/bin/sh\nexec python3 "{impl}" "$@"\n', + encoding="utf-8", + ) + launcher.chmod(0o755) + + return launcher + + +def make_fake_cmd(bin_dir: pathlib.Path, name: str, *, + exit_code: int = 0, + create_ir: bool = False, + stdout_text: str = "") -> None: + """ + Create a fake ``name`` command in ``bin_dir``. + + Parameters + ---------- + exit_code : int + Return code the stub will produce. + create_ir : bool + If True, the stub creates .xml/.bin files in the directory + given by the ``--output `` argument. + stdout_text : str + Text to print to stdout before exiting. + """ + body = textwrap.dedent(f"""\ + import sys, pathlib + if {stdout_text!r}: + print({stdout_text!r}) + args = sys.argv + if {create_ir!r} and "--output" in args: + out_idx = args.index("--output") + 1 + out = pathlib.Path(args[out_idx]) + out.mkdir(parents=True, exist_ok=True) + (out / "openvino_model.xml").write_text("") + (out / "openvino_model.bin").write_bytes(b"") + sys.exit({exit_code}) + """) + _make_stub_impl(bin_dir, name, body) + + +def patched_env(bin_dir: pathlib.Path, base: dict | None = None) -> dict: + """Return an env dict with *bin_dir* prepended to PATH.""" + env = (base or os.environ).copy() + env["PATH"] = str(bin_dir) + os.pathsep + env.get("PATH", "") + return env diff --git a/.github/scripts/meat/tests/test_build_report.py b/.github/scripts/meat/tests/test_build_report.py new file mode 100644 index 000000000000..4b18c1cb5db1 --- /dev/null +++ b/.github/scripts/meat/tests/test_build_report.py @@ -0,0 +1,364 @@ +# Copyright (C) 2018-2026 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +"""Tests for build_report.py. + +Covers: + - Status determination (success / partial / failed) + - All five output artefacts are produced + - conversion_report.md contains all required sections + - analyse_and_convert_result.json schema + - agent-complete marker: present, well-formed JSON, correct field values + - Missing input files → graceful degradation (no crash, outputs still written) + - No gh CLI invoked when PR_NUMBER / ISSUE_NUMBER are absent +""" + +import json +import os +import pathlib +import re + +import pytest + +from conftest import SCRIPTS_DIR, run_script, write_json + +SCRIPT = SCRIPTS_DIR / "build_report.py" +OUT_DIR = pathlib.Path("agent-results") / "analyze-and-convert" + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +_DEFAULT_PROFILE = { + "model_id": "test/my-model", + "model_type": "llama", + "architectures": ["LlamaForCausalLM"], + "pipeline_tag": "text-generation", + "estimated_params_b": 7.0, + "is_vlm": False, + "trust_remote_code_required": False, +} + +_DEFAULT_SIGNALS = { + "error_class": "missing_conversion_rule", + "target_agent": "enable-operator", + "requires_optimum_new_arch": False, + "requires_transformers_upgrade": False, + "transformers_override": "", + "requires_tokenizer_check": False, + "is_vlm": False, + "custom_ops_suspected": False, + "oom_suspected": False, +} + +_FAILED_ATTEMPT = { + "id": "A-fp16-stable", + "description": "fp16 stable", + "weight_format": "fp16", + "optimum_version": "stable", + "success": False, + "returncode": 1, + "elapsed_s": 10.0, + "stdout": "", + "stderr": "RuntimeError: no rule for aten::erfinv", +} + +_SUCCESS_ATTEMPT = { + "id": "A-fp16-stable", + "description": "fp16 stable", + "weight_format": "fp16", + "optimum_version": "stable", + "success": True, + "returncode": 0, + "elapsed_s": 8.0, + "stdout": "Export done", + "stderr": "", + "ir_files": ["openvino_model.xml", "openvino_model.bin"], + "ir_dir": "ov_model_A", +} + + +def _setup(tmp_path, *, attempts=None, signals=None, profile=None, excerpts=None): + write_json(tmp_path / "model_profile.json", profile or _DEFAULT_PROFILE) + write_json(tmp_path / "routing_signals.json", signals or _DEFAULT_SIGNALS) + write_json(tmp_path / "conversion_attempts.json", attempts if attempts is not None else [_FAILED_ATTEMPT]) + write_json(tmp_path / "error_excerpts.json", excerpts or {}) + + +def _run(tmp_path, **kwargs): + """Run build_report.py with PR_NUMBER/ISSUE_NUMBER unset to suppress gh calls.""" + env = os.environ.copy() + env.pop("PR_NUMBER", None) + env.pop("ISSUE_NUMBER", None) + return run_script(SCRIPT, tmp_path, extra_env=env) + + +def _report(tmp_path) -> str: + return (tmp_path / OUT_DIR / "conversion_report.md").read_text(encoding="utf-8") + + +def _result(tmp_path) -> dict: + return json.loads((tmp_path / OUT_DIR / "analyze_and_convert_result.json").read_text(encoding="utf-8")) + + +def _marker(stdout: str) -> dict: + """Parse the block from stdout.""" + m = re.search(r"", stdout, re.DOTALL) + assert m, f"agent-complete marker not found in stdout:\n{stdout}" + return json.loads(m.group(1)) + + +# --------------------------------------------------------------------------- +# Status determination +# --------------------------------------------------------------------------- + +def test_status_failed_when_all_attempts_failed(tmp_path): + _setup(tmp_path) + _run(tmp_path) + assert _result(tmp_path)["status"] == "failed" + + +def test_status_success_when_one_attempt_succeeded(tmp_path): + _setup(tmp_path, attempts=[_SUCCESS_ATTEMPT]) + _run(tmp_path) + assert _result(tmp_path)["status"] == "success" + + +def test_status_failed_when_no_attempts_at_all(tmp_path): + _setup(tmp_path, attempts=[]) + _run(tmp_path) + assert _result(tmp_path)["status"] == "failed" + + +# --------------------------------------------------------------------------- +# Output files are produced +# --------------------------------------------------------------------------- + +def test_conversion_report_md_is_written(tmp_path): + _setup(tmp_path) + _run(tmp_path) + assert (tmp_path / OUT_DIR / "conversion_report.md").exists() + + +def test_result_json_is_written(tmp_path): + _setup(tmp_path) + _run(tmp_path) + assert (tmp_path / OUT_DIR / "analyze_and_convert_result.json").exists() + + +def test_output_dir_is_created_automatically(tmp_path): + _setup(tmp_path) + _run(tmp_path) + assert (tmp_path / OUT_DIR).is_dir() + + +# --------------------------------------------------------------------------- +# result JSON schema +# --------------------------------------------------------------------------- + +def test_result_json_required_fields(tmp_path): + _setup(tmp_path) + _run(tmp_path) + d = _result(tmp_path) + for field in ("agent", "status", "model_id", "error_class", "target_agent", "routing_signals"): + assert field in d, f"Missing field: {field!r}" + + +def test_result_json_agent_name(tmp_path): + _setup(tmp_path) + _run(tmp_path) + assert _result(tmp_path)["agent"] == "analyze-and-convert" + + +def test_result_json_model_id_matches_profile(tmp_path): + _setup(tmp_path) + _run(tmp_path) + assert _result(tmp_path)["model_id"] == "test/my-model" + + +def test_result_json_error_class_matches_signals(tmp_path): + _setup(tmp_path) + _run(tmp_path) + assert _result(tmp_path)["error_class"] == "missing_conversion_rule" + + +def test_result_json_target_agent_matches_signals(tmp_path): + _setup(tmp_path) + _run(tmp_path) + assert _result(tmp_path)["target_agent"] == "enable-operator" + + +def test_result_json_routing_signals_subset(tmp_path): + """routing_signals in result JSON must only contain known signal keys.""" + _setup(tmp_path) + _run(tmp_path) + allowed = { + "requires_optimum_new_arch", "requires_transformers_upgrade", + "transformers_override", "requires_tokenizer_check", + "trust_remote_code_required", "is_vlm", + "custom_ops_suspected", "oom_suspected", + } + actual = set(_result(tmp_path)["routing_signals"].keys()) + assert actual <= allowed, f"Unexpected keys in routing_signals: {actual - allowed}" + + +# --------------------------------------------------------------------------- +# conversion_report.md content +# --------------------------------------------------------------------------- + +def test_report_contains_model_id(tmp_path): + _setup(tmp_path) + _run(tmp_path) + assert "test/my-model" in _report(tmp_path) + + +def test_report_has_model_profile_section(tmp_path): + _setup(tmp_path) + _run(tmp_path) + assert "Model Profile" in _report(tmp_path) + + +def test_report_has_conversion_attempts_section(tmp_path): + _setup(tmp_path) + _run(tmp_path) + assert "Conversion Attempts" in _report(tmp_path) + + +def test_report_has_routing_signals_section(tmp_path): + _setup(tmp_path) + _run(tmp_path) + assert "Routing Signals" in _report(tmp_path) + + +def test_report_has_recommended_next_step_section(tmp_path): + _setup(tmp_path) + _run(tmp_path) + assert "Recommended Next Step" in _report(tmp_path) + + +def test_report_contains_error_class(tmp_path): + _setup(tmp_path) + _run(tmp_path) + assert "missing_conversion_rule" in _report(tmp_path) + + +def test_report_has_failure_details_when_failed(tmp_path): + excerpts = {"A-fp16-stable": "Traceback (most recent call last):\n RuntimeError: no rule"} + _setup(tmp_path, excerpts=excerpts) + _run(tmp_path) + assert "Failure Details" in _report(tmp_path) + + +def test_report_no_failure_section_when_succeeded(tmp_path): + _setup(tmp_path, attempts=[_SUCCESS_ATTEMPT]) + _run(tmp_path) + assert "Failure Details" not in _report(tmp_path) + + +def test_report_has_successful_strategy_section_on_success(tmp_path): + _setup(tmp_path, attempts=[_SUCCESS_ATTEMPT]) + _run(tmp_path) + assert "Successful Strategy" in _report(tmp_path) + + +# --------------------------------------------------------------------------- +# agent-complete marker +# --------------------------------------------------------------------------- + +def test_agent_complete_marker_present_in_stdout(tmp_path): + _setup(tmp_path) + result = _run(tmp_path) + assert "" in result.stdout + + +def test_agent_complete_marker_is_valid_json(tmp_path): + _setup(tmp_path) + result = _run(tmp_path) + _marker(result.stdout) # asserts internally + + +def test_agent_complete_has_all_required_fields(tmp_path): + _setup(tmp_path) + result = _run(tmp_path) + m = _marker(result.stdout) + for field in ("agent", "status", "next_agent", "error_class", "model_id", "next_context"): + assert field in m, f"Missing field in agent-complete: {field!r}" + + +def test_agent_complete_agent_name_correct(tmp_path): + _setup(tmp_path) + result = _run(tmp_path) + assert _marker(result.stdout)["agent"] == "analyze-and-convert" + + +def test_agent_complete_status_failed(tmp_path): + _setup(tmp_path) + result = _run(tmp_path) + assert _marker(result.stdout)["status"] == "failed" + + +def test_agent_complete_status_success(tmp_path): + _setup(tmp_path, attempts=[_SUCCESS_ATTEMPT]) + result = _run(tmp_path) + assert _marker(result.stdout)["status"] == "success" + + +def test_agent_complete_next_agent_matches_signals(tmp_path): + _setup(tmp_path) + result = _run(tmp_path) + assert _marker(result.stdout)["next_agent"] == "enable-operator" + + +def test_agent_complete_model_id_correct(tmp_path): + _setup(tmp_path) + result = _run(tmp_path) + assert _marker(result.stdout)["model_id"] == "test/my-model" + + +# --------------------------------------------------------------------------- +# Resilience: missing inputs +# --------------------------------------------------------------------------- + +def test_missing_all_inputs_does_not_crash(tmp_path): + """Script must produce outputs even with zero input files.""" + result = _run(tmp_path) + assert result.returncode == 0 + assert (tmp_path / OUT_DIR / "conversion_report.md").exists() + assert (tmp_path / OUT_DIR / "analyze_and_convert_result.json").exists() + assert " + 5 + 16 + + + 5 + 16 + 4 + + + 16 + 1 + 4 + + + 16 + + + 3 + + + 5 + + + 3 + + + 2 + + + 2 + + + + + 5 + 16 + + + diff --git a/docs/articles_en/documentation/openvino-ir-format/operation-sets/operation-specs/internal/paged-gated-delta-net.rst b/docs/articles_en/documentation/openvino-ir-format/operation-sets/operation-specs/internal/paged-gated-delta-net.rst index 7260c7dbdd59..1a6b62020a66 100644 --- a/docs/articles_en/documentation/openvino-ir-format/operation-sets/operation-specs/internal/paged-gated-delta-net.rst +++ b/docs/articles_en/documentation/openvino-ir-format/operation-sets/operation-specs/internal/paged-gated-delta-net.rst @@ -50,8 +50,9 @@ value head ``h_v`` (``0 .. v_num_heads - 1``), where the corresponding query/key # Project with query to get output output[t, h_v] = q[t, h_q] @ S[h_v].transpose(1, 0) # shape: [value_head_dim] -The recurrent state is initialized to an all-zeros tensor. It is updated token by token within -each sequence in causal order. The operation caches intermediate states at regular intervals +For new input sequence the initial recurrent state should be zeroed. User can set own state if needed. +It is updated token by token within each sequence in causal order. +The operation caches intermediate states at regular intervals (controlled per sequence by ``cache_interval``) into the paged ``recurrent_state_table``, allowing efficient prefill replay and incremental decode. @@ -61,23 +62,40 @@ The recurrent state table is organized as non-contiguous pages (blocks). Each bl complete state snapshot for all value heads at a particular token position in the sequence. For sequence ``s``, the assigned physical block indices are -``block_indices[block_indices_begins[s] : block_indices_begins[s+1]]``. +``la_block_indices[la_block_indices_begins[s] : la_block_indices_begins[s+1]]``. These indices address rows in ``recurrent_state_table``. The first block stores the state after ``cache_interval[s]`` tokens, the second after ``2 * cache_interval[s]`` tokens, and so on. When ``cache_interval[s] <= 0``, no state caching is performed for that sequence. -The ``past_lens[s]`` value indicates how many tokens have already been processed for sequence -``s``. Combined with the cached blocks, it determines the starting state for new tokens: -the most recent cached block before ``past_lens[s]`` is loaded, and the remaining -tokens up to ``past_lens[s]`` are replayed from that checkpoint. +The ``num_processed_tokens[s]`` value indicates how many tokens have already been processed for sequence +``s``. Denote ``num_current_tokens[s]`` as the number of current tokens to process. +It can be computed as: ``subsequence_begins[s+1] - subsequence_begins[s]``. +Then `N`, the number of blocks for writing, is computed as: +``N = ceil((num_processed_tokens[s] % cache_interval[s] + num_current_tokens[s]) / cache_interval[s])`` +Let the blocks passed through `la_block_indices` be indexed as block `0, 1, ..., N`. +Cases for reading and updating blocks: + +1. **Prefill with no past** + Read from block 0 and write to blocks 1...N. + Block 0 and block 1 refer to the same block, so block 0 is updated in-place. + +2. **Prefill with `num_processed_tokens[s] % cache_interval[s] == 0`** + Read from block 0 and write to blocks 1...N. + Block 0 and block 1 refer to different blocks. + +3. **Prefill with `num_processed_tokens[s] % cache_interval[s] != 0`** + Read from block 0 and write to blocks 1...N. + Block 0 and block 1 refer to the same block, so block 0 is updated in-place. + +4. **Decode with `num_processed_tokens[s] % cache_interval[s] == 0`** + Read from block 0 and write to block 1. + Block 0 and block 1 refer to different blocks. + +5. **Decode with `num_processed_tokens[s] % cache_interval[s] != 0`** + Read from block 0 and write to block 1. + Block 0 and block 1 refer to the same block, so block 0 is updated in-place. -Use cases: -1. prefill with no past. Read from block 0, write to block 1...N -2. prefill with past_len % cache_interval == 0. Read from block 0, write to block 1...N -3. prefill with past_len % cache_interval !=0. Read from block 0, write to block 1...N -4. decode with past_len % cache_interval == 0. Read from block 0, write to block 1 -5. decode with past_len % cache_interval !=0. Read from block 0, write to block 1 **Attributes** * *use_qk_l2norm* @@ -136,18 +154,18 @@ Use cases: ``query``, ``key``, ``value``). The tokens of sequence ``s`` span ``[subsequence_begins[s], subsequence_begins[s+1])``. **Required.** -* **7**: ``block_indices`` - Tensor of type *T_IND* and shape ``[num_blocks]``. +* **7**: ``la_block_indices`` - Tensor of type *T_IND* and shape ``[num_blocks]``. Physical block row indices into ``recurrent_state_table``, concatenated across all sequences. For example, ``[0, 1, 3, 2, 4]`` with five blocks. **Required.** -* **8**: ``block_indices_begins`` - Tensor of type *T_IND* and shape +* **8**: ``la_block_indices_begins`` - Tensor of type *T_IND* and shape ``[batch_size_in_sequences + 1]``. - Splits ``block_indices`` among sequences. The block indices for sequence ``s`` are - ``block_indices[block_indices_begins[s] : block_indices_begins[s+1]]``. - For example, ``block_indices = [0, 1, 3, 2, 4]`` and ``block_indices_begins = [0, 3, 5]`` + Splits ``la_block_indices`` among sequences. The block indices for sequence ``s`` are + ``la_block_indices[la_block_indices_begins[s] : la_block_indices_begins[s+1]]``. + For example, ``la_block_indices = [0, 1, 3, 2, 4]`` and ``la_block_indices_begins = [0, 3, 5]`` means sequence 0 uses blocks ``[0, 1, 3]`` and sequence 1 uses blocks ``[2, 4]``. **Required.** -* **9**: ``past_lens`` - Tensor of type *T_IND* and shape ``[batch_size_in_sequences]``. +* **9**: ``num_processed_tokens`` - Tensor of type *T_IND* and shape ``[batch_size_in_sequences]``. Number of tokens already processed for each sequence. Used together with the cached states in ``recurrent_state_table`` to determine the starting recurrent state for each sequence. **Required.** diff --git a/docs/articles_en/documentation/openvino-ir-format/operation-sets/operation-specs/movement/pad-12.rst b/docs/articles_en/documentation/openvino-ir-format/operation-sets/operation-specs/movement/pad-12.rst index e68066733cfc..9b95ddd5d02f 100644 --- a/docs/articles_en/documentation/openvino-ir-format/operation-sets/operation-specs/movement/pad-12.rst +++ b/docs/articles_en/documentation/openvino-ir-format/operation-sets/operation-specs/movement/pad-12.rst @@ -192,7 +192,7 @@ Mixed pads example: * **3**: ``pads_end`` 1D tensor of type *T_INT*. Number of elements matches the shape rank of *data* input. Specifies the number of padding elements to add at the end of each axis. Negative value means cropping the corresponding dimension's value. **Required.** -* **4**: ``pad_value`` scalar tensor of type *T*. Takes effect only if ``pad_mode == "constant"`` only. All padding elements are populated with this value or with 0 if the input is not provided. This input should not be set with other values of ``pad_mode``. **Optional.** +* **4**: ``pad_value`` scalar tensor of type *T*. Takes effect only if ``pad_mode == "constant"`` only. All padding elements are populated with this value or with 0 (empty string for *T* = ``string``) if the input is not provided. This input should not be set with other values of ``pad_mode``. **Optional.** **Outputs** @@ -201,7 +201,7 @@ Mixed pads example: **Types** -* *T*: any numeric type. +* *T*: any numeric type or ``string``. When *T* is ``string``, only ``pad_mode = "constant"`` is supported. * *T_INT*: any integer type. diff --git a/docs/articles_en/documentation/openvino-ir-format/operation-sets/operation-specs/sort/top-k-1.rst b/docs/articles_en/documentation/openvino-ir-format/operation-sets/operation-specs/sort/top-k-1.rst index 9d7074711513..599608715b89 100644 --- a/docs/articles_en/documentation/openvino-ir-format/operation-sets/operation-specs/sort/top-k-1.rst +++ b/docs/articles_en/documentation/openvino-ir-format/operation-sets/operation-specs/sort/top-k-1.rst @@ -76,6 +76,10 @@ Sorting and minimum/maximum are controlled by ``sort`` and ``mode`` attributes: If there are several elements with the same value then their output order is not determined. +**NaN Handling** + +For floating-point types, the ordering of NaN values in the output is implementation-defined. Per IEEE 754, NaN fails all ordered comparisons, so there is no single correct placement for NaN in a sorted result. Different frameworks (e.g., NumPy, PyTorch) handle NaN differently, and the TopK implementation does not guarantee a specific NaN ordering. If deterministic behavior is required, NaN values should be sanitized before the TopK operation (e.g., replaced with ``-inf`` or ``+inf`` depending on the desired placement). + **Example** .. code-block:: cpp diff --git a/docs/articles_en/documentation/openvino-ir-format/operation-sets/operation-specs/sort/top-k-11.rst b/docs/articles_en/documentation/openvino-ir-format/operation-sets/operation-specs/sort/top-k-11.rst index 98bdfaf216a7..83e2403fb968 100644 --- a/docs/articles_en/documentation/openvino-ir-format/operation-sets/operation-specs/sort/top-k-11.rst +++ b/docs/articles_en/documentation/openvino-ir-format/operation-sets/operation-specs/sort/top-k-11.rst @@ -94,6 +94,11 @@ Sorting and minimum/maximum are controlled by ``sort`` and ``mode`` attributes w * *sort* = ``none`` , *mode* = ``min`` - undefined The relative order of equivalent elements is only preserved if the ``stable`` attribute is set to ``true``. This makes the implementation use stable sorting algorithm during the computation of TopK elements. Otherwise the output order is undefined. + +**NaN Handling** + +For floating-point types, the ordering of NaN values in the output is implementation-defined. Per IEEE 754, NaN fails all ordered comparisons, so there is no single correct placement for NaN in a sorted result. Different frameworks (e.g., NumPy, PyTorch) handle NaN differently, and the TopK implementation does not guarantee a specific NaN ordering. If deterministic behavior is required, NaN values should be sanitized before the TopK operation (e.g., replaced with ``-inf`` or ``+inf`` depending on the desired placement). + The "by index" order means that the input tensor's elements are still sorted by value but their order in the output tensor is additionally determined by the indices of those elements in the input tensor. This might yield multiple correct results though. For example if the input tensor contains the following elements: .. code-block:: cpp diff --git a/docs/articles_en/documentation/openvino-ir-format/operation-sets/operation-specs/sort/top-k-3.rst b/docs/articles_en/documentation/openvino-ir-format/operation-sets/operation-specs/sort/top-k-3.rst index 4ef7d71c91fd..009fd46d4cdb 100644 --- a/docs/articles_en/documentation/openvino-ir-format/operation-sets/operation-specs/sort/top-k-3.rst +++ b/docs/articles_en/documentation/openvino-ir-format/operation-sets/operation-specs/sort/top-k-3.rst @@ -83,6 +83,10 @@ Sorting and minimum/maximum are controlled by ``sort`` and ``mode`` attributes: If there are several elements with the same value then their output order is not determined. +**NaN Handling** + +For floating-point types, the ordering of NaN values in the output is implementation-defined. Per IEEE 754, NaN fails all ordered comparisons, so there is no single correct placement for NaN in a sorted result. Different frameworks (e.g., NumPy, PyTorch) handle NaN differently, and the TopK implementation does not guarantee a specific NaN ordering. If deterministic behavior is required, NaN values should be sanitized before the TopK operation (e.g., replaced with ``-inf`` or ``+inf`` depending on the desired placement). + **Example** .. code-block:: cpp diff --git a/docs/articles_en/get-started/install-openvino/install-openvino-windows.rst b/docs/articles_en/get-started/install-openvino/install-openvino-windows.rst index e3b2ee73249f..83d051efb70d 100644 --- a/docs/articles_en/get-started/install-openvino/install-openvino-windows.rst +++ b/docs/articles_en/get-started/install-openvino/install-openvino-windows.rst @@ -19,6 +19,7 @@ Install OpenVINO™ Runtime on Windows Use Docker Use Conan Use npm + Use WinGet If you want to install OpenVINO™ Runtime on Windows, you have the following options: @@ -30,5 +31,6 @@ If you want to install OpenVINO™ Runtime on Windows, you have the following op * :doc:`Install OpenVINO using Docker ` * :doc:`Install OpenVINO using Conan Package Manager ` * :doc:`Install OpenVINO using npm ` +* :doc:`Install OpenVINO using WinGet ` diff --git a/docs/articles_en/get-started/install-openvino/install-openvino-winget.rst b/docs/articles_en/get-started/install-openvino/install-openvino-winget.rst new file mode 100644 index 000000000000..7d63910d57b5 --- /dev/null +++ b/docs/articles_en/get-started/install-openvino/install-openvino-winget.rst @@ -0,0 +1,172 @@ +Install OpenVINO™ Runtime with WinGet +====================================== + +.. meta:: + :description: Learn how to install OpenVINO™ Runtime on Windows operating systems, using WinGet. + +.. note:: + + Due to the community-driven nature of this distribution channel, the OpenVINO team does not guarantee timely updates aligned with official releases, nor update availability for all OpenVINO versions. + +.. note:: + + The WinGet distribution: + + * provides OpenVINO Runtime for Windows x64; + * is delivered as an MSIX package from the WinGet Community Repository; + * uses versioned WinGet package identifiers, for example ``Intel.OpenVINOToolkit.2026.2.0``; + * allows different OpenVINO releases to be installed side by side; + * does not automatically move an existing project to a newer OpenVINO release line. + +Before installing OpenVINO, see the :doc:`System Requirements page <../../../about-openvino/release-notes-openvino/system-requirements>`. + +.. note:: + + WinGet is a Windows-only installation option. If you need OpenVINO Runtime for Linux or macOS, or if you need Python packages, use another installation method such as archive packages, PyPI or other supported package managers. + +Installing OpenVINO Runtime with WinGet +####################################### + +1. Check that WinGet is available on your system: + + .. code-block:: bat + + winget --version + + If the command is not available, install or repair App Installer. For details, see the `Microsoft WinGet documentation `__ and the `Microsoft App Installer troubleshooting guide `__. + +2. Update WinGet package sources: + + .. code-block:: bat + + winget source update + + For details, see the `Microsoft documentation for the WinGet source command `__. + +3. Search for the OpenVINO package: + + .. code-block:: bat + + winget search --id Intel.OpenVINOToolkit.2026.2.0 -e --source winget + + For details, see the `Microsoft documentation for the WinGet search command `__. + +4. Install OpenVINO Runtime: + + .. code-block:: bat + + winget install --id Intel.OpenVINOToolkit.2026.2.0 -e --source winget + + To install an exact WinGet package version, add the ``--version`` option: + + .. code-block:: bat + + winget install --id Intel.OpenVINOToolkit.2026.2.0 --version 2026.2.0.0 -e --source winget + + For details, see the `Microsoft documentation for the WinGet install command `__. + +OpenVINO Runtime is now installed using WinGet. + +Installing a specific OpenVINO release +++++++++++++++++++++++++++++++++++++++ + +OpenVINO packages in WinGet use the OpenVINO release number in the WinGet package identifier. This is intentional and allows you to keep different OpenVINO releases installed side by side. + +Use the following WinGet package identifier pattern: + +.. code-block:: text + + Intel.OpenVINOToolkit... + +For example, after the corresponding packages are published in WinGet, you can install a specific OpenVINO release with one of the following commands: + +.. code-block:: bat + + winget install --id Intel.OpenVINOToolkit.2026.2.0 -e --source winget + winget install --id Intel.OpenVINOToolkit.2026.1.0 -e --source winget + winget install --id Intel.OpenVINOToolkit.2026.0.0 -e --source winget + +The WinGet package version may include an additional revision number, for example ``2026.2.0.0``. Use ``--version`` if you need to select the exact WinGet package version. + +Understanding WinGet package identifier and MSIX package name ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +OpenVINO uses two different package names in the WinGet distribution flow: + +* ``Intel.OpenVINOToolkit.2026.2.0`` is the WinGet package identifier. Use this identifier with WinGet commands such as ``winget install``, ``winget list``, ``winget show``, and ``winget uninstall``. +* ``IntelCorporation.OpenVINOToolkit.2026.2.0`` is the MSIX package name used by Windows after installation. Use this name with PowerShell commands such as ``Get-AppxPackage``. + +This difference is expected. WinGet resolves the package by the WinGet package identifier and installs the MSIX package registered in the manifest. After installation, Windows manages the package as an MSIX package under its MSIX package name. + +Verifying the installation +++++++++++++++++++++++++++ + +To check that the package is installed, use the WinGet package identifier: + +.. code-block:: bat + + winget list --id Intel.OpenVINOToolkit.2026.2.0 -e + +For details, see the `Microsoft documentation for the WinGet list command `__. + +To show package metadata available from WinGet, use the WinGet package identifier: + +.. code-block:: bat + + winget show --id Intel.OpenVINOToolkit.2026.2.0 -e --source winget + +For details, see the `Microsoft documentation for the WinGet show command `__. + +Locating the OpenVINO installation +++++++++++++++++++++++++++++++++++ + +The WinGet package is installed as an MSIX package, so Windows manages the installation location. To find the installation directory, run the following command in PowerShell with the MSIX package name: + +.. code-block:: powershell + + (Get-AppxPackage -Name "IntelCorporation.OpenVINOToolkit.2026.2.0").InstallLocation + +For details, see the `Microsoft documentation for Get-AppxPackage `__. + +You can use this path as the OpenVINO installation root in your build scripts or local development environment. + +For CMake-based C++ applications, you may set ``OpenVINO_DIR`` to the OpenVINO CMake package location. For example: + +.. code-block:: powershell + + $openvinoPackage = Get-AppxPackage -Name "IntelCorporation.OpenVINOToolkit.2026.2.0" + $env:OpenVINO_DIR = Join-Path $openvinoPackage.InstallLocation "runtime\cmake" + +Then configure your application with CMake as usual. + +Enabling GPU and NPU devices for inference +++++++++++++++++++++++++++++++++++++++++++ + +The WinGet package installs OpenVINO Runtime, but it does not install all hardware drivers required for every device. Some hardware, including GPU and NPU, may require additional driver installation or operating system updates. + +For more details, see the :doc:`Additional Hardware Setup and Troubleshooting <./configurations>` page and the :doc:`System Requirements page <../../../about-openvino/release-notes-openvino/system-requirements>`. + +Uninstalling OpenVINO™ Runtime +############################## + +Once OpenVINO Runtime is installed via WinGet, you can remove it using the WinGet package identifier: + +.. code-block:: bat + + winget uninstall --id Intel.OpenVINOToolkit.2026.2.0 -e --source winget + +For details, see the `Microsoft documentation for the WinGet uninstall command `__. + +If you have multiple OpenVINO releases installed side by side, run the uninstall command for each WinGet package identifier you want to remove. + +What's Next? +############ + +Now that you have installed OpenVINO Runtime, you are ready to run your own machine learning applications. To learn more about how to integrate a model in OpenVINO applications, try out some tutorials and sample applications. + +Try the :doc:`C++ Quick Start Example <../../../get-started/learn-openvino/openvino-samples/get-started-demos>` for step-by-step instructions on building and running a basic image classification C++ application. + +Visit the :doc:`Samples <../../../get-started/learn-openvino/openvino-samples>` page for other C++ example applications to get you started with OpenVINO, such as: + +* :doc:`Basic object detection with the Hello Reshape SSD C++ sample <../../../get-started/learn-openvino/openvino-samples/hello-reshape-ssd>` +* :doc:`Object classification sample <../../../get-started/learn-openvino/openvino-samples/hello-classification>` diff --git a/docs/articles_en/openvino-workflow/model-preparation/convert-model-pytorch.rst b/docs/articles_en/openvino-workflow/model-preparation/convert-model-pytorch.rst index 2a64976bffa8..094f05e83bf5 100644 --- a/docs/articles_en/openvino-workflow/model-preparation/convert-model-pytorch.rst +++ b/docs/articles_en/openvino-workflow/model-preparation/convert-model-pytorch.rst @@ -68,7 +68,7 @@ inference in the existing PyTorch application to OpenVINO and how to get value f import requests, PIL, io, torch # Get a picture of a cat from the web: - img = PIL.Image.open(io.BytesIO(requests.get("https://placekitten.com/200/300").content)) + img = PIL.Image.open(io.BytesIO(requests.get("https://placekittens.com/200/300").content)) # Torchvision model and input data preparation from https://pytorch.org/vision/stable/models.html diff --git a/docs/articles_en/openvino-workflow/running-inference/inference-devices-and-modes/gpu-device/remote-tensor-api-gpu-plugin.rst b/docs/articles_en/openvino-workflow/running-inference/inference-devices-and-modes/gpu-device/remote-tensor-api-gpu-plugin.rst index adac7c64a9e1..8a014404459f 100644 --- a/docs/articles_en/openvino-workflow/running-inference/inference-devices-and-modes/gpu-device/remote-tensor-api-gpu-plugin.rst +++ b/docs/articles_en/openvino-workflow/running-inference/inference-devices-and-modes/gpu-device/remote-tensor-api-gpu-plugin.rst @@ -254,6 +254,19 @@ For more details, see the code snippets below: :language: cpp :fragment: [wrap_cl_image] + .. tab-item:: external shared handle + :sync: external-shared-handle + + Use this overload when your application already owns an OS-level shared memory handle + (for example, DX12 NT handle on Windows or DMA-BUF file descriptor on Linux). + + .. doxygensnippet:: docs/articles_en/assets/snippets/gpu/remote_objects_creation.cpp + :language: cpp + :fragment: [wrap_shared_handle] + + The ``shape`` and ``element type`` must describe the same memory layout as the external buffer. + The handle must remain valid for the whole lifetime of the created remote tensor. + .. tab-item:: biplanar NV12 surface :sync: biplanar-nv12-surface diff --git a/docs/articles_en/openvino-workflow/running-inference/inference-devices-and-modes/npu-device.rst b/docs/articles_en/openvino-workflow/running-inference/inference-devices-and-modes/npu-device.rst index fa6295237a54..bffa77e503eb 100644 --- a/docs/articles_en/openvino-workflow/running-inference/inference-devices-and-modes/npu-device.rst +++ b/docs/articles_en/openvino-workflow/running-inference/inference-devices-and-modes/npu-device.rst @@ -184,6 +184,11 @@ The NPU device is currently supported by AUTO inference modes ov::intel_npu::compiler_version ov::intel_npu::max_tiles + .. tab-item:: Write-only properties + + .. code-block:: + + ov::cache_encryption_callbacks .. note:: @@ -313,6 +318,66 @@ Example: Setting ``performance-hint-override=latency`` through ``ov::intel_npu:: to use all available resources for the given platform. If ``ov::intel_npu::max_tiles`` is not provided, the compiler falls back to a fixed lookup table embedded in the library to determine available resources, which might not be representative of all SKUs. +**ov::cache_encryption_callbacks** + +Enables blob encryption and decryption using user-provided callbacks. This is a write-only property that accepts a struct with encryption and decryption callback functions. +Can be used for both model caching and user requested export or import of compiled models (blobs) scenarios. + +.. note:: + + If user compiles a model using ``Compiler-In-Driver``, currently there is no secure compilation available in driver until ``32.0.100.4724`` (inclusively) and an exception will be thrown + for older drivers during compilation if encryption callbacks were set. + +Usage example: + +.. note:: + + Will set OV provided ``codec_xor`` utility encryption and decryption at plugin level, every compiled model created will inherit these callbacks if not changed explicitly. + +.. code-block:: + + core.set_property("NPU", ov::cache_encryption_callbacks(ov::EncryptionCallbacks(ov::util::codec_xor, ov::util::codec_xor))); + + core.compile_model(ov_model, "NPU").export_model(encrypted_blob_stream); + + core.import_model(encrypted_blob_stream, "NPU"); + +.. note:: + + Also force decryption function by providing it as a property to ``import_model`` API + +.. code-block:: + + core.import_model(encrypted_blob_stream, "NPU", {ov::cache_encryption_callbacks(ov::EncryptionCallbacks{nullptr, ov::util::codec_xor})}); + +.. note:: + + Force other encryption and decryption callbacks for compiled model below + +.. code-block:: + + auto compile_model = core.compile_model(ov_model, "NPU", {ov::cache_encryption_callbacks(ov::EncryptionCallbacks{[](const std::string& str) { + auto custom_encryption = str; + // user encryption logic here... + return custom_encryption; + }, nullptr})}); + + compiled_model.export_model(custom_encrypted_blob_stream); + +or + +.. code-block:: + + auto compile_model = core.compile_model(ov_model, "NPU"); + + compiled_model.set_property(ov::cache_encryption_callbacks(ov::EncryptionCallbacks{[](const std::string& str) { + auto custom_encryption = str; + // user encryption logic here... + return custom_encryption; + }, nullptr})); + + compiled_model.export_model(custom_encrypted_blob_stream); + Limitations ############################# diff --git a/docs/dev/build_android.md b/docs/dev/build_android.md index f973194b217a..9ecc8b691287 100644 --- a/docs/dev/build_android.md +++ b/docs/dev/build_android.md @@ -54,6 +54,9 @@ _For Windows and Mac operating systems, the downloading and unpacking steps are ### Build and install OneTBB™ (Not for RISC-V 64 architecture) To improve the parallelism performance of the OpenVINO™ library using OneTBB, it is required to separately build OneTBB for a specific version of the Android NDK: +Android OpenVINO build with `THREADING=TBB` requires this separate OneTBB build. +OpenVINO CMake configure must be called with `-DTBB_DIR=$OPV_HOME_DIR/one-tbb-install/lib/cmake/TBB`. +If `TBB_DIR` is not provided, Android configure fails by design. ```sh # Clone OneTBB™ repository git clone --recursive https://github.com/oneapi-src/oneTBB $OPV_HOME_DIR/one-tbb @@ -76,6 +79,9 @@ To improve the parallelism performance of the OpenVINO™ library using OneTBB, cmake --install $OPV_HOME_DIR/one-tbb-build ``` +Android OpenVINO configuration expects the separately built OneTBB package. +`TBB_DIR` must be provided when configuring OpenVINO. + ### Clone OpenVINO™ GenAI (Optional) ```sh git clone --recursive https://github.com/openvinotoolkit/openvino.genai $OPV_HOME_DIR/openvino.genai @@ -96,9 +102,8 @@ To improve the parallelism performance of the OpenVINO™ library using OneTBB, -DANDROID_ABI=$CURRENT_ANDROID_ABI \ -DANDROID_PLATFORM=$CURRENT_ANDROID_PLATFORM \ -DANDROID_STL=$CURRENT_ANDROID_STL \ - -DOPENVINO_EXTRA_MODULES=$OPV_HOME_DIR/openvino.genai \ - -DTBBROOT=$OPV_HOME_DIR/one-tbb-install \ - -DTBB_DIR=$OPV_HOME_DIR/one-tbb-install/lib/cmake/TBB + -DTBB_DIR=$OPV_HOME_DIR/one-tbb-install/lib/cmake/TBB \ + -DOPENVINO_EXTRA_MODULES=$OPV_HOME_DIR/openvino.genai # Build OpenVINO™ project cmake --build $OPV_HOME_DIR/openvino-build --parallel # Install OpenVINO™ project diff --git a/docs/dev/build_raspbian.md b/docs/dev/build_raspbian.md index d227fe6e78dd..16618dd6c7f1 100644 --- a/docs/dev/build_raspbian.md +++ b/docs/dev/build_raspbian.md @@ -51,6 +51,207 @@ sudo dphys-swapfile setup sudo dphys-swapfile swapon ``` +## Cross-compilation for Raspberry Pi ARM64 + +Cross-compilation uses one host-specific CMake toolchain file: + +* `cmake/arm64.toolchain.cmake` for Linux-hosted cross-compilation to Raspberry Pi AArch64 Linux; +* `cmake/rpi-aarch64-linux-from-macos.toolchain.cmake` for macOS-hosted cross-compilation to Raspberry Pi AArch64 Linux; +* `cmake/rpi-aarch64-linux-from-windows.toolchain.cmake` for Windows-hosted cross-compilation to Raspberry Pi AArch64 Linux. + +The macOS and Windows toolchains generate compiler, binutils, and Arm Compute Library SCons wrappers in the build directory. Wrapper templates and helper sources are stored in `cmake/rpi_cross/`. + +Create a working directory for downloaded host tools and build outputs: + +```bash +mkdir openvino-rpi-cross +export OV_RPI_HOME=${PWD}/openvino-rpi-cross +``` + +On Windows PowerShell: + +```powershell +$env:OV_RPI_HOME = "$PWD\openvino-rpi-cross" +New-Item -ItemType Directory -Force -Path $env:OV_RPI_HOME +``` + +### Prepare a target sysroot + +The macOS and Windows toolchains require a Raspberry Pi ARM64 Linux sysroot. The sysroot must match the Raspberry Pi userspace you will run on, especially glibc and C++ runtime versions. Use a reproducible package repository, OS image, or the sysroot bundled with the installed cross toolchain; do not copy files from a live Raspberry Pi. + +The examples below use the sysroot bundled with the selected cross toolchain. If you use a distro-matched sysroot from another source, pass it with `-DOV_RPI_SYSROOT=`. + +### Linux host + +Install host tools: + +```bash +sudo apt-get update +sudo apt-get install -y git cmake ninja-build scons python3 python3-pip \ + gcc-aarch64-linux-gnu g++-aarch64-linux-gnu pkg-config xz-utils zstd +``` + +Configure and build: + +```bash +cmake -S . -B build-rpi-aarch64 -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_TOOLCHAIN_FILE=$PWD/cmake/arm64.toolchain.cmake \ + -DCMAKE_INSTALL_PREFIX=$PWD/install-rpi-aarch64 + +cmake --build build-rpi-aarch64 --parallel +cmake --install build-rpi-aarch64 +``` + +The Linux toolchain file uses the standard `aarch64-linux-gnu-*` tool names and is intentionally kept minimal. If your Linux cross toolchain needs an explicit target sysroot, pass standard CMake sysroot settings that match your distribution toolchain. + +### macOS host + +Install host tools and download the Linux AArch64 GNU cross toolchain: + +```bash +brew install cmake ninja scons python xz zstd +brew install messense/macos-cross-toolchains/aarch64-unknown-linux-gnu +``` + +Set the toolchain sysroot for CMake: + +```bash +export OV_RPI_SYSROOT="$(aarch64-linux-gnu-gcc -print-sysroot)" +export OV_RPI_TOOLCHAIN_PREFIX=aarch64-linux-gnu +``` + +Configure and build with the macOS-hosted Raspberry Pi ARM64 Linux toolchain: + +```bash +cmake -S . -B build-rpi-aarch64-macos -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_TOOLCHAIN_FILE=$PWD/cmake/rpi-aarch64-linux-from-macos.toolchain.cmake \ + -DOV_RPI_TOOLCHAIN_PREFIX=$OV_RPI_TOOLCHAIN_PREFIX \ + -DOV_RPI_SYSROOT=$OV_RPI_SYSROOT \ + -DCMAKE_INSTALL_PREFIX=$PWD/install-rpi-aarch64-macos + +cmake --build build-rpi-aarch64-macos --parallel +cmake --install build-rpi-aarch64-macos +``` + +The macOS toolchain file generates host wrapper scripts in the build directory and points CMake, binutils, and Arm Compute Library at the target sysroot. + +### Windows host + +Download and unpack CMake and Ninja from PowerShell: + +```powershell +$env:OV_RPI_TOOLS = "$env:OV_RPI_HOME\tools" +New-Item -ItemType Directory -Force -Path $env:OV_RPI_TOOLS + +$CMakeVersion = "3.30.5" +$CMakeUrl = "https://github.com/Kitware/CMake/releases/download/v$CMakeVersion/cmake-$CMakeVersion-windows-x86_64.zip" +$CMakeZip = "$env:OV_RPI_HOME\cmake-$CMakeVersion-windows-x86_64.zip" +Invoke-WebRequest -Uri $CMakeUrl -OutFile $CMakeZip +Expand-Archive -Path $CMakeZip -DestinationPath $env:OV_RPI_TOOLS -Force +Move-Item -Force "$env:OV_RPI_TOOLS\cmake-$CMakeVersion-windows-x86_64" "$env:OV_RPI_TOOLS\cmake" + +$NinjaVersion = "1.12.1" +$NinjaUrl = "https://github.com/ninja-build/ninja/releases/download/v$NinjaVersion/ninja-win.zip" +$NinjaZip = "$env:OV_RPI_HOME\ninja-win.zip" +New-Item -ItemType Directory -Force -Path "$env:OV_RPI_TOOLS\ninja" +Invoke-WebRequest -Uri $NinjaUrl -OutFile $NinjaZip +Expand-Archive -Path $NinjaZip -DestinationPath "$env:OV_RPI_TOOLS\ninja" -Force +``` + +Install Python and SCons: + +```powershell +$PythonVersion = "3.12" +winget install --id "Python.Python.$PythonVersion" -e +py "-$PythonVersion" -m pip install --user scons +$PythonScripts = py "-$PythonVersion" -c "import site; print(site.getuserbase() + r'\Scripts')" +``` + +Download and unpack the Arm GNU Toolchain for Windows targeting AArch64 GNU/Linux: + +```powershell +$ToolchainUrl = "https://developer.arm.com/-/media/Files/downloads/gnu/14.3.rel1/binrel/arm-gnu-toolchain-14.3.rel1-mingw-w64-i686-aarch64-none-linux-gnu.zip" +$ToolchainZip = "$env:OV_RPI_HOME\arm-gnu-toolchain-aarch64-none-linux-gnu.zip" +Invoke-WebRequest -Uri $ToolchainUrl -OutFile $ToolchainZip +Expand-Archive -Path $ToolchainZip -DestinationPath $env:OV_RPI_TOOLS -Force +$env:OV_RPI_TOOLCHAIN_ROOT = "$env:OV_RPI_TOOLS\arm-gnu-toolchain-14.3.rel1-mingw-w64-i686-aarch64-none-linux-gnu" +``` + +The Windows-hosted Arm package uses the `aarch64-none-linux-gnu` prefix and provides the target sysroot under `$env:OV_RPI_TOOLCHAIN_ROOT\aarch64-none-linux-gnu\libc`. + +Configure and build with the Windows-hosted Raspberry Pi ARM64 Linux toolchain: + +```powershell +$env:OV_RPI_REPO = (Get-Location).Path +$env:Path = "$env:OV_RPI_TOOLCHAIN_ROOT\bin;$env:OV_RPI_TOOLS\ninja;$env:OV_RPI_TOOLS\cmake\bin;$PythonScripts;$env:Path" +$env:OV_RPI_SYSROOT = "$env:OV_RPI_TOOLCHAIN_ROOT\aarch64-none-linux-gnu\libc" +$env:OV_RPI_TOOLCHAIN_PREFIX = "aarch64-none-linux-gnu" + +& "$env:OV_RPI_TOOLS\cmake\bin\cmake.exe" -S $env:OV_RPI_REPO -B "$env:OV_RPI_REPO\build-rpi-aarch64-windows" -G Ninja ` + "-DCMAKE_MAKE_PROGRAM=$env:OV_RPI_TOOLS\ninja\ninja.exe" ` + -DCMAKE_BUILD_TYPE=Release ` + "-DCMAKE_TOOLCHAIN_FILE=$env:OV_RPI_REPO\cmake\rpi-aarch64-linux-from-windows.toolchain.cmake" ` + "-DOV_RPI_TOOLCHAIN_PREFIX=$env:OV_RPI_TOOLCHAIN_PREFIX" ` + "-DOV_RPI_SYSROOT=$env:OV_RPI_SYSROOT" ` + "-DCMAKE_INSTALL_PREFIX=$env:OV_RPI_REPO\install-rpi-aarch64-windows" + +& "$env:OV_RPI_TOOLS\cmake\bin\cmake.exe" --build "$env:OV_RPI_REPO\build-rpi-aarch64-windows" --parallel +& "$env:OV_RPI_TOOLS\cmake\bin\cmake.exe" --install "$env:OV_RPI_REPO\build-rpi-aarch64-windows" +``` + +The Windows toolchain file generates host wrapper scripts, a local `grep.exe` required by OpenVINO's CMake probes, and Arm Compute Library SCons wrappers in the build directory. Do not mix the Arm GNU Toolchain C++ runtime headers with a different distro sysroot unless the libc and runtime versions are known to be compatible. + +### Build CPU benchmark runtime targets + +To build only the runtime pieces needed to run `benchmark_app` on the Raspberry Pi CPU, build the CPU plugin, the IR frontend, and `benchmark_app` explicitly: + +```bash +cmake --build build-rpi-aarch64 --target \ + openvino_intel_cpu_plugin openvino_ir_frontend benchmark_app \ + --parallel +``` + +For macOS and Windows builds, replace `build-rpi-aarch64` with `build-rpi-aarch64-macos` or `build-rpi-aarch64-windows`. + +### Verify and run on Raspberry Pi + +After any cross-build, verify that the produced binaries are Linux AArch64 ELF files: + +```bash +${OV_RPI_TOOLCHAIN_PREFIX:-aarch64-linux-gnu}-readelf -h \ + bin/aarch64/Release/benchmark_app +``` + +On a Windows host, use the Arm GNU Toolchain `readelf.exe`: + +```powershell +& "$env:OV_RPI_TOOLCHAIN_ROOT\bin\aarch64-none-linux-gnu-readelf.exe" -h ` + .\bin\aarch64\Release\benchmark_app +``` + +The output must include `Class: ELF64` and `Machine: AArch64`. + +For a quick CPU smoke test, copy the runtime output directory to the Raspberry Pi and run `benchmark_app` with the copied libraries on `LD_LIBRARY_PATH`: + +```bash +rsync -a bin/aarch64/Release/ rpi@raspberrypi:~/openvino-rpi-cross/bin/ + +ssh rpi@raspberrypi \ + 'cd ~/openvino-rpi-cross/bin && \ + LD_LIBRARY_PATH=$PWD ./benchmark_app \ + -m ~/models/model.xml \ + -d CPU \ + -api sync \ + -niter 10 \ + -nireq 1 \ + -nstreams 1 \ + -hint none' +``` + +Installing the build with `cmake --install` is the preferred way to preserve the expected runtime layout. + ## Additional Build Options - To build Python API, install `libpython3-dev:armhf` and `python3-pip` diff --git a/docs/dev/coding_style.md b/docs/dev/coding_style.md index fb202bda2eef..3325aa2c6ca7 100644 --- a/docs/dev/coding_style.md +++ b/docs/dev/coding_style.md @@ -1,22 +1,100 @@ # OpenVINO Coding Guidelines -## Coding style +## Fix order -The majority of OpenVINO components use `clang-format-18` for code style check. +When bringing a set of changes into compliance, address the checks in the following order: -The code style is based on the Google Code style with some differences. All the differences are described in the configuration file: -https://github.com/openvinotoolkit/openvino/blob/69f709028a5f8da596d1d0df9a0101e517c35708/src/.clang-format#L1-L28 +1. **Copyright headers** — fast, no build required +2. **clang-tidy** — requires a clean build; fix compilation errors first, then apply tidy auto-fixes +3. **Naming style (ncc)** — requires clang, but no full rebuild; fix after tidy to avoid redundant churn +4. **clang-format** — purely cosmetic; run last so tidy auto-fixes don't reintroduce formatting issues + +## Copyright headers + +All source files must carry the standard OpenVINO copyright header. + +C++ (`.cpp`, `.hpp`, `.h`): +```cpp +// Copyright (C) 2018- Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// +``` + +Python (`.py`): +```python +# Copyright (C) 2018- Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +``` + +To check and fix headers automatically, the script at `.github/scripts/check_copyright.py` can be used. If it exits non-zero it writes `copyright_fixes.diff`, which can be applied with `patch -p1 < copyright_fixes.diff`. + +Note: automatically generated patch can occasionally produce incorrect changes — in some cases, the manual fix is required. + +## Static analysis (clang-tidy) + +`clang-tidy-18` is used for static analysis and enforcing modern C++ patterns. + +**Prerequisites:** `clang-tidy-18` must be installed, and the project must be compiled with clang as the compiler. Before enabling it, verify that an existing build directory already uses clang: +```bash +grep "CMAKE_CXX_COMPILER" /CMakeCache.txt | grep -i clang +``` + +**If it does**, re-run cmake in it to append the style flags (preserves the existing configuration): +```bash +cmake -DENABLE_CLANG_FORMAT=ON -DENABLE_CLANG_TIDY=ON +``` -To fix the code style on your local machine, you need to install the `clang-format-18` tool and make sure that the CMake option `ENABLE_CLANG_FORMAT` is enabled. -If all dependencies are resolved, you can use the `clang_format_fix_all` target to fix all code style issues. +**If it doesn't** (or no build directory exists), set `=build-clang` and configure it: +```bash +cmake -B -DCMAKE_CXX_COMPILER=clang++-18 -DCMAKE_C_COMPILER=clang-18 \ + -DENABLE_CLANG_FORMAT=ON -DENABLE_CLANG_TIDY=ON \ + -DCMAKE_BUILD_TYPE=Release +``` -## Naming style +Build a target to run checks (warnings appear inline during compilation): +```bash +cmake --build --target -- -j$(nproc) +``` + +To auto-fix diagnostics, enable the fix mode and rebuild: +```bash +cmake -DENABLE_CLANG_TIDY_FIX=ON +cmake --build --target -- -j$(nproc) +``` + +Notes: +- Compilation errors must be resolved before clang-tidy diagnostics can be applied. +- Auto-fix can occasionally produce incorrect changes — in some cases, the manual fix is required. +- It is better to use a dedicated build directory for clang-tidy (e.g. `build-clang/`) — clang-tidy rewrites compilation databases and can interfere with incremental builds in a shared directory. +- Do not use `// NOLINT` suppressions to silence diagnostics. A suppression hides the issue without fixing it and can mask real problems in future code. It is strongly recommended to suppress if if the check is a confirmed false positive with no correct alternative, and document the reason inline. + +## Naming style (ncc) OpenVINO has strict rules for naming style in public API. All classes must start with a capital letter, and methods and functions should be named in `snake_case` style. To check the naming style, `ncc` tool is integrated in the OpenVINO. Read the detailed information about the naming style can be found in the [configuration file](../../cmake/developer_package/ncc_naming_style/openvino.style). To activate this tool, you need to have `clang` on the local machine and enable the CMake option `ENABLE_NCC_STYLE`. After that, you can use the `ncc_all` target to check the naming style. +## Coding style (clang-format) + +The majority of OpenVINO components use `clang-format-18` for code style checks. + +**Prerequisites:** `clang-format-18` must be installed. No specific compiler is required — clang-format runs as a standalone tool independent of how the project was compiled. + +The code style is based on the Google Code style with some differences. All the differences are described in the configuration file: +https://github.com/openvinotoolkit/openvino/blob/69f709028a5f8da596d1d0df9a0101e517c35708/src/.clang-format#L1-L28 + +To enable clang-format checks in the build, set the corresponding CMake flag: +```bash +cmake -DENABLE_CLANG_FORMAT=ON .. +``` + +Check and fix targets: +```bash +cmake --build --target clang_format_check_all # report violations +cmake --build --target clang_format_fix_all # auto-fix violations +``` + ## See also * [OpenVINO™ README](../../README.md) * [Developer documentation](../../docs/dev/index.md) diff --git a/docs/requirements.txt b/docs/requirements.txt index 147f7148b9f0..e1e5e64873a4 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -2,13 +2,13 @@ alabaster==1.0.0 atomicwrites==1.4.1 attrs==26.1.0 Babel==2.18.0 -beautifulsoup4==4.14.3 +beautifulsoup4==4.15.0 breathe==4.36.0 -certifi==2026.2.25 +certifi==2026.5.20 colorama==0.4.6 -Cython==3.2.4 +Cython==3.2.5 docutils==0.21.2 -idna==3.11 +idna==3.18 imagesize==2.0.0 importlib-metadata>=4.8.0,<10.0.0 iniconfig==2.1.0 @@ -17,23 +17,23 @@ Jinja2==3.1.6 jsonschema==4.26.0 lxml>=4.9.2 MarkupSafe==3.0.3 -mistune==3.2.0 +mistune==3.2.1 myst-parser==4.0.1 -packaging==26.0 +packaging==26.2 pluggy==1.6.0 pydata-sphinx-theme==0.14.4 Pygments==2.20.0 pyparsing==3.3.2 -pytest==9.0.3 +pytest==9.1.0 pytest-html==4.2.0 pytest-metadata==3.1.1 py>=1.11.0 pytz==2026.1.post1 pyyaml==6.0.3 -requests==2.33.1 +requests==2.34.2 six==1.17.0 -snowballstemmer==3.0.1 -soupsieve==2.8.3 +snowballstemmer==3.1.1 +soupsieve==2.8.4 sphinx==8.1.3 sphinx-copybutton==0.5.2 sphinx-design==0.6.1 @@ -46,5 +46,5 @@ sphinxcontrib-jsmath==1.0.1 sphinxcontrib-qthelp==2.0.0 sphinxcontrib-serializinghtml==2.0.0 toml==0.10.2 -urllib3==2.6.3 -zipp==3.23.1 +urllib3==2.7.0 +zipp==4.1.0 diff --git a/pyproject.toml b/pyproject.toml index a9b2c89d6109..379e281ea850 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,9 +23,9 @@ dependencies = [ [build-system] requires = [ - "setuptools>=77,<=82.0.0", - "wheel<=0.45.1", - "cmake<=4.3.0", + "setuptools>=77,<=82.0.1", + "wheel<=0.46.3", + "cmake<=4.3.2", "patchelf<=0.17.2.4; sys_platform == 'linux' and platform_machine == 'x86_64'" ] build-backend = "setuptools.build_meta" diff --git a/samples/cpp/benchmark_app/benchmark_app.hpp b/samples/cpp/benchmark_app/benchmark_app.hpp index e6f5d034a09d..11fba1f36df6 100644 --- a/samples/cpp/benchmark_app/benchmark_app.hpp +++ b/samples/cpp/benchmark_app/benchmark_app.hpp @@ -109,7 +109,7 @@ static const char infer_num_streams_message[] = "Optional. Number of streams to use for inference on the CPU or GPU devices " "(for HETERO and MULTI device cases use format :,: or just " "). " - "Default value is determined automatically for a device.Please note that although the " + "Default value is determined automatically for a device. Please note that although the " "automatic selection " "usually provides a reasonable performance, it still may be non - optimal for some cases, " "especially for " diff --git a/samples/js/node/package-lock.json b/samples/js/node/package-lock.json index 20fc9a1d6899..cc61f01047ef 100644 --- a/samples/js/node/package-lock.json +++ b/samples/js/node/package-lock.json @@ -82,9 +82,9 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.5.tgz", - "integrity": "sha512-eIJYKTCECbP/nsKaaruF6LW967mtbQbsw4JTtSVkUQc9MneSkbrgPJAbKl9nWr0ZeowV8BfsarBmPpBzGelA2w==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.6.0.tgz", + "integrity": "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -139,9 +139,9 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.1.tgz", - "integrity": "sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.2.tgz", + "integrity": "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -205,9 +205,9 @@ } }, "node_modules/@napi-rs/canvas": { - "version": "0.1.98", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.98.tgz", - "integrity": "sha512-WDg3lxYMqlrg49sDVUlrHVfIEPsd5AjYDRuGD6Fu82K5agJx0UnWA+l5qd53GNLRiMN2WhOw7FLR+Er5QB/0SA==", + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.100.tgz", + "integrity": "sha512-xglYA6q3XO5P3BNJYxVZ1IV7DLVjp1Py6nwag88YntrS+3vKHyYcMqXVS4ZztJmwz2uGvz1FWhI/4LgbR5uQDA==", "dev": true, "license": "MIT", "workspaces": [ @@ -221,23 +221,23 @@ "url": "https://github.com/sponsors/Brooooooklyn" }, "optionalDependencies": { - "@napi-rs/canvas-android-arm64": "0.1.98", - "@napi-rs/canvas-darwin-arm64": "0.1.98", - "@napi-rs/canvas-darwin-x64": "0.1.98", - "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.98", - "@napi-rs/canvas-linux-arm64-gnu": "0.1.98", - "@napi-rs/canvas-linux-arm64-musl": "0.1.98", - "@napi-rs/canvas-linux-riscv64-gnu": "0.1.98", - "@napi-rs/canvas-linux-x64-gnu": "0.1.98", - "@napi-rs/canvas-linux-x64-musl": "0.1.98", - "@napi-rs/canvas-win32-arm64-msvc": "0.1.98", - "@napi-rs/canvas-win32-x64-msvc": "0.1.98" + "@napi-rs/canvas-android-arm64": "0.1.100", + "@napi-rs/canvas-darwin-arm64": "0.1.100", + "@napi-rs/canvas-darwin-x64": "0.1.100", + "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.100", + "@napi-rs/canvas-linux-arm64-gnu": "0.1.100", + "@napi-rs/canvas-linux-arm64-musl": "0.1.100", + "@napi-rs/canvas-linux-riscv64-gnu": "0.1.100", + "@napi-rs/canvas-linux-x64-gnu": "0.1.100", + "@napi-rs/canvas-linux-x64-musl": "0.1.100", + "@napi-rs/canvas-win32-arm64-msvc": "0.1.100", + "@napi-rs/canvas-win32-x64-msvc": "0.1.100" } }, "node_modules/@napi-rs/canvas-android-arm64": { - "version": "0.1.98", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.98.tgz", - "integrity": "sha512-O45Ifr0WZJUrSyg0QgB+67TiC0zYBRkBK+d43ZV4JtlwH3XttiVxLvlxEeULiH5y1MSELruspF0bjF6xXwJNPQ==", + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.100.tgz", + "integrity": "sha512-hjhCKhntPv9+t4ckHymdx0phYNcVW+GKQR6Lzw2zE+pOVjOplSmtx9nNNknTjbEDLcuLZqA1y8ufKg1XfgftzQ==", "cpu": [ "arm64" ], @@ -256,9 +256,9 @@ } }, "node_modules/@napi-rs/canvas-darwin-arm64": { - "version": "0.1.98", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.98.tgz", - "integrity": "sha512-1b/nQhw6Isdv14JokUqat+i5wrAYD+ce3egiotedBGRUjVxYSj4s2uQCh2bFsyX5/9A5iTKVGsWoQhFft+j7Lg==", + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.100.tgz", + "integrity": "sha512-2PcswRaC7Ly645DGt88///zuFDhJxJYdKAs1uU3mfk1atYkXufgcgLfBpk6Tm12nCQBaNt1wpybuPZ4qOhTo8A==", "cpu": [ "arm64" ], @@ -277,9 +277,9 @@ } }, "node_modules/@napi-rs/canvas-darwin-x64": { - "version": "0.1.98", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.98.tgz", - "integrity": "sha512-oefzfBM8mwnyYp6S+yNXwjCoLdkOalFG24mssHgvrJDS0FulOryyI35Q7GdJGmrzuL4oo1XW3ZTOcTBLdJ8Zkg==", + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.100.tgz", + "integrity": "sha512-ePNZtj7pNIva/siZMg+HmbeozkIjqUIYdoymH8HaA3qK7LfzFN4WMBM8G6HQ9ZC+H3+Dnn5pqtiXpgLykaPOhw==", "cpu": [ "x64" ], @@ -298,9 +298,9 @@ } }, "node_modules/@napi-rs/canvas-linux-arm-gnueabihf": { - "version": "0.1.98", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.98.tgz", - "integrity": "sha512-NDH5QXGmf8wlo5yhijCNGVFiJk7an5GvHwb2LHyfLQWY/6/S48i5+YtY6FPqPVVCUckNGudYOfXEJnb3/FiJGQ==", + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.100.tgz", + "integrity": "sha512-d5cDB48oWFGU8/XPhUOFAlySgb/VAu7D+s8fi55K1Pcfg8aPplHWqMgibhVLU8ky7Pyg/fuiVLz4Nf3JrSTuUA==", "cpu": [ "arm" ], @@ -319,9 +319,9 @@ } }, "node_modules/@napi-rs/canvas-linux-arm64-gnu": { - "version": "0.1.98", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.98.tgz", - "integrity": "sha512-KBLLM6tu1xs80LSAqdSLBKkgct0S23MCEf/aq8yxzg5imAceqp1ulKeELgWaYm27MgpUhm3Q7jmegX12FfphwA==", + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.100.tgz", + "integrity": "sha512-rDxgxRu69RvDlX/bh9o22DxLsGr8EqsNgotL9+RwQE1S0b0cqeatqsw6aW45mukm0B42DIAaAacKaYQ8cqS1nw==", "cpu": [ "arm64" ], @@ -340,9 +340,9 @@ } }, "node_modules/@napi-rs/canvas-linux-arm64-musl": { - "version": "0.1.98", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.98.tgz", - "integrity": "sha512-mfMNhjN5zDcJafqQ6sHj4Tc3YMTRxP5UA3MHtp/ssytBR/k6XO0x+1IIPtscnUKwha+ql1++WjDCGEgqu8OfWQ==", + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.100.tgz", + "integrity": "sha512-K3mDW66N+xT2/V439u1alFANiBUjdEx2gLiNYnCmUsva5jZMxWTjafBYwTzYK+EMFMHrUoabuU+T1BIP5CgbYQ==", "cpu": [ "arm64" ], @@ -361,9 +361,9 @@ } }, "node_modules/@napi-rs/canvas-linux-riscv64-gnu": { - "version": "0.1.98", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.98.tgz", - "integrity": "sha512-nfW8esrcaeuhrO3qGA5cwuyk4Ak6cn2eB0LtEYtqROIl+fz06CNGNCU0M95+Tspw5ZgfSbc98SaigT5r5B3LVQ==", + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.100.tgz", + "integrity": "sha512-mooqUBTIsccZpnoQC4NgrC1v6C1vof39etLNMnBwCY+p0gajWJvAHLGQ6g/gGyS5YrpDW+GefSN4+Cvcr08UWw==", "cpu": [ "riscv64" ], @@ -382,9 +382,9 @@ } }, "node_modules/@napi-rs/canvas-linux-x64-gnu": { - "version": "0.1.98", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.98.tgz", - "integrity": "sha512-318UT8j6Gro2bTjtutjQXHWp9SLTNw+WRS4wQ6XIRPAyzBGnGHg7x2ndD+oqkPrrSRIbYLA5WoBcCasaF7lSTQ==", + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.100.tgz", + "integrity": "sha512-1eCvkDCazm7FFhsT7DfGOdSaHgZVK3bt/dSBl5EWHOWmnz+I7j8tPseJqqD81NF+MH21jKUK4wQSDjN0mdhnTg==", "cpu": [ "x64" ], @@ -403,9 +403,9 @@ } }, "node_modules/@napi-rs/canvas-linux-x64-musl": { - "version": "0.1.98", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.98.tgz", - "integrity": "sha512-0vZhI74UxnA4VqlW4UvM0dFRrjE1RLEe/OXSBjzytGIxV+yOG4exlrhGoIpAQaIpQQQXMCdb1EmbvPC1k9vEqQ==", + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.100.tgz", + "integrity": "sha512-20arT6lnI19S68qNlii73TSEDbECNgzMz2EpldC1V3mZFuRkeujXkcebRk0LRJe9SEUAooYiLokfMViY8IX7yA==", "cpu": [ "x64" ], @@ -424,9 +424,9 @@ } }, "node_modules/@napi-rs/canvas-win32-arm64-msvc": { - "version": "0.1.98", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-arm64-msvc/-/canvas-win32-arm64-msvc-0.1.98.tgz", - "integrity": "sha512-oiC/IxgFEEVcZ7VH7JXXlmgsqRvmFb57PIQ4gQck35IKFZCNUvdNCcN3OeoLP7Hpf5160MWJf9jj/+E5V0bSvw==", + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-arm64-msvc/-/canvas-win32-arm64-msvc-0.1.100.tgz", + "integrity": "sha512-DZFFT1wIAg37LJw37yhMRFfjATd3vTQzjZ1Yki8u2vhO6Hi5VE6BVaGQ1aaDu7xb4iMErz+9EOwjpS7xcxFeBw==", "cpu": [ "arm64" ], @@ -445,9 +445,9 @@ } }, "node_modules/@napi-rs/canvas-win32-x64-msvc": { - "version": "0.1.98", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.98.tgz", - "integrity": "sha512-ZqstKAJBSyZetU8udUvBQWPlGN9buawFvjuo9mgCAxzbOoJAgXX39ihec/nn42T5Vb6/qyn45eTimx5ND9kMEw==", + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.100.tgz", + "integrity": "sha512-MyT1j3mHC2+Lu4pBi9mKyMJhtP6U7k7EldY7sj/uS5gJA65gTXt8MefJQXLJo5d/vZbuWmfxzkEUNc/urV3pHA==", "cpu": [ "x64" ], @@ -745,9 +745,9 @@ } }, "node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "dev": true, "license": "MIT", "dependencies": { @@ -892,18 +892,18 @@ } }, "node_modules/eslint": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.2.0.tgz", - "integrity": "sha512-+L0vBFYGIpSNIt/KWTpFonPrqYvgKw1eUI5Vn7mEogrQcWtWYtNQ7dNqC+px/J0idT3BAkiWrhfS7k+Tum8TUA==", + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.4.1.tgz", + "integrity": "sha512-AyIKhnOBuOAdueD7RB3xB+YeAWScb9jHsJBgH2Hcde8InP5JYhqrRR6iTMHyTEwgENK54Cp44e4v8BwNhsuHuw==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", - "@eslint/config-array": "^0.23.4", - "@eslint/config-helpers": "^0.5.4", - "@eslint/core": "^1.2.0", - "@eslint/plugin-kit": "^0.7.0", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.6.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.2", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", @@ -1383,9 +1383,9 @@ } }, "node_modules/openvino-node": { - "version": "2026.1.0", - "resolved": "https://registry.npmjs.org/openvino-node/-/openvino-node-2026.1.0.tgz", - "integrity": "sha512-OPz2dLB/3FObO1FDPsF4x0GRCYLoCYtzbP/Y5++clYbR9W/3/XglP+9tRlpxYL3md53dZslrKjNgfJ3CU7DR2Q==", + "version": "2026.2.0", + "resolved": "https://registry.npmjs.org/openvino-node/-/openvino-node-2026.2.0.tgz", + "integrity": "sha512-hwQ200HMBthgtLp3GLazl1S0QSkrZbyvY3ttiMJDnA1av+rmFNnlzaz3fX7CwxJHbhGIfZ8wdp3LD+QHZlwJ+w==", "hasInstallScript": true, "license": "Apache-2.0", "os": [ diff --git a/src/bindings/c/docs/api_overview.md b/src/bindings/c/docs/api_overview.md index 4282a461ef29..833d6a317c86 100644 --- a/src/bindings/c/docs/api_overview.md +++ b/src/bindings/c/docs/api_overview.md @@ -500,7 +500,7 @@ This struct represents OpenVINO entity and allows you to manipulate with plugins - `device_name` - Name of a device to load a model to. - `property_args_size` - How many properties args will be passed, each property contains 2 args: key and value. - `compiled_model` - A pointer to the newly created compiled_model. - - `...` - property paramater, optional pack of pairs: relevant only for this load operation operation. + - `...` - property parameter, optional pack of pairs: relevant only for this load operation operation. - Return value: Status code of the operation: OK(0) for success. diff --git a/src/bindings/c/include/openvino/c/ov_core.h b/src/bindings/c/include/openvino/c/ov_core.h index 235b41de3c44..c207d372efab 100644 --- a/src/bindings/c/include/openvino/c/ov_core.h +++ b/src/bindings/c/include/openvino/c/ov_core.h @@ -203,7 +203,7 @@ ov_core_read_model_from_memory_buffer(const ov_core_t* core, * @param device_name Name of a device to load a model to. * @param property_args_size How many properties args will be passed, each property contains 2 args: key and value. * @param compiled_model A pointer to the newly created compiled_model. - * @param ... property paramater: Optional pack of pairs: relevant only + * @param ... property parameter: Optional pack of pairs: relevant only * for this load operation operation. Supported property key please see ov_property.h. * @return Status code of the operation: OK(0) for success. */ diff --git a/src/bindings/js/node/CMakeLists.txt b/src/bindings/js/node/CMakeLists.txt index e2de621b1fb7..cdc6d2999eb0 100644 --- a/src/bindings/js/node/CMakeLists.txt +++ b/src/bindings/js/node/CMakeLists.txt @@ -61,6 +61,7 @@ add_library(${PROJECT_NAME} SHARED ${CMAKE_CURRENT_SOURCE_DIR}/src/errors.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/helper.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/type_validation.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/tensor_impl.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/tensor.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/infer_request.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/compiled_model.cpp @@ -80,7 +81,7 @@ target_include_directories(${PROJECT_NAME} PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/.." ) -target_link_libraries(${PROJECT_NAME} PRIVATE openvino::core::dev openvino::runtime openvino::util ${DELAYIMP_LIB} ${CMAKE_JS_LIB}) +target_link_libraries(${PROJECT_NAME} PRIVATE openvino::core::dev openvino::runtime::dev openvino::runtime openvino::util ${DELAYIMP_LIB} ${CMAKE_JS_LIB}) if(MSVC AND CMAKE_JS_NODELIB_DEF AND CMAKE_JS_NODELIB_TARGET) # Generate node.lib diff --git a/src/bindings/js/node/include/tensor_impl.hpp b/src/bindings/js/node/include/tensor_impl.hpp new file mode 100644 index 000000000000..5aa2691a433d --- /dev/null +++ b/src/bindings/js/node/include/tensor_impl.hpp @@ -0,0 +1,80 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#pragma once + +#include + +#include + +#include "openvino/runtime/itensor.hpp" +#include "openvino/runtime/tensor.hpp" + +namespace ov { +namespace js { + +/** + * @brief An ov::ITensor decorator that pins a JS TypedArray's ArrayBuffer in V8 heap. + * + * Delegates all tensor operations to a wrapped ov::ITensor implementation. + * Holds a strong napi_ref so V8 cannot GC the ArrayBuffer while any C++ copy of + * this tensor is alive. + * + * The reference is scheduled for deletion on the JS thread via a TSFN finalizer, + * making destruction safe from OpenVINO thread-pool threads (e.g. async inference). + */ +class TensorImpl final : public ov::ITensor { +public: + struct CleanupContext { + explicit CleanupContext(Napi::Reference* typed_array_ref) : ref(typed_array_ref) {} + + void release_owner(); + void release_tsfn(); + void remove_cleanup_hook(); + static void cleanup_hook(napi_async_cleanup_hook_handle handle, void* data); + + std::atomic owners{2}; + std::atomic tsfn_released{false}; + std::atomic cleanup_handle_removed{false}; + napi_async_cleanup_hook_handle cleanup_handle{nullptr}; + Napi::ThreadSafeFunction tsfn; + Napi::Reference* ref; + }; + + /** + * @brief Construct a TensorImpl that zero-copies @p typed_array data into a tensor. + * + * Must be called on the JS thread. + * + * @param env Current JS environment. + * @param typed_array Source TypedArray whose ArrayBuffer supplies tensor memory. + * @param type OpenVINO element type. + * @param shape TensorImpl shape. + */ + TensorImpl(Napi::Env env, + const Napi::TypedArray& typed_array, + const ov::element::Type& type, + const ov::Shape& shape); + + ~TensorImpl() override; + + void set_shape(ov::Shape shape) override; + const ov::element::Type& get_element_type() const override; + const ov::Shape& get_shape() const override; + const ov::Strides& get_strides() const override; + void* data() override; + const void* data() const override; + void* data_rw() override; + void* data(const ov::element::Type& type) override; + const void* data(const ov::element::Type& type) const override; + void* data_rw(const ov::element::Type& type) override; + +private: + ov::Tensor _impl; + ov::Strides _strides; + CleanupContext* _cleanup_ctx{nullptr}; +}; + +} // namespace js +} // namespace ov diff --git a/src/bindings/js/node/package-lock.json b/src/bindings/js/node/package-lock.json index 21bfb8f476d3..fa285bb0e18b 100644 --- a/src/bindings/js/node/package-lock.json +++ b/src/bindings/js/node/package-lock.json @@ -1,12 +1,12 @@ { "name": "openvino-node", - "version": "2026.1.0", + "version": "2026.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "openvino-node", - "version": "2026.1.0", + "version": "2026.2.0", "hasInstallScript": true, "license": "Apache-2.0", "os": [ @@ -246,13 +246,13 @@ } }, "node_modules/@pkgr/core": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", - "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.3.6.tgz", + "integrity": "sha512-SEeaJLb3qBNF/OaXnaR1NmmBbFYk1zC0ZH/52fATcRPLFg/p791YrcyFFy44Bo9sLaGuSuLp5Q6axbb/O+v/RA==", "dev": true, "license": "MIT", "engines": { - "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + "node": "^14.18.0 || >=16.0.0" }, "funding": { "url": "https://opencollective.com/pkgr" @@ -273,9 +273,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.19.17", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.17.tgz", - "integrity": "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==", + "version": "22.19.20", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.20.tgz", + "integrity": "sha512-6tELRwSDYWW9EdZhbeZmYGZ1/7Djkt+Ah3/ScEYT9cDord7UJzasR/4D3VONg9tQI5CDp+/CZC1AXj2pCFOvpw==", "dev": true, "license": "MIT", "dependencies": { @@ -283,17 +283,17 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.58.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.58.2.tgz", - "integrity": "sha512-aC2qc5thQahutKjP+cl8cgN9DWe3ZUqVko30CMSZHnFEHyhOYoZSzkGtAI2mcwZ38xeImDucI4dnqsHiOYuuCw==", + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.61.0.tgz", + "integrity": "sha512-bFNvl9ZczlVb+wR2Akszf3gHfKVj/8WanXaGJ3UstTA7brNKg0cNdk6X1Psu5V7MZ2oQtzZKOEzIUehaoxbDGw==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.58.2", - "@typescript-eslint/type-utils": "8.58.2", - "@typescript-eslint/utils": "8.58.2", - "@typescript-eslint/visitor-keys": "8.58.2", + "@typescript-eslint/scope-manager": "8.61.0", + "@typescript-eslint/type-utils": "8.61.0", + "@typescript-eslint/utils": "8.61.0", + "@typescript-eslint/visitor-keys": "8.61.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" @@ -306,7 +306,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.58.2", + "@typescript-eslint/parser": "^8.61.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } @@ -322,16 +322,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.58.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.58.2.tgz", - "integrity": "sha512-/Zb/xaIDfxeJnvishjGdcR4jmr7S+bda8PKNhRGdljDM+elXhlvN0FyPSsMnLmJUrVG9aPO6dof80wjMawsASg==", + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.61.0.tgz", + "integrity": "sha512-5B7PfA2e1NQGCnDHd/0lW7W3gvp3d59Ryw54FYO8Uswxo9f6ikw3AZV+Xj/TvpImmpsiYyUqAfhC6kJID1jF6w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.58.2", - "@typescript-eslint/types": "8.58.2", - "@typescript-eslint/typescript-estree": "8.58.2", - "@typescript-eslint/visitor-keys": "8.58.2", + "@typescript-eslint/scope-manager": "8.61.0", + "@typescript-eslint/types": "8.61.0", + "@typescript-eslint/typescript-estree": "8.61.0", + "@typescript-eslint/visitor-keys": "8.61.0", "debug": "^4.4.3" }, "engines": { @@ -347,14 +347,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.58.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.58.2.tgz", - "integrity": "sha512-Cq6UfpZZk15+r87BkIh5rDpi38W4b+Sjnb8wQCPPDDweS/LRCFjCyViEbzHk5Ck3f2QDfgmlxqSa7S7clDtlfg==", + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.61.0.tgz", + "integrity": "sha512-DV42F7MLJO6Rax7SK1yg43tcnEfGUrurSpSxKuVX+a3RCTzBlH3fuxprrOJXKCJGAaw82xXocikJ0uQaqwXgGA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.58.2", - "@typescript-eslint/types": "^8.58.2", + "@typescript-eslint/tsconfig-utils": "^8.61.0", + "@typescript-eslint/types": "^8.61.0", "debug": "^4.4.3" }, "engines": { @@ -369,14 +369,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.58.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.58.2.tgz", - "integrity": "sha512-SgmyvDPexWETQek+qzZnrG6844IaO02UVyOLhI4wpo82dpZJY9+6YZCKAMFzXb7qhx37mFK1QcPQ18tud+vo6Q==", + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.61.0.tgz", + "integrity": "sha512-IWdXFHFSb6mlC3HPc7QsLDm5zYEbUla6trDEHf32D3/dnuUyXd87plScSNXSbm0/RxMvObpI17sv/EDTGrGZkA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.58.2", - "@typescript-eslint/visitor-keys": "8.58.2" + "@typescript-eslint/types": "8.61.0", + "@typescript-eslint/visitor-keys": "8.61.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -387,9 +387,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.58.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.58.2.tgz", - "integrity": "sha512-3SR+RukipDvkkKp/d0jP0dyzuls3DbGmwDpVEc5wqk5f38KFThakqAAO0XMirWAE+kT00oTauTbzMFGPoAzB0A==", + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.61.0.tgz", + "integrity": "sha512-O5Amvdv9ztMpxpf+vmFULGG78IE6Qwdr3bCGvqwG4nwc9H2qXkOYJJnRbRHyMkQTjv1d03olqwwwzHLMqpFePQ==", "dev": true, "license": "MIT", "engines": { @@ -404,15 +404,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.58.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.58.2.tgz", - "integrity": "sha512-Z7EloNR/B389FvabdGeTo2XMs4W9TjtPiO9DAsmT0yom0bwlPyRjkJ1uCdW1DvrrrYP50AJZ9Xc3sByZA9+dcg==", + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.61.0.tgz", + "integrity": "sha512-TuBiQYIkd97yBfInHCTKVYMbX4kvEmpOEuixIuzCU9p8BGT1SfyyO0d0IfDMbPIHcjn/hWnusUX5e8v5Xg+X8A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.58.2", - "@typescript-eslint/typescript-estree": "8.58.2", - "@typescript-eslint/utils": "8.58.2", + "@typescript-eslint/types": "8.61.0", + "@typescript-eslint/typescript-estree": "8.61.0", + "@typescript-eslint/utils": "8.61.0", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, @@ -429,9 +429,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.58.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.58.2.tgz", - "integrity": "sha512-9TukXyATBQf/Jq9AMQXfvurk+G5R2MwfqQGDR2GzGz28HvY/lXNKGhkY+6IOubwcquikWk5cjlgPvD2uAA7htQ==", + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.61.0.tgz", + "integrity": "sha512-9QTQpZ5Iin4CdIodfbDQFSeiSJKidgYJYug1P9CC2xWgUTvlmixViqDZNciMjwLBZyJnG4tGmPl97rVAFb1AJg==", "dev": true, "license": "MIT", "engines": { @@ -443,16 +443,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.58.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.58.2.tgz", - "integrity": "sha512-ELGuoofuhhoCvNbQjFFiobFcGgcDCEm0ThWdmO4Z0UzLqPXS3KFvnEZ+SHewwOYHjM09tkzOWXNTv9u6Gqtyuw==", + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.61.0.tgz", + "integrity": "sha512-42zatd5qSvvcV1JdDBCLxYRznvP4eIHpPoZXdkPFnAmanA4FuZ5dibSnCBggY8hQnqajPpoGjXFdZ7fIJKQnlA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.58.2", - "@typescript-eslint/tsconfig-utils": "8.58.2", - "@typescript-eslint/types": "8.58.2", - "@typescript-eslint/visitor-keys": "8.58.2", + "@typescript-eslint/project-service": "8.61.0", + "@typescript-eslint/tsconfig-utils": "8.61.0", + "@typescript-eslint/types": "8.61.0", + "@typescript-eslint/visitor-keys": "8.61.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", @@ -481,9 +481,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "dev": true, "license": "MIT", "dependencies": { @@ -510,16 +510,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.58.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.58.2.tgz", - "integrity": "sha512-QZfjHNEzPY8+l0+fIXMvuQ2sJlplB4zgDZvA+NmvZsZv3EQwOcc1DuIU1VJUTWZ/RKouBMhDyNaBMx4sWvrzRA==", + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.61.0.tgz", + "integrity": "sha512-3bzFt7ImFMW/jVYwJamDoe/dMOdFLSC6pom6rRjdh4SZJEYupyMzem8e7vKZLclLfpHjlwSAXOUxtKxGXUiLqA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.58.2", - "@typescript-eslint/types": "8.58.2", - "@typescript-eslint/typescript-estree": "8.58.2" + "@typescript-eslint/scope-manager": "8.61.0", + "@typescript-eslint/types": "8.61.0", + "@typescript-eslint/typescript-estree": "8.61.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -534,13 +534,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.58.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.58.2.tgz", - "integrity": "sha512-f1WO2Lx8a9t8DARmcWAUPJbu0G20bJlj8L4z72K00TMeJAoyLr/tHhI/pzYBLrR4dXWkcxO1cWYZEOX8DKHTqA==", + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.61.0.tgz", + "integrity": "sha512-QVLZu3ZPQEE+HICQyAMZ2yLQhxf0meY/wx6Hx14YcTNj13JB3qHlX3lJ02L3fLGHgERRH71kvYDwiXIguT3AjQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.58.2", + "@typescript-eslint/types": "8.61.0", "eslint-visitor-keys": "^5.0.0" }, "engines": { @@ -1132,14 +1132,14 @@ } }, "node_modules/eslint-plugin-prettier": { - "version": "5.5.5", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.5.tgz", - "integrity": "sha512-hscXkbqUZ2sPithAuLm5MXL+Wph+U7wHngPBv9OMWwlP8iaflyxpjTYZkmdgB4/vPIhemRlBEoLrH7UC1n7aUw==", + "version": "5.5.6", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.6.tgz", + "integrity": "sha512-ifetmTcxWfz+4qRW3pH/ujdTq2jQIj59AxJMIN26K5avYgU8dxycUETQonWiW+wPrYXA0j3Try0l1CnwVQtDqQ==", "dev": true, "license": "MIT", "dependencies": { "prettier-linter-helpers": "^1.0.1", - "synckit": "^0.11.12" + "synckit": "^0.11.13" }, "engines": { "node": "^14.18.0 || >=16.0.0" @@ -1430,9 +1430,9 @@ } }, "node_modules/glob/node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "dev": true, "license": "MIT", "dependencies": { @@ -1659,10 +1659,20 @@ "license": "ISC" }, "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", - "dev": true, + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", + "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -1810,9 +1820,9 @@ } }, "node_modules/mocha/node_modules/brace-expansion": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", - "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", "dev": true, "license": "MIT", "dependencies": { @@ -2165,9 +2175,9 @@ "license": "MIT" }, "node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.3.tgz", + "integrity": "sha512-wnilbGyMxzbY7dNOl7jpKbLSjcfeweJWU5j4+u5qW+6/wuGD9KzIGOyZnQVSBM9E7DtWaaH3CyHkppYrKYoxwg==", "dev": true, "license": "ISC", "bin": { @@ -2263,13 +2273,13 @@ } }, "node_modules/synckit": { - "version": "0.11.12", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.12.tgz", - "integrity": "sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==", + "version": "0.11.13", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.13.tgz", + "integrity": "sha512-eNRKgb3z66Yp3D2CixVujOUvXLFUTij/zVnV8KRyvFdQwpz7I5DS8UfRkTeLzb64u+dkzDSdelE24izu+zSSUg==", "dev": true, "license": "MIT", "dependencies": { - "@pkgr/core": "^0.2.9" + "@pkgr/core": "^0.3.6" }, "engines": { "node": "^14.18.0 || >=16.0.0" @@ -2343,9 +2353,9 @@ } }, "node_modules/tinyglobby": { - "version": "0.2.16", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", - "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", "dev": true, "license": "MIT", "dependencies": { @@ -2400,16 +2410,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.58.2", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.58.2.tgz", - "integrity": "sha512-V8iSng9mRbdZjl54VJ9NKr6ZB+dW0J3TzRXRGcSbLIej9jV86ZRtlYeTKDR/QLxXykocJ5icNzbsl2+5TzIvcQ==", + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.61.0.tgz", + "integrity": "sha512-8y31Rd0eGTrDKqhy6vT0HtzhN+YLjQizwX3aA3hPXP/ynSfnrBXcQY5IzsP9/DM7+klX4IUncZZjkchP0z+rUw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.58.2", - "@typescript-eslint/parser": "8.58.2", - "@typescript-eslint/typescript-estree": "8.58.2", - "@typescript-eslint/utils": "8.58.2" + "@typescript-eslint/eslint-plugin": "8.61.0", + "@typescript-eslint/parser": "8.61.0", + "@typescript-eslint/typescript-estree": "8.61.0", + "@typescript-eslint/utils": "8.61.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" diff --git a/src/bindings/js/node/package.json b/src/bindings/js/node/package.json index 6a9c22d02119..ca309580fd4f 100644 --- a/src/bindings/js/node/package.json +++ b/src/bindings/js/node/package.json @@ -1,6 +1,6 @@ { "name": "openvino-node", - "version": "2026.1.0", + "version": "2026.2.0", "description": "OpenVINO™ utils for using from Node.js environment", "repository": { "url": "git+https://github.com/openvinotoolkit/openvino.git", @@ -20,7 +20,7 @@ "lint": "eslint .", "format": "prettier --ignore-path .gitignore --write .", "test_setup": "node ./tests/setup.js", - "test": "npm run test_setup && node --test ./tests/unit/*.test.js", + "test": "npm run test_setup && node --expose-gc --test ./tests/unit/*.test.js", "test:e2e": "mocha ./tests/e2e/electron-app.test.js", "tsc": "tsc", "postinstall": "npm run install_runtime", diff --git a/src/bindings/js/node/src/helper.cpp b/src/bindings/js/node/src/helper.cpp index b0e2eda06316..deb4b510f2b5 100644 --- a/src/bindings/js/node/src/helper.cpp +++ b/src/bindings/js/node/src/helper.cpp @@ -7,7 +7,9 @@ #include "node/include/compiled_model.hpp" #include "node/include/node_wrap.hpp" #include "node/include/tensor.hpp" +#include "node/include/tensor_impl.hpp" #include "node/include/type_validation.hpp" +#include "openvino/runtime/make_tensor.hpp" const std::vector& get_supported_types() { static const std::vector supported_element_types = @@ -369,17 +371,10 @@ ov::Tensor cast_to_tensor(const Napi::CallbackInfo& info, int index) { ov::Tensor cast_to_tensor(const Napi::TypedArray& typed_array, const ov::Shape& shape, const ov::element::Type_t& type) { - /* The difference between TypedArray::ArrayBuffer::Data() and e.g. Float32Array::Data() is byteOffset - because the TypedArray may have a non-zero `ByteOffset()` into the `ArrayBuffer`. */ - if (typed_array.ByteOffset() != 0) { - OPENVINO_THROW("TypedArray.byteOffset has to be equal to zero."); - } - auto array_buffer = typed_array.ArrayBuffer(); - auto tensor = ov::Tensor(type, shape, array_buffer.Data()); - if (tensor.get_byte_size() != array_buffer.ByteLength()) { - OPENVINO_THROW("Memory allocated using shape and element::type mismatch passed data's size"); - } - return tensor; + OPENVINO_ASSERT(typed_array.ByteOffset() == 0, + "TypedArray.byteOffset must be zero for zero-copy tensor construction."); + auto impl = std::make_shared(typed_array.Env(), typed_array, type, shape); + return ov::make_tensor(ov::SoPtr{impl, nullptr}); } void fill_tensor_from_strings(ov::Tensor& tensor, const Napi::Array& arr) { diff --git a/src/bindings/js/node/src/tensor_impl.cpp b/src/bindings/js/node/src/tensor_impl.cpp new file mode 100644 index 000000000000..df8ded01ee9c --- /dev/null +++ b/src/bindings/js/node/src/tensor_impl.cpp @@ -0,0 +1,140 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "node/include/tensor_impl.hpp" + +#include "openvino/core/except.hpp" +#include "openvino/runtime/tensor.hpp" + +namespace ov { +namespace js { + +// Drops one native owner of the shared cleanup state and deletes it on the last release. +void TensorImpl::CleanupContext::release_owner() { + if (owners.fetch_sub(1) == 1) { + delete this; + } +} + +// Releases the TSFN exactly once because both destructor and env cleanup may race here. +void TensorImpl::CleanupContext::release_tsfn() { + if (!tsfn_released.exchange(true)) { + const auto status = tsfn.Release(); + OPENVINO_ASSERT(status == napi_ok || status == napi_closing, + "TensorImpl: failed to release ThreadSafeFunction."); + } +} + +// Unregisters the async cleanup hook exactly once to avoid double removal during shutdown. +void TensorImpl::CleanupContext::remove_cleanup_hook() { + if (cleanup_handle != nullptr && !cleanup_handle_removed.exchange(true)) { + const auto status = napi_remove_async_cleanup_hook(cleanup_handle); + OPENVINO_ASSERT(status == napi_ok, "TensorImpl: failed to remove async cleanup hook."); + } +} + +// Handles Node environment teardown by forcing TSFN shutdown through the cleanup hook path. +void TensorImpl::CleanupContext::cleanup_hook(napi_async_cleanup_hook_handle handle, void* data) { + auto* cleanup_ctx = static_cast(data); + cleanup_ctx->cleanup_handle = handle; + cleanup_ctx->release_tsfn(); + cleanup_ctx->remove_cleanup_hook(); +} + +TensorImpl::TensorImpl(Napi::Env env, + const Napi::TypedArray& typed_array, + const ov::element::Type& type, + const ov::Shape& shape) { + _impl = ov::Tensor(type, shape, typed_array.ArrayBuffer().Data()); + _strides = _impl.get_strides(); + OPENVINO_ASSERT(_impl.get_byte_size() == typed_array.ByteLength(), + "Memory allocated using shape and element::type mismatch TypedArray byte length."); + + // TSFN keep a strong reference to the TypedArray, preventing GC of its ArrayBuffer while this TensorImpl is alive. + // - ~TensorImpl() calls Release() from any thread (thread-safe) and releases the strong reference. + auto* ref = new Napi::Reference(Napi::Persistent(typed_array)); + _cleanup_ctx = new CleanupContext(ref); + auto tsfn = + Napi::ThreadSafeFunction::New(env, + Napi::Function{}, // no JS callback needed + "ovTensorCleanup", + 0, // unlimited queue + 1, // initial_thread_count + _cleanup_ctx, // context (passed to finalizer) + [](Napi::Env, CleanupContext* cleanup_ctx) { + delete cleanup_ctx->ref; // runs on JS thread: releases strong reference + cleanup_ctx->release_owner(); + }); + OPENVINO_ASSERT(tsfn, "TensorImpl: failed to create ThreadSafeFunction for cleanup."); + + // Creates a zero-copy tensor over TypedArray storage and pins the JS object until native teardown completes. + _cleanup_ctx->tsfn = tsfn; + const auto status = + napi_add_async_cleanup_hook(env, CleanupContext::cleanup_hook, _cleanup_ctx, &_cleanup_ctx->cleanup_handle); + OPENVINO_ASSERT(status == napi_ok, "TensorImpl: failed to register async cleanup hook."); + + // Unref so the TSFN does not prevent the event loop from exiting. + tsfn.Unref(env); +} + +// Tears down the shutdown coordination state from any thread-safe destruction path. +TensorImpl::~TensorImpl() { + _cleanup_ctx->remove_cleanup_hook(); + _cleanup_ctx->release_tsfn(); + _cleanup_ctx->release_owner(); +} + +// Reshapes the wrapped tensor and refreshes cached strides that are exposed by reference. +void TensorImpl::set_shape(ov::Shape shape) { + _impl.set_shape(std::move(shape)); + _strides = _impl.get_strides(); +} + +// Forwards element type queries to the wrapped tensor. +const ov::element::Type& TensorImpl::get_element_type() const { + return _impl.get_element_type(); +} + +// Forwards shape queries to the wrapped tensor. +const ov::Shape& TensorImpl::get_shape() const { + return _impl.get_shape(); +} + +// Returns cached strides because ov::Tensor::get_strides() produces a temporary value. +const ov::Strides& TensorImpl::get_strides() const { + return _strides; +} + +// Returns mutable raw data from the wrapped tensor. +void* TensorImpl::data() { + return _impl.data(); +} + +// Returns const raw data from the wrapped tensor. +const void* TensorImpl::data() const { + return std::as_const(_impl).data(); +} + +// Returns writable raw data from the wrapped tensor. +void* TensorImpl::data_rw() { + return _impl.data(); +} + +// Returns mutable typed data after validating the requested element type. +void* TensorImpl::data(const ov::element::Type& type) { + return _impl.data(type); +} + +// Returns const typed data after validating the requested element type. +const void* TensorImpl::data(const ov::element::Type& type) const { + return std::as_const(_impl).data(type); +} + +// Returns writable typed data after validating the requested element type. +void* TensorImpl::data_rw(const ov::element::Type& type) { + return _impl.data(type); +} + +} // namespace js +} // namespace ov diff --git a/src/bindings/js/node/tests/unit/infer_request.test.js b/src/bindings/js/node/tests/unit/infer_request.test.js index eac05c3b522c..7b8f43ffa1bb 100644 --- a/src/bindings/js/node/tests/unit/infer_request.test.js +++ b/src/bindings/js/node/tests/unit/infer_request.test.js @@ -68,7 +68,10 @@ describe("ov.InferRequest tests", () => { const inputMessagePairs = [ ["string", "Cannot create a tensor from the passed Napi::Value."], [tensorData.slice(-10), /Memory allocated using shape and element::type mismatch/], - [new Float32Array(buffer, 4), "TypedArray.byteOffset has to be equal to zero."], + [ + new Float32Array(buffer, 4), + /TypedArray.byteOffset must be zero for zero-copy tensor construction./, + ], [{}, /Invalid argument/], // Test for object that is not Tensor ]; @@ -403,3 +406,77 @@ describe("ov.InferRequest tests with missing outputs names", () => { assert.deepStrictEqual(Object.keys(result).length, 1); }); }); + +describe("GC safety infer() / inferAsync()", () => { + const { reluLargeModel } = testModels; + const iterationCount = 5; + const elementCount = lengthFromShape(reluLargeModel.inputShape); + let compiledModel = null; + let inferRequest = null; + + before(() => { + const core = new ov.Core(); + const model = core.readModelSync(reluLargeModel.xml); + compiledModel = core.compileModelSync(model, "CPU"); + inferRequest = compiledModel.createInferRequest(); + }); + + it("original TypedArray is alive during infer()", async () => { + function testInfer() { + const inputData = new Float32Array(elementCount).fill(128.0); + const tensor = new ov.Tensor(ov.element.f32, reluLargeModel.inputShape, inputData); + return inferRequest.infer({ [reluLargeModel.inputName]: tensor }); + } + + const results = []; + for (let i = 0; i < iterationCount; i++) { + results.push(testInfer()); + } + for (const result of results) { + const data = result["relu_out"].getData(); + assert.strictEqual(data[0], 128.0, "First element should be 128"); + assert.strictEqual(data[elementCount - 1], 128.0, "Last element should be 128"); + } + }); + + it("original TypedArray is alive during inferAsync()", async () => { + async function testInferAsync() { + const inputData = new Float32Array(elementCount).fill(128.0); + const tensor = new ov.Tensor(ov.element.f32, reluLargeModel.inputShape, inputData); + return inferRequest.inferAsync({ [reluLargeModel.inputName]: tensor }); + } + + const promises = []; + for (let i = 0; i < iterationCount; i++) { + promises.push(testInferAsync()); + } + if (typeof global.gc === "function") global.gc(); + const results = await Promise.all(promises); + for (const result of results) { + const data = result["relu_out"].getData(); + assert.strictEqual(data[0], 128.0, "First element should be 128"); + assert.strictEqual(data[elementCount - 1], 128.0, "Last element should be 128"); + } + }); + + it("Full-cycle tensor converting test", () => { + function fillInferRequest(inferRequest) { + // 1. TypedArray → TensorWrap + const buf = new Float32Array(elementCount).fill(128.0); + const tensor = new ov.Tensor(ov.element.f32, reluLargeModel.inputShape, buf); + // 2. TensorWrap → ov::Tensor + inferRequest.setInputTensor(tensor); + } + + fillInferRequest(inferRequest); + + // Intermediate execution, where GC collects buffer + if (typeof global.gc === "function") global.gc(); + + // 3. the same ov::Tensor → new TensorWrap + const sameTensor = inferRequest.getInputTensor(); + + // 4. read data from buffer, but it is not available or rewrote + assert.deepStrictEqual(sameTensor.getData(), new Float32Array(elementCount).fill(128.0)); + }); +}); diff --git a/src/bindings/js/node/tests/unit/tensor.test.js b/src/bindings/js/node/tests/unit/tensor.test.js index 95f99c0a0279..6aa49f8f8d98 100644 --- a/src/bindings/js/node/tests/unit/tensor.test.js +++ b/src/bindings/js/node/tests/unit/tensor.test.js @@ -43,6 +43,23 @@ describe("ov.Tensor tests", () => { assert.strictEqual(tensor.data.length, elemNum); }); + test("Memory safe after the original TypedArray goes out of scope", () => { + const tensors = []; + const iterationCount = 5; + for (let i = 0; i < iterationCount; i++) { + const shape = [1, 3, 1024, 1024]; + const itemCount = lengthFromShape(shape); + const img = new Float32Array(itemCount).fill(128.0); + tensors.push(new ov.Tensor(ov.element.f32, shape, img)); + } + if (typeof global.gc === "function") global.gc(); + + for (const tensor of tensors) { + const view = tensor.getData(); + assert.strictEqual(view[0], 128.0); + } + }); + describe("Tensor data", () => { it("set tensor data with element type", () => { params.forEach(([type, , data]) => { diff --git a/src/bindings/js/node/tests/unit/test_models/relu_large_model.xml b/src/bindings/js/node/tests/unit/test_models/relu_large_model.xml new file mode 100644 index 000000000000..7067cb6b206d --- /dev/null +++ b/src/bindings/js/node/tests/unit/test_models/relu_large_model.xml @@ -0,0 +1,49 @@ + + + + + + + + 1 + 3 + 1024 + 1024 + + + + + + + 1 + 3 + 1024 + 1024 + + + + + 1 + 3 + 1024 + 1024 + + + + + + + 1 + 3 + 1024 + 1024 + + + + + + + + + + diff --git a/src/bindings/js/node/tests/utils.js b/src/bindings/js/node/tests/utils.js index 47459fb634da..836818a80444 100644 --- a/src/bindings/js/node/tests/utils.js +++ b/src/bindings/js/node/tests/utils.js @@ -42,6 +42,13 @@ const testModels = { reluModel: { xml: getModelPath("relu_model.xml"), }, + reluLargeModel: { + xml: getModelPath("relu_large_model.xml"), + inputShape: [1, 3, 1024, 1024], + outputShape: [1, 3, 1024, 1024], + inputName: "data", + outputName: "relu_out", + }, }; module.exports = { diff --git a/src/bindings/python/constraints.txt b/src/bindings/python/constraints.txt index a988d1a175d5..af377f334d30 100644 --- a/src/bindings/python/constraints.txt +++ b/src/bindings/python/constraints.txt @@ -8,7 +8,7 @@ pytest-html==4.2.0 pytest-timeout==2.4.0 # Python bindings -build<1.5 +build<1.6 pygments>=2.8.1 setuptools>=77,<80.10 sympy>=1.10 diff --git a/src/bindings/python/requirements_test.txt b/src/bindings/python/requirements_test.txt index c8d86c84fc76..a0e65f4e08e5 100644 --- a/src/bindings/python/requirements_test.txt +++ b/src/bindings/python/requirements_test.txt @@ -38,4 +38,4 @@ tox types-setuptools wheel pybind11-stubgen<2.6 -pyright<=1.1.408 +pyright<=1.1.410 diff --git a/src/bindings/python/src/openvino/_pyopenvino/op/__init__.pyi b/src/bindings/python/src/openvino/_pyopenvino/op/__init__.pyi index 8180bb22d31b..78d02435882c 100644 --- a/src/bindings/python/src/openvino/_pyopenvino/op/__init__.pyi +++ b/src/bindings/python/src/openvino/_pyopenvino/op/__init__.pyi @@ -177,12 +177,20 @@ class Result(openvino._pyopenvino.Node): ... def set_layout(self, layout: openvino._pyopenvino.Layout) -> None: ... +class _GroupQueryAttentionExtension(openvino._pyopenvino.Extension): + """ + Extension that registers GroupQueryAttention for IR deserialization. Pass an instance to core.add_extension() before reading an IR that contains this op. + """ + def __init__(self) -> None: + ... class _PagedAttentionExtension(openvino._pyopenvino.Node): """ Experimental extention for PagedAttention operation. Use with care: no backward compatibility is guaranteed in future releases. """ def __init__(self, arg0: collections.abc.Sequence[openvino._pyopenvino.Output]) -> None: ... + def get_write_kv_cache(self) -> bool: + ... class assign(openvino._pyopenvino.Node): """ openvino.op.assign wraps ov::op::v6::Assign diff --git a/src/bindings/python/src/openvino/_pyopenvino/properties/__init__.pyi b/src/bindings/python/src/openvino/_pyopenvino/properties/__init__.pyi index 6ca25ca02a85..08cfc40fd3a5 100644 --- a/src/bindings/python/src/openvino/_pyopenvino/properties/__init__.pyi +++ b/src/bindings/python/src/openvino/_pyopenvino/properties/__init__.pyi @@ -13,7 +13,7 @@ import typing """ openvino.properties submodule """ -__all__: list[str] = ['CacheMode', 'WorkloadType', 'auto_batch_timeout', 'available_devices', 'cache_dir', 'cache_encryption_callbacks', 'cache_mode', 'compilation_num_threads', 'device', 'enable_mmap', 'enable_profiling', 'enable_weightless', 'execution_devices', 'force_tbb_terminate', 'hint', 'inference_num_threads', 'intel_auto', 'intel_cpu', 'intel_gpu', 'intel_npu', 'key_cache_group_size', 'key_cache_precision', 'loaded_from_cache', 'log', 'max_batch_size', 'model_name', 'num_streams', 'optimal_batch_size', 'optimal_number_of_infer_requests', 'range_for_async_infer_requests', 'range_for_streams', 'streams', 'supported_properties', 'value_cache_group_size', 'value_cache_precision', 'weights_path', 'workload_type'] +__all__: list[str] = ['CacheMode', 'CompatibilityCheck', 'WorkloadType', 'auto_batch_timeout', 'available_devices', 'cache_dir', 'cache_encryption_callbacks', 'cache_mode', 'compatibility_check', 'compilation_num_threads', 'device', 'enable_mmap', 'enable_profiling', 'enable_weightless', 'execution_devices', 'force_tbb_terminate', 'hint', 'inference_num_threads', 'intel_auto', 'intel_cpu', 'intel_gpu', 'intel_npu', 'key_cache_group_size', 'key_cache_precision', 'loaded_from_cache', 'log', 'max_batch_size', 'model_name', 'num_streams', 'optimal_batch_size', 'optimal_number_of_infer_requests', 'range_for_async_infer_requests', 'range_for_streams', 'runtime_requirements', 'streams', 'supported_properties', 'value_cache_group_size', 'value_cache_precision', 'weights_path', 'workload_type'] class CacheMode: """ Members: @@ -59,6 +59,54 @@ class CacheMode: @property def value(self) -> int: ... +class CompatibilityCheck: + """ + Members: + + NOT_APPLICABLE + + SUPPORTED + + UNSUPPORTED + """ + NOT_APPLICABLE: typing.ClassVar[CompatibilityCheck] # value = + SUPPORTED: typing.ClassVar[CompatibilityCheck] # value = + UNSUPPORTED: typing.ClassVar[CompatibilityCheck] # value = + __members__: typing.ClassVar[dict[str, CompatibilityCheck]] # value = {'NOT_APPLICABLE': , 'SUPPORTED': , 'UNSUPPORTED': } + def __eq__(self, other: typing.Any) -> bool: + ... + def __ge__(self, other: typing.Any) -> bool: + ... + def __getstate__(self) -> int: + ... + def __gt__(self, other: typing.Any) -> bool: + ... + def __hash__(self) -> int: + ... + def __index__(self) -> int: + ... + def __init__(self, value: typing.SupportsInt | typing.SupportsIndex) -> None: + ... + def __int__(self) -> int: + ... + def __le__(self, other: typing.Any) -> bool: + ... + def __lt__(self, other: typing.Any) -> bool: + ... + def __ne__(self, other: typing.Any) -> bool: + ... + def __repr__(self) -> str: + ... + def __setstate__(self, state: typing.SupportsInt | typing.SupportsIndex) -> None: + ... + def __str__(self) -> str: + ... + @property + def name(self) -> str: + ... + @property + def value(self) -> int: + ... class WorkloadType: """ Members: @@ -126,6 +174,8 @@ def cache_mode() -> str: @typing.overload def cache_mode(arg0: CacheMode) -> tuple[str, openvino._pyopenvino.OVAny]: ... +def compatibility_check() -> str: + ... @typing.overload def compilation_num_threads() -> str: ... @@ -196,6 +246,8 @@ def range_for_async_infer_requests() -> str: ... def range_for_streams() -> str: ... +def runtime_requirements() -> str: + ... def supported_properties() -> str: ... @typing.overload diff --git a/src/bindings/python/src/openvino/frontend/pytorch/fx_decoder.py b/src/bindings/python/src/openvino/frontend/pytorch/fx_decoder.py index aa3b974f49d5..366e3b52f618 100644 --- a/src/bindings/python/src/openvino/frontend/pytorch/fx_decoder.py +++ b/src/bindings/python/src/openvino/frontend/pytorch/fx_decoder.py @@ -173,10 +173,12 @@ class TorchFXPythonDecoder (BaseFXDecoder): def __init__(self, pt_module, fx_gm=None, nodes=None, mark_node_callback=None, input_shapes=None, - input_types=None, dynamic_shapes=False): + input_types=None, dynamic_shapes=False, + op_type_mapping=None): super().__init__(mark_node_callback) self.pt_module = pt_module self.fx_gm = fx_gm if fx_gm is not None else pt_module + self._module_extension_target_ops = op_type_mapping or {} self.input_types = input_types or [] self.input_types = [OVAny(pt_to_ov_type_map[str(t)]) for t in self.input_types] @@ -264,14 +266,24 @@ def __init__(self, pt_module, fx_gm=None, nodes=None, @classmethod def from_exported_program( - cls, exported_program: torch.export.ExportedProgram, dynamic_shapes=True + cls, exported_program: torch.export.ExportedProgram, dynamic_shapes=True, + op_type_mapping=None, ) -> "TorchFXPythonDecoder": - """Create a TorchFXPythonDecoder instance from an exported PyTorch program.""" + """Create a TorchFXPythonDecoder instance from an exported PyTorch program. + + :param op_type_mapping: Optional mapping from registered + ``namespace::op_name`` strings (e.g. ``"ov_ext::MySinOp"``) to the + user-provided ``target_op`` labels from ``ModuleExtension`` + (e.g. ``"MySinOp"``). Built by ``patch_model_for_export()`` and + used by ``get_op_type()`` to translate the FX graph node name back + to the ``target_op`` that the C++ frontend expects for + ``ConversionExtension`` lookup. + """ from packaging import version if version.parse(torch.__version__) >= version.parse("2.6"): if cls._decomp_table is None: from torch.export.decomp_utils import CustomDecompTable - from openvino.frontend.pytorch.torchdynamo.decompositions import ops_to_not_decompose + from openvino.frontend.pytorch.torchdynamo.export_decompositions import ops_to_not_decompose cls._decomp_table = CustomDecompTable() for op in ops_to_not_decompose(): try: @@ -281,32 +293,57 @@ def from_exported_program( exported_program = exported_program.run_decompositions(cls._decomp_table) elif version.parse(torch.__version__) >= version.parse("2.2"): from torch._decomp import get_decompositions - from openvino.frontend.pytorch.torchdynamo.decompositions import get_export_decomposition_list + from openvino.frontend.pytorch.torchdynamo.export_decompositions import get_export_decomposition_list decomp = get_decompositions(get_export_decomposition_list()) exported_program = exported_program.run_decompositions(decomp_table=decomp) gm = exported_program.module() logger.debug(gm.code) - return cls(gm, dynamic_shapes=dynamic_shapes) + return cls(gm, dynamic_shapes=dynamic_shapes, + op_type_mapping=op_type_mapping) @classmethod def from_model( cls, model: torch.nn.Module, example_inputs, dynamic_shapes=None, + module_extensions=None, ) -> "TorchFXPythonDecoder": - """Export a model and create a decoder, auto-patching quantized models. + """Export a model and create a decoder, applying module extensions and auto-patching. This is the ``torch.export`` counterpart of TorchScriptPythonDecoder's - auto-patching of quantized models. The method patches the model before - ``torch.export.export`` so that custom ``ov_ext`` ops are captured in - the FX graph, then unpatches afterwards. + module extension support. User-provided ``ModuleExtension`` objects and + automatic quantized-model patches are applied before + ``torch.export.export`` so that custom ops are captured in the FX graph, + then unpatched afterwards. :param model: The ``torch.nn.Module`` to export and decode. :param example_inputs: Example inputs for ``torch.export.export``. :param dynamic_shapes: Dynamic shapes specification built by ``_build_dynamic_shapes`` for ``torch.export.export``, or ``None`` for a fully static graph. + :param module_extensions: A dict mapping module class/instance/name to + ``ModuleExtension`` objects, as returned by + ``extract_module_extensions``. ``None`` means no user extensions. """ from openvino.frontend.pytorch import quantized + from openvino.frontend.pytorch.patch_model import ( + patch_model_for_export, unpatch_model, _clear_export_cache) + + orig_forward_name = "_openvino_module_extension_patch_orig_forward" + + op_type_mapping = {} + ext_patched = False + if module_extensions: + try: + op_type_mapping = patch_model_for_export( + model, module_extensions, orig_forward_name) + ext_patched = True + except Exception: + # Clean up any partial patches before re-raising. + try: + unpatch_model(model, orig_forward_name) + except Exception: + pass + raise quant_patched = False if quantized.detect_quantized_model(model) is not None: @@ -329,9 +366,15 @@ def from_model( finally: if quant_patched: quantized.unpatch_quantized_for_export(model) + if ext_patched: + unpatch_model(model, orig_forward_name) - return cls.from_exported_program( - exported_program, dynamic_shapes=dynamic_shapes is not None) + try: + return cls.from_exported_program( + exported_program, dynamic_shapes=dynamic_shapes is not None, + op_type_mapping=op_type_mapping) + finally: + _clear_export_cache() @staticmethod def _export(model, inputs, dynamic_shapes=None): @@ -516,7 +559,9 @@ def visit_subgraph(self, node_visitor): if is_subgraph: continue decoder = TorchFXPythonDecoder( - node, self.fx_gm, self._nodes, mark_node_callback=self.mark_node_callback) + node, self.fx_gm, self._nodes, + mark_node_callback=self.mark_node_callback, + op_type_mapping=self._module_extension_target_ops) self.m_decoders.append(decoder) node_visitor(decoder) @@ -541,7 +586,8 @@ def get_subgraph_decoder(self, index): decoder = TorchFXPythonDecoder( subgraph, subgraph, # Use subgraph as fx_gm for proper constant resolution - mark_node_callback=self.mark_node_callback + mark_node_callback=self.mark_node_callback, + op_type_mapping=self._module_extension_target_ops ) self.m_decoders.append(decoder) return decoder @@ -549,8 +595,13 @@ def get_subgraph_decoder(self, index): def get_op_type(self): if self.pt_module.op == "call_function": if type(self.pt_module.target).__name__ == "EdgeOpOverload": - return self.pt_module.target.__name__ - return str(self.pt_module.target) + name = self.pt_module.target.__name__ + else: + name = str(self.pt_module.target) + if self._module_extension_target_ops: + if name in self._module_extension_target_ops: + return self._module_extension_target_ops[name] + return name elif self.pt_module.op == "get_attr": return "get_attr" # FIXME should be aligned with get_attr from TS implementation else: diff --git a/src/bindings/python/src/openvino/frontend/pytorch/gptq.py b/src/bindings/python/src/openvino/frontend/pytorch/gptq.py index f185109b9945..329a64e891c5 100644 --- a/src/bindings/python/src/openvino/frontend/pytorch/gptq.py +++ b/src/bindings/python/src/openvino/frontend/pytorch/gptq.py @@ -94,7 +94,7 @@ def patched_forward_sym(self, *args, **kwargs): # All the following AutoGPTQ/GPTQModel quant types are supposed to have the same weights packing schema -supported_quant_types = ["triton", "exllama", "exllamav2", "cuda-old", "hf_kernel"] +supported_quant_types = ["triton", "exllama", "exllamav2", "cuda-old", "hf_kernel", "torch_fused"] def patch_model(model): diff --git a/src/bindings/python/src/openvino/frontend/pytorch/patch_model.py b/src/bindings/python/src/openvino/frontend/pytorch/patch_model.py index 9dce2b013101..df4bc6eaca4c 100644 --- a/src/bindings/python/src/openvino/frontend/pytorch/patch_model.py +++ b/src/bindings/python/src/openvino/frontend/pytorch/patch_model.py @@ -169,27 +169,264 @@ def _unpatch_torch_functions(): del _patched_torch_functions[key] +# Single FRAGMENT library handle for auto-registered ModuleExtension ops. +# Created lazily on first use; reused for all subsequent registrations to +# avoid unbounded Library object accumulation in long-lived processes. +_module_ext_lib = None +_module_ext_registered_ops = {} # op_name → tuple(schema_args) +_module_ext_lock = threading.Lock() + +# Thread-local context used by auto-registered Meta/CPU impls to call the +# extension's ``evaluate`` callback with the original forward arguments. +# ``new_forward`` pushes a callable onto the stack before calling +# ``convert`` and pops it afterwards, so that the Meta dispatch inside +# ``target_op(...)`` can obtain correct output shapes. +_export_tracing_ctx = threading.local() + +# Cache of output (shape, dtype) keyed by (op_name, input_tensor_metadata). +# Populated during dynamo tracing (phase 1 of torch.export) when the +# evaluate stack is available. Consumed during aot_export metadata +# collection (phase 2) when the stack is no longer on the call path. +# Module-level (not thread-local) because phase 2 may run in a context +# where thread-local attributes are reset. Cleared after each export +# by ``_clear_export_cache()``. +_meta_shape_cache = {} + + +def _clear_export_cache(): + """Clear per-export shape cache. + + Called after ``torch.export`` completes to prevent stale shape + metadata from leaking across independent exports in long-lived + processes. + """ + _meta_shape_cache.clear() + + +def _auto_register_module_extension_op(namespace, op_name, schema_args): + """Auto-register a ``torch.library`` op for a ``ModuleExtension`` target. + + ``schema_args`` is a list of schema-argument strings understood by + ``torch.library`` (e.g. ``["Tensor x0", "int x1", "Tensor? x2"]``). + The op returns a single ``Tensor``. + + Uses a single shared ``FRAGMENT`` library handle for the ``ov_ext`` + namespace to avoid creating a new ``Library`` object per op. + + Thread-safe: guarded by ``_module_ext_lock``. + """ + global _module_ext_lib + schema_key = tuple(schema_args) + + with _module_ext_lock: + if _module_ext_lib is None: + # hasattr() is insufficient: torch.ops.__getattr__ raises + # RuntimeError (not AttributeError) for missing namespaces, + # so hasattr() may propagate the exception instead of + # returning False. + try: + getattr(torch.ops, namespace) + kind = "FRAGMENT" + except (AttributeError, RuntimeError): + kind = "DEF" + _module_ext_lib = torch.library.Library(namespace, kind) + + if op_name in _module_ext_registered_ops: + existing = _module_ext_registered_ops[op_name] + if existing != schema_key: + raise RuntimeError( + f"ModuleExtension op '{namespace}::{op_name}' was already " + f"registered with schema ({', '.join(existing)}) but is now " + f"requested with ({', '.join(schema_key)}). Use distinct " + f"target_op names for extensions with different signatures.") + return getattr(getattr(torch.ops, namespace), op_name) + + args = ", ".join(schema_args) + _module_ext_lib.define(f"{op_name}({args}) -> Tensor") + + @torch.library.impl(_module_ext_lib, op_name, "Meta") + def _meta(*xs): + # Build a cache key from tensor shapes/dtypes for + # cross-phase lookup (phase 1 populates, phase 2 consumes). + cache_key = (op_name,) + tuple( + (tuple(a.shape), a.dtype) if isinstance(a, torch.Tensor) + else (type(a).__name__, a) for a in xs) + + # Try evaluate callback (available during dynamo tracing). + stack = getattr(_export_tracing_ctx, "evaluate_stack", None) + if stack: + try: + result = stack[-1]() + if isinstance(result, torch.Tensor): + out_shape = tuple(result.shape) + out_dtype = result.dtype + _meta_shape_cache[cache_key] = (out_shape, out_dtype) + return torch.empty(out_shape, dtype=out_dtype, device="meta") + except Exception: + pass + + # Fall back to cached shape from a previous evaluate call + # (needed during aot_export metadata collection). + if cache_key in _meta_shape_cache: + out_shape, out_dtype = _meta_shape_cache[cache_key] + return torch.empty(out_shape, dtype=out_dtype, device="meta") + + for arg in xs: + if isinstance(arg, torch.Tensor): + return torch.empty_like(arg) + return torch.empty(1, device="meta") + + @torch.library.impl(_module_ext_lib, op_name, "CPU") + def _cpu(*xs): + stack = getattr(_export_tracing_ctx, "evaluate_stack", None) + if stack: + try: + result = stack[-1]() + if isinstance(result, torch.Tensor): + return result + except Exception: + pass + for arg in xs: + if isinstance(arg, torch.Tensor): + return arg + return torch.empty(1) + + _module_ext_registered_ops[op_name] = schema_key + return getattr(getattr(torch.ops, namespace), op_name) + + +def _derive_schema_from_args(call_args, target_op_name): + """Derive ``torch.library`` schema-argument strings from actual call arguments. + + Returns a list such as ``["Tensor x0", "int x1", "Tensor? x2"]``. + Raises ``RuntimeError`` for unsupported argument types. + + ``None`` is not accepted because it is ambiguous (could be + ``Tensor?``, ``int?``, etc.) and the correct schema type cannot be + inferred. Use an explicit sentinel or avoid passing ``None`` from + ``convert()`` for auto-registered ops. + """ + schema_parts = [] + for i, arg in enumerate(call_args): + if isinstance(arg, torch.Tensor): + schema_parts.append(f"Tensor x{i}") + elif arg is None: + raise RuntimeError( + f"ModuleExtension '{target_op_name}': convert() passed " + f"None at position {i} to target_op. None is ambiguous " + f"for schema inference (Tensor?, int?, …). For ops with " + f"optional arguments, pre-register the op with an " + f"explicit schema in ov_custom_ops.py instead of relying " + f"on lazy auto-registration.") + elif isinstance(arg, bool): + # bool before int: bool is a subclass of int in Python. + schema_parts.append(f"bool x{i}") + elif isinstance(arg, int): + schema_parts.append(f"int x{i}") + elif isinstance(arg, float): + schema_parts.append(f"float x{i}") + elif isinstance(arg, str): + schema_parts.append(f"str x{i}") + else: + raise RuntimeError( + f"ModuleExtension '{target_op_name}': convert() passed " + f"unsupported argument type {type(arg).__name__} at position " + f"{i} to target_op. Supported types: Tensor, bool, " + f"int, float, str.") + return schema_parts + + def patch_model_for_export(model, module_extensions, orig_forward_name): - """Patch model modules for ``torch.export`` by replacing forwards with ``torch.ops.ov_ext.*`` calls. + """Patch model modules for ``torch.export`` by replacing forwards with ``torch.library`` op calls. Unlike ``patch_model`` (which uses ``torch.autograd.Function`` / ``torch.jit.ignore`` for TorchScript tracing), this function creates forwards that call registered ``torch.library`` custom ops so that ``torch.export`` captures them as ``call_function`` nodes in the FX graph. + + Returns: + dict: Mapping from the registered ``namespace::op_name`` to the + user-provided ``target_op`` string. The FX decoder uses this in + ``get_op_type()`` so that the C++ frontend sees the user's original + ``target_op`` name. """ import openvino.frontend.pytorch.ov_custom_ops # noqa: F401 – triggers registration + op_type_mapping = {} # registered_name → user target_op + def _resolve_target_op(extension): - """Map an ``ov_ext::*`` target-op name to the corresponding ``torch.ops.ov_ext.*`` callable.""" - # extension.target_op is e.g. "ov_ext::linear" - parts = extension.target_op.split("::") - if len(parts) == 2 and parts[0] == "ov_ext": - op_fn = getattr(torch.ops.ov_ext, parts[1], None) - if op_fn is not None: - return op_fn - raise RuntimeError( - f"Cannot resolve torch.library op for target_op='{extension.target_op}'. " - "Make sure it is registered in openvino.frontend.pytorch.ov_custom_ops.") + """Resolve ``target_op`` to a ``torch.ops`` callable. + + If the op is already registered (from ``ov_custom_ops.py`` or a + previous call), returns it directly. Otherwise returns a + lazy-registering wrapper that auto-registers the op on its first + call during ``torch.export`` tracing, deriving the schema from + the actual arguments that ``convert()`` passes. + """ + target_op = extension.target_op + parts = target_op.split("::") + if len(parts) == 2: + namespace, op_name = parts + elif len(parts) == 1: + namespace, op_name = "ov_ext", parts[0] + else: + raise RuntimeError( + f"Invalid target_op format: '{target_op}'. " + "Expected 'op_name' or 'namespace::op_name'.") + + # Warn if target_op collides with an existing PyTorch op. + # Note: torch.ops namespace __getattr__ raises RuntimeError (not + # AttributeError) for missing ops, so getattr(ns, name, default) + # does not work — explicit try/except is required. + if namespace != "ov_ext": + try: + getattr(getattr(torch.ops, namespace), op_name) + log.warning( + "ModuleExtension target_op '%s' matches an existing PyTorch " + "op. A passthrough op will be registered under the ov_ext " + "namespace instead (the original op will NOT be called, " + "consistent with TorchScript ModuleExtension behavior).", + target_op) + except (AttributeError, RuntimeError): + pass + + # Always register under ov_ext — target_op is just a label. + # Sanitize dots in op_name (torch.library rejects them). + safe_op_name = op_name.replace(".", "_") + # Use the FX-style dotted name as the mapping key so that + # get_op_type() can do a direct dict lookup without parsing. + fx_name = f"ov_ext.{safe_op_name}.default" + + # Detect mapping collisions: same fx_name but different target_op. + if (fx_name in op_type_mapping + and op_type_mapping[fx_name] != target_op): + raise RuntimeError( + f"ModuleExtension target_op collision: '{target_op}' and " + f"'{op_type_mapping[fx_name]}' both map to " + f"FX name '{fx_name}' (after dot " + "sanitization). Use distinct target_op names.") + + # Reuse if already registered (pre-registered or previous extension). + try: + op_fn = getattr(torch.ops.ov_ext, safe_op_name) + op_type_mapping[fx_name] = target_op + return op_fn + except (AttributeError, RuntimeError): + pass + + # Return a lazy wrapper: the op schema is derived from the actual + # arguments that convert() passes on the first call during tracing. + log.debug("Will lazily register torch.library op ov_ext::%s for " + "ModuleExtension (target_op='%s')", safe_op_name, target_op) + + def _lazy_register_and_call(*call_args): + schema_args = _derive_schema_from_args(call_args, target_op) + op_fn = _auto_register_module_extension_op( + "ov_ext", safe_op_name, schema_args) + op_type_mapping[fx_name] = target_op + return op_fn(*call_args) + + return _lazy_register_and_call def module_patcher(module, name): extension = None @@ -203,12 +440,34 @@ def module_patcher(module, name): if extension and extension.condition(module): log.debug("Patching module %s for torch.export", module) target_op = _resolve_target_op(extension) + orig_fwd = module.forward # capture before overwrite def new_forward(*args, **kwargs): - return extension.convert(module, target_op, *args, **kwargs) - - new_forward = functools.wraps(module.forward)(new_forward) - setattr(module, orig_forward_name, module.forward) + # Push an evaluate thunk so the auto-registered Meta/CPU + # impls can produce the correct output shape instead of + # blindly returning ``empty_like(first_tensor)``. + def _evaluate(): + # Temporarily restore the original forward to avoid + # recursion (evaluate's default calls module()). + patched = module.forward + module.forward = orig_fwd + try: + return extension.evaluate(module, *args, **kwargs) + finally: + module.forward = patched + + stack = getattr(_export_tracing_ctx, "evaluate_stack", None) + if stack is None: + stack = [] + _export_tracing_ctx.evaluate_stack = stack + stack.append(_evaluate) + try: + return extension.convert(module, target_op, *args, **kwargs) + finally: + stack.pop() + + new_forward = functools.wraps(orig_fwd)(new_forward) + setattr(module, orig_forward_name, orig_fwd) module.forward = new_forward for name, module in model.named_modules(): @@ -219,6 +478,8 @@ def new_forward(*args, **kwargs): continue module_patcher(module, name) + return op_type_mapping + def _get_16bit_extensions(patch_condition=None): """Return a dict of ModuleExtension entries for known 16-bit module types.""" diff --git a/src/bindings/python/src/openvino/frontend/pytorch/torchdynamo/export_decompositions.py b/src/bindings/python/src/openvino/frontend/pytorch/torchdynamo/export_decompositions.py new file mode 100644 index 000000000000..b0623edf88d5 --- /dev/null +++ b/src/bindings/python/src/openvino/frontend/pytorch/torchdynamo/export_decompositions.py @@ -0,0 +1,271 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2018-2026 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +"""Decomposition lists for the ``torch.export`` path. + +Unlike ``decompositions.py``, this module does **not** register any global +decompositions (via ``@register_decomposition``). It is safe to import from +code paths that must not mutate PyTorch's global decomposition tables — in +particular from ``TorchFXPythonDecoder.from_exported_program``, which may run +before ``torch._inductor`` is imported. +""" + +import torch + + +def get_export_decomposition_list() -> list: + # list of decompositions from torch._decomp.core_aten_decompositions + # removed _backward ops and ops supported without decomposition + decomp = [ + torch.ops.aten.addcdiv, + torch.ops.aten.addcdiv_, + torch.ops.aten.addcmul, + torch.ops.aten.addcmul_, + torch.ops.aten.addr, + torch.ops.aten.affine_grid_generator, + torch.ops.aten.all, + torch.ops.aten.aminmax, + torch.ops.aten.arange.default, + torch.ops.aten.arange.start, + torch.ops.aten.baddbmm, + torch.ops.aten.binary_cross_entropy, + torch.ops.aten.binary_cross_entropy_with_logits, + torch.ops.aten.block_diag, + torch.ops.aten.celu, + torch.ops.aten.celu_, + torch.ops.aten.clamp_max, + torch.ops.aten.clamp_min, + torch.ops.aten.count_nonzero, + torch.ops.aten.linalg_cross, + torch.ops.aten.cudnn_batch_norm, + torch.ops.aten.deg2rad, + torch.ops.aten.deg2rad_, + torch.ops.aten.detach, + torch.ops.aten.diag_embed, + torch.ops.aten.dot, + torch.ops.aten.vdot, + torch.ops.aten.elu, + torch.ops.aten.elu_, + torch.ops.aten._embedding_bag, + torch.ops.aten.empty_like, + torch.ops.aten._euclidean_dist.default, + torch.ops.aten.expand_as, + torch.ops.aten.eye, + torch.ops.aten.fill, + torch.ops.aten.fill_, + torch.ops.aten.floor_divide, + torch.ops.aten.frac, + torch.ops.aten.frac_, + torch.ops.aten._fused_moving_avg_obs_fq_helper, + torch.ops.aten.gelu_, + torch.ops.aten.glu, + torch.ops.aten.hardshrink, + torch.ops.aten.hardsigmoid, + torch.ops.aten.hardsigmoid_, + torch.ops.aten.hardswish, + torch.ops.aten.hardswish_, + torch.ops.aten.hardtanh_, + torch.ops.aten.heaviside, + torch.ops.aten.heaviside_, + torch.ops.aten.huber_loss, + torch.ops.aten.im2col, + torch.ops.aten.index_add, + torch.ops.aten.index_add_, + torch.ops.aten.index_copy, + torch.ops.aten.index_copy_, + torch.ops.aten.index_fill, + torch.ops.aten.index_fill_, + torch.ops.aten.isin, + torch.ops.aten.isneginf, + torch.ops.aten.isposinf, + torch.ops.aten.l1_loss, + torch.ops.aten.leaky_relu_, + torch.ops.aten.lerp, + torch.ops.aten.lerp_, + torch.ops.aten.linspace, + torch.ops.aten.logaddexp, + torch.ops.aten.logaddexp2, + torch.ops.aten.logit, + torch.ops.aten.logit_, + torch.ops.aten.log_sigmoid_forward, + torch.ops.aten.logspace, + torch.ops.aten.logsumexp.default, + torch.ops.aten.masked_fill, + torch.ops.aten.masked_fill_, + torch.ops.aten.mish, + torch.ops.aten.mish_, + torch.ops.aten.mse_loss, + torch.ops.aten.multi_margin_loss, + torch.ops.aten.multilabel_margin_loss_forward, + torch.ops.aten.mv, + torch.ops.aten.mvlgamma, + torch.ops.aten.mvlgamma_, + torch.ops.aten.nansum, + torch.ops.aten.nan_to_num, + torch.ops.aten.nan_to_num_, + torch.ops.aten.narrow, + torch.ops.aten.new_empty, + torch.ops.aten.new_full, + torch.ops.aten.new_ones, + torch.ops.aten.new_zeros, + torch.ops.aten.nll_loss_forward, + torch.ops.aten.norm, + torch.ops.aten.ones, + torch.ops.aten.ones_like, + torch.ops.aten._prelu_kernel, + torch.ops.aten._reshape_alias, + torch.ops.aten.rad2deg, + torch.ops.aten.rad2deg_, + torch.ops.aten.reflection_pad1d, + torch.ops.aten.reflection_pad2d, + torch.ops.aten.reflection_pad3d, + torch.ops.aten.replication_pad1d, + torch.ops.aten.replication_pad2d, + torch.ops.aten.replication_pad3d, + torch.ops.aten.renorm, + torch.ops.aten.renorm_, + torch.ops.aten.resize_as, + torch.ops.aten.roll, + torch.ops.aten.rot90, + torch.ops.aten.rrelu_with_noise, + torch.ops.aten.rrelu_with_noise_, + torch.ops.aten.rsub, + torch.ops.aten.select_scatter, + torch.ops.aten.sgn, + torch.ops.aten.sgn_, + torch.ops.aten.silu, + torch.ops.aten.silu_, + torch.ops.aten.sinc, + torch.ops.aten.sinc_, + torch.ops.aten.smooth_l1_loss, + torch.ops.aten.soft_margin_loss, + torch.ops.aten.softplus, + torch.ops.aten.softshrink, + torch.ops.aten.special_entr, + torch.ops.aten.special_log_ndtr, + torch.ops.aten.special_xlog1py, + torch.ops.aten.split.Tensor, + torch.ops.aten.split_with_sizes_copy, + torch.ops.aten.squeeze.default, + torch.ops.aten.squeeze.dim, + torch.ops.aten.std, + torch.ops.aten.std_mean, + torch.ops.aten.stack, + torch.ops.aten.sum.default, + torch.ops.aten.sum.out, + torch.ops.aten.t, + torch.ops.aten.take, + torch.ops.aten.threshold, + torch.ops.aten.threshold_, + torch.ops.aten.trace, + torch.ops.aten.transpose.int, + torch.ops.aten.tril, + torch.ops.aten.tril_, + torch.ops.aten.triu, + torch.ops.aten.triu_, + torch.ops.aten.unbind, + torch.ops.aten.unfold_copy, + torch.ops.aten._unsafe_index, + torch.ops.aten.unsafe_split.Tensor, + torch.ops.aten.unsafe_split_with_sizes, + torch.ops.aten._unsafe_view, + torch.ops.aten.view_as_complex, + torch.ops.aten.xlogy, + torch.ops.aten.xlogy_, + torch.ops.aten.zero, + torch.ops.aten.zero_, + torch.ops.aten.zeros, + torch.ops.aten.zeros_like, + torch.ops.aten._weight_norm_interface, + ] + try: + from packaging import version + if version.parse(torch.__version__) >= version.parse("2.3"): + decomp += [ + torch.ops.aten._lazy_clone, + torch.ops.aten._test_parallel_materialize, + torch.ops.aten._chunk_cat, + ] + except ImportError: + pass + return decomp + + +def ops_to_not_decompose() -> list: + # list of operations that shouldn't be decomposed because + # OpenVINO frontend handles them directly and more efficiently + return [ + # Activation functions - each maps to a single dedicated OV op + torch.ops.aten.celu.default, + torch.ops.aten.elu_.default, + torch.ops.aten.glu.default, + torch.ops.aten.hardsigmoid.default, + torch.ops.aten.hardswish.default, + torch.ops.aten.hardswish_.default, + torch.ops.aten.hardtanh_.default, + torch.ops.aten.leaky_relu_.default, + torch.ops.aten.log_sigmoid_forward.default, + torch.ops.aten.mish.default, + torch.ops.aten.silu.default, + torch.ops.aten.silu_.default, + # Normalization + torch.ops.aten.linear.default, + torch.ops.aten.rms_norm.default, + # Math and reduction ops with dedicated translators + torch.ops.aten.all.default, + torch.ops.aten.argsort.default, + torch.ops.aten.argsort.stable, + torch.ops.aten.baddbmm.default, + torch.ops.aten.dot.default, + torch.ops.aten.logaddexp.default, + torch.ops.aten.logsumexp.default, + torch.ops.aten.outer.default, + torch.ops.aten.rad2deg.default, + torch.ops.aten.std.correction, + # Spatial and structural ops + torch.ops.aten.channel_shuffle.default, + torch.ops.aten.col2im.default, + torch.ops.aten.pixel_shuffle.default, + torch.ops.aten.pixel_unshuffle.default, + torch.ops.aten.reflection_pad1d.default, + torch.ops.aten.reflection_pad2d.default, + torch.ops.aten.reflection_pad3d.default, + torch.ops.aten.roll.default, + # Index and scatter ops + torch.ops.aten.index_add.default, + torch.ops.aten.index_add_.default, + torch.ops.aten.index_copy.default, + torch.ops.aten.index_fill.int_Scalar, + torch.ops.aten.index_fill_.int_Scalar, + torch.ops.aten.masked_fill.Scalar, + torch.ops.aten.masked_fill.Tensor, + torch.ops.aten.masked_fill_.Scalar, + torch.ops.aten.masked_fill_.Tensor, + torch.ops.aten.select_scatter.default, + # Tensor creation and manipulation + torch.ops.aten.hstack.default, + torch.ops.aten.linalg_cross.default, + torch.ops.aten.linspace.default, + torch.ops.aten.one_hot.default, + torch.ops.aten.repeat_interleave.self_int, + torch.ops.aten.repeat_interleave.self_Tensor, + # Note: aten.take_along_dim.default is not listed here because + # the translator doesn't handle the 2-input case (dim=None) + torch.ops.aten.tril.default, + torch.ops.aten.triu.default, + torch.ops.aten.vstack.default, + # Upsampling / interpolation + torch.ops.aten.upsample_nearest1d.default, + torch.ops.aten.upsample_nearest1d.vec, + torch.ops.aten.upsample_nearest2d.default, + torch.ops.aten.upsample_nearest2d.vec, + torch.ops.aten.upsample_nearest3d.default, + torch.ops.aten.upsample_nearest3d.vec, + torch.ops.aten.upsample_linear1d.vec, + torch.ops.aten.upsample_bilinear2d.vec, + torch.ops.aten.upsample_trilinear3d.vec, + torch.ops.aten.upsample_bicubic2d.vec, + # Attention + torch.ops.aten.scaled_dot_product_attention.default, + ] diff --git a/src/bindings/python/src/openvino/op/__init__.py b/src/bindings/python/src/openvino/op/__init__.py index 215174f6213b..01309ee48c52 100644 --- a/src/bindings/python/src/openvino/op/__init__.py +++ b/src/bindings/python/src/openvino/op/__init__.py @@ -11,6 +11,7 @@ from openvino._pyopenvino.op import Constant from openvino._pyopenvino.op import assign from openvino._pyopenvino.op import _PagedAttentionExtension +from openvino._pyopenvino.op import _GroupQueryAttentionExtension from openvino._pyopenvino.op import Parameter from openvino._pyopenvino.op import if_op from openvino._pyopenvino.op import loop diff --git a/src/bindings/python/src/openvino/op/__init__.pyi b/src/bindings/python/src/openvino/op/__init__.pyi index 0240416b064b..d0ade5bdedd9 100644 --- a/src/bindings/python/src/openvino/op/__init__.pyi +++ b/src/bindings/python/src/openvino/op/__init__.pyi @@ -4,6 +4,7 @@ from __future__ import annotations from openvino._pyopenvino.op import Constant from openvino._pyopenvino.op import Parameter from openvino._pyopenvino.op import Result +from openvino._pyopenvino.op import _GroupQueryAttentionExtension from openvino._pyopenvino.op import _PagedAttentionExtension from openvino._pyopenvino.op import assign from openvino._pyopenvino.op import if_op diff --git a/src/bindings/python/src/openvino/opset17/__init__.py b/src/bindings/python/src/openvino/opset17/__init__.py index 6b928a6d1cb4..c799ef21c18c 100644 --- a/src/bindings/python/src/openvino/opset17/__init__.py +++ b/src/bindings/python/src/openvino/opset17/__init__.py @@ -3,6 +3,7 @@ # SPDX-License-Identifier: Apache-2.0 # New operations added in Opset17 +from openvino.opset17.ops import erfinv # Operators from previous opsets # TODO (ticket: 179247): Add previous opset operators at the end of opset17 development diff --git a/src/bindings/python/src/openvino/opset17/__init__.pyi b/src/bindings/python/src/openvino/opset17/__init__.pyi index eb78e360c9f9..613e91cf5e40 100644 --- a/src/bindings/python/src/openvino/opset17/__init__.pyi +++ b/src/bindings/python/src/openvino/opset17/__init__.pyi @@ -1,3 +1,5 @@ # type: ignore +from . import ops from __future__ import annotations -__all__: list[str] = list() +from openvino.opset17.ops import erfinv +__all__: list[str] = ['erfinv', 'ops'] diff --git a/src/bindings/python/src/openvino/opset17/ops.py b/src/bindings/python/src/openvino/opset17/ops.py index 82212234c1e3..3d4a4e0dd2ee 100644 --- a/src/bindings/python/src/openvino/opset17/ops.py +++ b/src/bindings/python/src/openvino/opset17/ops.py @@ -4,9 +4,24 @@ """Factory functions for ops added to openvino opset17.""" from functools import partial +from typing import Optional +from openvino import Node +from openvino.utils.decorators import unary_op from openvino.utils.node_factory import _get_node_factory +from openvino.utils.types import NodeInput _get_node_factory_opset17 = partial(_get_node_factory, "opset17") # -------------------------------------------- ops ------------------------------------------------ + + +@unary_op +def erfinv(node: NodeInput, name: Optional[str] = None) -> Node: + """Return node which calculates the inverse error function element-wise on the input tensor. + + :param node: The node providing data for the operation. Must be of floating-point type. + :param name: The optional name for the new output node. + :return: The new node performing the element-wise ErfInv operation. + """ + return _get_node_factory_opset17().create("ErfInv", [node]) diff --git a/src/bindings/python/src/openvino/opset17/ops.pyi b/src/bindings/python/src/openvino/opset17/ops.pyi new file mode 100644 index 000000000000..66845415962c --- /dev/null +++ b/src/bindings/python/src/openvino/opset17/ops.pyi @@ -0,0 +1,24 @@ +# type: ignore +from __future__ import annotations +from functools import partial +from openvino._pyopenvino import Node +from openvino.utils.decorators import unary_op +from openvino.utils.node_factory import _get_node_factory +import functools +import openvino._pyopenvino +import typing +""" +Factory functions for ops added to openvino opset17. +""" +__all__: list[str] = ['Node', 'NodeInput', 'erfinv', 'partial', 'unary_op'] +def erfinv(input_value, *args, **kwargs) -> openvino._pyopenvino.Node: + """ + Return node which calculates the inverse error function element-wise on the input tensor. + + :param node: The node providing data for the operation. Must be of floating-point type. + :param name: The optional name for the new output node. + :return: The new node performing the element-wise ErfInv operation. + + """ +NodeInput: typing._UnionGenericAlias # value = typing.Union[openvino._pyopenvino.Node, int, float, numpy.ndarray] +_get_node_factory_opset17: functools.partial # value = functools.partial(, 'opset17') diff --git a/src/bindings/python/src/openvino/properties/__init__.py b/src/bindings/python/src/openvino/properties/__init__.py index 93dddb99f923..4f48de30a30f 100644 --- a/src/bindings/python/src/openvino/properties/__init__.py +++ b/src/bindings/python/src/openvino/properties/__init__.py @@ -3,6 +3,7 @@ # SPDX-License-Identifier: Apache-2.0 # Enums +from openvino._pyopenvino.properties import CompatibilityCheck from openvino._pyopenvino.properties import CacheMode from openvino._pyopenvino.properties import WorkloadType diff --git a/src/bindings/python/src/openvino/properties/__init__.pyi b/src/bindings/python/src/openvino/properties/__init__.pyi index e813b622d8f8..0c9279b5fb8a 100644 --- a/src/bindings/python/src/openvino/properties/__init__.pyi +++ b/src/bindings/python/src/openvino/properties/__init__.pyi @@ -10,6 +10,7 @@ from . import streams from __future__ import annotations from openvino._pyopenvino import properties as __properties from openvino._pyopenvino.properties import CacheMode +from openvino._pyopenvino.properties import CompatibilityCheck from openvino._pyopenvino.properties import WorkloadType from openvino.properties._properties import __make_properties -__all__: list[str] = ['CacheMode', 'WorkloadType', 'device', 'hint', 'intel_auto', 'intel_cpu', 'intel_gpu', 'log', 'streams'] +__all__: list[str] = ['CacheMode', 'CompatibilityCheck', 'WorkloadType', 'device', 'hint', 'intel_auto', 'intel_cpu', 'intel_gpu', 'log', 'streams'] diff --git a/src/bindings/python/src/openvino/tools/ovc/moc_frontend/pytorch_frontend_utils.pyi b/src/bindings/python/src/openvino/tools/ovc/moc_frontend/pytorch_frontend_utils.pyi index a379895076f3..b7b6c9a096e0 100644 --- a/src/bindings/python/src/openvino/tools/ovc/moc_frontend/pytorch_frontend_utils.pyi +++ b/src/bindings/python/src/openvino/tools/ovc/moc_frontend/pytorch_frontend_utils.pyi @@ -28,9 +28,14 @@ def _build_dynamic_shapes(inputs, input_specs = None): result back into the original structure that torch.export expects. """ -def _export_torch_model(model, inputs, input_specs = None): +def _is_pytorch_zip(path): """ - Export a torch.nn.Module using torch.export.export with Dim.AUTO dynamic shapes. + Check if a file looks like a PyTorch archive (TorchScript or ExportedProgram). + + Both formats are ZIP archives with specific internal entries. + This lightweight check avoids expensive torch.jit.load / torch.export.load + calls (and their warnings/side effects) on non-PyTorch files. + """ def extract_input_info_from_example(args, inputs): ... diff --git a/src/bindings/python/src/pyopenvino/core/properties/properties.cpp b/src/bindings/python/src/pyopenvino/core/properties/properties.cpp index 11a06e70a6fb..38aa50d7f6c2 100644 --- a/src/bindings/python/src/pyopenvino/core/properties/properties.cpp +++ b/src/bindings/python/src/pyopenvino/core/properties/properties.cpp @@ -26,6 +26,11 @@ void regmodule_properties(py::module m) { .value("OPTIMIZE_SIZE", ov::CacheMode::OPTIMIZE_SIZE) .value("OPTIMIZE_SPEED", ov::CacheMode::OPTIMIZE_SPEED); + py::enum_(m_properties, "CompatibilityCheck", py::arithmetic()) + .value("NOT_APPLICABLE", ov::CompatibilityCheck::NOT_APPLICABLE) + .value("SUPPORTED", ov::CompatibilityCheck::SUPPORTED) + .value("UNSUPPORTED", ov::CompatibilityCheck::UNSUPPORTED); + // Submodule properties - properties wrap_property_RW(m_properties, ov::enable_profiling, "enable_profiling"); wrap_property_RW(m_properties, ov::cache_dir, "cache_dir"); @@ -54,6 +59,8 @@ void regmodule_properties(py::module m) { wrap_property_RO(m_properties, ov::range_for_async_infer_requests, "range_for_async_infer_requests"); wrap_property_RO(m_properties, ov::execution_devices, "execution_devices"); wrap_property_RO(m_properties, ov::loaded_from_cache, "loaded_from_cache"); + wrap_property_RO(m_properties, ov::compatibility_check, "compatibility_check"); + wrap_property_RO(m_properties, ov::runtime_requirements, "runtime_requirements"); wrap_property_WO(m_properties, ov::cache_encryption_callbacks, "cache_encryption_callbacks"); diff --git a/src/bindings/python/src/pyopenvino/graph/dimension.cpp b/src/bindings/python/src/pyopenvino/graph/dimension.cpp index 7e21f0ee7bad..a64dba833ed6 100644 --- a/src/bindings/python/src/pyopenvino/graph/dimension.cpp +++ b/src/bindings/python/src/pyopenvino/graph/dimension.cpp @@ -42,7 +42,7 @@ void regclass_graph_Dimension(py::module m) { :type max_dimension: int )"); - dim.def(py::init(), py::arg("str")); + dim.def(py::init(), py::arg("str")); dim.def_static("dynamic", &ov::Dimension::dynamic); diff --git a/src/bindings/python/src/pyopenvino/graph/node_output.hpp b/src/bindings/python/src/pyopenvino/graph/node_output.hpp index b058b9128ab9..f7e519faaaee 100644 --- a/src/bindings/python/src/pyopenvino/graph/node_output.hpp +++ b/src/bindings/python/src/pyopenvino/graph/node_output.hpp @@ -178,6 +178,6 @@ void regclass_graph_Output(py::module m, std::string typestring) ov::Output::get_rt_info, py::return_value_policy::reference_internal); - // define functions avaliable only for specific type + // define functions available only for specific type def_type_dependent_functions(output); } diff --git a/src/bindings/python/src/pyopenvino/graph/ops/internal/gqa_extension.cpp b/src/bindings/python/src/pyopenvino/graph/ops/internal/gqa_extension.cpp new file mode 100644 index 000000000000..2141a1d10476 --- /dev/null +++ b/src/bindings/python/src/pyopenvino/graph/ops/internal/gqa_extension.cpp @@ -0,0 +1,23 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "pyopenvino/graph/ops/internal/gqa_extension.hpp" + +#include "openvino/core/op_extension.hpp" +#include "openvino/op/group_query_attention.hpp" +#include "openvino/op/op.hpp" +#include "pyopenvino/core/common.hpp" + +namespace py = pybind11; + +void regclass_graph_op_GroupQueryAttention(py::module m) { + using ov::op::internal::GroupQueryAttention; + + py::class_, + std::shared_ptr>, + ov::Extension>(m, "_GroupQueryAttentionExtension") + .def(py::init<>()) + .doc() = "Extension that registers GroupQueryAttention for IR deserialization. " + "Pass an instance to core.add_extension() before reading an IR that contains this op."; +} diff --git a/src/bindings/python/src/pyopenvino/graph/ops/internal/gqa_extension.hpp b/src/bindings/python/src/pyopenvino/graph/ops/internal/gqa_extension.hpp new file mode 100644 index 000000000000..fa209f85a3d7 --- /dev/null +++ b/src/bindings/python/src/pyopenvino/graph/ops/internal/gqa_extension.hpp @@ -0,0 +1,11 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#pragma once + +#include + +namespace py = pybind11; + +void regclass_graph_op_GroupQueryAttention(py::module m); diff --git a/src/bindings/python/src/pyopenvino/graph/ops/paged_attention_extension.cpp b/src/bindings/python/src/pyopenvino/graph/ops/paged_attention_extension.cpp index 428de6484d8c..54265d57c04e 100644 --- a/src/bindings/python/src/pyopenvino/graph/ops/paged_attention_extension.cpp +++ b/src/bindings/python/src/pyopenvino/graph/ops/paged_attention_extension.cpp @@ -18,4 +18,5 @@ void regclass_graph_op_PagedAttentionExtension(py::module m) { cls.doc() = "Experimental extention for PagedAttention operation. Use with care: no backward compatibility is " "guaranteed in future releases."; cls.def(py::init()); + cls.def("get_write_kv_cache", &PagedAttentionExtension::get_write_kv_cache); } diff --git a/src/bindings/python/src/pyopenvino/pyopenvino.cpp b/src/bindings/python/src/pyopenvino/pyopenvino.cpp index 20879b46b511..cb614a72d6f5 100644 --- a/src/bindings/python/src/pyopenvino/pyopenvino.cpp +++ b/src/bindings/python/src/pyopenvino/pyopenvino.cpp @@ -1,5 +1,6 @@ // Copyright (C) 2018-2026 Intel Corporation // SPDX-License-Identifier: Apache-2.0 +// #include @@ -57,6 +58,7 @@ #include "pyopenvino/graph/ops/if.hpp" #include "pyopenvino/graph/ops/loop.hpp" #include "pyopenvino/graph/ops/paged_attention_extension.hpp" +#include "pyopenvino/graph/ops/internal/gqa_extension.hpp" #include "pyopenvino/graph/ops/parameter.hpp" #include "pyopenvino/graph/ops/read_value.hpp" #include "pyopenvino/graph/ops/result.hpp" @@ -242,6 +244,7 @@ PYBIND11_MODULE(_pyopenvino, m) { regclass_graph_op_Assign(m_op); regclass_graph_op_Constant(m_op); regclass_graph_op_PagedAttentionExtension(m_op); + regclass_graph_op_GroupQueryAttention(m_op); regclass_graph_op_Parameter(m_op); regclass_graph_op_ReadValue(m_op); regclass_graph_op_Result(m_op); diff --git a/src/bindings/python/src/pyopenvino/utils/utils.cpp b/src/bindings/python/src/pyopenvino/utils/utils.cpp index 4c4a12bd600b..4b1a2669dcd6 100644 --- a/src/bindings/python/src/pyopenvino/utils/utils.cpp +++ b/src/bindings/python/src/pyopenvino/utils/utils.cpp @@ -106,6 +106,10 @@ py::object from_ov_any_map(const ov::AnyMap& map) { } py::object from_ov_any(const ov::Any& any) { + // Empty Any (e.g. write-only property queried via get_property) + if (any.empty()) { + return py::none(); + } // Check for py::object if (any.is>()) { return *any.as>(); diff --git a/src/bindings/python/tests/test_graph/test_any.py b/src/bindings/python/tests/test_graph/test_any.py index 86f6411ecdcd..262963c8ae77 100644 --- a/src/bindings/python/tests/test_graph/test_any.py +++ b/src/bindings/python/tests/test_graph/test_any.py @@ -73,6 +73,12 @@ def __init__(self): assert value.value.text == "test" +def test_any_empty(): + ovany = OVAny(None) + assert ovany.value is None + assert ovany.get() is None + + @pytest.mark.parametrize(("value", "dtype"), [ ("some_value", str), (31.23456, float), diff --git a/src/bindings/python/tests/test_graph/test_gqa_extension.py b/src/bindings/python/tests/test_graph/test_gqa_extension.py new file mode 100644 index 000000000000..2a039d58ad92 --- /dev/null +++ b/src/bindings/python/tests/test_graph/test_gqa_extension.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2018-2026 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +import pytest + +from openvino import Core, Extension +from openvino.op import _GroupQueryAttentionExtension + + +def test_gqa_extension_is_extension(): + ext = _GroupQueryAttentionExtension() + assert isinstance(ext, Extension) + + +def test_gqa_extension_add_to_core(): + ext = _GroupQueryAttentionExtension() + core = Core() + core.add_extension(ext) # must not raise diff --git a/src/bindings/python/tests/test_graph/test_ops_unary.py b/src/bindings/python/tests/test_graph/test_ops_unary.py index e953a87c2f3d..ff37ba0d7476 100644 --- a/src/bindings/python/tests/test_graph/test_ops_unary.py +++ b/src/bindings/python/tests/test_graph/test_ops_unary.py @@ -6,6 +6,7 @@ import pytest import openvino.opset13 as ops +import openvino.opset17 as ops_opset17 from openvino import Shape, Type R_TOLERANCE = 1e-6 # global relative tolerance @@ -121,6 +122,26 @@ def test_erf(): assert list(node.get_output_shape(0)) == [6] +def test_erfinv(): + input_tensor = np.array([-0.9, -0.5, 0.0, 0.5, 0.9], dtype=np.float32) + + node = ops_opset17.erfinv(input_tensor) + assert node.get_output_size() == 1 + assert node.get_type_name() == "ErfInv" + assert node.get_output_element_type(0) == Type.f32 + assert list(node.get_output_shape(0)) == [5] + + +def test_erfinv_parameter(): + data = ops.parameter(Shape([2, 3, 4]), dtype=np.float32, name="data") + + node = ops_opset17.erfinv(data) + assert node.get_output_size() == 1 + assert node.get_type_name() == "ErfInv" + assert node.get_output_element_type(0) == Type.f32 + assert list(node.get_output_shape(0)) == [2, 3, 4] + + def test_hswish(): float_dtype = np.float32 data = ops.parameter(Shape([3, 10]), dtype=float_dtype, name="data") diff --git a/src/bindings/python/tests/test_runtime/test_async_infer_request.py b/src/bindings/python/tests/test_runtime/test_async_infer_request.py index 2064b4769111..dc30d559f081 100644 --- a/src/bindings/python/tests/test_runtime/test_async_infer_request.py +++ b/src/bindings/python/tests/test_runtime/test_async_infer_request.py @@ -7,7 +7,6 @@ import numpy as np import pytest import time -import sysconfig import openvino.opset13 as ops from openvino import ( diff --git a/src/bindings/python/tests/test_runtime/test_model.py b/src/bindings/python/tests/test_runtime/test_model.py index 7814129da614..6fbb0226423a 100644 --- a/src/bindings/python/tests/test_runtime/test_model.py +++ b/src/bindings/python/tests/test_runtime/test_model.py @@ -672,6 +672,34 @@ def check_rt_info(model): os.remove(bin_path) +def test_serialize_node_rt_info_disable_fp16(request, tmp_path): + # Setting rt_info["precise_0"] = "" on a node from Python simulates + # disabling FP16 compression. During serialization, "precise_0" is + # converted into a DisablePrecisionConversion attribute so the flag + # persists in the IR. After deserialization the key becomes + # "DisablePrecisionConversion_0". This test verifies the round-trip. + core = Core() + xml_path, bin_path = create_filenames_for_ir(request.node.name, tmp_path) + + param = ops.parameter(PartialShape([1, 3]), dtype=np.float32, name="data") + relu = ops.relu(param, name="relu_node") + model = Model(relu, [param], "TestModel") + + # Set the string-based rt_info that Python users would use + relu.get_rt_info()["precise_0"] = "" + assert "precise_0" in relu.get_rt_info() + + serialize(model, xml_path, bin_path) + res_model = core.read_model(model=xml_path, weights=bin_path) + + for op in res_model.get_ops(): + if op.get_friendly_name() == "relu_node": + assert "DisablePrecisionConversion_0" in op.get_rt_info() + + os.remove(xml_path) + os.remove(bin_path) + + def make_enum_info(): from enum import Enum @@ -913,16 +941,6 @@ def test_model_with_statement(): model.friendly_name -@pytest.mark.skipif(sys.platform != "win32", reason="Windows only") -def test_tempdir_save_load_error(): - # Generate a model with stateful components, ensuring the .bin file will be non-empty after saving - mem_model = generate_model_with_memory(input_shape=Shape([2, 1]), data_type=Type.f32) - with pytest.raises((NotADirectoryError, PermissionError)): - with tempfile.TemporaryDirectory() as model_save_dir: - save_model(mem_model, f"{model_save_dir}/model.xml") - _ = Core().read_model(f"{model_save_dir}/model.xml") - - def test_model_dir(): model = generate_add_model() num_of_attrs = 83 diff --git a/src/bindings/python/tests/test_runtime/test_properties.py b/src/bindings/python/tests/test_runtime/test_properties.py index 98c1231c54f2..1ae6bcb78b3d 100644 --- a/src/bindings/python/tests/test_runtime/test_properties.py +++ b/src/bindings/python/tests/test_runtime/test_properties.py @@ -54,6 +54,14 @@ def test_properties_rw_base(): (props.CacheMode.OPTIMIZE_SPEED, "CacheMode.OPTIMIZE_SPEED", 1), ), ), + ( + props.CompatibilityCheck, + ( + (props.CompatibilityCheck.NOT_APPLICABLE, "CompatibilityCheck.NOT_APPLICABLE", 0), + (props.CompatibilityCheck.SUPPORTED, "CompatibilityCheck.SUPPORTED", 1), + (props.CompatibilityCheck.UNSUPPORTED, "CompatibilityCheck.UNSUPPORTED", 2), + ), + ), ( props.WorkloadType, ( @@ -88,9 +96,7 @@ def test_properties_rw_base(): ), ( hints.ModelDistributionPolicy, - ( - (hints.ModelDistributionPolicy.TENSOR_PARALLEL, "ModelDistributionPolicy.TENSOR_PARALLEL", 0), - ), + ((hints.ModelDistributionPolicy.TENSOR_PARALLEL, "ModelDistributionPolicy.TENSOR_PARALLEL", 0),), ), ( hints.ExecutionMode, @@ -187,6 +193,8 @@ def test_conflicting_enum(proxy_enums, expected_values): (props.range_for_async_infer_requests, "RANGE_FOR_ASYNC_INFER_REQUESTS"), (props.execution_devices, "EXECUTION_DEVICES"), (props.loaded_from_cache, "LOADED_FROM_CACHE"), + (props.runtime_requirements, "RUNTIME_REQUIREMENTS"), + (props.compatibility_check, "COMPATIBILITY_CHECK"), (device.full_name, "FULL_DEVICE_NAME"), (device.architecture, "DEVICE_ARCHITECTURE"), (device.type, "DEVICE_TYPE"), diff --git a/src/cmake/openvino.cmake b/src/cmake/openvino.cmake index dd6870279873..8bea7347e8e0 100644 --- a/src/cmake/openvino.cmake +++ b/src/cmake/openvino.cmake @@ -94,7 +94,11 @@ if(TBB_FOUND) # On Windows there is no RPATH, so copy downloaded/custom TBB DLLs next to openvino.dll. # System TBB is already findable by the loader, so no copying is needed in that case. _ov_get_tbb_location(TBB::tbb _ov_tbb_dll_location) - cmake_path(GET _ov_tbb_dll_location PARENT_PATH _ov_tbb_dll_dir) + if(CMAKE_VERSION VERSION_GREATER_EQUAL "3.20") + cmake_path(GET _ov_tbb_dll_location PARENT_PATH _ov_tbb_dll_dir) + else() + get_filename_component(_ov_tbb_dll_dir "${_ov_tbb_dll_location}" DIRECTORY) + endif() file(GLOB _ov_tbb_dlls "${_ov_tbb_dll_dir}/*.dll") if(_ov_tbb_dlls) add_custom_command(TARGET ${TARGET_NAME} POST_BUILD diff --git a/src/cmake/ov_parallel.cmake b/src/cmake/ov_parallel.cmake index 5fb96afb92a1..2f2efc00b78d 100644 --- a/src/cmake/ov_parallel.cmake +++ b/src/cmake/ov_parallel.cmake @@ -77,6 +77,15 @@ endfunction() macro(ov_find_package_tbb) if((THREADING STREQUAL "TBB" OR THREADING STREQUAL "TBB_AUTO" OR THREADING STREQUAL "TBB_ADAPTIVE") AND NOT TBB_FOUND) + if(ANDROID) + if(NOT DEFINED TBB_DIR AND NOT DEFINED ENV{TBB_DIR}) + message(FATAL_ERROR + "Android build with TBB threading requires a separately built oneTBB package. " + "Build oneTBB as described in docs/dev/build_android.md and configure OpenVINO " + "with -DTBB_DIR=/lib/cmake/TBB.") + endif() + endif() + # conan generates TBBConfig.cmake files, which follows cmake's # SameMajorVersion scheme, while TBB itself follows AnyNewerVersion one # see https://cmake.org/cmake/help/latest/module/CMakePackageConfigHelpers.html#generating-a-package-version-file @@ -132,6 +141,10 @@ macro(ov_find_package_tbb) ${_find_package_no_args}) set(CMAKE_IGNORE_PATH "${_old_CMAKE_IGNORE_PATH}") + if(ANDROID AND NOT TBB_FOUND) + message(FATAL_ERROR "TBB was not found by the configured TBB_DIR path. Use -DTHREADING=SEQ instead.") + endif() + if(NOT TBB_FOUND) # remove invalid TBB_DIR=TBB_DIR-NOTFOUND from cache unset(TBB_DIR CACHE) @@ -253,7 +266,7 @@ macro(ov_find_package_tbb) endif() if(NOT TBB_FOUND) - message(FATAL_ERROR "TBB was not found by the configured TBB_DIR / TBBROOT path. Use -DTHREADING=SEQ instead.") + message(FATAL_ERROR "TBB was not found by the configured TBB_DIR path. Use -DTHREADING=SEQ instead.") else() message(STATUS "TBB (${TBB_VERSION}) is found at ${TBB_DIR}") endif() diff --git a/src/common/low_precision_transformations/include/low_precision/fake_quantize_decomposition.hpp b/src/common/low_precision_transformations/include/low_precision/fake_quantize_decomposition.hpp index 6607a0ccdfc7..9b848034a505 100644 --- a/src/common/low_precision_transformations/include/low_precision/fake_quantize_decomposition.hpp +++ b/src/common/low_precision_transformations/include/low_precision/fake_quantize_decomposition.hpp @@ -5,13 +5,36 @@ #pragma once #include +#include #include "layer_transformation.hpp" +#include "openvino/op/fake_quantize.hpp" namespace ov { namespace pass { namespace low_precision { +namespace fq_decomposition { + +/** + * @brief Extracts output_low and output_high constant values from a FakeQuantize node. + * @return true if both inputs 3 and 4 are constants and values were extracted, false otherwise. + */ +LP_TRANSFORMATIONS_API bool getOutputRanges(const std::shared_ptr& layer, + std::vector& outputLowValues, + std::vector& outputHighValues); + +/** + * @brief Constructs a DataPrecision from a resolved precision and FQ parameters. + * Computes hasZeroPoint by checking whether the FQ output ranges match the target precision. + */ +LP_TRANSFORMATIONS_API DataPrecision makeDataPrecision(const ov::element::Type& precision, + size_t levels, + const std::vector& outputLowValues, + const std::vector& outputHighValues); + +} // namespace fq_decomposition + /** * @ingroup ov_transformation_common_api * @brief FakeQuantizeDecompositionTransformation decomposes FakeQuantize operations to quantize diff --git a/src/common/low_precision_transformations/include/low_precision/low_precision.hpp b/src/common/low_precision_transformations/include/low_precision/low_precision.hpp index 0dacbd26ef8c..421d0643eace 100644 --- a/src/common/low_precision_transformations/include/low_precision/low_precision.hpp +++ b/src/common/low_precision_transformations/include/low_precision/low_precision.hpp @@ -46,12 +46,14 @@ class ov::pass::low_precision::MarkupOptimizations : public ov::pass::ModelPass MarkupOptimizations( const std::vector& precisionRestrictions, const std::vector& quantizationRestrictions, - const AttributeParameters& params); + const AttributeParameters& params, + const std::vector>& additionalMarkupPasses); bool run_on_model(const std::shared_ptr& m) override; private: const std::vector precisionRestrictions; const std::vector quantizationRestrictions; const AttributeParameters params; + const std::vector> additionalMarkupPasses; }; class ov::pass::low_precision::TypeRelaxedReplacer : public ov::pass::GraphRewrite { @@ -81,10 +83,18 @@ class LP_TRANSFORMATIONS_API ov::pass::low_precision::LowPrecision : public ov:: return tr; } + template + std::shared_ptr add_markup(Args&&... args) { + const auto tr = std::make_shared(std::forward(args)...); + additional_markup_passes.push_back(tr); + return tr; + } + protected: std::vector precisionRestrictions; std::vector quantizationRestrictions; LayerTransformation::Params params; std::vector> additional_main_passes; + std::vector> additional_markup_passes; }; diff --git a/src/common/low_precision_transformations/include/low_precision/network_helper.hpp b/src/common/low_precision_transformations/include/low_precision/network_helper.hpp index daf88367a6c5..8386c7212c54 100644 --- a/src/common/low_precision_transformations/include/low_precision/network_helper.hpp +++ b/src/common/low_precision_transformations/include/low_precision/network_helper.hpp @@ -344,6 +344,14 @@ ov::Any getAttributeFromOutput(const Output& output) { return it->second; } +inline std::optional> getOutputPrecisionAttribute(const Output& output) { + auto attr = getAttributeFromOutput(output); + if (attr.empty()) { + return std::nullopt; + } + return attr.as().value(); +} + bool isDisabled(const std::shared_ptr& node); } // namespace low_precision diff --git a/src/common/low_precision_transformations/include/low_precision/resolve_precision_attribute.hpp b/src/common/low_precision_transformations/include/low_precision/resolve_precision_attribute.hpp new file mode 100644 index 000000000000..59f63fa94cad --- /dev/null +++ b/src/common/low_precision_transformations/include/low_precision/resolve_precision_attribute.hpp @@ -0,0 +1,39 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#pragma once + +#include "low_precision/layer_transformation.hpp" +#include "low_precision/lpt_visibility.hpp" +#include "openvino/op/fake_quantize.hpp" +#include "openvino/pass/graph_rewrite.hpp" + +namespace ov { +namespace pass { +namespace low_precision { + +/** + * @ingroup ov_transformation_common_api + * @brief ResolvePrecisionAttribute resolves multi-precision PrecisionsAttribute + * on FakeQuantize outputs to a single precision based on the FQ's output ranges. + * + * After MarkupOptimizations propagates a set of possible PrecisionsAttribute values + * (e.g. {u8, i8}), this pass visits each FakeQuantize and selects the most suitable + * precision from the existing set, based on the FQ’s output_low / output_high ranges + * (signed vs. unsigned). + * The selection prefers representations that avoid unnecessary zero-points, resulting + * in a single, resolved precision for downstream consumers. + */ +class LP_TRANSFORMATIONS_API ResolvePrecisionAttribute : public ov::pass::MatcherPass { +public: + OPENVINO_MATCHER_PASS_RTTI("low_precision::ResolvePrecisionAttribute"); + ResolvePrecisionAttribute(); + + static void filterPrecisionsAttribute(std::shared_ptr layer); + static DataPrecision getDataPrecision(std::shared_ptr layer); +}; + +} // namespace low_precision +} // namespace pass +} // namespace ov diff --git a/src/common/low_precision_transformations/src/convolution.cpp b/src/common/low_precision_transformations/src/convolution.cpp index 0733c397fef3..989eeb826c87 100644 --- a/src/common/low_precision_transformations/src/convolution.cpp +++ b/src/common/low_precision_transformations/src/convolution.cpp @@ -70,27 +70,7 @@ bool ConvolutionTransformation::transform(ov::pass::pattern::Matcher &m) { auto convolution = m.get_match_root(); if (!canConvolutionBeTransformed(convolution, defaultPrecisions)) { - const auto weightInput = convolution->get_input_node_shared_ptr(1); - const auto reshapeFromWeights = ov::as_type_ptr(weightInput); - FakeQuantizeDequantization dequantization = reshapeFromWeights == nullptr ? - NetworkHelper::getDequantization(convolution, defaultPrecisions, 1ul) : - NetworkHelper::getDequantization(reshapeFromWeights, defaultPrecisions); - if (dequantization.empty()) { - const auto fqOnWeights = getFakeQuantizeOnWeights(convolution); - std::shared_ptr resultConstant = NetworkHelper::fold_fake_quantize(fqOnWeights); - if (reshapeFromWeights != nullptr) { - resultConstant = fold_reshape( - resultConstant, - reshapeFromWeights->input_value(1), - false); - } - if (ov::is_type(resultConstant)) { - replace_node(weightInput, resultConstant); - } - } else { - NetworkHelper::foldDequantization(dequantization.multiply, 0, defaultPrecisions, true); - } - return true; + return false; } convolution = NetworkHelper::separateInStandaloneBranch(convolution, defaultPrecisions); diff --git a/src/common/low_precision_transformations/src/fake_quantize_decomposition.cpp b/src/common/low_precision_transformations/src/fake_quantize_decomposition.cpp index 31c8f18df8cf..d7fed21ba1d7 100644 --- a/src/common/low_precision_transformations/src/fake_quantize_decomposition.cpp +++ b/src/common/low_precision_transformations/src/fake_quantize_decomposition.cpp @@ -40,6 +40,32 @@ FakeQuantizeDecompositionTransformation::FakeQuantizeDecompositionTransformation } namespace fq_decomposition { + +bool getOutputRanges(const std::shared_ptr& layer, + std::vector& outputLowValues, + std::vector& outputHighValues) { + auto outputLowConst = ov::as_type_ptr(layer->get_input_node_shared_ptr(3)); + auto outputHighConst = ov::as_type_ptr(layer->get_input_node_shared_ptr(4)); + if (!outputLowConst || !outputHighConst) { + return false; + } + outputLowValues = outputLowConst->cast_vector(); + outputHighValues = outputHighConst->cast_vector(); + return true; +} + +DataPrecision makeDataPrecision(const ov::element::Type& precision, + size_t levels, + const std::vector& outputLowValues, + const std::vector& outputHighValues) { + const bool hasZeroPoint = + LayerTransformation::getPrecisionDetails(levels, outputLowValues, outputHighValues).precision != precision; + return DataPrecision(precision, + DataPrecision::getMinValue(precision, levels), + DataPrecision::getMaxValue(precision, levels), + hasZeroPoint); +} + namespace { // get precision details, depends on: @@ -94,88 +120,6 @@ DataPrecision getDataPrecisionByOutputPortAndFakeQuantize(std::shared_ptr layer) { - const size_t levels = layer->get_levels(); - const std::vector outputLowValues = ov::as_type_ptr(layer->get_input_node_shared_ptr(3))->cast_vector(); - const std::vector outputHighValues = ov::as_type_ptr(layer->get_input_node_shared_ptr(4))->cast_vector(); - - auto precisionsAttribute = getAttributeFromOutput(layer->output(0)); - if (precisionsAttribute.empty()) { - // TODO: explore this case in more details: - // 1. we should not be here - assert(true); - - // 2. not possible to get optimal precision by decomposed FakeQuantize - LayerTransformation::PrecisionDetails precisionDetailsAtOutputIntervals = LayerTransformation::getPrecisionDetails( - levels, - outputLowValues, - outputHighValues); - - return DataPrecision( - precisionDetailsAtOutputIntervals.precision, - DataPrecision::getMinValue(precisionDetailsAtOutputIntervals.precision, levels), - DataPrecision::getMaxValue(precisionDetailsAtOutputIntervals.precision, levels), - precisionDetailsAtOutputIntervals.hasZeroPoint); - } - - const auto& precisions = precisionsAttribute.as().value(); - std::vector precisionsForLevels{}; - switch (levels) { - case low_precision::levels::int16: - case low_precision::levels::int16_narrow_range: - precisionsForLevels = {element::u16, element::i16}; - break; - case low_precision::levels::int32: - case low_precision::levels::int32_narrow_range: - precisionsForLevels = {element::u32, element::i32}; - break; - default: - precisionsForLevels = {element::u8, element::i8}; - } - const auto resultPrecisions = NetworkHelper::precisionIntersection(precisions, precisionsForLevels); - if (resultPrecisions.empty()) { - return DataPrecision(); - } - - ov::element::Type precision; - bool hasZeroPoint; - if (resultPrecisions.size() > 1ul) { - LayerTransformation::PrecisionDetails precisionDetailsAtOutputIntervals = LayerTransformation::getPrecisionDetails( - levels, - outputLowValues, - outputHighValues); - const auto foundIt = std::find(resultPrecisions.begin(), resultPrecisions.end(), precisionDetailsAtOutputIntervals.precision); - - if (foundIt == resultPrecisions.end()) { - precision = *resultPrecisions.begin(); - hasZeroPoint = true; - } else { - precision = precisionDetailsAtOutputIntervals.precision; - hasZeroPoint = precisionDetailsAtOutputIntervals.hasZeroPoint; - } - - // update shared attribute to affect all operations in subgraph - precisionsAttribute.as().value() = { precision }; - } else { - // use only available precision - precision = *resultPrecisions.begin(); - LayerTransformation::PrecisionDetails precisionDetailsAtOutputIntervals = LayerTransformation::getPrecisionDetails( - levels, - outputLowValues, - outputHighValues); - hasZeroPoint = precisionDetailsAtOutputIntervals.precision != precision; - } - - return DataPrecision( - precision, - DataPrecision::getMinValue(precision, levels), - DataPrecision::getMaxValue(precision, levels), - hasZeroPoint); -} - // TODO: LPT: refactor: use one way to decompose FakeQuantize std::tuple, std::shared_ptr> decomposeFakeQuantize( MatcherPass* matcherPass, @@ -292,8 +236,8 @@ bool FakeQuantizeDecompositionTransformation::transform(ov::pass::pattern::Match return rewritten; } - auto attribute = getAttributeFromOutput(layer->output(0)); - if (attribute.empty() || (attribute.as().value().empty())) { + const auto outputPrecisions = getOutputPrecisionAttribute(layer->output(0)); + if (!outputPrecisions || outputPrecisions->empty()) { return rewritten; } @@ -326,10 +270,22 @@ bool FakeQuantizeDecompositionTransformation::transform(ov::pass::pattern::Match return rewritten; } - // check if level is supported in plugin - DataPrecision dataPrecision = fq_decomposition::getDataPrecisionByOutputPort(layer); - if (dataPrecision.empty()) { - return rewritten; + DataPrecision dataPrecision; + { + const auto precisions = getOutputPrecisionAttribute(layer->output(0)); + if (!precisions || precisions->empty()) { + return rewritten; + } + + const auto precision = (*precisions)[0]; + const size_t levels = layer->get_levels(); + + std::vector outputLowValues, outputHighValues; + if (!fq_decomposition::getOutputRanges(layer, outputLowValues, outputHighValues)) { + return rewritten; + } + + dataPrecision = fq_decomposition::makeDataPrecision(precision, levels, outputLowValues, outputHighValues); } PrecisionsAttribute precisionsAttribute(defaultPrecisions); @@ -404,11 +360,7 @@ bool FakeQuantizeDecompositionTransformation::transform(ov::pass::pattern::Match precision = *intervalsAlignment.as().value().preferablePrecisions.begin(); } - dataPrecision = DataPrecision( - precision, - DataPrecision::getMinValue(precision, levels), - DataPrecision::getMaxValue(precision, levels), - LayerTransformation::getPrecisionDetails(levels, outputLowValues, outputHighValues).precision != precision); + dataPrecision = fq_decomposition::makeDataPrecision(precision, levels, outputLowValues, outputHighValues); } } diff --git a/src/common/low_precision_transformations/src/low_precision.cpp b/src/common/low_precision_transformations/src/low_precision.cpp index 96c6af78bd8b..127d9dfc0bd0 100644 --- a/src/common/low_precision_transformations/src/low_precision.cpp +++ b/src/common/low_precision_transformations/src/low_precision.cpp @@ -40,6 +40,7 @@ // prerequisite transformations #include "low_precision/align_quantization_intervals.hpp" #include "low_precision/align_quantization_parameters.hpp" +#include "low_precision/resolve_precision_attribute.hpp" #include "low_precision/markup_avg_pool_precision_preserved.hpp" #include "low_precision/markup_bias.hpp" #include "low_precision/markup_can_be_quantized.hpp" @@ -189,10 +190,12 @@ TypeRelaxedReplacer::TypeRelaxedReplacer() { MarkupOptimizations::MarkupOptimizations( const std::vector& precisionRestrictions, const std::vector& quantizationRestrictions, - const AttributeParameters& params) + const AttributeParameters& params, + const std::vector>& additionalMarkupPasses) : precisionRestrictions(precisionRestrictions), quantizationRestrictions(quantizationRestrictions), - params(params) {} + params(params), + additionalMarkupPasses(additionalMarkupPasses) {} bool MarkupOptimizations::run_on_model(const std::shared_ptr& m) { RUN_ON_FUNCTION_SCOPE(MarkupOptimizations); @@ -214,6 +217,13 @@ bool MarkupOptimizations::run_on_model(const std::shared_ptr& m) { markup.register_pass(params.defaultPrecisions); } markup.register_pass(); + + const auto custom = markup.register_pass(); + for (const auto& tr : additionalMarkupPasses) { + custom->add_matcher(tr); + } + ADD_MATCHER(custom, low_precision::ResolvePrecisionAttribute) + markup.run_passes(m); return false; } @@ -235,7 +245,8 @@ bool LowPrecision::run_on_model(const std::shared_ptr& m) { AttributeParameters attributeParams(params.deqPrecision, params.defaultPrecisions); manager.register_pass(precisionRestrictions, quantizationRestrictions, - attributeParams); + attributeParams, + additional_markup_passes); const auto common = manager.register_pass(); ADD_MATCHER(common, AddTransformation, params) diff --git a/src/common/low_precision_transformations/src/mat_mul.cpp b/src/common/low_precision_transformations/src/mat_mul.cpp index 344bdea4ecbc..8c23aad27357 100644 --- a/src/common/low_precision_transformations/src/mat_mul.cpp +++ b/src/common/low_precision_transformations/src/mat_mul.cpp @@ -59,10 +59,7 @@ bool MatMulTransformation::transform(ov::pass::pattern::Matcher &m) { if (fakeQuantize != nullptr) { const QuantizationDetails quantizationDetails = QuantizationDetails::getDetails(fakeQuantize); - const auto precisionsAttribute = getAttributeFromOutput(fakeQuantize); - const auto precisions = precisionsAttribute.empty() ? - defaultPrecisions : - precisionsAttribute.as().value(); + const auto precisions = getOutputPrecisionAttribute(fakeQuantize->output(0)).value_or(defaultPrecisions); const DataPrecision dataPrecision = getDataPrecision(fakeQuantize, quantizationDetails, precisions); if (dataPrecision.empty()) { return false; diff --git a/src/common/low_precision_transformations/src/recurrent_cell.cpp b/src/common/low_precision_transformations/src/recurrent_cell.cpp index 4da20e0aa243..9f95fe94a773 100644 --- a/src/common/low_precision_transformations/src/recurrent_cell.cpp +++ b/src/common/low_precision_transformations/src/recurrent_cell.cpp @@ -181,10 +181,7 @@ bool RecurrentCellTransformation::transform(ov::pass::pattern::Matcher& m) { continue; } - const auto& precisionsAttribute = getAttributeFromOutput(fq); - const auto& precisions = precisionsAttribute.empty() ? - defaultPrecisions : - precisionsAttribute.as().value(); + const auto precisions = getOutputPrecisionAttribute(fq->output(0)).value_or(defaultPrecisions); const auto& dataPrecision = getDataPrecision(fq, quantizationDetails, precisions); if (dataPrecision.empty() || ((input.second != element::dynamic) && (dataPrecision.precision != input.second))) { @@ -220,10 +217,7 @@ bool RecurrentCellTransformation::transform(ov::pass::pattern::Matcher& m) { if (is_type(fq_parent)) { auto fq_node = as_type_ptr(lstm_parent); const QuantizationDetails quantizationDetails = QuantizationDetails::getDetails(fq_node); - const auto precisionsAttribute = getAttributeFromOutput(lstm_parent); - const auto precisions = precisionsAttribute.empty() - ? defaultPrecisions - : precisionsAttribute.as().value(); + const auto precisions = getOutputPrecisionAttribute(lstm_parent->output(0)).value_or(defaultPrecisions); const DataPrecision dataPrecision = getDataPrecision(lstm_parent, quantizationDetails, precisions); if (dataPrecision.empty() || dataPrecision.hasZeroPoint) { return false; diff --git a/src/common/low_precision_transformations/src/resolve_precision_attribute.cpp b/src/common/low_precision_transformations/src/resolve_precision_attribute.cpp new file mode 100644 index 000000000000..5aeb3bcb5067 --- /dev/null +++ b/src/common/low_precision_transformations/src/resolve_precision_attribute.cpp @@ -0,0 +1,117 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "low_precision/resolve_precision_attribute.hpp" + +#include + +#include "low_precision/fake_quantize_decomposition.hpp" +#include "low_precision/network_helper.hpp" +#include "low_precision/rt_info/precisions_attribute.hpp" +#include "openvino/opsets/opset1_decl.hpp" +#include "openvino/pass/pattern/op/wrap_type.hpp" + +namespace ov { +namespace pass { +namespace low_precision { + +ResolvePrecisionAttribute::ResolvePrecisionAttribute() { + auto matcher = pattern::wrap_type(); + + ov::graph_rewrite_callback callback = [](pattern::Matcher& m) { + auto fq = ov::as_type_ptr(m.get_match_root()); + if (!fq) { + return false; + } + + if (NetworkHelper::isConstantPath(fq)) { + return false; + } + + const auto precisions = getOutputPrecisionAttribute(fq->output(0)); + if (!precisions || precisions->size() <= 1ul) { + return false; + } + + // Resolve multi-precision attribute to a single precision based on FQ output ranges + filterPrecisionsAttribute(fq); + return false; + }; + + auto m = std::make_shared(matcher, "ResolvePrecisionAttribute"); + this->register_matcher(m, callback); +} + +// get precision details, depends on: +// 1. FakeQuantize operation parameters (QuantizationDetails::getDetails & LayerTransformation::getPrecisionDetails) +// 2. Precisions on port +void ResolvePrecisionAttribute::filterPrecisionsAttribute(std::shared_ptr layer) { + const size_t levels = layer->get_levels(); + std::vector outputLowValues, outputHighValues; + if (!fq_decomposition::getOutputRanges(layer, outputLowValues, outputHighValues)) { + return; + } + + auto precisionsAttribute = getAttributeFromOutput(layer->output(0)); + if (precisionsAttribute.empty()) { + return; + } + + const auto& precisions = precisionsAttribute.as().value(); + std::vector precisionsForLevels{}; + switch (levels) { + case low_precision::levels::int16: + case low_precision::levels::int16_narrow_range: + precisionsForLevels = {element::u16, element::i16}; + break; + case low_precision::levels::int32: + case low_precision::levels::int32_narrow_range: + precisionsForLevels = {element::u32, element::i32}; + break; + default: + precisionsForLevels = {element::u8, element::i8}; + } + const auto resultPrecisions = NetworkHelper::precisionIntersection(precisions, precisionsForLevels); + if (resultPrecisions.empty()) { + precisionsAttribute.as().value() = {}; + return; + } + + ov::element::Type precision; + if (resultPrecisions.size() > 1ul) { + LayerTransformation::PrecisionDetails precisionDetailsAtOutputIntervals = + LayerTransformation::getPrecisionDetails(levels, outputLowValues, outputHighValues); + const auto foundIt = std::find(resultPrecisions.begin(), + resultPrecisions.end(), + precisionDetailsAtOutputIntervals.precision); + precision = (foundIt == resultPrecisions.end()) ? *resultPrecisions.begin() + : precisionDetailsAtOutputIntervals.precision; + } else { + precision = *resultPrecisions.begin(); + } + + // update shared attribute to affect all operations in subgraph + precisionsAttribute.as().value() = {precision}; +} + +DataPrecision ResolvePrecisionAttribute::getDataPrecision(std::shared_ptr layer) { + const auto precisions = getOutputPrecisionAttribute(layer->output(0)); + if (!precisions || precisions->empty()) { + return DataPrecision(); + } + + const auto precision = (*precisions)[0]; + const size_t levels = layer->get_levels(); + + std::vector outputLowValues, outputHighValues; + if (!fq_decomposition::getOutputRanges(layer, outputLowValues, outputHighValues)) { + return DataPrecision(); + } + + return fq_decomposition::makeDataPrecision(precision, levels, outputLowValues, outputHighValues); +} + +} // namespace low_precision +} // namespace pass +} // namespace ov diff --git a/src/common/low_precision_transformations/src/weightable_layer_transformation.cpp b/src/common/low_precision_transformations/src/weightable_layer_transformation.cpp index be8de23885fa..cf475a6e4bc8 100644 --- a/src/common/low_precision_transformations/src/weightable_layer_transformation.cpp +++ b/src/common/low_precision_transformations/src/weightable_layer_transformation.cpp @@ -354,10 +354,7 @@ std::tuple, std::shared_ptr> WeightableLayerTr } const QuantizationDetails quantizationDetails = QuantizationDetails::getDetails(fq); - const auto precisionsAttribute = getAttributeFromOutput(fq); - const auto precisions = precisionsAttribute.empty() ? - defaultPrecisions : - precisionsAttribute.as().value(); + const auto precisions = getOutputPrecisionAttribute(fq->output(0)).value_or(defaultPrecisions); const DataPrecision dataPrecision = getDataPrecision(fq, quantizationDetails, precisions); if (dataPrecision.empty()) { @@ -428,10 +425,7 @@ DataPrecision WeightableLayerTransformation::getDataPrecisionOnWeights( return DataPrecision(); } - const auto precisionsAttribute = getAttributeFromOutput(fq); - const auto precisions = precisionsAttribute.empty() ? - defaultPrecisions : - precisionsAttribute.as().value(); + const auto precisions = getOutputPrecisionAttribute(fq->output(0)).value_or(defaultPrecisions); return getDataPrecision(fq, quantizationDetails, precisions); } diff --git a/src/common/low_precision_transformations/tests/convolution_qdq_transformation.cpp b/src/common/low_precision_transformations/tests/convolution_qdq_transformation.cpp index 800f710cdfc9..a7908e9beda6 100644 --- a/src/common/low_precision_transformations/tests/convolution_qdq_transformation.cpp +++ b/src/common/low_precision_transformations/tests/convolution_qdq_transformation.cpp @@ -237,61 +237,6 @@ const std::vector testValues = { } }, - // Actual: - // - // Parameter Constant Constant Constant - // |U8 |U8 |FP32 |I8 - // | | | | - // Convert Convert Convert Convert - // \FP32 /FP32 |FP32 /FP32 - // \ / | / - // Subtract Constant Subtract Constant - // \FP32 /FP32 |FP32 /FP32 - // \ / | / - // Multiply Multiply - // \FP32 /FP32 - // \ / - // Convolution - // - // Transformed: - // - // Parameter Constant - // |U8 |U8 - // | | - // Convert Convert - // \FP32 /FP32 - // \ / - // Subtract Constant - // \FP32 /FP32 - // \ / - // Multiply Constant - // \FP32 /FP32 - // \ / - // Convolution - { - LayerTransformation::createParamsU8I8().setSupportAsymmetricQuantization(true), - // ActualValues - { - ov::element::u8, - {{ov::element::f32}, { {127.f}, element::f32, {}, false, 1ul, element::u8, true }, { 0.02f }}, - {{ov::element::f32}, { {127.f}, element::f32, {}, false, 1ul, element::i8, true }, { 0.03f }}, - { std::vector{ 2.f }, ov::element::f32}, - {}, - ov::element::f32, - {} - }, - // ExpectedValues - { - ov::element::u8, - {{ov::element::f32}, { {127.f}, element::f32, {}, false, 1ul, element::u8, true }, { 0.02f }}, - {}, - { std::vector{ -3.75f }, ov::element::f32}, - {}, - ov::element::f32, - {} - } - }, - // Actual: // // Parameter Constant Constant Constant @@ -453,6 +398,32 @@ const std::vector testValues = { {{}, {}, {{ 0.0006f }, ov::element::f16, {}, false, 1, ov::element::f32}} } }, + + // fp32 base weight value [not transformed] + { + LayerTransformation::createParamsU8I8().setSupportAsymmetricQuantization(true), + // ActualValues + { + ov::element::u8, + {{ov::element::f32}, { {127.f}, element::f32, {}, false, 1ul, element::u8, true }, { 0.02f }}, + {{ov::element::f32}, { {127.f}, element::f32, {}, false, 1ul, element::i8, true }, { 0.03f }}, + { std::vector{ 2.f }, ov::element::f32}, + {}, + ov::element::f32, + {} + }, + // ExpectedValues + { + ov::element::u8, + {{ov::element::f32}, { {127.f}, element::f32, {}, false, 1ul, element::u8, true }, { 0.02f }}, + {{ov::element::f32}, { {127.f}, element::f32, {}, false, 1ul, element::i8, true }, { 0.03f }}, + { std::vector{ 2.f }, ov::element::f32}, + {}, + ov::element::f32, + {} + } + }, + // incorrect zero point on activations [not transformed] { LayerTransformation::createParamsU8I8().setSupportAsymmetricQuantization(true), @@ -482,14 +453,18 @@ const std::vector testValues = { { {1000.f}, element::f32, {}, false }, { {0.02f}, element::f32, {}, false } }, - {}, - { std::vector{ -3.75f }, ov::element::f32}, + { + { ov::element::f32, false }, + { {127.f}, element::f32, {}, false }, + { {0.03f}, element::f32, {}, false } + }, + { std::vector{ 2.f }, ov::element::i8}, {}, ov::element::f32, {} } }, - // incorrect zero point on weights [not transformed, weights folded] + // incorrect zero point on weights [not transformed] { LayerTransformation::createParamsU8I8().setSupportAsymmetricQuantization(true), // ActualValues @@ -518,8 +493,12 @@ const std::vector testValues = { { {127.f}, element::f32, {}, false, 1ul, element::u8, true }, { {0.02f}, element::f32, {}, false } }, - {}, - { std::vector{ -29.94f }, ov::element::f32}, + { + { ov::element::f32, false }, + { {1000.f}, element::f32, {}, false }, + { {0.03f}, element::f32, {}, false } + }, + { std::vector{ 2.f }, ov::element::i8}, {}, ov::element::f32, {} @@ -570,8 +549,12 @@ const std::vector testValues = { { {127.f}, element::f32, {}, false, 1ul, element::u8, true }, { {0.02f}, element::f32, {}, false } }, - {}, - { std::vector{ -3.75 }, ov::element::f32}, + { + { ov::element::f32, false }, + { {127.f}, element::f32, {}, false, 1ul, element::i8, true }, + { {0.03f}, element::f32, {}, false } + }, + { std::vector{ 2.f }, ov::element::i8}, {}, ov::element::f32, {} diff --git a/src/common/low_precision_transformations/tests/convolution_transformation.cpp b/src/common/low_precision_transformations/tests/convolution_transformation.cpp index acaf5983876c..604bb59f642f 100644 --- a/src/common/low_precision_transformations/tests/convolution_transformation.cpp +++ b/src/common/low_precision_transformations/tests/convolution_transformation.cpp @@ -300,10 +300,8 @@ const std::vector testValues = { {{ 128.f, 0.f, 128.f }, ov::element::f32, { 1, 3, 1, 1 }}, {{ 0.02f, 0.01f, 0.03f }, ov::element::f32, {1, 3, 1, 1}} }, - op::v0::Constant::create(ov::element::f32, ov::Shape{}, std::vector{ -1.25f }), - {}, - ov::element::f32, - {} + op::v0::Constant::create(ov::element::f32, ov::Shape{}, std::vector{ 2.f }), + { 255ul, Shape({ 1, 1, 1, 1 }), { 0.f }, { 254.f }, { -1.27f }, { 1.27f } } } }, // float input @@ -328,10 +326,8 @@ const std::vector testValues = { {{ 128.f }, ov::element::f32, { 1, 1, 1, 1 }}, {{ 0.02f }, ov::element::f32, {1, 1, 1, 1}} }, - op::v0::Constant::create(ov::element::f32, ov::Shape{}, std::vector{ -1.25f }), - {}, - ov::element::f32, - {} + op::v0::Constant::create(ov::element::f32, ov::Shape{}, std::vector{ 2.f }), + { 255ul, Shape({ 1, 1, 1, 1 }), { 0.f }, { 254.f }, { -1.27f }, { 1.27f } } } }, // without dequantization operations @@ -367,11 +363,9 @@ const std::vector testValues = { // ExpectedValues { ov::element::f32, - {{}, {}, { {0.02f}, element::f32 }}, - op::v0::Constant::create(ov::element::f32, ov::Shape{}, std::vector{ -1.25f }), - {}, - ov::element::f32, - {} + {{}, {}, { 0.02f }}, + op::v0::Constant::create(ov::element::f32, ov::Shape{}, std::vector{ 2.f }), + { 255ul, Shape({ 1, 1, 1, 1 }), { 0.f }, { 254.f }, { -1.27f }, { 1.27f } } } }, // without zero point @@ -408,10 +402,8 @@ const std::vector testValues = { { ov::element::u8, {{element::f32}, { 1000.f }, { {0.02f}, element::f32 }}, - op::v0::Constant::create(ov::element::f32, ov::Shape{}, std::vector{ -1.25f }), - {}, - ov::element::f32, - {} + op::v0::Constant::create(ov::element::f32, ov::Shape{}, std::vector{ 2.f }), + { 255ul, Shape({ 1, 1, 1, 1 }), { 0.f }, { 254.f }, { -1.27f }, { 1.27f } } } }, // TODO: uncomment: remove precisionsOnActivations & precisionsOnWeights @@ -471,11 +463,9 @@ const std::vector testValues = { // ExpectedValues { ov::element::u8, - {{ ov::element::f32 }, { 128.f }, { 0.02f }}, - op::v0::Constant::create(ov::element::f32, ov::Shape{}, std::vector{ -1.25f }), - {}, - ov::element::f32, - {} + {{ov::element::f32}, { 128.f }, { 0.02f }}, + op::v0::Constant::create(ov::element::f32, ov::Shape{}, std::vector{ 2.f }), + { 255ul, Shape({ 1, 1, 1, 1 }), { 0.f }, { 254.f }, { -1.27f }, { 1.27f } } } }, // without zero point @@ -491,11 +481,9 @@ const std::vector testValues = { // ExpectedValues { ov::element::u8, - {{ ov::element::f32 }, {}, { 0.02f }}, - op::v0::Constant::create(ov::element::f32, ov::Shape{}, std::vector{ -1.25f }), - {}, - ov::element::f32, - {} + {{ov::element::f32}, {}, { 0.02f }}, + op::v0::Constant::create(ov::element::f32, ov::Shape{}, std::vector{ 2.f }), + { 255ul, Shape({ 1, 1, 1, 1 }), { 0.f }, { 254.f }, { -1.27f }, { 1.27f } } } }, }; diff --git a/src/common/low_precision_transformations/tests/group_convolution_transformation.cpp b/src/common/low_precision_transformations/tests/group_convolution_transformation.cpp index 775a4e7794e6..523645ae1d68 100644 --- a/src/common/low_precision_transformations/tests/group_convolution_transformation.cpp +++ b/src/common/low_precision_transformations/tests/group_convolution_transformation.cpp @@ -378,10 +378,8 @@ const std::vector testValuesGroupConv = { { ov::element::f32, {{}, {}, {0.02f}}, - op::v0::Constant::create(ov::element::f32, ov::Shape{}, std::vector{-1.25f}), - {}, - {}, - ov::element::f32, + op::v0::Constant::create(ov::element::f32, ov::Shape{}, std::vector{2.f}), + {255ul, Shape({1, 1, 1, 1}), {0.f}, {254.f}, {-1.27f}, {1.27f}}, {} } }, @@ -864,10 +862,8 @@ const std::vector testValuesForDepthWiseConv = { { ov::element::f32, {{}, {}, {0.02f}}, - op::v0::Constant::create(ov::element::f32, ov::Shape{}, std::vector{-1.25f}), - {}, - {}, - ov::element::f32, + op::v0::Constant::create(ov::element::f32, ov::Shape{}, std::vector{2.f}), + {255ul, Shape({1, 1, 1, 1}), {0.f}, {254.f}, {-1.27f}, {1.27f}}, {} } }, diff --git a/src/common/low_precision_transformations/tests/mark_dequantization_subgraph_transformation.cpp b/src/common/low_precision_transformations/tests/mark_dequantization_subgraph_transformation.cpp index 345e828b75f2..edbb836a4605 100644 --- a/src/common/low_precision_transformations/tests/mark_dequantization_subgraph_transformation.cpp +++ b/src/common/low_precision_transformations/tests/mark_dequantization_subgraph_transformation.cpp @@ -8,7 +8,7 @@ #include "transformations/rt_info/decompression.hpp" #include "transformations/rt_info/dequantization_node.hpp" #include "transformations/rt_info/disable_constant_folding.hpp" -#include "transformations/rt_info/disable_fp16_compression.hpp" +#include "transformations/rt_info/disable_precision_conversion.hpp" #include "transformations/rt_info/keep_const_precision.hpp" #include "common_test_utils/ov_test_utils.hpp" @@ -781,7 +781,7 @@ TEST_F(TransformationTestsF, MarkDequantizationTransformationFoldSubConst) { TEST_F(TransformationTestsF, KeepDequantizationPrecisionTransformationMarkup) { // After KeepDequantizationPrecision all Convert, Subtract, and Multiply nodes - // are marked with the 'disable_fp16_compression' attribute + // are marked with the 'DisablePrecisionConversion' attribute auto quantization_dt = element::u16; auto dequantization_dt = element::f32; @@ -805,15 +805,15 @@ TEST_F(TransformationTestsF, KeepDequantizationPrecisionTransformationMarkup) { auto parameter = std::make_shared(dequantization_dt, Shape{1}); auto weights = opset10::Constant::create(quantization_dt, Shape{4, 16, 1, 1}, {3}); auto convert = std::make_shared(weights, dequantization_dt); - disable_fp16_compression(convert); + disable_conversion(convert, element::f16); auto zero_point = opset10::Constant::create(quantization_dt, Shape{}, {127}); auto convert_on_zero_point = std::make_shared(zero_point, dequantization_dt); - disable_fp16_compression(convert_on_zero_point); + disable_conversion(convert_on_zero_point, element::f16); auto subtract = std::make_shared(convert, convert_on_zero_point); - disable_fp16_compression(subtract); + disable_conversion(subtract, element::f16); auto scale = opset10::Constant::create(dequantization_dt, Shape{}, {0.2}); auto multiply = std::make_shared(subtract, scale); - disable_fp16_compression(multiply); + disable_conversion(multiply, element::f16); auto add = std::make_shared(parameter, multiply); model_ref = std::make_shared(ov::OutputVector{add}); } @@ -855,7 +855,7 @@ TEST_F(TransformationTests, KeepDequantizationPrecisionTransformationFolding) { // Once this issue is fixed, the test should be migrated to the new TransformationTestsF infrastructure. // After KeepDequantizationPrecision all Convert, Subtract, and Multiply nodes - // are marked with the 'disable_fp16_compression' attribute, and dequantization subgraph is folded + // are marked with the 'DisablePrecisionConversion' attribute, and dequantization subgraph is folded // during Constant Folding (called from ConvertPrecision pass), using f32 data type for constants. std::shared_ptr model, model_ref; @@ -903,7 +903,7 @@ TEST_F(TransformationTests, KeepDequantizationPrecisionTransformationFoldingWith // Once this issue is fixed, the test should be migrated to the new TransformationTestsF infrastructure. // After KeepDequantizationPrecision all Convert, Subtract, and Multiply nodes - // are marked with the 'disable_fp16_compression' attribute, and dequantization subgraph is folded + // are marked with the 'DisablePrecisionConversion' attribute, and dequantization subgraph is folded // during Constant Folding (called from ConvertPrecision pass), using f32 data type and finally // converted to the f16 data type. @@ -947,7 +947,7 @@ TEST_F(TransformationTests, KeepDequantizationPrecisionTransformationFoldingWith TEST_F(TransformationTestsF, KeepDequantizationPrecisionTransformationFQMarkup) { // After KeepDequantizationPrecision all Converts, Subtract, and Multiply nodes - // are marked with the 'disable_fp16_compression' attribute + // are marked with the 'DisablePrecisionConversion' attribute auto quantization_dt = element::u16; auto dequantization_dt = element::f32; @@ -981,17 +981,17 @@ TEST_F(TransformationTestsF, KeepDequantizationPrecisionTransformationFQMarkup) opset10::Constant::create(element::f32, Shape{}, { 65536 }), 65535); auto convert1 = std::make_shared(fq, quantization_dt); - disable_fp16_compression(convert1); + disable_conversion(convert1, element::f16); auto convert2 = std::make_shared(convert1, dequantization_dt); - disable_fp16_compression(convert2); + disable_conversion(convert2, element::f16); auto zero_point = opset10::Constant::create(quantization_dt, Shape{}, { 65535 }); auto convert_on_zero_point = std::make_shared(zero_point, dequantization_dt); - disable_fp16_compression(convert_on_zero_point); + disable_conversion(convert_on_zero_point, element::f16); auto subtract = std::make_shared(convert2, convert_on_zero_point); - disable_fp16_compression(subtract); + disable_conversion(subtract, element::f16); auto scale = opset10::Constant::create(dequantization_dt, Shape{}, { 0.2 }); auto multiply = std::make_shared(subtract, scale); - disable_fp16_compression(multiply); + disable_conversion(multiply, element::f16); model_ref = std::make_shared(ov::OutputVector{ multiply }); } comparator.enable(FunctionsComparator::CmpValues::CONST_VALUES); diff --git a/src/common/low_precision_transformations/tests/simple_low_precision_transformer.cpp b/src/common/low_precision_transformations/tests/simple_low_precision_transformer.cpp index c7df3a8eb241..9d594952217c 100644 --- a/src/common/low_precision_transformations/tests/simple_low_precision_transformer.cpp +++ b/src/common/low_precision_transformations/tests/simple_low_precision_transformer.cpp @@ -5,6 +5,7 @@ #include "simple_low_precision_transformer.hpp" #include "low_precision/align_quantization_parameters.hpp" +#include "low_precision/resolve_precision_attribute.hpp" #include "low_precision/layer_transformation.hpp" #include "low_precision/low_precision.hpp" #include "low_precision/markup_bias.hpp" @@ -45,6 +46,8 @@ SimpleLowPrecisionTransformer::SimpleLowPrecisionTransformer( markup->register_pass(params.defaultPrecisions); markup->register_pass(params.defaultPrecisions); markup->register_pass(); + auto filterPass = markup->register_pass(); + filterPass->add_matcher(); common = std::make_shared(passConfig); commonGraphRewrite = common->register_pass(); diff --git a/src/common/snippets/include/snippets/utils/debug_caps_config.hpp b/src/common/snippets/include/snippets/utils/debug_caps_config.hpp index 60f27574c4c1..4371e1238ac0 100644 --- a/src/common/snippets/include/snippets/utils/debug_caps_config.hpp +++ b/src/common/snippets/include/snippets/utils/debug_caps_config.hpp @@ -1,7 +1,6 @@ // Copyright (C) 2018-2026 Intel Corporation // SPDX-License-Identifier: Apache-2.0 // -#include #include #ifdef SNIPPETS_DEBUG_CAPS @@ -137,7 +136,9 @@ class DebugCapsConfig { ~MultipleStringPropertySetter() override = default; bool parseAndSet(const std::string& str) override { - propertyValues = ov::util::split(ov::util::to_lower(str), ','); + const auto lower = ov::util::to_lower(str); + const auto parts = ov::util::split(lower, ","); + propertyValues.assign(parts.begin(), parts.end()); return true; } @@ -167,8 +168,8 @@ class DebugCapsConfig { ~BitsetFilterPropertySetter() override = default; bool parseAndSet(const std::string& str) override { - const auto& tokens = - str.empty() ? std::vector{"all"} : ov::util::split(ov::util::to_lower(str), ','); + const auto lower = ov::util::to_lower(str); + const auto tokens = str.empty() ? std::vector{"all"} : ov::util::split(lower, ","); property.reset(); for (const auto& token : tokens) { const bool tokenVal = (token.front() != '-'); diff --git a/src/common/snippets/src/lowered/linear_ir.cpp b/src/common/snippets/src/lowered/linear_ir.cpp index 82d7778d39ba..b84c7e286e1c 100644 --- a/src/common/snippets/src/lowered/linear_ir.cpp +++ b/src/common/snippets/src/lowered/linear_ir.cpp @@ -48,16 +48,25 @@ namespace ov::snippets::lowered { LinearIR::LinearIR(Config config, const std::shared_ptr& factory) +#ifdef SNIPPETS_DEBUG_CAPS : m_config(std::move(config)), +#else + : m_config(config), +#endif m_loop_manager(std::make_shared()), m_shape_infer_factory(factory), m_shape_infer(std::make_shared(m_expressions, m_parameter_expressions, m_result_expressions)), - m_expression_factory(std::make_shared(m_shape_infer_factory)) {} + m_expression_factory(std::make_shared(m_shape_infer_factory)) { +} LinearIR::LinearIR(const std::shared_ptr& model, const std::shared_ptr& factory, Config config) +#ifdef SNIPPETS_DEBUG_CAPS : LinearIR(std::move(config), factory) { +#else + : LinearIR(config, factory) { +#endif const auto total_nodes = model->get_ops().size(); const auto inputs_count = model->get_parameters().size(); const auto outputs_count = model->get_results().size(); diff --git a/src/common/snippets/src/lowered/pass/insert_perf_count_verbose.cpp b/src/common/snippets/src/lowered/pass/insert_perf_count_verbose.cpp index 3f71c8cf9a1d..6d292c8b8827 100644 --- a/src/common/snippets/src/lowered/pass/insert_perf_count_verbose.cpp +++ b/src/common/snippets/src/lowered/pass/insert_perf_count_verbose.cpp @@ -2,26 +2,28 @@ // SPDX-License-Identifier: Apache-2.0 // -#include -#include -#include -#include -#include -#include -#include - -#include "openvino/core/except.hpp" -#include "openvino/core/type.hpp" -#include "openvino/core/type/element_type.hpp" -#include "openvino/util/common_util.hpp" -#include "snippets/lowered/expression.hpp" -#include "snippets/lowered/port_connector.hpp" -#include "snippets/op/brgemm.hpp" -#include "snippets/op/perf_count.hpp" #ifdef SNIPPETS_DEBUG_CAPS +# include "snippets/lowered/pass/insert_perf_count_verbose.hpp" + +# include +# include +# include +# include +# include +# include +# include +# include + +# include "openvino/core/except.hpp" +# include "openvino/core/type.hpp" +# include "openvino/core/type/element_type.hpp" +# include "openvino/util/common_util.hpp" # include "snippets/itt.hpp" +# include "snippets/lowered/expression.hpp" # include "snippets/lowered/linear_ir.hpp" -# include "snippets/lowered/pass/insert_perf_count_verbose.hpp" +# include "snippets/lowered/port_connector.hpp" +# include "snippets/op/brgemm.hpp" +# include "snippets/op/perf_count.hpp" # include "snippets/utils/utils.hpp" namespace ov::snippets::lowered::pass { @@ -99,12 +101,12 @@ std::string InsertPerfCountVerbose::collect_params(const ov::snippets::lowered:: std::stringstream ss; ss << m_subgraph_name << ','; ss << brgemm_expr->get_node()->get_friendly_name() << ','; - ss << ov::util::join(input_types, ";") << ','; - ss << ov::util::join(output_types, ";") << ','; - ss << ov::util::join(input_shapes, ";") << ','; - ss << ov::util::join(output_shapes, ";") << ','; - ss << ov::util::join(input_layouts, ";") << (input_layouts.empty() ? "" : ";") << ','; - ss << ov::util::join(output_layouts, ";") << (output_layouts.empty() ? "" : ";") << ','; + ss << ov::util::join(input_types, ";") << ','; + ss << ov::util::join(output_types, ";") << ','; + ss << ov::util::join(input_shapes, ";") << ','; + ss << ov::util::join(output_shapes, ";") << ','; + ss << ov::util::join(input_layouts, ";") << (input_layouts.empty() ? "" : ";") << ','; + ss << ov::util::join(output_layouts, ";") << (output_layouts.empty() ? "" : ";") << ','; const auto& in_0_desc = brgemm_expr->get_input_port_descriptor(0); const auto& in_1_desc = brgemm_expr->get_input_port_descriptor(1); diff --git a/src/common/snippets/src/lowered/pass/mha_parallel_wa_optimizer.cpp b/src/common/snippets/src/lowered/pass/mha_parallel_wa_optimizer.cpp index 4b4c4883286d..6e4ebe377b11 100644 --- a/src/common/snippets/src/lowered/pass/mha_parallel_wa_optimizer.cpp +++ b/src/common/snippets/src/lowered/pass/mha_parallel_wa_optimizer.cpp @@ -174,7 +174,7 @@ std::unordered_set MHAParallelWAOptimizer::find_applicab auto brgemm_it = std::find_if(linear_ir->begin(), linear_ir->end(), is_brgemm); std::unordered_set brgemms; while (brgemm_it != linear_ir->end()) { - brgemms.insert(*brgemm_it); + brgemms.insert(lowered::ExpressionPtr(*brgemm_it)); brgemm_it = std::find_if(std::next(brgemm_it), linear_ir->end(), is_brgemm); } const auto& loop_manager = linear_ir->get_loop_manager(); diff --git a/src/common/snippets/src/lowered/port_descriptor.cpp b/src/common/snippets/src/lowered/port_descriptor.cpp index 099d8868a744..2c3fe409db44 100644 --- a/src/common/snippets/src/lowered/port_descriptor.cpp +++ b/src/common/snippets/src/lowered/port_descriptor.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -120,7 +121,7 @@ std::string PortDescriptor::serialize() const { const auto serialize_container = [&ss](const auto& container) { ss << container.size() << " "; if (!container.empty()) { - ss << ov::util::join(container, " ") << " "; + ss << ov::util::join(container, " ") << " "; } }; serialize_container(*m_tensor_shape); diff --git a/src/common/snippets/src/op/perf_count.cpp b/src/common/snippets/src/op/perf_count.cpp index f2984d3494f3..52b423e1135d 100644 --- a/src/common/snippets/src/op/perf_count.cpp +++ b/src/common/snippets/src/op/perf_count.cpp @@ -1,30 +1,30 @@ // Copyright (C) 2018-2026 Intel Corporation // SPDX-License-Identifier: Apache-2.0 // -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "openvino/core/attribute_visitor.hpp" -#include "openvino/core/except.hpp" -#include "openvino/core/node.hpp" -#include "openvino/core/node_output.hpp" -#include "openvino/core/node_vector.hpp" -#include "openvino/core/type.hpp" -#include "openvino/core/type/element_type.hpp" -#include "openvino/op/op.hpp" #ifdef SNIPPETS_DEBUG_CAPS -# include - # include "snippets/op/perf_count.hpp" +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include + +# include "openvino/core/attribute_visitor.hpp" +# include "openvino/core/except.hpp" +# include "openvino/core/node.hpp" +# include "openvino/core/node_output.hpp" +# include "openvino/core/node_vector.hpp" +# include "openvino/core/type.hpp" +# include "openvino/core/type/element_type.hpp" +# include "openvino/op/op.hpp" + namespace ov::snippets { //////////////////utils/////////////// diff --git a/src/common/snippets/src/op/subgraph.cpp b/src/common/snippets/src/op/subgraph.cpp index d16f5e56d724..b0feabce2aba 100644 --- a/src/common/snippets/src/op/subgraph.cpp +++ b/src/common/snippets/src/op/subgraph.cpp @@ -9,7 +9,6 @@ #include #include #include -#include #include #include #include @@ -101,8 +100,12 @@ #include "snippets/shape_inference/shape_inference.hpp" #include "snippets/shape_types.hpp" #include "snippets/utils/debug_caps_config.hpp" -#include "snippets/utils/linear_ir_pass_dumper.hpp" #include "snippets/utils/utils.hpp" +#ifdef SNIPPETS_DEBUG_CAPS +# include + +# include "snippets/utils/linear_ir_pass_dumper.hpp" +#endif // SNIPPETS_DEBUG_CAPS #include "transformations/common_optimizations/nop_elimination.hpp" namespace ov::snippets::op { diff --git a/src/common/snippets/src/pass/propagate_precision.cpp b/src/common/snippets/src/pass/propagate_precision.cpp index 9aea52036941..f07fd5985d50 100644 --- a/src/common/snippets/src/pass/propagate_precision.cpp +++ b/src/common/snippets/src/pass/propagate_precision.cpp @@ -38,7 +38,7 @@ bool ov::snippets::pass::PropagatePrecision::run_on_model(const std::shared_ptr< std::unordered_map, element::Type> result_types; auto results = m->get_results(); for (auto& result : results) { - result_types.emplace(result, result->get_input_element_type(0)); + result_types.emplace(std::shared_ptr(result), result->get_input_element_type(0)); } bool was_updated = false; diff --git a/src/common/snippets/src/runtime_configurator.cpp b/src/common/snippets/src/runtime_configurator.cpp index be1f750e76fb..a638cd8d650e 100644 --- a/src/common/snippets/src/runtime_configurator.cpp +++ b/src/common/snippets/src/runtime_configurator.cpp @@ -10,13 +10,10 @@ #include #include #include -#include -#include #include #include #include "openvino/core/except.hpp" -#include "openvino/core/shape.hpp" #include "openvino/core/type.hpp" #include "openvino/core/type/element_type.hpp" #include "snippets/kernel_executor_table.hpp" @@ -33,6 +30,12 @@ #include "snippets/op/reorder.hpp" #include "snippets/utils/loop_utils.hpp" #include "snippets/utils/utils.hpp" +#ifdef SNIPPETS_DEBUG_CAPS +# include +# include + +# include "openvino/core/shape.hpp" +#endif // SNIPPETS_DEBUG_CAPS namespace ov::snippets { diff --git a/src/common/snippets/src/utils/debug_caps_config.cpp b/src/common/snippets/src/utils/debug_caps_config.cpp index 6fd6d85c3820..d73b5c5aa573 100644 --- a/src/common/snippets/src/utils/debug_caps_config.cpp +++ b/src/common/snippets/src/utils/debug_caps_config.cpp @@ -1,14 +1,16 @@ // Copyright (C) 2018-2026 Intel Corporation // SPDX-License-Identifier: Apache-2.0 // -#include -#include -#include "openvino/core/except.hpp" -#include "openvino/util/common_util.hpp" +#include "snippets/utils/debug_caps_config.hpp" + #ifdef SNIPPETS_DEBUG_CAPS -# include "snippets/utils/debug_caps_config.hpp" +# include +# include + +# include "openvino/core/except.hpp" +# include "openvino/util/common_util.hpp" namespace ov::snippets { @@ -32,7 +34,7 @@ void DebugCapsConfig::readProperties() { } void DebugCapsConfig::PropertyGroup::parseAndSet(const std::string& str) { - const auto& options = ov::util::split(str, ' '); + const auto& options = ov::util::split(str, " "); const auto& propertySetters = getPropertySetters(); bool failed = false; auto getHelp = [propertySetters]() { @@ -47,7 +49,7 @@ void DebugCapsConfig::PropertyGroup::parseAndSet(const std::string& str) { if (option.empty()) { continue; } - const auto& parts = ov::util::split(option, '='); + const auto& parts = ov::util::split(option, "="); if (parts.size() > 2) { failed = true; break; @@ -59,7 +61,7 @@ void DebugCapsConfig::PropertyGroup::parseAndSet(const std::string& str) { return setter->getPropertyName() == propertyName; }); if (foundSetter == propertySetters.end() || - !(*foundSetter)->parseAndSet(parts.size() == 1 ? "" : parts.back())) { + !(*foundSetter)->parseAndSet(parts.size() == 1 ? "" : std::string(parts.back()))) { failed = true; break; } diff --git a/src/common/snippets/src/utils/utils.cpp b/src/common/snippets/src/utils/utils.cpp index 6e1654aeb249..b5605cec84d0 100644 --- a/src/common/snippets/src/utils/utils.cpp +++ b/src/common/snippets/src/utils/utils.cpp @@ -460,7 +460,7 @@ void visit_path(const lowered::ExpressionPtr& expr, auto continue_traversal = [&](const lowered::ExpressionPtr& expr) { if (visited.count(expr) == 0) { exprs.push_front(expr); - visited.insert(expr); + visited.insert(lowered::ExpressionPtr(expr)); } }; diff --git a/src/common/snippets/tests/src/lir_comparator.cpp b/src/common/snippets/tests/src/lir_comparator.cpp index f9a2d4aec3bf..3c4ffaf73378 100644 --- a/src/common/snippets/tests/src/lir_comparator.cpp +++ b/src/common/snippets/tests/src/lir_comparator.cpp @@ -256,7 +256,7 @@ LIRComparator::Result LIRComparator::compare_handlers(const SpecificIterationHan if (pass->get_type_info() != pass_ref->get_type_info() || pass->merge(pass_ref) == nullptr) { return Result::error("Passes are not equal: " + std::string(pass->get_type_name()) + " and " + std::string(pass_ref->get_type_name()) + - ". Pass names or parameters are not matched, or merge method is not overrided."); + ". Pass names or parameters are not matched, or merge method is not overridden."); } } return Result::ok(); diff --git a/src/common/transformations/CMakeLists.txt b/src/common/transformations/CMakeLists.txt index e51446beccb6..273012c534df 100644 --- a/src/common/transformations/CMakeLists.txt +++ b/src/common/transformations/CMakeLists.txt @@ -15,6 +15,12 @@ file(GLOB_RECURSE PUBLIC_HEADERS ${PUBLIC_HEADERS_DIR}/*.hpp) source_group("src" FILES ${LIBRARY_SRC}) source_group("include" FILES ${PUBLIC_HEADERS}) +# Debug capabilities sources - only compiled when ENABLE_DEBUG_CAPS is ON +if(NOT ENABLE_DEBUG_CAPS) + list(REMOVE_ITEM LIBRARY_SRC "${CMAKE_CURRENT_SOURCE_DIR}/src/transformations/utils/extract_subgraph.cpp") + list(REMOVE_ITEM PUBLIC_HEADERS "${PUBLIC_HEADERS_DIR}/transformations/utils/extract_subgraph.hpp") +endif() + # Create library add_library(${TARGET_NAME}_obj OBJECT ${LIBRARY_SRC} ${PUBLIC_HEADERS}) target_compile_definitions(${TARGET_NAME}_obj PRIVATE IMPLEMENT_OPENVINO_API) diff --git a/src/common/transformations/docs/debug_capabilities/README.md b/src/common/transformations/docs/debug_capabilities/README.md new file mode 100644 index 000000000000..7954fd9e655a --- /dev/null +++ b/src/common/transformations/docs/debug_capabilities/README.md @@ -0,0 +1,29 @@ +# Debug capabilities +Debug capabilities are the set of useful debug features for OpenVINO transformations, controlled by environment variables. + +They can be activated at runtime and are useful for analyzing transformation behavior, profiling pass execution, and inspecting model state between passes. + +* [Matcher logging](matcher_logging.md) + When to use: a MatcherPass transformation is not firing or matching unexpectedly — logs the pattern matching process to show why matches succeed or fail. + Requires: `-DENABLE_DEBUG_CAPS=ON` + Example: `OV_MATCHER_LOGGING=true OV_MATCHERS_TO_LOG=EliminateSplitConcat ./your_program` + +* [Transformation profiling](transformation_profiling.md) + When to use: slow model compilation or need to identify which transformation passes take the most time. + Example: `OV_ENABLE_PROFILE_PASS=true` + +* [Model visualization](model_visualization.md) + When to use: need to see the model graph structure after specific passes — generates .svg files. + Example: `OV_ENABLE_VISUALIZE_TRACING=true` or `OV_ENABLE_VISUALIZE_TRACING="Pass1,Pass2"` + +* [Model serialization](model_serialization.md) + When to use: need to inspect model IR (.xml/.bin) after specific passes — useful for diffing model state before and after a transformation. + Example: `OV_ENABLE_SERIALIZE_TRACING=true` or `OV_ENABLE_SERIALIZE_TRACING="Pass1,Pass2"` + +* [Subgraph extraction](extract_subgraph.md) + When to use: working with a large model where full compilation or inference is slow, or where the serialized IR is too large to read or render in graph visualization tools — extract only the relevant subgraph as a standalone `ov::Model`, serialize to IR, and continue investigation on the smaller model independently. + Example: `extract_subgraph(model, {{"MatMul_0", 0}}, {{"Softmax_0", 0}})` + +## See also + +* [debug-matcher-pass skill](../../../../.claude/skills/debug-matcher-pass/SKILL.md) — automated diagnosis workflow for MatcherPass transformations that are not firing. Collects matcher logs, identifies root cause, and generates a reproducer test. diff --git a/src/common/transformations/docs/debug_capabilities/extract_subgraph.md b/src/common/transformations/docs/debug_capabilities/extract_subgraph.md new file mode 100644 index 000000000000..1af894869fde --- /dev/null +++ b/src/common/transformations/docs/debug_capabilities/extract_subgraph.md @@ -0,0 +1,31 @@ +# Subgraph extraction + +Extracts a subgraph from an `ov::Model` as a standalone `ov::Model`, without modifying the original model. + +Useful for inspecting, serializing, or testing a specific portion of a larger model during development or debugging. This is especially valuable when working with large models where full compilation or inference takes a significant amount of time, or where the serialized IR is too large to read or render in graph visualization tools — extracting only the relevant subgraph lets you iterate on it in isolation instead of processing the entire model on each run. + +Once extracted, the subgraph can be serialized to IR via `ov::pass::Serialize` and reloaded independently, so subsequent investigation or reproduction work can be done on the smaller model without access to the original. + +## Example + +```cpp +#include "transformations/utils/extract_subgraph.hpp" + +// Extract the subgraph between two named nodes +const std::multimap inputs = {{"MatMul_0", 0}}; +const std::multimap outputs = {{"Softmax_0", 0}}; + +auto subgraph = ov::op::util::extract_subgraph(model, inputs, outputs); + +// The original model is unchanged; subgraph is an independent ov::Model. +// Serialize it to IR so further investigation can be done on the isolated subgraph +// without rerunning the full model pipeline. +ov::pass::Serialize("subgraph.xml", "subgraph.bin").run_on_model(subgraph); +``` + +## Behavior + +- The original model is **not modified**. +- The extracted model is a deep clone — changes to it do not affect the source model. +- Subgraph `Parameter` nodes inherit the element type and partial shape of their corresponding input ports. +- Node order within the extracted model follows topological order. diff --git a/src/common/transformations/docs/debug_capabilities/matcher_logging.md b/src/common/transformations/docs/debug_capabilities/matcher_logging.md index 74665ef50620..13b02fe8a253 100644 --- a/src/common/transformations/docs/debug_capabilities/matcher_logging.md +++ b/src/common/transformations/docs/debug_capabilities/matcher_logging.md @@ -3,7 +3,7 @@ The logging functionality allows to observe/debug the pattern matching process. ## Usage -In order to utilzie the logging, first, you need to set the CMake flag ```-DENABLE_OPENVINO_DEBUG=ON``` +In order to utilzie the logging, first, you need to set the CMake flag ```-DENABLE_DEBUG_CAPS=ON``` _NOTE: the logging would also work if your build is configured as Release_ diff --git a/src/common/transformations/docs/debug_capabilities/model_serialization.md b/src/common/transformations/docs/debug_capabilities/model_serialization.md new file mode 100644 index 000000000000..a9e00dabf863 --- /dev/null +++ b/src/common/transformations/docs/debug_capabilities/model_serialization.md @@ -0,0 +1,19 @@ +# Model serialization + +Enables serialization of the model to .xml/.bin after each transformation pass. + +## Usage + +Set the `OV_ENABLE_SERIALIZE_TRACING` environment variable to `"true"`, `"on"` or `"1"` to enable serialization for all transformations. + +### Filtering + +You can specify filters to control which passes are serialized. +If the variable is set to a specific filter string (e.g., `"PassName"`, `"PassName1,PassName2"`), +only transformations matching that filter will be serialized. Delimiter is `,`. + +Example: +``` +export OV_ENABLE_SERIALIZE_TRACING=true +export OV_ENABLE_SERIALIZE_TRACING="Pass1,Pass2,Pass3" +``` \ No newline at end of file diff --git a/src/common/transformations/docs/debug_capabilities/model_visualization.md b/src/common/transformations/docs/debug_capabilities/model_visualization.md new file mode 100644 index 000000000000..67dfa0b9f9ac --- /dev/null +++ b/src/common/transformations/docs/debug_capabilities/model_visualization.md @@ -0,0 +1,19 @@ +# Model visualization + +Enables visualization of the model to .svg file after each transformation pass. + +## Usage + +Set the `OV_ENABLE_VISUALIZE_TRACING` environment variable to `"true"`, `"on"` or `"1"` to enable visualization for all transformations. + +### Filtering + +You can specify filters to control which passes are visualized. +If the variable is set to a specific filter string (e.g., `"PassName"`, `"PassName1,PassName2"`), +only transformations matching that filter will be visualized. Delimiter is `,`. + +Example: +``` +export OV_ENABLE_VISUALIZE_TRACING=true +export OV_ENABLE_VISUALIZE_TRACING="Pass1,Pass2,Pass3" +``` diff --git a/src/common/transformations/docs/debug_capabilities/transformation_profiling.md b/src/common/transformations/docs/debug_capabilities/transformation_profiling.md new file mode 100644 index 000000000000..08cd48342c1d --- /dev/null +++ b/src/common/transformations/docs/debug_capabilities/transformation_profiling.md @@ -0,0 +1,14 @@ +# Transformation profiling + +Enables profiling of transformation passes to log their execution times. + +## Usage + +Set the `OV_ENABLE_PROFILE_PASS` environment variable to `"true"` to enable profiling. +Alternatively, specify a file path where the execution times will be saved. + +Example: +``` +export OV_ENABLE_PROFILE_PASS=true +export OV_ENABLE_PROFILE_PASS="/path/to/save/profiling/results" +``` diff --git a/src/common/transformations/docs/debug_capabilities/transformation_statistics_collection.md b/src/common/transformations/docs/debug_capabilities/transformation_statistics_collection.md deleted file mode 100644 index e8457af464bf..000000000000 --- a/src/common/transformations/docs/debug_capabilities/transformation_statistics_collection.md +++ /dev/null @@ -1,43 +0,0 @@ -# Transformation statistics collection and visualization - -There are 3 environment variables which can be set for Transformations debugging: - -1. OV_ENABLE_PROFILE_PASS - Enables profiling of transformation passes to log their execution times. - - - Usage: Set this environment variable to "true" to enable visualizations. - Alternatively, specify a file path where the execution times will be saved. - - Example: - export OV_ENABLE_PROFILE_PASS=true - export OV_ENABLE_PROFILE_PASS="/path/to/save/profiling/results" - - -2. OV_ENABLE_VISUALIZE_TRACING - Enables visualization of the model to .svg file after each transformation pass. - - - Usage: Set this environment variable to "true", "on" or "1" to enable visualization for all Transformations. - - Filtering: You can specify filters to control which passes are visualized. - If the variable is set to a specific filter string (e.g., "PassName", "PassName1,PassName2"), - only transformations matching that filter will be visualized. Delimiter is ",". - - Example: - export OV_ENABLE_VISUALIZE_TRACING=true - export OV_ENABLE_VISUALIZE_TRACING="Pass1,Pass2,Pass3" - - -3. OV_ENABLE_SERIALIZE_TRACING - Enables serialization of the model to .xml/.bin after each transformation pass. - - - Usage: Set this environment variable to "true", "on" or "1" to enable serialization for all Transformations. - - Filtering: You can specify filters to control which passes are serialized. - If the variable is set to a specific filter string (e.g., "PassName", "PassName1,PassName2"), - only transformations matching that filter will be serialized. Delimiter is ",". - - Example: - export OV_ENABLE_SERIALIZE_TRACING=true - export OV_ENABLE_SERIALIZE_TRACING="Pass1,Pass2,Pass3" - -If you have suggestions for improvements or encounter any issues with statistics collection, feel free to submit your feedback or contact Ivan Tikhonov \ No newline at end of file diff --git a/src/common/transformations/include/openvino/decompositions/low_precision_dequantize.hpp b/src/common/transformations/include/openvino/decompositions/low_precision_dequantize.hpp new file mode 100644 index 000000000000..84a1a4e23f04 --- /dev/null +++ b/src/common/transformations/include/openvino/decompositions/low_precision_dequantize.hpp @@ -0,0 +1,74 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#pragma once + +#include "openvino/core/node.hpp" +#include "openvino/core/node_output.hpp" +#include "openvino/pass/node_registry.hpp" +#include "transformations_visibility.hpp" + +namespace ov { +namespace decomposition { + +/// \brief Build the canonical low-precision dequantization sub-graph that is +/// recognised by ov::pass::MarkDequantization and downstream LPT / +/// weight-decompression passes. +/// +/// The returned sub-graph implements one of: +/// y = Multiply(Convert(x, scale_type), scale) // symmetric +/// y = Multiply(Subtract(Convert(x, scale_type), zp), scale) // asymmetric +/// optionally followed by a Reshape when \p output_shape is provided. +/// +/// MarkDequantization matches the Multiply(Subtract(Convert(...), zp), scale) +/// / Multiply(Convert(...), scale) pattern and protects the leading Convert +/// from ConstantFolding. The optional trailing Reshape and any caller-applied +/// ConvertLike sit *outside* the matched pattern — they do not break matching, +/// but they are not themselves marked as dequantization nodes. +/// +/// The output element type is taken from \p scale. If \p zero_point is given +/// and its element type differs from \p scale, a Convert is inserted on the +/// zero_point input as well — both forms (with and without that Convert) are +/// matched by ov::pass::MarkDequantization. +/// +/// The helper intentionally does not append a trailing ConvertLike: that cast +/// is outside the pattern that MarkDequantization recognises and is best +/// applied by the caller when needed (e.g. mixed-precision frontends that +/// need to match the original input element type). +/// +/// All nodes created by the helper are added to \p reg so the caller can +/// post-process them uniformly (e.g. PyTorch frontend iterates the registry +/// and calls NodeContext::mark_node on each entry). Callers that don't need +/// post-processing can use the overload without a NodeRegistry. +/// +/// When \p output_shape is provided, a Reshape is appended only if the +/// Multiply output shape doesn't already match it (statically). This avoids +/// inserting a no-op Reshape when broadcasting already produces the desired +/// shape. +/// +/// \param reg Node registry that collects every node created by the helper. +/// \param x Quantized input tensor (typically a low-precision Constant). +/// \param scale Dequantization scale; its element type determines the +/// output element type of the sub-graph. +/// \param zero_point Optional zero point. When provided a Subtract is +/// inserted between the Convert and the Multiply. +/// \param output_shape Optional shape constant. When provided a Reshape with +/// special_zero=false is appended after the Multiply +/// (skipped if the Multiply output already has that shape). +ov::Output TRANSFORMATIONS_API low_precision_dequantize(ov::pass::NodeRegistry& reg, + const ov::Output& x, + const ov::Output& scale, + const ov::Output& zero_point = {}, + const ov::Output& output_shape = {}); + +/// \brief Convenience overload for callers that do not need access to the +/// intermediate nodes. Internally allocates a NodeRegistry and +/// forwards to the registry-based overload. +ov::Output TRANSFORMATIONS_API low_precision_dequantize(const ov::Output& x, + const ov::Output& scale, + const ov::Output& zero_point = {}, + const ov::Output& output_shape = {}); + +} // namespace decomposition +} // namespace ov diff --git a/src/common/transformations/include/openvino/decompositions/rms_norm.hpp b/src/common/transformations/include/openvino/decompositions/rms_norm.hpp new file mode 100644 index 000000000000..d82c5d30f356 --- /dev/null +++ b/src/common/transformations/include/openvino/decompositions/rms_norm.hpp @@ -0,0 +1,47 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#pragma once + +#include "openvino/core/node.hpp" +#include "openvino/core/node_output.hpp" +#include "openvino/pass/node_registry.hpp" +#include "transformations_visibility.hpp" + +namespace ov { +namespace decomposition { + +/// \brief Build a reference RMSNorm decomposition matched by ov::pass::RMSFusion. +/// +/// The returned sub-graph implements: +/// y = x * Power(Sqrt(ReduceMean(x^2, axes) + eps), -1) // when scale is empty +/// y = scale * (x * Power(Sqrt(ReduceMean(x^2, axes) + eps), -1)) // when scale is provided +/// +/// The shape of the resulting graph is intentionally aligned with the pattern +/// detected by ov::pass::RMSFusion so that this decomposition is always fused +/// back into ov::op::internal::RMS during plugin compilation. This is the +/// canonical building block to be used by frontends (e.g. PyTorch aten::rms_norm) +/// instead of emitting a hand-rolled sub-graph. +/// +/// All nodes created by the helper are added to \p reg so the caller can +/// post-process them uniformly (e.g. PyTorch frontend iterates the registry +/// and calls NodeContext::mark_node on each entry). +/// +/// \param reg Node registry that collects every node created by the helper. +/// \param x Normalized input tensor. +/// \param axes Reduction axes for ReduceMean. RMSFusion currently fuses only +/// the last-dimension reduction (axes constant with a single +/// element equal to -1 or rank-1). +/// \param eps Scalar epsilon constant added before the square root. Should +/// share element type with \p x. +/// \param scale Optional scaling tensor (gamma). Pass an empty Output to skip +/// the trailing multiplication. +ov::Output TRANSFORMATIONS_API rms_norm(ov::pass::NodeRegistry& reg, + const ov::Output& x, + const ov::Output& axes, + const ov::Output& eps, + const ov::Output& scale = {}); + +} // namespace decomposition +} // namespace ov diff --git a/src/common/transformations/include/openvino/decompositions/rope.hpp b/src/common/transformations/include/openvino/decompositions/rope.hpp new file mode 100644 index 000000000000..239ec7711cba --- /dev/null +++ b/src/common/transformations/include/openvino/decompositions/rope.hpp @@ -0,0 +1,50 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#pragma once + +#include + +#include "openvino/core/node.hpp" +#include "openvino/core/node_output.hpp" +#include "openvino/pass/node_registry.hpp" +#include "transformations_visibility.hpp" + +namespace ov { +namespace decomposition { + +/// \brief Build the canonical (simplest) RoPE decomposition that is +/// guaranteed to be fused by ov::pass::RoPEFusion into +/// ov::op::internal::RoPE. +/// +/// The returned sub-graph implements (along the last axis): +/// first_half, second_half = split(x, 2) +/// first_ = first_half * cos - second_half * sin +/// second_ = second_half * cos + first_half * sin +/// y = concat(first_, second_, axis=-1) +/// +/// The negation is expressed as Multiply(-1) + Add (not Subtract) because that +/// is the exact pattern the RoPEFusion matcher accepts. +/// +/// All nodes created by the helper are added to \p reg so the caller can +/// post-process them uniformly (e.g. PyTorch frontend iterates the registry +/// and calls NodeContext::mark_node on each entry). +/// +/// Frontends (e.g. ONNX com.microsoft.RotaryEmbedding) are expected to call +/// this helper for the core formula and add their own pre/post-processing +/// (position_ids gather, interleaved-mode shuffle, 3D<->4D layout conversion). +/// +/// \param reg Node registry that collects every node created by the helper. +/// \param x 4-D input tensor of shape [bs, num_heads, seqlen, head_size]. +/// \param cos Cosine cache broadcastable to [?, 1, ?, head_size/2]. +/// \param sin Sine cache broadcastable to [?, 1, ?, head_size/2]. +/// \param half_head_size Number of element pairs per head, i.e. head_size / 2. +ov::Output TRANSFORMATIONS_API rope(ov::pass::NodeRegistry& reg, + const ov::Output& x, + const ov::Output& cos, + const ov::Output& sin, + int64_t half_head_size); + +} // namespace decomposition +} // namespace ov diff --git a/src/common/transformations/include/ov_ops/moe_compressed.hpp b/src/common/transformations/include/ov_ops/moe_compressed.hpp index d68ebd01e42b..2075ac20bcf7 100644 --- a/src/common/transformations/include/ov_ops/moe_compressed.hpp +++ b/src/common/transformations/include/ov_ops/moe_compressed.hpp @@ -4,6 +4,8 @@ #pragma once +#include + #include "openvino/op/moe.hpp" #include "openvino/op/op.hpp" #include "transformations_visibility.hpp" @@ -15,7 +17,7 @@ class TRANSFORMATIONS_API MOECompressed : public ov::op::internal::MOE { public: OPENVINO_OP("MOECompressed", "", ov::op::internal::MOE); - enum class RoutingType { SOFTMAX, SIGMOID_BIAS }; + MOECompressed() = default; struct Config : public MOE::Config { size_t hidden_size = 0; @@ -28,22 +30,16 @@ class TRANSFORMATIONS_API MOECompressed : public ov::op::internal::MOE { size_t group_size = 0; // In CB, intermediate shapes are expanded to {SeqLen, 1, HiddenSize} // In Non-CB, intermediate shapes are expanded to {Batch, SeqLen, HiddenSize} - size_t has_batch_dim = 0; + bool has_batch_dim = false; bool has_zp = false; ov::element::Type out_type = ov::element::dynamic; - RoutingType routing_type = RoutingType::SOFTMAX; + std::optional scale_factor; }; /// \brief Constructs a MOECompressed operation with config only /// \param args The input tensors, in the following order: /// 0: hidden_states - input tensor with hidden representations - /// 1: routing_weights - normalized routing weights for selected experts. - /// Supports both: - /// * legacy "scattered" layout: [num_experts, ...] (one slice per expert) - /// * compact post-GatherMatmul layout, consistent with ov::op::MOE - /// routing_weights as documented in openvino/op/moe.hpp. - /// In all cases, the layout must be compatible with router_topk_output_indices - /// and the MOE configuration (top_k, num_expert, etc.). + /// 1: routing_weights - [..., topk] normalized routing weights for selected experts. /// 2: router_topk_output_indices - [..., topk] indices of selected top-k experts /// 3: w0_weight - expert weights for first projection, /// shape [num_experts, inter_size, group_num, group_size] @@ -66,6 +62,13 @@ class TRANSFORMATIONS_API MOECompressed : public ov::op::internal::MOE { /// \param config Configuration for the MOE operation 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 clone_with_new_inputs(const OutputVector& new_args) const override; @@ -74,20 +77,4 @@ class TRANSFORMATIONS_API MOECompressed : public ov::op::internal::MOE { Config m_config; }; -TRANSFORMATIONS_API std::ostream& operator<<(std::ostream& s, const MOECompressed::RoutingType& type); - } // namespace ov::op::internal - -namespace ov { -template <> -class AttributeAdapter - : public EnumAttributeAdapterBase { -public: - AttributeAdapter(ov::op::internal::MOECompressed::RoutingType& value) - : EnumAttributeAdapterBase(value) {} - - OPENVINO_RTTI("AttributeAdapter"); - ~AttributeAdapter() override = default; -}; - -} // namespace ov diff --git a/src/common/transformations/include/ov_ops/opset_private_tbl.hpp b/src/common/transformations/include/ov_ops/opset_private_tbl.hpp index b33ad04f3391..a7685313a34d 100644 --- a/src/common/transformations/include/ov_ops/opset_private_tbl.hpp +++ b/src/common/transformations/include/ov_ops/opset_private_tbl.hpp @@ -10,3 +10,4 @@ _OPENVINO_OP_REG(AUGRUCell, ov::op::internal) _OPENVINO_OP_REG(AUGRUSequence, ov::op::internal) _OPENVINO_OP_REG(RMS, ov::op::internal) +_OPENVINO_OP_REG(PagedAttentionExtension, ov::op) diff --git a/src/common/transformations/include/transformations/common_optimizations/convert_tiled_moe_block_to_gather_matmuls.hpp b/src/common/transformations/include/transformations/common_optimizations/convert_tiled_moe_block_to_gather_matmuls.hpp index c57fbeb25018..7e262e563861 100644 --- a/src/common/transformations/include/transformations/common_optimizations/convert_tiled_moe_block_to_gather_matmuls.hpp +++ b/src/common/transformations/include/transformations/common_optimizations/convert_tiled_moe_block_to_gather_matmuls.hpp @@ -4,6 +4,9 @@ #pragma once +#include + +#include "openvino/core/type/element_type.hpp" #include "openvino/pass/graph_rewrite.hpp" #include "openvino/pass/matcher_pass.hpp" #include "transformations_visibility.hpp" @@ -87,7 +90,7 @@ class TRANSFORMATIONS_API ConvertTiledMoeBlockToGatherMatmuls; class ov::pass::ConvertTiledMoeBlockTo2GatherMatmuls : public ov::pass::MatcherPass { public: OPENVINO_MATCHER_PASS_RTTI("ConvertTiledMoeBlockTo2GatherMatmuls"); - ConvertTiledMoeBlockTo2GatherMatmuls(); + ConvertTiledMoeBlockTo2GatherMatmuls(const std::vector& supported_weights_types = {}); }; // ============================================================================ @@ -147,15 +150,15 @@ class ov::pass::ConvertTiledMoeBlockTo2GatherMatmuls : public ov::pass::MatcherP class ov::pass::ConvertTiledMoeBlockTo3GatherMatmuls : public ov::pass::MatcherPass { public: OPENVINO_MATCHER_PASS_RTTI("ConvertTiledMoeBlockTo3GatherMatmuls"); - ConvertTiledMoeBlockTo3GatherMatmuls(); + ConvertTiledMoeBlockTo3GatherMatmuls(const std::vector& supported_weights_types = {}); }; // CPU uses BGM-producing passes only (stops at BGMs) class ov::pass::ConvertTiledMoeBlockToGatherMatmuls : public ov::pass::GraphRewrite { public: OPENVINO_GRAPH_REWRITE_RTTI("ConvertTiledMoeBlockToGatherMatmuls"); - ConvertTiledMoeBlockToGatherMatmuls() { - add_matcher(); - add_matcher(); + explicit ConvertTiledMoeBlockToGatherMatmuls(const std::vector& supported_weights_types = {}) { + add_matcher(supported_weights_types); + add_matcher(supported_weights_types); } }; diff --git a/src/common/transformations/include/transformations/common_optimizations/fuse_clamp_and_fake_quantize.hpp b/src/common/transformations/include/transformations/common_optimizations/fuse_clamp_and_fake_quantize.hpp new file mode 100644 index 000000000000..1b02ded3631a --- /dev/null +++ b/src/common/transformations/include/transformations/common_optimizations/fuse_clamp_and_fake_quantize.hpp @@ -0,0 +1,29 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#pragma once + +#include "openvino/pass/matcher_pass.hpp" +#include "transformations_visibility.hpp" + +namespace ov { +namespace pass { + +class TRANSFORMATIONS_API FuseClampAndFakeQuantize; + +} // namespace pass +} // namespace ov + +/** + * @ingroup ov_transformation_common_api + * @brief FuseClampAndFakeQuantize removes Clamp before FakeQuantize when the Clamp interval fully covers the + * FakeQuantize input interval and Clamp has a single consumer. + * + * The transformation requires FakeQuantize input_low and input_high to be Constant nodes. + */ +class ov::pass::FuseClampAndFakeQuantize : public ov::pass::MatcherPass { +public: + OPENVINO_MATCHER_PASS_RTTI("FuseClampAndFakeQuantize"); + FuseClampAndFakeQuantize(); +}; \ No newline at end of file diff --git a/src/common/transformations/include/transformations/common_optimizations/fuse_gated_delta_net.hpp b/src/common/transformations/include/transformations/common_optimizations/fuse_gated_delta_net.hpp index ef01e58c2b5f..7d6411af4c0d 100644 --- a/src/common/transformations/include/transformations/common_optimizations/fuse_gated_delta_net.hpp +++ b/src/common/transformations/include/transformations/common_optimizations/fuse_gated_delta_net.hpp @@ -49,6 +49,19 @@ class TRANSFORMATIONS_API FuseL2NormIntoGDN : public ov::pass::MatcherPass { FuseL2NormIntoGDN(); }; +/** + * @ingroup ov_transformation_common_api + * @brief Verifies that Q, K, and V inputs of GatedDeltaNet are connected to the same + * Split/Slice/Concat ancestor through a chain of + * Reshape/Transpose/Gather/GatherND/Broadcast/Unsqueeze/Squeeze operations. + */ + +class TRANSFORMATIONS_API FuseGroupedQueryIntoGDN : public ov::pass::MatcherPass { +public: + OPENVINO_MATCHER_PASS_RTTI("FuseGroupedQueryIntoGDN"); + FuseGroupedQueryIntoGDN(); +}; + /// This pass transforms a loop-based Gated Delta Net sub-graph to a single internal `GatedDeltaNet` operation. /// /// Before: diff --git a/src/common/transformations/include/transformations/common_optimizations/matmul_experts_fusion.hpp b/src/common/transformations/include/transformations/common_optimizations/matmul_experts_fusion.hpp deleted file mode 100644 index f696466ba498..000000000000 --- a/src/common/transformations/include/transformations/common_optimizations/matmul_experts_fusion.hpp +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright (C) 2018-2026 Intel Corporation -// SPDX-License-Identifier: Apache-2.0 -// - -#pragma once - -#include "openvino/pass/graph_rewrite.hpp" -#include "openvino/pass/matcher_pass.hpp" -#include "transformations_visibility.hpp" - -namespace ov { -namespace pass { - -class TRANSFORMATIONS_API FuseVectorizedMOE2GEMM; -class TRANSFORMATIONS_API FuseVectorizedMOE3GEMM; -class TRANSFORMATIONS_API VectorizedExpertsFusion; - -} // namespace pass -} // namespace ov - -class ov::pass::FuseVectorizedMOE2GEMM : public ov::pass::MatcherPass { -public: - OPENVINO_MATCHER_PASS_RTTI("FuseVectorizedMOE2GEMM"); - FuseVectorizedMOE2GEMM(); -}; - -class ov::pass::FuseVectorizedMOE3GEMM : public ov::pass::MatcherPass { -public: - OPENVINO_MATCHER_PASS_RTTI("FuseVectorizedMOE3GEMM"); - FuseVectorizedMOE3GEMM(); -}; - -class ov::pass::VectorizedExpertsFusion : public ov::pass::GraphRewrite { -public: - OPENVINO_GRAPH_REWRITE_RTTI("VectorizedExpertsFusion"); - VectorizedExpertsFusion() { - add_matcher(); - add_matcher(); - } -}; diff --git a/src/common/transformations/include/transformations/common_optimizations/moe_op_fusion.hpp b/src/common/transformations/include/transformations/common_optimizations/moe_op_fusion.hpp index 4077af934f40..c9353e2287cc 100644 --- a/src/common/transformations/include/transformations/common_optimizations/moe_op_fusion.hpp +++ b/src/common/transformations/include/transformations/common_optimizations/moe_op_fusion.hpp @@ -22,19 +22,19 @@ class TRANSFORMATIONS_API MoeOpFusion; class ov::pass::Convert2GatherMatmulMoeBlockToMoeOp : public ov::pass::MatcherPass { public: OPENVINO_MATCHER_PASS_RTTI("Convert2GatherMatmulMoeBlockToMoeOp"); - Convert2GatherMatmulMoeBlockToMoeOp(size_t has_batch_dim = 1); + Convert2GatherMatmulMoeBlockToMoeOp(bool has_batch_dim = true); }; class ov::pass::Convert3GatherMatmulMoeBlockToMoeOp : public ov::pass::MatcherPass { public: OPENVINO_MATCHER_PASS_RTTI("Convert3GatherMatmulMoeBlockToMoeOp"); - Convert3GatherMatmulMoeBlockToMoeOp(size_t has_batch_dim = 1); + Convert3GatherMatmulMoeBlockToMoeOp(bool has_batch_dim = true); }; class ov::pass::MoeOpFusion : public ov::pass::GraphRewrite { public: OPENVINO_GRAPH_REWRITE_RTTI("MoeOpFusion"); - MoeOpFusion(size_t has_batch_dim = 1) { + MoeOpFusion(bool has_batch_dim = true) { add_matcher(has_batch_dim); add_matcher(has_batch_dim); } diff --git a/src/common/transformations/include/transformations/common_optimizations/transpose_sinking.hpp b/src/common/transformations/include/transformations/common_optimizations/transpose_sinking.hpp index 853d9434a4d5..2bfbec7caf9c 100644 --- a/src/common/transformations/include/transformations/common_optimizations/transpose_sinking.hpp +++ b/src/common/transformations/include/transformations/common_optimizations/transpose_sinking.hpp @@ -14,6 +14,7 @@ namespace ov { namespace pass { class TRANSFORMATIONS_API TransposeSinking; +class TRANSFORMATIONS_API TransposeFQ; class TRANSFORMATIONS_API TransposeConvert; class TRANSFORMATIONS_API TransposeEltwise; class TRANSFORMATIONS_API TransposeReduction; @@ -23,6 +24,16 @@ class TRANSFORMATIONS_API TransposeFuse; } // namespace pass } // namespace ov +/** + * @ingroup ov_transformation_common_api + * @brief TransposeFQ transformation sinks Transpose through FakeQuantize + */ +class ov::pass::TransposeFQ : public ov::pass::MatcherPass { +public: + OPENVINO_MATCHER_PASS_RTTI("TransposeFQ"); + TransposeFQ(); +}; + /** * @ingroup ov_transformation_common_api * @brief TransposeReduction transformation sinks Transpose through Reduce operations @@ -83,6 +94,7 @@ class ov::pass::TransposeSinking : public ov::pass::GraphRewrite { public: OPENVINO_GRAPH_REWRITE_RTTI("TransposeSinking"); TransposeSinking() { + add_matcher(); add_matcher(); add_matcher(); add_matcher(); diff --git a/src/common/transformations/include/transformations/fp16_compression/convert_legacy_precision_attribute.hpp b/src/common/transformations/include/transformations/fp16_compression/convert_legacy_precision_attribute.hpp new file mode 100644 index 000000000000..e24fe8da2b56 --- /dev/null +++ b/src/common/transformations/include/transformations/fp16_compression/convert_legacy_precision_attribute.hpp @@ -0,0 +1,27 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#pragma once + +#include "openvino/pass/matcher_pass.hpp" +#include "transformations_visibility.hpp" + +namespace ov::pass { + +class TRANSFORMATIONS_API ConvertLegacyPrecisionAttribute; + +} // namespace ov::pass + +/** + * @ingroup ov_transformation_common_api + * @brief ConvertLegacyPrecisionAttribute migrates the legacy DisableFP16Compression + * runtime attribute ("precise") to the new DisablePrecisionConversion attribute. + * This ensures backward compatibility when loading old IR models that use the + * legacy attribute. + */ +class ov::pass::ConvertLegacyPrecisionAttribute : public ov::pass::ModelPass { +public: + OPENVINO_MODEL_PASS_RTTI("ConvertLegacyPrecisionAttribute"); + bool run_on_model(const std::shared_ptr& model) override; +}; diff --git a/src/common/transformations/include/transformations/op_conversions/group_query_attention_decomposition.hpp b/src/common/transformations/include/transformations/op_conversions/group_query_attention_decomposition.hpp index 3e61e50cbf4d..5fcf8c848ee7 100644 --- a/src/common/transformations/include/transformations/op_conversions/group_query_attention_decomposition.hpp +++ b/src/common/transformations/include/transformations/op_conversions/group_query_attention_decomposition.hpp @@ -27,7 +27,6 @@ class ov::pass::GroupQueryAttentionDecomposition : public ov::pass::MatcherPass std::shared_ptr get_dimensions(const std::shared_ptr& shape, const std::vector& dims); std::shared_ptr get_dimensions(const std::shared_ptr& node, const std::vector& dims); - ov::OutputVector make_split(const ov::Output& value, int64_t num_splits, int64_t axis); std::shared_ptr rotaryEmbedding(ov::Output input, ov::Output cos, ov::Output sin, diff --git a/src/common/transformations/include/transformations/common_optimizations/convert_pagedattn_inputs.hpp b/src/common/transformations/include/transformations/paged_attention/convert_pagedattn_inputs.hpp similarity index 98% rename from src/common/transformations/include/transformations/common_optimizations/convert_pagedattn_inputs.hpp rename to src/common/transformations/include/transformations/paged_attention/convert_pagedattn_inputs.hpp index 9e7d945cf87f..971ad7b43005 100644 --- a/src/common/transformations/include/transformations/common_optimizations/convert_pagedattn_inputs.hpp +++ b/src/common/transformations/include/transformations/paged_attention/convert_pagedattn_inputs.hpp @@ -52,4 +52,4 @@ class ConvertPagedAttnInputs : public ov::pass::MatcherPass { }; } // namespace pass -} // namespace ov +} // namespace ov \ No newline at end of file diff --git a/src/common/transformations/include/transformations/paged_attention/eliminate_conv_padding_mask_gating.hpp b/src/common/transformations/include/transformations/paged_attention/eliminate_conv_padding_mask_gating.hpp new file mode 100644 index 000000000000..2d493d17c9e9 --- /dev/null +++ b/src/common/transformations/include/transformations/paged_attention/eliminate_conv_padding_mask_gating.hpp @@ -0,0 +1,33 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#pragma once + +#include "openvino/pass/matcher_pass.hpp" +#include "transformations_visibility.hpp" + +namespace ov { +namespace pass { + +/** + * @ingroup ov_transformation_common_api + * @brief Eliminates the conv padding mask gating subgraph introduced by transformers 5.0+. + * + * Transformers 5.0 added `apply_mask_to_padding_states` which multiplies hidden_states + * by attention_mask before conv layers. First observed in the LFM2 model. + * In PagedAttention mode, sequences are packed contiguously without padding, so this gating is + * always an identity (mask is all-1s). + * + * Matches the pattern: + * attention_mask -> Slice -> Unsqueeze -> [Convert] -> Multiply -> Add -> Multiply(H, mask_expr) + * and replaces the final Multiply with its hidden_states input directly. + */ +class TRANSFORMATIONS_API EliminateConvPaddingMaskGating : public ov::pass::MatcherPass { +public: + OPENVINO_MATCHER_PASS_RTTI("EliminateConvPaddingMaskGating"); + EliminateConvPaddingMaskGating(); +}; + +} // namespace pass +} // namespace ov diff --git a/src/common/transformations/include/transformations/paged_attention/paged_causal_conv1d_fusion.hpp b/src/common/transformations/include/transformations/paged_attention/paged_causal_conv1d_fusion.hpp new file mode 100644 index 000000000000..7217b88e3605 --- /dev/null +++ b/src/common/transformations/include/transformations/paged_attention/paged_causal_conv1d_fusion.hpp @@ -0,0 +1,90 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#pragma once + +#include + +#include "openvino/pass/matcher_pass.hpp" +#include "openvino/pass/sdpa_to_paged_attention.hpp" +#include "transformations_visibility.hpp" + +namespace ov { +namespace pass { + +/** + * @ingroup ov_transformation_common_api + * @brief Fuses GroupConvolution-based causal Conv1D state update into internal::PagedCausalConv1D. + * + * For example, the following graph + * + * +------------------+ + * | ReadValue (cache)| + * +------------------+ + * | + * [optional Gather] + * | + * +--------------------+ + * | + * token branch --------------------------> +------------------+ + * | Concat (axis=-1) | + * +------------------+ + * | + * v + * +------------------+ + * | GroupConvolution | + * +------------------+ + * | + * v + * +------------------+ + * | Slice (axis=2) | + * +------------------+ + * | + * v + * ... + * + * [optional] Slice(state_concat) -> Result("cache_params.present.conv.*") + * + * is transformed to: + * + * token branch --> [optional Transpose] --> Reshape ----------+ + * | + * conv_state_table.N -----------------------------------------+ + * | + * reshaped_weights -------------------------------------------+ + * | + * zero_bias --------------------------------------------------+ + * | + * subsequence_begins -----------------------------------------+ + * la.block_indices -------------------------------------------+ + * la.block_indices_begins ------------------------------------+ + * la.past_lens -----------------------------------------------+ + * la.cache_interval ------------------------------------------+ + * v + * +--------------------------------+ + * | internal::PagedCausalConv1D | + * +--------------------------------+ + * | + * v + * +------------------------+ + * | Unsqueeze (axis = 2) | + * +------------------------+ + * | + * v + * ... + * + * [optional] legacy present-state Result is rewired to preserve external behavior. + */ +class TRANSFORMATIONS_API PagedCausalConv1DFusion : public ov::pass::MatcherPass { +public: + OPENVINO_MATCHER_PASS_RTTI("PagedCausalConv1DFusion"); + PagedCausalConv1DFusion(ov::pass::paged_attention::PaParams& pa_params, + std::unordered_set& var_ids_to_remove); + +private: + size_t m_layer_index = 0; +}; + +} // namespace pass +} // namespace ov diff --git a/src/common/transformations/include/transformations/paged_attention/paged_gated_delta_net_fusion.hpp b/src/common/transformations/include/transformations/paged_attention/paged_gated_delta_net_fusion.hpp new file mode 100644 index 000000000000..2fc8ec1a41e4 --- /dev/null +++ b/src/common/transformations/include/transformations/paged_attention/paged_gated_delta_net_fusion.hpp @@ -0,0 +1,67 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#pragma once + +#include + +#include "openvino/pass/matcher_pass.hpp" +#include "openvino/pass/sdpa_to_paged_attention.hpp" +#include "transformations_visibility.hpp" + +namespace ov { +namespace pass { + +/** + * @ingroup ov_transformation_common_api + * @brief Fuses GatedDeltaNet into internal::PagedGatedDeltaNet with paged attention. + * + * For example, the following graph + * + * +------------------+ + * | recurrent_state | + * | (Parameter) | + * +------------------+ + * | + * +---+---+-+--+---+ + * | | | | | + * v v v v v + * +------------------+ + * | GatedDeltaNet | + * | (internal op) | + * +------------------+ + * | | + * output0| |output1 (state) + * | | + * v v + * ... [optional Result/Assign for state writeback] + * + * is transformed to: + * + * gated_delta_state_table.N -----------+ + * gate, beta (flattened) -----+ | + * query, key, value (flat) ---+------> | internal::PagedGatedDeltaNet + * subsequence_begins ----------+ | + * la.block_indices -----------+ | + * la.* (runtime params) ------+ | + * v + * +------------------+ + * |Reshape for output| + * +------------------+ + * | + * v + * ... + */ +class TRANSFORMATIONS_API PagedGatedDeltaNetFusion : public ov::pass::MatcherPass { +public: + OPENVINO_MATCHER_PASS_RTTI("PagedGatedDeltaNetFusion"); + PagedGatedDeltaNetFusion(ov::pass::paged_attention::PaParams& pa_params, + std::unordered_set& var_ids_to_remove); + +private: + size_t m_layer_index = 0; +}; + +} // namespace pass +} // namespace ov diff --git a/src/common/transformations/include/transformations/sdpa_to_paged_attention/position_ids_replacer.hpp b/src/common/transformations/include/transformations/paged_attention/position_ids_replacer.hpp similarity index 86% rename from src/common/transformations/include/transformations/sdpa_to_paged_attention/position_ids_replacer.hpp rename to src/common/transformations/include/transformations/paged_attention/position_ids_replacer.hpp index 2e021fc22f41..9896190c8a05 100644 --- a/src/common/transformations/include/transformations/sdpa_to_paged_attention/position_ids_replacer.hpp +++ b/src/common/transformations/include/transformations/paged_attention/position_ids_replacer.hpp @@ -17,6 +17,7 @@ namespace pass { class TRANSFORMATIONS_API PositionIDsReplacer; class TRANSFORMATIONS_API PositionIDsReplacerQwen; +class TRANSFORMATIONS_API PositionIDsReplacerLFM2; class TRANSFORMATIONS_API PositionIDsReplacerCodeGen2; } // namespace pass @@ -84,8 +85,20 @@ class ov::pass::PositionIDsReplacerQwen : public ov::pass::MatcherPass { * │Gather├──────────┘ * └──────┘ */ + class ov::pass::PositionIDsReplacerCodeGen2 : public ov::pass::MatcherPass { public: OPENVINO_MATCHER_PASS_RTTI("PositionIDsReplacerCodeGen2"); explicit PositionIDsReplacerCodeGen2(const std::shared_ptr& position_ids); }; + +/** + * @brief LFM2-style models compute RoPE positions from an internal arange (aten::arange) rather + * than from the explicit position_ids input. This transformation replaces that arange output with + * position_ids so that Paged Attention can serve tokens in arbitrary order. + */ +class ov::pass::PositionIDsReplacerLFM2 : public ov::pass::MatcherPass { +public: + OPENVINO_MATCHER_PASS_RTTI("PositionIDsReplacerLFM2"); + explicit PositionIDsReplacerLFM2(const Output& position_ids); +}; diff --git a/src/common/transformations/include/transformations/sdpa_to_paged_attention/prev_sequence_length_pattern.hpp b/src/common/transformations/include/transformations/paged_attention/prev_sequence_length_pattern.hpp similarity index 100% rename from src/common/transformations/include/transformations/sdpa_to_paged_attention/prev_sequence_length_pattern.hpp rename to src/common/transformations/include/transformations/paged_attention/prev_sequence_length_pattern.hpp diff --git a/src/common/transformations/include/transformations/paged_attention/state_management_pattern.hpp b/src/common/transformations/include/transformations/paged_attention/state_management_pattern.hpp new file mode 100644 index 000000000000..b80a8ac6adce --- /dev/null +++ b/src/common/transformations/include/transformations/paged_attention/state_management_pattern.hpp @@ -0,0 +1,48 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#pragma once + +#include +#include +#include + +#include "openvino/pass/matcher_pass.hpp" +#include "openvino/pass/sdpa_to_paged_attention.hpp" +#include "transformations_visibility.hpp" + +namespace ov { +namespace pass { + +class TRANSFORMATIONS_API StateManagementPattern; + +} // namespace pass +} // namespace ov + +class ov::pass::StateManagementPattern : public ov::pass::MatcherPass { +public: + OPENVINO_MATCHER_PASS_RTTI("StateManagementPattern"); + + struct KvCacheParams { + std::shared_ptr k; + std::shared_ptr v; + bool write_kv_cache; + }; + + // Maps ReadValue variable_id (+ /k or /v suffix) to the parameter name (key_cache.N or value_cache.N). + using ReadValueToParamMap = std::unordered_map; + + StateManagementPattern(ov::pass::paged_attention::PaParams& pa_params, + ov::pass::paged_attention::PaResults& results, + const ov::pass::paged_attention::Options& options, + std::unordered_set& var_ids_to_remove); + +private: + KvCacheParams find_or_create_kv_params(const std::shared_ptr& k_rv, + const std::shared_ptr& v_rv, + ov::pass::paged_attention::PaParams& pa_params); + + int m_layer_index = 0; + ReadValueToParamMap m_read_value_to_params; +}; diff --git a/src/common/transformations/include/transformations/sdpa_to_paged_attention/total_sequence_length_pattern.hpp b/src/common/transformations/include/transformations/paged_attention/total_sequence_length_pattern.hpp similarity index 100% rename from src/common/transformations/include/transformations/sdpa_to_paged_attention/total_sequence_length_pattern.hpp rename to src/common/transformations/include/transformations/paged_attention/total_sequence_length_pattern.hpp diff --git a/src/common/transformations/include/transformations/rt_info/attributes.hpp b/src/common/transformations/include/transformations/rt_info/attributes.hpp index ddf0953f1ab6..f8114b9c04eb 100644 --- a/src/common/transformations/include/transformations/rt_info/attributes.hpp +++ b/src/common/transformations/include/transformations/rt_info/attributes.hpp @@ -14,7 +14,7 @@ #include "openvino/core/preprocess/input_tensor_info.hpp" #include "transformations/rt_info/decompression.hpp" #include "transformations/rt_info/disable_constant_folding.hpp" -#include "transformations/rt_info/disable_fp16_compression.hpp" +#include "transformations/rt_info/disable_precision_conversion.hpp" #include "transformations/rt_info/fused_names_attribute.hpp" #include "transformations/rt_info/nms_selected_indices.hpp" #include "transformations/rt_info/old_api_map_element_type_attribute.hpp" diff --git a/src/common/transformations/include/transformations/rt_info/disable_fp16_compression.hpp b/src/common/transformations/include/transformations/rt_info/disable_fp16_compression.hpp deleted file mode 100644 index 58a3895b7f60..000000000000 --- a/src/common/transformations/include/transformations/rt_info/disable_fp16_compression.hpp +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright (C) 2018-2026 Intel Corporation -// SPDX-License-Identifier: Apache-2.0 -// - -#pragma once - -#include "openvino/core/node.hpp" -#include "openvino/core/runtime_attribute.hpp" -#include "transformations_visibility.hpp" - -namespace ov { - -TRANSFORMATIONS_API void disable_fp16_compression(const std::shared_ptr& node); - -TRANSFORMATIONS_API void enable_fp16_compression(const std::shared_ptr& node); - -TRANSFORMATIONS_API bool fp16_compression_is_disabled(const std::shared_ptr& node); - -TRANSFORMATIONS_API void postpone_fp16_compression(RTMap& rt_info); - -TRANSFORMATIONS_API bool is_fp16_compression_postponed(const RTMap& rt_info); - -TRANSFORMATIONS_API void do_not_postpone_fp16_compression(RTMap& rt_info); - -/** - * @ingroup ov_runtime_attr_api - * @brief DisableFP16Compression class represents runtime info attribute that marks operation - * as prohibited to convert to lower precision (e.g. to FP16) and they should be inferred precisely in the original - * precision. - */ -class TRANSFORMATIONS_API DisableFP16Compression : public RuntimeAttribute { -public: - OPENVINO_RTTI("precise", "0", RuntimeAttribute); - - DisableFP16Compression() = default; - - bool visit_attributes(AttributeVisitor& visitor) override { - return true; - } - - bool is_copyable() const override { - return false; - } -}; - -} // namespace ov diff --git a/src/common/transformations/include/transformations/rt_info/disable_precision_conversion.hpp b/src/common/transformations/include/transformations/rt_info/disable_precision_conversion.hpp new file mode 100644 index 000000000000..596a9351fca3 --- /dev/null +++ b/src/common/transformations/include/transformations/rt_info/disable_precision_conversion.hpp @@ -0,0 +1,165 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#pragma once + +#include +#include +#include + +#include "openvino/core/attribute_adapter.hpp" +#include "openvino/core/deprecated.hpp" +#include "openvino/core/node.hpp" +#include "openvino/core/runtime_attribute.hpp" +#include "transformations_visibility.hpp" + +namespace ov { + +TRANSFORMATIONS_API void postpone_fp16_compression(RTMap& rt_info); + +TRANSFORMATIONS_API bool is_fp16_compression_postponed(const RTMap& rt_info); + +TRANSFORMATIONS_API void do_not_postpone_fp16_compression(RTMap& rt_info); + +/** + * @ingroup ov_runtime_attr_api + * @brief DisableFP16Compression class represents runtime info attribute that marks operation + * as prohibited to convert to lower precision (e.g. to FP16) and they should be inferred precisely in the original + * precision. + * @deprecated Use disable_conversion(node, element::f16) and is_conversion_disabled(node, element::f16) instead. + */ +class TRANSFORMATIONS_API OPENVINO_DEPRECATED("Use disable_conversion(node, element::f16) instead") + DisableFP16Compression : public RuntimeAttribute { +public: + OPENVINO_RTTI("precise", "0", RuntimeAttribute); + + DisableFP16Compression() = default; + + bool visit_attributes(AttributeVisitor& visitor) override { + return true; + } + + bool is_copyable() const override { + return false; + } +}; + +/** + * @brief Disable precision conversion from any type (dynamic) to the specified type on a @ref Node. + * + * @param node Node to apply the attribute to. + * @param to Target element type to disable conversion to. + */ +TRANSFORMATIONS_API void disable_conversion(const std::shared_ptr& node, const element::Type& to); + +/** + * @brief Disable precision conversion from one type to another on a @ref Node. + * + * @param node Node to apply the attribute to. + * @param from Source element type. + * @param to Target element type. + */ +TRANSFORMATIONS_API void disable_conversion(const std::shared_ptr& node, + const element::Type& from, + const element::Type& to); + +/** + * @brief Disable precision conversion for all combinations of the specified source and target types on a @ref Node. + * + * @param node Node to apply the attribute to. + * @param from_types Source element types. + * @param to_types Target element types. + */ +TRANSFORMATIONS_API void disable_conversion(const std::shared_ptr& node, + const element::TypeVector& from_types, + const element::TypeVector& to_types); + +/** + * @brief Enable precision conversion from any type (dynamic) to the specified type on a @ref Node. + * + * @param node Node to remove the attribute from. + * @param to Target element type to enable conversion to. + */ +TRANSFORMATIONS_API void enable_conversion(const std::shared_ptr& node, const element::Type& to); + +/** + * @brief Enable precision conversion from one type to another on a @ref Node. + * + * @param node Node to remove the attribute from. + * @param from Source element type. + * @param to Target element type. + */ +TRANSFORMATIONS_API void enable_conversion(const std::shared_ptr& node, + const element::Type& from, + const element::Type& to); + +/** + * @brief Enable precision conversion for all combinations of the specified source and target types on a @ref Node. + * + * @param node Node to remove the attribute from. + * @param from_types Source element types. + * @param to_types Target element types. + */ +TRANSFORMATIONS_API void enable_conversion(const std::shared_ptr& node, + const element::TypeVector& from_types, + const element::TypeVector& to_types); + +/** + * @brief Check if precision conversion from any type (dynamic) to the specified type is disabled on a @ref Node. + * + * @param node Node to check. + * @param to Target element type to check. + * @return true if conversion to the given type is disabled, false otherwise. + */ +TRANSFORMATIONS_API bool is_conversion_disabled(const std::shared_ptr& node, const element::Type& to); + +/** + * @brief Check if precision conversion from one type to another is disabled on a @ref Node. + * + * @param node Node to check. + * @param from Source element type. + * @param to Target element type. + * @return true if conversion from the given source to the given target type is disabled, false otherwise. + */ +TRANSFORMATIONS_API bool is_conversion_disabled(const std::shared_ptr& node, + const element::Type& from, + const element::Type& to); + +using DisabledPrecisionMap = std::map>; + +class TRANSFORMATIONS_API DisablePrecisionConversion : public RuntimeAttribute { +public: + OPENVINO_RTTI("DisablePrecisionConversion", "0", RuntimeAttribute); + + DisablePrecisionConversion() = default; + + explicit DisablePrecisionConversion(const element::Type& from, const element::Type& to) { + m_disabled_precisions[from].insert(to); + } + + bool is_copyable() const override { + return false; + } + + bool visit_attributes(AttributeVisitor& visitor) override; + + DisabledPrecisionMap m_disabled_precisions = {}; +}; + +template <> +class TRANSFORMATIONS_API AttributeAdapter : public ValueAccessor { +public: + OPENVINO_RTTI("AttributeAdapter"); + + explicit AttributeAdapter(DisabledPrecisionMap& value) : m_ref(value), m_serialized() {} + + const std::string& get() override; + void set(const std::string& value) override; + +private: + DisabledPrecisionMap& m_ref; + std::string m_serialized; +}; + +} // namespace ov \ No newline at end of file diff --git a/src/common/transformations/include/transformations/sdpa_to_paged_attention/state_management_pattern.hpp b/src/common/transformations/include/transformations/sdpa_to_paged_attention/state_management_pattern.hpp deleted file mode 100644 index e26caf3fd4c8..000000000000 --- a/src/common/transformations/include/transformations/sdpa_to_paged_attention/state_management_pattern.hpp +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright (C) 2018-2026 Intel Corporation -// SPDX-License-Identifier: Apache-2.0 -// - -#pragma once - -#include - -#include "openvino/pass/matcher_pass.hpp" -#include "transformations_visibility.hpp" - -namespace ov { -namespace pass { - -class TRANSFORMATIONS_API StateManagementPattern; - -} // namespace pass -} // namespace ov - -class ov::pass::StateManagementPattern : public ov::pass::MatcherPass { -public: - OPENVINO_MATCHER_PASS_RTTI("StateManagementPattern"); - StateManagementPattern(ParameterVector& kv_parameters, - ParameterVector& model_wide_params, - int& layer_index, - ov::Output max_context_len, - ParameterVector& block_indices_inputs_for_each_layer, - ResultVector& score_results, - bool use_per_layer_block_indices_inputs, - bool use_score_outputs, - bool allow_cache_rotation, - bool allow_score_aggregation, - bool allow_xattention, - bool allow_adaptive_rkv, - bool allow_qq_bias, - ParameterVector& rotated_block_indices_inputs_for_each_layer, - ParameterVector& rotation_deltas_inputs_for_each_layer, - ParameterVector& xattention_threshold_inputs_for_each_layer, - ParameterVector& adaptive_rkv_diversity_block_set_indices_inputs_for_each_layer, - ParameterVector& adaptive_rkv_diversity_block_set_indices_begins_inputs_for_each_layer, - ResultVector& adaptive_rkv_diversity_results, - const std::map>& optional_model_wide_params, - std::unordered_set& var_ids_to_remove); -}; diff --git a/src/common/transformations/include/transformations/utils/extract_subgraph.hpp b/src/common/transformations/include/transformations/utils/extract_subgraph.hpp new file mode 100644 index 000000000000..70384daca609 --- /dev/null +++ b/src/common/transformations/include/transformations/utils/extract_subgraph.hpp @@ -0,0 +1,47 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#pragma once + +#include +#include +#include +#include + +#include "openvino/core/model.hpp" +#include "openvino/core/node.hpp" +#include "transformations_visibility.hpp" + +namespace ov::util { + +/** + * @brief Extracts a subgraph bounded by the given input and output ports as a standalone model. + * The original model is not modified; the extracted model is a deep clone. + * + * @param subgraph_inputs Input ports that form the subgraph boundary. Each port is replaced + * by a new Parameter whose element type and partial shape match the port. + * @param subgraph_outputs Output ports that define the subgraph results. + * @return A new ov::Model representing the extracted subgraph. + */ +TRANSFORMATIONS_API std::shared_ptr extract_subgraph(const std::vector>& subgraph_inputs, + const ov::OutputVector& subgraph_outputs); + +/** + * @brief Extracts a subgraph by resolving boundary ports from node friendly names. + * + * Convenience wrapper over the port-based overload. Each multimap entry maps a node + * friendly name to a port index; multiple entries with the same name select different + * ports of the same node. Throws ov::Exception if any name is not found in the model. + * + * @param model Source model. + * @param subgraph_inputs Map of { friendly_name -> input_port_index } for boundary inputs. + * @param subgraph_outputs Map of { friendly_name -> output_port_index } for boundary outputs. + * @return A new ov::Model representing the extracted subgraph. + */ +TRANSFORMATIONS_API std::shared_ptr extract_subgraph( + const std::shared_ptr& model, + const std::multimap& subgraph_inputs, + const std::multimap& subgraph_outputs); + +} // namespace ov::util diff --git a/src/common/transformations/include/transformations/utils/utils.hpp b/src/common/transformations/include/transformations/utils/utils.hpp index 0023518f1471..9e8b0a6592d4 100644 --- a/src/common/transformations/include/transformations/utils/utils.hpp +++ b/src/common/transformations/include/transformations/utils/utils.hpp @@ -79,6 +79,10 @@ inline bool has_decompression_converts(const std::shared_ptr& f */ float cast_eps_to_float(double eps_d); +inline bool is_scalar_or_single_elem_constant(const std::shared_ptr& constant) { + return constant && shape_size(constant->get_shape()) == 1; +} + template bool get_constant_value(const std::shared_ptr& node, T& value) { auto constant = ov::as_type_ptr(node); @@ -103,8 +107,7 @@ bool has_constant_value(const std::shared_ptr& node, return false; } - const bool is_scalar_or_single_elem = is_scalar(constant->get_shape()) || shape_size(constant->get_shape()) == 1; - if (!is_scalar_or_single_elem) { + if (!is_scalar_or_single_elem_constant(constant)) { return false; } diff --git a/src/common/transformations/src/decompositions/low_precision_dequantize.cpp b/src/common/transformations/src/decompositions/low_precision_dequantize.cpp new file mode 100644 index 000000000000..fa5b4dc9bec0 --- /dev/null +++ b/src/common/transformations/src/decompositions/low_precision_dequantize.cpp @@ -0,0 +1,80 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "openvino/decompositions/low_precision_dequantize.hpp" + +#include "openvino/op/constant.hpp" +#include "openvino/op/convert.hpp" +#include "openvino/op/multiply.hpp" +#include "openvino/op/reshape.hpp" +#include "openvino/op/subtract.hpp" + +namespace ov { +namespace decomposition { + +namespace { + +bool reshape_is_noop(const ov::Output& multiply_out, const ov::Output& output_shape) { + const auto shape_const = ov::as_type_ptr(output_shape.get_node_shared_ptr()); + if (!shape_const) { + return false; + } + const auto& current_shape = multiply_out.get_partial_shape(); + if (current_shape.is_dynamic()) { + return false; + } + const auto target_shape = shape_const->cast_vector(); + const auto current = current_shape.to_shape(); + if (current.size() != target_shape.size()) { + return false; + } + for (size_t i = 0; i < current.size(); ++i) { + if (target_shape[i] != static_cast(current[i])) { + return false; + } + } + return true; +} + +} // namespace + +ov::Output low_precision_dequantize(ov::pass::NodeRegistry& reg, + const ov::Output& x, + const ov::Output& scale, + const ov::Output& zero_point, + const ov::Output& output_shape) { + // Decomposition shape (matches ov::pass::MarkDequantization): + // Multiply(Subtract(Convert(x), zp), scale) [-> Reshape] + // or, when zero_point is empty: + // Multiply(Convert(x), scale) [-> Reshape] + const auto& dst_type = scale.get_element_type(); + ov::Output result = reg.make(x, dst_type); + + if (zero_point.get_node_shared_ptr()) { + ov::Output zp = zero_point; + if (zp.get_element_type() != dst_type) { + zp = reg.make(zp, dst_type); + } + result = reg.make(result, zp); + } + + result = reg.make(result, scale); + + if (output_shape.get_node_shared_ptr() && !reshape_is_noop(result, output_shape)) { + result = reg.make(result, output_shape, /*special_zero=*/false); + } + + return result; +} + +ov::Output low_precision_dequantize(const ov::Output& x, + const ov::Output& scale, + const ov::Output& zero_point, + const ov::Output& output_shape) { + ov::pass::NodeRegistry reg; + return low_precision_dequantize(reg, x, scale, zero_point, output_shape); +} + +} // namespace decomposition +} // namespace ov diff --git a/src/common/transformations/src/decompositions/rms_norm.cpp b/src/common/transformations/src/decompositions/rms_norm.cpp new file mode 100644 index 000000000000..fce8fc5e99a3 --- /dev/null +++ b/src/common/transformations/src/decompositions/rms_norm.cpp @@ -0,0 +1,41 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "openvino/decompositions/rms_norm.hpp" + +#include "openvino/op/add.hpp" +#include "openvino/op/multiply.hpp" +#include "openvino/op/power.hpp" +#include "openvino/op/reduce_mean.hpp" +#include "openvino/op/sqrt.hpp" +#include "utils.hpp" + +namespace ov { +namespace decomposition { + +using detail::typed_scalar; + +ov::Output rms_norm(ov::pass::NodeRegistry& reg, + const ov::Output& x, + const ov::Output& axes, + const ov::Output& eps, + const ov::Output& scale) { + // Decomposition shape: + // y = x * Power(Sqrt(ReduceMean(x^2, axes) + eps), -1) [* scale] + // This exact graph is recognised by ov::pass::RMSFusion. + auto squared = reg.make(x, typed_scalar(reg, x, 2.0f)); + auto mean = reg.make(squared, axes, /*keep_dims=*/true); + auto mean_plus_eps = reg.make(mean, eps); + auto sqrt = reg.make(mean_plus_eps); + auto inv_rms = reg.make(sqrt, typed_scalar(reg, x, -1.0f)); + + ov::Output result = reg.make(x, inv_rms); + if (scale.get_node_shared_ptr()) { + result = reg.make(scale, result); + } + return result; +} + +} // namespace decomposition +} // namespace ov diff --git a/src/common/transformations/src/decompositions/rope.cpp b/src/common/transformations/src/decompositions/rope.cpp new file mode 100644 index 000000000000..aa5d17101fef --- /dev/null +++ b/src/common/transformations/src/decompositions/rope.cpp @@ -0,0 +1,51 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "openvino/decompositions/rope.hpp" + +#include "openvino/op/add.hpp" +#include "openvino/op/concat.hpp" +#include "openvino/op/constant.hpp" +#include "openvino/op/multiply.hpp" +#include "openvino/op/variadic_split.hpp" +#include "utils.hpp" + +namespace ov { +namespace decomposition { + +ov::Output rope(ov::pass::NodeRegistry& reg, + const ov::Output& x, + const ov::Output& cos, + const ov::Output& sin, + int64_t half_head_size) { + // Split x along the last axis into two equal halves of size `half_head_size`. + auto split_axis = reg.make(ov::element::i64, ov::Shape{}, std::vector{-1}); + auto split_lengths = reg.make(ov::element::i64, + ov::Shape{2}, + std::vector{half_head_size, half_head_size}); + auto halves = reg.make(x, split_axis, split_lengths)->outputs(); + const auto& first_half = halves[0]; + const auto& second_half = halves[1]; + + // Core RoPE formula: + // first_ = first_half * cos - second_half * sin + // second_ = second_half * cos + first_half * sin + // The "minus" is expressed as Multiply(-1) + Add (not Subtract) so the + // RoPEFusion pattern matcher recognises it. + auto first_half_mul_cos = reg.make(first_half, cos); + auto second_half_mul_sin = reg.make(second_half, sin); + + auto neg_one = detail::typed_scalar(reg, x, -1.0f); + auto neg_second_half_mul_sin = reg.make(second_half_mul_sin, neg_one); + auto first_part = reg.make(first_half_mul_cos, neg_second_half_mul_sin); + + auto second_half_mul_cos = reg.make(second_half, cos); + auto first_half_mul_sin = reg.make(first_half, sin); + auto second_part = reg.make(second_half_mul_cos, first_half_mul_sin); + + return reg.make(ov::NodeVector{first_part, second_part}, /*axis=*/-1); +} + +} // namespace decomposition +} // namespace ov diff --git a/src/common/transformations/src/decompositions/utils.hpp b/src/common/transformations/src/decompositions/utils.hpp new file mode 100644 index 000000000000..4cb85877c71d --- /dev/null +++ b/src/common/transformations/src/decompositions/utils.hpp @@ -0,0 +1,33 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#pragma once + +#include "openvino/core/node.hpp" +#include "openvino/core/node_output.hpp" +#include "openvino/op/constant.hpp" +#include "openvino/op/convert_like.hpp" +#include "openvino/pass/node_registry.hpp" + +namespace ov { +namespace decomposition { +namespace detail { + +// Create a scalar constant typed as `x` so that pattern matchers in the +// corresponding fusion transformations (which expect a raw `Constant` or +// `Convert(Constant)`, not `ConvertLike`) can recognise the sub-graph. +inline ov::Output typed_scalar(ov::pass::NodeRegistry& reg, + const ov::Output& x, + float value) { + const auto& et = x.get_element_type(); + if (et.is_static() && et != ov::element::dynamic) { + return reg.make(et, ov::Shape{}, std::vector{value}); + } + auto c = reg.make(ov::element::f32, ov::Shape{}, std::vector{value}); + return reg.make(c, x); +} + +} // namespace detail +} // namespace decomposition +} // namespace ov diff --git a/src/common/transformations/src/ov_ops/gather_matmul_compressed.cpp b/src/common/transformations/src/ov_ops/gather_matmul_compressed.cpp index 196a1e12f5fd..661b40e22495 100644 --- a/src/common/transformations/src/ov_ops/gather_matmul_compressed.cpp +++ b/src/common/transformations/src/ov_ops/gather_matmul_compressed.cpp @@ -40,13 +40,6 @@ void GatherMatmulCompressed::validate_and_infer_types() { const auto input_size = get_input_size(); NODE_VALIDATION_CHECK(this, input_size == 6, "Number of inputs is incorrect. Current value is: ", input_size); - // Check weight_scales and weight_zero_points are on constant path - NODE_VALIDATION_CHECK(this, - ov::is_type(get_input_node_shared_ptr(4)), - "Input weight_scales must be a Constant node."); - NODE_VALIDATION_CHECK(this, - ov::is_type(get_input_node_shared_ptr(5)), - "Input weight_zero_points must be a Constant node."); GatherMatmul::validate_and_infer_types(); } } // namespace ov::op::internal diff --git a/src/common/transformations/src/ov_ops/moe_compressed.cpp b/src/common/transformations/src/ov_ops/moe_compressed.cpp index 743d484a4f0d..c59a4bfc58b9 100644 --- a/src/common/transformations/src/ov_ops/moe_compressed.cpp +++ b/src/common/transformations/src/ov_ops/moe_compressed.cpp @@ -20,6 +20,153 @@ void MOECompressed::validate_and_infer_types() { auto output_type = m_config.out_type == ov::element::dynamic ? get_input_element_type(0) : m_config.out_type; set_output_type(0, output_type, get_input_partial_shape(0)); + + if (m_config.expert_type == Expert_type::GEMM2_BIAS_SWIGLU_CLAMP) { + NODE_VALIDATION_CHECK(this, + m_config.activation_type == Activation_type::SWIGLU, + "GEMM2_BIAS_SWIGLU_CLAMP expert type only supports SWIGLU activation"); + } + + // Check that routing_weights and router_topk_output_indices have the same shape + const auto& routing_weights_shape = get_input_partial_shape(1); + const auto& topk_indices_shape = get_input_partial_shape(2); + NODE_VALIDATION_CHECK(this, + routing_weights_shape == topk_indices_shape, + "routing_weights shape ", + routing_weights_shape, + " must be compatible with router_topk_output_indices shape ", + topk_indices_shape); + + // Config carries a single group_size; gate/up/down must share it. + auto check_scale = [&](size_t scale_idx, size_t K, const char* name) { + if (scale_idx >= get_input_size()) + return; + const auto& s = get_input_partial_shape(scale_idx); + OPENVINO_ASSERT(s.is_static() && s.size() >= 3, + "MOECompressed ", + name, + " scale shape must be static rank>=3, got ", + s); + const auto num_groups = static_cast(s[2].get_length()); + OPENVINO_ASSERT(K % num_groups == 0, + "MOECompressed ", + name, + " K=", + K, + " not divisible by scale num_groups=", + num_groups); + const size_t expected_group_size = K / num_groups; + if (m_config.group_size != std::numeric_limits::max()) { + OPENVINO_ASSERT(expected_group_size == m_config.group_size, + "MOECompressed ", + name, + " scale shape implies group_size=", + expected_group_size, + " but config.group_size=", + m_config.group_size, + " (K=", + K, + ", scale_shape=", + s, + ", num_groups=", + num_groups, + ")"); + } else { + OPENVINO_ASSERT(num_groups == 1, + "MOECompressed config.group_size==SIZE_MAX (per-channel) but ", + name, + " scale has num_groups=", + num_groups); + } + }; + auto check_zp = [&](size_t zp_idx, const char* name) { + if (zp_idx >= get_input_size()) + return; + // dynamic-typed zp = symmetric placeholder. + const bool zp_dynamic = get_input_element_type(zp_idx) == ov::element::dynamic; + if (m_config.has_zp) { + OPENVINO_ASSERT(!zp_dynamic, + "MOECompressed config.has_zp=true but ", + name, + " zp input has dynamic element type"); + } else { + OPENVINO_ASSERT(zp_dynamic, + "MOECompressed config.has_zp=false but ", + name, + " zp input has non-dynamic element type"); + } + }; + + // Cross-check config K/num_expert against weight shapes (scale check only proves config-internal coherence). + auto check_weight_K = [&](size_t weight_idx, size_t expected_K, const char* name) { + if (weight_idx >= get_input_size()) + return; + const auto& s = get_input_partial_shape(weight_idx); + OPENVINO_ASSERT( + s.is_static() && (s.size() == 3 || s.size() == 4), + "MOECompressed ", + name, + " weight shape must be static rank-3 [E, ofm, K] or rank-4 [E, ofm, K_groups, group_size], got ", + s); + const size_t actual_K = (s.size() == 4) + ? static_cast(s[2].get_length()) * static_cast(s[3].get_length()) + : static_cast(s[2].get_length()); + OPENVINO_ASSERT(actual_K == expected_K, + "MOECompressed ", + name, + " weight at idx=", + weight_idx, + " has K=", + actual_K, + " (shape=", + s, + ") but config-derived K=", + expected_K); + if (m_config.num_expert > 0) { + OPENVINO_ASSERT(static_cast(s[0].get_length()) == m_config.num_expert, + "MOECompressed ", + name, + " weight num_experts=", + s[0].get_length(), + " disagrees with config.num_expert=", + m_config.num_expert); + } + }; + + // Subclasses (e.g. MOE3GemmFusedCompressed) use different layouts — gate by canonical input count. + const size_t expected_gemm3_inputs = 12; + const size_t expected_gemm2_no_zp = 9; + const size_t expected_gemm2_with_zp = 11; + + if (m_config.expert_type == ov::op::internal::MOE::Expert_type::GEMM3_SWIGLU && + get_input_size() == expected_gemm3_inputs) { + // hidden, routing, topk, gate_{w,scale,zp}, up_{w,scale,zp}, down_{w,scale,zp} [, shared...] + check_weight_K(3, m_config.hidden_size, "GEMM3 gate"); + check_weight_K(6, m_config.hidden_size, "GEMM3 up"); + check_weight_K(9, m_config.inter_size, "GEMM3 down"); + check_scale(4, m_config.hidden_size, "GEMM3 gate"); + check_scale(7, m_config.hidden_size, "GEMM3 up"); + check_scale(10, m_config.inter_size, "GEMM3 down"); + check_zp(5, "GEMM3 gate"); + check_zp(8, "GEMM3 up"); + check_zp(11, "GEMM3 down"); + } else if (m_config.expert_type == ov::op::internal::MOE::Expert_type::GEMM2_BIAS_SWIGLU_CLAMP && + (get_input_size() == expected_gemm2_no_zp || get_input_size() == expected_gemm2_with_zp)) { + // hidden, routing, topk, gate_up_{w,scale,[zp,]bias}, down_{w,scale,[zp,]bias}. + // inter_size is the fused gate_up dim; down K = inter_size / 2. + const size_t gate_up_w_idx = 3; + const size_t gate_up_scale_idx = 4; + const size_t gate_up_zp_idx = m_config.has_zp ? 5 : SIZE_MAX; + const size_t down_w_idx = m_config.has_zp ? 7 : 6; + const size_t down_scale_idx = m_config.has_zp ? 8 : 7; + const size_t down_zp_idx = m_config.has_zp ? 9 : SIZE_MAX; + check_weight_K(gate_up_w_idx, m_config.hidden_size, "GEMM2 gate_up"); + check_weight_K(down_w_idx, m_config.inter_size / 2, "GEMM2 down"); + check_scale(gate_up_scale_idx, m_config.hidden_size, "GEMM2 gate_up"); + check_scale(down_scale_idx, m_config.inter_size / 2, "GEMM2 down"); + check_zp(gate_up_zp_idx, "GEMM2 gate_up"); + check_zp(down_zp_idx, "GEMM2 down"); + } } bool MOECompressed::visit_attributes(ov::AttributeVisitor& visitor) { @@ -33,26 +180,9 @@ bool MOECompressed::visit_attributes(ov::AttributeVisitor& visitor) { visitor.on_attribute("has_batch_dim", m_config.has_batch_dim); visitor.on_attribute("has_zp", m_config.has_zp); visitor.on_attribute("out_type", m_config.out_type); - visitor.on_attribute("routing_type", m_config.routing_type); + if (m_config.scale_factor.has_value()) + visitor.on_attribute("scale_factor", m_config.scale_factor.value()); return true; } -std::ostream& operator<<(std::ostream& s, const MOECompressed::RoutingType& type) { - return s << as_string(type); -} - } // namespace ov::op::internal - -namespace ov { -using RoutingType = ov::op::internal::MOECompressed::RoutingType; -template <> -EnumNames& EnumNames::get() { - static auto enum_names = EnumNames("MOECompressed::RoutingType", - { - {"softmax", RoutingType::SOFTMAX}, - {"sigmoid_bias", RoutingType::SIGMOID_BIAS}, - }); - return enum_names; -} - -} // namespace ov diff --git a/src/common/transformations/src/transformations/common_optimizations/activations_scaling.cpp b/src/common/transformations/src/transformations/common_optimizations/activations_scaling.cpp index c4805044a85b..06f275f87293 100644 --- a/src/common/transformations/src/transformations/common_optimizations/activations_scaling.cpp +++ b/src/common/transformations/src/transformations/common_optimizations/activations_scaling.cpp @@ -28,6 +28,7 @@ #include "openvino/pass/pattern/op/optional.hpp" #include "openvino/pass/pattern/op/or.hpp" #include "openvino/pass/pattern/op/wrap_type.hpp" +#include "ov_ops/moe_compressed.hpp" #include "ov_ops/rms.hpp" #include "transformations/common_optimizations/lin_op_sequence_fusion.hpp" #include "transformations/utils/utils.hpp" @@ -60,28 +61,28 @@ activations_scaling::ScaleDownSingleLayer::ScaleDownSingleLayer(float scale_fact auto weights_m = pattern::any_input(); auto convolution_m = pattern::wrap_type({activation_m, weights_m}); auto matmul_m = pattern::wrap_type({activation_m, weights_m}); - auto scaled_op_m = std::make_shared(OutputVector{convolution_m, matmul_m}); + auto moe_m = ov::pass::pattern::wrap_type(); + auto scaled_op_m = std::make_shared(OutputVector{convolution_m, matmul_m, moe_m}); ov::matcher_pass_callback callback = [OV_CAPTURE_CPY_AND_THIS](pattern::Matcher& m) { const auto& pattern_map = m.get_pattern_value_map(); - OPENVINO_ASSERT(pattern_map.count(convolution_m) || pattern_map.count(matmul_m), - "Not found any Convolution or MatMul layer"); + OPENVINO_ASSERT(pattern_map.count(convolution_m) || pattern_map.count(matmul_m) || pattern_map.count(moe_m), + "Not found any Convolution, MatMul or MOECompressed layer"); - auto insert_scale_down_layer = [&scale_factor, &scaled_prec](std::shared_ptr& node, - const size_t input_idx) { + auto insert_scale_down_layer = [&](std::shared_ptr& node, const size_t input_idx) { const std::vector scale_down_value = {1.f / scale_factor}; + const auto src = node->input(input_idx).get_source_output(); - auto scale_down_layer = - std::make_shared(node->input(input_idx).get_source_output(), - std::make_shared(node->input(input_idx).get_element_type(), - ov::Shape(), - scale_down_value)); - scale_down_layer->set_friendly_name(node->get_friendly_name() + "_scale_down"); + auto scale_const = + register_new_node(src.get_element_type(), ov::Shape(), scale_down_value); + auto scale_down_layer = register_new_node(src, scale_const); + scale_down_layer->set_friendly_name(node->get_friendly_name() + "_scale_down_in" + + std::to_string(input_idx)); ov::copy_runtime_info(node, scale_down_layer); if (scale_down_layer->output(0).get_element_type() != scaled_prec) { - auto convert_prec = std::make_shared(scale_down_layer->output(0), scaled_prec); + auto convert_prec = register_new_node(scale_down_layer->output(0), scaled_prec); node->input(input_idx).replace_source_output(convert_prec->output(0)); } else { node->input(input_idx).replace_source_output(scale_down_layer->output(0)); @@ -97,6 +98,28 @@ activations_scaling::ScaleDownSingleLayer::ScaleDownSingleLayer(float scale_fact if (pattern_map.count(matmul_m)) scaled_op = pattern_map.at(matmul_m).get_node_shared_ptr(); + bool is_moe = false; + if (pattern_map.count(moe_m)) { + scaled_op = pattern_map.at(moe_m).get_node_shared_ptr(); + auto moe_op = ov::as_type_ptr(scaled_op); + if (!moe_op) + return false; + + if (moe_op->get_config().expert_type != ov::op::internal::MOE::Expert_type::GEMM2_BIAS_SWIGLU_CLAMP) + return false; + + OPENVINO_ASSERT(moe_op->get_input_size() == 9 || moe_op->get_input_size() == 11, + "Unexpected input size for MOECompressed: ", + moe_op->get_input_size()); + + const size_t bias_up_idx = moe_op->get_config().has_zp ? 6u : 5u; + const size_t bias_down_idx = moe_op->get_input_size() - 1u; + insert_scale_down_layer(scaled_op, bias_up_idx); + insert_scale_down_layer(scaled_op, bias_down_idx); + moe_op->set_scale_factor(scale_factor); + is_moe = true; + } + if (transformation_callback(scaled_op)) return false; @@ -104,7 +127,7 @@ activations_scaling::ScaleDownSingleLayer::ScaleDownSingleLayer(float scale_fact std::shared_ptr output_of_scaled_op = scaled_op; auto child_node = scaled_op->get_output_target_inputs(0).begin()->get_node(); if (scaled_op->get_output_target_inputs(0).size() == 1 && ov::is_type(child_node) && - ov::fp16_compression_is_disabled(child_node->shared_from_this()) && + ov::is_conversion_disabled(child_node->shared_from_this(), element::f16) && constant_folding_is_disabled(child_node->shared_from_this())) { output_of_scaled_op = child_node->shared_from_this(); } @@ -114,7 +137,7 @@ activations_scaling::ScaleDownSingleLayer::ScaleDownSingleLayer(float scale_fact // adding a scale_down layer before the target node insert_scale_down_layer(scaled_op, 0); - if (scaled_op->input(1).get_element_type() != scaled_prec) { + if (!is_moe && scaled_op->input(1).get_element_type() != scaled_prec) { auto convert_prec1 = std::make_shared(scaled_op->input(1).get_source_output(), scaled_prec); scaled_op->input(1).replace_source_output(convert_prec1->output(0)); } @@ -127,7 +150,7 @@ activations_scaling::ScaleDownSingleLayer::ScaleDownSingleLayer(float scale_fact // So, we need to scale_down the bias layer too. bool has_bias = false; size_t bias_index = 1; - { + if (!is_moe) { if (scaled_op->get_output_target_inputs(0).size() == 1 && ov::is_type(child_node)) { bias_index = (child_node->get_input_node_shared_ptr(0) == scaled_op) ? 1 : 0; const auto& bias_pshape = child_node->get_input_partial_shape(bias_index); diff --git a/src/common/transformations/src/transformations/common_optimizations/common_optimizations.cpp b/src/common/transformations/src/transformations/common_optimizations/common_optimizations.cpp index bf2697972a06..76f0d9a42ac4 100644 --- a/src/common/transformations/src/transformations/common_optimizations/common_optimizations.cpp +++ b/src/common/transformations/src/transformations/common_optimizations/common_optimizations.cpp @@ -26,6 +26,7 @@ #include "transformations/common_optimizations/eliminate_unsqueeze_gather.hpp" #include "transformations/common_optimizations/fq_mul_fusion.hpp" #include "transformations/common_optimizations/fq_reshape_fusion.hpp" +#include "transformations/common_optimizations/fuse_clamp_and_fake_quantize.hpp" #include "transformations/common_optimizations/gelu_fusion.hpp" #include "transformations/common_optimizations/hsigmoid_fusion.hpp" #include "transformations/common_optimizations/hswish_fusion.hpp" @@ -249,6 +250,7 @@ bool ov::pass::CommonOptimizations::run_on_model(const std::shared_ptrset_name("ov::pass::FakeQuantizeFusions"); diff --git a/src/common/transformations/src/transformations/common_optimizations/compress_float_constants.cpp b/src/common/transformations/src/transformations/common_optimizations/compress_float_constants.cpp index b27f20a9105b..9fed06b0e2d4 100644 --- a/src/common/transformations/src/transformations/common_optimizations/compress_float_constants.cpp +++ b/src/common/transformations/src/transformations/common_optimizations/compress_float_constants.cpp @@ -22,8 +22,9 @@ #include "openvino/reference/convert.hpp" #include "transformations/common_optimizations/mark_precision_sensitive_shapeof_subgraphs.hpp" #include "transformations/rt_info/decompression.hpp" -#include "transformations/rt_info/disable_fp16_compression.hpp" +#include "transformations/rt_info/disable_precision_conversion.hpp" #include "transformations/rt_info/old_api_map_element_type_attribute.hpp" +#include "transformations/utils/utils.hpp" namespace v0 = ov::op::v0; namespace v1 = ov::op::v1; @@ -32,11 +33,34 @@ namespace ov::pass { namespace { +/// Slow (non-JIT) FP32/FP64 -> FP16 compression for a single Constant node. +/// +/// Returns either: +/// * the new `v0::Constant` with FP16 data (fully converted and approved), or +/// * `constant` unchanged when `postponed == true` (the Constant is kept in the +/// graph and compressed later during serialization), or +/// * `nullptr` if the tensor must stay in its original precision. +/// +/// The tensor is rejected (returns `nullptr`) when: +/// * any in-range element has absolute round-trip error above +/// `f16_compression_max_abs_error` (RoPE-like high-magnitude values), or +/// * the combined count of elements outside the finite FP16 range *plus* +/// elements whose relative round-trip error exceeds +/// the selected relative-error threshold reaches +/// `f16_compression_keep_threshold` (75% by default). +/// +/// Used by `CompressFloatConstantsImpl` for f64 constants on all platforms and +/// for both f32/f64 on non-x86 architectures where the JIT fast-path +/// `ov::reference::check_f16_compression` is unavailable. template std::shared_ptr change_constant_precision_to_fp16(std::shared_ptr& constant, bool postponed = false) { using src_type = typename ov::element_type_traits::value_type; + const bool is_scalar_or_single_elem = ov::op::util::is_scalar_or_single_elem_constant(constant); + const double max_relative_error = is_scalar_or_single_elem ? ov::reference::f16_scalar_compression_max_rel_error + : ov::reference::f16_tensor_compression_max_rel_error; + const auto* src_data = constant->get_data_ptr(); const auto size = ov::shape_size(constant->get_shape()); @@ -45,28 +69,43 @@ std::shared_ptr change_constant_precision_to_fp16(std::shared_ptr(src_data[i]); + num_rejected++; } else if (src_data[i] > std::numeric_limits::max()) { dst_data[i] = std::numeric_limits::max(); - num_out_of_range++; + num_rejected++; } else if (src_data[i] < std::numeric_limits::lowest()) { dst_data[i] = std::numeric_limits::lowest(); - num_out_of_range++; + num_rejected++; } else { - dst_data[i] = static_cast(src_data[i]); + constexpr double max_abs_error = ov::reference::f16_compression_max_abs_error; + const auto f16_val = static_cast(src_data[i]); + const double src_val = static_cast(src_data[i]); + const double roundtripped = static_cast(static_cast(f16_val)); + const double abs_diff = std::abs(src_val - roundtripped); + + if (abs_diff > max_abs_error) { + return nullptr; + } + + if (src_val != 0.0 && abs_diff / std::abs(src_val) > max_relative_error) { + num_rejected++; + } + dst_data[i] = f16_val; } } - // if more than 75% of a FP32 constant do not fit into FP16 keep in FP32 - const float keep_threshold = 0.75f; - const float out_of_range_proportion = static_cast(num_out_of_range) / static_cast(size); + // If the combined count of rejected elements (values outside the finite FP16 + // range PLUS in-range values whose FP16 round-trip relative error exceeds + // the selected relative-error threshold) reaches f16_compression_keep_threshold + // (inclusive, 75% by default), keep the Constant in its original precision. + const double rejected_proportion = static_cast(num_rejected) / static_cast(size); - if (out_of_range_proportion >= keep_threshold) { + if (rejected_proportion >= ov::reference::f16_compression_keep_threshold) { return nullptr; } @@ -138,27 +177,6 @@ void compress_model_to_f16(const std::shared_ptr& model, bool postponed) } } -namespace { - -// Returns true if a scalar constant of type T loses significant precision when rounded to FP16. -// Used to protect mathematical scale factors (e.g., log(16) in attention bucketing) from FP16 -// rounding errors that cascade through every computation referencing them. -template -bool scalar_has_high_f16_error(const ov::op::v0::Constant& const_node) { - constexpr double max_relative_error = 1e-4; - static_assert(sizeof(T) >= 4); - const T src = *const_node.get_data_ptr(); - if (std::isfinite(src) && src != T{0}) { - const double src_val = static_cast(src); - const ov::float16 f16_val = static_cast(src); - const double roundtripped = static_cast(static_cast(f16_val)); - return std::abs(src_val - roundtripped) / std::abs(src_val) > max_relative_error; - } - return false; -} - -} // namespace - CompressFloatConstantsImpl::CompressFloatConstantsImpl(bool postponed) { MATCHER_SCOPE(CompressFloatConstantsImpl); auto const_pattern = pattern::wrap_type(); @@ -171,21 +189,11 @@ CompressFloatConstantsImpl::CompressFloatConstantsImpl(bool postponed) { if (!const_node) return false; - if (ov::fp16_compression_is_disabled(const_node)) + if (ov::is_conversion_disabled(const_node, element::f16)) return false; auto c_type = const_node->get_element_type(); - // Skip FP16 compression for scalar constants with significant rounding error. - // Scalar constants often serve as mathematical scale factors (e.g., log(16) in attention - // bucketing) where FP16 rounding error cascades through every computation that uses them. - if (ov::shape_size(const_node->get_shape()) == 1) { - if (c_type == ov::element::f32 && scalar_has_high_f16_error(*const_node)) - return false; - if (c_type == ov::element::f64 && scalar_has_high_f16_error(*const_node)) - return false; - } - std::shared_ptr new_const; #if !defined(OPENVINO_ARCH_X86) && !defined(OPENVINO_ARCH_X86_64) @@ -201,13 +209,19 @@ CompressFloatConstantsImpl::CompressFloatConstantsImpl(bool postponed) { auto size = shape_size(const_node->get_output_shape(0)); if (size == 0) return false; - auto num_out_of_range = - ov::reference::count_out_of_f16_range(const_node->get_data_ptr(), size); + const auto* src_data = const_node->get_data_ptr(); + + // Single-pass check: counts out-of-range and bails on any lossy element (JIT accelerated) + auto check = ov::reference::check_f16_compression(src_data, size); + if (check.has_lossy) + return false; - // if more than 75% of a FP32 constant do not fit into FP16 keep in FP32 - const float keep_threshold = 0.75f; - const float out_of_range_proportion = static_cast(num_out_of_range) / static_cast(size); - if (out_of_range_proportion >= keep_threshold) + // If the combined count of rejected elements (values outside the finite FP16 + // range PLUS in-range values whose FP16 round-trip relative error exceeds + // the selected relative-error threshold) reaches f16_compression_keep_threshold + // (inclusive, 75% by default), keep the Constant in FP32. + const float rejected_proportion = static_cast(check.rejected_count) / static_cast(size); + if (rejected_proportion >= ov::reference::f16_compression_keep_threshold) return false; if (postponed) { diff --git a/src/common/transformations/src/transformations/common_optimizations/convert_pagedattn_inputs.cpp b/src/common/transformations/src/transformations/common_optimizations/convert_pagedattn_inputs.cpp deleted file mode 100644 index 403ae25362b7..000000000000 --- a/src/common/transformations/src/transformations/common_optimizations/convert_pagedattn_inputs.cpp +++ /dev/null @@ -1,184 +0,0 @@ -// Copyright (C) 2018-2026 Intel Corporation -// SPDX-License-Identifier: Apache-2.0 -// - -#include "transformations/common_optimizations/convert_pagedattn_inputs.hpp" - -#include -#include - -#include "itt.hpp" -#include "openvino/core/rt_info.hpp" -#include "openvino/op/add.hpp" -#include "openvino/op/constant.hpp" -#include "openvino/op/paged_attention.hpp" -#include "openvino/pass/pattern/op/wrap_type.hpp" -#include "openvino/util/log.hpp" -#include "transformations/utils/utils.hpp" - -using namespace ov::pass; - -namespace v0 = ov::op::v0; - -namespace ov::pass { - -ConvertPagedAttnInputs::ConvertPagedAttnInputs(const KVCacheConfig& config, - UpdateShapeFunc func, - UpdatePrecisionFunc update_precision_func) - : m_config(config), - m_update_shape_func(std::move(func)), - m_update_precision_func(std::move(update_precision_func)) { - MATCHER_SCOPE(ConvertPagedAttnInputs); - - auto Q = pattern::any_input(pattern::has_static_rank()); - auto K = pattern::any_input(pattern::has_static_rank()); - auto V = pattern::any_input(pattern::has_static_rank()); - auto key_cache_0 = pattern::wrap_type({}); - auto value_cache_0 = pattern::wrap_type({}); - auto past_lens = pattern::any_input(pattern::has_static_rank()); - auto subsequence_begins = pattern::any_input(pattern::has_static_rank()); - auto block_indices = pattern::any_input(pattern::has_static_rank()); - auto block_indices_begins = pattern::any_input(pattern::has_static_rank()); - auto scale = pattern::any_input(pattern::has_static_rank()); - auto sliding_window = pattern::any_input(pattern::has_static_rank()); - auto alibi_slopes = pattern::any_input(pattern::has_static_rank()); - auto max_context_len = pattern::any_input(pattern::has_static_rank()); - auto score_aggregation_window = pattern::any_input(pattern::has_static_rank()); - auto rotated_block_indices = pattern::any_input(pattern::has_static_rank()); - auto rotation_deltas = pattern::any_input(pattern::has_static_rank()); - auto rotation_trig_lut = pattern::any_input(pattern::has_static_rank()); - auto xattention_threshold = pattern::any_input(pattern::has_static_rank()); - auto xattention_block_size = pattern::any_input(pattern::has_static_rank()); - auto xattention_stride = pattern::any_input(pattern::has_static_rank()); - auto sinks = pattern::any_input(pattern::has_static_rank()); - auto adaptive_rkv_start_size = pattern::any_input(pattern::has_static_rank()); - auto adaptive_rkv_evictable_sizes = pattern::any_input(pattern::has_static_rank()); - auto adaptive_rkv_diversity_block_set_indices = pattern::any_input(pattern::has_static_rank()); - auto adaptive_rkv_diversity_block_set_indices_begins = pattern::any_input(pattern::has_static_rank()); - auto token_type_ids = pattern::any_input(pattern::has_static_rank()); - - auto qq_bias = pattern::any_input(pattern::has_static_rank()); - auto qq_bias_begins = pattern::any_input(pattern::has_static_rank()); - auto result = pattern::wrap_type({Q, - K, - V, - key_cache_0, - value_cache_0, - past_lens, - subsequence_begins, - block_indices, - block_indices_begins, - scale, - sliding_window, - alibi_slopes, - max_context_len, - score_aggregation_window, - rotated_block_indices, - rotation_deltas, - rotation_trig_lut, - xattention_threshold, - xattention_block_size, - xattention_stride, - sinks, - adaptive_rkv_start_size, - adaptive_rkv_evictable_sizes, - adaptive_rkv_diversity_block_set_indices, - adaptive_rkv_diversity_block_set_indices_begins, - token_type_ids, - qq_bias, - qq_bias_begins}); - ov::matcher_pass_callback callback = [OV_CAPTURE_CPY_AND_THIS](pattern::Matcher& m) { - const auto pa_op = m.get_match_root(); - auto key_cache = ov::as_type_ptr(pa_op->get_input_node_shared_ptr(3)); - auto value_cache = ov::as_type_ptr(pa_op->get_input_node_shared_ptr(4)); - auto format_cache_precision = [](ov::element::Type cache_precision, ov::element::Type infer_precision) { - return cache_precision == ov::element::f16 && infer_precision == ov::element::bf16 ? infer_precision - : cache_precision; - }; - - auto init_cache_shape = [&](const size_t head_nums, - const size_t head_size, - const size_t block_size, - const ov::element::Type precision, - const size_t group_size, - const bool bychannel, - const std::vector& orders) { - ov::Dimension::value_type _block_size = block_size; - ov::Dimension::value_type _head_nums = head_nums; - ov::Dimension::value_type _head_size = head_size; - ov::Dimension::value_type _group_size = group_size; - _group_size = _group_size ? _group_size : _head_size; - if (!bychannel) { - if (_head_size % _group_size != 0) { - OPENVINO_THROW("cache head_size ", head_size, "cannot be divided by group_size ", group_size); - } - } - size_t group_num = _head_size / _group_size; - // Update head_size and block_size by precision and quantizing channel mode - m_update_shape_func(precision, bychannel, group_num, _head_size, _block_size); - - auto block_shape = ov::PartialShape::dynamic(4); - block_shape[orders[0]] = -1; - block_shape[orders[1]] = _head_nums; - block_shape[orders[2]] = _block_size; - block_shape[orders[3]] = _head_size; - - return block_shape; - }; - auto key_cache_precision = format_cache_precision(m_config.keyCachePrecision, m_config.inferencePrecision); - auto value_cache_precision = format_cache_precision(m_config.valueCachePrecision, m_config.inferencePrecision); - key_cache->set_element_type(key_cache_precision); - value_cache->set_element_type(value_cache_precision); - bool status = false; - if (pa_op->get_rt_info().count("num_k_heads") && pa_op->get_rt_info().count("k_head_size") && - pa_op->get_rt_info().count("num_v_heads") && pa_op->get_rt_info().count("v_head_size")) { - const auto key_cache_shape = init_cache_shape(pa_op->get_rt_info()["num_k_heads"].as(), - pa_op->get_rt_info()["k_head_size"].as(), - m_config.keyCacheBlockSize, - key_cache_precision, - m_config.keyCacheGroupSize, - m_config.keyCacheQuantBychannel, - m_config.keyCacheDimOrder); - const auto value_cache_shape = init_cache_shape(pa_op->get_rt_info()["num_v_heads"].as(), - pa_op->get_rt_info()["v_head_size"].as(), - m_config.valueCacheBlockSize, - value_cache_precision, - m_config.valueCacheGroupSize, - m_config.valueCacheQuantBychannel, - m_config.valueCacheDimOrder); - - key_cache->set_partial_shape(key_cache_shape); - value_cache->set_partial_shape(value_cache_shape); - status = true; - } else { - OPENVINO_DEBUG("PagedAttn ", - pa_op->get_friendly_name(), - " doesn't have rtinfo for num_k_heads/k_head_size/num_v_heads/num_v_heads"); - status = false; - } - - if (m_update_precision_func) { - m_update_precision_func(key_cache_precision); - m_update_precision_func(value_cache_precision); - key_cache->set_element_type(key_cache_precision); - value_cache->set_element_type(value_cache_precision); - } - - key_cache->validate_and_infer_types(); - value_cache->validate_and_infer_types(); - return status; - }; - - auto m = std::make_shared(result, matcher_name); - this->register_matcher(m, callback); -} - -void ConvertPagedAttnInputs::setKVCacheConfig(const KVCacheConfig& config) { - m_config = config; -} - -const ConvertPagedAttnInputs::KVCacheConfig& ConvertPagedAttnInputs::getKVCacheConfig() const { - return m_config; -} - -} // namespace ov::pass diff --git a/src/common/transformations/src/transformations/common_optimizations/convert_tiled_moe_block_to_gather_matmuls.cpp b/src/common/transformations/src/transformations/common_optimizations/convert_tiled_moe_block_to_gather_matmuls.cpp index 900771a263d4..96867c048e78 100644 --- a/src/common/transformations/src/transformations/common_optimizations/convert_tiled_moe_block_to_gather_matmuls.cpp +++ b/src/common/transformations/src/transformations/common_optimizations/convert_tiled_moe_block_to_gather_matmuls.cpp @@ -12,6 +12,7 @@ #include "openvino/op/add.hpp" #include "openvino/op/clamp.hpp" #include "openvino/op/constant.hpp" +#include "openvino/op/gelu.hpp" #include "openvino/op/matmul.hpp" #include "openvino/op/minimum.hpp" #include "openvino/op/multiply.hpp" @@ -27,6 +28,7 @@ #include "openvino/pass/pattern/op/optional.hpp" #include "openvino/pass/pattern/op/wrap_type.hpp" #include "ov_ops/gather_matmul.hpp" +#include "transformations/pattern_blocks/compressed_weights_block.hpp" namespace { using namespace ov::pass; @@ -35,6 +37,7 @@ namespace v0 = ov::op::v0; namespace v1 = ov::op::v1; namespace v3 = ov::op::v3; namespace v4 = ov::op::v4; +namespace v7 = ov::op::v7; namespace v8 = ov::op::v8; namespace v12 = ov::op::v12; @@ -46,6 +49,14 @@ void validate_nodes(const pattern::PatternValueMap& map, const std::initializer_ } }; +// Build a per-expert MatMul weight pattern +std::shared_ptr make_expert_weight_pattern(const std::vector& supported_weights_types) { + if (supported_weights_types.empty()) { + return pattern::any_input(); + } + return std::make_shared(supported_weights_types, std::set{3}); +} + std::shared_ptr introduce_n_experts_dim(const ov::Output& data) { auto zero_const = std::make_shared(ov::element::i32, ov::Shape{}, 0); auto unsqueeze = std::make_shared(data, zero_const); @@ -66,42 +77,45 @@ struct MOE2GEMMPatternNodes { std::shared_ptr mul3, reduce_sum; }; -MOE2GEMMPatternNodes build_2gemm_pattern() { +MOE2GEMMPatternNodes build_2gemm_pattern(const std::vector& supported_weights_types) { MOE2GEMMPatternNodes p; p.experts_input = pattern::wrap_type({pattern::any_input(), pattern::any_input()}); - p.tile = pattern::wrap_type({p.experts_input, pattern::any_input()}); - p.after_tile_reshape = pattern::wrap_type({p.tile, pattern::any_input()}); + p.tile = pattern::wrap_type({p.experts_input, pattern::any_input()}, pattern::consumers_count(1)); + p.after_tile_reshape = pattern::wrap_type({p.tile, pattern::any_input()}, pattern::consumers_count(1)); p.gate_up_matmul = pattern::wrap_type( - {p.after_tile_reshape, pattern::any_input()}, + {p.after_tile_reshape, make_expert_weight_pattern(supported_weights_types)}, pattern::consumers_count(1) && pattern::attrs_match({{"transpose_a", false}, {"transpose_b", true}})); p.gate_up_bias = pattern::wrap_const(); p.gate_up_add = pattern::wrap_type({p.gate_up_matmul, p.gate_up_bias}, pattern::consumers_count(2)); // Branch 1: Slice_1 -> Clamp -> Add_1 p.slice1 = pattern::wrap_type( - {p.gate_up_add, pattern::any_input(), pattern::any_input(), pattern::any_input(), pattern::any_input()}); - p.clamp = pattern::wrap_type({p.slice1}); - p.add1 = pattern::wrap_type({p.clamp, pattern::wrap_const()}); + {p.gate_up_add, pattern::any_input(), pattern::any_input(), pattern::any_input(), pattern::any_input()}, + pattern::consumers_count(1)); + p.clamp = pattern::wrap_type({p.slice1}, pattern::consumers_count(1)); + p.add1 = pattern::wrap_type({p.clamp, pattern::wrap_const()}, pattern::consumers_count(1)); // Branch 2: Slice_2 -> Minimum_1 -> Swish p.slice2 = pattern::wrap_type( - {p.gate_up_add, pattern::any_input(), pattern::any_input(), pattern::any_input(), pattern::any_input()}); - p.minimum1 = pattern::wrap_type({p.slice2, pattern::wrap_const()}); + {p.gate_up_add, pattern::any_input(), pattern::any_input(), pattern::any_input(), pattern::any_input()}, + pattern::consumers_count(1)); + p.minimum1 = pattern::wrap_type({p.slice2, pattern::wrap_const()}, pattern::consumers_count(1)); p.swish_beta = pattern::wrap_const(); - p.swish = pattern::wrap_type({p.minimum1, p.swish_beta}); + p.swish = pattern::wrap_type({p.minimum1, p.swish_beta}, pattern::consumers_count(1)); // Join: Multiply_2 - p.multiply2 = pattern::wrap_type({p.add1, p.swish}); + p.multiply2 = pattern::wrap_type({p.add1, p.swish}, pattern::consumers_count(1)); // Down projection p.down_proj_matmul = pattern::wrap_type( - {p.multiply2, pattern::any_input()}, + {p.multiply2, make_expert_weight_pattern(supported_weights_types)}, pattern::consumers_count(1) && pattern::attrs_match({{"transpose_a", false}, {"transpose_b", true}})); p.down_proj_bias = pattern::wrap_const(); - p.down_proj_add = pattern::wrap_type({p.down_proj_matmul, p.down_proj_bias}); + p.down_proj_add = pattern::wrap_type({p.down_proj_matmul, p.down_proj_bias}, pattern::consumers_count(1)); p.end_reshape_target_shape = pattern::any_input(); - p.end_reshape = pattern::wrap_type({p.down_proj_add, p.end_reshape_target_shape}); + p.end_reshape = + pattern::wrap_type({p.down_proj_add, p.end_reshape_target_shape}, pattern::consumers_count(1)); // Routing weights/mask p.router_topk_indices = pattern::any_input(); @@ -113,8 +127,10 @@ MOE2GEMMPatternNodes build_2gemm_pattern() { p.router_reshape = pattern::wrap_type({p.router_transpose, pattern::any_input()}); p.optional_unsqueeze = pattern::optional({p.router_reshape, pattern::any_input()}); - p.mul3 = pattern::wrap_type({p.end_reshape, p.optional_unsqueeze}); - p.reduce_sum = pattern::wrap_type({p.mul3, pattern::any_input()}, {{"keep_dims", false}}); + p.mul3 = pattern::wrap_type({p.end_reshape, p.optional_unsqueeze}, pattern::consumers_count(1)); + p.reduce_sum = pattern::wrap_type({p.mul3, pattern::any_input()}, + pattern::consumers_count(1), + {{"keep_dims", false}}); return p; } @@ -129,31 +145,32 @@ struct MOE3GEMMPatternNodes { std::shared_ptr mul3, reduce_sum; }; -MOE3GEMMPatternNodes build_3gemm_pattern() { +MOE3GEMMPatternNodes build_3gemm_pattern(const std::vector& supported_weights_types) { MOE3GEMMPatternNodes p; - p.experts_input = pattern::wrap_type({pattern::any_input(), pattern::any_input()}); - p.tile = pattern::wrap_type({p.experts_input, pattern::any_input()}); - p.after_tile_reshape = pattern::wrap_type({p.tile, pattern::any_input()}); + p.experts_input = pattern::any_input(); + p.tile = pattern::wrap_type({p.experts_input, pattern::any_input()}, pattern::consumers_count(1)); + p.after_tile_reshape = pattern::wrap_type({p.tile, pattern::any_input()}, pattern::consumers_count(2)); // First GEMM (activation gate) p.gate_matmul = pattern::wrap_type( - {p.after_tile_reshape, pattern::any_input()}, + {p.after_tile_reshape, make_expert_weight_pattern(supported_weights_types)}, pattern::consumers_count(1) && pattern::attrs_match({{"transpose_a", false}, {"transpose_b", true}})); - p.swish = pattern::wrap_type({p.gate_matmul}); + p.swish = pattern::wrap_type({p.gate_matmul}, pattern::consumers_count(1)); // Second GEMM (up_projection) p.up_matmul = pattern::wrap_type( - {p.after_tile_reshape, pattern::any_input()}, + {p.after_tile_reshape, make_expert_weight_pattern(supported_weights_types)}, pattern::consumers_count(1) && pattern::attrs_match({{"transpose_a", false}, {"transpose_b", true}})); // Join: Multiply (SwiGLU) - p.swiglu = pattern::wrap_type({p.swish, p.up_matmul}); + p.swiglu = pattern::wrap_type({p.swish, p.up_matmul}, pattern::consumers_count(1)); // Third GEMM (down_projection) p.down_matmul = pattern::wrap_type( - {p.swiglu, pattern::any_input()}, + {p.swiglu, make_expert_weight_pattern(supported_weights_types)}, pattern::consumers_count(1) && pattern::attrs_match({{"transpose_a", false}, {"transpose_b", true}})); p.end_reshape_target_shape = pattern::any_input(); - p.end_reshape = pattern::wrap_type({p.down_matmul, p.end_reshape_target_shape}); + p.end_reshape = + pattern::wrap_type({p.down_matmul, p.end_reshape_target_shape}, pattern::consumers_count(1)); // Routing weights/mask p.router_topk_indices = pattern::any_input(); @@ -164,8 +181,10 @@ MOE3GEMMPatternNodes build_3gemm_pattern() { p.router_reshape = pattern::wrap_type({p.router_transpose, pattern::any_input()}); p.optional_unsqueeze = pattern::optional({p.router_reshape, pattern::any_input()}); - p.mul3 = pattern::wrap_type({p.end_reshape, p.optional_unsqueeze}); - p.reduce_sum = pattern::wrap_type({p.mul3, pattern::any_input()}, {{"keep_dims", false}}); + p.mul3 = pattern::wrap_type({p.end_reshape, p.optional_unsqueeze}, pattern::consumers_count(1)); + p.reduce_sum = pattern::wrap_type({p.mul3, pattern::any_input()}, + pattern::consumers_count(1), + {{"keep_dims", false}}); return p; } @@ -180,10 +199,11 @@ using ov::op::internal::GatherMatmul; // BGM-producing passes (IR → GatherMatmul) // ============================================================================ -ConvertTiledMoeBlockTo2GatherMatmuls::ConvertTiledMoeBlockTo2GatherMatmuls() { +ConvertTiledMoeBlockTo2GatherMatmuls::ConvertTiledMoeBlockTo2GatherMatmuls( + const std::vector& supported_weights_types) { MATCHER_SCOPE(ConvertTiledMoeBlockTo2GatherMatmuls); - auto p = build_2gemm_pattern(); + auto p = build_2gemm_pattern(supported_weights_types); matcher_pass_callback callback = [=](pattern::Matcher& m) { auto& pm = m.get_pattern_value_map(); @@ -271,10 +291,11 @@ ConvertTiledMoeBlockTo2GatherMatmuls::ConvertTiledMoeBlockTo2GatherMatmuls() { this->register_matcher(matcher, callback); } -ConvertTiledMoeBlockTo3GatherMatmuls::ConvertTiledMoeBlockTo3GatherMatmuls() { +ConvertTiledMoeBlockTo3GatherMatmuls::ConvertTiledMoeBlockTo3GatherMatmuls( + const std::vector& supported_weights_types) { MATCHER_SCOPE(ConvertTiledMoeBlockTo3GatherMatmuls); - auto p = build_3gemm_pattern(); + auto p = build_3gemm_pattern(supported_weights_types); matcher_pass_callback callback = [=](pattern::Matcher& m) { auto& pm = m.get_pattern_value_map(); diff --git a/src/common/transformations/src/transformations/common_optimizations/dimension_tracking.cpp b/src/common/transformations/src/transformations/common_optimizations/dimension_tracking.cpp index 1f513dbc7ac1..98537062021e 100644 --- a/src/common/transformations/src/transformations/common_optimizations/dimension_tracking.cpp +++ b/src/common/transformations/src/transformations/common_optimizations/dimension_tracking.cpp @@ -43,7 +43,7 @@ void ov::batch_util::mark_batch(const std::shared_ptr& parameter, for (auto& dim : shape) { const auto& dim_symbol = dim.get_symbol(); if (batches.count(dim_symbol) && mapped_batches.count(dim_symbol)) { - intersection_in_all_three_sources_of_batch.insert(dim_symbol); + intersection_in_all_three_sources_of_batch.insert(std::shared_ptr(dim_symbol)); } else { dim.set_symbol(nullptr); } @@ -76,7 +76,9 @@ void ov::batch_util::mark_layout_independent_batch(const std::shared_ptrget_partial_shape()) { if (const auto& symbol = dim.get_symbol()) { if (std::find(r_symbols.begin(), r_symbols.end(), symbol) != r_symbols.end()) { - mark_batch(parameter, map, {symbol}); + std::unordered_set> batches; + batches.insert(std::shared_ptr(symbol)); + mark_batch(parameter, map, batches); return; } } @@ -126,8 +128,11 @@ P2Btype ov::batch_util::find_batch(const std::shared_ptr& f) { const auto& batch_dim_symbol = shape[batch_placement.second].get_symbol(); if (batch_dim_symbol == nullptr) mark_no_batch(parameter, parameter_to_batch_symbols); - else - mark_batch(parameter, parameter_to_batch_symbols, {batch_dim_symbol}); + else { + std::unordered_set> batches; + batches.insert(std::shared_ptr(batch_dim_symbol)); + mark_batch(parameter, parameter_to_batch_symbols, batches); + } continue; // batch was or was not found at this point -- there is no point in searching further } } // node is not layout obvious -- checking if dims were propagated through diff --git a/src/common/transformations/src/transformations/common_optimizations/fuse_clamp_and_fake_quantize.cpp b/src/common/transformations/src/transformations/common_optimizations/fuse_clamp_and_fake_quantize.cpp new file mode 100644 index 000000000000..18736f0a4719 --- /dev/null +++ b/src/common/transformations/src/transformations/common_optimizations/fuse_clamp_and_fake_quantize.cpp @@ -0,0 +1,64 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "transformations/common_optimizations/fuse_clamp_and_fake_quantize.hpp" + +#include + +#include "itt.hpp" +#include "openvino/core/graph_util.hpp" +#include "openvino/op/clamp.hpp" +#include "openvino/op/constant.hpp" +#include "openvino/op/fake_quantize.hpp" +#include "openvino/pass/pattern/matcher.hpp" +#include "openvino/pass/pattern/op/wrap_type.hpp" + +namespace v0 = ov::op::v0; + +namespace ov::pass { + +FuseClampAndFakeQuantize::FuseClampAndFakeQuantize() { + MATCHER_SCOPE(FuseClampAndFakeQuantize); + + auto data_pattern = pattern::any_input(); + auto clamp_pattern = pattern::wrap_type({data_pattern}, pattern::consumers_count(1)); + auto input_low_pattern = pattern::wrap_type(); + auto input_high_pattern = pattern::wrap_type(); + auto fq_pattern = pattern::wrap_type( + {clamp_pattern, input_low_pattern, input_high_pattern, pattern::any_input(), pattern::any_input()}); + + ov::matcher_pass_callback callback = [OV_CAPTURE_CPY_AND_THIS](pattern::Matcher& m) { + const auto pattern_map = m.get_pattern_value_map(); + const auto clamp = ov::as_type_ptr(pattern_map.at(clamp_pattern).get_node_shared_ptr()); + const auto input_low = ov::as_type_ptr(pattern_map.at(input_low_pattern).get_node_shared_ptr()); + const auto input_high = ov::as_type_ptr(pattern_map.at(input_high_pattern).get_node_shared_ptr()); + if (!clamp || !input_low || !input_high) { + return false; + } + + const auto input_low_values = input_low->cast_vector(); + const auto input_high_values = input_high->cast_vector(); + const auto clamp_low = static_cast(clamp->get_min()); + const auto clamp_high = static_cast(clamp->get_max()); + + if (!std::all_of(input_low_values.begin(), input_low_values.end(), [&](auto v) { + return v >= clamp_low; + })) { + return false; + } + + if (!std::all_of(input_high_values.begin(), input_high_values.end(), [&](auto v) { + return v <= clamp_high; + })) { + return false; + } + + return ov::replace_output_update_name(clamp->output(0), clamp->input_value(0)); + }; + + auto m = std::make_shared(fq_pattern, matcher_name); + register_matcher(m, callback); +} + +} // namespace ov::pass \ No newline at end of file diff --git a/src/common/transformations/src/transformations/common_optimizations/fuse_gated_delta_net.cpp b/src/common/transformations/src/transformations/common_optimizations/fuse_gated_delta_net.cpp index 40a2d1373821..0eeddccc31a0 100644 --- a/src/common/transformations/src/transformations/common_optimizations/fuse_gated_delta_net.cpp +++ b/src/common/transformations/src/transformations/common_optimizations/fuse_gated_delta_net.cpp @@ -8,13 +8,17 @@ #include #include #include +#include +#include #include "itt.hpp" #include "openvino/core/graph_util.hpp" #include "openvino/core/node.hpp" #include "openvino/core/rt_info.hpp" +#include "openvino/core/shape.hpp" #include "openvino/core/type.hpp" #include "openvino/op/add.hpp" +#include "openvino/op/broadcast.hpp" #include "openvino/op/concat.hpp" #include "openvino/op/constant.hpp" #include "openvino/op/convert.hpp" @@ -22,6 +26,7 @@ #include "openvino/op/exp.hpp" #include "openvino/op/gated_delta_net.hpp" #include "openvino/op/gather.hpp" +#include "openvino/op/gather_nd.hpp" #include "openvino/op/loop.hpp" #include "openvino/op/multiply.hpp" #include "openvino/op/power.hpp" @@ -30,12 +35,17 @@ #include "openvino/op/scatter_update.hpp" #include "openvino/op/shape_of.hpp" #include "openvino/op/slice.hpp" +#include "openvino/op/split.hpp" #include "openvino/op/sqrt.hpp" #include "openvino/op/squeeze.hpp" #include "openvino/op/subtract.hpp" #include "openvino/op/transpose.hpp" #include "openvino/op/unsqueeze.hpp" +#include "openvino/op/util/gather_base.hpp" +#include "openvino/op/util/gather_nd_base.hpp" #include "openvino/op/util/op_types.hpp" +#include "openvino/op/util/squeeze_base.hpp" +#include "openvino/op/variadic_split.hpp" #include "openvino/pass/pattern/matcher.hpp" #include "openvino/pass/pattern/op/optional.hpp" #include "openvino/pass/pattern/op/wrap_type.hpp" @@ -358,6 +368,149 @@ ov::pass::FuseL2NormIntoGDN::FuseL2NormIntoGDN() { register_matcher(m, callback); } +namespace { + +bool is_allowed_qk_path_node(const std::shared_ptr& node) { + return ov::is_type(node) || ov::is_type(node) || + ov::is_type(node) || ov::is_type(node) || + ov::is_type(node) || ov::is_type(node) || + ov::is_type(node); +} + +bool is_qk_anchor_node(const std::shared_ptr& node) { + return ov::is_type(node) || ov::is_type(node) || + ov::is_type(node) || ov::is_type(node); +} + +// Traverse backward from `start` along input(0), through allowed reshaping ops, +// until a Split/Slice/Concat anchor is found. Returns the anchor output or {}. +ov::Output find_qk_anchor(const ov::Output& start) { + auto current = start; + while (true) { + const auto node = current.get_node_shared_ptr(); + if (!node) { + break; + } + if (is_qk_anchor_node(node)) { + return current; + } + if (!is_allowed_qk_path_node(node) || node->inputs().empty()) { + break; + } + current = node->input_value(0); + } + return {}; +} + +ov::Output align_to_reference_shape(const ov::Output& src, const ov::Output& reference) { + const auto& src_ps = src.get_partial_shape(); + const auto& ref_ps = reference.get_partial_shape(); + + if (src_ps == ref_ps) { + return src; + } + + if (src_ps.compatible(ref_ps)) { + auto ref_shape = std::make_shared(reference); + auto reshaped = std::make_shared(src, ref_shape, false); + ov::copy_runtime_info(src.get_node_shared_ptr(), {ref_shape, reshaped}); + return reshaped; + } + + const auto src_rank = src_ps.rank(); + const auto ref_rank = ref_ps.rank(); + + // Reference is always 4D per GatedDeltaNet spec. Source can be 2D, 3D, or 4D. + // Calculate num_heads = src_head_dim / ref_head_dim and reshape src to 4D. + if (src_rank.is_static() && src_rank.get_length() >= 2 && src_rank.get_length() <= 4 && ref_rank.is_static() && + ref_rank.get_length() == 4) { + const auto src_head_dim = src_ps[src_rank.get_length() - 1]; + const auto ref_head_dim = ref_ps[ref_rank.get_length() - 1]; + + if (src_head_dim.is_static() && ref_head_dim.is_static()) { + const auto src_head_size = src_head_dim.get_length(); + const auto ref_head_size = ref_head_dim.get_length(); + + if (ref_head_size > 0 && src_head_size % ref_head_size == 0) { + const int64_t num_heads = src_head_size / ref_head_size; + + // Create ref_shape and update num_heads at index 2: {ref[0], ref[1], num_heads, ref[3]}. + auto ref_shape = std::make_shared(reference); + auto indices = v0::Constant::create(ov::element::i64, {1}, {2}); + auto updates = v0::Constant::create(ov::element::i64, {1}, {num_heads}); + auto axis = v0::Constant::create(ov::element::i64, {}, {0}); + auto updated_shape = std::make_shared(ref_shape, indices, updates, axis); + + auto reshaped = std::make_shared(src, updated_shape, false); + if (!reshaped->get_output_partial_shape(0).compatible(ref_ps)) { + return {}; + } + + ov::copy_runtime_info(src.get_node_shared_ptr(), {ref_shape, updated_shape, reshaped}); + return reshaped; + } + } + } + + return {}; +} + +} // namespace + +ov::pass::FuseGroupedQueryIntoGDN::FuseGroupedQueryIntoGDN() { + auto gdn = pattern::wrap_type(); + + ov::matcher_pass_callback callback = [OV_CAPTURE_CPY_AND_THIS](ov::pass::pattern::Matcher& m) { + const auto& pattern_map = m.get_pattern_value_map(); + auto gdn_node = ov::as_type_ptr(pattern_map.at(gdn).get_node_shared_ptr()); + if (!gdn_node) { + return false; + } + + const auto q_anchor = find_qk_anchor(gdn_node->input_value(0)); + const auto k_anchor = find_qk_anchor(gdn_node->input_value(1)); + const auto v_anchor = find_qk_anchor(gdn_node->input_value(2)); + + // All Q/K/V must trace back to the same anchor node. + if (!q_anchor.get_node_shared_ptr() || !k_anchor.get_node_shared_ptr() || !v_anchor.get_node_shared_ptr() || + q_anchor.get_node_shared_ptr() != k_anchor.get_node_shared_ptr() || + k_anchor.get_node_shared_ptr() != v_anchor.get_node_shared_ptr()) { + return false; + } + + // If already directly connected from anchor outputs, skip. + if (gdn_node->input_value(0) == q_anchor && gdn_node->input_value(1) == k_anchor && + gdn_node->input_value(2) == v_anchor) { + return false; + } + + auto q_aligned = align_to_reference_shape(q_anchor, gdn_node->input_value(0)); + auto k_aligned = align_to_reference_shape(k_anchor, gdn_node->input_value(1)); + auto v_aligned = align_to_reference_shape(v_anchor, gdn_node->input_value(2)); + + if (!q_aligned.get_node_shared_ptr() || !k_aligned.get_node_shared_ptr() || !v_aligned.get_node_shared_ptr()) { + return false; + } + + auto new_gdn = std::make_shared(q_aligned, + k_aligned, + v_aligned, + gdn_node->input_value(3), + gdn_node->input_value(4), + gdn_node->input_value(5), + gdn_node->get_fuse_qk_l2norm(), + gdn_node->get_q_l2_norm_eps(), + gdn_node->get_k_l2_norm_eps()); + new_gdn->set_friendly_name(gdn_node->get_friendly_name()); + ov::copy_runtime_info(gdn_node, new_gdn); + ov::replace_node(gdn_node, new_gdn); + return true; + }; + + auto m = std::make_shared(gdn, "FuseGroupedQueryIntoGDN"); + register_matcher(m, callback); +} + bool ov::pass::GatedDeltaNetFusion::run_on_model(const std::shared_ptr& model) { RUN_ON_MODEL_SCOPE(GatedDeltaNetFusion); ov::pass::SymbolicOptimizations symbolic_optimizations(false, get_pass_config()); @@ -367,5 +520,6 @@ bool ov::pass::GatedDeltaNetFusion::run_on_model(const std::shared_ptrregister_pass(); symbolic_ctx_manager->register_pass(); + symbolic_ctx_manager->register_pass(); return symbolic_optimizations.run_on_model(model); -} \ No newline at end of file +} diff --git a/src/common/transformations/src/transformations/common_optimizations/fuse_rotary_positional_embeddings.cpp b/src/common/transformations/src/transformations/common_optimizations/fuse_rotary_positional_embeddings.cpp index 8cc867939584..7fe6a1766a3a 100644 --- a/src/common/transformations/src/transformations/common_optimizations/fuse_rotary_positional_embeddings.cpp +++ b/src/common/transformations/src/transformations/common_optimizations/fuse_rotary_positional_embeddings.cpp @@ -1148,10 +1148,10 @@ RoPEFusionGPTOSS::RoPEFusionGPTOSS() { auto t_sin = pattern::any_input(pattern::shape_matches("[?, 1, ?, half_ndims]")); auto vsplit_out0 = pattern::wrap_type( - {x, -1, {"half_ndims", "?"}}, + {x, pattern::wrap_type(), {"half_ndims", "?"}}, pattern::output_index_matches(0) && pattern::shape_matches("[?, ?, ?, half_ndims]")); auto vsplit_out1 = pattern::wrap_type( - {x, -1, {"half_ndims", "?"}}, + {x, pattern::wrap_type(), {"half_ndims", "?"}}, pattern::output_index_matches(1) && pattern::shape_matches("[?, ?, ?, half_ndims]")); auto first_half_mul_cos = pattern::wrap_type({vsplit_out0, t_cos}, {{"auto_broadcast", "numpy"}}); auto second_half_mul_sin = pattern::wrap_type({vsplit_out1, t_sin}, {{"auto_broadcast", "numpy"}}); @@ -1162,7 +1162,7 @@ RoPEFusionGPTOSS::RoPEFusionGPTOSS() { auto first_half_mul_sin = pattern::wrap_type({vsplit_out0, t_sin}, {{"auto_broadcast", "numpy"}}); auto add_Add = pattern::wrap_type({second_half_mul_cos, first_half_mul_sin}, {{"auto_broadcast", "numpy"}}); - auto concat_result = pattern::wrap_type({sub_Subtract, add_Add}, {{"axis", -1}}); + auto concat_result = pattern::wrap_type({sub_Subtract, add_Add}); auto result = concat_result; @@ -1172,6 +1172,31 @@ RoPEFusionGPTOSS::RoPEFusionGPTOSS() { const auto& x_val = pattern_map.at(x); const auto& v_cos = pattern_map.at(t_cos); + // Verify concat axis is the last dimension (accepts both -1 and positive equivalent) + auto concat_node = ov::as_type_ptr(root); + if (!concat_node) + return false; + auto concat_rank = concat_node->get_output_partial_shape(0).rank(); + if (concat_rank.is_dynamic()) + return false; + int64_t concat_axis = concat_node->get_axis(); + if (concat_axis != -1 && concat_axis != concat_rank.get_length() - 1) + return false; + + // Verify VariadicSplit axis is the last dimension (accepts both -1 and positive equivalent) + auto vsplit_node = pattern_map.at(vsplit_out0).get_node_shared_ptr(); + auto axis_const = ov::as_type_ptr(vsplit_node->input_value(1).get_node_shared_ptr()); + if (!axis_const) + return false; + auto axis_val = axis_const->cast_vector(); + if (axis_val.size() != 1) + return false; + auto input_rank = x_val.get_partial_shape().rank(); + if (input_rank.is_dynamic()) + return false; + if (axis_val[0] != -1 && axis_val[0] != input_rank.get_length() - 1) + return false; + auto symbols = m.get_symbols(); const auto& half_ndims = symbols["half_ndims"]; if (!half_ndims.is_integer()) { diff --git a/src/common/transformations/src/transformations/common_optimizations/group_normalization_fusion.cpp b/src/common/transformations/src/transformations/common_optimizations/group_normalization_fusion.cpp index 6e736a591c7e..3545347c7ebb 100644 --- a/src/common/transformations/src/transformations/common_optimizations/group_normalization_fusion.cpp +++ b/src/common/transformations/src/transformations/common_optimizations/group_normalization_fusion.cpp @@ -4,6 +4,8 @@ #include "transformations/common_optimizations/group_normalization_fusion.hpp" +#include + #include "itt.hpp" #include "openvino/core/graph_util.hpp" #include "openvino/core/rt_info.hpp" @@ -39,14 +41,41 @@ GroupNormalizationFusion::GroupNormalizationFusion() { return (output_ps.rank().is_static()) && (output_ps.rank().get_length() >= 2); }; + // This pattern matches GroupNormalization decomposed as InstanceNormalization. + // Two variants are supported: + // + // 1. 3D MVN pattern: + // Input -> Reshape{N,G,-1} -> MVN(axes={2}) -> [Mul] -> [Add] + // -> Reshape(original) -> Mul(gamma) -> Add(beta) + // + // 2. 4D MVN pattern: + // Input -> Reshape{N,G,-1,1} -> MVN(axes={2,3}) -> [Mul] -> [Add] + // -> Reshape(original) -> Mul(gamma) -> Add(beta) + // + // Some frameworks decompose GroupNormalization via InstanceNormalization + // and reshape directly to 4D {N,G,-1,1} with a trailing unit dimension. + // MVN then reduces over axes {2,3}, which is mathematically equivalent + // to the 3D variant since the trailing dimension is always 1. + // + // In both variants, optional instance-norm scale [Mul] and bias [Add] may + // appear immediately after MVN, before the reshape back to original shape. + auto input_m = pattern::any_input( pattern::all_of({has_real_not_quantized_type, has_at_least_2d_shape, pattern::has_static_dim(1)})); + auto is_rank_3_or_4 = [](const ov::Output& output) -> bool { + const auto& ps = output.get_partial_shape(); + if (ps.rank().is_dynamic()) + return false; + const auto r = ps.rank().get_length(); + return r == 3 || r == 4; + }; + auto pre_mvn_shape_const_m = pattern::wrap_type(pattern::all_of({pattern::rank_equals(1), pattern::has_static_dim(0)})); auto pre_mvn_reshape_m = pattern::wrap_type( {input_m, pre_mvn_shape_const_m}, - pattern::all_of({has_real_not_quantized_type, pattern::rank_equals(3), pattern::has_static_dim(1)})); + pattern::all_of({has_real_not_quantized_type, is_rank_3_or_4, pattern::has_static_dim(1)})); auto mvn_reduction_axes_const_m = pattern::wrap_type(pattern::all_of({pattern::rank_equals(1), pattern::has_static_dim(0)})); @@ -95,26 +124,62 @@ GroupNormalizationFusion::GroupNormalizationFusion() { const auto& pre_mvn_shape_const = ov::as_type_ptr(pattern_map.at(pre_mvn_shape_const_m).get_node_shared_ptr()); const auto& pre_mvn_shape_out_ps = pre_mvn_shape.get_shape(); - if (pre_mvn_shape_out_ps[0] != 3) + const auto pre_mvn_rank = pre_mvn_reshape_out_ps.rank().get_length(); + if (pre_mvn_shape_out_ps[0] != static_cast(pre_mvn_rank)) return false; + // For 4D pattern, the trailing dimension must be 1 + const bool is_4d_pattern = (pre_mvn_rank == 4); + if (is_4d_pattern) { + if (!pre_mvn_reshape_out_ps[3].is_static() || pre_mvn_reshape_out_ps[3].get_length() != 1) + return false; + } + auto pre_mvn_shape_vals_correct = [](const std::vector& pre_mvn_shape_vals, const ov::PartialShape& input_ps, const ov::Dimension::value_type num_groups) -> bool { - bool res = true; + // Validate first dimension (batch): must be 0 (special value) or match input batch size if (input_ps[0].is_dynamic()) { if (pre_mvn_shape_vals[0] != 0ll) - res = false; + return false; } else { if ((pre_mvn_shape_vals[0] != 0ll) && (pre_mvn_shape_vals[0] != static_cast(input_ps[0].get_max_length()))) - res = false; + return false; + } + // Validate second dimension (groups): allow either an explicit num_groups value + // or special-zero copy when it provably copies the input channel dimension and + // that channel dimension is equal to num_groups. + if (pre_mvn_shape_vals[1] == 0ll) { + if (!input_ps[1].is_static() || input_ps[1].get_length() != static_cast(num_groups)) + return false; + } else if (pre_mvn_shape_vals[1] != static_cast(num_groups)) { + return false; + } + // Validate third dimension: can be -1 (infer) or the actual flattened size. + // When a concrete value is given, we must verify it against the expected + // merged spatial size computed from the input shape, not from the reshape + // output (which is derived from the same constant and would be tautological). + if (pre_mvn_shape_vals[2] != -1ll) { + // Compute expected merged spatial: (C / G) * product(spatial dims) + // All relevant input dimensions must be static to verify. + if (!input_ps[1].is_static()) + return false; + int64_t expected_merged = input_ps[1].get_length() / num_groups; + for (int64_t d = 2; d < input_ps.rank().get_length(); d++) { + if (input_ps[d].is_dynamic()) + return false; + expected_merged *= input_ps[d].get_length(); + } + if (pre_mvn_shape_vals[2] != expected_merged) + return false; } - if ((pre_mvn_shape_vals[1] != 0ll) && (pre_mvn_shape_vals[1] != static_cast(num_groups))) - res = false; - if (pre_mvn_shape_vals[2] != -1ll) - res = false; - return res; + // For 4D pattern, validate the trailing dimension value is 1 + if (pre_mvn_shape_vals.size() == 4) { + if (pre_mvn_shape_vals[3] != 1ll) + return false; + } + return true; }; if (!pre_mvn_shape_vals_correct(pre_mvn_shape_const->cast_vector(), input_ps, num_groups)) @@ -129,22 +194,32 @@ GroupNormalizationFusion::GroupNormalizationFusion() { if (input_ps[0].get_max_length() != pre_mvn_reshape_out_ps[0].get_max_length()) return false; - // we expect to execute normalization over last dimension of MVN input + // we expect to execute normalization over last dimension(s) of MVN input const auto& mvn_reduction_axes = pattern_map.at(mvn_reduction_axes_const_m); const auto& mvn_reduction_axes_const = ov::as_type_ptr(mvn_reduction_axes.get_node_shared_ptr()); const auto& mvn_reduction_axes_out_shape = mvn_reduction_axes.get_shape(); - if (mvn_reduction_axes_out_shape[0] != 1) - return false; - auto mvn_reduction_axes_correct = [](const std::vector& mvn_reduction_axes) -> bool { - bool res = true; - if ((mvn_reduction_axes[0] != 2ll) && (mvn_reduction_axes[0] != -1ll)) + auto axes = mvn_reduction_axes_const->cast_vector(); + if (is_4d_pattern) { + // 4D pattern: MVN axes must be {2, 3} + if (mvn_reduction_axes_out_shape[0] != 2) return false; - return res; - }; - - if (!mvn_reduction_axes_correct(mvn_reduction_axes_const->cast_vector())) - return false; + for (auto& ax : axes) { + if (ax < -4 || ax >= 4) + return false; + if (ax < 0) + ax += 4; // normalize negative axes + } + std::sort(axes.begin(), axes.end()); + if (axes.size() != 2 || axes[0] != 2 || axes[1] != 3) + return false; + } else { + // 3D pattern: MVN axis must be 2 (or -1 which equals 2 for rank-3) + if (mvn_reduction_axes_out_shape[0] != 1) + return false; + if (axes[0] != 2 && axes[0] != -1) + return false; + } const auto& post_instance_norm_reshape_out_ps = pattern_map.at(post_instance_norm_reshape_m).get_partial_shape(); diff --git a/src/common/transformations/src/transformations/common_optimizations/mark_precision_sensitive_shapeof_subgraphs.cpp b/src/common/transformations/src/transformations/common_optimizations/mark_precision_sensitive_shapeof_subgraphs.cpp index ca8abe3f17de..2060676edbc4 100644 --- a/src/common/transformations/src/transformations/common_optimizations/mark_precision_sensitive_shapeof_subgraphs.cpp +++ b/src/common/transformations/src/transformations/common_optimizations/mark_precision_sensitive_shapeof_subgraphs.cpp @@ -13,7 +13,7 @@ #include "openvino/op/util/multi_subgraph_base.hpp" #include "openvino/op/util/precision_sensitive_attribute.hpp" #include "openvino/pass/pattern/op/wrap_type.hpp" -#include "transformations/rt_info/disable_fp16_compression.hpp" +#include "transformations/rt_info/disable_precision_conversion.hpp" #include "transformations/rt_info/is_shape_subgraph.hpp" #include "transformations/rt_info/nonconvertible_divide.hpp" #include "transformations/utils/utils.hpp" @@ -23,14 +23,14 @@ using namespace std; namespace op_util = ov::op::util; ov::pass::MarkPrecisionSensitiveShapeOfSubgraphs::MarkPrecisionSensitiveShapeOfSubgraphs() { m_markup_func = [](Node* node) { - ov::disable_fp16_compression(node->shared_from_this()); + ov::disable_conversion(node->shared_from_this(), element::f16); }; } ov::pass::MarkPrecisionSensitiveConstants::MarkPrecisionSensitiveConstants() { m_markup_func = [](Node* node) { if (ov::is_type(node)) { - ov::disable_fp16_compression(node->shared_from_this()); + ov::disable_conversion(node->shared_from_this(), element::f16); } }; } diff --git a/src/common/transformations/src/transformations/common_optimizations/mark_rope_input_to_keep_in_mixed_precision.cpp b/src/common/transformations/src/transformations/common_optimizations/mark_rope_input_to_keep_in_mixed_precision.cpp index ff818965e120..3af92259a85f 100644 --- a/src/common/transformations/src/transformations/common_optimizations/mark_rope_input_to_keep_in_mixed_precision.cpp +++ b/src/common/transformations/src/transformations/common_optimizations/mark_rope_input_to_keep_in_mixed_precision.cpp @@ -12,7 +12,7 @@ #include "openvino/pass/constant_folding.hpp" #include "openvino/pass/pattern/op/wrap_type.hpp" #include "ov_ops/rotary_positional_embeddings.hpp" -#include "transformations/rt_info/disable_fp16_compression.hpp" +#include "transformations/rt_info/disable_precision_conversion.hpp" #include "transformations/utils/utils.hpp" namespace v0 = ov::op::v0; @@ -32,7 +32,7 @@ MarkRopeInputsToKeepInMixedPrecision::MarkRopeInputsToKeepInMixedPrecision() { auto sin_input_node = pattern_map.at(sin_tab).get_node(); // mark the node as disable_fp16_compression auto visit_func = [](ov::Node* node) { - ov::disable_fp16_compression(node->shared_from_this()); + ov::disable_conversion(node->shared_from_this(), element::f16); }; // skip constant, parameter and shapeof // The inputs of cos_sin table generation are position_ids and a ShapeOf [batch, input_length] diff --git a/src/common/transformations/src/transformations/common_optimizations/matmul_experts_fusion.cpp b/src/common/transformations/src/transformations/common_optimizations/matmul_experts_fusion.cpp deleted file mode 100644 index c8459f9b72ba..000000000000 --- a/src/common/transformations/src/transformations/common_optimizations/matmul_experts_fusion.cpp +++ /dev/null @@ -1,215 +0,0 @@ -// Copyright (C) 2018-2026 Intel Corporation -// SPDX-License-Identifier: Apache-2.0 -// - -#include "transformations/common_optimizations/matmul_experts_fusion.hpp" - -#include "itt.hpp" -#include "openvino/core/graph_util.hpp" -#include "openvino/op/add.hpp" -#include "openvino/op/clamp.hpp" -#include "openvino/op/matmul.hpp" -#include "openvino/op/minimum.hpp" -#include "openvino/op/moe.hpp" -#include "openvino/op/multiply.hpp" -#include "openvino/op/reduce_sum.hpp" -#include "openvino/op/reshape.hpp" -#include "openvino/op/scatter_elements_update.hpp" -#include "openvino/op/slice.hpp" -#include "openvino/op/swish.hpp" -#include "openvino/op/tile.hpp" -#include "openvino/op/topk.hpp" -#include "openvino/op/transpose.hpp" -#include "openvino/op/unsqueeze.hpp" -#include "openvino/pass/manager.hpp" -#include "openvino/pass/pattern/matcher.hpp" -#include "openvino/pass/pattern/op/wrap_type.hpp" -#include "transformations/utils/utils.hpp" - -namespace ov::pass { - -namespace v0 = ov::op::v0; -namespace v1 = ov::op::v1; -namespace v4 = ov::op::v4; -namespace v8 = ov::op::v8; -namespace v12 = ov::op::v12; - -FuseVectorizedMOE2GEMM::FuseVectorizedMOE2GEMM() { - MATCHER_SCOPE(FuseVectorizedMOE2GEMM); - - auto experts_input = pattern::wrap_type({pattern::any_input(), pattern::any_input()}); - auto tile = pattern::wrap_type({experts_input, pattern::any_input()}); - auto after_tile_reshape = pattern::wrap_type({tile, pattern::any_input()}); - auto gate_up_matmul = pattern::wrap_type({after_tile_reshape, pattern::any_input()}, - {{"transpose_a", false}, {"transpose_b", true}}); - auto gate_up_add = pattern::wrap_type({gate_up_matmul, pattern::any_input()}); - - // Branch 1: Slice_1 -> Clamp -> Add_1 - auto slice1 = pattern::wrap_type( - {gate_up_add, pattern::any_input(), pattern::any_input(), pattern::any_input(), pattern::any_input()}); - auto clamp = pattern::wrap_type({slice1}); - auto add1 = pattern::wrap_type({clamp, ov::pass::pattern::wrap_const()}); - - // Branch 2: Slice_2 -> Minimum_1 -> Swish - auto slice2 = pattern::wrap_type( - {gate_up_add, pattern::any_input(), pattern::any_input(), pattern::any_input(), pattern::any_input()}); - auto minimum1 = pattern::wrap_type({slice2, ov::pass::pattern::wrap_const()}); - auto swish_beta = ov::pass::pattern::wrap_const(); - auto swish = pattern::wrap_type({minimum1, swish_beta}); - - // Join: Multiply_2 - auto multiply2 = pattern::wrap_type({add1, swish}); - - // Down projection - auto down_proj_matmul = pattern::wrap_type({multiply2, pattern::any_input()}, - {{"transpose_a", false}, {"transpose_b", true}}); - auto down_proj_add = pattern::wrap_type({down_proj_matmul, ov::pass::pattern::wrap_const()}); - auto end_reshape = pattern::wrap_type({down_proj_add, pattern::any_input()}); - - // Routing weights/mask - auto router_topk_indices = pattern::any_input(); - auto scatter_elements_update = pattern::wrap_type( - {pattern::any_input(), router_topk_indices, pattern::any_input(), pattern::any_input()}); - - auto router_transpose = pattern::wrap_type({scatter_elements_update, pattern::any_input()}); - auto router_reshape = pattern::wrap_type({router_transpose, pattern::any_input()}); - auto unsqueeze_routing_weights = pattern::wrap_type({router_reshape, pattern::any_input()}); - - auto mul3 = pattern::wrap_type({end_reshape, unsqueeze_routing_weights}); - auto reduce_sum = pattern::wrap_type({mul3, pattern::any_input()}, {{"keep_dims", false}}); - auto moe_pattern = reduce_sum; - - matcher_pass_callback callback = [=](pattern::Matcher& m) { - auto& pm = m.get_pattern_value_map(); - - if (transformation_callback(m.get_match_root())) { - return false; - } - - auto experts_input_node = pm.at(experts_input).get_node()->input_value(0); - - auto routing_weights_node = pm.at(unsqueeze_routing_weights).get_node_shared_ptr(); - auto gate_up_weight = pm.at(gate_up_matmul).get_node()->input_value(1).get_node_shared_ptr(); - auto gate_up_bias_node = pm.at(gate_up_add).get_node()->input_value(1).get_node_shared_ptr(); - auto down_proj_weight = pm.at(down_proj_matmul).get_node()->input_value(1).get_node_shared_ptr(); - auto down_proj_bias_node = pm.at(down_proj_add).get_node()->input_value(1).get_node_shared_ptr(); - auto topk_indices_node = pm.at(scatter_elements_update).get_node()->input_value(1); - - ov::OutputVector moe_inputs = {experts_input_node, - routing_weights_node, - topk_indices_node, - gate_up_weight, - gate_up_bias_node, - down_proj_weight, - down_proj_bias_node}; - - ov::op::internal::MOE::Config config; - - // Extract expert_beta from Swish beta attribute - auto swish_beta_const = ov::as_type_ptr(pm.at(swish_beta).get_node_shared_ptr()); - auto swish_beta_const_val = swish_beta_const->cast_vector()[0]; - config.expert_beta = swish_beta_const_val; - - // Extract expert_alpha from Clamp max attribute - if (auto clamp_op = ov::as_type_ptr(pm.at(clamp).get_node_shared_ptr())) { - config.expert_alpha = static_cast(clamp_op->get_max()); - } - - // Set expert_type - config.expert_type = ov::op::internal::MOE::Expert_type::GEMM2_BIAS_SWIGLU_CLAMP; - - auto moe = std::make_shared(moe_inputs, config); - moe->set_friendly_name(m.get_match_root()->get_friendly_name()); - ov::copy_runtime_info(m.get_matched_nodes(), moe); - ov::replace_node(m.get_match_root(), moe); - - register_new_node(moe); - return true; - }; - - auto matcher = std::make_shared(moe_pattern, matcher_name); - this->register_matcher(matcher, callback); -} - -FuseVectorizedMOE3GEMM::FuseVectorizedMOE3GEMM() { - MATCHER_SCOPE(FuseVectorizedMOE3GEMM); - - auto experts_input = pattern::wrap_type({pattern::any_input(), pattern::any_input()}); - auto tile = pattern::wrap_type({experts_input, pattern::any_input()}); - auto after_tile_reshape = pattern::wrap_type({tile, pattern::any_input()}); - - // First GEMM (activation gate) - auto gate_matmul = pattern::wrap_type({after_tile_reshape, pattern::any_input()}, - {{"transpose_a", false}, {"transpose_b", true}}); - auto swish = pattern::wrap_type({gate_matmul}); - // Second GEMM (up_projection) - auto up_matmul = pattern::wrap_type({after_tile_reshape, pattern::any_input()}, - {{"transpose_a", false}, {"transpose_b", true}}); - // Join: Multiply (SwiGLU) - auto swiglu = pattern::wrap_type({swish, up_matmul}); - - // Third GEMM (down_projection) - auto down_matmul = - pattern::wrap_type({swiglu, pattern::any_input()}, {{"transpose_a", false}, {"transpose_b", true}}); - auto end_reshape = pattern::wrap_type({down_matmul, pattern::any_input()}); - - // Routing weights/mask - auto router_topk_indices = pattern::any_input(); - auto scatter_elements_update = pattern::wrap_type( - {pattern::any_input(), router_topk_indices, pattern::any_input(), pattern::any_input()}); - auto router_transpose = pattern::wrap_type({scatter_elements_update, pattern::any_input()}); - auto router_reshape = pattern::wrap_type({router_transpose, pattern::any_input()}); - auto unsqueeze_routing_weights = pattern::wrap_type({router_reshape, pattern::any_input()}); - - auto mul3 = pattern::wrap_type({end_reshape, unsqueeze_routing_weights}); - auto reduce_sum = pattern::wrap_type({mul3, pattern::any_input()}, {{"keep_dims", false}}); - auto moe_pattern = reduce_sum; - - matcher_pass_callback callback = [=](pattern::Matcher& m) { - auto& pm = m.get_pattern_value_map(); - - if (transformation_callback(m.get_match_root())) { - return false; - } - - auto experts_input_node = pm.at(experts_input).get_node()->input_value(0); - auto routing_weights_node = pm.at(unsqueeze_routing_weights).get_node_shared_ptr(); - auto gate_weight = pm.at(gate_matmul).get_node()->input_value(1).get_node_shared_ptr(); - auto up_weight = pm.at(up_matmul).get_node()->input_value(1).get_node_shared_ptr(); - auto down_weight = pm.at(down_matmul).get_node()->input_value(1).get_node_shared_ptr(); - auto topk_indices_node = pm.at(scatter_elements_update).get_node()->input_value(1); - - ov::OutputVector moe_inputs = { - experts_input_node, - routing_weights_node, - topk_indices_node, - gate_weight, - up_weight, - down_weight, - }; - - ov::op::internal::MOE::Config config; - config.expert_type = ov::op::internal::MOE::Expert_type::GEMM3_SWIGLU; - // Extract expert_beta if Swish has beta input provided - if (auto swish_op = ov::as_type_ptr(pm.at(swish).get_node_shared_ptr())) { - if (swish_op->get_input_size() > 1) { - if (auto swish_beta_const = ov::as_type_ptr(swish_op->get_input_node_shared_ptr(1))) { - config.expert_beta = swish_beta_const->cast_vector()[0]; - } - } - } - - auto moe = std::make_shared(moe_inputs, config); - moe->set_friendly_name(m.get_match_root()->get_friendly_name()); - ov::copy_runtime_info(m.get_matched_nodes(), moe); - ov::replace_node(m.get_match_root(), moe); - - register_new_node(moe); - return true; - }; - - auto matcher = std::make_shared(moe_pattern, matcher_name); - this->register_matcher(matcher, callback); -} - -} // namespace ov::pass diff --git a/src/common/transformations/src/transformations/common_optimizations/matmul_multiply_fusion.cpp b/src/common/transformations/src/transformations/common_optimizations/matmul_multiply_fusion.cpp index 2a63a7e07c66..577322a8e589 100644 --- a/src/common/transformations/src/transformations/common_optimizations/matmul_multiply_fusion.cpp +++ b/src/common/transformations/src/transformations/common_optimizations/matmul_multiply_fusion.cpp @@ -26,10 +26,20 @@ namespace v1 = ov::op::v1; static std::shared_ptr fuse_const_to_weights(const std::shared_ptr& matmul, const Output& weights, std::shared_ptr mul_const) { - auto const_shape = mul_const->get_shape(); + const auto& const_shape = mul_const->get_shape(); auto const_rank = static_cast(const_shape.size()); const auto& weights_shape = weights.get_partial_shape(); int64_t weights_rank = static_cast(weights_shape.rank().get_length()); + bool is_dynamic_weights = !ov::is_type(weights.get_node()); + const auto& weights_el_type = weights.get_element_type(); + + // Don't fuse amplifying scalar into non-constant weights in f16/bf16/f8 to avoid potential overflow. + if (weights_el_type.is_real() && weights_el_type.size() <= 2 && is_dynamic_weights) { + float value; + if (op::util::get_constant_value(mul_const, value) && std::abs(value) > 1.0f) { + return nullptr; + } + } // Fuse if const is a scalar if (ov::is_scalar(const_shape)) { @@ -55,7 +65,7 @@ static std::shared_ptr fuse_const_to_weights(const std::shared_ptr& // If weights is not a constant node - disallow Multiply constant // that extends weights rank. This is LPT requirement in case where // weights are meant to be quantized. - if (const_rank > weights_rank && !ov::is_type(weights.get_node())) { + if (const_rank > weights_rank && is_dynamic_weights) { return nullptr; } diff --git a/src/common/transformations/src/transformations/common_optimizations/moc_transformations.cpp b/src/common/transformations/src/transformations/common_optimizations/moc_transformations.cpp index c9329e7d3726..c1effbbda757 100644 --- a/src/common/transformations/src/transformations/common_optimizations/moc_transformations.cpp +++ b/src/common/transformations/src/transformations/common_optimizations/moc_transformations.cpp @@ -35,6 +35,7 @@ #include "transformations/common_optimizations/fold_subgraph_empty_inputs.hpp" #include "transformations/common_optimizations/fq_mul_fusion.hpp" #include "transformations/common_optimizations/fq_reshape_fusion.hpp" +#include "transformations/common_optimizations/fuse_clamp_and_fake_quantize.hpp" #include "transformations/common_optimizations/fuse_moe_experts.hpp" #include "transformations/common_optimizations/gelu_fusion.hpp" #include "transformations/common_optimizations/gru_cell_fusion.hpp" @@ -285,6 +286,7 @@ bool ov::pass::MOCTransformations::run_on_model(const std::shared_ptr ADD_MATCHER(fq_fusions, FakeQuantizeReshapeFusion) ADD_MATCHER(fq_fusions, PullTransposeThroughFQUp) ADD_MATCHER(fq_fusions, ReluFakeQuantizeFusion) + ADD_MATCHER(fq_fusions, FuseClampAndFakeQuantize) ADD_MATCHER(fq_fusions, AddFakeQuantizeFusion) ADD_MATCHER(fq_fusions, MulFakeQuantizeFusion) fq_fusions->set_name("ov::pass::FakeQuantizeFusions"); diff --git a/src/common/transformations/src/transformations/common_optimizations/moe_op_fusion.cpp b/src/common/transformations/src/transformations/common_optimizations/moe_op_fusion.cpp index 27b6c0cf3697..badb4bee9204 100644 --- a/src/common/transformations/src/transformations/common_optimizations/moe_op_fusion.cpp +++ b/src/common/transformations/src/transformations/common_optimizations/moe_op_fusion.cpp @@ -14,6 +14,7 @@ #include "openvino/op/clamp.hpp" #include "openvino/op/constant.hpp" #include "openvino/op/convert.hpp" +#include "openvino/op/gelu.hpp" #include "openvino/op/minimum.hpp" #include "openvino/op/moe.hpp" #include "openvino/op/multiply.hpp" @@ -24,6 +25,7 @@ #include "openvino/op/transpose.hpp" #include "openvino/op/unsqueeze.hpp" #include "openvino/pass/pattern/matcher.hpp" +#include "openvino/pass/pattern/op/optional.hpp" #include "openvino/pass/pattern/op/or.hpp" #include "openvino/pass/pattern/op/wrap_type.hpp" #include "ov_ops/gather_matmul.hpp" @@ -39,13 +41,21 @@ using ov::op::internal::MOECompressed; namespace v0 = ov::op::v0; namespace v1 = ov::op::v1; namespace v4 = ov::op::v4; +namespace v7 = ov::op::v7; namespace v8 = ov::op::v8; -Convert3GatherMatmulMoeBlockToMoeOp::Convert3GatherMatmulMoeBlockToMoeOp(size_t has_batch_dim) { +// Logical K from a weight shape: rank-3 [E, ofm, K] or rank-4 [E, ofm, G, GS]. +static size_t weight_logical_K(const ov::Shape& shape) { + OPENVINO_ASSERT(shape.size() == 3 || shape.size() == 4, "MOE weight must be rank 3 or 4, got rank ", shape.size()); + return shape.size() == 4 ? shape[2] * shape[3] : shape[2]; +} + +Convert3GatherMatmulMoeBlockToMoeOp::Convert3GatherMatmulMoeBlockToMoeOp(bool has_batch_dim) { MATCHER_SCOPE(Convert3GatherMatmulMoeBlockToMoeOp); - auto experts_reshape_m = pattern::any_input(); - auto unsqueeze_m = pattern::wrap_type({experts_reshape_m, pattern::any_input()}); + auto hidden_states_m = pattern::any_input(); + auto hidden_state_reshape = pattern::optional({hidden_states_m, pattern::any_input()}); + auto unsqueeze_m = pattern::wrap_type({hidden_state_reshape, pattern::any_input()}); auto gate_w_m = pattern::any_input(); auto topk_indices_m = pattern::any_input(); @@ -60,7 +70,8 @@ Convert3GatherMatmulMoeBlockToMoeOp::Convert3GatherMatmulMoeBlockToMoeOp(size_t // Or-pattern auto bgm_gate_m = std::make_shared(OutputVector{bgm_gate_4_m, bgm_gate_6_m}); - auto swish_m = pattern::wrap_type({bgm_gate_m}); + // Gate activation: Swish (SwiGLU) or Gelu (GeGLU) with TANH or ERF approximation. + auto swish_m = pattern::wrap_type({bgm_gate_m}); auto up_w_m = pattern::any_input(); auto bgm_up_4_m = pattern::wrap_type({unsqueeze_m, up_w_m, topk_indices_m, pattern::any_input()}); @@ -80,8 +91,10 @@ Convert3GatherMatmulMoeBlockToMoeOp::Convert3GatherMatmulMoeBlockToMoeOp(size_t {swiglu_m, down_w_m, topk_indices_m, pattern::any_input(), down_scale_m, down_zp_m}); auto bgm_down_m = std::make_shared(OutputVector{bgm_down_4_m, bgm_down_6_m}); - // Compact routing: Transpose → Unsqueeze - auto routing_transpose_m = pattern::wrap_type({pattern::any_input(), pattern::any_input()}); + auto routing_m = pattern::any_input(); + auto routing_slice_m = pattern::optional( + {routing_m, pattern::any_input(), pattern::any_input(), pattern::any_input(), pattern::any_input()}); + auto routing_transpose_m = pattern::wrap_type({routing_slice_m, pattern::any_input()}); auto routing_unsqueeze_m = pattern::wrap_type({routing_transpose_m, pattern::any_input()}); auto final_mul_m = pattern::wrap_type({bgm_down_m, routing_unsqueeze_m}, pattern::consumers_count(1)); @@ -96,26 +109,35 @@ Convert3GatherMatmulMoeBlockToMoeOp::Convert3GatherMatmulMoeBlockToMoeOp(size_t return false; } - auto experts_reshape_node = pm.at(experts_reshape_m).get_node_shared_ptr(); - auto hidden_states = experts_reshape_node->input_value(0); + auto hidden_states = pm.at(hidden_states_m); - auto routing = pm.at(routing_unsqueeze_m).get_node_shared_ptr(); + auto routing = pm.at(routing_m); auto topk_indices = pm.at(topk_indices_m); auto gate_w = pm.at(gate_w_m); auto up_w = pm.at(up_w_m); auto down_w = pm.at(down_w_m); - // Extract expert_beta from Swish + // Extract expert_beta and detect activation type from the matched gate activation node. float expert_beta = 1.0f; - auto swish_node = pm.at(swish_m).get_node_shared_ptr(); - - auto swish_op = ov::as_type_ptr(swish_node); - OPENVINO_ASSERT(swish_op, "Unexpected node type matched for Swish: ", *swish_node); - - if (swish_op->get_input_size() > 1) { - if (auto beta_const = ov::as_type_ptr(swish_op->get_input_node_shared_ptr(1))) { - expert_beta = beta_const->cast_vector()[0]; + ov::op::internal::MOE::Activation_type activation_type = ov::op::internal::MOE::Activation_type::SWIGLU; + auto activation_node = pm.at(swish_m).get_node_shared_ptr(); + + if (auto swish_op = ov::as_type_ptr(activation_node)) { + if (swish_op->get_input_size() > 1) { + if (auto beta_const = ov::as_type_ptr(swish_op->get_input_node_shared_ptr(1))) { + expert_beta = beta_const->cast_vector()[0]; + } + } + } else if (auto gelu_op = ov::as_type_ptr(activation_node)) { + if (gelu_op->get_approximation_mode() == ov::op::GeluApproximationMode::TANH) { + activation_type = ov::op::internal::MOE::Activation_type::GEGLU_TANH; + } else if (gelu_op->get_approximation_mode() == ov::op::GeluApproximationMode::ERF) { + activation_type = ov::op::internal::MOE::Activation_type::GEGLU_ERF; + } else { + return false; } + } else { + OPENVINO_THROW("Unexpected node type matched for gate activation: ", activation_node); } std::shared_ptr moe_node; @@ -153,21 +175,30 @@ Convert3GatherMatmulMoeBlockToMoeOp::Convert3GatherMatmulMoeBlockToMoeOp(size_t auto wei_partial_shape = gate_w.get_partial_shape(); OPENVINO_ASSERT(wei_partial_shape.is_static(), "MOE weight shape should be static."); auto weight_shape = wei_partial_shape.to_shape(); - bool group_compressed = (weight_shape.size() == 4); auto topk_shape = topk_indices.get_partial_shape(); OPENVINO_ASSERT(topk_shape[1].is_static(), "K dimension in moe topk input should be static."); + // group_size derived from down-scale; weight_logical_K handles rank-3/4. + const auto gate_K = weight_logical_K(weight_shape); + const auto down_K = weight_logical_K(pm.at(down_w_m).get_partial_shape().to_shape()); + const auto down_scale_shape = pm.at(down_scale_m).get_partial_shape().to_shape(); + const size_t down_num_groups = (down_scale_shape.size() >= 3) ? down_scale_shape[2] : 1; + const size_t group_size = + (down_num_groups <= 1) ? std::numeric_limits::max() : (down_K / down_num_groups); + // dynamic-typed zp = symmetric placeholder. + const bool has_zp = pm.at(gate_zp_m).get_element_type() != ov::element::dynamic; + MOECompressed::Config compressed_config{ - {ov::op::internal::MOE::Expert_type::GEMM3_SWIGLU, 0.0f, expert_beta}, - group_compressed ? weight_shape[2] * weight_shape[3] : weight_shape[2], + {ov::op::internal::MOE::Expert_type::GEMM3_SWIGLU, 0.0f, expert_beta, 0, activation_type}, + gate_K, weight_shape[1], weight_shape[0], 0, // num_shared_expert static_cast(topk_shape[1].get_length()), - group_compressed ? weight_shape[3] : std::numeric_limits::max(), + group_size, has_batch_dim, - false, + has_zp, ov::element::f16, }; @@ -184,7 +215,11 @@ Convert3GatherMatmulMoeBlockToMoeOp::Convert3GatherMatmulMoeBlockToMoeOp(size_t moe_node = moe_compressed; } } else { - ov::op::internal::MOE::Config config{ov::op::internal::MOE::Expert_type::GEMM3_SWIGLU, 0.0f, expert_beta}; + ov::op::internal::MOE::Config config{ov::op::internal::MOE::Expert_type::GEMM3_SWIGLU, + 0.0f, + expert_beta, + 0, + activation_type}; // Plain MOE with 6 inputs ov::OutputVector moe_inputs = {hidden_states, routing, topk_indices, gate_w, up_w, down_w}; @@ -203,11 +238,12 @@ Convert3GatherMatmulMoeBlockToMoeOp::Convert3GatherMatmulMoeBlockToMoeOp(size_t this->register_matcher(matcher, callback); } -Convert2GatherMatmulMoeBlockToMoeOp::Convert2GatherMatmulMoeBlockToMoeOp(size_t has_batch_dim) { +Convert2GatherMatmulMoeBlockToMoeOp::Convert2GatherMatmulMoeBlockToMoeOp(bool has_batch_dim) { MATCHER_SCOPE(Convert2GatherMatmulMoeBlockToMoeOp); - auto experts_reshape_m = pattern::any_input(); - auto unsqueeze_m = pattern::wrap_type({experts_reshape_m, pattern::any_input()}); + auto hidden_states_m = pattern::any_input(); + auto hidden_state_reshape = pattern::optional({hidden_states_m, pattern::any_input()}); + auto unsqueeze_m = pattern::wrap_type({hidden_state_reshape, pattern::any_input()}); auto gate_up_w_m = pattern::any_input(); auto topk_indices_m = pattern::any_input(); @@ -247,9 +283,12 @@ Convert2GatherMatmulMoeBlockToMoeOp::Convert2GatherMatmulMoeBlockToMoeOp(size_t {multiply2_m, down_w_m, topk_indices_m, down_bias_m, down_scale_m, down_zp_m}); auto bgm_down_m = bgm_down_4_m | bgm_down_6_m; - // Compact routing: Transpose → Unsqueeze - auto routing_transpose_m = pattern::wrap_type({pattern::any_input(), pattern::any_input()}); - auto routing_unsqueeze_m = pattern::wrap_type({routing_transpose_m, pattern::any_input()}); + // No-op Reshape between Transpose and Unsqueeze remains in the graph because + // CommonOptimizations run after MoE passes in the GPU pipeline; match it as optional here. + auto routing_transpose_order_m = pattern::wrap_type(pattern::value_matches("1, 0")); + auto routing_transpose_m = pattern::wrap_type({pattern::any_input(), routing_transpose_order_m}); + auto routing_reshape_m = pattern::optional({routing_transpose_m, pattern::any_input()}); + auto routing_unsqueeze_m = pattern::wrap_type({routing_reshape_m, pattern::any_input()}); auto final_mul_m = pattern::wrap_type({bgm_down_m, routing_unsqueeze_m}); auto reduce_sum_m = pattern::wrap_type({final_mul_m, pattern::any_input()}, {{"keep_dims", false}}); @@ -263,10 +302,12 @@ Convert2GatherMatmulMoeBlockToMoeOp::Convert2GatherMatmulMoeBlockToMoeOp(size_t return false; } - auto experts_reshape_node = pm.at(experts_reshape_m).get_node_shared_ptr(); - auto hidden_states = experts_reshape_node->input_value(0); + auto hidden_states = pm.at(hidden_states_m); - auto routing = pm.at(routing_unsqueeze_m).get_node_shared_ptr(); + // Bypass the [1,0] Transpose: moe_scatter_reduction expects tokens-major routing. + // Order is enforced by the pattern (value_matches("1, 0")). + auto routing_transpose_node = pm.at(routing_transpose_m).get_node_shared_ptr(); + ov::Output routing = routing_transpose_node->input_value(0); auto topk_indices = pm.at(topk_indices_m); auto gate_up_w = pm.at(gate_up_w_m); auto gate_up_bias = pm.at(gate_up_bias_m); @@ -278,11 +319,30 @@ Convert2GatherMatmulMoeBlockToMoeOp::Convert2GatherMatmulMoeBlockToMoeOp(size_t float expert_beta = swish_beta_const->cast_vector()[0]; // Extract expert_alpha from Clamp max - float expert_alpha = 0.0f; - auto clamp_node = pm.at(clamp_m).get_node_shared_ptr(); auto clamp_op = ov::as_type_ptr(clamp_node); OPENVINO_ASSERT(clamp_op, "Unexpected node type matched for clamp: ", *clamp_node); + float expert_alpha = static_cast(clamp_op->get_max()); + + // gate_idx = start of the swish (slice2) lane on the step-2 axis. + auto slice2_node = pm.at(slice2_m).get_node_shared_ptr(); + auto slice2_start_c = ov::as_type_ptr(slice2_node->input_value(1).get_node_shared_ptr()); + auto slice2_step_c = ov::as_type_ptr(slice2_node->input_value(3).get_node_shared_ptr()); + if (!slice2_start_c || !slice2_step_c) { + return false; + } + const auto starts = slice2_start_c->cast_vector(); + const auto steps = slice2_step_c->cast_vector(); + if (starts.size() != steps.size()) { + return false; + } + size_t gate_idx = 0; + for (size_t i = 0; i < steps.size(); ++i) { + if (steps[i] == 2) { + gate_idx = static_cast(starts[i]); + break; + } + } std::shared_ptr moe_node; @@ -327,22 +387,28 @@ Convert2GatherMatmulMoeBlockToMoeOp::Convert2GatherMatmulMoeBlockToMoeOp(size_t // Populate compressed config from weight shapes auto weight_shape = gate_up_w.get_shape(); auto scale_shape = pm.at(gate_up_scale_m).get_shape(); - bool group_compressed = (weight_shape.size() == 4); - size_t hidden = group_compressed ? weight_shape[2] * weight_shape[3] : weight_shape[2]; + const size_t hidden = weight_logical_K(weight_shape); auto topk_indices_shape = topk_indices.get_partial_shape(); auto topk_rank = topk_indices_shape.rank().get_length(); OPENVINO_ASSERT(topk_indices_shape[topk_rank - 1].is_static(), "K dimension in moe topk_indices input should be static."); + // group_size derived from down-scale; weight_logical_K handles rank-3/4. + const auto down_K = weight_logical_K(pm.at(down_w_m).get_partial_shape().to_shape()); + const auto down_scale_shape = pm.at(down_scale_m).get_partial_shape().to_shape(); + const size_t down_num_groups = (down_scale_shape.size() >= 3) ? down_scale_shape[2] : 1; + const size_t group_size = + (down_num_groups <= 1) ? std::numeric_limits::max() : (down_K / down_num_groups); + MOECompressed::Config compressed_config{ - {ov::op::internal::MOE::Expert_type::GEMM2_BIAS_SWIGLU_CLAMP, expert_alpha, expert_beta}, + {ov::op::internal::MOE::Expert_type::GEMM2_BIAS_SWIGLU_CLAMP, expert_alpha, expert_beta, gate_idx}, hidden, weight_shape[1], weight_shape[0], 0, // num_shared_expert static_cast(topk_indices_shape[topk_rank - 1].get_length()), - group_compressed ? weight_shape[3] : std::numeric_limits::max(), + group_size, has_batch_dim, has_zp, ov::element::dynamic, @@ -352,7 +418,8 @@ Convert2GatherMatmulMoeBlockToMoeOp::Convert2GatherMatmulMoeBlockToMoeOp(size_t } else { const ov::op::internal::MOE::Config config{ov::op::internal::MOE::Expert_type::GEMM2_BIAS_SWIGLU_CLAMP, expert_alpha, - expert_beta}; + expert_beta, + gate_idx}; // Plain MOE with 7 inputs const ov::OutputVector moe_inputs = {hidden_states, routing, topk_indices, gate_up_w, gate_up_bias, down_w, down_bias}; diff --git a/src/common/transformations/src/transformations/common_optimizations/moe_transpose_weights.cpp b/src/common/transformations/src/transformations/common_optimizations/moe_transpose_weights.cpp index f1eb45e91dfb..514fc227c61b 100644 --- a/src/common/transformations/src/transformations/common_optimizations/moe_transpose_weights.cpp +++ b/src/common/transformations/src/transformations/common_optimizations/moe_transpose_weights.cpp @@ -26,7 +26,7 @@ #include "openvino/pass/pattern/matcher.hpp" #include "openvino/pass/pattern/op/wrap_type.hpp" #include "transformations/rt_info/decompression.hpp" -#include "transformations/rt_info/disable_fp16_compression.hpp" +#include "transformations/rt_info/disable_precision_conversion.hpp" #include "transformations/utils/utils.hpp" using namespace ov::pass; diff --git a/src/common/transformations/src/transformations/common_optimizations/move_eltwise_up_data_movement.cpp b/src/common/transformations/src/transformations/common_optimizations/move_eltwise_up_data_movement.cpp index 503c551cec42..9e5b1db37451 100644 --- a/src/common/transformations/src/transformations/common_optimizations/move_eltwise_up_data_movement.cpp +++ b/src/common/transformations/src/transformations/common_optimizations/move_eltwise_up_data_movement.cpp @@ -12,11 +12,9 @@ #include "openvino/core/rt_info.hpp" #include "openvino/core/type.hpp" #include "openvino/op/batch_to_space.hpp" -#include "openvino/op/broadcast.hpp" #include "openvino/op/constant.hpp" #include "openvino/op/depth_to_space.hpp" #include "openvino/op/fake_quantize.hpp" -#include "openvino/op/gather.hpp" #include "openvino/op/reshape.hpp" #include "openvino/op/reverse_sequence.hpp" #include "openvino/op/roll.hpp" @@ -25,7 +23,8 @@ #include "openvino/op/transpose.hpp" #include "openvino/op/unsqueeze.hpp" #include "openvino/op/util/binary_elementwise_arithmetic.hpp" -#include "openvino/opsets/opset8_decl.hpp" +#include "openvino/op/util/broadcast_base.hpp" +#include "openvino/op/util/gather_base.hpp" #include "openvino/pass/pattern/op/wrap_type.hpp" #include "transformations/utils/utils.hpp" @@ -39,17 +38,9 @@ namespace op_util = ov::op::util; namespace { bool is_data_movement_operation(const std::shared_ptr& node, const std::vector& allowed_data_movement_ops) { - for (auto& allowed_type : allowed_data_movement_ops) { - if (node->get_type_info().is_castable(allowed_type)) - return true; - } - - return false; -} - -bool is_scalar_like(const std::shared_ptr& node) { - auto constant_op = ov::as_type_ptr(node); - return constant_op != nullptr && shape_size(constant_op->get_shape()) == 1; + return std::any_of(allowed_data_movement_ops.begin(), allowed_data_movement_ops.end(), [&](const auto& type) { + return node->get_type_info().is_castable(type); + }); } } // namespace @@ -64,11 +55,8 @@ std::vector ov::pass::MoveEltwiseUpThroughDataMov::get_def v0::ReverseSequence::get_type_info_static(), v0::DepthToSpace::get_type_info_static(), v1::BatchToSpace::get_type_info_static(), - v1::Broadcast::get_type_info_static(), - ov::op::v3::Broadcast::get_type_info_static(), - v1::Gather::get_type_info_static(), - v7::Gather::get_type_info_static(), - ov::op::v8::Gather::get_type_info_static(), + op_util::BroadcastBase::get_type_info_static(), + op_util::GatherBase::get_type_info_static(), }; } @@ -92,7 +80,8 @@ ov::pass::MoveEltwiseUpThroughDataMovScalar::MoveEltwiseUpThroughDataMovScalar( } for (size_t i = 1; i < eltwise->get_input_size(); ++i) { - if (!is_scalar_like(eltwise->get_input_node_shared_ptr(i))) { + if (!ov::op::util::is_scalar_or_single_elem_constant( + ov::as_type_ptr(eltwise->get_input_node_shared_ptr(i)))) { return false; } } @@ -118,10 +107,11 @@ ov::pass::MoveEltwiseUpThroughDataMovScalar::MoveEltwiseUpThroughDataMovScalar( // eltwise constant shape should match new input shape for (size_t i = 1; i < eltwise->get_input_size(); i++) { if (current->get_output_partial_shape(0).size() != eltwise->get_input_partial_shape(i).size()) { - auto old_eltwise_const = ov::as_type_ptr(eltwise->get_input_node_shared_ptr(i)); + auto old_eltwise_const = ov::as_type_ptr(eltwise->get_input_node_shared_ptr(i)); if (old_eltwise_const->get_shape().size() != 0) { - auto new_constant = std::make_shared(*old_eltwise_const.get(), ov::Shape{}); - ov::replace_node_update_name(old_eltwise_const, new_constant); + auto new_constant = std::make_shared(*old_eltwise_const.get(), ov::Shape{}); + ov::copy_runtime_info(old_eltwise_const, new_constant); + eltwise->input(i).replace_source_output(new_constant->output(0)); } } } @@ -153,7 +143,7 @@ ov::pass::MoveEltwiseUpThroughDataMovPerChannel::MoveEltwiseUpThroughDataMovPerC MATCHER_SCOPE(MoveEltwiseUpThroughDataMovPerChannel); auto const_predicate = [](const ov::Output& output) { - auto constant_op = ov::as_type_ptr(output.get_node_shared_ptr()); + auto constant_op = ov::as_type_ptr(output.get_node_shared_ptr()); if (!constant_op) return false; diff --git a/src/common/transformations/src/transformations/common_optimizations/nop_elimination.cpp b/src/common/transformations/src/transformations/common_optimizations/nop_elimination.cpp index e04656c1e8dc..a563fa291083 100644 --- a/src/common/transformations/src/transformations/common_optimizations/nop_elimination.cpp +++ b/src/common/transformations/src/transformations/common_optimizations/nop_elimination.cpp @@ -495,13 +495,17 @@ EliminateIdentityConvert::EliminateIdentityConvert() { auto identity = pattern_map.at(convert_identity_pattern); auto convert_end = pattern_map.at(convert_identity_convert_pattern); - if (convert_begin->get_input_element_type(0) == convert_end->get_element_type()) { - convert_begin->output(0).replace(convert_begin->input_value(0)); + if (convert_begin->get_input_element_type(0) != convert_end->get_element_type()) { + return false; + } + + if (identity->output(0).get_target_inputs().size() == 1) { + identity->input(0).replace_source_output(convert_begin->input_value(0)); identity->validate_and_infer_types(); - convert_end->output(0).replace(identity->output(0)); - return true; + copy_runtime_info({convert_begin, identity, convert_end}, identity); + return replace_output_update_name(convert_end->output(0), identity->output(0)); } - return false; + return replace_output_update_name(convert_end->output(0), convert_begin->input_value(0)); }; auto m = std::make_shared(convert_identity_convert_pattern, matcher_name); this->register_matcher(m, callback); diff --git a/src/common/transformations/src/transformations/common_optimizations/pull_through_reduce.cpp b/src/common/transformations/src/transformations/common_optimizations/pull_through_reduce.cpp index 3cff0083c69e..3d4035df1f61 100644 --- a/src/common/transformations/src/transformations/common_optimizations/pull_through_reduce.cpp +++ b/src/common/transformations/src/transformations/common_optimizations/pull_through_reduce.cpp @@ -62,6 +62,7 @@ const std::vector adjust_axes(const std::vector& axes_to_align // - Reshape(input_shape={5,10,15}, target_shape={1,5,10,15}), 0 axis returned // - Reshape(input_shape={5,10,15}, target_shape={1,5,10,15,1}), 0 and 3 axes returned // - Reshape(input_shape={5,10,15}, target_shape={5,10,1,15}), 2 axis is returned +// - Reshape(input_shape={2,16}, target_shape={1,2,4,4}), empty returned (last dim is split, not a pure unsqueeze) std::vector try_get_unsqueeze_axes_from_reshape(const ov::Shape& target_shape, const ov::Shape& input_shape) { std::vector result; if (target_shape.size() <= input_shape.size()) { @@ -86,11 +87,18 @@ std::vector try_get_unsqueeze_axes_from_reshape(const ov::Shape& target } } if (cur_input_shape_elem_idx == input_shape.size() - 1 && target_shape_idx == target_shape.size()) { + size_t input_dim_idx = 0; + for (size_t t = 0; t < target_shape.size(); ++t) { + if (std::find(result.begin(), result.end(), static_cast(t)) == result.end()) { + if (input_dim_idx >= input_shape.size() || target_shape[t] != input_shape[input_dim_idx++]) { + return {}; + } + } + } return result; } else { return {}; } - return result; } // Update given reshape_input_shape by inserting "1" dimension on the postion represented by axes_to_insert diff --git a/src/common/transformations/src/transformations/common_optimizations/rms_fusion.cpp b/src/common/transformations/src/transformations/common_optimizations/rms_fusion.cpp index 5ee4a646eb00..e2179c16f559 100644 --- a/src/common/transformations/src/transformations/common_optimizations/rms_fusion.cpp +++ b/src/common/transformations/src/transformations/common_optimizations/rms_fusion.cpp @@ -66,7 +66,13 @@ RMSFusion::RMSFusion(bool force_tail_convert, bool enable_div_x, bool enable_wit auto const_div = pattern::wrap_type(pattern::value_matches("1")); auto const_div_convert = pattern::optional(const_div); auto div = pattern::wrap_type({const_div_convert, sqrt}); - auto div_or_pow = std::make_shared(OutputVector{div, pow}); + + // Power(ReduceMean(x^2,axes)+eps, -0.5) — direct rsqrt without Sqrt node + auto const_neg_half = pattern::wrap_type(pattern::value_matches("-0.5")); + auto const_neg_half_convert = pattern::optional(const_neg_half); + auto pow_direct = pattern::wrap_type({add_eps, const_neg_half_convert}); + + std::shared_ptr div_or_pow = std::make_shared(OutputVector{div, pow, pow_direct}); // x * 1/Sqrt(ReduceMean(x^2,axes)+eps) auto mul1 = pattern::wrap_type({x, div_or_pow}); diff --git a/src/common/transformations/src/transformations/common_optimizations/sdpa_fusion.cpp b/src/common/transformations/src/transformations/common_optimizations/sdpa_fusion.cpp index 52e526dc627c..0d10de551cd7 100644 --- a/src/common/transformations/src/transformations/common_optimizations/sdpa_fusion.cpp +++ b/src/common/transformations/src/transformations/common_optimizations/sdpa_fusion.cpp @@ -523,12 +523,12 @@ SDPAFusionMatcher::SDPAFusionMatcher() { // Align output shapes by inserting Squeeze/Unsqueeze nodes as needed when the fused SDPA output rank // differs from the original output rank. - sdpa = try_align_outputs(sdpa, m.get_match_root()); - if (!sdpa) { + const auto aligned_output = try_align_outputs(sdpa, m.get_match_root()); + if (!aligned_output) { return false; } - ov::replace_node(m.get_match_root(), sdpa); + ov::replace_node(m.get_match_root(), aligned_output); return true; }; diff --git a/src/common/transformations/src/transformations/common_optimizations/shared_ops_optimization.cpp b/src/common/transformations/src/transformations/common_optimizations/shared_ops_optimization.cpp index 9a56d2f4e4bd..281a4dcf9d29 100644 --- a/src/common/transformations/src/transformations/common_optimizations/shared_ops_optimization.cpp +++ b/src/common/transformations/src/transformations/common_optimizations/shared_ops_optimization.cpp @@ -39,7 +39,11 @@ class NodeComparingVisitor : public ov::AttributeVisitor { ACCESSOR_V(double) void on_adapter(const std::string& name, ValueAccessor& adapter) override { - OPENVINO_THROW_NOT_IMPLEMENTED("Can not compare void"); + if (auto a = ov::as_type>>(&adapter)) { + m_attributes_map.insert({name, a->get()}); + } else { + OPENVINO_THROW_NOT_IMPLEMENTED("Can not compare void"); + } }; void on_adapter(const std::string& name, ValueAccessor& adapter) override { OPENVINO_THROW_NOT_IMPLEMENTED("Can not compare void*"); diff --git a/src/common/transformations/src/transformations/common_optimizations/transpose_sinking.cpp b/src/common/transformations/src/transformations/common_optimizations/transpose_sinking.cpp index a883ba127592..60a667a72ac3 100644 --- a/src/common/transformations/src/transformations/common_optimizations/transpose_sinking.cpp +++ b/src/common/transformations/src/transformations/common_optimizations/transpose_sinking.cpp @@ -78,6 +78,77 @@ std::shared_ptr get_reversed_order_constant(const std::shared_ptr< } // namespace +ov::pass::TransposeFQ::TransposeFQ() { + MATCHER_SCOPE(TransposeFQ); + + auto transpose_order_m = wrap_type(); + auto transpose_label = wrap_type({any_input(pattern::has_static_rank()), transpose_order_m}); + auto fq_label = wrap_type({transpose_label, + any_input(ov::pass::pattern::has_static_rank()), + any_input(ov::pass::pattern::has_static_rank()), + any_input(ov::pass::pattern::has_static_rank()), + any_input(ov::pass::pattern::has_static_rank())}, + consumers_count(1)); + + matcher_pass_callback matcher_pass_callback = [OV_CAPTURE_CPY_AND_THIS](Matcher& m) { + auto& pattern_to_output = m.get_pattern_value_map(); + + auto transpose = pattern_to_output.at(transpose_label).get_node_shared_ptr(); + auto fq = pattern_to_output.at(fq_label).get_node_shared_ptr(); + auto transpose_order = + ov::as_type_ptr(pattern_to_output.at(transpose_order_m).get_node_shared_ptr()); + if (!transpose_order || !fq) + return false; + + ov::NodeVector new_ops; + + const auto& reverse_order_constant = get_reversed_order_constant(transpose_order); + new_ops.push_back(reverse_order_constant); + + const auto& input_rank = fq->get_input_partial_shape(0).rank().get_length(); + ov::OutputVector fq_inputs = {transpose->input_value(0)}; + for (size_t i = 1; i < fq->inputs().size(); ++i) { + auto input = fq->input_value(i); + if (ov::shape_size(input.get_shape()) == 1) { + fq_inputs.push_back(input); + continue; + } + + const auto& range_rank = input.get_partial_shape().rank().get_length(); + if (range_rank > input_rank) + return false; + + const auto& ranks_diff = input_rank - range_rank; + if (ranks_diff > 0) { + std::vector axes(ranks_diff); + std::iota(axes.begin(), axes.end(), 0); + const auto& axes_const = v0::Constant::create(element::i64, Shape{axes.size()}, axes); + new_ops.push_back(axes_const); + const auto& unsqueezed_input = op_util::make_try_fold(input, axes_const); + new_ops.push_back(unsqueezed_input); + input = unsqueezed_input->output(0); + } + const auto& transposed_input = op_util::make_try_fold(input, reverse_order_constant); + new_ops.push_back(transposed_input); + fq_inputs.push_back(transposed_input); + } + + auto new_fq = fq->clone_with_new_inputs(fq_inputs); + new_ops.push_back(new_fq); + + auto new_transpose = register_new_node(new_fq, transpose_order); + new_ops.push_back(new_transpose); + new_transpose->set_friendly_name(fq->get_friendly_name()); + + ov::copy_runtime_info({fq, transpose}, new_ops); + ov::replace_node(fq, new_transpose); + return true; + }; + + auto m = std::make_shared(fq_label, matcher_name); + register_matcher(m, matcher_pass_callback); +} + ov::pass::TransposeEltwise::TransposeEltwise() { MATCHER_SCOPE(TransposeEltwise); diff --git a/src/common/transformations/src/transformations/control_flow/unroll_tensor_iterator.cpp b/src/common/transformations/src/transformations/control_flow/unroll_tensor_iterator.cpp index afe51852ac97..d9fc4f84f72a 100644 --- a/src/common/transformations/src/transformations/control_flow/unroll_tensor_iterator.cpp +++ b/src/common/transformations/src/transformations/control_flow/unroll_tensor_iterator.cpp @@ -193,7 +193,8 @@ bool ov::pass::UnrollTensorIterator::run_on_model(const std::shared_ptrget_special_body_ports().current_iteration_input_idx; const auto& param_to_delete = body_functions[idx]->get_parameters()[iter_idx]; - auto cur_iter_const = std::make_shared(ov::element::i64, Shape{}, idx); + auto cur_iter_const = + std::make_shared(param_to_delete->get_element_type(), Shape{}, idx); replace_node(param_to_delete, cur_iter_const); body_functions[idx]->remove_parameter(param_to_delete); } diff --git a/src/common/transformations/src/transformations/convert_precision.cpp b/src/common/transformations/src/transformations/convert_precision.cpp index 544ca36fad29..254971650fa9 100644 --- a/src/common/transformations/src/transformations/convert_precision.cpp +++ b/src/common/transformations/src/transformations/convert_precision.cpp @@ -11,17 +11,19 @@ #include "openvino/core/graph_util.hpp" #include "openvino/core/rt_info/weightless_caching_attributes.hpp" #include "openvino/core/type/element_iterator.hpp" +#include "openvino/core/weight_sharing_util.hpp" #include "openvino/op/ops.hpp" #include "openvino/pass/constant_folding.hpp" #include "openvino/pass/manager.hpp" #include "openvino/reference/convert.hpp" +#include "openvino/util/common_util.hpp" #include "ov_ops/rms.hpp" #include "ov_ops/type_relaxed.hpp" #include "transformations/fp16_compression/align_mixed_fp32_fp16_types.hpp" #include "transformations/fp16_compression/mark_decompression_convert_constant_folding.hpp" #include "transformations/fp16_compression/mark_subgraphs_to_keep_in_mixed_precision.hpp" #include "transformations/rt_info/decompression.hpp" -#include "transformations/rt_info/disable_fp16_compression.hpp" +#include "transformations/rt_info/disable_precision_conversion.hpp" #include "transformations/rt_info/keep_const_precision.hpp" #include "transformations/rt_info/original_precision_attribute.hpp" #include "transformations/utils/utils.hpp" @@ -175,6 +177,15 @@ bool node_is_replaced(const std::shared_ptr& node) { return has_consumers && !(is_type(node) || is_type(node)); } +static precisions_map filter_precisions_for_node(const std::shared_ptr& node, + const precisions_map& precisions) { + precisions_map result = precisions; + ov::util::erase_if(result, [&](const precisions_map::value_type& entry) { + return is_conversion_disabled(node, entry.first, entry.second); + }); + return result; +} + bool convert_node_output_precision( const std::shared_ptr& node, const precisions_map& precisions, @@ -219,8 +230,7 @@ bool convert_function_precision(ov::pass::PassBase& pass, const type_to_fuse_map& type_to_extend, const precisions_map& precisions, std::unordered_map>>& const_to_internal_output, - bool has_fp16_compression, - bool skip_precision_sensitive, + bool keep_sensitive_in_fp32, bool is_changed, bool is_subgraph, bool convert_input_output_precision, @@ -228,7 +238,7 @@ bool convert_function_precision(ov::pass::PassBase& pass, bool names_compatibility_mode) { bool is_output_precision_changed = false; - if (skip_precision_sensitive && has_fp16_compression) { + if (keep_sensitive_in_fp32 && precisions.count(element::f32) && precisions.at(element::f32) == element::f16) { pass::Manager manager(pass.get_pass_config(), "KeepPrecisionSensitiveInFP32"); // Mark subgraphs with disable_fp16_compression to keep them in FP32 manager.register_pass(); @@ -250,15 +260,13 @@ bool convert_function_precision(ov::pass::PassBase& pass, // otherwise we insert Convert operation. auto ops = f->get_ordered_ops(); for (auto& node : ops) { - if (skip_precision_sensitive && fp16_compression_is_disabled(node) && has_fp16_compression) - continue; - is_changed = convert_node_input_precision(node, precisions, type_to_extend) || is_changed; + auto node_precisions = filter_precisions_for_node(node, precisions); + is_changed = convert_node_input_precision(node, node_precisions, type_to_extend) || is_changed; } for (const auto& param : f->get_parameters()) { - if (skip_precision_sensitive && fp16_compression_is_disabled(param) && has_fp16_compression) - continue; - is_changed = fuse_type_to_parameter(param, precisions, convert_input_output_precision) || is_changed; + auto node_precisions = filter_precisions_for_node(param, precisions); + is_changed = fuse_type_to_parameter(param, node_precisions, convert_input_output_precision) || is_changed; } if (convert_input_output_precision || store_original_precision_as_rt_attribute) { @@ -288,11 +296,16 @@ bool convert_function_precision(ov::pass::PassBase& pass, // to the freshly allocated replacement. Without the std::move the original // constants stay alive until the loop ends, doubling the peak memory of // the pass on constant-heavy models. + for (size_t i = 0; i < ops.size(); ++i) { auto node = std::move(ops[i]); - // skip precision sensitive nodes - if (skip_precision_sensitive && fp16_compression_is_disabled(node) && has_fp16_compression) + auto node_precisions = filter_precisions_for_node(node, precisions); + + // Skip nodes where all requested conversions are disabled (e.g. sensitive ops kept in FP32) + if (keep_sensitive_in_fp32 && node_precisions.empty()) { continue; + } + // Recursively apply transformation for sub-graph based operations if (auto sub_graph_node = ov::as_type_ptr(node)) { size_t sub_graphs_num = sub_graph_node->get_internal_subgraphs_size(); @@ -303,11 +316,10 @@ bool convert_function_precision(ov::pass::PassBase& pass, type_to_extend, precisions, const_to_internal_output, - has_fp16_compression, - skip_precision_sensitive, + keep_sensitive_in_fp32, is_changed || is_output_precision_changed, - true, - true, + true, // is_changed + true, // is_subgraph store_original_precision_as_rt_attribute, names_compatibility_mode) || is_changed; @@ -322,7 +334,7 @@ bool convert_function_precision(ov::pass::PassBase& pass, continue; } is_output_precision_changed = convert_node_output_precision(node, - precisions, + node_precisions, type_to_fuse, const_to_internal_output, is_changed || is_output_precision_changed) || @@ -390,8 +402,7 @@ bool convert_precision(ov::pass::PassBase& pass, const type_to_fuse_map& type_to_fuse, const type_to_fuse_map& type_to_extend, const precisions_map& precisions, - bool has_fp16_compression, - bool skip_precision_sensitive, + bool keep_sensitive_in_fp32, bool convert_input_output_precision, bool store_original_precision_as_rt_attribute) { // As Constant operations can be shared between multiple ov::Models so before @@ -406,8 +417,7 @@ bool convert_precision(ov::pass::PassBase& pass, type_to_extend, precisions, const_to_internal_output, - has_fp16_compression, - skip_precision_sensitive, + keep_sensitive_in_fp32, false, false, convert_input_output_precision, @@ -451,8 +461,6 @@ bool ov::pass::ConvertPrecision::run_on_model(const std::shared_ptr& if (used_precisions.empty()) return false; - bool has_fp16_compression = m_precisions.count(element::f32) > 0 && m_precisions[element::f32] == element::f16; - type_to_fuse_map type_to_fuse{ {v0::Convert::get_type_info_static(), fuse_type_to_convert}, {v3::ShapeOf::get_type_info_static(), fuse_type_to_shapeof}, @@ -524,7 +532,6 @@ bool ov::pass::ConvertPrecision::run_on_model(const std::shared_ptr& type_to_fuse, type_to_extend, used_precisions, - has_fp16_compression, m_keep_precision_sensitive_in_fp32, m_convert_input_output_precision, m_store_original_precision_as_rt_attribute); @@ -609,7 +616,7 @@ bool fuse_type_to_range_v4(const std::shared_ptr& node, const precisio return false; const auto& to = it->second; if (auto range = ov::as_type_ptr(node)) { - if ((to == ov::element::f16 && !fp16_compression_is_disabled(node)) || to.is_integral_number()) { + if (to.is_real() || to.is_integral_number()) { range->set_output_type(to); return true; } @@ -653,11 +660,7 @@ bool fuse_type_to_parameter(const std::shared_ptr& node, auto convert = std::make_shared(param, to); for (auto& input : param_consumers) { const auto consumer = input.get_node(); - if (ov::is_type(consumer) || ov::is_type(consumer) || - // TODO: refactor after ngraph op defined - // The fourth and fifth inputs are kvcache and should be directly connected to parameters - (consumer->get_type_name() == std::string("PagedAttentionExtension") && - (input.get_index() == 3 || input.get_index() == 4))) { + if (ov::is_type(consumer) || ov::is_type(consumer)) { continue; } input.replace_source_output(convert); @@ -1374,6 +1377,7 @@ bool fuse_type_to_constant(const std::shared_ptr& node, new_const->set_friendly_name(constant->get_friendly_name()); ov::copy_runtime_info(constant, new_const); ov::copy_weightless_cache_attr(constant, new_const); + ov::wsh::Extension::hint_evict(*constant); return true; } return false; diff --git a/src/common/transformations/src/transformations/fp16_compression/align_mixed_fp32_fp16_types.cpp b/src/common/transformations/src/transformations/fp16_compression/align_mixed_fp32_fp16_types.cpp index b235b869bc9f..322543e3024e 100644 --- a/src/common/transformations/src/transformations/fp16_compression/align_mixed_fp32_fp16_types.cpp +++ b/src/common/transformations/src/transformations/fp16_compression/align_mixed_fp32_fp16_types.cpp @@ -10,7 +10,7 @@ #include "openvino/op/result.hpp" #include "openvino/op/util/precision_sensitive_attribute.hpp" #include "openvino/pass/constant_folding.hpp" -#include "transformations/rt_info/disable_fp16_compression.hpp" +#include "transformations/rt_info/disable_precision_conversion.hpp" using namespace ov; @@ -36,7 +36,7 @@ bool ov::pass::AlignMixedFP32FP16Types::run_on_model(const std::shared_ptrset_friendly_name(generate_uniq_name(init_name)); copy_runtime_info(incoming_node, convert); input.replace_source_output(convert); - disable_fp16_compression(convert); + disable_conversion(convert, element::f16); pass::disable_constant_folding(convert); is_changed = true; } @@ -60,7 +60,7 @@ bool ov::pass::AlignMixedFP32FP16Types::run_on_model(const std::shared_ptroutputs()) { for (const auto& out_inputs : output.get_target_inputs()) { auto out_node = out_inputs.get_node()->shared_from_this(); - if (fp16_compression_is_disabled(out_node) || is_precision_sensitive(out_inputs)) + if (is_conversion_disabled(out_node, ov::element::f16) || is_precision_sensitive(out_inputs)) continue; if (!out_inputs.get_element_type().is_real()) continue; @@ -81,7 +81,7 @@ bool ov::pass::AlignMixedFP32FP16Types::run_on_model(const std::shared_ptrget_ordered_ops()) { - if (!fp16_compression_is_disabled(node)) + if (!is_conversion_disabled(node, element::f16)) continue; is_changed = insert_converts_before_if_needed(node) || is_changed; diff --git a/src/common/transformations/src/transformations/fp16_compression/convert_compression_only_to_legacy.cpp b/src/common/transformations/src/transformations/fp16_compression/convert_compression_only_to_legacy.cpp index ba9529ad6bd8..0220c95f2c36 100644 --- a/src/common/transformations/src/transformations/fp16_compression/convert_compression_only_to_legacy.cpp +++ b/src/common/transformations/src/transformations/fp16_compression/convert_compression_only_to_legacy.cpp @@ -9,7 +9,7 @@ #include "openvino/pass/pattern/op/wrap_type.hpp" #include "transformations/convert_precision.hpp" #include "transformations/fp16_compression/mark_decompression_convert_constant_folding.hpp" -#include "transformations/rt_info/disable_fp16_compression.hpp" +#include "transformations/rt_info/disable_precision_conversion.hpp" #include "transformations/utils/utils.hpp" using namespace ov; diff --git a/src/common/transformations/src/transformations/fp16_compression/convert_legacy_precision_attribute.cpp b/src/common/transformations/src/transformations/fp16_compression/convert_legacy_precision_attribute.cpp new file mode 100644 index 000000000000..d60b835be8d4 --- /dev/null +++ b/src/common/transformations/src/transformations/fp16_compression/convert_legacy_precision_attribute.cpp @@ -0,0 +1,30 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "transformations/fp16_compression/convert_legacy_precision_attribute.hpp" + +#include "itt.hpp" +#include "openvino/op/util/multi_subgraph_base.hpp" +#include "transformations/rt_info/disable_precision_conversion.hpp" + +bool ov::pass::ConvertLegacyPrecisionAttribute::run_on_model(const std::shared_ptr& model) { + RUN_ON_MODEL_SCOPE(ConvertLegacyPrecisionAttribute); + bool changed = false; + for (const auto& node : model->get_ordered_ops()) { + OPENVINO_SUPPRESS_DEPRECATED_START + if (node->get_rt_info().count(DisableFP16Compression::get_type_info_static())) { + node->get_rt_info().erase(DisableFP16Compression::get_type_info_static()); // remove legacy attribute + OPENVINO_SUPPRESS_DEPRECATED_END + disable_conversion(node, element::f16); + changed = true; + } + + if (auto sub_graph_node = ov::as_type_ptr(node)) { + for (size_t i = 0; i < sub_graph_node->get_internal_subgraphs_size(); ++i) { + changed = run_on_model(sub_graph_node->get_function(static_cast(i))) || changed; + } + } + } + return changed; +} diff --git a/src/common/transformations/src/transformations/fp16_compression/mark_decompression_convert_constant_folding.cpp b/src/common/transformations/src/transformations/fp16_compression/mark_decompression_convert_constant_folding.cpp index 1ae30f28e876..a04dfdfefc6c 100644 --- a/src/common/transformations/src/transformations/fp16_compression/mark_decompression_convert_constant_folding.cpp +++ b/src/common/transformations/src/transformations/fp16_compression/mark_decompression_convert_constant_folding.cpp @@ -12,7 +12,7 @@ #include "openvino/pass/pattern/op/wrap_type.hpp" #include "transformations/rt_info/decompression.hpp" #include "transformations/rt_info/disable_constant_folding.hpp" -#include "transformations/rt_info/disable_fp16_compression.hpp" +#include "transformations/rt_info/disable_precision_conversion.hpp" #include "transformations/rt_info/is_shape_subgraph.hpp" #include "transformations/rt_info/keep_const_precision.hpp" #include "transformations/utils/utils.hpp" diff --git a/src/common/transformations/src/transformations/fp16_compression/mark_floatpoint_range.cpp b/src/common/transformations/src/transformations/fp16_compression/mark_floatpoint_range.cpp index 445617600432..470bd573abd0 100644 --- a/src/common/transformations/src/transformations/fp16_compression/mark_floatpoint_range.cpp +++ b/src/common/transformations/src/transformations/fp16_compression/mark_floatpoint_range.cpp @@ -15,7 +15,7 @@ #include "openvino/op/squeeze.hpp" #include "openvino/op/unsqueeze.hpp" #include "openvino/pass/pattern/op/wrap_type.hpp" -#include "transformations/rt_info/disable_fp16_compression.hpp" +#include "transformations/rt_info/disable_precision_conversion.hpp" #include "transformations/utils/utils.hpp" namespace v0 = ov::op::v0; @@ -60,11 +60,11 @@ MarkFloatingPointRange::MarkFloatingPointRange() { auto range = ov::as_type_ptr(node); if (range && range->get_output_type().is_real()) { mark_range_path(node); - ov::disable_fp16_compression(node); + ov::disable_conversion(node, element::f16); // mark inputs as well for (const auto& range_input : range->input_values()) { - ov::disable_fp16_compression(range_input.get_node_shared_ptr()); + ov::disable_conversion(range_input.get_node_shared_ptr(), element::f16); } is_changed = true; } else { @@ -73,7 +73,7 @@ MarkFloatingPointRange::MarkFloatingPointRange() { if (is_range_path(input_node)) { mark_range_path(node); - ov::disable_fp16_compression(node); + ov::disable_conversion(node, element::f16); is_changed = true; break; } diff --git a/src/common/transformations/src/transformations/fp16_compression/mark_subgraphs_to_keep_in_mixed_precision.cpp b/src/common/transformations/src/transformations/fp16_compression/mark_subgraphs_to_keep_in_mixed_precision.cpp index c93687e563c9..5d3cdc8ab04e 100644 --- a/src/common/transformations/src/transformations/fp16_compression/mark_subgraphs_to_keep_in_mixed_precision.cpp +++ b/src/common/transformations/src/transformations/fp16_compression/mark_subgraphs_to_keep_in_mixed_precision.cpp @@ -51,7 +51,7 @@ #include "openvino/pass/pattern/op/wrap_type.hpp" #include "transformations/common_optimizations/mark_precision_sensitive_shapeof_subgraphs.hpp" #include "transformations/fp16_compression/mark_floatpoint_range.hpp" -#include "transformations/rt_info/disable_fp16_compression.hpp" +#include "transformations/rt_info/disable_precision_conversion.hpp" #include "transformations/utils/utils.hpp" using namespace std; @@ -130,7 +130,7 @@ class PropagateUpMarkToKeepInMixedPrecision : public MatcherPass { for (const auto& output : node->outputs()) { for (const auto& out_inputs : output.get_target_inputs()) { if (out_inputs.get_element_type().is_real() && - fp16_compression_is_disabled(out_inputs.get_node()->shared_from_this())) { + is_conversion_disabled(out_inputs.get_node()->shared_from_this(), element::f16)) { has_marked_output = true; } } @@ -148,7 +148,7 @@ class PropagateUpMarkToKeepInMixedPrecision : public MatcherPass { return false; } - disable_fp16_compression(node); + disable_conversion(node, element::f16); return true; }; @@ -182,12 +182,12 @@ class PropagateDownMarkToKeepInMixedPrecision : public MatcherPass { if (!in_node.get_element_type().is_real()) continue; if (is_fq_path(in_node.get_node_shared_ptr())) { - enable_fp16_compression(node); + enable_conversion(node, element::f16); return true; } - if (fp16_compression_is_disabled(in_node.get_node_shared_ptr())) { - disable_fp16_compression(node); + if (is_conversion_disabled(in_node.get_node_shared_ptr(), element::f16)) { + disable_conversion(node, element::f16); is_changed = true; break; } @@ -262,7 +262,7 @@ class MarkExp : public MatcherPass { if (!is_reduceop_path(node)) return false; - disable_fp16_compression(node); + disable_conversion(node, element::f16); return true; }; auto m = make_shared(exp_pattern, matcher_name); @@ -283,7 +283,7 @@ class MarkRandomUniform : public MatcherPass { if (!node) return false; - disable_fp16_compression(node); + disable_conversion(node, element::f16); for (const auto& output : node->outputs()) { auto target_inputs = output.get_target_inputs(); for (const auto& input : target_inputs) { @@ -390,7 +390,7 @@ class MarkDivWithEps : public MatcherPass { if (val > float16_min_normalized) return false; } - disable_fp16_compression(m.get_match_root()); + disable_conversion(m.get_match_root(), element::f16); return true; }; @@ -448,7 +448,7 @@ class PropagateDownDisableSensitivityForQuantized : public MatcherPass { auto is_quantize = as_type_ptr(input_node); if (is_quantize || is_fq_path(input_node)) { mark_fq_path(node); - enable_fp16_compression(node); + enable_conversion(node, element::f16); is_changed = true; } } diff --git a/src/common/transformations/src/transformations/low_precision/mark_dequantization_subgraph.cpp b/src/common/transformations/src/transformations/low_precision/mark_dequantization_subgraph.cpp index 06e85d0f11ea..46c923998dfd 100644 --- a/src/common/transformations/src/transformations/low_precision/mark_dequantization_subgraph.cpp +++ b/src/common/transformations/src/transformations/low_precision/mark_dequantization_subgraph.cpp @@ -378,7 +378,7 @@ KeepDequantizationPrecision::KeepDequantizationPrecision(const element::TypeVect for (const auto& node_to_mark : nodes_to_mark) { if (pt_map.count(node_to_mark)) { auto node_ptr = pt_map.at(node_to_mark).get_node_shared_ptr(); - disable_fp16_compression(node_ptr); + ov::disable_conversion(node_ptr, element::f16); } } diff --git a/src/common/transformations/src/transformations/op_conversions/convert_fc_to_compressed.cpp b/src/common/transformations/src/transformations/op_conversions/convert_fc_to_compressed.cpp index 54a7a57ed7fe..096b61bd9529 100644 --- a/src/common/transformations/src/transformations/op_conversions/convert_fc_to_compressed.cpp +++ b/src/common/transformations/src/transformations/op_conversions/convert_fc_to_compressed.cpp @@ -16,6 +16,7 @@ #include "openvino/op/reshape.hpp" #include "openvino/op/subtract.hpp" #include "openvino/op/transpose.hpp" +#include "openvino/op/unsqueeze.hpp" #include "openvino/pass/pattern/op/or.hpp" #include "openvino/pass/pattern/op/pattern.hpp" #include "openvino/pass/pattern/op/wrap_type.hpp" @@ -98,23 +99,58 @@ ConvertFullyConnectedToFullyConnectedCompressed::process_compressed_weights( const auto& transpose = weights_block->get_anchor("transpose", pattern_map).value().get_node_shared_ptr(); std::shared_ptr transpose_const = weights_block->get_anchor("transpose_const", pattern_map).value().get_node_shared_ptr(); - if (ov::shape_size(transpose_const->get_shape()) != fc_input_b->get_output_partial_shape(0).size()) { - std::vector new_order(fc_input_b->get_output_partial_shape(0).size()); - std::iota(new_order.begin(), new_order.end(), 0); - std::swap(new_order[new_order.size() - 1], new_order[new_order.size() - 2]); - transpose_const = std::make_shared(ov::element::i32, ov::Shape{new_order.size()}, new_order); - } - fc_input_b = transpose->clone_with_new_inputs({fc_input_b->output(0), transpose_const}); - ov::disable_constant_folding(fc_input_b); - result_nodes.push_back(fc_input_b); - fc_input_scale = transpose->clone_with_new_inputs({scale->output(0), transpose_const}); - ov::disable_constant_folding(fc_input_scale); - result_nodes.push_back(fc_input_scale); + // The matched `transpose_const` was authored for the weights tensor and may have a + // different rank than `scale` / `zero_point` (which can be per-channel rank-1 + // constants while the weights are rank-2). Align each input's rank, then build a + // perm that matches it; rank-1 per-channel constants are unsqueezed to rank-2 + // [N, 1] so that downstream consumers (e.g. DnnlPostOpsComposer in the CPU plugin) + // which require rank-2/3 decompression params can prepack them. Inputs with a + // single element are left as-is. + // All inputs reaching this lambda are Constants (optionally wrapped in a Convert + // injected by `convert_u4const_to_u8`), so their shapes are always static. + auto align_and_transpose = [&](const ov::Output& in) -> std::shared_ptr { + const auto& in_shape = in.get_shape(); + const auto in_rank = in_shape.size(); + if (in_rank == 0 || ov::shape_size(in_shape) == 1) { + return in.get_node_shared_ptr(); + } + std::shared_ptr node = in.get_node_shared_ptr(); + if (in_rank == 1) { + // Promote rank-1 per-channel constant to rank-2 [N, 1] via Unsqueeze. + // Peel a wrapping Convert (injected by convert_u4const_to_u8 for u4 ZP) + // so make_try_fold can collapse Unsqueeze on the underlying Constant, + // then re-apply the Convert on the folded result. + auto wrapping_convert = ov::as_type_ptr(node); + auto inner = wrapping_convert ? wrapping_convert->get_input_node_shared_ptr(0) : node; + auto axis = v0::Constant::create(ov::element::i32, ov::Shape{}, {1}); + auto unsqueezed = ov::op::util::make_try_fold(inner, axis); + result_nodes.push_back(unsqueezed); + if (wrapping_convert) { + auto rewrapped = + std::make_shared(unsqueezed, wrapping_convert->get_destination_type()); + result_nodes.push_back(rewrapped); + return rewrapped; + } + return unsqueezed; + } + std::shared_ptr perm = transpose_const; + if (ov::shape_size(perm->get_shape()) != in_rank) { + std::vector new_order(in_rank); + std::iota(new_order.begin(), new_order.end(), 0); + std::swap(new_order[in_rank - 1], new_order[in_rank - 2]); + perm = std::make_shared(ov::element::i32, ov::Shape{in_rank}, new_order); + } + auto transposed = transpose->clone_with_new_inputs({node->output(0), perm}); + ov::disable_constant_folding(transposed); + result_nodes.push_back(transposed); + return transposed; + }; + + fc_input_b = align_and_transpose(fc_input_b->output(0)); + fc_input_scale = align_and_transpose(scale->output(0)); if (with_zero_point && ov::shape_size(optional_zero_point->output(0).get_shape()) > 1) { - fc_input_zp = transpose->clone_with_new_inputs({optional_zero_point->output(0), transpose_const}); - ov::disable_constant_folding(fc_input_zp); - result_nodes.push_back(fc_input_zp); + fc_input_zp = align_and_transpose(optional_zero_point->output(0)); } } diff --git a/src/common/transformations/src/transformations/op_conversions/group_query_attention_decomposition.cpp b/src/common/transformations/src/transformations/op_conversions/group_query_attention_decomposition.cpp index ce1d86b258d4..58a805b78261 100644 --- a/src/common/transformations/src/transformations/op_conversions/group_query_attention_decomposition.cpp +++ b/src/common/transformations/src/transformations/op_conversions/group_query_attention_decomposition.cpp @@ -21,14 +21,15 @@ #include "openvino/op/range.hpp" #include "openvino/op/reshape.hpp" #include "openvino/op/scaled_dot_product_attention.hpp" +#include "openvino/op/scatter_update.hpp" #include "openvino/op/select.hpp" #include "openvino/op/shape_of.hpp" #include "openvino/op/slice.hpp" -#include "openvino/op/split.hpp" #include "openvino/op/squeeze.hpp" #include "openvino/op/subtract.hpp" #include "openvino/op/transpose.hpp" #include "openvino/op/unsqueeze.hpp" +#include "openvino/op/variadic_split.hpp" #include "openvino/pass/pattern/op/wrap_type.hpp" using ov::pass::pattern::Matcher; @@ -110,7 +111,10 @@ ov::OutputVector ov::pass::GroupQueryAttentionDecomposition::decompose( ov::Output position_ids = register_new_node(zero_without_shape, curr_seqlen_scalar, one_without_shape, ov::element::i64); if (node->get_input_size() > 9 && !is_null(node->input_value(9))) { - position_ids = node->input_value(9).get_node_shared_ptr(); + // Flatten position_ids to 1D so that Gather produces 2D [seqlen, head_size/2] output, + // ensuring correct 4D shapes after Unsqueeze in rotaryEmbedding. + const auto neg_one = register_new_node(v0::Constant::create(ov::element::i64, ov::Shape{1}, {-1})); + position_ids = register_new_node(node->input_value(9), neg_one, false); } else { position_ids = register_new_node(position_ids, past_seqlen); } @@ -122,27 +126,33 @@ ov::OutputVector ov::pass::GroupQueryAttentionDecomposition::decompose( } const auto is_static_input = K.get_partial_shape().is_static() && past_key.get_partial_shape().is_static(); - auto construct_kv_cache = [&](const ov::Output& past, const ov::Output& current) { - return register_new_node(ov::OutputVector{past, current}, 2); - }; if (is_static_input) { - // Cache memory layout for static shapes: - // - Keys: [0, ..., 0, past_key[0], ..., past_key[N-1], K[0], ..., K[M-1]] - // - Values: [0, ..., 0, past_value[0], ..., past_value[N-1], V[0], ..., V[M-1]] - // Here, padding 0 are lay on front of the buffer. - // M = current_seqlen, which is always 1 for the KV cache model. - const auto current_kv_len_const = register_new_node( - v0::Constant::create(ov::element::i64, ov::Shape{1}, {K.get_partial_shape()[2].get_length()})); - const auto past_kv_len_const = register_new_node( - v0::Constant::create(ov::element::i64, ov::Shape{1}, {past_key.get_partial_shape()[2].get_length()})); - past_key = register_new_node(past_key, current_kv_len_const, past_kv_len_const, one, two); - past_value = register_new_node(past_value, current_kv_len_const, past_kv_len_const, one, two); + // static design for GQA (KV cache is static max length, valid KVs are left align) + // inputs are: + // 1. past_key/past_value: [1, num_heads, max_seq_len, head_size], data is in the front along axis 2, [P0, P1, + // ..., Pn, 0, 0, ...] + // 2. current K/V: [1, num_heads, current_kv_len, head_size], data is in the front along axis 2, [C0, C1, ..., + // Ck, 0, 0, ...] + // Output present_key/present_value has the same shape with past_key/past_value, but with data in order [P0, P1, + // ..., Pn, C0, C1, ..., Ck, 0, 0, ...] + // + // Use ScatterUpdate to scatter insert Current into Past + // Insert current K/V at the correct position [past_seqlen, past_seqlen+curr_seqlen]. + std::shared_ptr scatter_idx = + register_new_node(zero_without_shape, curr_seqlen_scalar, one_without_shape, ov::element::i64); + scatter_idx = register_new_node(scatter_idx, past_seqlen); + const auto scatter_axis = register_new_node(v0::Constant::create(ov::element::i64, ov::Shape{1}, {2})); + K = register_new_node(past_key, scatter_idx, K, scatter_axis); + V = register_new_node(past_value, scatter_idx, V, scatter_axis); } else { + auto construct_kv_cache = [&](const ov::Output& past, const ov::Output& current) { + return register_new_node(ov::OutputVector{past, current}, 2); + }; past_key = register_new_node(past_key, zero, past_seqlen, one, two); past_value = register_new_node(past_value, zero, past_seqlen, one, two); + K = construct_kv_cache(past_key, K); + V = construct_kv_cache(past_value, V); } - K = construct_kv_cache(past_key, K); - V = construct_kv_cache(past_value, V); ov::Output present_k = K; ov::Output present_v = V; @@ -184,12 +194,7 @@ ov::OutputVector ov::pass::GroupQueryAttentionDecomposition::decompose( std::shared_ptr vert_range = register_new_node(zero_without_shape, curr_seqlen_scalar, one_without_shape, ov::element::i64); vert_range = register_new_node(vert_range, one); - if (is_static_input) { - const auto past_k_node_len = get_dimensions(past_key.get_node_shared_ptr(), {2}); - vert_range = register_new_node(vert_range, past_k_node_len); - } else { - vert_range = register_new_node(vert_range, past_seqlen); - } + vert_range = register_new_node(vert_range, past_seqlen); const auto triu = register_new_node(hori_range, vert_range); const auto typed_zero = register_new_node(v0::Constant::create(T, ov::Shape{}, {0})); @@ -202,14 +207,6 @@ ov::OutputVector ov::pass::GroupQueryAttentionDecomposition::decompose( minus_inf = register_new_node(v0::Constant::create(T, ov::Shape{}, {std::numeric_limits::lowest()})); mask = register_new_node(triu, minus_inf, typed_zero); - - if (is_static_input) { - const auto padding_len = register_new_node(concat_kv_len, seqlens_1d); - const auto padding_mask_vert_shape = register_new_node(ov::NodeVector{current_seqlen, one}, 0); - const auto padding_mask_vert = register_new_node(padding_len, padding_mask_vert_shape); - const auto padding_mask = register_new_node(hori_range, padding_mask_vert); - mask = register_new_node(padding_mask, mask, minus_inf); - } } std::shared_ptr qga_output; @@ -230,16 +227,6 @@ ov::OutputVector ov::pass::GroupQueryAttentionDecomposition::decompose( return {output, present_k, present_v}; } -// make split functions is a copy-past from ONNX FE. TODO: move it to one place -ov::OutputVector ov::pass::GroupQueryAttentionDecomposition::make_split(const ov::Output& value, - int64_t num_splits, - int64_t axis) { - const auto axis_node = register_new_node(v0::Constant::create(ov::element::i64, ov::Shape{}, {axis})); - const auto split = register_new_node(value, axis_node, num_splits); - - return split->outputs(); -} - std::shared_ptr ov::pass::GroupQueryAttentionDecomposition::get_dimensions( const std::shared_ptr& shape, const std::vector& dims) { @@ -258,42 +245,66 @@ std::shared_ptr ov::pass::GroupQueryAttentionDecomposition::rotaryEmbe ov::Output cos, ov::Output sin, bool interleaved) { - auto zero = v0::Constant::create(ov::element::i64, ov::Shape{1}, {0}); - auto one = v0::Constant::create(ov::element::i64, ov::Shape{1}, {1}); - + auto two = v0::Constant::create(ov::element::i64, ov::Shape{1}, {2}); + + // Unsqueeze cos/sin to 4D [1, 1, seqlen, head_size/2] to match RoPE fusion pattern + auto unsqueeze_axes = v0::Constant::create(ov::element::i64, ov::Shape{2}, {0, 1}); + auto cos_4d = register_new_node(cos, unsqueeze_axes); + auto sin_4d = register_new_node(sin, unsqueeze_axes); + + // For interleaved mode, deinterleave first so the core RoPE formula is identical + ov::Output rope_input = input; + std::shared_ptr input_shape; + std::shared_ptr dim_bns, half_head_size; + std::shared_ptr perm_5d; if (interleaved) { - auto two = v0::Constant::create(ov::element::i64, ov::Shape{1}, {2}); - auto cos_last_dim = get_dimensions(cos.get_node_shared_ptr(), {-1}); - auto input_shape = register_new_node(input); - auto dim_bns = get_dimensions(input_shape, {0, 1, 2}); - - auto negtive_one = v0::Constant::create(ov::element::i64, ov::Shape{1}, {-1}); - auto split_input_shape = register_new_node(ov::NodeVector{dim_bns, cos_last_dim, two}, 0); - auto reshaped_input = register_new_node(input, split_input_shape, false); - - auto in_split = make_split(reshaped_input, 2, -1); - split_input_shape = register_new_node(ov::NodeVector{dim_bns, cos_last_dim}, 0); - auto in_split_0 = register_new_node(in_split[0], split_input_shape, false); - auto in_split_1 = register_new_node(in_split[1], split_input_shape, false); - - auto res_0 = register_new_node(register_new_node(in_split_0, cos), - register_new_node(in_split_1, sin)); - auto res_1 = register_new_node(register_new_node(in_split_0, sin), - register_new_node(in_split_1, cos)); - - split_input_shape = register_new_node(ov::NodeVector{dim_bns, cos_last_dim, one}, 0); - auto res_0_5d = register_new_node(res_0, split_input_shape, false); - auto res_1_5d = register_new_node(res_1, split_input_shape, false); - - auto concat_ret = register_new_node(ov::NodeVector{res_0_5d, res_1_5d}, -1); - return register_new_node(concat_ret, input_shape, false); - } else { - auto in_split = make_split(input, 2, -1); - auto res_0 = register_new_node(register_new_node(in_split[0], cos), - register_new_node(in_split[1], sin)); - auto res_1 = register_new_node(register_new_node(in_split[0], sin), - register_new_node(in_split[1], cos)); + input_shape = register_new_node(input); + dim_bns = get_dimensions(input_shape, {0, 1, 2}); + half_head_size = get_dimensions(cos.get_node_shared_ptr(), {-1}); + perm_5d = v0::Constant::create(ov::element::i64, ov::Shape{5}, {0, 1, 2, 4, 3}); + + // Deinterleave: [bs,nh,seq,head_size] + // -> reshape [bs,nh,seq,head_size/2,2] + // -> transpose [bs,nh,seq,2,head_size/2] + // -> reshape [bs,nh,seq,head_size] (now [first_half, second_half]) + auto deinterleave_5d = register_new_node(ov::NodeVector{dim_bns, half_head_size, two}, 0); + auto reshaped_5d = register_new_node(input, deinterleave_5d, false); + auto transposed_5d = register_new_node(reshaped_5d, perm_5d); + rope_input = register_new_node(transposed_5d, input_shape, false); + } - return register_new_node(ov::NodeVector{res_0, res_1}, -1); + // Core RoPE formula (matches RoPEFusionGPTOSS pattern for both modes) + // first_ = first_half * cos - second_half * sin + // second_ = second_half * cos + first_half * sin + const auto& cos_partial_shape = cos.get_partial_shape(); + const auto half_head_size_val = + static_cast(cos_partial_shape[cos_partial_shape.rank().get_length() - 1].get_length()); + const auto split_axis = v0::Constant::create(ov::element::i64, ov::Shape{}, {-1}); + const auto split_lengths = + v0::Constant::create(ov::element::i64, ov::Shape{2}, {half_head_size_val, half_head_size_val}); + // Split along last axis using constant split_lengths to enable RoPE fusion pattern matching + auto in_split = register_new_node(rope_input, split_axis, split_lengths)->outputs(); + auto first_half_mul_cos = register_new_node(in_split[0], cos_4d); + auto second_half_mul_sin = register_new_node(in_split[1], sin_4d); + auto neg_one = v0::Constant::create(input.get_element_type(), ov::Shape{}, {-1.0f}); + auto neg_second_sin = register_new_node(second_half_mul_sin, neg_one); + auto res_0 = register_new_node(first_half_mul_cos, neg_second_sin); + auto second_half_mul_cos = register_new_node(in_split[1], cos_4d); + auto first_half_mul_sin = register_new_node(in_split[0], sin_4d); + auto res_1 = register_new_node(second_half_mul_cos, first_half_mul_sin); + ov::Output output = register_new_node(ov::NodeVector{res_0, res_1}, -1); + + // For interleaved mode, re-interleave the result + if (interleaved) { + // Re-interleave: [bs,nh,seq,head_size] + // -> reshape [bs,nh,seq,2,head_size/2] + // -> transpose [bs,nh,seq,head_size/2,2] + // -> reshape [bs,nh,seq,head_size] + auto reinterleave_5d = register_new_node(ov::NodeVector{dim_bns, two, half_head_size}, 0); + auto result_5d = register_new_node(output, reinterleave_5d, false); + auto result_transposed = register_new_node(result_5d, perm_5d); + output = register_new_node(result_transposed, input_shape, false); } + + return output.get_node_shared_ptr(); } diff --git a/src/common/transformations/src/transformations/paged_attention/convert_pagedattn_inputs.cpp b/src/common/transformations/src/transformations/paged_attention/convert_pagedattn_inputs.cpp new file mode 100644 index 000000000000..cd21357efc57 --- /dev/null +++ b/src/common/transformations/src/transformations/paged_attention/convert_pagedattn_inputs.cpp @@ -0,0 +1,188 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "transformations/paged_attention/convert_pagedattn_inputs.hpp" + +#include +#include + +#include "itt.hpp" +#include "openvino/core/rt_info.hpp" +#include "openvino/op/add.hpp" +#include "openvino/op/constant.hpp" +#include "openvino/op/paged_attention.hpp" +#include "openvino/op/paged_causal_conv1d.hpp" +#include "openvino/op/paged_gated_delta_net.hpp" +#include "openvino/pass/pattern/op/wrap_type.hpp" +#include "openvino/util/log.hpp" +#include "transformations/rt_info/keep_const_precision.hpp" +#include "transformations/utils/utils.hpp" + +using namespace ov::pass; + +namespace v0 = ov::op::v0; + +namespace ov::pass { + +ConvertPagedAttnInputs::ConvertPagedAttnInputs(const KVCacheConfig& config, + UpdateShapeFunc func, + UpdatePrecisionFunc update_precision_func) + : m_config(config), + m_update_shape_func(std::move(func)), + m_update_precision_func(std::move(update_precision_func)) { + MATCHER_SCOPE(ConvertPagedAttnInputs); + + auto result = pattern::wrap_type() | + pattern::wrap_type() | + pattern::wrap_type(); + ov::matcher_pass_callback callback = [OV_CAPTURE_CPY_AND_THIS](pattern::Matcher& m) { + const auto root = m.get_match_root(); + bool status = false; + + if (const auto pa_op = ov::as_type_ptr(root)) { + auto key_cache = ov::as_type_ptr(pa_op->get_input_node_shared_ptr(3)); + auto value_cache = ov::as_type_ptr(pa_op->get_input_node_shared_ptr(4)); + if (!key_cache || !value_cache) { + return false; + } + auto format_cache_precision = [](ov::element::Type cache_precision, ov::element::Type infer_precision) { + return cache_precision == ov::element::f16 && infer_precision == ov::element::bf16 ? infer_precision + : cache_precision; + }; + + auto init_cache_shape = [&](const size_t head_nums, + const size_t head_size, + const size_t block_size, + const ov::element::Type precision, + const size_t group_size, + const bool bychannel, + const std::vector& orders) { + ov::Dimension::value_type _block_size = block_size; + ov::Dimension::value_type _head_nums = head_nums; + ov::Dimension::value_type _head_size = head_size; + ov::Dimension::value_type _group_size = group_size; + _group_size = _group_size ? _group_size : _head_size; + if (!bychannel) { + if (_head_size % _group_size != 0) { + OPENVINO_THROW("cache head_size ", head_size, "cannot be divided by group_size ", group_size); + } + } + size_t group_num = _head_size / _group_size; + // Update head_size and block_size by precision and quantizing channel mode + m_update_shape_func(precision, bychannel, group_num, _head_size, _block_size); + + auto block_shape = ov::PartialShape::dynamic(4); + block_shape[orders[0]] = -1; + block_shape[orders[1]] = _head_nums; + block_shape[orders[2]] = _block_size; + block_shape[orders[3]] = _head_size; + + return block_shape; + }; + auto key_cache_precision = format_cache_precision(m_config.keyCachePrecision, m_config.inferencePrecision); + auto value_cache_precision = + format_cache_precision(m_config.valueCachePrecision, m_config.inferencePrecision); + key_cache->set_element_type(key_cache_precision); + value_cache->set_element_type(value_cache_precision); + enable_keep_const_precision(key_cache); + enable_keep_const_precision(value_cache); + if (pa_op->get_rt_info().count("num_k_heads") && pa_op->get_rt_info().count("k_head_size") && + pa_op->get_rt_info().count("num_v_heads") && pa_op->get_rt_info().count("v_head_size")) { + const auto key_cache_shape = init_cache_shape(pa_op->get_rt_info()["num_k_heads"].as(), + pa_op->get_rt_info()["k_head_size"].as(), + m_config.keyCacheBlockSize, + key_cache_precision, + m_config.keyCacheGroupSize, + m_config.keyCacheQuantBychannel, + m_config.keyCacheDimOrder); + const auto value_cache_shape = init_cache_shape(pa_op->get_rt_info()["num_v_heads"].as(), + pa_op->get_rt_info()["v_head_size"].as(), + m_config.valueCacheBlockSize, + value_cache_precision, + m_config.valueCacheGroupSize, + m_config.valueCacheQuantBychannel, + m_config.valueCacheDimOrder); + + key_cache->set_partial_shape(key_cache_shape); + value_cache->set_partial_shape(value_cache_shape); + status = true; + } else { + OPENVINO_DEBUG("PagedAttn ", + pa_op->get_friendly_name(), + " doesn't have rtinfo for num_k_heads/k_head_size/num_v_heads/v_head_size"); + status = false; + } + + if (m_update_precision_func) { + m_update_precision_func(key_cache_precision); + m_update_precision_func(value_cache_precision); + key_cache->set_element_type(key_cache_precision); + value_cache->set_element_type(value_cache_precision); + enable_keep_const_precision(key_cache); + enable_keep_const_precision(value_cache); + } + + // Prevent the ConvertPrecision pass from converting sub-byte cache + // precisions (e.g. u4→u8). The PA executor handles quantized caches natively. + if (key_cache_precision.bitwidth() < 8) { + enable_keep_const_precision(key_cache); + } + if (value_cache_precision.bitwidth() < 8) { + enable_keep_const_precision(value_cache); + } + + key_cache->validate_and_infer_types(); + value_cache->validate_and_infer_types(); + } + + if (const auto paged_conv = ov::as_type_ptr(root)) { + auto conv_state_table = ov::as_type_ptr(paged_conv->get_input_node_shared_ptr(1)); + if (!conv_state_table) { + return false; + } + + auto conv_cache_precision = m_config.inferencePrecision; + if (m_update_precision_func) { + m_update_precision_func(conv_cache_precision); + } + + conv_state_table->set_element_type(conv_cache_precision); + enable_keep_const_precision(conv_state_table); + conv_state_table->validate_and_infer_types(); + return true; + } + + if (const auto paged_gdn = ov::as_type_ptr(root)) { + auto gated_delta_state_table = ov::as_type_ptr(paged_gdn->get_input_node_shared_ptr(3)); + if (!gated_delta_state_table) { + return false; + } + + auto gated_delta_cache_precision = m_config.inferencePrecision; + if (m_update_precision_func) { + m_update_precision_func(gated_delta_cache_precision); + } + + gated_delta_state_table->set_element_type(gated_delta_cache_precision); + enable_keep_const_precision(gated_delta_state_table); + gated_delta_state_table->validate_and_infer_types(); + return true; + } + + return status; + }; + + auto m = std::make_shared(result, matcher_name); + this->register_matcher(m, callback); +} + +void ConvertPagedAttnInputs::setKVCacheConfig(const KVCacheConfig& config) { + m_config = config; +} + +const ConvertPagedAttnInputs::KVCacheConfig& ConvertPagedAttnInputs::getKVCacheConfig() const { + return m_config; +} + +} // namespace ov::pass diff --git a/src/common/transformations/src/transformations/paged_attention/eliminate_conv_padding_mask_gating.cpp b/src/common/transformations/src/transformations/paged_attention/eliminate_conv_padding_mask_gating.cpp new file mode 100644 index 000000000000..6da1c595b006 --- /dev/null +++ b/src/common/transformations/src/transformations/paged_attention/eliminate_conv_padding_mask_gating.cpp @@ -0,0 +1,51 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "transformations/paged_attention/eliminate_conv_padding_mask_gating.hpp" + +#include "itt.hpp" +#include "openvino/op/add.hpp" +#include "openvino/op/convert.hpp" +#include "openvino/op/multiply.hpp" +#include "openvino/op/parameter.hpp" +#include "openvino/op/slice.hpp" +#include "openvino/op/unsqueeze.hpp" +#include "openvino/pass/pattern/op/optional.hpp" +#include "openvino/pass/pattern/op/wrap_type.hpp" + +using ov::pass::pattern::any_input; +using ov::pass::pattern::wrap_type; + +namespace v0 = ov::op::v0; +namespace v1 = ov::op::v1; +namespace v8 = ov::op::v8; + +namespace ov::pass { + +EliminateConvPaddingMaskGating::EliminateConvPaddingMaskGating() { + MATCHER_SCOPE(EliminateConvPaddingMaskGating); + + // Pattern: attention_mask -> Slice -> Unsqueeze -> [Convert] -> Multiply -> Add -> Multiply(H, mask_expr) + auto attn_mask = wrap_type([](const ov::Output& output) { + return output.get_names().count("attention_mask"); + }); + auto slice = wrap_type({attn_mask, any_input(), any_input(), any_input(), any_input()}); + auto unsqueeze = pattern::optional({slice, any_input()}); + auto convert = pattern::optional({unsqueeze}); + auto mul_mask = wrap_type({convert, any_input()}); + auto add = wrap_type({mul_mask, any_input()}); + auto hidden_states = any_input(); + auto mul_gate = wrap_type({hidden_states, add}); + + ov::matcher_pass_callback callback = [=](ov::pass::pattern::Matcher& m) { + const auto& pm = m.get_pattern_value_map(); + pm.at(mul_gate).get_node_shared_ptr()->output(0).replace(pm.at(hidden_states)); + return true; + }; + + auto m = std::make_shared(mul_gate, matcher_name); + this->register_matcher(m, callback); +} + +} // namespace ov::pass diff --git a/src/common/transformations/src/transformations/paged_attention/paged_causal_conv1d_fusion.cpp b/src/common/transformations/src/transformations/paged_attention/paged_causal_conv1d_fusion.cpp new file mode 100644 index 000000000000..be867eef1cff --- /dev/null +++ b/src/common/transformations/src/transformations/paged_attention/paged_causal_conv1d_fusion.cpp @@ -0,0 +1,204 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "transformations/paged_attention/paged_causal_conv1d_fusion.hpp" + +#include +#include +#include +#include +#include +#include + +#include "itt.hpp" +#include "openvino/core/graph_util.hpp" +#include "openvino/core/rt_info.hpp" +#include "openvino/core/type/element_type.hpp" +#include "openvino/op/add.hpp" +#include "openvino/op/assign.hpp" +#include "openvino/op/concat.hpp" +#include "openvino/op/constant.hpp" +#include "openvino/op/gather.hpp" +#include "openvino/op/group_conv.hpp" +#include "openvino/op/multiply.hpp" +#include "openvino/op/paged_causal_conv1d.hpp" +#include "openvino/op/parameter.hpp" +#include "openvino/op/reshape.hpp" +#include "openvino/op/result.hpp" +#include "openvino/op/slice.hpp" +#include "openvino/op/transpose.hpp" +#include "openvino/op/unsqueeze.hpp" +#include "openvino/op/util/read_value_base.hpp" +#include "openvino/pass/pattern/matcher.hpp" +#include "openvino/pass/pattern/op/optional.hpp" +#include "openvino/pass/pattern/op/or.hpp" +#include "openvino/pass/pattern/op/wrap_type.hpp" +#include "transformations/rt_info/keep_const_precision.hpp" +#include "transformations/utils/utils.hpp" + +using ov::pass::pattern::any_input; +using ov::pass::pattern::has_static_rank; +using ov::pass::pattern::has_static_shape; +using ov::pass::pattern::rank_equals; +using ov::pass::pattern::wrap_type; + +namespace v0 = ov::op::v0; +namespace v1 = ov::op::v1; +namespace v8 = ov::op::v8; + +namespace { + +ov::PartialShape make_conv_state_table_shape(const ov::PartialShape& past_state_shape) { + if (past_state_shape.rank().is_static() && past_state_shape.rank().get_length() == 3) { + return ov::PartialShape{ov::Dimension::dynamic(), past_state_shape[1], past_state_shape[2]}; + } + return ov::PartialShape::dynamic(3); +} + +} // namespace + +namespace ov::pass { + +PagedCausalConv1DFusion::PagedCausalConv1DFusion(ov::pass::paged_attention::PaParams& pa_params, + std::unordered_set& var_ids_to_remove) { + MATCHER_SCOPE(PagedCausalConv1DFusion); + + auto p_read_value = wrap_type(has_static_rank() && rank_equals(3)); + auto p_past_via_gather = ov::pass::pattern::optional({p_read_value, any_input(), any_input()}); + auto p_token_input = any_input(rank_equals(3)); + auto p_concat_past_first = wrap_type({p_past_via_gather, p_token_input}, {{"axis", -1}}); + auto p_concat_token_first = wrap_type({p_token_input, p_past_via_gather}, {{"axis", -1}}); + auto p_state_concat = + std::make_shared(ov::OutputVector{p_concat_past_first, p_concat_token_first}); + + auto p_weight_input = any_input(has_static_shape() && rank_equals(4)); + auto p_group_conv = wrap_type({p_state_concat, p_weight_input}); + + auto p_bias_input = any_input(has_static_shape()); + auto p_add_bias = ov::pass::pattern::optional({p_group_conv, p_bias_input}); + + auto p_slice_out = wrap_type({p_add_bias, any_input(), any_input(), any_input(), any_input()}); + + ov::matcher_pass_callback callback = [OV_CAPTURE_CPY_AND_THIS, &pa_params, &var_ids_to_remove]( + ov::pass::pattern::Matcher& m) { + if (transformation_callback(m.get_match_root())) { + return false; + } + + const auto& pm = m.get_pattern_value_map(); + + const auto state_concat = pm.at(p_state_concat).get_node_shared_ptr(); + const auto group_conv_node = ov::as_type_ptr(pm.at(p_group_conv).get_node_shared_ptr()); + const auto weight_node = pm.at(p_weight_input).get_node_shared_ptr(); + const auto cache_rv = ov::as_type_ptr(pm.at(p_read_value).get_node_shared_ptr()); + const auto slice_out = pm.at(p_slice_out).get_node_shared_ptr(); + + pa_params.add("subsequence_begins", ov::element::i32, ov::PartialShape{-1}); + pa_params.add("la.block_indices", ov::element::i32, ov::PartialShape{-1}); + pa_params.add("la.block_indices_begins", ov::element::i32, ov::PartialShape{-1}); + pa_params.add("la.past_lens", ov::element::i32, ov::PartialShape{-1}); + pa_params.add("la.cache_interval", ov::element::i32, ov::PartialShape{-1}); + + const auto conv_state_table = pa_params.add("conv_state_table." + std::to_string(m_layer_index++), + ov::element::dynamic, + make_conv_state_table_shape(cache_rv->get_output_partial_shape(0))); + + enable_keep_const_precision(conv_state_table); + var_ids_to_remove.insert(cache_rv->get_variable_id()); + + auto token_input = pm.at(p_token_input).get_node_shared_ptr(); + const auto past_state = pm.count(p_past_via_gather) ? pm.at(p_past_via_gather).get_node_shared_ptr() : cache_rv; + + const auto& weight_shape = weight_node->get_output_shape(0); + const size_t hidden_size = weight_shape[0]; + const size_t kernel_size = weight_shape[3]; + + const auto& state_pshape = past_state->get_output_partial_shape(0); + if (!state_pshape[1].compatible(hidden_size) || !state_pshape[2].compatible(kernel_size)) { + return false; + } + + const auto& token_pshape = token_input->get_output_partial_shape(0); + if (token_pshape[1].compatible(hidden_size)) { + const auto order = v0::Constant::create(ov::element::i64, ov::Shape{3}, {0, 2, 1}); + token_input = std::make_shared(token_input, order); + } else if (!token_pshape[2].compatible(hidden_size)) { + return false; + } + + const auto input_embeds_shape = + v0::Constant::create(ov::element::i64, + ov::Shape{2}, + std::vector{-1, static_cast(hidden_size)}); + const auto input_embeds_node = std::make_shared(token_input, input_embeds_shape, false); + + const auto pa_weight_shape = v0::Constant::create(ov::element::i64, + ov::Shape{3}, + std::vector{static_cast(hidden_size), + static_cast(1), + static_cast(kernel_size)}); + const auto weight_reshaped = std::make_shared(weight_node, pa_weight_shape, false); + + const auto& elem_type = input_embeds_node->get_output_element_type(0); + std::shared_ptr bias_node; + if (pm.count(p_add_bias)) { + const auto bias_input = pm.at(p_bias_input).get_node_shared_ptr(); + const auto& bias_shape = bias_input->get_output_partial_shape(0); + if (bias_shape.rank().is_static() && bias_shape.rank().get_length() == 1 && + bias_shape[0].compatible(hidden_size)) { + bias_node = bias_input; + } else { + if (bias_shape.is_static()) { + // Validate that the total element count matches hidden_size + size_t num_elements = 1; + for (size_t i = 0; i < static_cast(bias_shape.rank().get_length()); ++i) { + num_elements *= bias_shape[i].get_length(); + } + if (num_elements != hidden_size) { + return false; + } + } + const auto bias_shape_node = + v0::Constant::create(ov::element::i64, ov::Shape{1}, std::vector{-1}); + bias_node = std::make_shared(bias_input, bias_shape_node, false); + } + } else { + bias_node = v0::Constant::create(elem_type, ov::Shape{0}, std::vector{}); + } + + const auto paged_conv = + std::make_shared(input_embeds_node, + conv_state_table, + weight_reshaped, + bias_node, + pa_params["subsequence_begins"], + pa_params["la.block_indices"], + pa_params["la.block_indices_begins"], + pa_params["la.past_lens"], + pa_params["la.cache_interval"]); + + paged_conv->set_friendly_name(group_conv_node->get_friendly_name() + "/PagedCausalConv1D"); + + const auto unsqueeze_axis = v0::Constant::create(ov::element::i64, ov::Shape{1}, {2}); + const auto unsqueeze = std::make_shared(paged_conv, unsqueeze_axis); + unsqueeze->set_friendly_name(group_conv_node->get_friendly_name()); + + ov::NodeVector new_nodes{input_embeds_shape, + input_embeds_node, + pa_weight_shape, + weight_reshaped, + bias_node, + paged_conv, + unsqueeze}; + + ov::copy_runtime_info(m.get_matched_nodes(), new_nodes); + ov::replace_node(slice_out, unsqueeze); + return true; + }; + + const auto matcher = std::make_shared(p_slice_out, matcher_name); + register_matcher(matcher, callback); +} + +} // namespace ov::pass diff --git a/src/common/transformations/src/transformations/paged_attention/paged_gated_delta_net_fusion.cpp b/src/common/transformations/src/transformations/paged_attention/paged_gated_delta_net_fusion.cpp new file mode 100644 index 000000000000..00738a193b09 --- /dev/null +++ b/src/common/transformations/src/transformations/paged_attention/paged_gated_delta_net_fusion.cpp @@ -0,0 +1,176 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "transformations/paged_attention/paged_gated_delta_net_fusion.hpp" + +#include +#include +#include + +#include "itt.hpp" +#include "openvino/core/graph_util.hpp" +#include "openvino/core/rt_info.hpp" +#include "openvino/op/concat.hpp" +#include "openvino/op/constant.hpp" +#include "openvino/op/convert.hpp" +#include "openvino/op/gated_delta_net.hpp" +#include "openvino/op/gather.hpp" +#include "openvino/op/paged_gated_delta_net.hpp" +#include "openvino/op/parameter.hpp" +#include "openvino/op/reshape.hpp" +#include "openvino/op/result.hpp" +#include "openvino/op/shape_of.hpp" +#include "openvino/op/slice.hpp" +#include "openvino/op/transpose.hpp" +#include "openvino/op/unsqueeze.hpp" +#include "openvino/op/util/read_value_base.hpp" +#include "openvino/pass/pattern/matcher.hpp" +#include "openvino/pass/pattern/op/optional.hpp" +#include "openvino/pass/pattern/op/wrap_type.hpp" +#include "transformations/rt_info/keep_const_precision.hpp" + +using ov::pass::pattern::any_input; +using ov::pass::pattern::wrap_type; + +namespace v0 = ov::op::v0; + +namespace { + +constexpr const char* GATED_DELTA_STATE_TABLE_PREFIX = "gated_delta_state_table."; + +std::string make_gated_delta_state_table_name(const size_t layer_index) { + return std::string(GATED_DELTA_STATE_TABLE_PREFIX) + std::to_string(layer_index); +} + +ov::PartialShape make_gated_delta_state_table_shape(const ov::PartialShape& state_shape) { + // GDN input state shape is [B, H, D_k, D_v]. + // PagedGatedDeltaNet state table expects [?, H, D_v, D_k] — swap last two dims. + if (state_shape.rank().is_static() && state_shape.rank().get_length() == 4) { + return ov::PartialShape{ov::Dimension::dynamic(), state_shape[1], state_shape[3], state_shape[2]}; + } + return ov::PartialShape::dynamic(4); +} + +ov::Output flatten_batch_length(const ov::Output& input, + const std::vector& tail_dim_indices) { + // Flattens [B, L, ...tail_dims] to [B*L, ...tail_dims] by preserving tail_dim_indices. + const auto shape_of = std::make_shared(input, ov::element::i64); + const auto idx_const = v0::Constant::create(ov::element::i64, ov::Shape{tail_dim_indices.size()}, tail_dim_indices); + const auto axis_0 = v0::Constant::create(ov::element::i64, ov::Shape{}, {0}); + const auto tail_dims = std::make_shared(shape_of, idx_const, axis_0); + const auto flat_dim = v0::Constant::create(ov::element::i64, ov::Shape{1}, {-1}); + const auto flat_shape = std::make_shared(ov::OutputVector{flat_dim, tail_dims}, 0); + const auto reshaped = std::make_shared(input, flat_shape, false); + + ov::copy_runtime_info(input.get_node_shared_ptr(), {shape_of, tail_dims, flat_shape, reshaped}); + + return reshaped; +} +} // namespace + +namespace ov::pass { + +PagedGatedDeltaNetFusion::PagedGatedDeltaNetFusion(ov::pass::paged_attention::PaParams& pa_params, + std::unordered_set& var_ids_to_remove) { + auto query = any_input(); + auto key = any_input(); + auto value = any_input(); + auto gate = any_input(); + auto beta = any_input(); + + // recurrent_state must come from ReadValue(cache_param) directly or via Gather(ReadValue, beam_idx, axis). + auto cache_param = any_input(); + auto read_value = wrap_type({cache_param}); + auto gathered_state = ov::pass::pattern::optional({read_value, any_input(), any_input()}); + auto gdn = wrap_type({query, key, value, gathered_state, gate, beta}); + + ov::matcher_pass_callback callback = [OV_CAPTURE_CPY_AND_THIS, &pa_params, &var_ids_to_remove]( + ov::pass::pattern::Matcher& m) { + if (transformation_callback(m.get_match_root())) { + return false; + } + + const auto& pm = m.get_pattern_value_map(); + const auto gdn_node = ov::as_type_ptr(pm.at(gdn).get_node_shared_ptr()); + if (!gdn_node || gdn_node->get_output_size() != 2) { + return false; + } + + // Add la.* params lazily on first match — PaParams::add is idempotent. + pa_params.add("subsequence_begins", ov::element::i32, ov::PartialShape{-1}); + pa_params.add("la.block_indices", ov::element::i32, ov::PartialShape{-1}); + pa_params.add("la.block_indices_begins", ov::element::i32, ov::PartialShape{-1}); + pa_params.add("la.past_lens", ov::element::i32, ov::PartialShape{-1}); + pa_params.add("la.cache_interval", ov::element::i32, ov::PartialShape{-1}); + + const auto state_consumers = gdn_node->output(1).get_target_inputs(); + const auto& state_out = pm.at(read_value); + + const auto state_table_param = pa_params.add(make_gated_delta_state_table_name(m_layer_index++), + ov::element::dynamic, + make_gated_delta_state_table_shape(state_out.get_partial_shape())); + enable_keep_const_precision(state_table_param); + + const auto rv = ov::as_type_ptr(pm.at(read_value).get_node_shared_ptr()); + OPENVINO_ASSERT(rv, "Matched cache node is expected to be ReadValue"); + var_ids_to_remove.insert(rv->get_variable_id()); + + const auto query_flat = flatten_batch_length(pm.at(query), {2, 3}); + const auto key_flat = flatten_batch_length(pm.at(key), {2, 3}); + const auto value_flat = flatten_batch_length(pm.at(value), {2, 3}); + const auto gate_flat = flatten_batch_length(pm.at(gate), {2}); + const auto beta_flat = flatten_batch_length(pm.at(beta), {2}); + + // Inputs 0-5 are flattened from matched GatedDeltaNet [B,L,H,*] to PagedGDN [B*L,H,*]. + const auto paged_gdn = + std::make_shared(query_flat, + key_flat, + value_flat, + state_table_param->output(0), + gate_flat, + beta_flat, + pa_params["subsequence_begins"], + pa_params["la.block_indices"], + pa_params["la.block_indices_begins"], + pa_params["la.past_lens"], + pa_params["la.cache_interval"], + gdn_node->get_fuse_qk_l2norm(), + gdn_node->get_q_l2_norm_eps(), + gdn_node->get_k_l2_norm_eps()); + + paged_gdn->set_friendly_name(gdn_node->get_friendly_name() + "/PagedGatedDeltaNet"); + const auto query_shape = std::make_shared(pm.at(query), ov::element::i64); + const auto value_shape = std::make_shared(pm.at(value), ov::element::i64); + const auto axis_0 = v0::Constant::create(ov::element::i64, ov::Shape{}, {0}); + const auto idx_q = v0::Constant::create(ov::element::i64, ov::Shape{3}, {0, 1, 2}); + const auto idx_v = v0::Constant::create(ov::element::i64, ov::Shape{1}, {3}); + const auto q_dims = std::make_shared(query_shape, idx_q, axis_0); + const auto v_dim = std::make_shared(value_shape, idx_v, axis_0); + const auto out0_shape = std::make_shared(ov::OutputVector{q_dims, v_dim}, 0); + const auto paged_gdn_out = std::make_shared(paged_gdn, out0_shape, false); + paged_gdn_out->set_friendly_name(gdn_node->get_friendly_name()); + + ov::copy_runtime_info(gdn_node, + {paged_gdn, query_shape, value_shape, q_dims, v_dim, out0_shape, paged_gdn_out}); + + // Disconnect GDN state output consumers; cleanup is driven by ReadValue variable ids. + for (const auto& state_consumer : state_consumers) { + // Reconnect consumer to the original state source so it becomes a dead branch. + state_consumer.replace_source_output(gdn_node->input_value(3)); + } + + if (!ov::replace_output_update_name(gdn_node->output(0), paged_gdn_out->output(0))) { + gdn_node->output(0).replace(paged_gdn_out->output(0)); + } + + register_new_node(paged_gdn_out); + register_new_node(paged_gdn); + return true; + }; + + const auto matcher = std::make_shared(gdn, "PagedGatedDeltaNetFusion"); + register_matcher(matcher, callback); +} + +} // namespace ov::pass diff --git a/src/common/transformations/src/transformations/sdpa_to_paged_attention/position_ids_replacer.cpp b/src/common/transformations/src/transformations/paged_attention/position_ids_replacer.cpp similarity index 64% rename from src/common/transformations/src/transformations/sdpa_to_paged_attention/position_ids_replacer.cpp rename to src/common/transformations/src/transformations/paged_attention/position_ids_replacer.cpp index 036d8d334a1a..9767051b83bd 100644 --- a/src/common/transformations/src/transformations/sdpa_to_paged_attention/position_ids_replacer.cpp +++ b/src/common/transformations/src/transformations/paged_attention/position_ids_replacer.cpp @@ -2,10 +2,11 @@ // SPDX-License-Identifier: Apache-2.0 // -#include "transformations/sdpa_to_paged_attention/position_ids_replacer.hpp" +#include "transformations/paged_attention/position_ids_replacer.hpp" #include "openvino/cc/pass/itt.hpp" #include "openvino/core/graph_util.hpp" +#include "openvino/op/concat.hpp" #include "openvino/op/cos.hpp" #include "openvino/op/einsum.hpp" #include "openvino/op/gather.hpp" @@ -23,6 +24,7 @@ #include "openvino/op/transpose.hpp" #include "openvino/op/unsqueeze.hpp" #include "openvino/pass/pattern/op/optional.hpp" +#include "openvino/pass/pattern/op/or.hpp" #include "openvino/pass/pattern/op/wrap_type.hpp" #include "transformations/utils/utils.hpp" @@ -141,6 +143,86 @@ ov::pass::PositionIDsReplacerQwen::PositionIDsReplacerQwen(const Output& p register_matcher(m, callback); } +// Handles models like LFM2 where RoPE positions are generated by a dynamic arange branch: +// max_context_len - prev_seq_len -> start, start + current_len -> end, Range(start, end, step). +// Replace this Range output with explicit position_ids so PagedAttention can process tokens +// in non-sequential order. +ov::pass::PositionIDsReplacerLFM2::PositionIDsReplacerLFM2(const Output& position_ids) { + MATCHER_SCOPE(PositionIDsReplacerLFM2); + + auto p_max_context_len = wrap_type(); + + // Allow either the dequant-like start branch or a direct Parameter connection. + auto p_start_subtract = wrap_type({p_max_context_len, any_input()}); + auto p_start_dequant = wrap_type({p_start_subtract}); + auto p_start = std::make_shared(OutputVector{p_start_dequant, p_max_context_len}); + auto p_end = wrap_type({p_start, any_input()}); + auto p_range = wrap_type({p_start, p_end, ov::pass::pattern::wrap_const()}); + + // Model variants may include optional Unsqueeze/Reshape/Convert wrappers before RoPE MatMul. + auto p_opt_layout_1 = ov::pass::pattern::optional({p_range, any_input()}); + auto p_opt_layout_2 = ov::pass::pattern::optional({p_opt_layout_1, any_input()}); + auto p_opt_convert = ov::pass::pattern::optional({p_opt_layout_2}); + auto p_matmul = wrap_type({any_input(), p_opt_convert}); + auto p_opt_transpose = ov::pass::pattern::optional({p_matmul, any_input()}); + auto p_concat = wrap_type({p_opt_transpose, any_input()}); + auto p_sin_cos_l = wrap_type({p_concat}); + auto p_sin_cos_r = wrap_type({p_concat}); + auto p_sin_cos_layout_l = ov::pass::pattern::optional({p_sin_cos_l, any_input()}); + auto p_sin_cos_layout_r = ov::pass::pattern::optional({p_sin_cos_r, any_input()}); + auto p_mul_l = wrap_type({p_sin_cos_layout_l, any_input()}); + auto p_mul_r = wrap_type({p_sin_cos_layout_r, any_input()}); + auto p_add = wrap_type({p_mul_l, p_mul_r}); + + ov::matcher_pass_callback callback = [=](Matcher& m) { + const auto& pattern_map = m.get_pattern_value_map(); + const auto range = pattern_map.at(p_range).get_node_shared_ptr(); + + std::shared_ptr replacement_node = position_ids.get_node_shared_ptr(); + const auto position_ids_rank = position_ids.get_partial_shape().rank(); + if (position_ids_rank.is_dynamic() || position_ids_rank.get_length() != 1) { + const auto pos_ids_shape = v0::Constant::create(element::i64, Shape{1}, {-1}); + auto position_ids_1d = std::make_shared(position_ids, pos_ids_shape, false); + position_ids_1d->set_friendly_name(range->get_friendly_name() + "_position_ids"); + position_ids_1d->validate_and_infer_types(); + replacement_node = position_ids_1d; + } + + replace_node(range, replacement_node); + copy_runtime_info(range, replacement_node); + + // In PA mode the RoPE node receives input [seq_tokens, heads, 1, dim] where + // seq_tokens are in the batch dimension and the seq axis is always 1. + const auto concat = pattern_map.at(p_concat).get_node_shared_ptr(); + const auto concat_pshape = concat->get_output_partial_shape(0); + if (concat_pshape.rank().is_static()) { + const auto concat_rank = concat_pshape.rank().get_length(); + std::vector permutation; + if (concat_rank == 3) { + permutation = {1, 0, 2}; // Transpose [1, seq, dim] → [seq, 1, dim] + } else if (concat_rank == 4) { + permutation = {2, 1, 0, 3}; // Transpose [1, 1, seq, dim] → [seq, 1, 1, dim] + } + + if (!permutation.empty()) { + const auto consumers = concat->output(0).get_target_inputs(); + const auto order = v0::Constant::create(element::i64, Shape{permutation.size()}, permutation); + auto transpose = std::make_shared(concat, order); + transpose->set_friendly_name(concat->get_friendly_name() + "_seq_dim_transpose"); + copy_runtime_info(concat, transpose); + + for (auto& consumer : consumers) { + consumer.replace_source_output(transpose->output(0)); + } + } + } + return true; + }; + + const auto m = std::make_shared(p_add, matcher_name); + register_matcher(m, callback); +} + ov::pass::PositionIDsReplacerCodeGen2::PositionIDsReplacerCodeGen2(const std::shared_ptr& position_ids) { MATCHER_SCOPE(PositionIDsReplacerCodeGen2); diff --git a/src/common/transformations/src/transformations/sdpa_to_paged_attention/prev_sequence_length_pattern.cpp b/src/common/transformations/src/transformations/paged_attention/prev_sequence_length_pattern.cpp similarity index 89% rename from src/common/transformations/src/transformations/sdpa_to_paged_attention/prev_sequence_length_pattern.cpp rename to src/common/transformations/src/transformations/paged_attention/prev_sequence_length_pattern.cpp index a207c4d9f333..9c3a95a25697 100644 --- a/src/common/transformations/src/transformations/sdpa_to_paged_attention/prev_sequence_length_pattern.cpp +++ b/src/common/transformations/src/transformations/paged_attention/prev_sequence_length_pattern.cpp @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 // -#include "transformations/sdpa_to_paged_attention/prev_sequence_length_pattern.hpp" +#include "transformations/paged_attention/prev_sequence_length_pattern.hpp" #include "openvino/cc/pass/itt.hpp" #include "openvino/core/graph_util.hpp" @@ -55,7 +55,10 @@ ov::pass::PrevSequenceLengthPattern::PrevSequenceLengthPattern(const std::shared replacement = prev_max_seq_len; } else { // it is not always required, so will be disposed if not needed - auto batch_dim = std::make_shared(position_ids); + auto position_ids_shape = std::make_shared(position_ids); + auto batch_dim = std::make_shared(position_ids_shape, + v0::Constant::create(element::i64, Shape{}, {0}), + v0::Constant::create(element::i64, Shape{}, {0})); // assumption that any other axis should point to batch dimension, precise reasoning is too complex // TODO: provide more reliable check diff --git a/src/common/transformations/src/transformations/sdpa_to_paged_attention/state_management_pattern.cpp b/src/common/transformations/src/transformations/paged_attention/state_management_pattern.cpp similarity index 78% rename from src/common/transformations/src/transformations/sdpa_to_paged_attention/state_management_pattern.cpp rename to src/common/transformations/src/transformations/paged_attention/state_management_pattern.cpp index 715d25cfe4ff..619bb4547249 100644 --- a/src/common/transformations/src/transformations/sdpa_to_paged_attention/state_management_pattern.cpp +++ b/src/common/transformations/src/transformations/paged_attention/state_management_pattern.cpp @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 // -#include "transformations/sdpa_to_paged_attention/state_management_pattern.hpp" +#include "transformations/paged_attention/state_management_pattern.hpp" #include @@ -38,11 +38,12 @@ #include "openvino/pass/pattern/op/optional.hpp" #include "openvino/pass/pattern/op/or.hpp" #include "openvino/pass/pattern/op/wrap_type.hpp" +#include "transformations/rt_info/keep_const_precision.hpp" #include "transformations/utils/utils.hpp" +namespace pattern = ov::pass::pattern; using ov::pass::pattern::any_input; using ov::pass::pattern::Matcher; -using ov::pass::pattern::optional; using ov::pass::pattern::wrap_type; using ov::pass::pattern::op::Or; @@ -60,6 +61,8 @@ constexpr const char* V_HEAD_SIZE = "v_head_size"; using namespace ov::pass; using ov::OutputVector; +using ov::pass::paged_attention::PaParams; + static std::tuple, std::shared_ptr> general_alibi_pattern() { // Optional pattern to capture alibi slopes (based on pattern from bloom) auto general_alibi = any_input(); @@ -72,13 +75,13 @@ static std::tuple, std::shared_ptr> general_ static std::tuple, std::shared_ptr> jais_13b_alibi_pattern() { auto jais_13b_alibi = any_input(); - auto alibi_opt_conv = optional(jais_13b_alibi); + auto alibi_opt_conv = pattern::optional(jais_13b_alibi); auto mirroring_abs = wrap_type({any_input()}); auto unsqueeze = wrap_type({mirroring_abs, any_input()}); - auto broadcast = optional({unsqueeze, any_input()}); - broadcast = optional(broadcast); + auto broadcast = pattern::optional({unsqueeze, any_input()}); + broadcast = pattern::optional(broadcast); auto jais_alibi_mask = wrap_type({alibi_opt_conv, broadcast}); - jais_alibi_mask = optional({jais_alibi_mask, any_input()}); + jais_alibi_mask = pattern::optional({jais_alibi_mask, any_input()}); jais_alibi_mask = wrap_type({jais_alibi_mask, any_input()}); jais_alibi_mask = wrap_type({any_input(), jais_alibi_mask}); return {jais_13b_alibi, jais_alibi_mask}; @@ -201,13 +204,13 @@ static std::shared_ptr handle_baichuan2_13b_alibi( static std::tuple, std::shared_ptr> phi3_sliding_window_pattern() { auto offset = wrap_type(); auto t196 = wrap_type({any_input(), offset}); - auto t197 = ov::pass::pattern::optional(t196); + auto t197 = pattern::optional(t196); auto t200 = wrap_type({t197, any_input(), any_input()}); auto t201 = wrap_type({t200, any_input()}); auto t202 = wrap_type({any_input(), t201}); auto t208 = wrap_type({t202, any_input(), any_input()}); auto t209 = wrap_type({any_input(), t208}); - auto t210 = ov::pass::pattern::optional(t209); + auto t210 = pattern::optional(t209); auto t211 = wrap_type({t210, any_input(), any_input()}); auto t213 = wrap_type({t211, any_input()}); auto t214 = wrap_type({t213, any_input()}); @@ -221,7 +224,7 @@ static std::tuple, std::shared_ptr> gptoss_g auto q_idx = any_input(); auto kv_idx = any_input(); - auto kv_idx_opt_conv = ov::pass::pattern::optional(kv_idx); + auto kv_idx_opt_conv = pattern::optional(kv_idx); auto offset = wrap_type(); @@ -233,20 +236,11 @@ static std::tuple, std::shared_ptr> gptoss_g auto bitwise_and_3 = wrap_type({bitwise_and_2, any_input()}); auto broadcast = wrap_type({bitwise_and_3, any_input()}); auto select = wrap_type({broadcast, any_input(), any_input()}); - auto mask = wrap_type({select, any_input(), any_input(), any_input(), any_input()}); + auto mask = pattern::optional({select, any_input(), any_input(), any_input(), any_input()}); return {mask, offset}; } -static std::shared_ptr named_parameter(std::shared_ptr node, const std::string& name) { - // Set name for both node and output tensor (should be only one tensor, and any other names will be overriden by a - // given single name) - node->set_friendly_name(name); - OPENVINO_ASSERT(node->get_output_size() == 1); - node->get_output_tensor(0).set_names({name}); - return node; -} - typedef std:: tuple, std::shared_ptr, std::shared_ptr, std::shared_ptr> node_tuple; @@ -254,8 +248,8 @@ typedef std:: static node_tuple kv_read_and_concat(ov::Output kv_current) { auto kv_past_var = wrap_type({any_input()}); auto kv_past = wrap_type({kv_past_var, any_input(), any_input()}); - kv_past = optional({kv_past, any_input()}); // Transpose is used when kv-cache is stored - // in a not usual layout, example: bloom + kv_past = pattern::optional({kv_past, any_input()}); // Transpose is used when kv-cache is stored + // in a not usual layout, example: bloom auto kv_current2 = any_input(); auto kv_current_reshaped = wrap_type({kv_current2, any_input()}); auto kv_concat = @@ -301,28 +295,33 @@ static ov::Dimension extract_num_kv_heads(const std::shared_ptr& unsqu } }; -ov::pass::StateManagementPattern::StateManagementPattern( - ParameterVector& kv_parameters, - ParameterVector& model_wide_params, - int& layer_index, - Output max_context_len, - ParameterVector& block_indices_inputs_for_each_layer, - ResultVector& score_results, - bool use_per_layer_block_indices_inputs, - bool use_score_outputs, - bool allow_cache_rotation, - bool allow_score_aggregation, - bool allow_xattention, - bool allow_adaptive_rkv, - bool allow_qq_bias, - ParameterVector& rotated_block_indices_inputs_for_each_layer, - ParameterVector& rotation_deltas_inputs_for_each_layer, - ParameterVector& xattention_threshold_inputs_for_each_layer, - ParameterVector& adaptive_rkv_diversity_block_set_indices_inputs_for_each_layer, - ParameterVector& adaptive_rkv_diversity_block_set_indices_begins_inputs_for_each_layer, - ResultVector& adaptive_rkv_diversity_results, - const std::map>& optional_model_wide_params, - std::unordered_set& var_ids_to_remove) { +StateManagementPattern::KvCacheParams StateManagementPattern::find_or_create_kv_params( + const std::shared_ptr& k_rv, + const std::shared_ptr& v_rv, + PaParams& pa_params) { + const auto& k_var_id = k_rv->get_variable_id() + "/k"; + const auto& v_var_id = v_rv->get_variable_id() + "/v"; + auto k_name = "key_cache." + std::to_string(m_layer_index); + auto v_name = "value_cache." + std::to_string(m_layer_index); + bool write_kv_cache = true; + + if (m_read_value_to_params.count(k_var_id) && m_read_value_to_params.count(v_var_id)) { + k_name = m_read_value_to_params.at(k_var_id); + v_name = m_read_value_to_params.at(v_var_id); + write_kv_cache = false; + } + + auto k_param = pa_params.add(k_name, ov::element::dynamic, ov::PartialShape::dynamic(4)); + auto v_param = pa_params.add(v_name, ov::element::dynamic, ov::PartialShape::dynamic(4)); + m_read_value_to_params.emplace(k_var_id, k_name); + m_read_value_to_params.emplace(v_var_id, v_name); + return {k_param, v_param, write_kv_cache}; +} + +ov::pass::StateManagementPattern::StateManagementPattern(PaParams& pa_params, + ov::pass::paged_attention::PaResults& results, + const ov::pass::paged_attention::Options& options, + std::unordered_set& var_ids_to_remove) { MATCHER_SCOPE(StateManagementPattern); auto k_current = any_input(); @@ -358,7 +357,7 @@ ov::pass::StateManagementPattern::StateManagementPattern( interim = wrap_type({unsqueeze, any_input(), any_input(), any_input()}); interim = wrap_type({interim, any_input(), any_input(), any_input()}); interim = wrap_type({std::make_shared(OutputVector{unsqueeze, interim}), any_input()}); - interim = optional({interim, any_input()}); // Reshape is missing sometimes in MQA case + interim = pattern::optional({interim, any_input()}); // Reshape is missing sometimes in MQA case return interim; }; @@ -427,25 +426,7 @@ ov::pass::StateManagementPattern::StateManagementPattern( auto sdpa_variants = std::make_shared(OutputVector{sdpa_with_4_inputs, sdpa_with_5_inputs, sdpa_with_6_inputs}); - // Set to true once a sliding_attention layer matching the gptoss_gemma3 pattern is found - // alongside a token_type_ids model input - the combination that uniquely identifies Gemma3 - // since pattern for full attention mask in Gemma3 is different than sliding window - // it has to be persistent in the callback, so shared_ptr is used - auto has_token_type_ids = std::make_shared(false); - - ov::matcher_pass_callback callback = [=, - &kv_parameters, - &model_wide_params, - &block_indices_inputs_for_each_layer, - &score_results, - &layer_index, - &rotated_block_indices_inputs_for_each_layer, - &rotation_deltas_inputs_for_each_layer, - &xattention_threshold_inputs_for_each_layer, - &adaptive_rkv_diversity_block_set_indices_inputs_for_each_layer, - &adaptive_rkv_diversity_block_set_indices_begins_inputs_for_each_layer, - &adaptive_rkv_diversity_results, - &var_ids_to_remove](Matcher& m) { + ov::matcher_pass_callback callback = [=, &pa_params, &results, &var_ids_to_remove](Matcher& m) { const auto& pattern_map = m.get_pattern_value_map(); const auto& real_q = pattern_map.at(q); @@ -474,17 +455,32 @@ ov::pass::StateManagementPattern::StateManagementPattern( auto num_k_heads = num_k_heads_dim.get_length(); auto num_v_heads = num_v_heads_dim.get_length(); - std::string layer_index_str = std::to_string(layer_index); - auto k_parameter = - named_parameter(std::make_shared(element::dynamic, ov::PartialShape::dynamic(4)), - "key_cache." + layer_index_str); - auto v_parameter = - named_parameter(std::make_shared(element::dynamic, ov::PartialShape::dynamic(4)), - "value_cache." + layer_index_str); - - layer_index += 1; - kv_parameters.push_back(k_parameter); - kv_parameters.push_back(v_parameter); + KvCacheParams kv_params; + + if (pattern_map.count(kv_past_var)) { + auto rv = ov::as_type_ptr(pattern_map.at(kv_past_var).get_node_shared_ptr()); + if (!rv) + return false; + kv_params = find_or_create_kv_params(rv, rv, pa_params); + var_ids_to_remove.insert(rv->get_variable_id()); + } else { + auto k_rv = ov::as_type_ptr(pattern_map.at(k_past_var).get_node_shared_ptr()); + auto v_rv = ov::as_type_ptr(pattern_map.at(v_past_var).get_node_shared_ptr()); + if (!k_rv || !v_rv) + return false; + kv_params = find_or_create_kv_params(k_rv, v_rv, pa_params); + var_ids_to_remove.insert(k_rv->get_variable_id()); + var_ids_to_remove.insert(v_rv->get_variable_id()); + } + + auto& k_parameter = kv_params.k; + auto& v_parameter = kv_params.v; + + // Set parameters to be in the same precision as the original K/V tensors, + // that allows to avoid unnecessary Convert operations in the graph + enable_keep_const_precision(k_parameter); + enable_keep_const_precision(v_parameter); + auto kv_transpose_order = v0::Constant::create(element::i64, Shape{4}, {0, 2, 1, 3}); auto q_transpose = std::make_shared(real_q, kv_transpose_order); @@ -495,7 +491,7 @@ ov::pass::StateManagementPattern::StateManagementPattern( if (pattern_map.count(qkv_current_split_node)) { // Fast track for merged K/V caches, based on the currently observed models topologies we don't need to // change layout and there is no point in the graph where it is in 4D. So `else` branch below is not - // applicable for this case. + std::to_string(layer_index - 1) + // applicable for this case. + std::to_string(m_layer_index) auto qkv_split = pattern_map.at(qkv_current_split_node).get_node_shared_ptr(); // TODO: Consider handling Q part as well as KV here, requires more changes in the code and sets // VariadicSplit before Concat as essential part of the pattern @@ -591,17 +587,13 @@ ov::pass::StateManagementPattern::StateManagementPattern( alibi_slopes = v0::Constant::create(element::f32, Shape{0}, {}); } - for (const auto& read_value : {k_past_var, v_past_var, kv_past_var}) { - if (pattern_map.count(read_value)) { - if (auto rv = ov::as_type_ptr( - pattern_map.at(read_value).get_node_shared_ptr())) { - var_ids_to_remove.insert(rv->get_variable_id()); - } - } - } - OutputVector pa_arguments = {q_reshape, k_reshape, v_reshape, k_parameter, v_parameter}; - pa_arguments.insert(pa_arguments.end(), model_wide_params.begin(), model_wide_params.end()); + pa_arguments.push_back(pa_params["past_lens"]); + pa_arguments.push_back(pa_params["subsequence_begins"]); + if (!options.use_per_layer_block_indices_inputs) { + pa_arguments.push_back(pa_params["block_indices"]); + } + pa_arguments.push_back(pa_params["block_indices_begins"]); std::shared_ptr sliding_window; if (pattern_map.count(phi3_offset)) { @@ -611,9 +603,6 @@ ov::pass::StateManagementPattern::StateManagementPattern( } sliding_window = std::make_shared(v0::Constant::create(element::i32, Shape{}, {2}), offset); } else if (pattern_map.count(gptoss_gemma3_offset)) { - // gptoss_gemma3 pattern + token_type_ids input uniquely identifies Gemma3; - // gpt-oss shares this sliding window pattern but has no token_type_ids. - *has_token_type_ids = optional_model_wide_params.count("token_type_ids"); auto offset = pattern_map.at(gptoss_gemma3_offset).get_node_shared_ptr(); if (pattern_map.at(gptoss_gemma3_offset).get_partial_shape().rank() != 0) { offset = std::make_shared(offset); @@ -626,47 +615,43 @@ ov::pass::StateManagementPattern::StateManagementPattern( sliding_window = v0::Constant::create(element::i32, Shape{}, {0}); } - std::initializer_list> additional_params = {scale, - sliding_window, - alibi_slopes, - max_context_len.get_node_shared_ptr()}; + auto max_context_len_param = pa_params["max_context_len"]; + OutputVector additional_params = {scale, sliding_window, alibi_slopes, max_context_len_param->output(0)}; pa_arguments.insert(pa_arguments.end(), additional_params.begin(), additional_params.end()); - if (use_per_layer_block_indices_inputs) { - auto block_indices = named_parameter(std::make_shared(element::i32, PartialShape{-1}), - "block_indices." + std::to_string(layer_index - 1)); + if (options.use_per_layer_block_indices_inputs) { + auto block_indices_name = "block_indices." + std::to_string(m_layer_index); + auto block_indices = pa_params.add(block_indices_name, element::i32, PartialShape{-1}); pa_arguments.insert(pa_arguments.begin() + 7, block_indices); - block_indices_inputs_for_each_layer.push_back(block_indices); } - if (allow_score_aggregation) { + if (options.allow_score_aggregation) { + auto score_aggregation_window = pa_params.add("score_aggregation_window", element::i32, PartialShape{-1}); OPENVINO_ASSERT( - optional_model_wide_params.count("score_aggregation_window"), + score_aggregation_window, "No score_aggregation_window input found. For using score aggregation mode, the model have to contain " "an additional input (Parameter) called score_aggregation_window."); - pa_arguments.insert(pa_arguments.end(), optional_model_wide_params.at("score_aggregation_window")); + pa_arguments.insert(pa_arguments.end(), score_aggregation_window); } else { pa_arguments.insert(pa_arguments.end(), v0::Constant::create(element::i32, Shape{0}, {})); } OPENVINO_ASSERT(pa_arguments.size() == 14); - if (allow_cache_rotation) { + if (options.allow_cache_rotation) { + auto model_rotation_trig_lut = pa_params.add("model_rotation_trig_lut", element::f32, PartialShape{-1, -1}); OPENVINO_ASSERT( - optional_model_wide_params.count("model_rotation_trig_lut"), + model_rotation_trig_lut, "No model_rotation_trig_lut input found. For using cache rotation, the model have to contain " "an additional input (Parameter) called model_rotation_trig_lut."); - auto rotated_block_indices = - named_parameter(std::make_shared(element::i32, PartialShape{-1}), - "rotated_block_indices." + std::to_string(layer_index - 1)); - auto rotation_deltas = named_parameter(std::make_shared(element::i32, PartialShape{-1, -1}), - "rotation_deltas." + std::to_string(layer_index - 1)); + auto rotated_block_indices_name = "rotated_block_indices." + std::to_string(m_layer_index); + auto rotation_deltas_name = "rotation_deltas." + std::to_string(m_layer_index); + auto rotated_block_indices = pa_params.add(rotated_block_indices_name, element::i32, PartialShape{-1}); + auto rotation_deltas = pa_params.add(rotation_deltas_name, element::i32, PartialShape{-1, -1}); pa_arguments.insert(pa_arguments.begin() + 14, rotated_block_indices); pa_arguments.insert(pa_arguments.begin() + 15, rotation_deltas); - pa_arguments.insert(pa_arguments.begin() + 16, optional_model_wide_params.at("model_rotation_trig_lut")); + pa_arguments.insert(pa_arguments.begin() + 16, model_rotation_trig_lut); - rotated_block_indices_inputs_for_each_layer.push_back(rotated_block_indices); - rotation_deltas_inputs_for_each_layer.push_back(rotation_deltas); } else { auto rotated_block_indices = v0::Constant::create(element::i32, Shape{0}, {}); auto rotation_deltas = v0::Constant::create(element::i32, Shape{0}, {}); @@ -676,19 +661,20 @@ ov::pass::StateManagementPattern::StateManagementPattern( } OPENVINO_ASSERT(pa_arguments.size() == 17); - if (allow_xattention) { - OPENVINO_ASSERT(optional_model_wide_params.count("xattention_block_size"), + if (options.allow_xattention) { + auto xattention_block_size = pa_params.add("xattention_block_size", element::i32, PartialShape{}); + OPENVINO_ASSERT(xattention_block_size, "No xattention_block_size input found. For using XAttention, the model have to contain " "an additional input (Parameter) called xattention_block_size."); - OPENVINO_ASSERT(optional_model_wide_params.count("xattention_stride"), + auto xattention_stride = pa_params.add("xattention_stride", element::i32, PartialShape{}); + OPENVINO_ASSERT(xattention_stride, "No xattention_stride input found. For using XAttention, the model have to contain " "an additional input (Parameter) called xattention_stride."); - auto xattention_threshold = named_parameter(std::make_shared(element::f32, PartialShape{-1}), - "xattention_threshold." + std::to_string(layer_index - 1)); + auto xattention_threshold_name = "xattention_threshold." + std::to_string(m_layer_index); + auto xattention_threshold = pa_params.add(xattention_threshold_name, element::f32, PartialShape{-1}); pa_arguments.insert(pa_arguments.begin() + 17, xattention_threshold); - pa_arguments.insert(pa_arguments.begin() + 18, optional_model_wide_params.at("xattention_block_size")); - pa_arguments.insert(pa_arguments.begin() + 19, optional_model_wide_params.at("xattention_stride")); - xattention_threshold_inputs_for_each_layer.push_back(xattention_threshold); + pa_arguments.insert(pa_arguments.begin() + 18, xattention_block_size); + pa_arguments.insert(pa_arguments.begin() + 19, xattention_stride); } else { auto xattention_threshold = v0::Constant::create(element::f32, Shape{0}, {}); pa_arguments.insert(pa_arguments.begin() + 17, xattention_threshold); @@ -711,32 +697,32 @@ ov::pass::StateManagementPattern::StateManagementPattern( v0::Constant::create(real_q.get_element_type(), Shape{0, 0, 0, 0}, {})); } - if (allow_adaptive_rkv) { + if (options.allow_adaptive_rkv) { + auto adaptive_rkv_start_size = pa_params.add("adaptive_rkv_start_size", element::i32, PartialShape{}); OPENVINO_ASSERT( - optional_model_wide_params.count("adaptive_rkv_start_size"), + adaptive_rkv_start_size, "No adaptive_rkv_start_size input found. For using Adaptive R-KV, the model have to contain " "an additional input (Parameter) called adaptive_rkv_start_size."); + auto adaptive_rkv_evictable_sizes = + pa_params.add("adaptive_rkv_evictable_sizes", element::i32, PartialShape{-1}); OPENVINO_ASSERT( - optional_model_wide_params.count("adaptive_rkv_evictable_sizes"), + adaptive_rkv_evictable_sizes, "No adaptive_rkv_evictable_sizes input found. For using Adaptive R-KV, the model have to contain " "an additional input (Parameter) called adaptive_rkv_evictable_sizes."); - pa_arguments.insert(pa_arguments.begin() + 21, optional_model_wide_params.at("adaptive_rkv_start_size")); - pa_arguments.insert(pa_arguments.begin() + 22, - optional_model_wide_params.at("adaptive_rkv_evictable_sizes")); + pa_arguments.insert(pa_arguments.begin() + 21, adaptive_rkv_start_size); + pa_arguments.insert(pa_arguments.begin() + 22, adaptive_rkv_evictable_sizes); + auto adaptive_rkv_diversity_block_set_indices_name = + "adaptive_rkv_diversity_block_set_indices." + std::to_string(m_layer_index); auto adaptive_rkv_diversity_block_set_indices = - named_parameter(std::make_shared(element::i32, PartialShape{-1}), - "adaptive_rkv_diversity_block_set_indices." + std::to_string(layer_index - 1)); + pa_params.add(adaptive_rkv_diversity_block_set_indices_name, element::i32, PartialShape{-1}); pa_arguments.insert(pa_arguments.begin() + 23, adaptive_rkv_diversity_block_set_indices); - adaptive_rkv_diversity_block_set_indices_inputs_for_each_layer.push_back( - adaptive_rkv_diversity_block_set_indices); + auto adaptive_rkv_diversity_block_set_indices_begins_name = + "adaptive_rkv_diversity_block_set_indices_begins." + std::to_string(m_layer_index); auto adaptive_rkv_diversity_block_set_indices_begins = - named_parameter(std::make_shared(element::i32, PartialShape{-1}), - "adaptive_rkv_diversity_block_set_indices_begins." + std::to_string(layer_index - 1)); + pa_params.add(adaptive_rkv_diversity_block_set_indices_begins_name, element::i32, PartialShape{-1}); pa_arguments.insert(pa_arguments.begin() + 24, adaptive_rkv_diversity_block_set_indices_begins); - adaptive_rkv_diversity_block_set_indices_begins_inputs_for_each_layer.push_back( - adaptive_rkv_diversity_block_set_indices_begins); } else { pa_arguments.insert(pa_arguments.begin() + 21, v0::Constant::create(element::i32, Shape{}, {0})); @@ -745,8 +731,11 @@ ov::pass::StateManagementPattern::StateManagementPattern( pa_arguments.insert(pa_arguments.begin() + 24, v0::Constant::create(element::i32, Shape{0}, {})); } - if (*has_token_type_ids) { - std::shared_ptr token_type_ids = optional_model_wide_params.at("token_type_ids"); + // Meant to be used for Gemma family models with bidirectional image attention. If the condition is met for + // other models (ex. BERT) it's probably unintended behavior, as token_type_ids has been historically used + // in different contexts. + if (pa_params.exists("token_type_ids")) { + std::shared_ptr token_type_ids = pa_params["token_type_ids"]; if (token_type_ids->get_element_type() != element::i32) { token_type_ids = std::make_shared(token_type_ids, element::i32); } @@ -755,19 +744,25 @@ ov::pass::StateManagementPattern::StateManagementPattern( pa_arguments.insert(pa_arguments.begin() + 25, v0::Constant::create(element::i32, Shape{0}, {})); } - if (allow_qq_bias) { - OPENVINO_ASSERT(optional_model_wide_params.find("qq_bias") != optional_model_wide_params.end(), + if (options.allow_qq_bias) { + auto qq_bias = pa_params.add("qq_bias", element::u8, PartialShape{-1}); + OPENVINO_ASSERT(qq_bias, "No qq_bias input found. For using QQ bias, the model have to contain " "an additional input (Parameter) called qq_bias."); - pa_arguments.insert(pa_arguments.begin() + 26, optional_model_wide_params.at("qq_bias")); - pa_arguments.insert(pa_arguments.begin() + 27, optional_model_wide_params.at("qq_bias_begins")); + auto qq_bias_begins = pa_params.add("qq_bias_begins", element::i32, PartialShape{-1}); + OPENVINO_ASSERT(qq_bias_begins, + "No qq_bias_begins input found. For using QQ bias, the model have to contain " + "an additional input (Parameter) called qq_bias_begins."); + pa_arguments.insert(pa_arguments.begin() + 26, qq_bias); + pa_arguments.insert(pa_arguments.begin() + 27, qq_bias_begins); } else { pa_arguments.insert(pa_arguments.begin() + 26, v0::Constant::create(element::u8, Shape{0}, {})); pa_arguments.insert(pa_arguments.begin() + 27, v0::Constant::create(element::i32, Shape{0}, {})); } OPENVINO_ASSERT(pa_arguments.size() == 28); - auto paged_attention = std::make_shared(pa_arguments); + auto paged_attention = + std::make_shared(pa_arguments, kv_params.write_kv_cache); paged_attention->get_rt_info()[NUM_K_HEADS] = num_k_heads; paged_attention->get_rt_info()[K_HEAD_SIZE] = k_head_size; paged_attention->get_rt_info()[NUM_V_HEADS] = num_v_heads; @@ -789,21 +784,19 @@ ov::pass::StateManagementPattern::StateManagementPattern( 0); auto pa_reshape = std::make_shared(paged_attention->output(0), pa_shape, true); auto pa_transpose = std::make_shared(pa_reshape, kv_transpose_order); - if (use_score_outputs) { - auto score_result = std::make_shared(paged_attention->output(1)); - score_result->get_output_tensor(0).set_names({"scores." + std::to_string(layer_index - 1)}); - score_results.push_back(score_result); + if (options.use_score_outputs) { + auto score_name = "scores." + std::to_string(m_layer_index); + results.add(score_name, paged_attention->output(1)); } - if (allow_adaptive_rkv) { - auto similarity_result = std::make_shared(paged_attention->output(2)); - similarity_result->get_output_tensor(0).set_names( - {"adaptive_rkv_diversity." + std::to_string(layer_index - 1)}); - adaptive_rkv_diversity_results.push_back(similarity_result); + if (options.allow_adaptive_rkv) { + auto similarity_name = "adaptive_rkv_diversity." + std::to_string(m_layer_index); + results.add(similarity_name, paged_attention->output(2)); } pa_transpose->set_friendly_name(sdpa_node->get_friendly_name()); replace_node(m.get_match_root(), pa_transpose); + m_layer_index += 1; return true; }; diff --git a/src/common/transformations/src/transformations/sdpa_to_paged_attention/total_sequence_length_pattern.cpp b/src/common/transformations/src/transformations/paged_attention/total_sequence_length_pattern.cpp similarity index 98% rename from src/common/transformations/src/transformations/sdpa_to_paged_attention/total_sequence_length_pattern.cpp rename to src/common/transformations/src/transformations/paged_attention/total_sequence_length_pattern.cpp index f4c7113b3e99..177d1183b505 100644 --- a/src/common/transformations/src/transformations/sdpa_to_paged_attention/total_sequence_length_pattern.cpp +++ b/src/common/transformations/src/transformations/paged_attention/total_sequence_length_pattern.cpp @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 // -#include "transformations/sdpa_to_paged_attention/total_sequence_length_pattern.hpp" +#include "transformations/paged_attention/total_sequence_length_pattern.hpp" #include "openvino/cc/pass/itt.hpp" #include "openvino/core/graph_util.hpp" diff --git a/src/common/transformations/src/transformations/rt_info/attributes.cpp b/src/common/transformations/src/transformations/rt_info/attributes.cpp index 97420f849f93..d19422334d68 100644 --- a/src/common/transformations/src/transformations/rt_info/attributes.cpp +++ b/src/common/transformations/src/transformations/rt_info/attributes.cpp @@ -8,7 +8,10 @@ ov::pass::Attributes::Attributes() { register_factory(); register_factory(); register_factory(); + OPENVINO_SUPPRESS_DEPRECATED_START register_factory(); + OPENVINO_SUPPRESS_DEPRECATED_END + register_factory(); register_factory(); register_factory(); register_factory(); diff --git a/src/common/transformations/src/transformations/rt_info/disable_fp16_compression.cpp b/src/common/transformations/src/transformations/rt_info/disable_fp16_compression.cpp deleted file mode 100644 index 28999cfb2f01..000000000000 --- a/src/common/transformations/src/transformations/rt_info/disable_fp16_compression.cpp +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (C) 2018-2026 Intel Corporation -// SPDX-License-Identifier: Apache-2.0 -// - -#include "transformations/rt_info/disable_fp16_compression.hpp" - -namespace { -const std::string& get_postponed_fp16_compression_tag() { - static const std::string postponed_fp16_compression_tag("postponed_fp16_compression"); - return postponed_fp16_compression_tag; -} -} // namespace - -void ov::disable_fp16_compression(const std::shared_ptr& node) { - auto& rt_info = node->get_rt_info(); - rt_info[DisableFP16Compression::get_type_info_static()] = DisableFP16Compression{}; -} - -void ov::enable_fp16_compression(const std::shared_ptr& node) { - auto& rt_info = node->get_rt_info(); - rt_info.erase(DisableFP16Compression::get_type_info_static()); -} - -bool ov::fp16_compression_is_disabled(const std::shared_ptr& node) { - const auto& rt_info = node->get_rt_info(); - return rt_info.count(DisableFP16Compression::get_type_info_static()); -} - -void ov::postpone_fp16_compression(ov::RTMap& rt_info) { - rt_info[get_postponed_fp16_compression_tag()] = true; -} - -bool ov::is_fp16_compression_postponed(const ov::RTMap& rt_info) { - return rt_info.count(get_postponed_fp16_compression_tag()); -} - -void ov::do_not_postpone_fp16_compression(ov::RTMap& rt_info) { - rt_info.erase(get_postponed_fp16_compression_tag()); -} diff --git a/src/common/transformations/src/transformations/rt_info/disable_precision_conversion.cpp b/src/common/transformations/src/transformations/rt_info/disable_precision_conversion.cpp new file mode 100644 index 000000000000..8a00b05d2090 --- /dev/null +++ b/src/common/transformations/src/transformations/rt_info/disable_precision_conversion.cpp @@ -0,0 +1,149 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "transformations/rt_info/disable_precision_conversion.hpp" + +#include + +#include "openvino/util/common_util.hpp" + +namespace { +const std::string& get_postponed_fp16_compression_tag() { + static const std::string postponed_fp16_compression_tag("postponed_fp16_compression"); + return postponed_fp16_compression_tag; +} +} // namespace + +void ov::postpone_fp16_compression(ov::RTMap& rt_info) { + rt_info[get_postponed_fp16_compression_tag()] = true; +} + +bool ov::is_fp16_compression_postponed(const ov::RTMap& rt_info) { + return rt_info.count(get_postponed_fp16_compression_tag()); +} + +void ov::do_not_postpone_fp16_compression(ov::RTMap& rt_info) { + rt_info.erase(get_postponed_fp16_compression_tag()); +} + +void ov::disable_conversion(const std::shared_ptr& node, const element::Type& to) { + return disable_conversion(node, element::dynamic, to); +} + +void ov::disable_conversion(const std::shared_ptr& node, const element::Type& from, const element::Type& to) { + auto& rt_info = node->get_rt_info(); + auto it = rt_info.find(DisablePrecisionConversion::get_type_info_static()); + if (it != rt_info.end()) { + auto& dpc_attribute = it->second.as(); + dpc_attribute.m_disabled_precisions[from].insert(to); + } else { + rt_info[DisablePrecisionConversion::get_type_info_static()] = DisablePrecisionConversion(from, to); + } +} + +void ov::disable_conversion(const std::shared_ptr& node, + const element::TypeVector& from_types, + const element::TypeVector& to_types) { + for (const auto& from : from_types) { + for (const auto& to : to_types) { + disable_conversion(node, from, to); + } + } +} + +void ov::enable_conversion(const std::shared_ptr& node, const element::Type& to) { + enable_conversion(node, element::dynamic, to); +} + +void ov::enable_conversion(const std::shared_ptr& node, const element::Type& from, const element::Type& to) { + auto& rt_info = node->get_rt_info(); + auto it = rt_info.find(DisablePrecisionConversion::get_type_info_static()); + if (it != rt_info.end()) { + auto& dpc_attribute = it->second.as(); + auto from_it = dpc_attribute.m_disabled_precisions.find(from); + if (from_it != dpc_attribute.m_disabled_precisions.end()) { + from_it->second.erase(to); + if (from_it->second.empty()) { + dpc_attribute.m_disabled_precisions.erase(from_it); + } + } + } +} + +void ov::enable_conversion(const std::shared_ptr& node, + const element::TypeVector& from_types, + const element::TypeVector& to_types) { + for (const auto& from : from_types) { + for (const auto& to : to_types) { + enable_conversion(node, from, to); + } + } +} + +bool ov::is_conversion_disabled(const std::shared_ptr& node, const element::Type& to) { + return is_conversion_disabled(node, element::dynamic, to); +} + +bool ov::is_conversion_disabled(const std::shared_ptr& node, + const element::Type& from, + const element::Type& to) { + auto& rt_info = node->get_rt_info(); + auto it = rt_info.find(DisablePrecisionConversion::get_type_info_static()); + if (it != rt_info.end()) { + auto& dpc_attribute = it->second.as(); + + auto dyn_it = dpc_attribute.m_disabled_precisions.find(element::dynamic); + if (dyn_it != dpc_attribute.m_disabled_precisions.end() && + (dyn_it->second.count(element::dynamic) || dyn_it->second.count(to))) { + return true; + } + + auto from_it = dpc_attribute.m_disabled_precisions.find(from); + if (from_it != dpc_attribute.m_disabled_precisions.end()) { + const auto& to_set = from_it->second; + if (to_set.count(to) || to_set.count(element::dynamic)) { + return true; + } + } + + return false; + } + return false; +} + +bool ov::DisablePrecisionConversion::visit_attributes(AttributeVisitor& visitor) { + visitor.on_attribute("value", m_disabled_precisions); + return true; +} + +// Format: "from:to1,to2;from2:to3,to4" - entries separated by ';' (no ';' at the end) +const std::string& ov::AttributeAdapter::get() { + std::ostringstream oss; + auto it = m_ref.begin(); + if (it != m_ref.end()) { + oss << it->first << ':' << ov::util::join(it->second, ","); + while (++it != m_ref.end()) { + const auto& [from, to] = *it; + oss << ';' << from << ':' << ov::util::join(to, ","); + } + } + m_serialized = oss.str(); + return m_serialized; +} + +void ov::AttributeAdapter::set(const std::string& value) { + m_ref.clear(); + for (std::string_view sv(value); !sv.empty();) { + const auto sep_pos = sv.find(';'); + const auto entry = sv.substr(0, sep_pos); + sv = sep_pos != std::string_view::npos ? sv.substr(sep_pos + 1) : std::string_view{}; + if (const auto colon_pos = entry.find(':'); colon_pos != std::string_view::npos) { + element::Type from_type(std::string(entry.substr(0, colon_pos))); + auto& to_set = m_ref[from_type]; + ov::util::view_transform(entry.substr(colon_pos + 1), std::inserter(to_set, to_set.end()), ",", [](auto s) { + return element::Type(std::string(s)); + }); + } + } +} \ No newline at end of file diff --git a/src/common/transformations/src/transformations/utils/extract_subgraph.cpp b/src/common/transformations/src/transformations/utils/extract_subgraph.cpp new file mode 100644 index 000000000000..d2dcb920cf75 --- /dev/null +++ b/src/common/transformations/src/transformations/utils/extract_subgraph.cpp @@ -0,0 +1,144 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "transformations/utils/extract_subgraph.hpp" + +#include +#include +#include +#include +#include + +#include "openvino/core/graph_util.hpp" +#include "openvino/op/parameter.hpp" +#include "openvino/op/sink.hpp" + +namespace ov::util { + +namespace { +ov::SinkVector collect_sinks(const ov::OutputVector& subgraph_outputs, + const ov::ParameterVector& subgraph_parameters) { + std::unordered_set param_set; + for (const auto& p : subgraph_parameters) { + param_set.insert(p.get()); + } + + // First pass: collect all nodes in the subgraph by walking backward from outputs + std::unordered_set subgraph_nodes; + std::stack stack; + for (const auto& output : subgraph_outputs) { + stack.push(output.get_node()); + } + while (!stack.empty()) { + auto* node = stack.top(); + stack.pop(); + if (!subgraph_nodes.insert(node).second || param_set.count(node)) { + continue; + } + for (size_t i = 0; i < node->get_input_size(); ++i) { + stack.push(node->get_input_node_ptr(i)); + } + } + + // Second pass: find Sink nodes that consume outputs of subgraph nodes + ov::SinkVector sinks; + for (auto* node : subgraph_nodes) { + for (const auto& output : node->shared_from_this()->outputs()) { + for (const auto& target_input : output.get_target_inputs()) { + if (auto sink = std::dynamic_pointer_cast(target_input.get_node()->shared_from_this())) { + sinks.push_back(sink); + } + } + } + } + return sinks; +} +} // namespace + +std::shared_ptr extract_subgraph(const std::vector>& subgraph_inputs, + const ov::OutputVector& subgraph_outputs) { + ov::OutputVector replaced_outputs; + ov::ParameterVector subgraph_parameters; + subgraph_parameters.reserve(subgraph_inputs.size()); + replaced_outputs.reserve(subgraph_inputs.size()); + + for (const auto& input : subgraph_inputs) { + const auto new_parameter = + std::make_shared(input.get_element_type(), input.get_partial_shape()); + subgraph_parameters.push_back(new_parameter); + + const auto& source_output = input.get_source_output(); + replaced_outputs.push_back(source_output); + OPENVINO_ASSERT(ov::replace_output_update_name(source_output, new_parameter->output(0)), + "extract_subgraph: failed to replace boundary source output '", + source_output.get_node()->get_friendly_name(), + "' at port ", + source_output.get_index(), + ". The requested subgraph input cannot be replaced safely."); + } + OPENVINO_ASSERT(subgraph_parameters.size() == replaced_outputs.size(), + "extract_subgraph: number of subgraph_parameters is not equal to the number of replaced_outputs"); + + auto sinks = collect_sinks(subgraph_outputs, subgraph_parameters); + auto subgraph = + std::make_shared(subgraph_outputs, sinks, subgraph_parameters)->clone(); + + for (size_t i = 0; i < subgraph_parameters.size(); ++i) { + subgraph_parameters[i]->output(0).replace(replaced_outputs[i]); + } + return subgraph; +} + +namespace { +std::pair>, std::vector>> resolve_subgraph_boundaries( + const std::shared_ptr& model, + const std::multimap& input_map, + const std::multimap& output_map) { + std::vector> inputs; + std::vector> outputs; + + std::set resolved_input_names; + std::set resolved_output_names; + + for (const auto& node : model->get_ordered_ops()) { + const auto& name = node->get_friendly_name(); + + const auto input_range = input_map.equal_range(name); + for (auto it = input_range.first; it != input_range.second; ++it) { + inputs.push_back(node->input(it->second)); + resolved_input_names.insert(name); + } + + const auto output_range = output_map.equal_range(name); + for (auto it = output_range.first; it != output_range.second; ++it) { + outputs.push_back(node->output(it->second)); + resolved_output_names.insert(name); + } + } + + for (const auto& kv : input_map) { + OPENVINO_ASSERT(resolved_input_names.count(kv.first), + "extract_subgraph: node '", + kv.first, + "' specified in subgraph_inputs was not found in the model"); + } + for (const auto& kv : output_map) { + OPENVINO_ASSERT(resolved_output_names.count(kv.first), + "extract_subgraph: node '", + kv.first, + "' specified in subgraph_outputs was not found in the model"); + } + + return {inputs, outputs}; +} +} // namespace + +std::shared_ptr extract_subgraph(const std::shared_ptr& model, + const std::multimap& subgraph_inputs, + const std::multimap& subgraph_outputs) { + auto [inputs, outputs] = resolve_subgraph_boundaries(model, subgraph_inputs, subgraph_outputs); + return extract_subgraph(inputs, outputs); +} + +} // namespace ov::util diff --git a/src/common/transformations/tests/CMakeLists.txt b/src/common/transformations/tests/CMakeLists.txt index d146b554e4ed..46ffd390b470 100644 --- a/src/common/transformations/tests/CMakeLists.txt +++ b/src/common/transformations/tests/CMakeLists.txt @@ -8,9 +8,16 @@ if(CMAKE_COMPILER_IS_GNUCXX OR OV_COMPILER_IS_CLANG) ov_add_compiler_flags(-Wno-missing-declarations) endif() +set(EXCLUDED_PATHS "") +if(NOT ENABLE_DEBUG_CAPS) + list(APPEND EXCLUDED_PATHS "${CMAKE_CURRENT_SOURCE_DIR}/utils/extract_subgraph_test") +endif() + ov_add_test_target( NAME ${TARGET_NAME} ROOT ${CMAKE_CURRENT_SOURCE_DIR} + EXCLUDED_SOURCE_PATHS + ${EXCLUDED_PATHS} DEPENDENCIES LINK_LIBRARIES gmock diff --git a/src/common/transformations/tests/common_optimizations/activations_scaling_test.cpp b/src/common/transformations/tests/common_optimizations/activations_scaling_test.cpp index 9ea6fe7204d6..ee133448b056 100644 --- a/src/common/transformations/tests/common_optimizations/activations_scaling_test.cpp +++ b/src/common/transformations/tests/common_optimizations/activations_scaling_test.cpp @@ -23,6 +23,7 @@ #include "openvino/op/shape_of.hpp" #include "openvino/op/variadic_split.hpp" #include "openvino/pass/manager.hpp" +#include "ov_ops/moe_compressed.hpp" #include "transformations/common_optimizations/lin_op_sequence_fusion.hpp" #include "transformations/common_optimizations/nop_elimination.hpp" #include "transformations/utils/utils.hpp" @@ -85,7 +86,7 @@ TEST_F(TransformationTestsF, ScaleDownSingleLayerTest_f32) { auto weights_const1 = v0::Constant::create(ov::element::f16, ov::Shape{8, 16}, {1}); auto matmul1 = std::make_shared(matmul0, weights_const1); auto convert = std::make_shared(matmul1, ov::element::f32); - disable_fp16_compression(convert); + disable_conversion(convert, ov::element::f16); disable_constant_folding(convert); auto convert_f16 = std::make_shared(convert, ov::element::f16); auto result = std::make_shared(convert_f16); @@ -113,6 +114,164 @@ TEST_F(TransformationTestsF, ScaleDownSingleLayerTest_f32) { } } +namespace { +// Shapes used for all GEMM2_BIAS_SWIGLU_CLAMP tests. +// Matches the layout expected by CreateMOECompressedOp in src/plugin/ops/moe.cpp. +constexpr size_t kTokens = 4; +constexpr size_t kHiddenSize = 16; +constexpr size_t kInterSize = 32; // ofm for up projection (2 * swiglu half) +constexpr size_t kNumExperts = 2; +constexpr size_t kTopK = 2; +constexpr size_t kGroupSize = 8; +constexpr size_t kNumGroups = kHiddenSize / kGroupSize; + +// Build a GEMM2_BIAS_SWIGLU_CLAMP MOECompressed node with the given has_zp layout. +// Input order (see scale_down_moe_compressed.cpp): +// 0: hidden_states, 1: routing_weights, 2: topk_idx, +// 3: w_up, 4: scale_up, [5: zp_up,] , +// , , [,] +std::shared_ptr make_moe_compressed_gemm2(const ov::Output& hidden_states, + const ov::Output& routing_weights, + const ov::Output& topk_idx, + const ov::Output& bias_up, + const ov::Output& bias_down, + bool has_zp, + float scale_factor = -1.0f) { + auto w_up = ov::op::v0::Constant::create(element::u4, Shape{kNumExperts, kInterSize, kNumGroups, kGroupSize}, {1}); + auto scale_up = ov::op::v0::Constant::create(element::f16, Shape{kNumExperts, kInterSize, kNumGroups, 1}, {0.01f}); + auto w_down = + ov::op::v0::Constant::create(element::u4, Shape{kNumExperts, kHiddenSize, kNumGroups, kGroupSize}, {1}); + auto scale_down = + ov::op::v0::Constant::create(element::f16, Shape{kNumExperts, kHiddenSize, kNumGroups, 1}, {0.01f}); + + ov::OutputVector args; + args.push_back(hidden_states); + args.push_back(routing_weights); + args.push_back(topk_idx); + args.push_back(w_up); + args.push_back(scale_up); + if (has_zp) { + auto zp_up = ov::op::v0::Constant::create(element::u4, Shape{kNumExperts, kInterSize, kNumGroups, 1}, {0}); + args.push_back(zp_up); + } + args.push_back(bias_up); + args.push_back(w_down); + args.push_back(scale_down); + if (has_zp) { + auto zp_down = ov::op::v0::Constant::create(element::u4, Shape{kNumExperts, kHiddenSize, kNumGroups, 1}, {0}); + args.push_back(zp_down); + } + args.push_back(bias_down); + + ov::op::internal::MOECompressed::Config config; + config.expert_type = ov::op::internal::MOE::Expert_type::GEMM2_BIAS_SWIGLU_CLAMP; + config.hidden_size = kHiddenSize; + config.inter_size = kInterSize; + config.num_expert = kNumExperts; + config.top_k = kTopK; + config.group_size = kGroupSize; + config.has_zp = has_zp; + config.out_type = ov::element::dynamic; + config.scale_factor = scale_factor; + return std::make_shared(args, config); +} + +} // namespace + +TEST_F(TransformationTestsF, ScaleDownSingleLayerTest_MOE_NoZp) { + const float scale_factor = 8.f; + { + auto hidden_states = std::make_shared(element::f16, PartialShape{kTokens, kHiddenSize}); + auto routing_weights = std::make_shared(element::f16, PartialShape{kTokens, kTopK}); + auto topk_idx = std::make_shared(element::i32, PartialShape{kTokens, kTopK}); + auto bias_up = ov::op::v0::Constant::create(element::f16, Shape{kNumExperts, 1, kInterSize}, {0.5f}); + auto bias_down = ov::op::v0::Constant::create(element::f16, Shape{kNumExperts, 1, kHiddenSize}, {0.25f}); + + auto moe = + make_moe_compressed_gemm2(hidden_states, routing_weights, topk_idx, bias_up, bias_down, /*has_zp=*/false); + auto convert = std::make_shared(moe, element::f32); + auto result = std::make_shared(convert); + model = std::make_shared(ov::ResultVector{result}, + ov::ParameterVector{hidden_states, routing_weights, topk_idx}); + manager.register_pass(scale_factor, ov::element::f16); + } + { + auto hidden_states = std::make_shared(element::f16, PartialShape{kTokens, kHiddenSize}); + auto routing_weights = std::make_shared(element::f16, PartialShape{kTokens, kTopK}); + auto topk_idx = std::make_shared(element::i32, PartialShape{kTokens, kTopK}); + auto bias_up = ov::op::v0::Constant::create(element::f16, Shape{kNumExperts, 1, kInterSize}, {0.5f}); + auto bias_down = ov::op::v0::Constant::create(element::f16, Shape{kNumExperts, 1, kHiddenSize}, {0.25f}); + + auto scale_down_const = ov::op::v0::Constant::create(element::f16, Shape{}, {1.f / scale_factor}); + auto hidden_scaled = std::make_shared(hidden_states, scale_down_const); + auto bias_up_scaled = std::make_shared(bias_up, scale_down_const); + auto bias_down_scaled = std::make_shared(bias_down, scale_down_const); + + auto moe = make_moe_compressed_gemm2(hidden_scaled, + routing_weights, + topk_idx, + bias_up_scaled, + bias_down_scaled, + /*has_zp=*/false, + /*scale_factor=*/scale_factor); + + auto scale_up_const = ov::op::v0::Constant::create(element::f16, Shape{}, {scale_factor}); + auto scale_up = std::make_shared(moe, scale_up_const); + auto convert = std::make_shared(scale_up, element::f32); + auto result = std::make_shared(convert); + model_ref = std::make_shared(ov::ResultVector{result}, + ov::ParameterVector{hidden_states, routing_weights, topk_idx}); + comparator.enable(FunctionsComparator::ATTRIBUTES); + } +} + +TEST_F(TransformationTestsF, ScaleDownSingleLayerTest_MOE_WithZp) { + const float scale_factor = 8.f; + { + auto hidden_states = std::make_shared(element::f16, PartialShape{kTokens, kHiddenSize}); + auto routing_weights = std::make_shared(element::f16, PartialShape{kTokens, kTopK}); + auto topk_idx = std::make_shared(element::i32, PartialShape{kTokens, kTopK}); + auto bias_up = ov::op::v0::Constant::create(element::f16, Shape{kNumExperts, 1, kInterSize}, {0.5f}); + auto bias_down = ov::op::v0::Constant::create(element::f16, Shape{kNumExperts, 1, kHiddenSize}, {0.25f}); + + auto moe = + make_moe_compressed_gemm2(hidden_states, routing_weights, topk_idx, bias_up, bias_down, /*has_zp=*/true); + auto convert = std::make_shared(moe, element::f32); + auto result = std::make_shared(convert); + model = std::make_shared(ov::ResultVector{result}, + ov::ParameterVector{hidden_states, routing_weights, topk_idx}); + manager.register_pass(scale_factor, ov::element::f16); + } + { + auto hidden_states = std::make_shared(element::f16, PartialShape{kTokens, kHiddenSize}); + auto routing_weights = std::make_shared(element::f16, PartialShape{kTokens, kTopK}); + auto topk_idx = std::make_shared(element::i32, PartialShape{kTokens, kTopK}); + auto bias_up = ov::op::v0::Constant::create(element::f16, Shape{kNumExperts, 1, kInterSize}, {0.5f}); + auto bias_down = ov::op::v0::Constant::create(element::f16, Shape{kNumExperts, 1, kHiddenSize}, {0.25f}); + + auto scale_down_const = ov::op::v0::Constant::create(element::f16, Shape{}, {1.f / scale_factor}); + auto hidden_scaled = std::make_shared(hidden_states, scale_down_const); + auto bias_up_scaled = std::make_shared(bias_up, scale_down_const); + auto bias_down_scaled = std::make_shared(bias_down, scale_down_const); + + auto moe = make_moe_compressed_gemm2(hidden_scaled, + routing_weights, + topk_idx, + bias_up_scaled, + bias_down_scaled, + /*has_zp=*/true, + /*scale_factor=*/scale_factor); + + auto scale_up_const = ov::op::v0::Constant::create(element::f16, Shape{}, {scale_factor}); + auto scale_up = std::make_shared(moe, scale_up_const); + auto convert = std::make_shared(scale_up, element::f32); + auto result = std::make_shared(convert); + model_ref = std::make_shared(ov::ResultVector{result}, + ov::ParameterVector{hidden_states, routing_weights, topk_idx}); + comparator.enable(FunctionsComparator::ATTRIBUTES); + } +} + TEST_F(TransformationTestsF, EliminateScalarMulTest) { double epsilon = 1.f; float scale_factor = 8.f; diff --git a/src/common/transformations/tests/common_optimizations/compress_float_constants_test.cpp b/src/common/transformations/tests/common_optimizations/compress_float_constants_test.cpp index addcf8241ec9..c65ed87d5f9e 100644 --- a/src/common/transformations/tests/common_optimizations/compress_float_constants_test.cpp +++ b/src/common/transformations/tests/common_optimizations/compress_float_constants_test.cpp @@ -605,15 +605,34 @@ TEST_F(TransformationTestsF, KeepFWPrecisionForBF16Constants_test_1) { comparator.enable(FunctionsComparator::CmpValues::CONST_VALUES); } -// Scalar constant with high FP16 relative rounding error (e.g., log(16) = 2.7725887, -// rel_error = 0.031%) should NOT be compressed to FP16 to avoid precision degradation -// that cascades through deep networks. -TEST_F(TransformationTestsF, CompressConstants_skip_scalar_with_high_f16_error) { - // Model: Parameter -> Multiply(input, Const(2.7725887)) -> Result - // Scalar 2.7725887 has rel_error > 1e-4 when rounded to FP16 -> stays in FP32. +struct RelativeThresholdTestParams { + // Input shape for Multiply. + ov::Shape input_shape; + // Constant shape controls whether scalar or tensor relative threshold is used. + ov::Shape constant_shape; + // Repeated value used to build constant tensor. + float value; + // Expected transform result: FP16+Convert when true, keep FP32 when false. + bool expect_compressed; + // Stable, readable gtest suffix. + std::string test_name; +}; + +class CompressConstantsRelativeThresholdTest : public TransformationTestsF, + public testing::WithParamInterface { +public: + static std::string get_test_name(const testing::TestParamInfo& obj) { + return obj.param.test_name; + } +}; + +TEST_P(CompressConstantsRelativeThresholdTest, HandlesRelativeThresholdByConstantShape) { + const auto& param = GetParam(); + const auto values = std::vector(shape_size(param.constant_shape), param.value); + { - auto input = std::make_shared(ov::element::f32, ov::Shape{1, 3, 4}); - auto scale = v0::Constant::create(ov::element::f32, ov::Shape{1}, {2.7725887f}); + auto input = std::make_shared(ov::element::f32, param.input_shape); + auto scale = v0::Constant::create(ov::element::f32, param.constant_shape, values); auto mul = std::make_shared(input, scale); model = std::make_shared(ov::OutputVector{mul}, ov::ParameterVector{input}); @@ -622,15 +641,55 @@ TEST_F(TransformationTestsF, CompressConstants_skip_scalar_with_high_f16_error) } { - auto input = std::make_shared(ov::element::f32, ov::Shape{1, 3, 4}); - // Constant stays in FP32 — FP16 relative error exceeds threshold - auto scale = v0::Constant::create(ov::element::f32, ov::Shape{1}, {2.7725887f}); - auto mul = std::make_shared(input, scale); + auto input = std::make_shared(ov::element::f32, param.input_shape); + auto element_type = param.expect_compressed ? ov::element::f16 : ov::element::f32; + auto scale = v0::Constant::create(element_type, param.constant_shape, values); + std::shared_ptr scale_node = scale; + if (param.expect_compressed) { + scale_node = std::make_shared(scale, ov::element::f32); + } + auto mul = std::make_shared(input, scale_node); model_ref = std::make_shared(ov::OutputVector{mul}, ov::ParameterVector{input}); } comparator.enable(FunctionsComparator::CmpValues::CONST_VALUES); } +INSTANTIATE_TEST_SUITE_P( + CompressConstantsRelativeThreshold, + CompressConstantsRelativeThresholdTest, + testing::Values( + // Scalar constant with high FP16 relative rounding error (e.g., log(16) = 2.7725887, + // rel_error = 0.031%) should NOT be compressed to FP16. + RelativeThresholdTestParams{ov::Shape{1, 3, 4}, ov::Shape{1}, 2.7725887f, false, "ScalarRejectsTightThreshold"}, + // Negative counterpart for scalar rejection. + RelativeThresholdTestParams{ov::Shape{1, 3, 4}, + ov::Shape{1}, + -2.7725887f, + false, + "ScalarNegativeRejectsTightThreshold"}, + // Non-scalar uses looser tensor relative threshold and should be compressed for this value. + RelativeThresholdTestParams{ov::Shape{1, 4}, ov::Shape{4}, 2.7725887f, true, "TensorAcceptsLooserThreshold"}, + // Negative counterpart for tensor acceptance. + RelativeThresholdTestParams{ov::Shape{1, 4}, + ov::Shape{4}, + -2.7725887f, + true, + "TensorNegativeAcceptsLooserThreshold"}, + // Non-scalar with high per-element relative error should stay FP32. + // The keep-threshold comparison is inclusive (>= f16_compression_keep_threshold). + RelativeThresholdTestParams{ov::Shape{1, 4}, + ov::Shape{4}, + 1.0e-5f, + false, + "TensorRejectsWhenAboveTensorThreshold"}, + // Negative counterpart for tensor rejection. + RelativeThresholdTestParams{ov::Shape{1, 4}, + ov::Shape{4}, + -1.0e-5f, + false, + "TensorNegativeRejectsWhenAboveTensorThreshold"}), + CompressConstantsRelativeThresholdTest::get_test_name); + // Scalar constant exactly representable in FP16 (e.g., 8.0) should be compressed normally. TEST_F(TransformationTestsF, CompressConstants_compress_scalar_exact_f16) { // Model: Parameter -> Multiply(input, Const(8.0)) -> Result @@ -656,16 +715,13 @@ TEST_F(TransformationTestsF, CompressConstants_compress_scalar_exact_f16) { comparator.enable(FunctionsComparator::CmpValues::CONST_VALUES); } -// Non-scalar constants should be compressed even if individual elements have high FP16 -// rounding error. The scalar error check only applies to numel == 1 constants. -TEST_F(TransformationTestsF, CompressConstants_compress_non_scalar_with_high_error) { - // Model: Parameter -> Multiply(input, Const({2.7725887, 2.7725887, 2.7725887, 2.7725887})) -> Result - // Non-scalar (numel=4) with high per-element error -> still compressed. +// Non-scalar constant with high absolute FP16 error (e.g. RoPE frequency table) +// should NOT be compressed — value 5002.0 has FP16 abs error = 2.0 (ULP=4 in [4096,8192) range). +TEST_F(TransformationTestsF, CompressConstants_skip_non_scalar_with_high_abs_error) { { auto input = std::make_shared(ov::element::f32, ov::Shape{1, 4}); - auto scale = - v0::Constant::create(ov::element::f32, ov::Shape{4}, {2.7725887f, 2.7725887f, 2.7725887f, 2.7725887f}); - auto mul = std::make_shared(input, scale); + auto freqs = v0::Constant::create(ov::element::f32, ov::Shape{4}, {1.0f, 100.0f, 500.0f, 5002.0f}); + auto mul = std::make_shared(input, freqs); model = std::make_shared(ov::OutputVector{mul}, ov::ParameterVector{input}); manager.register_pass(); @@ -674,10 +730,52 @@ TEST_F(TransformationTestsF, CompressConstants_compress_non_scalar_with_high_err { auto input = std::make_shared(ov::element::f32, ov::Shape{1, 4}); - // Non-scalar constant compressed to FP16 + Convert (error check is scalar-only) - auto scale = - v0::Constant::create(ov::element::f16, ov::Shape{4}, {2.7725887f, 2.7725887f, 2.7725887f, 2.7725887f}); - auto convert = std::make_shared(scale, ov::element::f32); + // Stays FP32: 5002.0 rounds to 5000.0 in FP16, abs error = 2.0 > threshold 1.0 + auto freqs = v0::Constant::create(ov::element::f32, ov::Shape{4}, {1.0f, 100.0f, 500.0f, 5002.0f}); + auto mul = std::make_shared(input, freqs); + model_ref = std::make_shared(ov::OutputVector{mul}, ov::ParameterVector{input}); + } + comparator.enable(FunctionsComparator::CmpValues::CONST_VALUES); +} + +// f64 constant with high absolute FP16 error should NOT be compressed (same logic as f32 path). +TEST_F(TransformationTestsF, CompressConstants_skip_f64_non_scalar_with_high_abs_error) { + { + auto input = std::make_shared(ov::element::f64, ov::Shape{1, 4}); + auto freqs = v0::Constant::create(ov::element::f64, ov::Shape{4}, {1.0, 100.0, 500.0, 5002.0}); + auto mul = std::make_shared(input, freqs); + model = std::make_shared(ov::OutputVector{mul}, ov::ParameterVector{input}); + + manager.register_pass(); + manager.register_pass(); + } + + { + auto input = std::make_shared(ov::element::f64, ov::Shape{1, 4}); + // Stays FP64: 5002.0 rounds to 5000.0 in FP16, abs error = 2.0 > threshold 1.0 + auto freqs = v0::Constant::create(ov::element::f64, ov::Shape{4}, {1.0, 100.0, 500.0, 5002.0}); + auto mul = std::make_shared(input, freqs); + model_ref = std::make_shared(ov::OutputVector{mul}, ov::ParameterVector{input}); + } + comparator.enable(FunctionsComparator::CmpValues::CONST_VALUES); +} + +// Non-scalar constant with low absolute FP16 error (normal weights) should be compressed. +TEST_F(TransformationTestsF, CompressConstants_compress_non_scalar_with_low_abs_error) { + { + auto input = std::make_shared(ov::element::f32, ov::Shape{1, 4}); + auto weights = v0::Constant::create(ov::element::f32, ov::Shape{4}, {0.5f, 1.23f, -3.14f, 7.77f}); + auto mul = std::make_shared(input, weights); + model = std::make_shared(ov::OutputVector{mul}, ov::ParameterVector{input}); + + manager.register_pass(); + manager.register_pass(); + } + + { + auto input = std::make_shared(ov::element::f32, ov::Shape{1, 4}); + auto weights = v0::Constant::create(ov::element::f16, ov::Shape{4}, {0.5f, 1.23f, -3.14f, 7.77f}); + auto convert = std::make_shared(weights, ov::element::f32); auto mul = std::make_shared(input, convert); model_ref = std::make_shared(ov::OutputVector{mul}, ov::ParameterVector{input}); } diff --git a/src/common/transformations/tests/common_optimizations/convert_gather_matmuls_moe_block_to_moe_op.cpp b/src/common/transformations/tests/common_optimizations/convert_gather_matmuls_moe_block_to_moe_op.cpp index 51232e90abd7..f2a47fc97271 100644 --- a/src/common/transformations/tests/common_optimizations/convert_gather_matmuls_moe_block_to_moe_op.cpp +++ b/src/common/transformations/tests/common_optimizations/convert_gather_matmuls_moe_block_to_moe_op.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -278,7 +279,8 @@ inline std::shared_ptr build_3gemm_moe_pattern_model() { // Post-BGM model builders (3 BGMs + compact routing + ReduceSum + Reshape) // ============================================================================ -inline std::shared_ptr build_3gemm_bgm_model() { +inline std::shared_ptr build_3gemm_bgm_model( + ov::op::internal::MOE::Activation_type activation_type = ov::op::internal::MOE::Activation_type::SWIGLU) { using namespace ov; const size_t batch = 2; @@ -325,9 +327,16 @@ inline std::shared_ptr build_3gemm_bgm_model() { // 3 BGMs auto bgm_gate = std::make_shared(unsqueeze, gate_w, topk_indices); - auto swish = std::make_shared(bgm_gate); + std::shared_ptr gate_act; + if (activation_type == ov::op::internal::MOE::Activation_type::GEGLU_TANH) { + gate_act = std::make_shared(bgm_gate, ov::op::GeluApproximationMode::TANH); + } else if (activation_type == ov::op::internal::MOE::Activation_type::GEGLU_ERF) { + gate_act = std::make_shared(bgm_gate, ov::op::GeluApproximationMode::ERF); + } else { + gate_act = std::make_shared(bgm_gate); + } auto bgm_up = std::make_shared(unsqueeze, up_w, topk_indices); - auto swiglu = std::make_shared(swish, bgm_up); + auto swiglu = std::make_shared(gate_act, bgm_up); auto bgm_down = std::make_shared(swiglu, down_w, topk_indices); // Compact routing: chosen_experts → Transpose({1,0}) → Unsqueeze(-1) @@ -355,7 +364,8 @@ inline std::shared_ptr build_3gemm_bgm_model() { return std::make_shared(ov::OutputVector{end_reshape}, ov::ParameterVector{input}); } -inline std::shared_ptr build_3gemm_bgm_to_moe_reference_model() { +inline std::shared_ptr build_3gemm_bgm_to_moe_reference_model( + ov::op::internal::MOE::Activation_type activation_type = ov::op::internal::MOE::Activation_type::SWIGLU) { using namespace ov; const size_t batch = 2; @@ -385,14 +395,7 @@ inline std::shared_ptr build_3gemm_bgm_to_moe_reference_model() { op::v11::TopK::SortType::SORT_VALUES, element::i64); auto topk_indices = router_topk->output(1); - auto chosen_experts = router_topk->output(0); - - // Compact routing (stays as-is, becomes MOE input 1) - auto router_transpose = std::make_shared( - chosen_experts, - op::v0::Constant::create(element::i64, Shape{2}, std::vector{1, 0})); - auto router_unsqueeze = - std::make_shared(router_transpose, op::v0::Constant::create(element::i32, Shape{}, {-1})); + auto routing = router_topk->output(0); // Weights auto gate_w = @@ -403,9 +406,10 @@ inline std::shared_ptr build_3gemm_bgm_to_moe_reference_model() { op::v0::Constant::create(element::f32, Shape{number_of_experts, hidden_size, intermediate_size}, {1.0f}); // MOE op with compact routing - ov::OutputVector moe_inputs = {input, router_unsqueeze, topk_indices, gate_w, up_w, down_w}; + ov::OutputVector moe_inputs = {input, routing, topk_indices, gate_w, up_w, down_w}; ov::op::internal::MOE::Config config; config.expert_type = ov::op::internal::MOE::Expert_type::GEMM3_SWIGLU; + config.activation_type = activation_type; auto moe = std::make_shared(moe_inputs, config); return std::make_shared(ov::OutputVector{moe}, ov::ParameterVector{input}); @@ -558,12 +562,8 @@ inline std::shared_ptr build_2gemm_bgm_to_moe_reference_model() { auto topk_indices = router_topk->output(1); auto chosen_experts = router_topk->output(0); - // Compact routing (becomes MOE input 1) - auto router_transpose = std::make_shared( - chosen_experts, - op::v0::Constant::create(element::i64, Shape{2}, std::vector{1, 0})); - auto router_unsqueeze = - std::make_shared(router_transpose, op::v0::Constant::create(element::i32, Shape{}, {-1})); + // Convert2GatherMatmulMoeBlockToMoeOp bypasses Transpose+Unsqueeze (tokens-major). + auto routing = chosen_experts; // Weights auto gate_up_w = op::v0::Constant::create(element::f32, @@ -575,7 +575,7 @@ inline std::shared_ptr build_2gemm_bgm_to_moe_reference_model() { op::v0::Constant::create(element::f32, Shape{number_of_experts, hidden_size, intermediate_size}, {1.0f}); auto down_bias = op::v0::Constant::create(element::f32, Shape{number_of_experts, 1, hidden_size}, {1.0f}); - ov::OutputVector moe_inputs = {input, router_unsqueeze, topk_indices, gate_up_w, gate_up_bias, down_w, down_bias}; + ov::OutputVector moe_inputs = {input, routing, topk_indices, gate_up_w, gate_up_bias, down_w, down_bias}; ov::op::internal::MOE::Config config; config.expert_type = ov::op::internal::MOE::Expert_type::GEMM2_BIAS_SWIGLU_CLAMP; @@ -586,6 +586,156 @@ inline std::shared_ptr build_2gemm_bgm_to_moe_reference_model() { return std::make_shared(ov::OutputVector{moe}, ov::ParameterVector{input}); } +// ============================================================================ +// Post-BGM model builders (3 BGMs + compact routing) — Multiply instead of Reshape +// before Unsqueeze (gemma4 pattern: layernorm Multiply feeds expert path directly) +// ============================================================================ + +inline std::shared_ptr build_3gemm_bgm_model_multiply_input( + ov::op::internal::MOE::Activation_type activation_type = ov::op::internal::MOE::Activation_type::SWIGLU) { + using namespace ov; + + const size_t batch = 2; + const Dimension in_dim = Dimension::dynamic(); + const size_t hidden_size = 2048; + const size_t intermediate_size = 4096; + const size_t number_of_experts = 3; + const size_t topk = 2; + + auto input = std::make_shared(element::f32, PartialShape{batch, in_dim, hidden_size}); + + // Simulate layernorm output: Multiply(input, scale) — no Reshape + auto norm_scale = op::v0::Constant::create(element::f32, Shape{1, 1, hidden_size}, {1.0f}); + auto layernorm_mul = std::make_shared(input, norm_scale); + + // Reshape to [batch*seq, hidden] for router path + auto experts_reshape = std::make_shared( + layernorm_mul, + op::v0::Constant::create(element::i64, Shape{2}, std::vector{-1, static_cast(hidden_size)}), + false); + + // Unsqueeze feeds from Multiply (via Reshape) — but the pattern's optional + // means hidden_states_m captures the Multiply output (layernorm_mul). + auto unsqueeze = + std::make_shared(experts_reshape, op::v0::Constant::create(element::i32, Shape{}, {0})); + + // Router subgraph + auto router_matmul = std::make_shared( + experts_reshape, + op::v0::Constant::create(element::f32, Shape{number_of_experts, hidden_size}, {1.0f}), + false, + true); + auto router_topk = std::make_shared(router_matmul, + op::v0::Constant::create(element::i64, Shape{}, {topk}), + -1, + op::v11::TopK::Mode::MAX, + op::v11::TopK::SortType::SORT_VALUES, + element::i64); + auto topk_indices = router_topk->output(1); + auto chosen_experts = router_topk->output(0); + + // Weights + auto gate_w = + op::v0::Constant::create(element::f32, Shape{number_of_experts, intermediate_size, hidden_size}, {1.0f}); + auto up_w = + op::v0::Constant::create(element::f32, Shape{number_of_experts, intermediate_size, hidden_size}, {1.0f}); + auto down_w = + op::v0::Constant::create(element::f32, Shape{number_of_experts, hidden_size, intermediate_size}, {1.0f}); + + // 3 BGMs + auto bgm_gate = std::make_shared(unsqueeze, gate_w, topk_indices); + std::shared_ptr gate_act; + if (activation_type == ov::op::internal::MOE::Activation_type::GEGLU_TANH) { + gate_act = std::make_shared(bgm_gate, ov::op::GeluApproximationMode::TANH); + } else if (activation_type == ov::op::internal::MOE::Activation_type::GEGLU_ERF) { + gate_act = std::make_shared(bgm_gate, ov::op::GeluApproximationMode::ERF); + } else { + gate_act = std::make_shared(bgm_gate); + } + auto bgm_up = std::make_shared(unsqueeze, up_w, topk_indices); + auto swiglu = std::make_shared(gate_act, bgm_up); + auto bgm_down = std::make_shared(swiglu, down_w, topk_indices); + + // Compact routing + auto router_transpose = std::make_shared( + chosen_experts, + op::v0::Constant::create(element::i64, Shape{2}, std::vector{1, 0})); + auto router_unsqueeze = + std::make_shared(router_transpose, op::v0::Constant::create(element::i32, Shape{}, {-1})); + + // Final: Multiply → ReduceSum → Reshape + auto final_mul = std::make_shared(bgm_down, router_unsqueeze); + auto reduce_sum = + std::make_shared(final_mul, + op::v0::Constant::create(element::i64, Shape{1}, std::vector{0}), + false); + + auto end_reshape = std::make_shared( + reduce_sum, + op::v0::Constant::create( + element::i64, + Shape{3}, + std::vector{static_cast(batch), -1, static_cast(hidden_size)}), + true); + + return std::make_shared(ov::OutputVector{end_reshape}, ov::ParameterVector{input}); +} + +inline std::shared_ptr build_3gemm_bgm_to_moe_reference_model_multiply_input( + ov::op::internal::MOE::Activation_type activation_type = ov::op::internal::MOE::Activation_type::SWIGLU) { + using namespace ov; + + const size_t batch = 2; + const Dimension in_dim = Dimension::dynamic(); + const size_t hidden_size = 2048; + const size_t intermediate_size = 4096; + const size_t number_of_experts = 3; + const size_t topk = 2; + + auto input = std::make_shared(element::f32, PartialShape{batch, in_dim, hidden_size}); + + // Layernorm Multiply — its OUTPUT is the correct hidden_states for the MOE op + auto norm_scale = op::v0::Constant::create(element::f32, Shape{1, 1, hidden_size}, {1.0f}); + auto layernorm_mul = std::make_shared(input, norm_scale); + + // Router subgraph (not fused, remains in the graph) + auto experts_reshape = std::make_shared( + layernorm_mul, + op::v0::Constant::create(element::i64, Shape{2}, std::vector{-1, static_cast(hidden_size)}), + false); + + auto router_matmul = std::make_shared( + experts_reshape, + op::v0::Constant::create(element::f32, Shape{number_of_experts, hidden_size}, {1.0f}), + false, + true); + auto router_topk = std::make_shared(router_matmul, + op::v0::Constant::create(element::i64, Shape{}, {topk}), + -1, + op::v11::TopK::Mode::MAX, + op::v11::TopK::SortType::SORT_VALUES, + element::i64); + auto topk_indices = router_topk->output(1); + auto routing = router_topk->output(0); + + // Weights + auto gate_w = + op::v0::Constant::create(element::f32, Shape{number_of_experts, intermediate_size, hidden_size}, {1.0f}); + auto up_w = + op::v0::Constant::create(element::f32, Shape{number_of_experts, intermediate_size, hidden_size}, {1.0f}); + auto down_w = + op::v0::Constant::create(element::f32, Shape{number_of_experts, hidden_size, intermediate_size}, {1.0f}); + + // MOE op — hidden_states input is the layernorm Multiply output (NOT the Parameter) + ov::OutputVector moe_inputs = {layernorm_mul, routing, topk_indices, gate_w, up_w, down_w}; + ov::op::internal::MOE::Config config; + config.expert_type = ov::op::internal::MOE::Expert_type::GEMM3_SWIGLU; + config.activation_type = activation_type; + auto moe = std::make_shared(moe_inputs, config); + + return std::make_shared(ov::OutputVector{moe}, ov::ParameterVector{input}); +} + // ============================================================================ // Tests for BGM→MOE passes (Convert3GatherMatmulMoeBlockToMoeOp, Convert2GatherMatmulMoeBlockToMoeOp) // ============================================================================ @@ -596,6 +746,33 @@ TEST_F(TransformationTestsF, Convert3GatherMatmulMoeBlockToMoeOp_basic) { model_ref = build_3gemm_bgm_to_moe_reference_model(); } +TEST_F(TransformationTestsF, Convert3GatherMatmulMoeBlockToMoeOp_gelu_tanh) { + using AT = ov::op::internal::MOE::Activation_type; + model = build_3gemm_bgm_model(AT::GEGLU_TANH); + manager.register_pass(); + model_ref = build_3gemm_bgm_to_moe_reference_model(AT::GEGLU_TANH); +} + +TEST_F(TransformationTestsF, Convert3GatherMatmulMoeBlockToMoeOp_gelu_erf) { + using AT = ov::op::internal::MOE::Activation_type; + model = build_3gemm_bgm_model(AT::GEGLU_ERF); + manager.register_pass(); + model_ref = build_3gemm_bgm_to_moe_reference_model(AT::GEGLU_ERF); +} + +TEST_F(TransformationTestsF, Convert3GatherMatmulMoeBlockToMoeOp_multiply_input) { + model = build_3gemm_bgm_model_multiply_input(); + manager.register_pass(); + model_ref = build_3gemm_bgm_to_moe_reference_model_multiply_input(); +} + +TEST_F(TransformationTestsF, Convert3GatherMatmulMoeBlockToMoeOp_multiply_input_gelu_tanh) { + using AT = ov::op::internal::MOE::Activation_type; + model = build_3gemm_bgm_model_multiply_input(AT::GEGLU_TANH); + manager.register_pass(); + model_ref = build_3gemm_bgm_to_moe_reference_model_multiply_input(AT::GEGLU_TANH); +} + TEST_F(TransformationTestsF, Convert2GatherMatmulMoeBlockToMoeOp_basic) { model = build_2gemm_bgm_model(); manager.register_pass(); diff --git a/src/common/transformations/tests/common_optimizations/convert_legacy_precision_attribute_test.cpp b/src/common/transformations/tests/common_optimizations/convert_legacy_precision_attribute_test.cpp new file mode 100644 index 000000000000..d8b2af7eec2f --- /dev/null +++ b/src/common/transformations/tests/common_optimizations/convert_legacy_precision_attribute_test.cpp @@ -0,0 +1,114 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "transformations/fp16_compression/convert_legacy_precision_attribute.hpp" + +#include + +#include "common_test_utils/ov_test_utils.hpp" +#include "openvino/core/model.hpp" +#include "openvino/op/add.hpp" +#include "openvino/op/constant.hpp" +#include "openvino/op/if.hpp" +#include "openvino/op/matmul.hpp" +#include "openvino/op/parameter.hpp" +#include "openvino/op/result.hpp" +#include "openvino/pass/manager.hpp" +#include "transformations/rt_info/disable_precision_conversion.hpp" + +namespace ov::test { + +TEST(ConvertLegacyPrecisionAttributeTest, basic_migration) { + // Create model with legacy attribute on a node + auto data_1 = std::make_shared(element::f32, Shape{1, 10}); + auto data_2 = std::make_shared(element::f32, Shape{10, 1}); + auto matmul = std::make_shared(data_1, data_2); + OPENVINO_SUPPRESS_DEPRECATED_START + matmul->get_rt_info()[DisableFP16Compression::get_type_info_static()] = DisableFP16Compression{}; + OPENVINO_SUPPRESS_DEPRECATED_END + auto result = std::make_shared(matmul); + auto model = std::make_shared(ResultVector{result}, ParameterVector{data_1, data_2}); + + // Verify legacy attribute is present before the pass + OPENVINO_SUPPRESS_DEPRECATED_START + ASSERT_TRUE(matmul->get_rt_info().count(DisableFP16Compression::get_type_info_static())); + OPENVINO_SUPPRESS_DEPRECATED_END + + pass::Manager m; + m.register_pass(); + bool res = m.run_passes(model); + + ASSERT_TRUE(res); + + const auto& rt_info = matmul->get_rt_info(); + + OPENVINO_SUPPRESS_DEPRECATED_START + ASSERT_FALSE(rt_info.count(DisableFP16Compression::get_type_info_static())); + OPENVINO_SUPPRESS_DEPRECATED_END + ASSERT_TRUE(rt_info.count(DisablePrecisionConversion::get_type_info_static())); + + // Verify the map content: {dynamic -> {f16}} + const auto& attr = rt_info.at(DisablePrecisionConversion::get_type_info_static()).as(); + DisabledPrecisionMap expected = {{element::dynamic, {element::f16}}}; + ASSERT_EQ(attr.m_disabled_precisions, expected); + + ASSERT_TRUE(is_conversion_disabled(matmul, element::f16)); +} + +TEST(ConvertLegacyPrecisionAttributeTest, no_legacy_attribute) { + // Node without any attribute — pass should not modify it + auto data_1 = std::make_shared(element::f32, Shape{1, 10}); + auto data_2 = std::make_shared(element::f32, Shape{10, 1}); + auto matmul = std::make_shared(data_1, data_2); + auto result = std::make_shared(matmul); + auto model = std::make_shared(ResultVector{result}, ParameterVector{data_1, data_2}); + + pass::Manager m; + m.register_pass(); + bool res = m.run_passes(model); + + ASSERT_FALSE(res); + + const auto& rt_info = matmul->get_rt_info(); + OPENVINO_SUPPRESS_DEPRECATED_START + ASSERT_FALSE(rt_info.count(DisableFP16Compression::get_type_info_static())); + OPENVINO_SUPPRESS_DEPRECATED_END + ASSERT_FALSE(rt_info.count(DisablePrecisionConversion::get_type_info_static())); + ASSERT_FALSE(is_conversion_disabled(matmul, element::f16)); +} + +TEST(ConvertLegacyPrecisionAttributeTest, multiple_nodes) { + // Multiple nodes with legacy attribute + auto data_1 = std::make_shared(element::f32, Shape{1, 10}); + auto data_2 = std::make_shared(element::f32, Shape{10, 1}); + auto add = std::make_shared(data_1, data_1); + auto matmul = std::make_shared(add, data_2); + OPENVINO_SUPPRESS_DEPRECATED_START + add->get_rt_info()[DisableFP16Compression::get_type_info_static()] = DisableFP16Compression{}; + matmul->get_rt_info()[DisableFP16Compression::get_type_info_static()] = DisableFP16Compression{}; + OPENVINO_SUPPRESS_DEPRECATED_END + auto result = std::make_shared(matmul); + auto model = std::make_shared(ResultVector{result}, ParameterVector{data_1, data_2}); + + pass::Manager m; + m.register_pass(); + bool res = m.run_passes(model); + + ASSERT_TRUE(res); + + // Both nodes should be migrated + OPENVINO_SUPPRESS_DEPRECATED_START + ASSERT_FALSE(add->get_rt_info().count(DisableFP16Compression::get_type_info_static())); + OPENVINO_SUPPRESS_DEPRECATED_END + ASSERT_TRUE(add->get_rt_info().count(DisablePrecisionConversion::get_type_info_static())); + ASSERT_TRUE(is_conversion_disabled(add, element::f16)); + + OPENVINO_SUPPRESS_DEPRECATED_START + ASSERT_FALSE(matmul->get_rt_info().count(DisableFP16Compression::get_type_info_static())); + OPENVINO_SUPPRESS_DEPRECATED_END + ASSERT_TRUE(matmul->get_rt_info().count(DisablePrecisionConversion::get_type_info_static())); + ASSERT_TRUE(is_conversion_disabled(matmul, element::f16)); +} + +} // namespace ov::test \ No newline at end of file diff --git a/src/common/transformations/tests/common_optimizations/convert_pagedattn_inputs.cpp b/src/common/transformations/tests/common_optimizations/convert_pagedattn_inputs.cpp index 0aa648aab3f4..84ec988d5e01 100644 --- a/src/common/transformations/tests/common_optimizations/convert_pagedattn_inputs.cpp +++ b/src/common/transformations/tests/common_optimizations/convert_pagedattn_inputs.cpp @@ -2,14 +2,20 @@ // SPDX-License-Identifier: Apache-2.0 // -#include "transformations/common_optimizations/convert_pagedattn_inputs.hpp" +#include "transformations/paged_attention/convert_pagedattn_inputs.hpp" #include #include "common_test_utils/ov_test_utils.hpp" #include "openvino/core/rt_info.hpp" #include "openvino/op/paged_attention.hpp" +#include "openvino/op/paged_causal_conv1d.hpp" +#include "openvino/op/paged_gated_delta_net.hpp" +#include "openvino/pass/constant_folding.hpp" +#include "openvino/pass/manager.hpp" #include "openvino/runtime/properties.hpp" +#include "transformations/convert_precision.hpp" +#include "transformations/rt_info/keep_const_precision.hpp" #include "transformations/utils/gen_pattern.hpp" using namespace ov::test; @@ -390,4 +396,141 @@ INSTANTIATE_TEST_SUITE_P(smoke_ConvertPagedAttnInputsTest, ::testing::Values(true, false)), ConvertPagedAttnInputsTest::getTestCaseName); +class ConvertPagedAttnInputsStateTableTest : public testing::Test {}; + +// The tests bellow replicate the behavior of ConvertPagedAttnInputs pass in a GPU pipeline. +// Testing it in isolation does not really make sense because in a real pipeline the model is +// different and it's important to replicate the same conditions that a model is going through. +// TODO: consider moving ConvertPagedAttnInputs above in the pipeline so we avoid taking into account +// the ConvertPrecision pass that much. +TEST_F(ConvertPagedAttnInputsStateTableTest, ConvertPagedCausalConv1DInputsPrecision) { + auto input_embeds = std::make_shared(element::f32, PartialShape{-1, 256}); + auto conv_state_table = std::make_shared(element::dynamic, PartialShape{-1, 256, 4}); + conv_state_table->set_friendly_name("conv_state_table.0"); + ov::enable_keep_const_precision(conv_state_table); + auto conv_weight = std::make_shared(element::f32, PartialShape{256, 1, 4}); + auto conv_bias = std::make_shared(element::f32, PartialShape{256}); + auto subsequence_begins = std::make_shared(element::i32, PartialShape{-1}); + auto block_indices = std::make_shared(element::i32, PartialShape{-1}); + auto block_indices_begins = std::make_shared(element::i32, PartialShape{-1}); + auto past_lens = std::make_shared(element::i32, PartialShape{-1}); + auto cache_interval = std::make_shared(element::i32, PartialShape{-1}); + + auto paged_conv = std::make_shared(input_embeds, + conv_state_table, + conv_weight, + conv_bias, + subsequence_begins, + block_indices, + block_indices_begins, + past_lens, + cache_interval); + auto local_model = std::make_shared(OutputVector{paged_conv}, + ParameterVector{input_embeds, + conv_state_table, + conv_weight, + conv_bias, + subsequence_begins, + block_indices, + block_indices_begins, + past_lens, + cache_interval}); + + // Replicate GPU pipeline: clear keep_const_precision on all nodes + for (auto& node : local_model->get_ops()) { + ov::disable_keep_const_precision(node); + } + + ov::pass::ConvertPagedAttnInputs::KVCacheConfig cacheConfig; + cacheConfig.inferencePrecision = ov::element::f16; + + ov::pass::Manager local_manager; + + precisions_map fp_convert_precision_map = {{ov::element::f64, ov::element::f32}, + {ov::element::f32, ov::element::f16}}; + type_to_fuse_map empty_fuse_map = {}; + local_manager.register_pass(fp_convert_precision_map, + empty_fuse_map, + /*keep_precision_sensitive_in_fp32*/ true, + /*convert_input_output_precision*/ false, + /*store_original_precision_as_rt_attribute*/ true); + + auto update_paged_attention_shape_func = + [](const ov::element::Type&, const bool, const size_t, int64_t&, int64_t&) {}; + local_manager.register_pass(cacheConfig, update_paged_attention_shape_func); + local_manager.run_passes(local_model); + + // Verify no Convert node between Parameter and PagedCausalConv1D + EXPECT_TRUE(ov::is_type(paged_conv->get_input_node_shared_ptr(1))); + EXPECT_EQ(conv_state_table->get_element_type(), ov::element::f16); +} + +TEST_F(ConvertPagedAttnInputsStateTableTest, ConvertPagedGatedDeltaNetPrecision) { + auto query = std::make_shared(element::f32, PartialShape{-1, 2, 64}); + auto key = std::make_shared(element::f32, PartialShape{-1, 2, 64}); + auto value = std::make_shared(element::f32, PartialShape{-1, 2, 64}); + auto gated_delta_state_table = std::make_shared(element::dynamic, PartialShape{-1, 2, 64, 64}); + gated_delta_state_table->set_friendly_name("gated_delta_state_table.0"); + enable_keep_const_precision(gated_delta_state_table); + auto gate = std::make_shared(element::f32, PartialShape{-1, 2}); + auto beta = std::make_shared(element::f32, PartialShape{-1, 2}); + auto subsequence_begins = std::make_shared(element::i32, PartialShape{-1}); + auto block_indices = std::make_shared(element::i32, PartialShape{-1}); + auto block_indices_begins = std::make_shared(element::i32, PartialShape{-1}); + auto past_lens = std::make_shared(element::i32, PartialShape{-1}); + auto cache_interval = std::make_shared(element::i32, PartialShape{-1}); + + auto paged_gdn = std::make_shared(query, + key, + value, + gated_delta_state_table, + gate, + beta, + subsequence_begins, + block_indices, + block_indices_begins, + past_lens, + cache_interval); + auto local_model = std::make_shared(OutputVector{paged_gdn}, + ParameterVector{query, + key, + value, + gated_delta_state_table, + gate, + beta, + subsequence_begins, + block_indices, + block_indices_begins, + past_lens, + cache_interval}); + + // Replicate GPU pipeline: clear keep_const_precision on all nodes + for (auto& node : local_model->get_ops()) { + ov::disable_keep_const_precision(node); + } + + ov::pass::ConvertPagedAttnInputs::KVCacheConfig cacheConfig; + cacheConfig.inferencePrecision = ov::element::f16; + + ov::pass::Manager local_manager; + + precisions_map fp_convert_precision_map = {{ov::element::f64, ov::element::f32}, + {ov::element::f32, ov::element::f16}}; + type_to_fuse_map empty_fuse_map = {}; + local_manager.register_pass(fp_convert_precision_map, + empty_fuse_map, + /*keep_precision_sensitive_in_fp32*/ true, + /*convert_input_output_precision*/ false, + /*store_original_precision_as_rt_attribute*/ true); + + auto update_paged_attention_shape_func = + [](const ov::element::Type&, const bool, const size_t, int64_t&, int64_t&) {}; + local_manager.register_pass(cacheConfig, update_paged_attention_shape_func); + local_manager.run_passes(local_model); + + // Verify no Convert node between Parameter and PagedGatedDeltaNet + EXPECT_TRUE(ov::is_type(paged_gdn->get_input_node_shared_ptr(3))); + EXPECT_EQ(gated_delta_state_table->get_element_type(), ov::element::f16); +} + } // namespace diff --git a/src/common/transformations/tests/common_optimizations/convert_tiled_moe_block_to_gather_matmuls_test.cpp b/src/common/transformations/tests/common_optimizations/convert_tiled_moe_block_to_gather_matmuls_test.cpp index 534910e8b309..6565590cc603 100644 --- a/src/common/transformations/tests/common_optimizations/convert_tiled_moe_block_to_gather_matmuls_test.cpp +++ b/src/common/transformations/tests/common_optimizations/convert_tiled_moe_block_to_gather_matmuls_test.cpp @@ -19,6 +19,7 @@ #include "openvino/op/concat.hpp" #include "openvino/op/constant.hpp" #include "openvino/op/divide.hpp" +#include "openvino/op/gelu.hpp" #include "openvino/op/matmul.hpp" #include "openvino/op/minimum.hpp" #include "openvino/op/multiply.hpp" @@ -57,13 +58,34 @@ inline std::ostream& operator<<(std::ostream& os, const MoEType& type) { } } -using ConvertTiledMoeBlockToGatherMatmulsParams = std::tuple; // transformation_should_be_applied +enum class GateActivationType { SWISH, GELU }; + +enum class AdditionalConsumersMode { NO, MATMULS, AFTER_MATMULS, REDUCE }; + +inline std::ostream& operator<<(std::ostream& os, const AdditionalConsumersMode& mode) { + switch (mode) { + case AdditionalConsumersMode::NO: + return os << "NO"; + case AdditionalConsumersMode::MATMULS: + return os << "MATMULS"; + case AdditionalConsumersMode::AFTER_MATMULS: + return os << "AFTER_MATMULS"; + case AdditionalConsumersMode::REDUCE: + return os << "REDUCE"; + default: + OPENVINO_THROW("Unsupported AdditionalConsumersMode"); + } +} + +using ConvertTiledMoeBlockToGatherMatmulsParams = std::tuple; // transformation_should_be_applied inline std::shared_ptr build_matmul_weights(const ov::Shape& weights_shape, const ov::element::Type& weights_precision, @@ -84,7 +106,7 @@ inline std::shared_ptr initMoE2GeMMSubgraph(bool use_scatter_v12, bool use_broadcast_v3, bool skip_unsqueeze, bool matmul_transpose_b, - bool additional_node_consumers) { + AdditionalConsumersMode additional_consumers_mode) { // Fixed values that don't affect pass behavior const auto expert_alpha = 1.625f; const auto expert_beta = 7.0f; @@ -280,11 +302,13 @@ inline std::shared_ptr initMoE2GeMMSubgraph(bool use_scatter_v12, ov::ParameterVector params = {input}; ov::ResultVector results = {std::make_shared(reduce_sum)}; - if (additional_node_consumers) { - results.push_back(std::make_shared(after_tile_reshape)); - results.push_back(std::make_shared(gate_up_add)); - results.push_back(std::make_shared(multiply2)); - results.push_back(std::make_shared(down_proj_add)); + if (additional_consumers_mode == AdditionalConsumersMode::MATMULS) { + results.push_back(std::make_shared(gate_up_matmul)); + results.push_back(std::make_shared(down_proj_matmul)); + } else if (additional_consumers_mode == AdditionalConsumersMode::AFTER_MATMULS) { + results.push_back(std::make_shared(add1)); + } else if (additional_consumers_mode == AdditionalConsumersMode::REDUCE) { + results.push_back(std::make_shared(reduce_sum)); } return std::make_shared(results, params); @@ -442,7 +466,10 @@ inline std::shared_ptr initMoE3GeMMSubgraph(bool use_scatter_v12, bool use_broadcast_v3, bool skip_unsqueeze, bool matmul_transpose_b, - bool additional_node_consumers) { + AdditionalConsumersMode additional_consumers_mode, + GateActivationType gate_activation = GateActivationType::SWISH, + bool norm_before_tile = false, + bool use_normalized_router = true) { // Fixed values that don't affect pass behavior const ov::PartialShape input_shape = {-1, -1, 256}; const size_t topk = 4; @@ -462,8 +489,15 @@ inline std::shared_ptr initMoE3GeMMSubgraph(bool use_scatter_v12, std::vector{-1, static_cast(hidden_size)}), // -1 flattens batch*seq_len false); + // Optional normalization scaling (e.g. RMS-norm) before tile + std::shared_ptr tile_input = experts_reshape; + if (norm_before_tile) { + auto scale_const = ov::op::v0::Constant::create(data_precision, ov::Shape{}, {0.7071f}); + tile_input = std::make_shared(experts_reshape, scale_const); + } + auto tile = std::make_shared( - experts_reshape, + tile_input, ov::op::v0::Constant::create(ov::element::i64, ov::Shape{2}, std::vector{static_cast(number_of_experts), 1})); @@ -488,7 +522,13 @@ inline std::shared_ptr initMoE3GeMMSubgraph(bool use_scatter_v12, gate_matmul->set_friendly_name("GateMatMul"); - auto swish = std::make_shared(gate_matmul); + // Gate activation: Swish (SwiGLU) or Gelu (GeGLU) + std::shared_ptr gate_act; + if (gate_activation == GateActivationType::GELU) { + gate_act = std::make_shared(gate_matmul); + } else { + gate_act = std::make_shared(gate_matmul); + } auto up_weights = build_matmul_weights(ov::Shape{number_of_experts, hidden_size, intermediate_size}, weights_precision, seed++, @@ -498,7 +538,7 @@ inline std::shared_ptr initMoE3GeMMSubgraph(bool use_scatter_v12, up_matmul->set_friendly_name("UpMatMul"); - auto swiglu = std::make_shared(swish, up_matmul); + auto swiglu = std::make_shared(gate_act, up_matmul); auto down_weights = build_matmul_weights(ov::Shape{number_of_experts, intermediate_size, hidden_size}, weights_precision, @@ -515,29 +555,44 @@ inline std::shared_ptr initMoE3GeMMSubgraph(bool use_scatter_v12, auto reshape_2nd_consumer_router_matmul = std::make_shared(experts_reshape, router_weights, false, matmul_transpose_b); - auto router_softmax = std::make_shared(reshape_2nd_consumer_router_matmul, 1); - - auto router_topk_values_and_indices = - std::make_shared(router_softmax, - ov::op::v0::Constant::create(ov::element::i64, ov::Shape{}, {topk}), - -1, - ov::op::v11::TopK::Mode::MAX, - ov::op::v11::TopK::SortType::SORT_VALUES, - ov::element::i64); - - auto router_topk_values_reduce = std::make_shared( - router_topk_values_and_indices->output(0), - ov::op::v0::Constant::create(ov::element::i64, ov::Shape{1}, std::vector{-1}), - true); - auto router_topk_values_normalization = - std::make_shared(router_topk_values_and_indices->output(0), router_topk_values_reduce); - auto router_topk_indices = router_topk_values_and_indices->output(1); + // Router: normalized (softmax + topk normalization) or simple (raw TopK values) + ov::Output router_topk_indices; + ov::Output chosen_experts; + if (use_normalized_router) { + auto router_softmax = std::make_shared(reshape_2nd_consumer_router_matmul, 1); + + auto router_topk_values_and_indices = + std::make_shared(router_softmax, + ov::op::v0::Constant::create(ov::element::i64, ov::Shape{}, {topk}), + -1, + ov::op::v11::TopK::Mode::MAX, + ov::op::v11::TopK::SortType::SORT_VALUES, + ov::element::i64); + + auto router_topk_values_reduce = std::make_shared( + router_topk_values_and_indices->output(0), + ov::op::v0::Constant::create(ov::element::i64, ov::Shape{1}, std::vector{-1}), + true); + auto router_topk_values_normalization = + std::make_shared(router_topk_values_and_indices->output(0), router_topk_values_reduce); + router_topk_indices = router_topk_values_and_indices->output(1); + chosen_experts = router_topk_values_normalization; + } else { + auto router_topk_values_and_indices = + std::make_shared(reshape_2nd_consumer_router_matmul, + ov::op::v0::Constant::create(ov::element::i64, ov::Shape{}, {topk}), + -1, + ov::op::v11::TopK::Mode::MAX, + ov::op::v11::TopK::SortType::SORT_VALUES, + ov::element::i64); + router_topk_indices = router_topk_values_and_indices->output(1); + chosen_experts = router_topk_values_and_indices->output(0); + } const auto number_of_experts_const = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{1}, {static_cast(number_of_experts)}); - auto zero_const = - ov::op::v0::Constant::create(router_topk_values_normalization->get_output_element_type(0), ov::Shape{1}, {0}); + auto zero_const = ov::op::v0::Constant::create(chosen_experts.get_element_type(), ov::Shape{1}, {0}); auto first_topk_dim = ov::op::util::node_to_get_shape_value_of_indices_from_shape_source(router_topk_indices, {0}); auto bcast_target_shape = std::make_shared(ov::OutputVector{first_topk_dim, number_of_experts_const}, 0); @@ -554,13 +609,13 @@ inline std::shared_ptr initMoE3GeMMSubgraph(bool use_scatter_v12, scatter_elements_update = std::make_shared( broadcast_zero, router_topk_indices, - router_topk_values_normalization, + chosen_experts, ov::op::v0::Constant::create(ov::element::i64, ov::Shape{1}, std::vector{1})); } else { scatter_elements_update = std::make_shared( broadcast_zero, router_topk_indices, - router_topk_values_normalization, + chosen_experts, ov::op::v0::Constant::create(ov::element::i64, ov::Shape{1}, std::vector{1})); } @@ -612,24 +667,35 @@ inline std::shared_ptr initMoE3GeMMSubgraph(bool use_scatter_v12, auto final_reshape = std::make_shared(reduce_sum, final_reshape_const, true); ov::ParameterVector params = {input}; - ov::ResultVector results = {std::make_shared(final_reshape)}; + ov::ResultVector results; + if (use_normalized_router) { + results.push_back(std::make_shared(final_reshape)); + } else { + results.push_back(std::make_shared(reduce_sum)); + } - if (additional_node_consumers) { - results.push_back(std::make_shared(after_tile_reshape)); + if (additional_consumers_mode == AdditionalConsumersMode::MATMULS) { results.push_back(std::make_shared(gate_matmul)); - results.push_back(std::make_shared(swish)); results.push_back(std::make_shared(up_matmul)); - results.push_back(std::make_shared(swiglu)); results.push_back(std::make_shared(down_matmul)); + } else if (additional_consumers_mode == AdditionalConsumersMode::AFTER_MATMULS) { + results.push_back(std::make_shared(gate_act)); + results.push_back(std::make_shared(swiglu)); + } else if (additional_consumers_mode == AdditionalConsumersMode::REDUCE) { + results.push_back(std::make_shared(reduce_sum)); } return std::make_shared(results, params); } -inline std::shared_ptr initMoE3GeMMSubgraphRef(bool use_scatter_v12, - bool use_broadcast_v3, - bool skip_unsqueeze, - bool matmul_transpose_b) { +inline std::shared_ptr initMoE3GeMMSubgraphRef( + bool use_scatter_v12, + bool use_broadcast_v3, + bool skip_unsqueeze, + bool matmul_transpose_b, + GateActivationType gate_activation = GateActivationType::SWISH, + bool norm_before_tile = false, + bool use_normalized_router = true) { // Fixed values that don't affect pass behavior const ov::PartialShape input_shape = {-1, -1, 256}; const size_t topk = 4; @@ -649,33 +715,58 @@ inline std::shared_ptr initMoE3GeMMSubgraphRef(bool use_scatter_v12, std::vector{-1, static_cast(hidden_size)}), false); + // The transformation captures experts_input (the Tile's direct predecessor) and unsqueezes it. + // When norm_before_tile=true, experts_input is the norm_multiply node, not experts_reshape. + std::shared_ptr experts_input = experts_reshape; + if (norm_before_tile) { + auto scale_const = ov::op::v0::Constant::create(data_precision, ov::Shape{}, {0.7071f}); + experts_input = std::make_shared(experts_reshape, scale_const); + } + auto unsqueeze_experts = - std::make_shared(experts_reshape, + std::make_shared(experts_input, ov::op::v0::Constant::create(ov::element::i32, ov::Shape{}, {0})); + // Router weights always use seed=4 (4th in the source model's seed sequence) auto router_weights = build_matmul_weights(ov::Shape{hidden_size, number_of_experts}, weights_precision, 4, matmul_transpose_b); auto reshape_2nd_consumer_router_matmul = std::make_shared(experts_reshape, router_weights, false, matmul_transpose_b); - auto router_softmax = std::make_shared(reshape_2nd_consumer_router_matmul, 1); - - auto router_topk_values_and_indices = - std::make_shared(router_softmax, - ov::op::v0::Constant::create(ov::element::i64, ov::Shape{}, {topk}), - -1, - ov::op::v11::TopK::Mode::MAX, - ov::op::v11::TopK::SortType::SORT_VALUES, - ov::element::i64); - - auto router_topk_values_reduce = std::make_shared( - router_topk_values_and_indices->output(0), - ov::op::v0::Constant::create(ov::element::i64, ov::Shape{1}, std::vector{-1}), - true); - auto router_topk_values_normalization = - std::make_shared(router_topk_values_and_indices->output(0), router_topk_values_reduce); - auto router_topk_indices = router_topk_values_and_indices->output(1); + // Router: normalized (softmax + normalization) or simple (raw TopK values) + ov::Output router_topk_indices; + ov::Output chosen_experts; + if (use_normalized_router) { + auto router_softmax = std::make_shared(reshape_2nd_consumer_router_matmul, 1); + + auto router_topk_values_and_indices = + std::make_shared(router_softmax, + ov::op::v0::Constant::create(ov::element::i64, ov::Shape{}, {topk}), + -1, + ov::op::v11::TopK::Mode::MAX, + ov::op::v11::TopK::SortType::SORT_VALUES, + ov::element::i64); + + auto router_topk_values_reduce = std::make_shared( + router_topk_values_and_indices->output(0), + ov::op::v0::Constant::create(ov::element::i64, ov::Shape{1}, std::vector{-1}), + true); + auto router_topk_values_normalization = + std::make_shared(router_topk_values_and_indices->output(0), router_topk_values_reduce); + router_topk_indices = router_topk_values_and_indices->output(1); + chosen_experts = router_topk_values_normalization; + } else { + auto router_topk_values_and_indices = + std::make_shared(reshape_2nd_consumer_router_matmul, + ov::op::v0::Constant::create(ov::element::i64, ov::Shape{}, {topk}), + -1, + ov::op::v11::TopK::Mode::MAX, + ov::op::v11::TopK::SortType::SORT_VALUES, + ov::element::i64); + router_topk_indices = router_topk_values_and_indices->output(1); + chosen_experts = router_topk_values_and_indices->output(0); + } // Note: we need to use different seed to avoid the exact weights generation for the MatMuls with the same shape int seed = 1; @@ -686,7 +777,13 @@ inline std::shared_ptr initMoE3GeMMSubgraphRef(bool use_scatter_v12, auto gate_gathered_mm = std::make_shared(unsqueeze_experts, gate_weights, router_topk_indices); - auto swish = std::make_shared(gate_gathered_mm); + // Gate activation: Swish (SwiGLU) or Gelu (GeGLU) + std::shared_ptr gate_act; + if (gate_activation == GateActivationType::GELU) { + gate_act = std::make_shared(gate_gathered_mm); + } else { + gate_act = std::make_shared(gate_gathered_mm); + } auto up_weights = build_matmul_weights(ov::Shape{number_of_experts, hidden_size, intermediate_size}, weights_precision, @@ -695,7 +792,7 @@ inline std::shared_ptr initMoE3GeMMSubgraphRef(bool use_scatter_v12, auto up_gathered_mm = std::make_shared(unsqueeze_experts, up_weights, router_topk_indices); - auto swiglu = std::make_shared(swish, up_gathered_mm); + auto swiglu = std::make_shared(gate_act, up_gathered_mm); auto down_weights = build_matmul_weights(ov::Shape{number_of_experts, intermediate_size, hidden_size}, weights_precision, @@ -704,8 +801,9 @@ inline std::shared_ptr initMoE3GeMMSubgraphRef(bool use_scatter_v12, auto down_gathered_mm = std::make_shared(swiglu, down_weights, router_topk_indices); + // The transformation replaces the scatter chain with transpose + unsqueeze of chosen_experts auto router_transpose = std::make_shared( - router_topk_values_normalization, + chosen_experts, ov::op::v0::Constant::create(ov::element::i64, ov::Shape{2}, std::vector{1, 0})); auto router_unsqueeze = @@ -738,13 +836,17 @@ inline std::shared_ptr initMoE3GeMMSubgraphRef(bool use_scatter_v12, auto final_reshape = std::make_shared(reduce_sum, slice_shape, true); - auto final_reshape_const = ov::op::v0::Constant::create(ov::element::i64, - ov::Shape{2}, - std::vector{0, static_cast(hidden_size)}); - auto original_final_reshape = std::make_shared(final_reshape, final_reshape_const, true); - ov::ParameterVector params = {input}; - return std::make_shared(ov::OutputVector{original_final_reshape}, params); + if (use_normalized_router) { + auto final_reshape_const = + ov::op::v0::Constant::create(ov::element::i64, + ov::Shape{2}, + std::vector{0, static_cast(hidden_size)}); + auto original_final_reshape = std::make_shared(final_reshape, final_reshape_const, true); + return std::make_shared(ov::OutputVector{original_final_reshape}, params); + } else { + return std::make_shared(ov::OutputVector{final_reshape}, params); + } } class ConvertTiledMoeBlockToGatherMatmulsTest : public TransformationTestsF, @@ -756,13 +858,16 @@ class ConvertTiledMoeBlockToGatherMatmulsTest : public TransformationTestsF, use_broadcast_v3, skip_unsqueeze, matmul_transpose_b, - additional_node_consumers, + additional_consumers_mode, + use_gelu_gate, + norm_before_tile, should_be_applied] = obj.param; std::ostringstream result; result << "MoEType_" << moe_type << "_ScatterV" << (use_scatter_v12 ? "12" : "3") << "_BroadcastV" << (use_broadcast_v3 ? "3" : "1") << "_SkipUnsqueeze_" << (skip_unsqueeze ? "true" : "false") << "_MatMulTransposeB_" << (matmul_transpose_b ? "true" : "false") << "_AdditionalConsumers_" - << (additional_node_consumers ? "true" : "false") << "_shouldBeApplied_" + << additional_consumers_mode << "_GeluGate_" << (use_gelu_gate ? "true" : "false") << "_NormBeforeTile_" + << (norm_before_tile ? "true" : "false") << "_shouldBeApplied_" << (should_be_applied ? "true" : "false"); return result.str(); } @@ -775,23 +880,36 @@ class ConvertTiledMoeBlockToGatherMatmulsTest : public TransformationTestsF, use_broadcast_v3, skip_unsqueeze, matmul_transpose_b, - additional_node_consumers, + additional_consumers_mode, + use_gelu_gate, + norm_before_tile, should_be_applied] = this->GetParam(); + // 3GeMM-only params must be false for MoE2GeMM + ASSERT_FALSE(moe_type == MoEType::MoE2GeMM && use_gelu_gate) << "use_gelu_gate must be false for MoE2GeMM"; + ASSERT_FALSE(moe_type == MoEType::MoE2GeMM && norm_before_tile) + << "norm_before_tile must be false for MoE2GeMM"; + + const auto gate_act = use_gelu_gate ? GateActivationType::GELU : GateActivationType::SWISH; + const bool use_normalized_router = !use_gelu_gate && !norm_before_tile; + switch (moe_type) { case MoEType::MoE2GeMM: model = initMoE2GeMMSubgraph(use_scatter_v12, use_broadcast_v3, skip_unsqueeze, matmul_transpose_b, - additional_node_consumers); + additional_consumers_mode); break; case MoEType::MoE3GeMM: model = initMoE3GeMMSubgraph(use_scatter_v12, use_broadcast_v3, skip_unsqueeze, matmul_transpose_b, - additional_node_consumers); + additional_consumers_mode, + gate_act, + norm_before_tile, + use_normalized_router); break; default: OPENVINO_THROW("Unexpected MoEType value"); @@ -806,8 +924,13 @@ class ConvertTiledMoeBlockToGatherMatmulsTest : public TransformationTestsF, initMoE2GeMMSubgraphRef(use_scatter_v12, use_broadcast_v3, skip_unsqueeze, matmul_transpose_b); break; case MoEType::MoE3GeMM: - model_ref = - initMoE3GeMMSubgraphRef(use_scatter_v12, use_broadcast_v3, skip_unsqueeze, matmul_transpose_b); + model_ref = initMoE3GeMMSubgraphRef(use_scatter_v12, + use_broadcast_v3, + skip_unsqueeze, + matmul_transpose_b, + gate_act, + norm_before_tile, + use_normalized_router); break; default: OPENVINO_THROW("Unexpected MoEType value"); @@ -830,10 +953,25 @@ INSTANTIATE_TEST_SUITE_P(ConvertTiledMoeBlockToGatherMatmulsTest_positive_cases, ::testing::ValuesIn(broadcast_versions), ::testing::ValuesIn(skip_unsqueeze_versions), ::testing::Values(true), + ::testing::Values(AdditionalConsumersMode::NO), + ::testing::Values(false), ::testing::Values(false), ::testing::Values(true)), ConvertTiledMoeBlockToGatherMatmulsTest::getTestCaseName); +INSTANTIATE_TEST_SUITE_P(ConvertTiledMoeBlockToGatherMatmulsTest_3gemm_specific_positive_cases, + ConvertTiledMoeBlockToGatherMatmulsTest, + ::testing::Combine(::testing::Values(MoEType::MoE3GeMM), + ::testing::ValuesIn(scatter_versions), + ::testing::ValuesIn(broadcast_versions), + ::testing::ValuesIn(skip_unsqueeze_versions), + ::testing::Values(true), + ::testing::Values(AdditionalConsumersMode::NO), + ::testing::Values(false, true), + ::testing::Values(false, true), + ::testing::Values(true)), + ConvertTiledMoeBlockToGatherMatmulsTest::getTestCaseName); + INSTANTIATE_TEST_SUITE_P(ConvertTiledMoeBlockToGatherMatmulsTest_negative_cases_no_transpose, ConvertTiledMoeBlockToGatherMatmulsTest, ::testing::Combine(::testing::ValuesIn(moe_types), @@ -841,6 +979,8 @@ INSTANTIATE_TEST_SUITE_P(ConvertTiledMoeBlockToGatherMatmulsTest_negative_cases_ ::testing::ValuesIn(broadcast_versions), ::testing::ValuesIn(skip_unsqueeze_versions), ::testing::Values(false), + ::testing::Values(AdditionalConsumersMode::NO), + ::testing::Values(false), ::testing::Values(false), ::testing::Values(false)), ConvertTiledMoeBlockToGatherMatmulsTest::getTestCaseName); @@ -852,7 +992,11 @@ INSTANTIATE_TEST_SUITE_P(ConvertTiledMoeBlockToGatherMatmulsTest_negative_cases_ ::testing::Values(false), ::testing::Values(false), ::testing::Values(true), - ::testing::Values(true), + ::testing::Values(AdditionalConsumersMode::MATMULS, + AdditionalConsumersMode::AFTER_MATMULS, + AdditionalConsumersMode::REDUCE), + ::testing::Values(false), + ::testing::Values(false), ::testing::Values(false)), ConvertTiledMoeBlockToGatherMatmulsTest::getTestCaseName); } // namespace diff --git a/src/common/transformations/tests/common_optimizations/disable_precision_conversion_api_test.cpp b/src/common/transformations/tests/common_optimizations/disable_precision_conversion_api_test.cpp new file mode 100644 index 000000000000..07b4558ec283 --- /dev/null +++ b/src/common/transformations/tests/common_optimizations/disable_precision_conversion_api_test.cpp @@ -0,0 +1,132 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include + +#include "openvino/op/matmul.hpp" +#include "openvino/op/parameter.hpp" +#include "transformations/rt_info/disable_precision_conversion.hpp" + +namespace ov::test { + +TEST(DisablePrecisionConversionAPITest, no_attribute_returns_false) { + auto data_1 = std::make_shared(element::f32, Shape{1, 10}); + auto data_2 = std::make_shared(element::f32, Shape{10, 1}); + auto node = std::make_shared(data_1, data_2); + + // Node with no DisablePrecisionConversion at all + ASSERT_FALSE(is_conversion_disabled(node, element::f16)); + ASSERT_FALSE(is_conversion_disabled(node, element::f32, element::f16)); + ASSERT_FALSE(is_conversion_disabled(node, element::dynamic, element::f16)); +} + +TEST(DisablePrecisionConversionAPITest, specific_from_to_without_dynamic_entry) { + auto data_1 = std::make_shared(element::f32, Shape{1, 10}); + auto data_2 = std::make_shared(element::f32, Shape{10, 1}); + auto node = std::make_shared(data_1, data_2); + + // Only f32->f16, no dynamic entry + disable_conversion(node, element::f32, element::f16); + + ASSERT_FALSE(is_conversion_disabled(node, element::f16)); + ASSERT_TRUE(is_conversion_disabled(node, element::f32, element::f16)); + ASSERT_FALSE(is_conversion_disabled(node, element::f32, element::bf16)); + ASSERT_FALSE(is_conversion_disabled(node, element::f64, element::f16)); +} + +TEST(DisablePrecisionConversionAPITest, enable_erases_dynamic_entry) { + auto data_1 = std::make_shared(element::f32, Shape{1, 10}); + auto data_2 = std::make_shared(element::f32, Shape{10, 1}); + auto node = std::make_shared(data_1, data_2); + + // Add dynamic->f16, then remove it. dynamic entry should be erased + disable_conversion(node, element::f16); + + ASSERT_TRUE(is_conversion_disabled(node, element::f16)); + ASSERT_TRUE(node->get_rt_info().count(DisablePrecisionConversion::get_type_info_static())); + + const auto& attr_before = + node->get_rt_info().at(DisablePrecisionConversion::get_type_info_static()).as(); + ASSERT_EQ(attr_before.m_disabled_precisions.count(element::dynamic), 1); + + enable_conversion(node, element::f16); + + const auto& attr_after = + node->get_rt_info().at(DisablePrecisionConversion::get_type_info_static()).as(); + ASSERT_EQ(attr_after.m_disabled_precisions.count(element::dynamic), 0); + + ASSERT_FALSE(is_conversion_disabled(node, element::f16)); +} + +TEST(DisablePrecisionConversionAPITest, specific_from_dynamic_to_blocks_all_targets) { + auto data_1 = std::make_shared(element::f32, Shape{1, 10}); + auto data_2 = std::make_shared(element::f32, Shape{10, 1}); + auto node = std::make_shared(data_1, data_2); + + // f32->dynamic means block f32 to anything + disable_conversion(node, element::f32, element::dynamic); + + ASSERT_TRUE(is_conversion_disabled(node, element::f32, element::f16)); + ASSERT_TRUE(is_conversion_disabled(node, element::f32, element::bf16)); + ASSERT_TRUE(is_conversion_disabled(node, element::f32, element::i8)); + + // Other source types are not blocked + ASSERT_FALSE(is_conversion_disabled(node, element::f64, element::f16)); + ASSERT_FALSE(is_conversion_disabled(node, element::f16)); +} + +TEST(DisablePrecisionConversionAPITest, dynamic_to_dynamic_blocks_everything) { + auto data_1 = std::make_shared(element::f32, Shape{1, 10}); + auto data_2 = std::make_shared(element::f32, Shape{10, 1}); + auto node = std::make_shared(data_1, data_2); + + // dynamic->dynamic means block all conversions + disable_conversion(node, element::dynamic, element::dynamic); + + ASSERT_TRUE(is_conversion_disabled(node, element::f16)); + ASSERT_TRUE(is_conversion_disabled(node, element::bf16)); + ASSERT_TRUE(is_conversion_disabled(node, element::f32, element::f16)); + ASSERT_TRUE(is_conversion_disabled(node, element::f64, element::bf16)); + ASSERT_TRUE(is_conversion_disabled(node, element::f32, element::i8)); +} + +TEST(DisablePrecisionConversionAPITest, adapter_roundtrip) { + DisabledPrecisionMap original = {{element::dynamic, {element::f16}}, {element::f32, {element::bf16, element::f16}}}; + + AttributeAdapter serializing_adapter(original); + const std::string& serialized = serializing_adapter.get(); + + DisabledPrecisionMap restored; + AttributeAdapter deserializing_adapter(restored); + deserializing_adapter.set(serialized); + + // Verify the restored map matches the original + ASSERT_EQ(restored.size(), original.size()); + + auto it_dynamic = restored.find(element::dynamic); + ASSERT_NE(it_dynamic, restored.end()); + ASSERT_EQ(it_dynamic->second.size(), 1); + ASSERT_TRUE(it_dynamic->second.count(element::f16)); + + auto it_f32 = restored.find(element::f32); + ASSERT_NE(it_f32, restored.end()); + ASSERT_EQ(it_f32->second.size(), 2); + ASSERT_TRUE(it_f32->second.count(element::bf16)); + ASSERT_TRUE(it_f32->second.count(element::f16)); +} + +TEST(DisablePrecisionConversionAPITest, adapter_empty_roundtrip) { + // Empty map roundtrip + DisabledPrecisionMap original; + AttributeAdapter adapter(original); + const std::string& serialized = adapter.get(); + ASSERT_TRUE(serialized.empty()); + + DisabledPrecisionMap restored; + AttributeAdapter deserializing_adapter(restored); + deserializing_adapter.set(serialized); + ASSERT_TRUE(restored.empty()); +} + +} // namespace ov::test \ No newline at end of file diff --git a/src/common/transformations/tests/common_optimizations/fuse_clamp_and_fake_quantize.cpp b/src/common/transformations/tests/common_optimizations/fuse_clamp_and_fake_quantize.cpp new file mode 100644 index 000000000000..0265bdcfde00 --- /dev/null +++ b/src/common/transformations/tests/common_optimizations/fuse_clamp_and_fake_quantize.cpp @@ -0,0 +1,73 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "transformations/common_optimizations/fuse_clamp_and_fake_quantize.hpp" + +#include + +#include +#include +#include + +#include "common_test_utils/ov_test_utils.hpp" +#include "openvino/core/model.hpp" +#include "openvino/op/clamp.hpp" +#include "openvino/op/constant.hpp" +#include "openvino/op/fake_quantize.hpp" +#include "openvino/opsets/opset8_decl.hpp" + +namespace ov::test { + +using FuseClampAndFakeQuantizeParams = std::tuple, // clamp range + std::pair, // fq input range + bool>; // whether Clamp is expected to be fused + +class FuseClampAndFakeQuantizeTestP : public testing::WithParamInterface, + public TransformationTestsF {}; + +TEST_P(FuseClampAndFakeQuantizeTestP, CompareFunctions) { + const auto& [clamp_range, fq_range, should_transform] = GetParam(); + const Shape data_shape{1, 3, 8, 8}; + + { + auto data = std::make_shared(element::f32, data_shape); + auto clamp = std::make_shared(data, clamp_range.first, clamp_range.second); + auto input_low = op::v0::Constant::create(element::f32, Shape{1}, {fq_range.first}); + auto input_high = op::v0::Constant::create(element::f32, Shape{1}, {fq_range.second}); + auto output_low = op::v0::Constant::create(element::f32, Shape{1}, {0.f}); + auto output_high = op::v0::Constant::create(element::f32, Shape{1}, {255.f}); + auto fq = std::make_shared(clamp, input_low, input_high, output_low, output_high, 256); + + model = std::make_shared(OutputVector{fq}, ParameterVector{data}); + manager.register_pass(); + } + + { + auto data = std::make_shared(element::f32, data_shape); + std::shared_ptr fq_input = data; + if (!should_transform) { + fq_input = std::make_shared(fq_input, clamp_range.first, clamp_range.second); + } + + auto input_low = op::v0::Constant::create(element::f32, Shape{1}, {fq_range.first}); + auto input_high = op::v0::Constant::create(element::f32, Shape{1}, {fq_range.second}); + auto output_low = op::v0::Constant::create(element::f32, Shape{1}, {0.f}); + auto output_high = op::v0::Constant::create(element::f32, Shape{1}, {255.f}); + auto fq = std::make_shared(fq_input, input_low, input_high, output_low, output_high, 256); + + model_ref = std::make_shared(OutputVector{fq}, ParameterVector{data}); + } + + comparator.enable(FunctionsComparator::CmpValues::CONST_VALUES); +} + +INSTANTIATE_TEST_SUITE_P(TransformationTests, + FuseClampAndFakeQuantizeTestP, + ::testing::Values(FuseClampAndFakeQuantizeParams({0.f, 10.f}, {1.f, 4.f}, true), + FuseClampAndFakeQuantizeParams({1.f, 4.f}, {1.f, 4.f}, true), + FuseClampAndFakeQuantizeParams({0.f, 2.f}, {1.f, 4.f}, false), + FuseClampAndFakeQuantizeParams({2.f, 8.f}, {1.f, 4.f}, false), + FuseClampAndFakeQuantizeParams({1.f, 4.f}, {0.f, 10.f}, false))); + +} // namespace ov::test \ No newline at end of file diff --git a/src/common/transformations/tests/common_optimizations/fuse_gated_delta_net.cpp b/src/common/transformations/tests/common_optimizations/fuse_gated_delta_net.cpp index 12b7e1c2772e..6628ed5c8c5e 100644 --- a/src/common/transformations/tests/common_optimizations/fuse_gated_delta_net.cpp +++ b/src/common/transformations/tests/common_optimizations/fuse_gated_delta_net.cpp @@ -8,7 +8,11 @@ #include #include +#include +#include #include +#include +#include #include "common_test_utils/ov_test_utils.hpp" #include "openvino/op/add.hpp" @@ -29,11 +33,13 @@ #include "openvino/op/scatter_update.hpp" #include "openvino/op/shape_of.hpp" #include "openvino/op/slice.hpp" +#include "openvino/op/split.hpp" #include "openvino/op/sqrt.hpp" #include "openvino/op/squeeze.hpp" #include "openvino/op/subtract.hpp" #include "openvino/op/transpose.hpp" #include "openvino/op/unsqueeze.hpp" +#include "openvino/runtime/core.hpp" #include "transformations/convert_precision.hpp" namespace ov::test { @@ -339,6 +345,155 @@ TEST_F(TransformationTestsF, GatedDeltaNetFusion_BuildBLHSLoopedGDNMode) { model_ref = build_fused_gdn_ref(batch, seq_len, qk_head_num, v_head_num, qk_head_size, v_head_size); } +namespace { + +std::shared_ptr build_gdn_with_shared_qk_anchor(bool shared_anchor) { + using ov::op::v0::Constant; + using ov::op::v0::Parameter; + using ov::op::v0::Squeeze; + using ov::op::v0::Unsqueeze; + using ov::op::v1::Reshape; + using ov::op::v1::Split; + using ov::op::v1::Transpose; + using ov::op::v8::Gather; + + auto q_src = std::make_shared(ov::element::f32, ov::PartialShape{1, 4, 4, 8}); + auto k_src = std::make_shared(ov::element::f32, ov::PartialShape{1, 4, 4, 8}); + auto v_src = std::make_shared(ov::element::f32, ov::PartialShape{1, 4, 4, 8}); + + auto split_axis = Constant::create(ov::element::i64, {}, {1}); + auto q_split = std::make_shared(q_src, split_axis, 2); + auto k_split = shared_anchor ? q_split : std::make_shared(k_src, split_axis, 2); + auto v_split = shared_anchor ? q_split : std::make_shared(v_src, split_axis, 2); + + auto q_anchor = q_split->output(0); + auto k_anchor = k_split->output(1); + auto v_anchor = v_split->output(0); + + auto gather_idx = Constant::create(ov::element::i64, {2}, {0, 1}); + auto gather_axis = Constant::create(ov::element::i64, {}, {1}); + auto q_gather = std::make_shared(q_anchor, gather_idx, gather_axis); + auto k_gather = std::make_shared(k_anchor, gather_idx, gather_axis); + auto v_gather = std::make_shared(v_anchor, gather_idx, gather_axis); + + auto q_shape = Constant::create(ov::element::i64, {4}, {1, 2, 4, 8}); + auto k_shape = Constant::create(ov::element::i64, {4}, {1, 2, 4, 8}); + auto v_shape = Constant::create(ov::element::i64, {4}, {1, 2, 4, 8}); + auto q_reshape = std::make_shared(q_gather, q_shape, false); + auto k_reshape = std::make_shared(k_gather, k_shape, false); + auto v_reshape = std::make_shared(v_gather, v_shape, false); + + auto perm = Constant::create(ov::element::i64, {4}, {0, 1, 2, 3}); + auto q_transpose = std::make_shared(q_reshape, perm); + auto k_transpose = std::make_shared(k_reshape, perm); + auto v_transpose = std::make_shared(v_reshape, perm); + + auto unsq_axis = Constant::create(ov::element::i32, {1}, {0}); + auto q_unsq = std::make_shared(q_transpose, unsq_axis); + auto k_unsq = std::make_shared(k_transpose, unsq_axis); + auto v_unsq = std::make_shared(v_transpose, unsq_axis); + + auto sq_axis = Constant::create(ov::element::i32, {1}, {0}); + auto q = std::make_shared(q_unsq, sq_axis); + auto k = std::make_shared(k_unsq, sq_axis); + auto v = std::make_shared(v_unsq, sq_axis); + + auto state = std::make_shared(ov::element::f32, ov::PartialShape{1, 4, 8, 8}); + auto gate = std::make_shared(ov::element::f32, ov::PartialShape{1, 4, 4}); + auto beta = std::make_shared(ov::element::f32, ov::PartialShape{1, 4, 4}); + + auto gdn = std::make_shared(q, k, v, state, gate, beta); + auto result0 = std::make_shared(gdn->output(0)); + auto result1 = std::make_shared(gdn->output(1)); + + ov::ParameterVector params = {q_src, k_src, v_src, state, gate, beta}; + return std::make_shared(ov::ResultVector{result0, result1}, params); +} + +} // namespace + +TEST(TransformationTests, FuseGroupedQueryIntoGDN_Positive) { + auto model = build_gdn_with_shared_qk_anchor(true); + ov::pass::Manager manager; + manager.register_pass(); + manager.run_passes(model); + + auto gdn = ov::as_type_ptr( + model->get_results().at(0)->input_value(0).get_node_shared_ptr()); + ASSERT_NE(gdn, nullptr); + + auto q_input = gdn->input_value(0); + auto k_input = gdn->input_value(1); + auto v_input = gdn->input_value(2); + + // Find the anchor (Split) node for Q input + // It may be wrapped in Reshape/Transpose/etc + auto find_split_ancestor = [](const ov::Output& output) -> std::shared_ptr { + auto node = output.get_node_shared_ptr(); + std::set> visited; + std::queue> to_visit; + to_visit.push(node); + + while (!to_visit.empty()) { + auto current = to_visit.front(); + to_visit.pop(); + + if (visited.count(current)) + continue; + visited.insert(current); + + if (ov::is_type(current)) { + return current; + } + + // Continue searching upwards + for (size_t i = 0; i < current->get_input_size(); ++i) { + to_visit.push(current->input_value(i).get_node_shared_ptr()); + } + } + return nullptr; + }; + + auto q_split = find_split_ancestor(q_input); + auto k_split = find_split_ancestor(k_input); + auto v_split = find_split_ancestor(v_input); + + ASSERT_NE(q_split, nullptr); + ASSERT_NE(k_split, nullptr); + ASSERT_NE(v_split, nullptr); + ASSERT_EQ(q_split, k_split); + ASSERT_EQ(k_split, v_split); +} + +TEST(TransformationTests, FuseGroupedQueryIntoGDN_NegativeDifferentAnchors) { + auto model = build_gdn_with_shared_qk_anchor(false); + ov::pass::Manager manager; + manager.register_pass(); + manager.run_passes(model); + + auto gdn = ov::as_type_ptr( + model->get_results().at(0)->input_value(0).get_node_shared_ptr()); + ASSERT_NE(gdn, nullptr); + + auto q_input_node = gdn->input_value(0).get_node_shared_ptr(); + auto k_input_node = gdn->input_value(1).get_node_shared_ptr(); + auto v_input_node = gdn->input_value(2).get_node_shared_ptr(); + + if (ov::is_type(q_input_node)) { + q_input_node = q_input_node->input_value(0).get_node_shared_ptr(); + } + if (ov::is_type(k_input_node)) { + k_input_node = k_input_node->input_value(0).get_node_shared_ptr(); + } + if (ov::is_type(v_input_node)) { + v_input_node = v_input_node->input_value(0).get_node_shared_ptr(); + } + + // With different anchors, transformation should not apply, so at least one should not be Split + ASSERT_FALSE(ov::is_type(q_input_node) && q_input_node == k_input_node && + k_input_node == v_input_node); +} + TEST_F(TransformationTestsF, GatedDeltaNetFusion_BuildBHLSLoopedGDNMode_F16) { disable_rt_info_check(); disable_result_friendly_names_check(); @@ -402,4 +557,233 @@ TEST_F(TransformationTestsF, GatedDeltaNetFusion_BuildBLHSLoopedGDNMode_F16) { model_ref = build_fused_gdn_ref(batch, seq_len, qk_head_num, v_head_num, qk_head_size, v_head_size, ov::element::f16); } + +namespace { + +// Build a model where Q, K, V share one Split anchor, each connected via a single Transpose. +// After FuseGroupedQueryIntoGDN the Transposes are removed and GDN is directly fed from Split outputs. +std::shared_ptr build_grouped_query_gdn_before() { + // src: {1, 4, 4, 8} — Split along dim=1 (H) into 2 groups → outputs {1, 2, 4, 8} + auto src = std::make_shared(ov::element::f32, ov::PartialShape{1, 4, 4, 8}); + auto state = std::make_shared(ov::element::f32, ov::PartialShape{1, 4, 8, 8}); + auto gate = std::make_shared(ov::element::f32, ov::PartialShape{1, 2, 4}); + auto beta = std::make_shared(ov::element::f32, ov::PartialShape{1, 2, 4}); + + auto split_axis = ov::op::v0::Constant::create(ov::element::i64, {}, {1}); + auto split = std::make_shared(src, split_axis, 2); + // split->output(0) and split->output(1) are both {1, 2, 4, 8} + + auto identity_perm = ov::op::v0::Constant::create(ov::element::i64, {4}, {0, 1, 2, 3}); + auto q = std::make_shared(split->output(0), identity_perm); + auto k = std::make_shared(split->output(1), identity_perm); + auto v = std::make_shared(split->output(0), identity_perm); + + auto gdn = std::make_shared(q, k, v, state, gate, beta); + return std::make_shared(ov::ResultVector{std::make_shared(gdn->output(0)), + std::make_shared(gdn->output(1))}, + ov::ParameterVector{src, state, gate, beta}); +} + +// Reference "after" model: GDN directly fed from Split outputs (Transposes removed). +// align_to_reference_shape: {1,2,4,8} compatible with {1,2,4,8} → no Reshape inserted. +std::shared_ptr build_grouped_query_gdn_after() { + auto src = std::make_shared(ov::element::f32, ov::PartialShape{1, 4, 4, 8}); + auto state = std::make_shared(ov::element::f32, ov::PartialShape{1, 4, 8, 8}); + auto gate = std::make_shared(ov::element::f32, ov::PartialShape{1, 2, 4}); + auto beta = std::make_shared(ov::element::f32, ov::PartialShape{1, 2, 4}); + + auto split_axis = ov::op::v0::Constant::create(ov::element::i64, {}, {1}); + auto split = std::make_shared(src, split_axis, 2); + + auto gdn = std::make_shared(split->output(0), + split->output(1), + split->output(0), + state, + gate, + beta); + return std::make_shared(ov::ResultVector{std::make_shared(gdn->output(0)), + std::make_shared(gdn->output(1))}, + ov::ParameterVector{src, state, gate, beta}); +} + +// Build a model where Q, K, V each come from their own Split → anchors differ → no transformation. +std::shared_ptr build_grouped_query_gdn_different_anchors() { + auto src_q = std::make_shared(ov::element::f32, ov::PartialShape{1, 4, 4, 8}); + auto src_k = std::make_shared(ov::element::f32, ov::PartialShape{1, 4, 4, 8}); + auto src_v = std::make_shared(ov::element::f32, ov::PartialShape{1, 4, 4, 8}); + auto state = std::make_shared(ov::element::f32, ov::PartialShape{1, 4, 8, 8}); + auto gate = std::make_shared(ov::element::f32, ov::PartialShape{1, 2, 4}); + auto beta = std::make_shared(ov::element::f32, ov::PartialShape{1, 2, 4}); + + auto split_axis = ov::op::v0::Constant::create(ov::element::i64, {}, {1}); + auto q_split = std::make_shared(src_q, split_axis, 2); + auto k_split = std::make_shared(src_k, split_axis, 2); + auto v_split = std::make_shared(src_v, split_axis, 2); + + auto identity_perm = ov::op::v0::Constant::create(ov::element::i64, {4}, {0, 1, 2, 3}); + auto q = std::make_shared(q_split->output(0), identity_perm); + auto k = std::make_shared(k_split->output(0), identity_perm); + auto v = std::make_shared(v_split->output(0), identity_perm); + + auto gdn = std::make_shared(q, k, v, state, gate, beta); + return std::make_shared(ov::ResultVector{std::make_shared(gdn->output(0)), + std::make_shared(gdn->output(1))}, + ov::ParameterVector{src_q, src_k, src_v, state, gate, beta}); +} + +// Build a model where the shared Concat anchor is 3D, while GDN consumes a 4D tensor. +// This forces the grouped-query fusion path to rebuild the reshape into a 4D shape. +std::shared_ptr build_grouped_query_gdn_rank3_before() { + auto src0 = std::make_shared(ov::element::f32, ov::PartialShape{1, 4, 8}); + auto src1 = std::make_shared(ov::element::f32, ov::PartialShape{1, 4, 8}); + auto state = std::make_shared(ov::element::f32, ov::PartialShape{1, 2, 8, 8}); + auto gate = std::make_shared(ov::element::f32, ov::PartialShape{1, 4, 2}); + auto beta = std::make_shared(ov::element::f32, ov::PartialShape{1, 4, 2}); + + auto src = std::make_shared(ov::OutputVector{src0, src1}, 2); + + auto reshape_shape = ov::op::v0::Constant::create(ov::element::i64, {4}, {1, 4, 2, 8}); + auto q = std::make_shared(src, reshape_shape, false); + auto k = std::make_shared(src, reshape_shape, false); + auto v = std::make_shared(src, reshape_shape, false); + + auto gdn = std::make_shared(q, k, v, state, gate, beta); + return std::make_shared(ov::ResultVector{std::make_shared(gdn->output(0)), + std::make_shared(gdn->output(1))}, + ov::ParameterVector{src0, src1, state, gate, beta}); +} + +std::shared_ptr build_grouped_query_gdn_rank3_after() { + auto src0 = std::make_shared(ov::element::f32, ov::PartialShape{1, 4, 8}); + auto src1 = std::make_shared(ov::element::f32, ov::PartialShape{1, 4, 8}); + auto state = std::make_shared(ov::element::f32, ov::PartialShape{1, 2, 8, 8}); + auto gate = std::make_shared(ov::element::f32, ov::PartialShape{1, 4, 2}); + auto beta = std::make_shared(ov::element::f32, ov::PartialShape{1, 4, 2}); + + auto src = std::make_shared(ov::OutputVector{src0, src1}, 2); + + auto q_ref = + std::make_shared(src, + ov::op::v0::Constant::create(ov::element::i64, {4}, {1, 4, 2, 8}), + false); + auto k_ref = + std::make_shared(src, + ov::op::v0::Constant::create(ov::element::i64, {4}, {1, 4, 2, 8}), + false); + auto v_ref = + std::make_shared(src, + ov::op::v0::Constant::create(ov::element::i64, {4}, {1, 4, 2, 8}), + false); + + auto q_ref_shape = std::make_shared(q_ref); + auto k_ref_shape = std::make_shared(k_ref); + auto v_ref_shape = std::make_shared(v_ref); + auto indices = ov::op::v0::Constant::create(ov::element::i64, {1}, {2}); + auto axis = ov::op::v0::Constant::create(ov::element::i64, {}, {0}); + auto updates = ov::op::v0::Constant::create(ov::element::i64, {1}, {2}); + auto q_shape = std::make_shared(q_ref_shape, indices, updates, axis); + auto k_shape = std::make_shared(k_ref_shape, indices, updates, axis); + auto v_shape = std::make_shared(v_ref_shape, indices, updates, axis); + + auto q = std::make_shared(src, q_shape, false); + auto k = std::make_shared(src, k_shape, false); + auto v = std::make_shared(src, v_shape, false); + + auto gdn = std::make_shared(q, k, v, state, gate, beta); + return std::make_shared(ov::ResultVector{std::make_shared(gdn->output(0)), + std::make_shared(gdn->output(1))}, + ov::ParameterVector{src0, src1, state, gate, beta}); +} + +// Same reshape regression, but with batch > 1 and dynamic sequence length. +std::shared_ptr build_grouped_query_gdn_rank3_dynamic_before() { + auto src0 = std::make_shared(ov::element::f32, ov::PartialShape{2, -1, 8}); + auto src1 = std::make_shared(ov::element::f32, ov::PartialShape{2, -1, 8}); + auto state = std::make_shared(ov::element::f32, ov::PartialShape{2, 2, 8, 8}); + auto gate = std::make_shared(ov::element::f32, ov::PartialShape{2, -1, 2}); + auto beta = std::make_shared(ov::element::f32, ov::PartialShape{2, -1, 2}); + + auto src = std::make_shared(ov::OutputVector{src0, src1}, 2); + + auto reshape_shape = ov::op::v0::Constant::create(ov::element::i64, {4}, {2, -1, 2, 8}); + auto q = std::make_shared(src, reshape_shape, false); + auto k = std::make_shared(src, reshape_shape, false); + auto v = std::make_shared(src, reshape_shape, false); + + auto gdn = std::make_shared(q, k, v, state, gate, beta); + return std::make_shared(ov::ResultVector{std::make_shared(gdn->output(0)), + std::make_shared(gdn->output(1))}, + ov::ParameterVector{src0, src1, state, gate, beta}); +} + +std::shared_ptr build_grouped_query_gdn_rank3_dynamic_after() { + auto src0 = std::make_shared(ov::element::f32, ov::PartialShape{2, -1, 8}); + auto src1 = std::make_shared(ov::element::f32, ov::PartialShape{2, -1, 8}); + auto state = std::make_shared(ov::element::f32, ov::PartialShape{2, 2, 8, 8}); + auto gate = std::make_shared(ov::element::f32, ov::PartialShape{2, -1, 2}); + auto beta = std::make_shared(ov::element::f32, ov::PartialShape{2, -1, 2}); + + auto src = std::make_shared(ov::OutputVector{src0, src1}, 2); + + auto q_ref = + std::make_shared(src, + ov::op::v0::Constant::create(ov::element::i64, {4}, {2, -1, 2, 8}), + false); + auto k_ref = + std::make_shared(src, + ov::op::v0::Constant::create(ov::element::i64, {4}, {2, -1, 2, 8}), + false); + auto v_ref = + std::make_shared(src, + ov::op::v0::Constant::create(ov::element::i64, {4}, {2, -1, 2, 8}), + false); + + auto q_ref_shape = std::make_shared(q_ref); + auto k_ref_shape = std::make_shared(k_ref); + auto v_ref_shape = std::make_shared(v_ref); + auto indices = ov::op::v0::Constant::create(ov::element::i64, {1}, {2}); + auto axis = ov::op::v0::Constant::create(ov::element::i64, {}, {0}); + auto updates = ov::op::v0::Constant::create(ov::element::i64, {1}, {2}); + auto q_shape = std::make_shared(q_ref_shape, indices, updates, axis); + auto k_shape = std::make_shared(k_ref_shape, indices, updates, axis); + auto v_shape = std::make_shared(v_ref_shape, indices, updates, axis); + + auto q = std::make_shared(src, q_shape, false); + auto k = std::make_shared(src, k_shape, false); + auto v = std::make_shared(src, v_shape, false); + + auto gdn = std::make_shared(q, k, v, state, gate, beta); + return std::make_shared(ov::ResultVector{std::make_shared(gdn->output(0)), + std::make_shared(gdn->output(1))}, + ov::ParameterVector{src0, src1, state, gate, beta}); +} + +} // namespace + +// Positive: all Q/K/V share one Split anchor via a Transpose each → transformation fuses them. +TEST_F(TransformationTestsF, FuseGroupedQueryIntoGDN_SharedAnchor_Applied) { + model = build_grouped_query_gdn_before(); + manager.register_pass(); + model_ref = build_grouped_query_gdn_after(); +} + +// Negative: Q, K, V each come from a distinct Split node → anchors mismatch → transformation skipped. +TEST_F(TransformationTestsF, FuseGroupedQueryIntoGDN_DifferentAnchors_NotApplied) { + model = build_grouped_query_gdn_different_anchors(); + manager.register_pass(); + model_ref = build_grouped_query_gdn_different_anchors(); +} + +TEST_F(TransformationTestsF, FuseGroupedQueryIntoGDN_ReshapeTo4D_Applied) { + model = build_grouped_query_gdn_rank3_before(); + manager.register_pass(); + model_ref = build_grouped_query_gdn_rank3_after(); +} + +TEST_F(TransformationTestsF, FuseGroupedQueryIntoGDN_ReshapeTo4D_DynamicSeqLen_Applied) { + model = build_grouped_query_gdn_rank3_dynamic_before(); + manager.register_pass(); + model_ref = build_grouped_query_gdn_rank3_dynamic_after(); +} + } // namespace ov::test \ No newline at end of file diff --git a/src/common/transformations/tests/common_optimizations/fuse_moe_test.cpp b/src/common/transformations/tests/common_optimizations/fuse_moe_test.cpp index bd716d48ef82..822a52a914d3 100644 --- a/src/common/transformations/tests/common_optimizations/fuse_moe_test.cpp +++ b/src/common/transformations/tests/common_optimizations/fuse_moe_test.cpp @@ -16,7 +16,6 @@ #include "openvino/opsets/opset6.hpp" #include "openvino/opsets/opset8.hpp" #include "transformations/common_optimizations/fuse_moe_experts.hpp" -#include "transformations/common_optimizations/matmul_experts_fusion.hpp" #include "transformations/rt_info/decompression.hpp" #include "transformations/utils/gen_pattern.hpp" @@ -299,107 +298,6 @@ static std::shared_ptr BuildFusedMOE(const int expert_num, const int ov::ParameterVector{final_hidden_states_, router_logits, hidden_states_2d}); } -static std::shared_ptr BuildFusedMOEWithInternalOp(const int expert_num, const int topk) { - constexpr int64_t hidden_size = 2048; - constexpr int64_t intermediate_size = 768; - - ov::element::Type inType = ov::element::f32; - auto final_hidden_states_ = std::make_shared(inType, ov::PartialShape{-1, hidden_size}); - auto router_logits = std::make_shared(inType, ov::PartialShape{-1, expert_num}); - auto hidden_states_2d = std::make_shared(inType, ov::PartialShape{-1, hidden_size}); - - auto residual_input = makeOP({final_hidden_states_}, {{"destination_type", "f32"}}); - auto hidden_states_ = makeOP({hidden_states_2d}, {{"destination_type", "f32"}}); - - auto softmax_Softmax = makeOP({router_logits}, {{"axis", 1}}); - auto topk_TopK = makeOP( - {softmax_Softmax, topk}, - {{"axis", -1}, {"mode", "max"}, {"sort", "value"}, {"index_element_type", "i64"}, {"stable", false}}); - - auto target_shape = makeOP({hidden_states_}, {{"output_type", "i64"}}); - - auto axis_minus_one = makeConst(ov::element::i64, ov::Shape{1}, std::vector{-1}); - auto topk_values = topk_TopK->output(0); - auto sum_reduce = makeOP({topk_values, axis_minus_one}, {{"keep_dims", true}}); - auto normalized_topk = - makeOP({topk_values, sum_reduce}, {{"auto_broadcast", "numpy"}, {"m_pythondiv", true}}); - - auto axis0_scalar = makeConst(ov::element::i64, ov::Shape{}, std::vector{0}); - auto axis0_vector = makeConst(ov::element::i64, ov::Shape{1}, std::vector{0}); - auto axis1_vector = makeConst(ov::element::i64, ov::Shape{1}, std::vector{1}); - auto transpose_axes = makeConst(ov::element::i64, ov::Shape{2}, std::vector{1, 0}); - auto axis_minus_one_vector = makeConst(ov::element::i64, ov::Shape{1}, std::vector{-1}); - auto minus_one = makeConst(ov::element::i64, ov::Shape{1}, std::vector{-1}); - - auto router_topk_indices = topk_TopK->output(1); - auto topk_indices_shape = makeOP({router_topk_indices}, {{"output_type", "i64"}}); - auto batch_dim_scalar = - makeOP({topk_indices_shape, axis0_scalar, axis0_scalar}, {{"batch_dims", 0}}); - auto batch_dim_unsqueeze = makeOP({batch_dim_scalar, axis0_vector}); - - auto num_experts_const = - makeConst(ov::element::i64, ov::Shape{}, std::vector{static_cast(expert_num)}); - auto num_experts_unsqueeze = makeOP({num_experts_const, axis0_vector}); - - auto zeros_scalar = makeConst(ov::element::f32, ov::Shape{}, std::vector{0.0f}); - auto scatter_shape = std::make_shared(ov::OutputVector{batch_dim_unsqueeze, num_experts_unsqueeze}, 0); - auto zeros_tensor = std::make_shared(zeros_scalar, scatter_shape); - auto scatter = - std::make_shared(zeros_tensor, router_topk_indices, normalized_topk, axis1_vector); - auto router_transpose = std::make_shared(scatter, transpose_axes); - auto router_shape = - std::make_shared(ov::OutputVector{num_experts_unsqueeze, batch_dim_unsqueeze, minus_one}, 0); - auto router_reshape = std::make_shared(router_transpose, router_shape, true); - auto routing_weights = std::make_shared(router_reshape, axis_minus_one_vector); - - auto build_fused_weight = [&](ov::element::Type elem_type, - const ov::Shape& single_expert_shape) -> std::shared_ptr { - ov::OutputVector expert_weights; - for (int i = 0; i < expert_num; i++) { - auto weight_const = makeConst(elem_type, single_expert_shape, {0}); - expert_weights.push_back(weight_const); - } - auto concat = std::make_shared(expert_weights, 0); - concat->get_rt_info()["postponed_constant"] = true; - return concat; - }; - - auto fused_gate_weights_f16 = - build_fused_weight(ov::element::f16, - ov::Shape{1, static_cast(intermediate_size), static_cast(hidden_size)}); - auto fused_gate_weights = makeOP({fused_gate_weights_f16}, {{"destination_type", "f32"}}); - ov::mark_as_decompression(fused_gate_weights); - - auto fused_up_weights_f16 = - build_fused_weight(ov::element::f16, - ov::Shape{1, static_cast(intermediate_size), static_cast(hidden_size)}); - auto fused_up_weights = makeOP({fused_up_weights_f16}, {{"destination_type", "f32"}}); - ov::mark_as_decompression(fused_up_weights); - - auto fused_down_weights_f16 = - build_fused_weight(ov::element::f16, - ov::Shape{1, static_cast(hidden_size), static_cast(intermediate_size)}); - auto fused_down_weights = makeOP({fused_down_weights_f16}, {{"destination_type", "f32"}}); - ov::mark_as_decompression(fused_down_weights); - - ov::OutputVector moe_inputs = {hidden_states_, - routing_weights, - router_topk_indices, - fused_gate_weights, - fused_up_weights, - fused_down_weights}; - - ov::op::internal::MOE::Config config; - config.expert_type = ov::op::internal::MOE::Expert_type::GEMM3_SWIGLU; - - auto moe = std::make_shared(moe_inputs, config); - auto final_reshape = std::make_shared(moe, target_shape, false); - auto final_add = std::make_shared(residual_input, final_reshape); - - return std::make_shared(ov::OutputVector{final_add}, - ov::ParameterVector{final_hidden_states_, router_logits, hidden_states_2d}); -} - TEST_F(TransformationTestsF, ConvertMOEToFuseMOE_FP16) { disable_rt_info_check(); disable_result_friendly_names_check(); @@ -411,16 +309,3 @@ TEST_F(TransformationTestsF, ConvertMOEToFuseMOE_FP16) { ov::pass::FuseMOE().run_on_model(model); model_ref = BuildFusedMOE(expert_num, topk); } - -TEST_F(TransformationTestsF, FuseMOEExperts_to_FuseVectorizedMOE3GEMM_Integration) { - disable_rt_info_check(); - disable_result_friendly_names_check(); - - int expert_num = 16; - int topk = 8; - - model = BuildMOE(expert_num, topk); - ov::pass::FuseMOE().run_on_model(model); - manager.register_pass(); - model_ref = BuildFusedMOEWithInternalOp(expert_num, topk); -} diff --git a/src/common/transformations/tests/common_optimizations/fuse_rotary_positional_embeddings.cpp b/src/common/transformations/tests/common_optimizations/fuse_rotary_positional_embeddings.cpp index 781110418ed0..43f28fa33ede 100644 --- a/src/common/transformations/tests/common_optimizations/fuse_rotary_positional_embeddings.cpp +++ b/src/common/transformations/tests/common_optimizations/fuse_rotary_positional_embeddings.cpp @@ -2123,3 +2123,186 @@ TEST_F(TransformationTestsF, ConvertToROPE_LtxVideo) { model_ref = std::make_shared(OutputVector{rope}, ParameterVector{input, cos_freqs, sin_freqs}); } } + +TEST_F(TransformationTestsF, ConvertToROPE_GPTOSS_negative_axis) { + disable_rt_info_check(); + const int batch = 2; + const int num_head = 32; + const int seq_len = 16; + const int ndims = 128; + const int half_ndims = ndims / 2; + using namespace ov; + { + // gpt-oss style RoPE pattern with Concat and VariadicSplit using axis = -1 to represent the last dimension. + auto input = std::make_shared(element::f32, PartialShape{batch, num_head, seq_len, ndims}); + auto t_cos = std::make_shared(element::f32, PartialShape{batch, 1, seq_len, half_ndims}); + auto t_sin = std::make_shared(element::f32, PartialShape{batch, 1, seq_len, half_ndims}); + + auto split_lengths = makeConst(element::i64, {2}, std::vector{half_ndims, half_ndims}); + auto axis_const = makeConst(element::i64, {}, std::vector{-1}); + auto vsplit = makeOP({input, axis_const, split_lengths}); + + // first_ = first_half * cos - second_half * sin + auto first_half_mul_cos = makeOP({vsplit->output(0), t_cos}, {{"auto_broadcast", "numpy"}}); + auto second_half_mul_sin = makeOP({vsplit->output(1), t_sin}, {{"auto_broadcast", "numpy"}}); + auto neg = makeOP({second_half_mul_sin, -1.0f}, {{"auto_broadcast", "numpy"}}); + auto first_ = makeOP({first_half_mul_cos, neg}, {{"auto_broadcast", "numpy"}}); + + // second_ = second_half * cos + first_half * sin + auto second_half_mul_cos = makeOP({vsplit->output(1), t_cos}, {{"auto_broadcast", "numpy"}}); + auto first_half_mul_sin = makeOP({vsplit->output(0), t_sin}, {{"auto_broadcast", "numpy"}}); + auto second_ = makeOP({second_half_mul_cos, first_half_mul_sin}, {{"auto_broadcast", "numpy"}}); + + auto result = makeOP({first_, second_}, {{"axis", -1}}); + + model = std::make_shared(OutputVector{result}, ParameterVector{input, t_cos, t_sin}); + } + manager.register_pass(); + { + auto input = std::make_shared(element::f32, PartialShape{batch, num_head, seq_len, ndims}); + auto t_cos = std::make_shared(element::f32, PartialShape{batch, 1, seq_len, half_ndims}); + auto t_sin = std::make_shared(element::f32, PartialShape{batch, 1, seq_len, half_ndims}); + + auto rope = makeOP({input, t_cos, t_sin}, + {{"config.slice_start", 0}, + {"config.slice_stop", 0}, + {"config.input_trans0213", false}, + {"config.output_trans0213", false}, + {"config.is_interleaved", false}, + {"config.rotary_ndims", ndims}, + {"config.cos_sin_ndims", half_ndims}, + {"config.is_chatglm", false}, + {"config.support_2d_rope", false}, + {"config.support_3d_rope", false}, + {"config.is_qwen", false}, + {"config.use_rope_cache", false}, + {"config.is_ltx_video", false}, + {"config.head_cnt", 0}, + {"config.head_size", 0}, + {"config.gather_position_arg_id", 0}}); + + model_ref = std::make_shared(OutputVector{rope}, ParameterVector{input, t_cos, t_sin}); + } +} + +TEST_F(TransformationTestsF, ConvertToROPE_GPTOSS_concat_axis_positive) { + disable_rt_info_check(); + const int batch = 2; + const int num_head = 32; + const int seq_len = 16; + const int ndims = 128; + const int half_ndims = ndims / 2; + using namespace ov; + { + // gpt-oss style RoPE pattern with Concat using axis = 3 to represent the last dimension. + auto input = std::make_shared(element::f32, PartialShape{batch, num_head, seq_len, ndims}); + auto t_cos = std::make_shared(element::f32, PartialShape{batch, 1, seq_len, half_ndims}); + auto t_sin = std::make_shared(element::f32, PartialShape{batch, 1, seq_len, half_ndims}); + + auto split_lengths = makeConst(element::i64, {2}, std::vector{half_ndims, half_ndims}); + auto axis_const = makeConst(element::i64, {}, std::vector{-1}); + auto vsplit = makeOP({input, axis_const, split_lengths}); + + // first_ = first_half * cos - second_half * sin + auto first_half_mul_cos = makeOP({vsplit->output(0), t_cos}, {{"auto_broadcast", "numpy"}}); + auto second_half_mul_sin = makeOP({vsplit->output(1), t_sin}, {{"auto_broadcast", "numpy"}}); + auto neg = makeOP({second_half_mul_sin, -1.0f}, {{"auto_broadcast", "numpy"}}); + auto first_ = makeOP({first_half_mul_cos, neg}, {{"auto_broadcast", "numpy"}}); + + // second_ = second_half * cos + first_half * sin + auto second_half_mul_cos = makeOP({vsplit->output(1), t_cos}, {{"auto_broadcast", "numpy"}}); + auto first_half_mul_sin = makeOP({vsplit->output(0), t_sin}, {{"auto_broadcast", "numpy"}}); + auto second_ = makeOP({second_half_mul_cos, first_half_mul_sin}, {{"auto_broadcast", "numpy"}}); + + auto result = makeOP({first_, second_}, {{"axis", 3}}); + + model = std::make_shared(OutputVector{result}, ParameterVector{input, t_cos, t_sin}); + } + manager.register_pass(); + { + auto input = std::make_shared(element::f32, PartialShape{batch, num_head, seq_len, ndims}); + auto t_cos = std::make_shared(element::f32, PartialShape{batch, 1, seq_len, half_ndims}); + auto t_sin = std::make_shared(element::f32, PartialShape{batch, 1, seq_len, half_ndims}); + + auto rope = makeOP({input, t_cos, t_sin}, + {{"config.slice_start", 0}, + {"config.slice_stop", 0}, + {"config.input_trans0213", false}, + {"config.output_trans0213", false}, + {"config.is_interleaved", false}, + {"config.rotary_ndims", ndims}, + {"config.cos_sin_ndims", half_ndims}, + {"config.is_chatglm", false}, + {"config.support_2d_rope", false}, + {"config.support_3d_rope", false}, + {"config.is_qwen", false}, + {"config.use_rope_cache", false}, + {"config.is_ltx_video", false}, + {"config.head_cnt", 0}, + {"config.head_size", 0}, + {"config.gather_position_arg_id", 0}}); + + model_ref = std::make_shared(OutputVector{rope}, ParameterVector{input, t_cos, t_sin}); + } +} + +TEST_F(TransformationTestsF, ConvertToROPE_GPTOSS_split_axis_positive) { + disable_rt_info_check(); + const int batch = 2; + const int num_head = 32; + const int seq_len = 16; + const int ndims = 128; + const int half_ndims = ndims / 2; + using namespace ov; + { + // gpt-oss style RoPE pattern with VariadicSplit using axis = 3 to represent the last dimension. + auto input = std::make_shared(element::f32, PartialShape{batch, num_head, seq_len, ndims}); + auto t_cos = std::make_shared(element::f32, PartialShape{batch, 1, seq_len, half_ndims}); + auto t_sin = std::make_shared(element::f32, PartialShape{batch, 1, seq_len, half_ndims}); + + auto split_lengths = makeConst(element::i64, {2}, std::vector{half_ndims, half_ndims}); + auto axis_const = makeConst(element::i64, {}, std::vector{3}); + auto vsplit = makeOP({input, axis_const, split_lengths}); + + // first_ = first_half * cos - second_half * sin + auto first_half_mul_cos = makeOP({vsplit->output(0), t_cos}, {{"auto_broadcast", "numpy"}}); + auto second_half_mul_sin = makeOP({vsplit->output(1), t_sin}, {{"auto_broadcast", "numpy"}}); + auto neg = makeOP({second_half_mul_sin, -1.0f}, {{"auto_broadcast", "numpy"}}); + auto first_ = makeOP({first_half_mul_cos, neg}, {{"auto_broadcast", "numpy"}}); + + // second_ = second_half * cos + first_half * sin + auto second_half_mul_cos = makeOP({vsplit->output(1), t_cos}, {{"auto_broadcast", "numpy"}}); + auto first_half_mul_sin = makeOP({vsplit->output(0), t_sin}, {{"auto_broadcast", "numpy"}}); + auto second_ = makeOP({second_half_mul_cos, first_half_mul_sin}, {{"auto_broadcast", "numpy"}}); + + auto result = makeOP({first_, second_}, {{"axis", -1}}); + + model = std::make_shared(OutputVector{result}, ParameterVector{input, t_cos, t_sin}); + } + manager.register_pass(); + { + auto input = std::make_shared(element::f32, PartialShape{batch, num_head, seq_len, ndims}); + auto t_cos = std::make_shared(element::f32, PartialShape{batch, 1, seq_len, half_ndims}); + auto t_sin = std::make_shared(element::f32, PartialShape{batch, 1, seq_len, half_ndims}); + + auto rope = makeOP({input, t_cos, t_sin}, + {{"config.slice_start", 0}, + {"config.slice_stop", 0}, + {"config.input_trans0213", false}, + {"config.output_trans0213", false}, + {"config.is_interleaved", false}, + {"config.rotary_ndims", ndims}, + {"config.cos_sin_ndims", half_ndims}, + {"config.is_chatglm", false}, + {"config.support_2d_rope", false}, + {"config.support_3d_rope", false}, + {"config.is_qwen", false}, + {"config.use_rope_cache", false}, + {"config.is_ltx_video", false}, + {"config.head_cnt", 0}, + {"config.head_size", 0}, + {"config.gather_position_arg_id", 0}}); + + model_ref = std::make_shared(OutputVector{rope}, ParameterVector{input, t_cos, t_sin}); + } +} diff --git a/src/common/transformations/tests/common_optimizations/group_normalization_fusion_tests.cpp b/src/common/transformations/tests/common_optimizations/group_normalization_fusion_tests.cpp index 882ddb56166b..453f402f9d46 100644 --- a/src/common/transformations/tests/common_optimizations/group_normalization_fusion_tests.cpp +++ b/src/common/transformations/tests/common_optimizations/group_normalization_fusion_tests.cpp @@ -47,6 +47,7 @@ class GroupNormalizationFusionTestBase { protected: element::Type elem_type; int64_t num_channels; + bool positive_test; bool instance_norm_gamma_present; bool instance_norm_beta_present; @@ -65,70 +66,7 @@ class GroupNormalizationFusionTestBase { virtual void read_test_parameters() = 0; - void generate_weights_init_values() { - if (instance_norm_gamma_present) { - auto instanceNormGammaTensor = utils::create_and_fill_tensor(elem_type, - instance_norm_gamma_shape, - utils::InputGenerateData(1, 10, 1, 2)); - instance_norm_gamma_const = std::make_shared(instanceNormGammaTensor); - } - if (instance_norm_beta_present) { - auto instanceNormBetaTensor = utils::create_and_fill_tensor(elem_type, - instance_norm_beta_shape, - utils::InputGenerateData(1, 10, 1, 3)); - instance_norm_beta_const = std::make_shared(instanceNormBetaTensor); - } - - auto groupNormGammaTensor = - utils::create_and_fill_tensor(elem_type, group_norm_gamma_shape, utils::InputGenerateData(1, 10, 1, 1)); - group_norm_gamma_const = std::make_shared(groupNormGammaTensor); - - auto groupNormBetaTensor = - utils::create_and_fill_tensor(elem_type, group_norm_beta_shape, utils::InputGenerateData(1, 10, 1, 11)); - group_norm_beta_const = std::make_shared(groupNormBetaTensor); - } - - std::shared_ptr create_model() { - auto input = std::make_shared(elem_type, data_shape); - auto pre_mvn_shape_const = op::v0::Constant::create(element::i64, Shape{3}, {0, num_groups, -1}); - auto pre_mvn_reshape = std::make_shared(input, pre_mvn_shape_const, true); - - auto mvn_axes_const = op::v0::Constant::create(element::i64, Shape{1}, {2}); - auto mvn = std::make_shared(pre_mvn_reshape, - mvn_axes_const, - true, - static_cast(epsilon), - op::MVNEpsMode::INSIDE_SQRT); - - std::shared_ptr opt_instance_norm_gamma_multiply = mvn; - if (instance_norm_gamma_present) - opt_instance_norm_gamma_multiply = std::make_shared(mvn, instance_norm_gamma_const); - - std::shared_ptr opt_instance_norm_beta_add = opt_instance_norm_gamma_multiply; - if (instance_norm_beta_present) - opt_instance_norm_beta_add = - std::make_shared(opt_instance_norm_gamma_multiply, instance_norm_beta_const); - - auto post_instance_norm_shape = std::make_shared(input); - auto post_instance_norm_reshape = - std::make_shared(opt_instance_norm_beta_add, post_instance_norm_shape, true); - auto group_norm_gamma_multiply = - std::make_shared(post_instance_norm_reshape, group_norm_gamma_const); - auto group_norm_beta_add = std::make_shared(group_norm_gamma_multiply, group_norm_beta_const); - - return std::make_shared(OutputVector{group_norm_beta_add}, ParameterVector{input}); - } -}; - -class GroupNormalizationFusionTransformationTestsF - : public GroupNormalizationFusionTestBase, - public TransformationTestsF, - public testing::WithParamInterface { -public: - static std::string getTestCaseName( - const testing::TestParamInfo& obj) { - const auto& params = obj.param; - + static std::string get_test_case_name(const GroupNormalizationFusionTransformationTestValues& params) { const auto& data_shape = std::get<0>(params); const auto& instance_norm_gamma_shape = std::get<1>(params); const auto& instance_norm_beta_shape = std::get<2>(params); @@ -154,26 +92,7 @@ class GroupNormalizationFusionTransformationTestsF return results.str(); } - void run() { - read_test_parameters(); - generate_weights_init_values(); - model = create_model(); - manager.register_pass(); - - if (positive_test) { - model_ref = create_ref_model(); - } else { - ASSERT_EQ(count_ops_of_type(model), 0); - test_skipped = true; - } - } - -protected: - bool positive_test; - - void read_test_parameters() override { - const auto& params = GetParam(); - + void read_test_parameters_impl(const GroupNormalizationFusionTransformationTestValues& params) { data_shape = std::get<0>(params); if (!data_shape.rank().is_static()) throw std::runtime_error("Rank of input tensor has to be static!"); @@ -217,7 +136,61 @@ class GroupNormalizationFusionTransformationTestsF } } - std::shared_ptr create_ref_model() { + void generate_weights_init_values() { + if (instance_norm_gamma_present) { + auto instanceNormGammaTensor = utils::create_and_fill_tensor(elem_type, + instance_norm_gamma_shape, + utils::InputGenerateData(1, 10, 1, 2)); + instance_norm_gamma_const = std::make_shared(instanceNormGammaTensor); + } + if (instance_norm_beta_present) { + auto instanceNormBetaTensor = utils::create_and_fill_tensor(elem_type, + instance_norm_beta_shape, + utils::InputGenerateData(1, 10, 1, 3)); + instance_norm_beta_const = std::make_shared(instanceNormBetaTensor); + } + + auto groupNormGammaTensor = + utils::create_and_fill_tensor(elem_type, group_norm_gamma_shape, utils::InputGenerateData(1, 10, 1, 1)); + group_norm_gamma_const = std::make_shared(groupNormGammaTensor); + + auto groupNormBetaTensor = + utils::create_and_fill_tensor(elem_type, group_norm_beta_shape, utils::InputGenerateData(1, 10, 1, 11)); + group_norm_beta_const = std::make_shared(groupNormBetaTensor); + } + + std::shared_ptr create_model() { + auto input = std::make_shared(elem_type, data_shape); + auto pre_mvn_shape_const = op::v0::Constant::create(element::i64, Shape{3}, {0, num_groups, -1}); + auto pre_mvn_reshape = std::make_shared(input, pre_mvn_shape_const, true); + + auto mvn_axes_const = op::v0::Constant::create(element::i64, Shape{1}, {2}); + auto mvn = std::make_shared(pre_mvn_reshape, + mvn_axes_const, + true, + static_cast(epsilon), + op::MVNEpsMode::INSIDE_SQRT); + + std::shared_ptr opt_instance_norm_gamma_multiply = mvn; + if (instance_norm_gamma_present) + opt_instance_norm_gamma_multiply = std::make_shared(mvn, instance_norm_gamma_const); + + std::shared_ptr opt_instance_norm_beta_add = opt_instance_norm_gamma_multiply; + if (instance_norm_beta_present) + opt_instance_norm_beta_add = + std::make_shared(opt_instance_norm_gamma_multiply, instance_norm_beta_const); + + auto post_instance_norm_shape = std::make_shared(input); + auto post_instance_norm_reshape = + std::make_shared(opt_instance_norm_beta_add, post_instance_norm_shape, true); + auto group_norm_gamma_multiply = + std::make_shared(post_instance_norm_reshape, group_norm_gamma_const); + auto group_norm_beta_add = std::make_shared(group_norm_gamma_multiply, group_norm_beta_const); + + return std::make_shared(OutputVector{group_norm_beta_add}, ParameterVector{input}); + } + + std::shared_ptr create_ref_model_impl() { auto input = std::make_shared(elem_type, data_shape); auto shape_1d_const = op::v0::Constant::create(element::i64, Shape{1}, {1}); @@ -272,6 +245,45 @@ class GroupNormalizationFusionTransformationTestsF return std::make_shared(OutputVector{group_norm}, ParameterVector{input}); } + + void run_transformation_test(const std::shared_ptr& model_under_test, + pass::Manager& test_manager, + std::shared_ptr& transformed_model, + std::shared_ptr& reference_model) { + transformed_model = model_under_test; + test_manager.register_pass(); + + if (positive_test) { + reference_model = create_ref_model_impl(); + } else { + // Verify no GroupNormalization exists before the pass runs. + // Leave reference_model unset so TransformationTestsF::TearDown + // clones the original model and verifies the pass leaves it unchanged. + ASSERT_EQ(count_ops_of_type(transformed_model), 0); + } + } +}; + +class GroupNormalizationFusionTransformationTestsF + : public GroupNormalizationFusionTestBase, + public TransformationTestsF, + public testing::WithParamInterface { +public: + static std::string getTestCaseName( + const testing::TestParamInfo& obj) { + return get_test_case_name(obj.param); + } + + void run() { + read_test_parameters(); + generate_weights_init_values(); + run_transformation_test(create_model(), manager, model, model_ref); + } + +protected: + void read_test_parameters() override { + read_test_parameters_impl(GetParam()); + } }; TEST_P(GroupNormalizationFusionTransformationTestsF, GroupNormalizationFusionTransformationTests) { @@ -551,5 +563,396 @@ INSTANTIATE_TEST_SUITE_P( GroupNormalizationFusionTransformationTestAdditionalValues(element::f8e8m0, false))), GroupNormalizationFusionTransformationTestsF::getTestCaseName); +// 4D InstanceNormalization MVN Pattern Tests +// Pattern: Input -> Reshape{N,G,-1,1} -> MVN(axes={2,3}) -> [Mul] -> [Add] -> Reshape(original) +// This pattern appears when GroupNormalization is decomposed via InstanceNormalization +// and reshaped directly to 4D with a trailing unit dimension before MVN. + +class GroupNormalizationFusion4DTestsF + : public GroupNormalizationFusionTestBase, + public TransformationTestsF, + public testing::WithParamInterface { +public: + static std::string getTestCaseName( + const testing::TestParamInfo& obj) { + return get_test_case_name(obj.param); + } + + void run() { + read_test_parameters(); + generate_weights_init_values(); + run_transformation_test(create_model_4d(), manager, model, model_ref); + } + +protected: + void read_test_parameters() override { + read_test_parameters_impl(GetParam()); + } + + // Creates the 4D InstanceNorm pattern model + std::shared_ptr create_model_4d() { + auto input = std::make_shared(elem_type, data_shape); + + // Reshape directly to 4D: {0, G, -1, 1} + auto pre_mvn_shape_const_4d = + op::v0::Constant::create(element::i64, Shape{4}, {0, num_groups, -1, 1}); + auto pre_mvn_reshape_4d = std::make_shared(input, pre_mvn_shape_const_4d, true); + + // MVN with axes={2,3} + auto mvn_axes_const = op::v0::Constant::create(element::i64, Shape{2}, {2, 3}); + auto mvn = std::make_shared(pre_mvn_reshape_4d, + mvn_axes_const, + true, + static_cast(epsilon), + op::MVNEpsMode::INSIDE_SQRT); + + std::shared_ptr opt_instance_norm_gamma_multiply = mvn; + if (instance_norm_gamma_present) { + opt_instance_norm_gamma_multiply = std::make_shared(mvn, instance_norm_gamma_const); + } + + std::shared_ptr opt_instance_norm_beta_add = opt_instance_norm_gamma_multiply; + if (instance_norm_beta_present) { + opt_instance_norm_beta_add = + std::make_shared(opt_instance_norm_gamma_multiply, instance_norm_beta_const); + } + + // Reshape back to original shape + auto post_instance_norm_shape = std::make_shared(input); + auto post_instance_norm_reshape = + std::make_shared(opt_instance_norm_beta_add, post_instance_norm_shape, true); + + auto group_norm_gamma_multiply = + std::make_shared(post_instance_norm_reshape, group_norm_gamma_const); + auto group_norm_beta_add = std::make_shared(group_norm_gamma_multiply, group_norm_beta_const); + + return std::make_shared(OutputVector{group_norm_beta_add}, ParameterVector{input}); + } +}; + +TEST_P(GroupNormalizationFusion4DTestsF, GroupNormalizationFusion4DTests) { + GroupNormalizationFusion4DTestsF::run(); +} + +const std::vector valid_vals_4d = { + std::make_tuple(PartialShape{1, 320}, Shape{}, Shape{}, Shape{320}, Shape{320}, 1, 1e-5), + std::make_tuple(PartialShape{1, 320}, Shape{1, 32, 1, 1}, Shape{1, 32, 1, 1}, Shape{320}, Shape{320}, 32, 1e-5), + std::make_tuple(PartialShape{1, 64, 8, 8}, + Shape{1, 8, 1, 1}, + Shape{1, 8, 1, 1}, + Shape{64, 1, 1}, + Shape{64, 1, 1}, + 8, + 1e-5), + std::make_tuple(PartialShape{2, 128, 16, 16}, Shape{}, Shape{}, Shape{128, 1, 1}, Shape{128, 1, 1}, 32, 1e-5), + std::make_tuple(PartialShape{1, 320, 64, 64}, Shape{}, Shape{}, Shape{320, 1, 1}, Shape{1, 320, 1, 1}, 32, 1e-5), + std::make_tuple(PartialShape{1, 512, 32, 32}, Shape{}, Shape{}, Shape{512, 1, 1}, Shape{1, 512, 1, 1}, 32, 1e-6), + std::make_tuple(PartialShape{4, 512, 64, 64}, + Shape{1, 32, 1, 1}, + Shape{1, 32, 1, 1}, + Shape{512, 1, 1}, + Shape{512, 1, 1}, + 32, + 1e-6), +}; + +INSTANTIATE_TEST_SUITE_P(GroupNormalizationFusion4DPositiveTests_f32, + GroupNormalizationFusion4DTestsF, + ValuesIn(expand_vals(valid_vals_4d, + GroupNormalizationFusionTransformationTestAdditionalValues(element::f32, + true))), + GroupNormalizationFusion4DTestsF::getTestCaseName); + +INSTANTIATE_TEST_SUITE_P(GroupNormalizationFusion4DPositiveTests_f16, + GroupNormalizationFusion4DTestsF, + ValuesIn(expand_vals(valid_vals_4d, + GroupNormalizationFusionTransformationTestAdditionalValues(element::f16, + true))), + GroupNormalizationFusion4DTestsF::getTestCaseName); + +INSTANTIATE_TEST_SUITE_P(GroupNormalizationFusion4DPositiveTests_bf16, + GroupNormalizationFusion4DTestsF, + ValuesIn(expand_vals(valid_vals_4d, + GroupNormalizationFusionTransformationTestAdditionalValues(element::bf16, + true))), + GroupNormalizationFusion4DTestsF::getTestCaseName); + +// 4D InstanceNorm pattern with concrete shape values (vs special markers like {0, G, -1, 1}) +// Some frameworks resolve shapes during optimization and emit concrete dimension values. +class GroupNormalizationFusion4DConcreteValuesTestsF + : public GroupNormalizationFusionTestBase, + public TransformationTestsF, + public testing::WithParamInterface { +public: + static std::string getTestCaseName( + const testing::TestParamInfo& obj) { + return get_test_case_name(obj.param); + } + + void run() { + read_test_parameters(); + generate_weights_init_values(); + run_transformation_test(create_model_4d_concrete(), manager, model, model_ref); + } + +protected: + void read_test_parameters() override { + read_test_parameters_impl(GetParam()); + } + + // Creates the 4D InstanceNorm pattern model with concrete shape values + std::shared_ptr create_model_4d_concrete() { + if (!data_shape.is_static()) + throw std::runtime_error("Data shape must be static for concrete values test!"); + + auto static_shape = data_shape.to_shape(); + auto input = std::make_shared(elem_type, static_shape); + + // Compute concrete shape values + int64_t batch = static_cast(static_shape[0]); + int64_t merged_spatial = 1; + for (size_t i = 1; i < static_shape.size(); i++) { + if (i == 1) { + const auto channels = static_cast(static_shape[i]); + if (channels % num_groups != 0) { + throw std::runtime_error( + "Channel dimension must be divisible by num_groups for concrete values test!"); + } + merged_spatial = channels / num_groups; + } else { + merged_spatial *= static_cast(static_shape[i]); + } + } + + // Reshape directly to 4D with concrete values: {batch, num_groups, merged_spatial, 1} + auto pre_mvn_shape_const_4d = + op::v0::Constant::create(element::i64, Shape{4}, {batch, num_groups, merged_spatial, 1}); + auto pre_mvn_reshape_4d = std::make_shared(input, pre_mvn_shape_const_4d, false); + + // MVN with axes={2,3} + auto mvn_axes_const = op::v0::Constant::create(element::i64, Shape{2}, {2, 3}); + auto mvn = std::make_shared(pre_mvn_reshape_4d, + mvn_axes_const, + true, + static_cast(epsilon), + op::MVNEpsMode::INSIDE_SQRT); + + std::shared_ptr opt_instance_norm_gamma_multiply = mvn; + if (instance_norm_gamma_present) { + opt_instance_norm_gamma_multiply = std::make_shared(mvn, instance_norm_gamma_const); + } + + std::shared_ptr opt_instance_norm_beta_add = opt_instance_norm_gamma_multiply; + if (instance_norm_beta_present) { + opt_instance_norm_beta_add = + std::make_shared(opt_instance_norm_gamma_multiply, instance_norm_beta_const); + } + + // Reshape back to original shape with concrete values + std::vector orig_shape_vals; + for (auto dim : static_shape) + orig_shape_vals.push_back(static_cast(dim)); + auto original_shape_const = + op::v0::Constant::create(element::i64, Shape{static_shape.size()}, orig_shape_vals); + auto post_instance_norm_reshape = + std::make_shared(opt_instance_norm_beta_add, original_shape_const, false); + + auto group_norm_gamma_multiply = + std::make_shared(post_instance_norm_reshape, group_norm_gamma_const); + auto group_norm_beta_add = std::make_shared(group_norm_gamma_multiply, group_norm_beta_const); + + return std::make_shared(OutputVector{group_norm_beta_add}, ParameterVector{input}); + } +}; + +TEST_P(GroupNormalizationFusion4DConcreteValuesTestsF, GroupNormalizationFusion4DConcreteValuesTests) { + GroupNormalizationFusion4DConcreteValuesTestsF::run(); +} + +const std::vector valid_vals_4d_concrete = { + std::make_tuple(PartialShape{2, 64, 8, 8}, Shape{}, Shape{}, Shape{64, 1, 1}, Shape{64, 1, 1}, 8, 1e-5), + std::make_tuple(PartialShape{1, 320, 64, 64}, Shape{}, Shape{}, Shape{320, 1, 1}, Shape{1, 320, 1, 1}, 32, 1e-5), + std::make_tuple(PartialShape{4, 512, 64, 64}, Shape{}, Shape{}, Shape{512, 1, 1}, Shape{1, 512, 1, 1}, 32, 1e-6), +}; + +INSTANTIATE_TEST_SUITE_P(GroupNormalizationFusion4DConcreteValuesPositiveTests_f32, + GroupNormalizationFusion4DConcreteValuesTestsF, + ValuesIn(expand_vals(valid_vals_4d_concrete, + GroupNormalizationFusionTransformationTestAdditionalValues(element::f32, + true))), + GroupNormalizationFusion4DConcreteValuesTestsF::getTestCaseName); + +INSTANTIATE_TEST_SUITE_P(GroupNormalizationFusion4DConcreteValuesPositiveTests_f16, + GroupNormalizationFusion4DConcreteValuesTestsF, + ValuesIn(expand_vals(valid_vals_4d_concrete, + GroupNormalizationFusionTransformationTestAdditionalValues(element::f16, + true))), + GroupNormalizationFusion4DConcreteValuesTestsF::getTestCaseName); + +INSTANTIATE_TEST_SUITE_P(GroupNormalizationFusion4DConcreteValuesPositiveTests_bf16, + GroupNormalizationFusion4DConcreteValuesTestsF, + ValuesIn(expand_vals(valid_vals_4d_concrete, + GroupNormalizationFusionTransformationTestAdditionalValues(element::bf16, + true))), + GroupNormalizationFusion4DConcreteValuesTestsF::getTestCaseName); + +// Helper: builds a 4D InstanceNorm-style pattern with customizable MVN axes and 4D trailing dimension. +// Returns a Model whose output is the final Add node (beta_add). +static std::shared_ptr build_4d_pattern_model(const PartialShape& data_shape, + int64_t num_groups, + const std::vector& mvn_axes_vals, + long long trailing_dim, + float epsilon = 1e-5f, + element::Type elem_type = element::f32) { + OPENVINO_ASSERT(data_shape[1].is_static() && data_shape[1].get_length() > 0, + "Channel dimension must be static and positive for build_4d_pattern_model"); + const auto num_channels = data_shape[1].get_max_length(); + + auto input = std::make_shared(elem_type, data_shape); + + // Reshape directly to 4D: {0, G, -1, trailing_dim} + auto pre_mvn_shape_4d = + op::v0::Constant::create(element::i64, Shape{4}, {0, num_groups, -1, trailing_dim}); + auto reshape_4d = std::make_shared(input, pre_mvn_shape_4d, true); + + // MVN with custom axes + auto mvn_axes = op::v0::Constant::create(element::i64, Shape{mvn_axes_vals.size()}, mvn_axes_vals); + auto mvn = std::make_shared(reshape_4d, mvn_axes, true, epsilon, op::MVNEpsMode::INSIDE_SQRT); + + // Reshape back to original shape + auto post_shape = std::make_shared(input); + auto post_reshape = std::make_shared(mvn, post_shape, true); + + // Group norm gamma/beta + auto gamma_const = op::v0::Constant::create(elem_type, + Shape{static_cast(num_channels), 1, 1}, + std::vector(num_channels, 1.0f)); + auto gamma_mul = std::make_shared(post_reshape, gamma_const); + auto beta_const = op::v0::Constant::create(elem_type, + Shape{1, static_cast(num_channels), 1, 1}, + std::vector(num_channels, 0.0f)); + auto beta_add = std::make_shared(gamma_mul, beta_const); + + return std::make_shared(OutputVector{beta_add}, ParameterVector{input}); +} + +// 4D pattern with negative MVN axes {-2, -1}; these should normalize to {2, 3} +// and still fuse successfully. +TEST(GroupNormalizationFusion4DAdditionalTests, NegativeMVNAxesNormalizedToPositive) { + auto model = build_4d_pattern_model(PartialShape{1, 320, 64, 64}, + /*num_groups=*/32, + /*mvn_axes_vals=*/{-2, -1}, + /*trailing_dim=*/1); + + ASSERT_EQ(count_ops_of_type(model), 0); + + pass::Manager m; + m.register_pass(); + OV_ASSERT_NO_THROW(m.run_passes(model)); + + ASSERT_EQ(count_ops_of_type(model), 1); +} + +// 4D pattern with wrong MVN axes: {1, 2} instead of {2, 3} +TEST(GroupNormalizationFusion4DNegativeEdgeCases, WrongMVNAxes) { + auto model = build_4d_pattern_model(PartialShape{1, 320, 64, 64}, + /*num_groups=*/32, + /*mvn_axes_vals=*/{1, 2}, + /*trailing_dim=*/1); + pass::Manager m; + m.register_pass(); + m.run_passes(model); + ASSERT_EQ(count_ops_of_type(model), 0); +} + +// 4D pattern with single MVN axis {2} — requires {2, 3} for 4D pattern +TEST(GroupNormalizationFusion4DNegativeEdgeCases, SingleMVNAxisWith4DReshape) { + auto model = build_4d_pattern_model(PartialShape{1, 320, 64, 64}, + /*num_groups=*/32, + /*mvn_axes_vals=*/{2}, + /*trailing_dim=*/1); + pass::Manager m; + m.register_pass(); + m.run_passes(model); + ASSERT_EQ(count_ops_of_type(model), 0); +} + +// 4D pattern with trailing dimension != 1 (e.g., 2) +TEST(GroupNormalizationFusion4DNegativeEdgeCases, TrailingDimNotOne) { + auto model = build_4d_pattern_model(PartialShape{1, 320, 64, 64}, + /*num_groups=*/32, + /*mvn_axes_vals=*/{2, 3}, + /*trailing_dim=*/2); + pass::Manager m; + m.register_pass(); + m.run_passes(model); + ASSERT_EQ(count_ops_of_type(model), 0); +} + +// 4D pattern with MVN axes {0, 1} — normalizing over wrong dimensions +TEST(GroupNormalizationFusion4DNegativeEdgeCases, MVNAxesOverBatchAndGroups) { + auto model = build_4d_pattern_model(PartialShape{1, 320, 64, 64}, + /*num_groups=*/32, + /*mvn_axes_vals=*/{0, 1}, + /*trailing_dim=*/1); + pass::Manager m; + m.register_pass(); + m.run_passes(model); + ASSERT_EQ(count_ops_of_type(model), 0); +} + +// 4D pattern with three MVN axes {1, 2, 3} — too many axes for 4D pattern +TEST(GroupNormalizationFusion4DNegativeEdgeCases, ThreeMVNAxes) { + auto model = build_4d_pattern_model(PartialShape{1, 320, 64, 64}, + /*num_groups=*/32, + /*mvn_axes_vals=*/{1, 2, 3}, + /*trailing_dim=*/1); + pass::Manager m; + m.register_pass(); + m.run_passes(model); + ASSERT_EQ(count_ops_of_type(model), 0); +} + +// 3D pattern with concrete (non -1) third dimension. +// Input {1, 320, 2, 4} with 32 groups → merged spatial = (320/32)*2*4 = 80 +// Reshape uses {1, 32, 80} instead of {0, 32, -1}. +TEST(GroupNormalizationFusion3DAdditionalTests, ConcreteSpatialDim) { + const PartialShape data_shape{1, 320, 2, 4}; + const int64_t num_groups = 32; + const int64_t num_channels = 320; + // (320 / 32) * 2 * 4 = 80 + const int64_t merged_spatial = (num_channels / num_groups) * 2 * 4; + + auto input = std::make_shared(element::f32, data_shape); + auto pre_mvn_shape_const = + op::v0::Constant::create(element::i64, Shape{3}, {1, num_groups, merged_spatial}); + auto pre_mvn_reshape = std::make_shared(input, pre_mvn_shape_const, false); + + auto mvn_axes_const = op::v0::Constant::create(element::i64, Shape{1}, {2}); + auto mvn = std::make_shared(pre_mvn_reshape, mvn_axes_const, true, 1e-5f, op::MVNEpsMode::INSIDE_SQRT); + + auto post_shape = std::make_shared(input); + auto post_reshape = std::make_shared(mvn, post_shape, true); + + auto gamma_const = op::v0::Constant::create(element::f32, + Shape{static_cast(num_channels), 1, 1}, + std::vector(num_channels, 1.0f)); + auto gamma_mul = std::make_shared(post_reshape, gamma_const); + auto beta_const = op::v0::Constant::create(element::f32, + Shape{1, static_cast(num_channels), 1, 1}, + std::vector(num_channels, 0.0f)); + auto beta_add = std::make_shared(gamma_mul, beta_const); + + auto model = std::make_shared(OutputVector{beta_add}, ParameterVector{input}); + + ASSERT_EQ(count_ops_of_type(model), 0); + + pass::Manager m; + m.register_pass(); + OV_ASSERT_NO_THROW(m.run_passes(model)); + + ASSERT_EQ(count_ops_of_type(model), 1); +} + } // namespace test } // namespace ov diff --git a/src/common/transformations/tests/common_optimizations/mark_precision_sensitive_shapeof_subgraphs_test.cpp b/src/common/transformations/tests/common_optimizations/mark_precision_sensitive_shapeof_subgraphs_test.cpp index 52c85082e95c..51feaf30e5ac 100644 --- a/src/common/transformations/tests/common_optimizations/mark_precision_sensitive_shapeof_subgraphs_test.cpp +++ b/src/common/transformations/tests/common_optimizations/mark_precision_sensitive_shapeof_subgraphs_test.cpp @@ -23,7 +23,7 @@ #include "openvino/opsets/opset10_decl.hpp" #include "openvino/pass/manager.hpp" #include "transformations/init_node_info.hpp" -#include "transformations/rt_info/disable_fp16_compression.hpp" +#include "transformations/rt_info/disable_precision_conversion.hpp" #include "transformations/utils/utils.hpp" using namespace testing; @@ -80,10 +80,10 @@ TEST_F(TransformationTestsF, MarkEntireShapeSubgraphs_whole_shape_subgraph_is_ma auto new_shape = std::make_shared(div, element::i64); auto reshape = std::make_shared(input_1, new_shape, false); - disable_fp16_compression(convert_to_float); - disable_fp16_compression(const_denominator); - disable_fp16_compression(div); - disable_fp16_compression(new_shape); + disable_conversion(convert_to_float, element::f16); + disable_conversion(const_denominator, element::f16); + disable_conversion(div, element::f16); + disable_conversion(new_shape, element::f16); model_ref = std::make_shared(OutputVector{reshape}, ParameterVector{input_1, input_2}); } } @@ -136,13 +136,13 @@ TEST_F(TransformationTestsF, MarkEntireShapeSubgraphs_whole_shape_subgraph_is_ma auto slice = std::make_shared(input_1, begin, concat_with_ends, begin_mask, end_mask); auto result = std::make_shared(slice); - disable_fp16_compression(gather_1); - disable_fp16_compression(convert_to_float); - disable_fp16_compression(const_denominator); - disable_fp16_compression(div); - disable_fp16_compression(new_dim_size); - disable_fp16_compression(const_ends); - disable_fp16_compression(concat_with_ends); + disable_conversion(gather_1, element::f16); + disable_conversion(convert_to_float, element::f16); + disable_conversion(const_denominator, element::f16); + disable_conversion(div, element::f16); + disable_conversion(new_dim_size, element::f16); + disable_conversion(const_ends, element::f16); + disable_conversion(concat_with_ends, element::f16); model_ref = std::make_shared(OutputVector{result}, ParameterVector{input_1}); } } @@ -228,15 +228,15 @@ TEST_F(TransformationTestsF, MarkEntireShapeSubgraphs_whole_shape_subgraph_is_ma auto result_1 = std::make_shared(add_1); auto result_2 = std::make_shared(interpolate); - disable_fp16_compression(gather_1); - disable_fp16_compression(convert_1); - disable_fp16_compression(convert_2); - disable_fp16_compression(const_2); - disable_fp16_compression(div_1); - disable_fp16_compression(const_3); - disable_fp16_compression(concat); - disable_fp16_compression(mul_1); - disable_fp16_compression(convert_3); + disable_conversion(gather_1, element::f16); + disable_conversion(convert_1, element::f16); + disable_conversion(convert_2, element::f16); + disable_conversion(const_2, element::f16); + disable_conversion(div_1, element::f16); + disable_conversion(const_3, element::f16); + disable_conversion(concat, element::f16); + disable_conversion(mul_1, element::f16); + disable_conversion(convert_3, element::f16); model_ref = std::make_shared(OutputVector{result_1, result_2}, ParameterVector{input_1, input_2}); } @@ -271,7 +271,7 @@ TEST_F(TransformationTestsF, MarkConstantsInShapeSubgraphs_only_consts_marked_1) auto new_shape = std::make_shared(div, element::i64); auto reshape = std::make_shared(input_1, new_shape, false); - disable_fp16_compression(const_denominator); + disable_conversion(const_denominator, element::f16); model_ref = std::make_shared(OutputVector{reshape}, ParameterVector{input_1, input_2}); } @@ -326,8 +326,8 @@ TEST_F(TransformationTestsF, MarkConstantsInShapeSubgraphs_only_consts_marked_2) auto slice = std::make_shared(input_1, begin, concat_with_ends, begin_mask, end_mask); auto result = std::make_shared(slice); - disable_fp16_compression(const_denominator); - disable_fp16_compression(const_ends); + disable_conversion(const_denominator, element::f16); + disable_conversion(const_ends, element::f16); model_ref = std::make_shared(OutputVector{result}, ParameterVector{input_1}); } } diff --git a/src/common/transformations/tests/common_optimizations/mark_rope_input_to_keep_in_mixed_precision_test.cpp b/src/common/transformations/tests/common_optimizations/mark_rope_input_to_keep_in_mixed_precision_test.cpp index 1db0c22847e1..dfc806336bd0 100644 --- a/src/common/transformations/tests/common_optimizations/mark_rope_input_to_keep_in_mixed_precision_test.cpp +++ b/src/common/transformations/tests/common_optimizations/mark_rope_input_to_keep_in_mixed_precision_test.cpp @@ -12,7 +12,7 @@ #include "openvino/opsets/opset1_decl.hpp" #include "openvino/pass/manager.hpp" #include "ov_ops/rotary_positional_embeddings.hpp" -#include "transformations/rt_info/disable_fp16_compression.hpp" +#include "transformations/rt_info/disable_precision_conversion.hpp" namespace v0 = ov::op::v0; TEST_F(TransformationTestsF, MarkRopeInputsToKeepInMixedPrecisionTest) { @@ -63,11 +63,11 @@ TEST_F(TransformationTestsF, MarkRopeInputsToKeepInMixedPrecisionTest) { auto concat = std::make_shared(ov::NodeVector{transpose, transpose}, -1); auto cos = std::make_shared(concat); auto sin = std::make_shared(concat); - disable_fp16_compression(matmul); - disable_fp16_compression(transpose); - disable_fp16_compression(concat); - disable_fp16_compression(cos); - disable_fp16_compression(sin); + disable_conversion(matmul, ov::element::f16); + disable_conversion(transpose, ov::element::f16); + disable_conversion(concat, ov::element::f16); + disable_conversion(cos, ov::element::f16); + disable_conversion(sin, ov::element::f16); ov::op::internal::RoPE::Config config; auto rope = std::make_shared(ov::OutputVector{input->output(0), cos->output(0), sin->output(0)}, diff --git a/src/common/transformations/tests/common_optimizations/mark_subgraph_to_keep_in_mixed_precision_test.cpp b/src/common/transformations/tests/common_optimizations/mark_subgraph_to_keep_in_mixed_precision_test.cpp index c3cf24cf7088..21738464a449 100644 --- a/src/common/transformations/tests/common_optimizations/mark_subgraph_to_keep_in_mixed_precision_test.cpp +++ b/src/common/transformations/tests/common_optimizations/mark_subgraph_to_keep_in_mixed_precision_test.cpp @@ -30,7 +30,7 @@ #include "openvino/pass/manager.hpp" #include "transformations/convert_precision.hpp" #include "transformations/fp16_compression/mark_subgraphs_to_keep_in_mixed_precision.hpp" -#include "transformations/rt_info/disable_fp16_compression.hpp" +#include "transformations/rt_info/disable_precision_conversion.hpp" using namespace testing; using namespace ov; @@ -72,11 +72,11 @@ TEST(TransformationTests, keep_precission_sensitive_fp32_1) { auto matmul_1 = make_shared(mul_1, input_2); // marking nodes to be kept in fp32 for mixed precision - disable_fp16_compression(exp_1); - disable_fp16_compression(reduce_sum_1); - disable_fp16_compression(mul_1); - disable_fp16_compression(factor_const); - disable_fp16_compression(factor_const_decompressed); + disable_conversion(exp_1, element::f16); + disable_conversion(reduce_sum_1, element::f16); + disable_conversion(mul_1, element::f16); + disable_conversion(factor_const, element::f16); + disable_conversion(factor_const_decompressed, element::f16); model_ref = make_shared(OutputVector{matmul_1}, ParameterVector{input_1, input_2}); } @@ -124,11 +124,11 @@ TEST(TransformationTests, keep_precission_sensitive_fp32_with_reducemean) { auto matmul_1 = make_shared(mul_1, input_2); // marking nodes to be kept in fp32 for mixed precision - disable_fp16_compression(exp_1); - disable_fp16_compression(reduce_mean_1); - disable_fp16_compression(mul_1); - disable_fp16_compression(factor_const); - disable_fp16_compression(factor_const_decompressed); + disable_conversion(exp_1, element::f16); + disable_conversion(reduce_mean_1, element::f16); + disable_conversion(mul_1, element::f16); + disable_conversion(factor_const, element::f16); + disable_conversion(factor_const_decompressed, element::f16); model_ref = make_shared(OutputVector{matmul_1}, ParameterVector{input_1, input_2}); } @@ -214,12 +214,12 @@ TEST(TransformationTests, keep_precission_sensitive_fp32_2) { auto matmul_1 = make_shared(mul_1, input_2); // marking nodes to be kept in fp32 for mixed precision - disable_fp16_compression(exp_1); - disable_fp16_compression(reduce_sum_1); - disable_fp16_compression(mul_1); - disable_fp16_compression(unsqueeze_1); - disable_fp16_compression(factor_const); - disable_fp16_compression(factor_const_decompressed); + disable_conversion(exp_1, ov::element::f16); + disable_conversion(reduce_sum_1, ov::element::f16); + disable_conversion(mul_1, ov::element::f16); + disable_conversion(unsqueeze_1, ov::element::f16); + disable_conversion(factor_const, ov::element::f16); + disable_conversion(factor_const_decompressed, ov::element::f16); model_ref = make_shared(OutputVector{matmul_1}, ParameterVector{input_1, input_2}); } @@ -274,13 +274,13 @@ TEST(TransformationTests, keep_precission_sensitive_fp32_3) { auto matmul_1 = make_shared(mul_1, input_2); // marking nodes to be kept in fp32 for mixed precision - disable_fp16_compression(exp_1); - disable_fp16_compression(reduce_sum_1); - disable_fp16_compression(mul_1); - disable_fp16_compression(add_1); - disable_fp16_compression(addition_const); - disable_fp16_compression(factor_const); - disable_fp16_compression(factor_const_decompressed); + disable_conversion(exp_1, ov::element::f16); + disable_conversion(reduce_sum_1, ov::element::f16); + disable_conversion(mul_1, ov::element::f16); + disable_conversion(add_1, ov::element::f16); + disable_conversion(addition_const, ov::element::f16); + disable_conversion(factor_const, ov::element::f16); + disable_conversion(factor_const_decompressed, ov::element::f16); model_ref = make_shared(OutputVector{matmul_1}, ParameterVector{input_1, input_2}); } @@ -395,34 +395,34 @@ TEST(TransformationTests, keep_precission_sensitive_fp32_7) { auto matmul_1 = make_shared(div_1, input_4, false, true); // marking nodes to be kept in fp32 for mixed precision - disable_fp16_compression(mul_1); - disable_fp16_compression(mul_2); - disable_fp16_compression(mul_3); - disable_fp16_compression(mul_4); - disable_fp16_compression(mul_5); - disable_fp16_compression(unsqueeze_1); - disable_fp16_compression(unsqueeze_2); - disable_fp16_compression(unsqueeze_3); - disable_fp16_compression(reduce_sum_1); - disable_fp16_compression(reduce_sum_2); - disable_fp16_compression(reduce_sum_3); - disable_fp16_compression(reduce_sum_4); - disable_fp16_compression(exp_1); - disable_fp16_compression(exp_2); - disable_fp16_compression(tile); - disable_fp16_compression(eps_const); - disable_fp16_compression(add_1); - disable_fp16_compression(broadcast); - disable_fp16_compression(div_1); - - disable_fp16_compression(factor_1); - disable_fp16_compression(factor_2); - - disable_fp16_compression(broadcast_to_shape); - disable_fp16_compression(tile_shape); - disable_fp16_compression(const_unsqueeze_1); - disable_fp16_compression(const_unsqueeze_2); - disable_fp16_compression(const_unsqueeze_3); + disable_conversion(mul_1, ov::element::f16); + disable_conversion(mul_2, ov::element::f16); + disable_conversion(mul_3, ov::element::f16); + disable_conversion(mul_4, ov::element::f16); + disable_conversion(mul_5, ov::element::f16); + disable_conversion(unsqueeze_1, ov::element::f16); + disable_conversion(unsqueeze_2, ov::element::f16); + disable_conversion(unsqueeze_3, ov::element::f16); + disable_conversion(reduce_sum_1, ov::element::f16); + disable_conversion(reduce_sum_2, ov::element::f16); + disable_conversion(reduce_sum_3, ov::element::f16); + disable_conversion(reduce_sum_4, ov::element::f16); + disable_conversion(exp_1, ov::element::f16); + disable_conversion(exp_2, ov::element::f16); + disable_conversion(tile, ov::element::f16); + disable_conversion(eps_const, ov::element::f16); + disable_conversion(add_1, ov::element::f16); + disable_conversion(broadcast, ov::element::f16); + disable_conversion(div_1, ov::element::f16); + + disable_conversion(factor_1, ov::element::f16); + disable_conversion(factor_2, ov::element::f16); + + disable_conversion(broadcast_to_shape, ov::element::f16); + disable_conversion(tile_shape, ov::element::f16); + disable_conversion(const_unsqueeze_1, ov::element::f16); + disable_conversion(const_unsqueeze_2, ov::element::f16); + disable_conversion(const_unsqueeze_3, ov::element::f16); model_ref = make_shared(OutputVector{matmul_1}, ParameterVector{input_1, input_2, input_3, input_4}); } @@ -459,9 +459,9 @@ TEST(TransformationTests, DivisionByZeroMinimalPattern) { auto eps_const = Constant::create(element::f32, Shape{1}, {eps_value}); auto add = std::make_shared(input_2, eps_const); auto divide = std::make_shared(input_1, add); - disable_fp16_compression(divide); - disable_fp16_compression(eps_const); - disable_fp16_compression(add); + disable_conversion(divide, ov::element::f16); + disable_conversion(eps_const, ov::element::f16); + disable_conversion(add, ov::element::f16); model_ref = std::make_shared(OutputVector{divide}, ParameterVector{input_1, input_2}); } @@ -501,10 +501,10 @@ TEST(TransformationTests, DivisionByZeroEpsWithConvert) { auto convert_eps = std::make_shared(eps_const, element::f32); auto add = std::make_shared(input_2, convert_eps); auto divide = std::make_shared(input_1, add); - disable_fp16_compression(divide); - disable_fp16_compression(eps_const); - disable_fp16_compression(convert_eps); - disable_fp16_compression(add); + disable_conversion(divide, ov::element::f16); + disable_conversion(eps_const, ov::element::f16); + disable_conversion(convert_eps, ov::element::f16); + disable_conversion(add, ov::element::f16); model_ref = std::make_shared(OutputVector{divide}, ParameterVector{input_1, input_2}); } @@ -547,11 +547,11 @@ TEST(TransformationTests, PowWithNegativeExponent) { auto mul = std::make_shared(input_1, pow); // marking nodes to be kept in fp32 for mixed precision - disable_fp16_compression(eps_const); - disable_fp16_compression(add); - disable_fp16_compression(pow_exp_const); - disable_fp16_compression(pow); - disable_fp16_compression(mul); + disable_conversion(eps_const, ov::element::f16); + disable_conversion(add, ov::element::f16); + disable_conversion(pow_exp_const, ov::element::f16); + disable_conversion(pow, ov::element::f16); + disable_conversion(mul, ov::element::f16); model_ref = std::make_shared(OutputVector{mul}, ParameterVector{input_1, input_2}); } @@ -651,13 +651,13 @@ TEST(TransformationTests, DivisionByZeroInL2NormWithSqrtAndWithMax) { auto divide = std::make_shared(input, sqrt); // marking nodes to be kept in fp32 for mixed precision - disable_fp16_compression(exp); - disable_fp16_compression(pow); - disable_fp16_compression(reduce_sum); - disable_fp16_compression(max); - disable_fp16_compression(eps_const); - disable_fp16_compression(sqrt); - disable_fp16_compression(divide); + disable_conversion(exp, ov::element::f16); + disable_conversion(pow, ov::element::f16); + disable_conversion(reduce_sum, ov::element::f16); + disable_conversion(max, ov::element::f16); + disable_conversion(eps_const, ov::element::f16); + disable_conversion(sqrt, ov::element::f16); + disable_conversion(divide, ov::element::f16); model_ref = std::make_shared(OutputVector{divide}, ParameterVector{input}); } @@ -705,14 +705,14 @@ TEST(TransformationTests, DivisionByZeroMaxAndEpsWithConvert) { auto divide = std::make_shared(input, sqrt); // marking nodes to be kept in fp32 for mixed precision - disable_fp16_compression(exp); - disable_fp16_compression(pow); - disable_fp16_compression(reduce_sum); - disable_fp16_compression(max); - disable_fp16_compression(eps_const); - disable_fp16_compression(convert_eps); - disable_fp16_compression(sqrt); - disable_fp16_compression(divide); + disable_conversion(exp, ov::element::f16); + disable_conversion(pow, ov::element::f16); + disable_conversion(reduce_sum, ov::element::f16); + disable_conversion(max, ov::element::f16); + disable_conversion(eps_const, ov::element::f16); + disable_conversion(convert_eps, ov::element::f16); + disable_conversion(sqrt, ov::element::f16); + disable_conversion(divide, ov::element::f16); model_ref = std::make_shared(OutputVector{divide}, ParameterVector{input}); } @@ -758,13 +758,13 @@ TEST(TransformationTests, DivisionByZeroInL2NormWithSqrtAndWithAdd) { auto divide = std::make_shared(input, sqrt); // marking nodes to be kept in fp32 for mixed precision - disable_fp16_compression(exp); - disable_fp16_compression(pow); - disable_fp16_compression(sqrt); - disable_fp16_compression(reduce_sum); - disable_fp16_compression(eps_const); - disable_fp16_compression(add); - disable_fp16_compression(divide); + disable_conversion(exp, ov::element::f16); + disable_conversion(pow, ov::element::f16); + disable_conversion(sqrt, ov::element::f16); + disable_conversion(reduce_sum, ov::element::f16); + disable_conversion(eps_const, ov::element::f16); + disable_conversion(add, ov::element::f16); + disable_conversion(divide, ov::element::f16); model_ref = std::make_shared(OutputVector{divide}, ParameterVector{input}); } @@ -811,11 +811,11 @@ TEST(TransformationTests, MarkReduceOpExpToKeepInMixedPrecision_with_reducesum) auto matmul_1 = make_shared(mul_1, input_2); model_ref = make_shared(OutputVector{matmul_1}, ParameterVector{input_1, input_2}); - disable_fp16_compression(exp_1); - disable_fp16_compression(mul_1); - disable_fp16_compression(reduce_sum_1); - disable_fp16_compression(factor_const_decompressed); - disable_fp16_compression(factor_const); + disable_conversion(exp_1, ov::element::f16); + disable_conversion(mul_1, ov::element::f16); + disable_conversion(reduce_sum_1, ov::element::f16); + disable_conversion(factor_const_decompressed, ov::element::f16); + disable_conversion(factor_const, ov::element::f16); } const FunctionsComparator func_comparator = @@ -861,11 +861,11 @@ TEST(TransformationTests, MarkReduceOpExpToKeepInMixedPrecision_with_reducemean) auto matmul_1 = make_shared(mul_1, input_2); model_ref = make_shared(OutputVector{matmul_1}, ParameterVector{input_1, input_2}); - disable_fp16_compression(exp_1); - disable_fp16_compression(mul_1); - disable_fp16_compression(reduce_mean_1); - disable_fp16_compression(factor_const_decompressed); - disable_fp16_compression(factor_const); + disable_conversion(exp_1, ov::element::f16); + disable_conversion(mul_1, ov::element::f16); + disable_conversion(reduce_mean_1, ov::element::f16); + disable_conversion(factor_const_decompressed, ov::element::f16); + disable_conversion(factor_const, ov::element::f16); } const FunctionsComparator func_comparator = @@ -946,12 +946,12 @@ TEST(TransformationTests, MarkReduceOpExpToKeepInMixedPrecision_reducesum_exp_th auto matmul_1 = make_shared(mul_1, input_2); model_ref = make_shared(OutputVector{matmul_1}, ParameterVector{input_1, input_2}); - disable_fp16_compression(exp_1); - disable_fp16_compression(mul_1); - disable_fp16_compression(reduce_sum_1); - disable_fp16_compression(factor_const_decompressed); - disable_fp16_compression(factor_const); - disable_fp16_compression(unsqueeze_1); + disable_conversion(exp_1, ov::element::f16); + disable_conversion(mul_1, ov::element::f16); + disable_conversion(reduce_sum_1, ov::element::f16); + disable_conversion(factor_const_decompressed, ov::element::f16); + disable_conversion(factor_const, ov::element::f16); + disable_conversion(unsqueeze_1, ov::element::f16); } const FunctionsComparator func_comparator = @@ -986,9 +986,9 @@ TEST(TransformationTests, MarkDivWithEps) { auto eps_const = Constant::create(element::f32, Shape{1}, {eps_value}); auto add = std::make_shared(input_2, eps_const); auto divide = std::make_shared(input_1, add); - disable_fp16_compression(divide); - disable_fp16_compression(add); - disable_fp16_compression(eps_const); + disable_conversion(divide, ov::element::f16); + disable_conversion(add, ov::element::f16); + disable_conversion(eps_const, ov::element::f16); model_ref = std::make_shared(OutputVector{divide}, ParameterVector{input_1, input_2}); } @@ -1030,11 +1030,11 @@ TEST(TransformationTests, MarkDivWithEpsToKeepInMixedPrecision_PowWithNegativeEx auto pow_exp_const = Constant::create(element::f32, Shape{1}, {-1.77}); auto pow = std::make_shared(add, pow_exp_const); auto mul = std::make_shared(input_1, pow); - disable_fp16_compression(mul); - disable_fp16_compression(eps_const); - disable_fp16_compression(add); - disable_fp16_compression(pow_exp_const); - disable_fp16_compression(pow); + disable_conversion(mul, ov::element::f16); + disable_conversion(eps_const, ov::element::f16); + disable_conversion(add, ov::element::f16); + disable_conversion(pow_exp_const, ov::element::f16); + disable_conversion(pow, ov::element::f16); model_ref = std::make_shared(OutputVector{mul}, ParameterVector{input_1, input_2}); } @@ -1161,16 +1161,16 @@ TEST(TransformationTests, MarkFloatingPointRange) { auto multiply = make_shared(convert, multiply_const); // marking nodes to be kept in fp32 for mixed precision - disable_fp16_compression(begin); - disable_fp16_compression(end); - disable_fp16_compression(step); - disable_fp16_compression(range_1); - disable_fp16_compression(range_2); - disable_fp16_compression(convert_1); - disable_fp16_compression(convert_2); - disable_fp16_compression(unsqueeze); - disable_fp16_compression(greater); - disable_fp16_compression(convert); + disable_conversion(begin, ov::element::f16); + disable_conversion(end, ov::element::f16); + disable_conversion(step, ov::element::f16); + disable_conversion(range_1, ov::element::f16); + disable_conversion(range_2, ov::element::f16); + disable_conversion(convert_1, ov::element::f16); + disable_conversion(convert_2, ov::element::f16); + disable_conversion(unsqueeze, ov::element::f16); + disable_conversion(greater, ov::element::f16); + disable_conversion(convert, ov::element::f16); model_ref = make_shared(OutputVector{convert}, ParameterVector{end}); } @@ -1214,14 +1214,14 @@ TEST(TransformationTests, MarkDivWithEpsToKeepInMixedPrecision_InL2NormWithSqrtA auto max = std::make_shared(reduce_sum, eps_const); auto sqrt = std::make_shared(max); auto divide = std::make_shared(input, sqrt); - disable_fp16_compression(divide); + disable_conversion(divide, ov::element::f16); - disable_fp16_compression(exp); - disable_fp16_compression(pow); - disable_fp16_compression(reduce_sum); - disable_fp16_compression(eps_const); - disable_fp16_compression(max); - disable_fp16_compression(sqrt); + disable_conversion(exp, ov::element::f16); + disable_conversion(pow, ov::element::f16); + disable_conversion(reduce_sum, ov::element::f16); + disable_conversion(eps_const, ov::element::f16); + disable_conversion(max, ov::element::f16); + disable_conversion(sqrt, ov::element::f16); model_ref = std::make_shared(OutputVector{divide}, ParameterVector{input}); } @@ -1266,14 +1266,14 @@ TEST(TransformationTests, MarkDivWithEpsToKeepInMixedPrecision_InL2NormWithSqrtA auto add = std::make_shared(reduce_sum, eps_const); auto sqrt = std::make_shared(add); auto divide = std::make_shared(input, sqrt); - disable_fp16_compression(divide); - - disable_fp16_compression(exp); - disable_fp16_compression(pow); - disable_fp16_compression(reduce_sum); - disable_fp16_compression(eps_const); - disable_fp16_compression(add); - disable_fp16_compression(sqrt); + disable_conversion(divide, ov::element::f16); + + disable_conversion(exp, ov::element::f16); + disable_conversion(pow, ov::element::f16); + disable_conversion(reduce_sum, ov::element::f16); + disable_conversion(eps_const, ov::element::f16); + disable_conversion(add, ov::element::f16); + disable_conversion(sqrt, ov::element::f16); model_ref = std::make_shared(OutputVector{divide}, ParameterVector{input}); } diff --git a/src/common/transformations/tests/common_optimizations/matmul_multiply_fusion.cpp b/src/common/transformations/tests/common_optimizations/matmul_multiply_fusion.cpp index dd381ba0b79e..ba312cdebd89 100644 --- a/src/common/transformations/tests/common_optimizations/matmul_multiply_fusion.cpp +++ b/src/common/transformations/tests/common_optimizations/matmul_multiply_fusion.cpp @@ -115,6 +115,41 @@ TEST_F(TransformationTestsF, MatMulMultiplyFusionNonSingleConsumer) { comparator.enable(FunctionsComparator::CmpValues::CONST_VALUES); } +TEST_F(TransformationTestsF, MatMulMultiplyFusionBlockedForFP16DynamicWeightsAmplifyingScale) { + auto data = std::make_shared(element::f16, Shape{2, 3}); + auto weights = std::make_shared(element::f16, Shape{2, 3}); + auto matmul = std::make_shared(data, weights, true, false); + auto mul_const = opset8::Constant::create(element::f16, Shape{1, 1}, {2}); + auto mul = std::make_shared(matmul, mul_const); + model = std::make_shared(OutputVector{mul}, ParameterVector{data, weights}); + + manager.register_pass(); + comparator.enable(FunctionsComparator::CmpValues::CONST_VALUES); +} + +TEST_F(TransformationTestsF, MatMulMultiplyFusionAllowedForFP16DynamicWeightsReducingScale) { + { + auto data = std::make_shared(element::f16, Shape{2, 3}); + auto weights = std::make_shared(element::f16, Shape{2, 3}); + auto matmul = std::make_shared(data, weights, false, true); + auto mul_const = opset8::Constant::create(element::f16, Shape{1, 1}, {0.5f}); + auto mul = std::make_shared(matmul, mul_const); + model = std::make_shared(OutputVector{mul}, ParameterVector{data, weights}); + + manager.register_pass(); + } + + { + auto data = std::make_shared(element::f16, Shape{2, 3}); + auto weights = std::make_shared(element::f16, Shape{2, 3}); + auto mul_const = opset8::Constant::create(element::f16, Shape{1, 1}, {0.5f}); + auto mul = std::make_shared(weights, mul_const); + auto matmul = std::make_shared(data, mul, false, true); + model_ref = std::make_shared(OutputVector{matmul}, ParameterVector{data, weights}); + } + comparator.enable(FunctionsComparator::CmpValues::CONST_VALUES); +} + using MatMulMultiplyFusionParams = std::tuple; class MatMulMultiplyFusionDynamicShapes : public testing::WithParamInterface, diff --git a/src/common/transformations/tests/common_optimizations/move_eltwise_up_data_movement_test.cpp b/src/common/transformations/tests/common_optimizations/move_eltwise_up_data_movement_test.cpp index 7090efd6c61d..2d3e5a8dec48 100644 --- a/src/common/transformations/tests/common_optimizations/move_eltwise_up_data_movement_test.cpp +++ b/src/common/transformations/tests/common_optimizations/move_eltwise_up_data_movement_test.cpp @@ -12,16 +12,18 @@ #include "common_test_utils/ov_test_utils.hpp" #include "openvino/op/add.hpp" #include "openvino/op/clamp.hpp" +#include "openvino/op/constant.hpp" #include "openvino/op/fake_quantize.hpp" #include "openvino/op/matmul.hpp" #include "openvino/op/multiply.hpp" +#include "openvino/op/parameter.hpp" #include "openvino/op/relu.hpp" #include "openvino/op/reshape.hpp" #include "openvino/op/sigmoid.hpp" #include "openvino/op/squeeze.hpp" +#include "openvino/op/strided_slice.hpp" #include "openvino/op/transpose.hpp" #include "openvino/op/unsqueeze.hpp" -#include "openvino/opsets/opset8_decl.hpp" #include "ov_ops/type_relaxed.hpp" #include "transformations/init_node_info.hpp" @@ -36,31 +38,29 @@ TEST_F(MoveEltwiseUpThroughDataMovTest, SingleUnaryEltwise) { const std::vector input_order = {3, 2, 1, 0}; const int64_t unsqueeze_axis = 2; { - auto input = std::make_shared(ov::element::f32, shape); + auto input = std::make_shared(ov::element::f32, shape); - auto transpose_const = - ov::opset8::Constant::create(ov::element::i64, ov::Shape{input_order.size()}, input_order); - auto transpose = std::make_shared(input, transpose_const); + auto transpose_const = v0::Constant::create(ov::element::i64, ov::Shape{input_order.size()}, input_order); + auto transpose = std::make_shared(input, transpose_const); - auto unsqueeze_const = ov::opset8::Constant::create(ov::element::i64, ov::Shape{}, {unsqueeze_axis}); - auto unsqueeze = std::make_shared(transpose, unsqueeze_const); + auto unsqueeze_const = v0::Constant::create(ov::element::i64, ov::Shape{}, {unsqueeze_axis}); + auto unsqueeze = std::make_shared(transpose, unsqueeze_const); - auto sigmoid = std::make_shared(unsqueeze); + auto sigmoid = std::make_shared(unsqueeze); model = std::make_shared(ov::OutputVector{sigmoid}, ov::ParameterVector{input}); manager.register_pass(); } { - auto input = std::make_shared(ov::element::f32, shape); + auto input = std::make_shared(ov::element::f32, shape); - auto sigmoid = std::make_shared(input); + auto sigmoid = std::make_shared(input); - auto transpose_const = - ov::opset8::Constant::create(ov::element::i64, ov::Shape{input_order.size()}, input_order); - auto transpose = std::make_shared(sigmoid, transpose_const); + auto transpose_const = v0::Constant::create(ov::element::i64, ov::Shape{input_order.size()}, input_order); + auto transpose = std::make_shared(sigmoid, transpose_const); - auto unsqueeze_const = ov::opset8::Constant::create(ov::element::i64, ov::Shape{}, {unsqueeze_axis}); - auto unsqueeze = std::make_shared(transpose, unsqueeze_const); + auto unsqueeze_const = v0::Constant::create(ov::element::i64, ov::Shape{}, {unsqueeze_axis}); + auto unsqueeze = std::make_shared(transpose, unsqueeze_const); model_ref = std::make_shared(ov::OutputVector{unsqueeze}, ov::ParameterVector{input}); } @@ -70,29 +70,27 @@ TEST_F(MoveEltwiseUpThroughDataMovTest, TypeRelaxedEltwise) { const ov::Shape shape{1, 3, 224, 224}; const std::vector input_order = {3, 2, 1, 0}; { - auto input = std::make_shared(ov::element::f32, shape); - auto intermediate_op = std::make_shared(input, 0, 6); + auto input = std::make_shared(ov::element::f32, shape); + auto intermediate_op = std::make_shared(input, 0, 6); - auto transpose_const = - ov::opset8::Constant::create(ov::element::i64, ov::Shape{input_order.size()}, input_order); - auto transpose = std::make_shared(intermediate_op, transpose_const); + auto transpose_const = v0::Constant::create(ov::element::i64, ov::Shape{input_order.size()}, input_order); + auto transpose = std::make_shared(intermediate_op, transpose_const); - auto mul_const = ov::opset8::Constant::create(ov::element::f32, {}, {2.f}); - auto multiply = std::make_shared>(transpose, mul_const); + auto mul_const = v0::Constant::create(ov::element::f32, {}, {2.f}); + auto multiply = std::make_shared>(transpose, mul_const); model = std::make_shared(ov::OutputVector{multiply}, ov::ParameterVector{input}); manager.register_pass(); } { - auto input = std::make_shared(ov::element::f32, shape); - auto intermediate_op = std::make_shared(input, 0, 6); + auto input = std::make_shared(ov::element::f32, shape); + auto intermediate_op = std::make_shared(input, 0, 6); - auto mul_const = ov::opset8::Constant::create(ov::element::f32, {}, {2.f}); - auto multiply = std::make_shared>(intermediate_op, mul_const); + auto mul_const = v0::Constant::create(ov::element::f32, {}, {2.f}); + auto multiply = std::make_shared>(intermediate_op, mul_const); - auto transpose_const = - ov::opset8::Constant::create(ov::element::i64, ov::Shape{input_order.size()}, input_order); - auto transpose = std::make_shared(multiply, transpose_const); + auto transpose_const = v0::Constant::create(ov::element::i64, ov::Shape{input_order.size()}, input_order); + auto transpose = std::make_shared(multiply, transpose_const); model_ref = std::make_shared(ov::OutputVector{transpose}, ov::ParameterVector{input}); } @@ -103,41 +101,39 @@ TEST_F(MoveEltwiseUpThroughDataMovTest, EltwiseSequence) { const std::vector input_order = {1, 2, 0, 3}; const int64_t unsqueeze_axis = 1; { - auto input_left = std::make_shared(ov::element::f32, shape); - auto input_right = std::make_shared(ov::element::f32, shape); + auto input_left = std::make_shared(ov::element::f32, shape); + auto input_right = std::make_shared(ov::element::f32, shape); - auto matmul = std::make_shared(input_left, input_right); + auto matmul = std::make_shared(input_left, input_right); - auto transpose_const = - ov::opset8::Constant::create(ov::element::i64, ov::Shape{input_order.size()}, input_order); - auto transpose = std::make_shared(matmul, transpose_const); + auto transpose_const = v0::Constant::create(ov::element::i64, ov::Shape{input_order.size()}, input_order); + auto transpose = std::make_shared(matmul, transpose_const); - auto relu = std::make_shared(transpose); + auto relu = std::make_shared(transpose); - auto unsqueeze_const = ov::opset8::Constant::create(ov::element::i64, ov::Shape{}, {unsqueeze_axis}); - auto unsqueeze = std::make_shared(relu, unsqueeze_const); + auto unsqueeze_const = v0::Constant::create(ov::element::i64, ov::Shape{}, {unsqueeze_axis}); + auto unsqueeze = std::make_shared(relu, unsqueeze_const); - auto sigmoid = std::make_shared(unsqueeze); + auto sigmoid = std::make_shared(unsqueeze); model = std::make_shared(ov::OutputVector{sigmoid}, ov::ParameterVector{input_left, input_right}); manager.register_pass(); } { - auto input_left = std::make_shared(ov::element::f32, shape); - auto input_right = std::make_shared(ov::element::f32, shape); + auto input_left = std::make_shared(ov::element::f32, shape); + auto input_right = std::make_shared(ov::element::f32, shape); - auto matmul = std::make_shared(input_left, input_right); + auto matmul = std::make_shared(input_left, input_right); - auto relu = std::make_shared(matmul); + auto relu = std::make_shared(matmul); - auto sigmoid = std::make_shared(relu); + auto sigmoid = std::make_shared(relu); - auto transpose_const = - ov::opset8::Constant::create(ov::element::i64, ov::Shape{input_order.size()}, input_order); - auto transpose = std::make_shared(sigmoid, transpose_const); + auto transpose_const = v0::Constant::create(ov::element::i64, ov::Shape{input_order.size()}, input_order); + auto transpose = std::make_shared(sigmoid, transpose_const); - auto unsqueeze_const = ov::opset8::Constant::create(ov::element::i64, ov::Shape{}, {unsqueeze_axis}); - auto unsqueeze = std::make_shared(transpose, unsqueeze_const); + auto unsqueeze_const = v0::Constant::create(ov::element::i64, ov::Shape{}, {unsqueeze_axis}); + auto unsqueeze = std::make_shared(transpose, unsqueeze_const); model_ref = std::make_shared(ov::OutputVector{unsqueeze}, ov::ParameterVector{input_left, input_right}); @@ -150,20 +146,20 @@ TEST_F(MoveEltwiseUpThroughDataMovTest, DataMovementTwoConsumers) { const std::vector input_order = {1, 2, 0, 3}; const int64_t unsqueeze_axis = 1; - auto input_left = std::make_shared(ov::element::f32, shape); - auto input_right = std::make_shared(ov::element::f32, shape); + auto input_left = std::make_shared(ov::element::f32, shape); + auto input_right = std::make_shared(ov::element::f32, shape); - auto matmul = std::make_shared(input_left, input_right); + auto matmul = std::make_shared(input_left, input_right); - auto transpose_const = ov::opset8::Constant::create(ov::element::i64, ov::Shape{input_order.size()}, input_order); - auto transpose = std::make_shared(matmul, transpose_const); + auto transpose_const = v0::Constant::create(ov::element::i64, ov::Shape{input_order.size()}, input_order); + auto transpose = std::make_shared(matmul, transpose_const); - auto unsqueeze_const = ov::opset8::Constant::create(ov::element::i64, ov::Shape{}, {unsqueeze_axis}); - auto unsqueeze = std::make_shared(transpose, unsqueeze_const); + auto unsqueeze_const = v0::Constant::create(ov::element::i64, ov::Shape{}, {unsqueeze_axis}); + auto unsqueeze = std::make_shared(transpose, unsqueeze_const); - auto sigmoid = std::make_shared(unsqueeze); + auto sigmoid = std::make_shared(unsqueeze); - auto relu = std::make_shared(transpose); + auto relu = std::make_shared(transpose); model = std::make_shared(ov::OutputVector{sigmoid, relu}, ov::ParameterVector{input_left, input_right}); manager.register_pass(); @@ -175,35 +171,29 @@ TEST_F(MoveEltwiseUpThroughDataMovTest, SingleBinaryEltwiseWithScalarOnSecondBra const int64_t unsqueeze_axis = 2; const float scalar_value = 0.5f; { - auto input = std::make_shared(ov::element::f32, shape); + auto input = std::make_shared(ov::element::f32, shape); - auto transpose_const = - ov::opset8::Constant::create(ov::element::i64, ov::Shape{input_order.size()}, input_order); - auto transpose = std::make_shared(input, transpose_const); + auto transpose_const = v0::Constant::create(ov::element::i64, ov::Shape{input_order.size()}, input_order); + auto transpose = std::make_shared(input, transpose_const); - auto unsqueeze_const = ov::opset8::Constant::create(ov::element::i64, ov::Shape{}, {unsqueeze_axis}); - auto unsqueeze = std::make_shared(transpose, unsqueeze_const); + auto unsqueeze_const = v0::Constant::create(ov::element::i64, ov::Shape{}, {unsqueeze_axis}); + auto unsqueeze = std::make_shared(transpose, unsqueeze_const); - auto add = - std::make_shared(unsqueeze, - ov::opset8::Constant::create(ov::element::f32, {}, {scalar_value})); + auto add = std::make_shared(unsqueeze, v0::Constant::create(ov::element::f32, {}, {scalar_value})); manager.register_pass(); model = std::make_shared(ov::OutputVector{add}, ov::ParameterVector{input}); } { - auto input = std::make_shared(ov::element::f32, shape); + auto input = std::make_shared(ov::element::f32, shape); - auto add = - std::make_shared(input, - ov::opset8::Constant::create(ov::element::f32, {}, {scalar_value})); + auto add = std::make_shared(input, v0::Constant::create(ov::element::f32, {}, {scalar_value})); - auto transpose_const = - ov::opset8::Constant::create(ov::element::i64, ov::Shape{input_order.size()}, input_order); - auto transpose = std::make_shared(add, transpose_const); + auto transpose_const = v0::Constant::create(ov::element::i64, ov::Shape{input_order.size()}, input_order); + auto transpose = std::make_shared(add, transpose_const); - auto unsqueeze_const = ov::opset8::Constant::create(ov::element::i64, ov::Shape{}, {unsqueeze_axis}); - auto unsqueeze = std::make_shared(transpose, unsqueeze_const); + auto unsqueeze_const = v0::Constant::create(ov::element::i64, ov::Shape{}, {unsqueeze_axis}); + auto unsqueeze = std::make_shared(transpose, unsqueeze_const); model_ref = std::make_shared(ov::OutputVector{unsqueeze}, ov::ParameterVector{input}); } @@ -215,27 +205,24 @@ TEST_F(MoveEltwiseUpThroughDataMovTest, SingleEltwiseWith5ScalarOnSecondBranch) const int64_t unsqueeze_axis = 2; const float scalar_value = 0.5f; { - auto input = std::make_shared(ov::element::f32, shape); + auto input = std::make_shared(ov::element::f32, shape); - auto unsqueeze_const = ov::opset8::Constant::create(ov::element::i64, ov::Shape{}, {unsqueeze_axis}); - auto unsqueeze = std::make_shared(input, unsqueeze_const); + auto unsqueeze_const = v0::Constant::create(ov::element::i64, ov::Shape{}, {unsqueeze_axis}); + auto unsqueeze = std::make_shared(input, unsqueeze_const); - auto add = std::make_shared( - unsqueeze, - ov::opset8::Constant::create(ov::element::f32, {1, 1, 1, 1, 1}, {scalar_value})); + auto add = std::make_shared(unsqueeze, + v0::Constant::create(ov::element::f32, {1, 1, 1, 1, 1}, {scalar_value})); manager.register_pass(); model = std::make_shared(ov::OutputVector{add}, ov::ParameterVector{input}); } { - auto input = std::make_shared(ov::element::f32, shape); + auto input = std::make_shared(ov::element::f32, shape); - auto add = - std::make_shared(input, - ov::opset8::Constant::create(ov::element::f32, {}, {scalar_value})); + auto add = std::make_shared(input, v0::Constant::create(ov::element::f32, {}, {scalar_value})); - auto unsqueeze_const = ov::opset8::Constant::create(ov::element::i64, ov::Shape{}, {unsqueeze_axis}); - auto unsqueeze = std::make_shared(add, unsqueeze_const); + auto unsqueeze_const = v0::Constant::create(ov::element::i64, ov::Shape{}, {unsqueeze_axis}); + auto unsqueeze = std::make_shared(add, unsqueeze_const); model_ref = std::make_shared(ov::OutputVector{unsqueeze}, ov::ParameterVector{input}); } @@ -246,16 +233,16 @@ TEST_F(MoveEltwiseUpThroughDataMovTest, SingleBinaryEltwiseWithNotScalarOnSecond const std::vector input_order = {3, 2, 1, 0}; const int64_t unsqueeze_axis = 2; - auto input = std::make_shared(ov::element::f32, shape); + auto input = std::make_shared(ov::element::f32, shape); - auto transpose_const = ov::opset8::Constant::create(ov::element::i64, ov::Shape{input_order.size()}, input_order); - auto transpose = std::make_shared(input, transpose_const); + auto transpose_const = v0::Constant::create(ov::element::i64, ov::Shape{input_order.size()}, input_order); + auto transpose = std::make_shared(input, transpose_const); - auto unsqueeze_const = ov::opset8::Constant::create(ov::element::i64, ov::Shape{}, {unsqueeze_axis}); - auto unsqueeze = std::make_shared(transpose, unsqueeze_const); + auto unsqueeze_const = v0::Constant::create(ov::element::i64, ov::Shape{}, {unsqueeze_axis}); + auto unsqueeze = std::make_shared(transpose, unsqueeze_const); - auto add_scalar = ov::opset8::Constant::create(ov::element::f32, {1, 1, 1, 3}, {0.5, 0.2, 0.3}); - auto add = std::make_shared(unsqueeze, add_scalar); + auto add_scalar = v0::Constant::create(ov::element::f32, {1, 1, 1, 3}, {0.5, 0.2, 0.3}); + auto add = std::make_shared(unsqueeze, add_scalar); model = std::make_shared(ov::OutputVector{add}, ov::ParameterVector{input}); manager.register_pass(); @@ -265,24 +252,24 @@ TEST_F(MoveEltwiseUpThroughDataMovTest, SingleUnaryEltwiseDynamicShape) { const std::vector input_order = {3, 2, 1, 0}; const int64_t unsqueeze_axis = 2; { - auto input = std::make_shared(ov::element::f32, ov::PartialShape::dynamic(3)); + auto input = std::make_shared(ov::element::f32, ov::PartialShape::dynamic(3)); - auto unsqueeze_const = ov::opset8::Constant::create(ov::element::i64, ov::Shape{}, {unsqueeze_axis}); - auto unsqueeze = std::make_shared(input, unsqueeze_const); + auto unsqueeze_const = v0::Constant::create(ov::element::i64, ov::Shape{}, {unsqueeze_axis}); + auto unsqueeze = std::make_shared(input, unsqueeze_const); - auto sigmoid = std::make_shared(unsqueeze); + auto sigmoid = std::make_shared(unsqueeze); model = std::make_shared(ov::OutputVector{sigmoid}, ov::ParameterVector{input}); manager.register_pass(); } { - auto input = std::make_shared(ov::element::f32, ov::PartialShape::dynamic(3)); + auto input = std::make_shared(ov::element::f32, ov::PartialShape::dynamic(3)); - auto sigmoid = std::make_shared(input); + auto sigmoid = std::make_shared(input); - auto unsqueeze_const = ov::opset8::Constant::create(ov::element::i64, ov::Shape{}, {unsqueeze_axis}); - auto unsqueeze = std::make_shared(sigmoid, unsqueeze_const); + auto unsqueeze_const = v0::Constant::create(ov::element::i64, ov::Shape{}, {unsqueeze_axis}); + auto unsqueeze = std::make_shared(sigmoid, unsqueeze_const); model_ref = std::make_shared(ov::OutputVector{unsqueeze}, ov::ParameterVector{input}); } @@ -292,12 +279,11 @@ TEST_F(MoveEltwiseUpThroughDataMovTest, SingleUnaryEltwiseDynamicRank) { const std::vector input_order = {3, 2, 1, 0}; const int64_t unsqueeze_axis = 2; - auto input = - std::make_shared(ov::element::f32, ov::PartialShape::dynamic(ov::Rank::dynamic())); + auto input = std::make_shared(ov::element::f32, ov::PartialShape::dynamic(ov::Rank::dynamic())); - auto unsqueeze_const = ov::opset8::Constant::create(ov::element::i64, ov::Shape{}, {unsqueeze_axis}); - auto unsqueeze = std::make_shared(input, unsqueeze_const); - auto sigmoid = std::make_shared(unsqueeze); + auto unsqueeze_const = v0::Constant::create(ov::element::i64, ov::Shape{}, {unsqueeze_axis}); + auto unsqueeze = std::make_shared(input, unsqueeze_const); + auto sigmoid = std::make_shared(unsqueeze); model = std::make_shared(ov::OutputVector{sigmoid}, ov::ParameterVector{input}); manager.register_pass(); } @@ -306,35 +292,33 @@ TEST_F(MoveEltwiseUpThroughDataMovTest, TransposeFakeQuantize) { const ov::Shape shape{1, 128, 12, 64}; const std::vector input_order = {0, 2, 1, 3}; { - auto input = std::make_shared(ov::element::f32, shape); - - auto transpose_const = - ov::opset8::Constant::create(ov::element::i64, ov::Shape{input_order.size()}, input_order); - auto transpose = std::make_shared(input, transpose_const); - auto fakequantize = std::make_shared( - transpose, - ov::opset8::Constant::create(ov::element::f32, ov::Shape{}, {-8.5}), - ov::opset8::Constant::create(ov::element::f32, ov::Shape{}, {8.5}), - ov::opset8::Constant::create(ov::element::f32, ov::Shape{}, {-128}), - ov::opset8::Constant::create(ov::element::f32, ov::Shape{}, {127}), - 255); + auto input = std::make_shared(ov::element::f32, shape); + + auto transpose_const = v0::Constant::create(ov::element::i64, ov::Shape{input_order.size()}, input_order); + auto transpose = std::make_shared(input, transpose_const); + auto fakequantize = + std::make_shared(transpose, + v0::Constant::create(ov::element::f32, ov::Shape{}, {-8.5}), + v0::Constant::create(ov::element::f32, ov::Shape{}, {8.5}), + v0::Constant::create(ov::element::f32, ov::Shape{}, {-128}), + v0::Constant::create(ov::element::f32, ov::Shape{}, {127}), + 255); model = std::make_shared(ov::OutputVector{fakequantize}, ov::ParameterVector{input}); manager.register_pass(); } { - auto input = std::make_shared(ov::element::f32, shape); - - auto fakequantize = std::make_shared( - input, - ov::opset8::Constant::create(ov::element::f32, ov::Shape{}, {-8.5}), - ov::opset8::Constant::create(ov::element::f32, ov::Shape{}, {8.5}), - ov::opset8::Constant::create(ov::element::f32, ov::Shape{}, {-128}), - ov::opset8::Constant::create(ov::element::f32, ov::Shape{}, {127}), - 255); - auto transpose_const = - ov::opset8::Constant::create(ov::element::i64, ov::Shape{input_order.size()}, input_order); - auto transpose = std::make_shared(fakequantize, transpose_const); + auto input = std::make_shared(ov::element::f32, shape); + + auto fakequantize = + std::make_shared(input, + v0::Constant::create(ov::element::f32, ov::Shape{}, {-8.5}), + v0::Constant::create(ov::element::f32, ov::Shape{}, {8.5}), + v0::Constant::create(ov::element::f32, ov::Shape{}, {-128}), + v0::Constant::create(ov::element::f32, ov::Shape{}, {127}), + 255); + auto transpose_const = v0::Constant::create(ov::element::i64, ov::Shape{input_order.size()}, input_order); + auto transpose = std::make_shared(fakequantize, transpose_const); model_ref = std::make_shared(ov::OutputVector{transpose}, ov::ParameterVector{input}); } @@ -344,18 +328,17 @@ TEST_F(MoveEltwiseUpThroughDataMovTest, TransposeFakeQuantizePerChannel) { const ov::Shape shape{1, 12, 3, 64}; const std::vector input_order = {0, 2, 1, 3}; { - auto input = std::make_shared(ov::element::f32, shape); + auto input = std::make_shared(ov::element::f32, shape); - auto transpose_const = - ov::opset8::Constant::create(ov::element::i64, ov::Shape{input_order.size()}, input_order); - auto transpose = std::make_shared(input, transpose_const); + auto transpose_const = v0::Constant::create(ov::element::i64, ov::Shape{input_order.size()}, input_order); + auto transpose = std::make_shared(input, transpose_const); - auto fakequantize = std::make_shared( + auto fakequantize = std::make_shared( transpose, - ov::opset8::Constant::create(ov::element::f32, ov::Shape{1, 3, 1, 1}, {-8.5, -7.5, -10.}), - ov::opset8::Constant::create(ov::element::f32, ov::Shape{1, 3, 1, 1}, {8.5, 7.5, 10.}), - ov::opset8::Constant::create(ov::element::f32, ov::Shape{}, {-128}), - ov::opset8::Constant::create(ov::element::f32, ov::Shape{}, {127}), + v0::Constant::create(ov::element::f32, ov::Shape{1, 3, 1, 1}, {-8.5, -7.5, -10.}), + v0::Constant::create(ov::element::f32, ov::Shape{1, 3, 1, 1}, {8.5, 7.5, 10.}), + v0::Constant::create(ov::element::f32, ov::Shape{}, {-128}), + v0::Constant::create(ov::element::f32, ov::Shape{}, {127}), 255); model = std::make_shared(ov::OutputVector{fakequantize}, ov::ParameterVector{input}); @@ -366,25 +349,25 @@ TEST_F(MoveEltwiseUpThroughDataMovTest, TransposeFakeQuantizePerChannel) { TEST_F(MoveEltwiseUpThroughDataMovTest, PerChannelEltwiseUnsqueeze) { const ov::Shape shape{10, 20}; { - auto input = std::make_shared(ov::element::f32, shape); + auto input = std::make_shared(ov::element::f32, shape); - auto unsqueeze_const = ov::opset8::Constant::create(ov::element::i64, ov::Shape{2}, {2, 3}); // {10, 20, 1, 1} - auto unsqueeze = std::make_shared(input, unsqueeze_const); + auto unsqueeze_const = v0::Constant::create(ov::element::i64, ov::Shape{2}, {2, 3}); // {10, 20, 1, 1} + auto unsqueeze = std::make_shared(input, unsqueeze_const); - auto per_channel_const = ov::opset8::Constant::create(ov::element::f32, {1, 20, 1, 1}, {0.5}); - auto add = std::make_shared(unsqueeze, per_channel_const); + auto per_channel_const = v0::Constant::create(ov::element::f32, {1, 20, 1, 1}, {0.5}); + auto add = std::make_shared(unsqueeze, per_channel_const); model = std::make_shared(ov::OutputVector{add}, ov::ParameterVector{input}); manager.register_pass(); } { - auto input = std::make_shared(ov::element::f32, shape); + auto input = std::make_shared(ov::element::f32, shape); - auto per_channel_const = ov::opset8::Constant::create(ov::element::f32, {1, 20}, {0.5}); - auto add = std::make_shared(input, per_channel_const); + auto per_channel_const = v0::Constant::create(ov::element::f32, {1, 20}, {0.5}); + auto add = std::make_shared(input, per_channel_const); - auto unsqueeze_const = ov::opset8::Constant::create(ov::element::i64, ov::Shape{2}, {2, 3}); // {10, 20, 1, 1} - auto unsqueeze = std::make_shared(add, unsqueeze_const); + auto unsqueeze_const = v0::Constant::create(ov::element::i64, ov::Shape{2}, {2, 3}); // {10, 20, 1, 1} + auto unsqueeze = std::make_shared(add, unsqueeze_const); model_ref = std::make_shared(ov::OutputVector{unsqueeze}, ov::ParameterVector{input}); } @@ -393,25 +376,25 @@ TEST_F(MoveEltwiseUpThroughDataMovTest, PerChannelEltwiseUnsqueeze) { TEST_F(MoveEltwiseUpThroughDataMovTest, PerChannelEltwiseUnsqueezeReverseInOrder) { const ov::Shape shape{10, 20}; { - auto input = std::make_shared(ov::element::f32, shape); + auto input = std::make_shared(ov::element::f32, shape); - auto unsqueeze_const = ov::opset8::Constant::create(ov::element::i64, ov::Shape{2}, {2, 3}); // {10, 20, 1, 1} - auto unsqueeze = std::make_shared(input, unsqueeze_const); + auto unsqueeze_const = v0::Constant::create(ov::element::i64, ov::Shape{2}, {2, 3}); // {10, 20, 1, 1} + auto unsqueeze = std::make_shared(input, unsqueeze_const); - auto per_channel_const = ov::opset8::Constant::create(ov::element::f32, {1, 20, 1, 1}, {0.5}); - auto add = std::make_shared(per_channel_const, unsqueeze); + auto per_channel_const = v0::Constant::create(ov::element::f32, {1, 20, 1, 1}, {0.5}); + auto add = std::make_shared(per_channel_const, unsqueeze); model = std::make_shared(ov::OutputVector{add}, ov::ParameterVector{input}); manager.register_pass(); } { - auto input = std::make_shared(ov::element::f32, shape); + auto input = std::make_shared(ov::element::f32, shape); - auto per_channel_const = ov::opset8::Constant::create(ov::element::f32, {1, 20}, {0.5}); - auto add = std::make_shared(per_channel_const, input); + auto per_channel_const = v0::Constant::create(ov::element::f32, {1, 20}, {0.5}); + auto add = std::make_shared(per_channel_const, input); - auto unsqueeze_const = ov::opset8::Constant::create(ov::element::i64, ov::Shape{2}, {2, 3}); // {10, 20, 1, 1} - auto unsqueeze = std::make_shared(add, unsqueeze_const); + auto unsqueeze_const = v0::Constant::create(ov::element::i64, ov::Shape{2}, {2, 3}); // {10, 20, 1, 1} + auto unsqueeze = std::make_shared(add, unsqueeze_const); model_ref = std::make_shared(ov::OutputVector{unsqueeze}, ov::ParameterVector{input}); } @@ -420,24 +403,24 @@ TEST_F(MoveEltwiseUpThroughDataMovTest, PerChannelEltwiseUnsqueezeReverseInOrder TEST_F(MoveEltwiseUpThroughDataMovTest, PerChannelEltwiseSqueeze) { const ov::Shape shape{10, 20, 1, 1}; { - auto input = std::make_shared(ov::element::f32, shape); + auto input = std::make_shared(ov::element::f32, shape); - auto squeeze_const = ov::opset8::Constant::create(ov::element::i64, ov::Shape{}, {2}); // {10, 20, 1} + auto squeeze_const = v0::Constant::create(ov::element::i64, ov::Shape{}, {2}); // {10, 20, 1} auto squeeze = std::make_shared(input, squeeze_const); - auto per_channel_const = ov::opset8::Constant::create(ov::element::f32, {10, 1, 1}, {0.5}); - auto add = std::make_shared(squeeze, per_channel_const); + auto per_channel_const = v0::Constant::create(ov::element::f32, {10, 1, 1}, {0.5}); + auto add = std::make_shared(squeeze, per_channel_const); model = std::make_shared(ov::OutputVector{add}, ov::ParameterVector{input}); manager.register_pass(); } { - auto input = std::make_shared(ov::element::f32, shape); + auto input = std::make_shared(ov::element::f32, shape); - auto per_channel_const = ov::opset8::Constant::create(ov::element::f32, {10, 1, 1, 1}, {0.5}); - auto add = std::make_shared(input, per_channel_const); + auto per_channel_const = v0::Constant::create(ov::element::f32, {10, 1, 1, 1}, {0.5}); + auto add = std::make_shared(input, per_channel_const); - auto squeeze_const = ov::opset8::Constant::create(ov::element::i64, ov::Shape{}, {2}); // {10, 20, 1} + auto squeeze_const = v0::Constant::create(ov::element::i64, ov::Shape{}, {2}); // {10, 20, 1} auto squeeze = std::make_shared(add, squeeze_const); model_ref = std::make_shared(ov::OutputVector{squeeze}, ov::ParameterVector{input}); @@ -447,13 +430,13 @@ TEST_F(MoveEltwiseUpThroughDataMovTest, PerChannelEltwiseSqueeze) { TEST_F(MoveEltwiseUpThroughDataMovTest, PerChannelEltwiseSqueezeIllegal_1) { // Only last dimensions can be updated by squeeze/unsqueeze op, while this subgraph removes dimension in the middle const ov::Shape shape{10, 1, 20}; - auto input = std::make_shared(ov::element::f32, shape); + auto input = std::make_shared(ov::element::f32, shape); - auto squeeze_const = ov::opset8::Constant::create(ov::element::i64, ov::Shape{}, {1}); // {10, 20} + auto squeeze_const = v0::Constant::create(ov::element::i64, ov::Shape{}, {1}); // {10, 20} auto squeeze = std::make_shared(input, squeeze_const); - auto per_channel_const = ov::opset8::Constant::create(ov::element::f32, {1, 1, 20}, {0.5}); - auto add = std::make_shared(squeeze, per_channel_const); + auto per_channel_const = v0::Constant::create(ov::element::f32, {1, 1, 20}, {0.5}); + auto add = std::make_shared(squeeze, per_channel_const); model = std::make_shared(ov::OutputVector{add}, ov::ParameterVector{input}); manager.register_pass(); @@ -462,18 +445,18 @@ TEST_F(MoveEltwiseUpThroughDataMovTest, PerChannelEltwiseSqueezeIllegal_1) { TEST_F(MoveEltwiseUpThroughDataMovTest, PerChannelEltwiseSqueezeIllegal_2) { const ov::Shape shape{10, 20, 1, 1}; // Data movement op with multiple consumers is not applicable - auto input = std::make_shared(ov::element::f32, shape); + auto input = std::make_shared(ov::element::f32, shape); - auto squeeze_const = ov::opset8::Constant::create(ov::element::i64, ov::Shape{}, {2}); // {10, 20, 1} + auto squeeze_const = v0::Constant::create(ov::element::i64, ov::Shape{}, {2}); // {10, 20, 1} auto squeeze = std::make_shared(input, squeeze_const); - auto per_channel_const1 = ov::opset8::Constant::create(ov::element::f32, {10, 1, 1}, {0.5}); - auto add1 = std::make_shared(squeeze, per_channel_const1); + auto per_channel_const1 = v0::Constant::create(ov::element::f32, {10, 1, 1}, {0.5}); + auto add1 = std::make_shared(squeeze, per_channel_const1); - auto per_channel_const2 = ov::opset8::Constant::create(ov::element::f32, {10, 1, 1}, {0.5}); - auto add2 = std::make_shared(squeeze, per_channel_const2); + auto per_channel_const2 = v0::Constant::create(ov::element::f32, {10, 1, 1}, {0.5}); + auto add2 = std::make_shared(squeeze, per_channel_const2); - auto add3 = std::make_shared(add1, add2); + auto add3 = std::make_shared(add1, add2); model = std::make_shared(ov::OutputVector{add3}, ov::ParameterVector{input}); manager.register_pass(); @@ -483,22 +466,22 @@ TEST_F(MoveEltwiseUpThroughDataMovTest, PerChannelReshapeMultiply) { const ov::Shape shape{1, 3, 20}; const std::vector target_shape = {1, 3, 4, 5}; { - auto input = std::make_shared(ov::element::f32, shape); + auto input = std::make_shared(ov::element::f32, shape); auto reshape_constant = std::make_shared(ov::element::i64, ov::Shape{target_shape.size()}, target_shape); auto reshape = std::make_shared(input, reshape_constant, false); - auto per_channel_const = ov::opset8::Constant::create(ov::element::f32, {1, 3, 1, 1}, {0.5}); + auto per_channel_const = v0::Constant::create(ov::element::f32, {1, 3, 1, 1}, {0.5}); auto multiply = std::make_shared(reshape, per_channel_const); model = std::make_shared(ov::OutputVector{multiply}, ov::ParameterVector{input}); manager.register_pass(); } { - auto input = std::make_shared(ov::element::f32, shape); + auto input = std::make_shared(ov::element::f32, shape); - auto per_channel_const = ov::opset8::Constant::create(ov::element::f32, {1, 3, 1}, {0.5}); + auto per_channel_const = v0::Constant::create(ov::element::f32, {1, 3, 1}, {0.5}); auto multiply = std::make_shared(input, per_channel_const); auto reshape_constant = @@ -508,3 +491,61 @@ TEST_F(MoveEltwiseUpThroughDataMovTest, PerChannelReshapeMultiply) { model_ref = std::make_shared(ov::OutputVector{reshape}, ov::ParameterVector{input}); } } + +TEST_F(MoveEltwiseUpThroughDataMovTest, SharedConstantNotReshapedForOtherConsumers) { + const ov::Shape data_shape{1, 3, 224, 224}; + const std::vector input_order = {3, 2, 1, 0}; + const int64_t unsqueeze_axis = 2; + { + auto input = std::make_shared(ov::element::i64, data_shape); + + auto transpose_const = v0::Constant::create(ov::element::i64, ov::Shape{input_order.size()}, input_order); + auto transpose = std::make_shared(input, transpose_const); + + auto unsqueeze_const = v0::Constant::create(ov::element::i64, ov::Shape{}, {unsqueeze_axis}); + auto unsqueeze = std::make_shared(transpose, unsqueeze_const); + + auto shared_const = v0::Constant::create(ov::element::i64, ov::Shape{1}, {2}); + + auto multiply = std::make_shared(unsqueeze, shared_const); + + auto slice_input = std::make_shared(ov::element::f32, ov::Shape{4}); + auto begin = v0::Constant::create(ov::element::i64, ov::Shape{1}, {0}); + auto stride = v0::Constant::create(ov::element::i64, ov::Shape{1}, {1}); + auto slice = std::make_shared(slice_input, + begin, + shared_const, + stride, + std::vector{0}, + std::vector{0}); + + model = std::make_shared(ov::OutputVector{multiply, slice}, ov::ParameterVector{input, slice_input}); + manager.register_pass(); + } + { + auto input = std::make_shared(ov::element::i64, data_shape); + + auto shared_const = v0::Constant::create(ov::element::i64, ov::Shape{1}, {2}); + auto scalar_const = v0::Constant::create(ov::element::i64, ov::Shape{}, {2}); + auto multiply = std::make_shared(input, scalar_const); + + auto transpose_const = v0::Constant::create(ov::element::i64, ov::Shape{input_order.size()}, input_order); + auto transpose = std::make_shared(multiply, transpose_const); + + auto unsqueeze_const = v0::Constant::create(ov::element::i64, ov::Shape{}, {unsqueeze_axis}); + auto unsqueeze = std::make_shared(transpose, unsqueeze_const); + + auto slice_input = std::make_shared(ov::element::f32, ov::Shape{4}); + auto begin = v0::Constant::create(ov::element::i64, ov::Shape{1}, {0}); + auto stride = v0::Constant::create(ov::element::i64, ov::Shape{1}, {1}); + auto slice = std::make_shared(slice_input, + begin, + shared_const, + stride, + std::vector{0}, + std::vector{0}); + + model_ref = + std::make_shared(ov::OutputVector{unsqueeze, slice}, ov::ParameterVector{input, slice_input}); + } +} diff --git a/src/common/transformations/tests/common_optimizations/nop_elimination.cpp b/src/common/transformations/tests/common_optimizations/nop_elimination.cpp index 2201a73f43e9..e299438e3041 100644 --- a/src/common/transformations/tests/common_optimizations/nop_elimination.cpp +++ b/src/common/transformations/tests/common_optimizations/nop_elimination.cpp @@ -16,6 +16,7 @@ #include "common_test_utils/ov_test_utils.hpp" #include "common_test_utils/test_common.hpp" #include "openvino/core/model.hpp" +#include "openvino/op/identity.hpp" #include "openvino/op/ops.hpp" #include "openvino/pass/constant_folding.hpp" #include "openvino/pass/manager.hpp" @@ -2153,3 +2154,125 @@ TEST_F(TransformationTestsF, ScatterNDUpdates15Elimination) { model_ref = std::make_shared(OutputVector{result}, ParameterVector{data, indices, updates}); } } + +TEST_F(TransformationTestsF, EliminateIdentityConvert_convert_multiple_consumers_node_after) { + { + auto param = std::make_shared(element::f16, PartialShape{1, 55, 1}); + auto convert_to_f32 = std::make_shared(param, element::f32); + auto identity = std::make_shared(convert_to_f32); + auto convert_to_f16 = std::make_shared(identity, element::f16); + auto relu = std::make_shared(convert_to_f16); + + auto constant = op::v0::Constant::create(element::f32, Shape{1, 1, 1}, {1.0f}); + auto concat = std::make_shared(OutputVector{constant, convert_to_f32}, 1); + + auto result1 = std::make_shared(relu); + auto result2 = std::make_shared(concat); + + model = std::make_shared(OutputVector{result1, result2}, ParameterVector{param}); + } + { + auto param = std::make_shared(element::f16, PartialShape{1, 55, 1}); + auto convert_to_f32 = std::make_shared(param, element::f32); + auto identity = std::make_shared(param); + auto relu = std::make_shared(identity); + + auto constant = op::v0::Constant::create(element::f32, Shape{1, 1, 1}, {1.0f}); + auto concat = std::make_shared(OutputVector{constant, convert_to_f32}, 1); + + auto result1 = std::make_shared(relu); + auto result2 = std::make_shared(concat); + + model_ref = std::make_shared(OutputVector{result1, result2}, ParameterVector{param}); + } + manager.register_pass(); +} + +// Same model as above, but without additional nodes on the Identity path. Second Convert goes to Result directly. +// Identity has only one consumer (Convert_B), so we can safely rewire it and redirect Result to Identity. +TEST_F(TransformationTestsF, EliminateIdentityConvert_convert_multiple_consumers_no_node_after) { + { + auto param = std::make_shared(element::f16, PartialShape{1, 55, 1}); + auto convert_to_f32 = std::make_shared(param, element::f32); + auto identity = std::make_shared(convert_to_f32); + auto convert_to_f16 = std::make_shared(identity, element::f16); + + auto constant = op::v0::Constant::create(element::f32, Shape{1, 1, 1}, {1.0f}); + auto concat = std::make_shared(OutputVector{constant, convert_to_f32}, 1); + + auto result1 = std::make_shared(convert_to_f16); + auto result2 = std::make_shared(concat); + + model = std::make_shared(OutputVector{result1, result2}, ParameterVector{param}); + } + { + auto param = std::make_shared(element::f16, PartialShape{1, 55, 1}); + auto convert_to_f32 = std::make_shared(param, element::f32); + auto identity = std::make_shared(param); + + auto constant = op::v0::Constant::create(element::f32, Shape{1, 1, 1}, {1.0f}); + auto concat = std::make_shared(OutputVector{constant, convert_to_f32}, 1); + + auto result1 = std::make_shared(identity); + auto result2 = std::make_shared(concat); + + model_ref = std::make_shared(OutputVector{result1, result2}, ParameterVector{param}); + } + manager.register_pass(); +} + +// Identity has multiple consumers (Convert_B + Concat). Convert_B feeds Relu (not Result). +// The pass redirects Relu to take Parameter directly (bypassing the convert pair). +TEST_F(TransformationTestsF, EliminateIdentityConvert_identity_multiple_consumers_node_after) { + { + auto param = std::make_shared(element::f16, PartialShape{1, 55, 1}); + auto convert_to_f32 = std::make_shared(param, element::f32); + auto identity = std::make_shared(convert_to_f32); + auto convert_to_f16 = std::make_shared(identity, element::f16); + auto relu = std::make_shared(convert_to_f16); + + auto constant = op::v0::Constant::create(element::f32, Shape{1, 1, 1}, {1.0f}); + auto concat = std::make_shared(OutputVector{constant, identity}, 1); + + auto result1 = std::make_shared(relu); + auto result2 = std::make_shared(concat); + + model = std::make_shared(OutputVector{result1, result2}, ParameterVector{param}); + } + { + auto param = std::make_shared(element::f16, PartialShape{1, 55, 1}); + auto convert_to_f32 = std::make_shared(param, element::f32); + auto identity = std::make_shared(convert_to_f32); + auto relu = std::make_shared(param); + + auto constant = op::v0::Constant::create(element::f32, Shape{1, 1, 1}, {1.0f}); + auto concat = std::make_shared(OutputVector{constant, identity}, 1); + + auto result1 = std::make_shared(relu); + auto result2 = std::make_shared(concat); + + model_ref = std::make_shared(OutputVector{result1, result2}, ParameterVector{param}); + } + manager.register_pass(); +} + +// Identity has multiple consumers (Convert_B + Concat). Convert_B feeds Result directly. +// replace_output_update_name refuses Parameter->Result, so the graph stays unchanged. +TEST_F(TransformationTestsF, EliminateIdentityConvert_identity_multiple_consumers_no_node_after) { + auto param = std::make_shared(element::f16, PartialShape{1, 55, 1}); + auto convert_to_f32 = std::make_shared(param, element::f32); + auto identity = std::make_shared(convert_to_f32); + auto convert_to_f16 = std::make_shared(identity, element::f16); + + auto constant = op::v0::Constant::create(element::f32, Shape{1, 1, 1}, {1.0f}); + auto concat = std::make_shared(OutputVector{constant, identity}, 1); + + auto result1 = std::make_shared(convert_to_f16); + auto result2 = std::make_shared(concat); + + model = std::make_shared(OutputVector{result1, result2}, ParameterVector{param}); + + manager.register_pass(); + + model_ref = model->clone(); +} \ No newline at end of file diff --git a/src/common/transformations/tests/common_optimizations/paged_causal_conv1d_fusion.cpp b/src/common/transformations/tests/common_optimizations/paged_causal_conv1d_fusion.cpp new file mode 100644 index 000000000000..a4b3960f2a28 --- /dev/null +++ b/src/common/transformations/tests/common_optimizations/paged_causal_conv1d_fusion.cpp @@ -0,0 +1,598 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "transformations/paged_attention/paged_causal_conv1d_fusion.hpp" + +#include + +#include +#include +#include + +#include "common_test_utils/ov_test_utils.hpp" +#include "openvino/core/model.hpp" +#include "openvino/op/add.hpp" +#include "openvino/op/concat.hpp" +#include "openvino/op/constant.hpp" +#include "openvino/op/convert.hpp" +#include "openvino/op/group_conv.hpp" +#include "openvino/op/multiply.hpp" +#include "openvino/op/paged_causal_conv1d.hpp" +#include "openvino/op/parameter.hpp" +#include "openvino/op/reshape.hpp" +#include "openvino/op/result.hpp" +#include "openvino/op/slice.hpp" +#include "openvino/op/swish.hpp" +#include "openvino/op/transpose.hpp" +#include "openvino/op/unsqueeze.hpp" +#include "openvino/pass/manager.hpp" +#include "openvino/pass/sdpa_to_paged_attention.hpp" +#include "openvino/runtime/core.hpp" + +namespace { + +using namespace ov; +namespace v0 = ov::op::v0; +namespace v1 = ov::op::v1; +namespace v4 = ov::op::v4; +namespace v8 = ov::op::v8; + +std::shared_ptr build_model(bool add_present_state_result, bool use_read_value = true) { + const Shape input_shape{2, 3}; + const Shape state_shape{2, 3, 4}; + + auto input_embeds = std::make_shared(element::f32, input_shape); + auto past_state_param = std::make_shared(element::f32, state_shape); + past_state_param->set_friendly_name("past_state"); + + ov::Output past_state = past_state_param; + if (use_read_value) { + auto read_value = std::make_shared(past_state_param->output(0), "past_cache"); + read_value->get_output_tensor(0).set_names({"cache_params.past.conv.0"}); + past_state = read_value->output(0); + } + + auto part_shape = v0::Constant::create(element::i64, Shape{3}, {2, 3, 1}); + auto part0 = std::make_shared(input_embeds, part_shape, false); + auto part1 = std::make_shared(input_embeds, part_shape, false); + auto part2 = std::make_shared(input_embeds, part_shape, false); + + auto token_concat = std::make_shared(OutputVector{part0, part1, part2}, -1); + auto transpose_order = v0::Constant::create(element::i64, Shape{3}, {0, 2, 1}); + auto token_transpose = std::make_shared(token_concat, transpose_order); + + auto state_concat = std::make_shared(OutputVector{past_state, token_transpose}, -1); + + auto weights = v0::Constant::create(element::f32, Shape{3, 1, 1, 4}, std::vector(12, 0.25f)); + auto group_conv = std::make_shared(state_concat, + weights, + Strides{1}, + CoordinateDiff{0}, + CoordinateDiff{0}, + Strides{1}); + + auto neg_one = v0::Constant::create(element::i64, Shape{1}, {-1}); + auto one = v0::Constant::create(element::i64, Shape{1}, {1}); + auto slice_begin = std::make_shared(neg_one, one); + auto slice_end = v0::Constant::create(element::i64, Shape{1}, {std::numeric_limits::max()}); + auto slice_step = v0::Constant::create(element::i64, Shape{1}, {1}); + auto slice_axis = v0::Constant::create(element::i64, Shape{1}, {2}); + + auto slice2 = std::make_shared(group_conv, slice_begin, slice_end, slice_step, slice_axis); + auto swish = std::make_shared(slice2); + + auto state_slice = std::make_shared(state_concat, slice_begin, slice_end, slice_step, slice_axis); + ResultVector results; + results.push_back(std::make_shared(swish)); + + if (add_present_state_result) { + auto present_res = std::make_shared(state_slice); + present_res->get_output_tensor(0).set_names({"cache_params.present.conv.0"}); + results.push_back(present_res); + } + + ParameterVector params{input_embeds, past_state_param}; + return std::make_shared(results, params); +} + +std::shared_ptr build_model_with_multiply_post_op(bool add_present_state_result) { + const Shape state_shape{2, 3, 4}; + const Shape token_shape{2, 3, 1}; + + auto past_state_param = std::make_shared(element::f32, state_shape); + past_state_param->set_friendly_name("past_state"); + auto read_value = std::make_shared(past_state_param->output(0), "past_cache"); + read_value->get_output_tensor(0).set_names({"cache_params.past.conv.0"}); + auto past_state = read_value->output(0); + + auto token = std::make_shared(element::f32, token_shape); + auto gate = std::make_shared(element::f32, token_shape); + auto token_mul = std::make_shared(token, gate); + + auto state_concat = std::make_shared(OutputVector{past_state, token_mul}, -1); + + auto weights = v0::Constant::create(element::f32, Shape{3, 1, 1, 4}, std::vector(12, 0.25f)); + auto group_conv = std::make_shared(state_concat, + weights, + Strides{1}, + CoordinateDiff{0}, + CoordinateDiff{0}, + Strides{1}); + + auto neg_one = v0::Constant::create(element::i64, Shape{1}, {-1}); + auto one = v0::Constant::create(element::i64, Shape{1}, {1}); + auto slice_begin = std::make_shared(neg_one, one); + auto slice_end = v0::Constant::create(element::i64, Shape{1}, {std::numeric_limits::max()}); + auto slice_step = v0::Constant::create(element::i64, Shape{1}, {1}); + auto slice_axis = v0::Constant::create(element::i64, Shape{1}, {2}); + + auto slice2 = std::make_shared(group_conv, slice_begin, slice_end, slice_step, slice_axis); + auto post_op_mul_rhs = std::make_shared(element::f32, PartialShape::dynamic(3)); + auto post_op_mul = std::make_shared(slice2, post_op_mul_rhs); + + auto state_slice = std::make_shared(state_concat, slice_begin, slice_end, slice_step, slice_axis); + + ResultVector results; + results.push_back(std::make_shared(post_op_mul)); + if (add_present_state_result) { + auto present_res = std::make_shared(state_slice); + present_res->get_output_tensor(0).set_names({"cache_params.present.conv.0"}); + results.push_back(present_res); + } + + ParameterVector params{token, gate, post_op_mul_rhs, past_state_param}; + return std::make_shared(results, params); +} + +std::shared_ptr build_model_with_converted_weights(bool add_present_state_result) { + const Shape state_shape{2, 3, 4}; + const Shape token_shape{2, 3, 1}; + + auto past_state_param = std::make_shared(element::f32, state_shape); + past_state_param->set_friendly_name("past_state"); + auto read_value = std::make_shared(past_state_param->output(0), "past_cache"); + read_value->get_output_tensor(0).set_names({"cache_params.past.conv.0"}); + auto past_state = read_value->output(0); + + auto token = std::make_shared(element::f32, token_shape); + auto gate = std::make_shared(element::f32, token_shape); + auto token_mul = std::make_shared(token, gate); + + auto state_concat = std::make_shared(OutputVector{past_state, token_mul}, -1); + + auto weights_u8 = v0::Constant::create(element::u8, Shape{3, 1, 1, 4}, std::vector(12, 7)); + auto weights_f32 = std::make_shared(weights_u8, element::f32); + auto group_conv = std::make_shared(state_concat, + weights_f32, + Strides{1}, + CoordinateDiff{0}, + CoordinateDiff{0}, + Strides{1}); + group_conv->set_friendly_name("__module.model.model.layers.0.conv/aten::_convolution/GroupConvolution"); + + auto neg_one = v0::Constant::create(element::i64, Shape{1}, {-1}); + auto one = v0::Constant::create(element::i64, Shape{1}, {1}); + auto slice_begin = std::make_shared(neg_one, one); + auto slice_end = v0::Constant::create(element::i64, Shape{1}, {std::numeric_limits::max()}); + auto slice_step = v0::Constant::create(element::i64, Shape{1}, {1}); + auto slice_axis = v0::Constant::create(element::i64, Shape{1}, {2}); + + auto slice2 = std::make_shared(group_conv, slice_begin, slice_end, slice_step, slice_axis); + auto swish = std::make_shared(slice2); + + auto state_slice = std::make_shared(state_concat, slice_begin, slice_end, slice_step, slice_axis); + ResultVector results; + results.push_back(std::make_shared(swish)); + if (add_present_state_result) { + auto present_res = std::make_shared(state_slice); + present_res->get_output_tensor(0).set_names({"cache_params.present.conv.0"}); + results.push_back(present_res); + } + + ParameterVector params{token, gate, past_state_param}; + return std::make_shared(results, params); +} + +// GroupConvolution followed by Add(bias) before Slice. +std::shared_ptr build_model_with_add_bias() { + const Shape input_shape{2, 3}; + const Shape state_shape{2, 3, 4}; + + auto input_embeds = std::make_shared(element::f32, input_shape); + auto past_state_param = std::make_shared(element::f32, state_shape); + past_state_param->set_friendly_name("past_state"); + auto read_value = std::make_shared(past_state_param->output(0), "past_cache"); + read_value->get_output_tensor(0).set_names({"cache_params.past.conv.0"}); + + auto part_shape = v0::Constant::create(element::i64, Shape{3}, {2, 3, 1}); + auto part0 = std::make_shared(input_embeds, part_shape, false); + auto part1 = std::make_shared(input_embeds, part_shape, false); + auto part2 = std::make_shared(input_embeds, part_shape, false); + auto token_concat = std::make_shared(OutputVector{part0, part1, part2}, -1); + auto transpose_order = v0::Constant::create(element::i64, Shape{3}, {0, 2, 1}); + auto token_transpose = std::make_shared(token_concat, transpose_order); + + auto state_concat = std::make_shared(OutputVector{read_value, token_transpose}, -1); + + auto weights = v0::Constant::create(element::f32, Shape{3, 1, 1, 4}, std::vector(12, 0.25f)); + auto group_conv = std::make_shared(state_concat, + weights, + Strides{1}, + CoordinateDiff{0}, + CoordinateDiff{0}, + Strides{1}); + + // explicit bias constant added after GroupConvolution + auto bias = v0::Constant::create(element::f32, Shape{3, 1}, std::vector(3, 0.1f)); + auto add_bias = std::make_shared(group_conv, bias); + + auto neg_one = v0::Constant::create(element::i64, Shape{1}, {-1}); + auto one = v0::Constant::create(element::i64, Shape{1}, {1}); + auto slice_begin = std::make_shared(neg_one, one); + auto slice_end = v0::Constant::create(element::i64, Shape{1}, {std::numeric_limits::max()}); + auto slice_step = v0::Constant::create(element::i64, Shape{1}, {1}); + auto slice_axis = v0::Constant::create(element::i64, Shape{1}, {2}); + auto slice2 = std::make_shared(add_bias, slice_begin, slice_end, slice_step, slice_axis); + auto swish = std::make_shared(slice2); + + auto state_slice = std::make_shared(state_concat, slice_begin, slice_end, slice_step, slice_axis); + auto present_res = std::make_shared(state_slice); + present_res->get_output_tensor(0).set_names({"cache_params.present.conv.0"}); + + ParameterVector params{input_embeds, past_state_param}; + return std::make_shared(ResultVector{std::make_shared(swish), present_res}, params); +} + +std::shared_ptr build_fused_reference_model(const bool use_explicit_bias) { + const Shape input_shape{2, 3}; + const Shape state_shape{2, 3, 4}; + + auto input_embeds = std::make_shared(element::f32, input_shape); + auto past_state_param = std::make_shared(element::f32, state_shape); + past_state_param->set_friendly_name("past_state"); + + auto read_value = std::make_shared(past_state_param->output(0), "past_cache"); + read_value->get_output_tensor(0).set_names({"cache_params.past.conv.0"}); + + auto part_shape = v0::Constant::create(element::i64, Shape{3}, {2, 3, 1}); + auto part0 = std::make_shared(input_embeds, part_shape, false); + auto part1 = std::make_shared(input_embeds, part_shape, false); + auto part2 = std::make_shared(input_embeds, part_shape, false); + + auto token_concat = std::make_shared(OutputVector{part0, part1, part2}, -1); + auto transpose_order = v0::Constant::create(element::i64, Shape{3}, {0, 2, 1}); + auto token_transpose = std::make_shared(token_concat, transpose_order); + auto state_concat = std::make_shared(OutputVector{read_value, token_transpose}, -1); + + auto input_embeds_shape = v0::Constant::create(element::i64, Shape{2}, std::vector{-1, 3}); + auto input_embeds_node = std::make_shared(token_transpose, input_embeds_shape, false); + + auto weights = v0::Constant::create(element::f32, Shape{3, 1, 1, 4}, std::vector(12, 0.25f)); + auto pa_weight_shape = v0::Constant::create(element::i64, Shape{3}, std::vector{3, 1, 4}); + auto weight_reshaped = std::make_shared(weights, pa_weight_shape, false); + + std::shared_ptr bias; + if (use_explicit_bias) { + auto bias_const = v0::Constant::create(element::f32, Shape{3, 1}, std::vector(3, 0.1f)); + auto bias_shape = v0::Constant::create(element::i64, Shape{1}, {-1}); + bias = std::make_shared(bias_const, bias_shape, false); + } else { + bias = v0::Constant::create(element::f32, Shape{0}, std::vector{}); + } + + auto subsequence_begins = std::make_shared(element::i32, PartialShape{-1}); + subsequence_begins->set_friendly_name("subsequence_begins"); + subsequence_begins->get_output_tensor(0).set_names({"subsequence_begins"}); + + auto block_indices = std::make_shared(element::i32, PartialShape{-1}); + block_indices->set_friendly_name("la.block_indices"); + block_indices->get_output_tensor(0).set_names({"la.block_indices"}); + + auto block_indices_begins = std::make_shared(element::i32, PartialShape{-1}); + block_indices_begins->set_friendly_name("la.block_indices_begins"); + block_indices_begins->get_output_tensor(0).set_names({"la.block_indices_begins"}); + + auto past_lens = std::make_shared(element::i32, PartialShape{-1}); + past_lens->set_friendly_name("la.past_lens"); + past_lens->get_output_tensor(0).set_names({"la.past_lens"}); + + auto cache_interval = std::make_shared(element::i32, PartialShape{-1}); + cache_interval->set_friendly_name("la.cache_interval"); + cache_interval->get_output_tensor(0).set_names({"la.cache_interval"}); + + auto conv_state_table = std::make_shared(element::dynamic, PartialShape{-1, 3, 4}); + conv_state_table->set_friendly_name("conv_state_table.0"); + conv_state_table->get_output_tensor(0).set_names({"conv_state_table.0"}); + + auto paged_conv = std::make_shared(input_embeds_node, + conv_state_table, + weight_reshaped, + bias, + subsequence_begins, + block_indices, + block_indices_begins, + past_lens, + cache_interval); + auto unsqueeze_axis = v0::Constant::create(element::i64, Shape{1}, {2}); + auto unsqueeze = std::make_shared(paged_conv, unsqueeze_axis); + auto swish = std::make_shared(unsqueeze); + + auto neg_one = v0::Constant::create(element::i64, Shape{1}, {-1}); + auto one = v0::Constant::create(element::i64, Shape{1}, {1}); + auto slice_begin = std::make_shared(neg_one, one); + auto slice_end = v0::Constant::create(element::i64, Shape{1}, {std::numeric_limits::max()}); + auto slice_step = v0::Constant::create(element::i64, Shape{1}, {1}); + auto slice_axis = v0::Constant::create(element::i64, Shape{1}, {2}); + auto state_slice = std::make_shared(state_concat, slice_begin, slice_end, slice_step, slice_axis); + auto present_res = std::make_shared(state_slice); + present_res->get_output_tensor(0).set_names({"cache_params.present.conv.0"}); + + ParameterVector params{input_embeds, + past_state_param, + subsequence_begins, + block_indices, + block_indices_begins, + past_lens, + cache_interval, + conv_state_table}; + return std::make_shared(ResultVector{std::make_shared(swish), present_res}, params); +} + +std::shared_ptr build_model_without_concat_lfm2_like() { + const Shape state_shape{2, 3, 4}; + const Shape token_shape{2, 3, 1}; + + auto conv_state_hint = std::make_shared(element::f32, state_shape); + conv_state_hint->set_friendly_name("conv_state_hint"); + auto read_value = std::make_shared(conv_state_hint->output(0), "cache_param_0"); + read_value->get_output_tensor(0).set_names({"cache_params.past.conv.0"}); + auto cache_param_output = read_value->output(0); + + auto token = std::make_shared(element::f32, token_shape); + auto gate = std::make_shared(element::f32, token_shape); + auto conv_input = std::make_shared(token, gate); + + auto weights = v0::Constant::create(element::f32, Shape{3, 1, 1, 4}, std::vector(12, 0.25f)); + auto group_conv = std::make_shared(conv_input, + weights, + Strides{1}, + CoordinateDiff{2}, + CoordinateDiff{2}, + Strides{1}); + group_conv->set_friendly_name("__module.model.model.layers.0.conv.conv/aten::_convolution/GroupConvolution"); + + auto neg_one = v0::Constant::create(element::i64, Shape{1}, {-1}); + auto one = v0::Constant::create(element::i64, Shape{1}, {1}); + auto slice_begin = std::make_shared(neg_one, one); + auto slice_end = v0::Constant::create(element::i64, Shape{1}, {std::numeric_limits::max()}); + auto slice_step = v0::Constant::create(element::i64, Shape{1}, {1}); + auto slice_axis = v0::Constant::create(element::i64, Shape{1}, {2}); + + auto slice2 = std::make_shared(group_conv, slice_begin, slice_end, slice_step, slice_axis); + auto swish = std::make_shared(slice2); + + ParameterVector params{token, gate, conv_state_hint}; + return std::make_shared(ResultVector{std::make_shared(swish)}, params); +} + +std::shared_ptr build_model_without_concat_lfm2_like_multiple() { + const Shape state_shape{2, 3, 4}; + const Shape token_shape{2, 3, 1}; + + auto conv_state_hint_0 = std::make_shared(element::f32, state_shape); + conv_state_hint_0->set_friendly_name("conv_state_hint_0"); + auto read_value_0 = std::make_shared(conv_state_hint_0->output(0), "cache_param_0"); + read_value_0->get_output_tensor(0).set_names({"cache_params.past.conv.0"}); + + auto conv_state_hint_1 = std::make_shared(element::f32, state_shape); + conv_state_hint_1->set_friendly_name("conv_state_hint_1"); + auto read_value_1 = std::make_shared(conv_state_hint_1->output(0), "cache_param_1"); + read_value_1->get_output_tensor(0).set_names({"cache_params.past.conv.1"}); + + auto token_0 = std::make_shared(element::f32, token_shape); + auto gate_0 = std::make_shared(element::f32, token_shape); + auto conv_input_0 = std::make_shared(token_0, gate_0); + + auto token_1 = std::make_shared(element::f32, token_shape); + auto gate_1 = std::make_shared(element::f32, token_shape); + auto conv_input_1 = std::make_shared(token_1, gate_1); + + auto weights_0 = v0::Constant::create(element::f32, Shape{3, 1, 1, 4}, std::vector(12, 0.25f)); + auto group_conv_0 = std::make_shared(conv_input_0, + weights_0, + Strides{1}, + CoordinateDiff{2}, + CoordinateDiff{2}, + Strides{1}); + group_conv_0->set_friendly_name("__module.model.model.layers.0.conv.conv/aten::_convolution/GroupConvolution"); + + auto weights_1 = v0::Constant::create(element::f32, Shape{3, 1, 1, 4}, std::vector(12, 0.5f)); + auto group_conv_1 = std::make_shared(conv_input_1, + weights_1, + Strides{1}, + CoordinateDiff{2}, + CoordinateDiff{2}, + Strides{1}); + group_conv_1->set_friendly_name("__module.model.model.layers.1.conv.conv/aten::_convolution/GroupConvolution"); + + auto neg_one = v0::Constant::create(element::i64, Shape{1}, {-1}); + auto one = v0::Constant::create(element::i64, Shape{1}, {1}); + auto slice_begin = std::make_shared(neg_one, one); + auto slice_end = v0::Constant::create(element::i64, Shape{1}, {std::numeric_limits::max()}); + auto slice_step = v0::Constant::create(element::i64, Shape{1}, {1}); + auto slice_axis = v0::Constant::create(element::i64, Shape{1}, {2}); + + auto slice_0 = std::make_shared(group_conv_0, slice_begin, slice_end, slice_step, slice_axis); + auto slice_1 = std::make_shared(group_conv_1, slice_begin, slice_end, slice_step, slice_axis); + + auto swish_0 = std::make_shared(slice_0); + auto swish_1 = std::make_shared(slice_1); + + ParameterVector params{token_0, gate_0, token_1, gate_1, conv_state_hint_0, conv_state_hint_1}; + + ResultVector results{std::make_shared(swish_0), std::make_shared(swish_1)}; + return std::make_shared(results, params); +} + +} // namespace + +class PagedCausalConv1DFusionTest : public ::TransformationTestsF {}; + +void run_paged_causal_conv1d_fusion(const std::shared_ptr& model) { + ov::pass::paged_attention::PaParams pa_params{model->get_parameters()}; + std::unordered_set var_ids_to_remove; + + ov::pass::Manager manager; + manager.set_per_pass_validation(false); + manager.register_pass(pa_params, var_ids_to_remove); + manager.run_passes(model); + model->add_parameters(pa_params.items()); + model->validate_nodes_and_infer_types(); +} + +TEST_F(PagedCausalConv1DFusionTest, DoesNotFuseWithoutPresentStateResult) { + model = build_model(false, false); + model_ref = build_model(false, false); + + run_paged_causal_conv1d_fusion(model); + + compare_functions(model, model_ref); +} + +TEST_F(PagedCausalConv1DFusionTest, FusesWhenPresentStateIsResult) { + model = build_model(true); + + run_paged_causal_conv1d_fusion(model); + + // Verify transformation result: PagedCausalConv1D present, GroupConv absent + size_t pcc_count = 0; + size_t group_conv_count = 0; + for (const auto& op : model->get_ordered_ops()) { + if (std::string(op->get_type_name()) == "PagedCausalConv1D") { + ++pcc_count; + } + if (ov::is_type(op)) { + ++group_conv_count; + } + } + EXPECT_EQ(pcc_count, 1u) << "Expected 1 PagedCausalConv1D after fusion"; + EXPECT_EQ(group_conv_count, 0u) << "Expected 0 GroupConvolution after fusion"; +} + +TEST_F(PagedCausalConv1DFusionTest, FusesMultiplyPostOpAndGenericTokenInput) { + model = build_model_with_multiply_post_op(true); + + run_paged_causal_conv1d_fusion(model); + + // Verify transformation result: PagedCausalConv1D present, GroupConv and Slice removed + size_t pcc_count = 0; + size_t group_conv_count = 0; + for (const auto& op : model->get_ordered_ops()) { + if (std::string(op->get_type_name()) == "PagedCausalConv1D") { + ++pcc_count; + } + if (ov::is_type(op)) { + ++group_conv_count; + } + } + EXPECT_EQ(pcc_count, 1u) << "Expected 1 PagedCausalConv1D after fusion"; + EXPECT_EQ(group_conv_count, 0u) << "Expected 0 GroupConvolution after fusion"; +} + +TEST_F(PagedCausalConv1DFusionTest, FusesNonConcatPatternWithSymmetricPadding) { + model = build_model_without_concat_lfm2_like(); + + run_paged_causal_conv1d_fusion(model); + + // Verify transformation result: PagedCausalConv1D present, GroupConv and Slice removed + size_t pcc_count = 0; + size_t group_conv_count = 0; + size_t slice_count = 0; + for (const auto& op : model->get_ordered_ops()) { + if (std::string(op->get_type_name()) == "PagedCausalConv1D") { + ++pcc_count; + } + if (ov::is_type(op)) { + ++group_conv_count; + } + if (ov::is_type(op)) { + ++slice_count; + } + } + EXPECT_EQ(pcc_count, 0u) << "Expected no PagedCausalConv1D when fallback cache detection is disabled"; + EXPECT_EQ(group_conv_count, 1u) + << "Expected GroupConvolution to remain when non-concat path has no matched ReadValue"; + EXPECT_EQ(slice_count, 1u) << "Expected Slice to remain when fusion is not applied"; +} + +TEST_F(PagedCausalConv1DFusionTest, FusesSeveralNonConcatPatternsWithSymmetricPadding) { + model = build_model_without_concat_lfm2_like_multiple(); + + run_paged_causal_conv1d_fusion(model); + + // Verify transformation result: 2 PagedCausalConv1D, no GroupConv + size_t pcc_count = 0; + size_t group_conv_count = 0; + size_t slice_count = 0; + for (const auto& op : model->get_ordered_ops()) { + if (std::string(op->get_type_name()) == "PagedCausalConv1D") { + ++pcc_count; + } + if (ov::is_type(op)) { + ++group_conv_count; + } + if (ov::is_type(op)) { + ++slice_count; + } + } + EXPECT_EQ(pcc_count, 0u) << "Expected no PagedCausalConv1D when fallback cache detection is disabled"; + EXPECT_EQ(group_conv_count, 2u) + << "Expected GroupConvolution nodes to remain when non-concat paths have no matched ReadValue"; + EXPECT_EQ(slice_count, 2u) << "Expected Slice nodes to remain when fusion is not applied"; +} + +TEST_F(PagedCausalConv1DFusionTest, FusesWhenWeightsComeFromConvertPath) { + model = build_model_with_converted_weights(true); + + run_paged_causal_conv1d_fusion(model); + + // Verify transformation result: PagedCausalConv1D present, GroupConv and Slice removed + size_t pcc_count = 0; + size_t group_conv_count = 0; + for (const auto& op : model->get_ordered_ops()) { + if (std::string(op->get_type_name()) == "PagedCausalConv1D") { + ++pcc_count; + } + if (ov::is_type(op)) { + ++group_conv_count; + } + } + EXPECT_EQ(pcc_count, 1u) << "Expected 1 PagedCausalConv1D after fusion"; + EXPECT_EQ(group_conv_count, 0u) << "Expected 0 GroupConvolution after fusion"; +} + +// Verifies that when GroupConvolution is followed by Add(bias_const) before Slice, +// the fusion fires and the explicit bias constant is passed to PagedCausalConv1D. +TEST_F(PagedCausalConv1DFusionTest, FusesWithExplicitBiasConstant) { + comparator.enable(FunctionsComparator::CmpValues::ATTRIBUTES); + comparator.enable(FunctionsComparator::CmpValues::CONST_VALUES); + + model = build_model_with_add_bias(); + run_paged_causal_conv1d_fusion(model); + + model_ref = build_fused_reference_model(true); +} + +// Verifies that when GroupConvolution has no Add(bias) before Slice, +// the fusion still fires and passes an empty bias Constant (shape {0}), +// which represents "no bias" for PagedCausalConv1D. +TEST_F(PagedCausalConv1DFusionTest, FusesNoBiasUsesEmptyBiasConstant) { + comparator.enable(FunctionsComparator::CmpValues::ATTRIBUTES); + comparator.enable(FunctionsComparator::CmpValues::CONST_VALUES); + + model = build_model(true); + run_paged_causal_conv1d_fusion(model); + + model_ref = build_fused_reference_model(false); +} diff --git a/src/common/transformations/tests/common_optimizations/paged_gated_delta_net_fusion.cpp b/src/common/transformations/tests/common_optimizations/paged_gated_delta_net_fusion.cpp new file mode 100644 index 000000000000..df5c15f26feb --- /dev/null +++ b/src/common/transformations/tests/common_optimizations/paged_gated_delta_net_fusion.cpp @@ -0,0 +1,407 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "transformations/paged_attention/paged_gated_delta_net_fusion.hpp" + +#include + +#include +#include +#include + +#include "common_test_utils/ov_test_utils.hpp" +#include "openvino/op/add.hpp" +#include "openvino/op/concat.hpp" +#include "openvino/op/constant.hpp" +#include "openvino/op/gated_delta_net.hpp" +#include "openvino/op/gather.hpp" +#include "openvino/op/paged_gated_delta_net.hpp" +#include "openvino/op/parameter.hpp" +#include "openvino/op/read_value.hpp" +#include "openvino/op/reshape.hpp" +#include "openvino/op/result.hpp" +#include "openvino/op/shape_of.hpp" +#include "openvino/pass/manager.hpp" +#include "openvino/pass/sdpa_to_paged_attention.hpp" +#include "openvino/runtime/core.hpp" + +namespace { + +using namespace ov; +namespace v0 = ov::op::v0; +namespace v1 = ov::op::v1; +namespace v3 = ov::op::v3; +namespace v8 = ov::op::v8; +namespace internal = ov::op::internal; + +std::shared_ptr make_f32_param(const std::string& name, const Shape& shape) { + auto p = std::make_shared(element::f32, shape); + p->set_friendly_name(name); + p->get_output_tensor(0).set_names({name}); + return p; +} + +std::shared_ptr build_fusable_model() { + auto query = make_f32_param("query", Shape{2, 3, 4, 8}); + auto key = make_f32_param("key", Shape{2, 3, 4, 8}); + auto value = make_f32_param("value", Shape{2, 3, 4, 6}); + + auto recurrent_state = make_f32_param("past_recurrent_state", Shape{2, 4, 8, 6}); + recurrent_state->get_output_tensor(0).set_names({"cache_params.past.recurrent_state.0"}); + auto read_value = std::make_shared(recurrent_state->output(0), "cache_param_0"); + + auto gate = make_f32_param("gate", Shape{2, 3, 4}); + auto beta = make_f32_param("beta", Shape{2, 3, 4}); + + auto gdn = std::make_shared(query, key, value, read_value, gate, beta); + + auto out = std::make_shared(gdn->output(0)); + auto present_state = std::make_shared(gdn->output(1)); + present_state->get_output_tensor(0).set_names({"cache_params.present.recurrent_state.0"}); + + ParameterVector params{query, key, value, recurrent_state, gate, beta}; + + return std::make_shared(ResultVector{out, present_state}, params); +} + +std::shared_ptr build_non_fusable_model() { + auto query = make_f32_param("query", Shape{2, 3, 4, 8}); + auto key = make_f32_param("key", Shape{2, 3, 4, 8}); + auto value = make_f32_param("value", Shape{2, 3, 4, 6}); + + auto recurrent_state = make_f32_param("past_recurrent_state", Shape{2, 4, 8, 6}); + recurrent_state->get_output_tensor(0).set_names({"cache_params.past.recurrent_state.0"}); + auto read_value = std::make_shared(recurrent_state->output(0), "cache_param_0"); + + auto gate = make_f32_param("gate", Shape{2, 3, 4}); + auto beta = make_f32_param("beta", Shape{2, 3, 4}); + + auto gdn = std::make_shared(query, key, value, read_value, gate, beta); + auto add_rhs = make_f32_param("state_add_rhs", Shape{2, 4, 8, 6}); + auto state_add = std::make_shared(gdn->output(1), add_rhs); + + auto out = std::make_shared(gdn->output(0)); + auto present_state = std::make_shared(state_add); + present_state->get_output_tensor(0).set_names({"cache_params.present.recurrent_state.0"}); + + ParameterVector params{query, key, value, recurrent_state, gate, beta, add_rhs}; + return std::make_shared(ResultVector{out, present_state}, params); +} + +std::shared_ptr build_fusable_model_with_gathered_state() { + auto query = make_f32_param("query", Shape{2, 3, 4, 8}); + auto key = make_f32_param("key", Shape{2, 3, 4, 8}); + auto value = make_f32_param("value", Shape{2, 3, 4, 6}); + + auto recurrent_state = make_f32_param("past_recurrent_state", Shape{2, 4, 8, 6}); + recurrent_state->get_output_tensor(0).set_names({"cache_params.past.recurrent_state.0"}); + auto read_value = std::make_shared(recurrent_state->output(0), "cache_param_0"); + auto beam_idx = std::make_shared(element::i32, PartialShape{-1}); + beam_idx->set_friendly_name("beam_idx"); + beam_idx->get_output_tensor(0).set_names({"beam_idx"}); + auto gather_axis = v0::Constant::create(element::i64, Shape{}, {0}); + auto gathered_state = std::make_shared(read_value, beam_idx, gather_axis); + + auto gate = make_f32_param("gate", Shape{2, 3, 4}); + auto beta = make_f32_param("beta", Shape{2, 3, 4}); + + auto gdn = std::make_shared(query, key, value, gathered_state, gate, beta); + + auto out = std::make_shared(gdn->output(0)); + auto present_state = std::make_shared(gdn->output(1)); + present_state->get_output_tensor(0).set_names({"cache_params.present.recurrent_state.0"}); + + ParameterVector params{query, key, value, recurrent_state, beam_idx, gate, beta}; + return std::make_shared(ResultVector{out, present_state}, params); +} + +std::shared_ptr make_pa_param(const std::string& name, + ov::element::Type et, + const ov::PartialShape& shape) { + auto p = std::make_shared(et, shape); + p->set_friendly_name(name); + p->get_output_tensor(0).set_names({name}); + return p; +} + +// Mirrors flatten_blhd_to_thd from paged_gated_delta_net_fusion.cpp. +ov::Output ref_flatten_blhd_to_thd(const ov::Output& input) { + const auto shape_of = std::make_shared(input, element::i64); + const auto idx_hd = v0::Constant::create(element::i64, Shape{2}, {2, 3}); + const auto axis_0 = v0::Constant::create(element::i64, Shape{}, {0}); + const auto dims_hd = std::make_shared(shape_of, idx_hd, axis_0); + const auto flat_dim = v0::Constant::create(element::i64, Shape{1}, {-1}); + const auto flat_shape = std::make_shared(OutputVector{flat_dim, dims_hd}, 0); + return std::make_shared(input, flat_shape, false); +} + +// Mirrors flatten_blh_to_th from paged_gated_delta_net_fusion.cpp. +ov::Output ref_flatten_blh_to_th(const ov::Output& input) { + const auto shape_of = std::make_shared(input, element::i64); + const auto idx_h = v0::Constant::create(element::i64, Shape{1}, {2}); + const auto axis_0 = v0::Constant::create(element::i64, Shape{}, {0}); + const auto dim_h = std::make_shared(shape_of, idx_h, axis_0); + const auto flat_dim = v0::Constant::create(element::i64, Shape{1}, {-1}); + const auto flat_shape = std::make_shared(OutputVector{flat_dim, dim_h}, 0); + return std::make_shared(input, flat_shape, false); +} + +// Builds the PagedGDN block + output reshape that replaces GDN in the fused graph. +// Returns {paged_gdn_out, paged_gdn} where paged_gdn_out is the final reshaped output. +ov::Output build_paged_gdn_block(const std::shared_ptr& query, + const std::shared_ptr& key, + const std::shared_ptr& value, + const std::shared_ptr& gate, + const std::shared_ptr& beta, + const std::shared_ptr& state_table, + const std::shared_ptr& subseq_begins, + const std::shared_ptr& block_indices, + const std::shared_ptr& block_indices_begins, + const std::shared_ptr& past_lens, + const std::shared_ptr& cache_interval, + const std::string& gdn_friendly_name) { + const auto query_flat = ref_flatten_blhd_to_thd(query); + const auto key_flat = ref_flatten_blhd_to_thd(key); + const auto value_flat = ref_flatten_blhd_to_thd(value); + const auto gate_flat = ref_flatten_blh_to_th(gate); + const auto beta_flat = ref_flatten_blh_to_th(beta); + + auto paged_gdn = std::make_shared(query_flat, + key_flat, + value_flat, + state_table->output(0), + gate_flat, + beta_flat, + subseq_begins->output(0), + block_indices->output(0), + block_indices_begins->output(0), + past_lens->output(0), + cache_interval->output(0)); + paged_gdn->set_friendly_name(gdn_friendly_name + "/PagedGatedDeltaNet"); + + const auto q_shape = std::make_shared(query, element::i64); + const auto v_shape = std::make_shared(value, element::i64); + const auto axis_0 = v0::Constant::create(element::i64, Shape{}, {0}); + const auto idx_q = v0::Constant::create(element::i64, Shape{3}, {0, 1, 2}); + const auto idx_v = v0::Constant::create(element::i64, Shape{1}, {3}); + const auto q_dims = std::make_shared(q_shape, idx_q, axis_0); + const auto v_dim = std::make_shared(v_shape, idx_v, axis_0); + const auto out_shape = std::make_shared(OutputVector{q_dims, v_dim}, 0); + auto paged_gdn_out = std::make_shared(paged_gdn, out_shape, false); + paged_gdn_out->set_friendly_name(gdn_friendly_name); + return paged_gdn_out->output(0); +} + +// Reference graph for build_fusable_model() after PagedGatedDeltaNetFusion. +// GDN is replaced by PagedGDN; state Result reconnected to ReadValue. +std::shared_ptr build_reference_fused_model() { + auto query = make_f32_param("query", Shape{2, 3, 4, 8}); + auto key = make_f32_param("key", Shape{2, 3, 4, 8}); + auto value = make_f32_param("value", Shape{2, 3, 4, 6}); + auto recurrent_state = make_f32_param("past_recurrent_state", Shape{2, 4, 8, 6}); + recurrent_state->get_output_tensor(0).set_names({"cache_params.past.recurrent_state.0"}); + auto gate = make_f32_param("gate", Shape{2, 3, 4}); + auto beta = make_f32_param("beta", Shape{2, 3, 4}); + + // ReadValue remains as the reconnected source for the state Result (dead branch). + const auto read_value = std::make_shared(recurrent_state->output(0), "cache_param_0"); + + // New PA params added by the pass (in creation order). + // State input shape [2,4,8,6] = [B,H,D_k,D_v] → table shape [?,H,D_v,D_k] = [?,4,6,8]. + auto subseq_begins = make_pa_param("subsequence_begins", element::i32, PartialShape{-1}); + auto block_indices = make_pa_param("la.block_indices", element::i32, PartialShape{-1}); + auto block_indices_begins = make_pa_param("la.block_indices_begins", element::i32, PartialShape{-1}); + auto past_lens = make_pa_param("la.past_lens", element::i32, PartialShape{-1}); + auto cache_interval = make_pa_param("la.cache_interval", element::i32, PartialShape{-1}); + auto state_table = + make_pa_param("gated_delta_state_table.0", element::f32, PartialShape{Dimension::dynamic(), 4, 6, 8}); + + const auto paged_gdn_out = build_paged_gdn_block(query, + key, + value, + gate, + beta, + state_table, + subseq_begins, + block_indices, + block_indices_begins, + past_lens, + cache_interval, + "GatedDeltaNet"); + + auto out = std::make_shared(paged_gdn_out); + auto present_state = std::make_shared(read_value->output(0)); + present_state->get_output_tensor(0).set_names({"cache_params.present.recurrent_state.0"}); + + ParameterVector params{query, + key, + value, + recurrent_state, + gate, + beta, + subseq_begins, + block_indices, + block_indices_begins, + past_lens, + cache_interval, + state_table}; + return std::make_shared(ResultVector{out, present_state}, params); +} + +// Reference graph for build_non_fusable_model() after PagedGatedDeltaNetFusion. +// GDN is replaced by PagedGDN; the Add consumer of the state output is reconnected to ReadValue. +std::shared_ptr build_reference_fused_non_fusable_model() { + auto query = make_f32_param("query", Shape{2, 3, 4, 8}); + auto key = make_f32_param("key", Shape{2, 3, 4, 8}); + auto value = make_f32_param("value", Shape{2, 3, 4, 6}); + auto recurrent_state = make_f32_param("past_recurrent_state", Shape{2, 4, 8, 6}); + recurrent_state->get_output_tensor(0).set_names({"cache_params.past.recurrent_state.0"}); + auto gate = make_f32_param("gate", Shape{2, 3, 4}); + auto beta = make_f32_param("beta", Shape{2, 3, 4}); + auto add_rhs = make_f32_param("state_add_rhs", Shape{2, 4, 8, 6}); + + const auto read_value = std::make_shared(recurrent_state->output(0), "cache_param_0"); + + auto subseq_begins = make_pa_param("subsequence_begins", element::i32, PartialShape{-1}); + auto block_indices = make_pa_param("la.block_indices", element::i32, PartialShape{-1}); + auto block_indices_begins = make_pa_param("la.block_indices_begins", element::i32, PartialShape{-1}); + auto past_lens = make_pa_param("la.past_lens", element::i32, PartialShape{-1}); + auto cache_interval = make_pa_param("la.cache_interval", element::i32, PartialShape{-1}); + auto state_table = + make_pa_param("gated_delta_state_table.0", element::dynamic, PartialShape{Dimension::dynamic(), 4, 6, 8}); + + const auto paged_gdn_out = build_paged_gdn_block(query, + key, + value, + gate, + beta, + state_table, + subseq_begins, + block_indices, + block_indices_begins, + past_lens, + cache_interval, + "GatedDeltaNet"); + + // The Add consumer of GDN state output is reconnected to ReadValue. + const auto state_add = std::make_shared(read_value->output(0), add_rhs); + auto out = std::make_shared(paged_gdn_out); + auto present_state = std::make_shared(state_add); + present_state->get_output_tensor(0).set_names({"cache_params.present.recurrent_state.0"}); + + ParameterVector params{query, + key, + value, + recurrent_state, + gate, + beta, + add_rhs, + subseq_begins, + block_indices, + block_indices_begins, + past_lens, + cache_interval, + state_table}; + return std::make_shared(ResultVector{out, present_state}, params); +} + +// Reference graph for build_fusable_model_with_gathered_state() after PagedGatedDeltaNetFusion. +// GDN is replaced by PagedGDN; state Result reconnected to Gather(ReadValue, beam_idx, axis). +std::shared_ptr build_reference_fused_model_with_gathered_state() { + auto query = make_f32_param("query", Shape{2, 3, 4, 8}); + auto key = make_f32_param("key", Shape{2, 3, 4, 8}); + auto value = make_f32_param("value", Shape{2, 3, 4, 6}); + auto recurrent_state = make_f32_param("past_recurrent_state", Shape{2, 4, 8, 6}); + recurrent_state->get_output_tensor(0).set_names({"cache_params.past.recurrent_state.0"}); + auto beam_idx = std::make_shared(element::i32, PartialShape{-1}); + beam_idx->set_friendly_name("beam_idx"); + beam_idx->get_output_tensor(0).set_names({"beam_idx"}); + auto gate = make_f32_param("gate", Shape{2, 3, 4}); + auto beta = make_f32_param("beta", Shape{2, 3, 4}); + + const auto read_value = std::make_shared(recurrent_state->output(0), "cache_param_0"); + const auto gather_axis = v0::Constant::create(element::i64, Shape{}, {0}); + const auto gathered_state = std::make_shared(read_value, beam_idx, gather_axis); + + auto subseq_begins = make_pa_param("subsequence_begins", element::i32, PartialShape{-1}); + auto block_indices = make_pa_param("la.block_indices", element::i32, PartialShape{-1}); + auto block_indices_begins = make_pa_param("la.block_indices_begins", element::i32, PartialShape{-1}); + auto past_lens = make_pa_param("la.past_lens", element::i32, PartialShape{-1}); + auto cache_interval = make_pa_param("la.cache_interval", element::i32, PartialShape{-1}); + // The pattern matches on read_value (not gathered_state), so state shape comes from ReadValue output: [2,4,8,6]. + auto state_table = + make_pa_param("gated_delta_state_table.0", element::dynamic, PartialShape{Dimension::dynamic(), 4, 6, 8}); + + const auto paged_gdn_out = build_paged_gdn_block(query, + key, + value, + gate, + beta, + state_table, + subseq_begins, + block_indices, + block_indices_begins, + past_lens, + cache_interval, + "GatedDeltaNet"); + + auto out = std::make_shared(paged_gdn_out); + // State Result reconnected to gathered_state (gdn_node->input_value(3)). + auto present_state = std::make_shared(gathered_state->output(0)); + present_state->get_output_tensor(0).set_names({"cache_params.present.recurrent_state.0"}); + + ParameterVector params{query, + key, + value, + recurrent_state, + beam_idx, + gate, + beta, + subseq_begins, + block_indices, + block_indices_begins, + past_lens, + cache_interval, + state_table}; + return std::make_shared(ResultVector{out, present_state}, params); +} + +} // namespace + +class PagedGatedDeltaNetFusionTest : public ::TransformationTestsF {}; + +void run_paged_gated_delta_net_fusion(const std::shared_ptr& model) { + ov::pass::paged_attention::PaParams pa_params{model->get_parameters()}; + std::unordered_set var_ids_to_remove; + + ov::pass::Manager manager; + manager.set_per_pass_validation(false); + manager.register_pass(pa_params, var_ids_to_remove); + manager.run_passes(model); + model->add_parameters(pa_params.items()); + model->validate_nodes_and_infer_types(); +} + +TEST_F(PagedGatedDeltaNetFusionTest, FusesWhenStateOutputIsOnlyResultConsumer) { + model = build_fusable_model(); + model_ref = build_reference_fused_model(); + disable_rt_info_check(); + comparator.disable(FunctionsComparator::PRECISIONS); + comparator.disable(FunctionsComparator::TENSOR_NAMES); + + run_paged_gated_delta_net_fusion(model); +} + +TEST_F(PagedGatedDeltaNetFusionTest, FusesWhenStateOutputHasNonResultConsumer) { + model = build_non_fusable_model(); + model_ref = build_reference_fused_non_fusable_model(); + + run_paged_gated_delta_net_fusion(model); +} + +TEST_F(PagedGatedDeltaNetFusionTest, FusesWhenStateInputIsGatherFromReadValue) { + model = build_fusable_model_with_gathered_state(); + model_ref = build_reference_fused_model_with_gathered_state(); + run_paged_gated_delta_net_fusion(model); +} diff --git a/src/common/transformations/tests/common_optimizations/pull_through_reduce_test.cpp b/src/common/transformations/tests/common_optimizations/pull_through_reduce_test.cpp index d42ed4f3edb4..5328229b4600 100644 --- a/src/common/transformations/tests/common_optimizations/pull_through_reduce_test.cpp +++ b/src/common/transformations/tests/common_optimizations/pull_through_reduce_test.cpp @@ -404,3 +404,15 @@ TEST_F(TransformationTestsF, PullReshapeThroughReduceMeanSkipIfMoreThanOneReshap model = std::make_shared(OutputVector{reduce_mean, add}, ParameterVector{input}); manager.register_pass(); } + +TEST_F(TransformationTestsF, PullReshapeThroughReduceSkipIfReshapeIsNotPureUnsqueeze) { + // [2,16] -> [1,2,4,4]: last dim 16 is split to 4*4, NOT a pure unsqueeze + const auto input = std::make_shared(element::f32, Shape{2, 16}); + const auto target_shape = Constant::create(element::i64, Shape{4}, {1, 2, 4, 4}); + const auto reshape = std::make_shared(input, target_shape, false); + const auto reduce_axes = Constant::create(element::i64, Shape{1}, {-1}); + const auto reduce_mean = std::make_shared(reshape, reduce_axes); + + model = std::make_shared(OutputVector{reduce_mean}, ParameterVector{input}); + manager.register_pass(); +} diff --git a/src/common/transformations/tests/common_optimizations/rms_norm_decomposition_test.cpp b/src/common/transformations/tests/common_optimizations/rms_norm_decomposition_test.cpp index 2b319bdc753c..3ce950117f1e 100644 --- a/src/common/transformations/tests/common_optimizations/rms_norm_decomposition_test.cpp +++ b/src/common/transformations/tests/common_optimizations/rms_norm_decomposition_test.cpp @@ -424,3 +424,148 @@ TEST_F(TransformationTestsF, RMSNormFusionTest12) { comparator.enable(FunctionsComparator::CmpValues::CONST_VALUES); comparator.enable(FunctionsComparator::CmpValues::ATTRIBUTES); } + +// --- Power(-0.5) direct rsqrt path tests --- + +// Power(-0.5) direct path with gamma and tail convert +TEST_F(TransformationTestsF, RMSNormFusionTest13_PowerNegHalf_GammaConvert) { + { + auto input = std::make_shared(ov::element::f32, ov::Shape{1, 2, 6}); + auto power_const = ov::op::v0::Constant::create(ov::element::f32, {}, {2.f}); + auto power = std::make_shared(input, power_const); + auto mean_axes = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{1}, {-1}); + auto mean = std::make_shared(power, mean_axes, true); + auto eps = ov::op::v0::Constant::create(ov::element::f32, {}, {1e-6f}); + auto add_eps = std::make_shared(mean, eps); + auto neg_half = ov::op::v0::Constant::create(ov::element::f32, {}, {-0.5f}); + auto rsqrt = std::make_shared(add_eps, neg_half); + auto mul1 = std::make_shared(input, rsqrt); + auto gamma = ov::op::v0::Constant::create(ov::element::f32, + ov::Shape{6}, + {0.029f, 0.014f, 0.003f, 0.013f, 0.015f, 0.009f}); + auto mul2 = std::make_shared(gamma, mul1); + auto comp = std::make_shared(mul2, ov::element::f16); + + model = std::make_shared(ov::OutputVector{comp}, ov::ParameterVector{input}); + manager.register_pass(); + } + { + auto input = std::make_shared(ov::element::f32, ov::Shape{1, 2, 6}); + auto rms_const = ov::op::v0::Constant::create(ov::element::f32, + ov::Shape{6}, + {0.029f, 0.014f, 0.003f, 0.013f, 0.015f, 0.009f}); + auto rms = std::make_shared(input, rms_const, 1e-6f, ov::element::f16); + + model_ref = std::make_shared(ov::OutputVector{rms}, ov::ParameterVector{input}); + } + comparator.enable(FunctionsComparator::CmpValues::ACCURACY); + comparator.enable(FunctionsComparator::CmpValues::CONST_VALUES); + comparator.enable(FunctionsComparator::CmpValues::ATTRIBUTES); +} + +// Power(-0.5) direct path with gamma, no tail convert +TEST_F(TransformationTestsF, RMSNormFusionTest14_PowerNegHalf_GammaNoConvert) { + { + auto input = std::make_shared(ov::element::f32, ov::PartialShape{-1, -1, 6}); + auto power_const = ov::op::v0::Constant::create(ov::element::f32, {}, {2.f}); + auto power = std::make_shared(input, power_const); + auto mean_axes = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{1}, {-1}); + auto mean = std::make_shared(power, mean_axes, true); + auto eps = ov::op::v0::Constant::create(ov::element::f32, {}, {1e-6f}); + auto add_eps = std::make_shared(mean, eps); + auto neg_half = ov::op::v0::Constant::create(ov::element::f32, {}, {-0.5f}); + auto rsqrt = std::make_shared(add_eps, neg_half); + auto mul1 = std::make_shared(input, rsqrt); + auto gamma = ov::op::v0::Constant::create(ov::element::f32, + ov::Shape{6}, + {0.029f, 0.014f, 0.003f, 0.013f, 0.015f, 0.009f}); + auto mul2 = std::make_shared(gamma, mul1); + + model = std::make_shared(ov::OutputVector{mul2}, ov::ParameterVector{input}); + manager.register_pass(false); + } + { + auto input = std::make_shared(ov::element::f32, ov::PartialShape{-1, -1, 6}); + auto rms_const = ov::op::v0::Constant::create(ov::element::f32, + ov::Shape{6}, + {0.029f, 0.014f, 0.003f, 0.013f, 0.015f, 0.009f}); + auto rms = std::make_shared(input, rms_const, 1e-6f, ov::element::f32); + + model_ref = std::make_shared(ov::OutputVector{rms}, ov::ParameterVector{input}); + } + comparator.enable(FunctionsComparator::CmpValues::ACCURACY); + comparator.enable(FunctionsComparator::CmpValues::CONST_VALUES); + comparator.enable(FunctionsComparator::CmpValues::ATTRIBUTES); +} + +// Power(-0.5) direct path without gamma, with dynamic scale +TEST_F(TransformationTestsF, RMSNormFusionTest15_PowerNegHalf_NoGammaScale) { + { + auto input = std::make_shared(ov::element::f32, ov::PartialShape{-1, -1, 6}); + auto scale = std::make_shared(ov::element::f32, ov::PartialShape{-1, -1, 6}); + + auto power_const = ov::op::v0::Constant::create(ov::element::f32, {}, {2.f}); + auto power = std::make_shared(input, power_const); + auto mean_axes = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{1}, {-1}); + auto mean = std::make_shared(power, mean_axes, true); + auto eps = ov::op::v0::Constant::create(ov::element::f32, {}, {1e-6f}); + auto add_eps = std::make_shared(mean, eps); + auto neg_half = ov::op::v0::Constant::create(ov::element::f32, {}, {-0.5f}); + auto rsqrt = std::make_shared(add_eps, neg_half); + auto mul1 = std::make_shared(input, rsqrt); + auto mul2 = std::make_shared(mul1, scale); + + model = std::make_shared(ov::OutputVector{mul2}, ov::ParameterVector{input, scale}); + manager.register_pass(false, false, true); + } + { + auto input = std::make_shared(ov::element::f32, ov::PartialShape{-1, -1, 6}); + auto scale = std::make_shared(ov::element::f32, ov::PartialShape{-1, -1, 6}); + + auto rms = std::make_shared(input, 1e-6f, ov::element::f32); + auto mul = std::make_shared(rms, scale); + + model_ref = std::make_shared(ov::OutputVector{mul}, ov::ParameterVector{input, scale}); + } + comparator.enable(FunctionsComparator::CmpValues::ACCURACY); +} + +// Power(-0.5) direct path with f16 constants and Convert +TEST_F(TransformationTestsF, RMSNormFusionTest16_PowerNegHalf_F16Convert) { + { + auto input = std::make_shared(ov::element::f32, ov::PartialShape{-1, -1, 6}); + auto power_const = ov::op::v0::Constant::create(ov::element::f16, {}, {2.f}); + auto power_const_convert = std::make_shared(power_const, ov::element::f32); + auto power = std::make_shared(input, power_const_convert); + auto mean_axes = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{1}, {-1}); + auto mean = std::make_shared(power, mean_axes, true); + auto eps = ov::op::v0::Constant::create(ov::element::f16, {}, {1e-5f}); + auto eps_convert = std::make_shared(eps, ov::element::f32); + auto add_eps = std::make_shared(mean, eps_convert); + auto neg_half = ov::op::v0::Constant::create(ov::element::f16, {}, {-0.5f}); + auto neg_half_convert = std::make_shared(neg_half, ov::element::f32); + auto rsqrt = std::make_shared(add_eps, neg_half_convert); + auto mul1 = std::make_shared(input, rsqrt); + auto gamma = ov::op::v0::Constant::create(ov::element::f16, + ov::Shape{6}, + {0.029f, 0.014f, 0.003f, 0.013f, 0.015f, 0.009f}); + auto gamma_convert = std::make_shared(gamma, ov::element::f32); + auto mul2 = std::make_shared(gamma_convert, mul1); + + model = std::make_shared(ov::OutputVector{mul2}, ov::ParameterVector{input}); + manager.register_pass(false); + } + { + auto input = std::make_shared(ov::element::f32, ov::PartialShape{-1, -1, 6}); + auto gamma = ov::op::v0::Constant::create(ov::element::f16, + ov::Shape{6}, + {0.029f, 0.014f, 0.003f, 0.013f, 0.015f, 0.009f}); + auto gamma_convert = std::make_shared(gamma, ov::element::f32); + auto rms = std::make_shared(input, gamma_convert, 1e-5f, ov::element::f32); + + model_ref = std::make_shared(ov::OutputVector{rms}, ov::ParameterVector{input}); + } + comparator.enable(FunctionsComparator::CmpValues::ACCURACY); + comparator.enable(FunctionsComparator::CmpValues::CONST_VALUES); + comparator.enable(FunctionsComparator::CmpValues::ATTRIBUTES); +} diff --git a/src/common/transformations/tests/common_optimizations/sdpa_fusion_test.cpp b/src/common/transformations/tests/common_optimizations/sdpa_fusion_test.cpp index 62e571b96e14..d4f23a9c58d0 100644 --- a/src/common/transformations/tests/common_optimizations/sdpa_fusion_test.cpp +++ b/src/common/transformations/tests/common_optimizations/sdpa_fusion_test.cpp @@ -1523,3 +1523,39 @@ TEST_F(TransformationTestsF, SDPAFusionTest_DynamicMaskScores) { comparator.enable(FunctionsComparator::CmpValues::CONST_VALUES); comparator.enable(FunctionsComparator::CmpValues::ATTRIBUTES); } + +// Regression test that locks in the order of `set_friendly_name` and `try_align_outputs` inside the +// SDPAFusion callback. The fused SDPA must inherit the match-root's friendly name *before* the +// alignment step runs, because `try_align_outputs` derives the wrapping reshape's name from the SDPA's +// name with a "/Reshape" suffix. If a future refactor reorders these steps the assertions below fail. +TEST(SDPAFusionTest, AlignedOutputFriendlyName) { + const PartialShape shape{55, 128}; + + auto q_param = make_shared(f16, shape); + auto k_param = make_shared(f16, shape); + auto v_param = make_shared(f16, shape); + + auto qk = make_shared(q_param, k_param, false, true); + auto softmax = make_shared(qk, -1); + auto qkv = make_shared(softmax, v_param, false, false); + + constexpr auto kMatchRootName = "match_root"; + qkv->set_friendly_name(kMatchRootName); + + const auto model = make_shared(OutputVector{qkv}, ParameterVector{q_param, k_param, v_param}); + + ov::pass::Manager manager; + manager.register_pass(); + manager.run_passes(model); + + const auto result = model->get_results().front(); + const auto wrapping = result->get_input_node_shared_ptr(0); + + ASSERT_TRUE(ov::is_type(wrapping)) + << "Expected a Squeeze to wrap the SDPA output for 2D inputs (rank-3 SDPA → rank-2 match-root)."; + EXPECT_EQ(wrapping->get_friendly_name(), std::string(kMatchRootName) + "/Reshape"); + + const auto fused_sdpa = wrapping->get_input_node_shared_ptr(0); + ASSERT_TRUE(ov::is_type(fused_sdpa)); + EXPECT_EQ(fused_sdpa->get_friendly_name(), kMatchRootName); +} diff --git a/src/common/transformations/tests/common_optimizations/transpose_sinking_test.cpp b/src/common/transformations/tests/common_optimizations/transpose_sinking_test.cpp index a1287ada70f8..1189cc2b62c4 100644 --- a/src/common/transformations/tests/common_optimizations/transpose_sinking_test.cpp +++ b/src/common/transformations/tests/common_optimizations/transpose_sinking_test.cpp @@ -7,6 +7,7 @@ #include #include +#include #include #include "common_test_utils/ov_test_utils.hpp" @@ -105,6 +106,55 @@ class TransposeSinkingFQ : public ov::test::TestsCommon, } }; +struct TransposeFQTransposeFuseParams { + Shape ranges_shape; + Shape expected_ranges_shape; +}; + +class TransposeSinkingFQTransposeFuse : public ov::test::TestsCommon, + public testing::WithParamInterface> { +public: + std::shared_ptr f, f_ref; + + void SetUp() override { + const auto& test_case = std::get<0>(GetParam()); + std::vector fq_values(shape_size(test_case.ranges_shape)); + std::iota(fq_values.begin(), fq_values.end(), 0.f); + + { + auto input = std::make_shared(element::f32, PartialShape{1, 3, 4, 5}); + + auto first_order = + std::make_shared(element::i64, Shape{4}, std::vector{0, 2, 3, 1}); + auto first_transpose = std::make_shared(input, first_order); + + auto i_low = std::make_shared(element::f32, test_case.ranges_shape, fq_values); + auto i_high = std::make_shared(element::f32, test_case.ranges_shape, fq_values); + auto o_low = std::make_shared(element::f32, test_case.ranges_shape, fq_values); + auto o_high = std::make_shared(element::f32, test_case.ranges_shape, fq_values); + auto fq = std::make_shared(first_transpose, i_low, i_high, o_low, o_high, 256); + + auto second_order = + std::make_shared(element::i64, Shape{4}, std::vector{0, 3, 1, 2}); + auto second_transpose = std::make_shared(fq, second_order); + + f = std::make_shared(OutputVector{second_transpose}, ParameterVector{input}); + } + + { + auto input = std::make_shared(element::f32, PartialShape{1, 3, 4, 5}); + + auto i_low = std::make_shared(element::f32, test_case.expected_ranges_shape, fq_values); + auto i_high = std::make_shared(element::f32, test_case.expected_ranges_shape, fq_values); + auto o_low = std::make_shared(element::f32, test_case.expected_ranges_shape, fq_values); + auto o_high = std::make_shared(element::f32, test_case.expected_ranges_shape, fq_values); + auto fq = std::make_shared(input, i_low, i_high, o_low, o_high, 256); + + f_ref = std::make_shared(OutputVector{fq}, ParameterVector{input}); + } + } +}; + TEST_P(TransposeSinkingFQ, TransposeFQReduce) { auto unh = std::make_shared(); pass::Manager manager; @@ -155,6 +205,34 @@ INSTANTIATE_TEST_SUITE_P(TransformationTest, {2, 3}, {0, 1}})); +TEST_P(TransposeSinkingFQTransposeFuse, TransposeFQTransposeFuse) { + auto unh = std::make_shared(); + pass::Manager manager; + manager.register_pass(unh); + manager.register_pass(); + manager.register_pass(); + manager.register_pass(unh); + manager.run_passes(f); + OV_ASSERT_NO_THROW(check_rt_info(f)); + + auto fc = FunctionsComparator::no_default() + .enable(FunctionsComparator::NODES) + .enable(FunctionsComparator::PRECISIONS) + .enable(FunctionsComparator::CONST_VALUES); + auto res = fc.compare(f, f_ref); + ASSERT_TRUE(res.valid) << res.message; +} + +INSTANTIATE_TEST_SUITE_P(TransposeSinkingCommon, + TransposeSinkingFQTransposeFuse, + testing::Values(TransposeFQTransposeFuseParams{{}, {}}, + TransposeFQTransposeFuseParams{{1}, {1}}, + TransposeFQTransposeFuseParams{{1, 1, 1, 1}, {1, 1, 1, 1}}, + TransposeFQTransposeFuseParams{{3}, {1, 3, 1, 1}}, + TransposeFQTransposeFuseParams{{1, 3}, {1, 3, 1, 1}}, + TransposeFQTransposeFuseParams{{1, 1, 3}, {1, 3, 1, 1}}, + TransposeFQTransposeFuseParams{{1, 1, 1, 3}, {1, 3, 1, 1}})); + struct TransposeReduceParams { // given params PartialShape transpose_input_shape; diff --git a/src/common/transformations/tests/control_flow/unroll_tensor_iterator_test.cpp b/src/common/transformations/tests/control_flow/unroll_tensor_iterator_test.cpp index 1a89fac09967..e5ebcfad93cd 100644 --- a/src/common/transformations/tests/control_flow/unroll_tensor_iterator_test.cpp +++ b/src/common/transformations/tests/control_flow/unroll_tensor_iterator_test.cpp @@ -13,9 +13,13 @@ #include "common_test_utils/ov_test_utils.hpp" #include "common_test_utils/test_common.hpp" #include "openvino/core/model.hpp" +#include "openvino/op/add.hpp" #include "openvino/op/concat.hpp" +#include "openvino/op/constant.hpp" +#include "openvino/op/convert.hpp" #include "openvino/op/gru_cell.hpp" #include "openvino/op/gru_sequence.hpp" +#include "openvino/op/loop.hpp" #include "openvino/op/lstm_cell.hpp" #include "openvino/op/rnn_cell.hpp" #include "openvino/op/split.hpp" @@ -627,3 +631,60 @@ TEST(TransformationTests, CheckTensorNamesAfterUnrolling) { ASSERT_EQ(names_after.size(), 0); EXPECT_EQ(names_before, names_after); } + +// Regression test: when the Loop's current_iteration parameter has type i32, the +// replacement Constant generated by UnrollTensorIterator must also be i32 (not i64). +// Hardcoding i64 causes type mismatches downstream (e.g. Add gets i64+i32 inputs), +// which is the root cause of the GPU bug reported in GitHub issue #35411. +TEST(TransformationTests, UnrollLoopCurrentIterationPreservesElementType) { + const size_t num_iter = 4; + const ov::Shape scalar{}; + + auto b_iter = std::make_shared(ov::element::i32, scalar); + auto b_data = std::make_shared(ov::element::f32, ov::Shape{1}); + b_iter->set_friendly_name("b_iter"); + b_data->set_friendly_name("b_data"); + + auto cast = std::make_shared(b_iter, ov::element::f32); + auto unsqueeze_axis = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{1}, {0}); + auto iter_f32 = std::make_shared(cast, unsqueeze_axis); + auto b_add = std::make_shared(b_data, iter_f32); + auto b_cond = ov::op::v0::Constant::create(ov::element::boolean, scalar, {true}); + + auto body = std::make_shared(ov::OutputVector{b_cond, b_add}, ov::ParameterVector{b_iter, b_data}); + + auto trip_count = ov::op::v0::Constant::create(ov::element::i64, scalar, {num_iter}); + auto exec_cond = ov::op::v0::Constant::create(ov::element::boolean, scalar, {true}); + + auto loop = std::make_shared(trip_count, exec_cond); + loop->set_function(body); + loop->set_special_body_ports({0, 0}); + + auto outer_data = std::make_shared(ov::element::f32, ov::Shape{1}); + loop->set_merged_input(b_data, outer_data, b_add); + loop->get_iter_value(b_add, -1); + + auto result = std::make_shared(loop->output(0)); + auto model = std::make_shared(ov::ResultVector{result}, ov::ParameterVector{outer_data}); + + ov::pass::Manager manager; + manager.register_pass(); + manager.register_pass(); + manager.run_passes(model); + + for (const auto& node : model->get_ops()) { + if (const auto constant = std::dynamic_pointer_cast(node)) { + if (constant->get_element_type() == ov::element::i64 && constant->get_shape() == ov::Shape{}) { + const auto& consumers = constant->output(0).get_target_inputs(); + for (const auto& consumer : consumers) { + EXPECT_FALSE(std::dynamic_pointer_cast( + consumer.get_node()->shared_from_this()) != nullptr) + << "i64 constant feeds a Convert node after unrolling; " + "current_iteration replacement should be i32, not i64"; + } + } + } + } + + EXPECT_NO_THROW(model->validate_nodes_and_infer_types()); +} diff --git a/src/common/transformations/tests/decompositions/low_precision_dequantize_decomposition_mark_test.cpp b/src/common/transformations/tests/decompositions/low_precision_dequantize_decomposition_mark_test.cpp new file mode 100644 index 000000000000..650babeabda5 --- /dev/null +++ b/src/common/transformations/tests/decompositions/low_precision_dequantize_decomposition_mark_test.cpp @@ -0,0 +1,233 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// +// Tests that the canonical decomposition emitted by +// ov::decomposition::low_precision_dequantize is recognised by +// ov::pass::MarkDequantization. These tests act as a guard for both +// directions: +// * decomposition authors must keep the produced sub-graph in the shape +// accepted by MarkDequantization; +// * MarkDequantization authors must keep accepting the canonical +// decomposition shape that frontends emit. + +#include + +#include + +#include "common_test_utils/ov_test_utils.hpp" +#include "openvino/decompositions/low_precision_dequantize.hpp" +#include "openvino/op/constant.hpp" +#include "openvino/op/convert.hpp" +#include "openvino/op/multiply.hpp" +#include "openvino/op/reshape.hpp" +#include "openvino/op/subtract.hpp" +#include "openvino/pass/constant_folding.hpp" +#include "transformations/low_precision/mark_dequantization_subgraph.hpp" +#include "transformations/rt_info/dequantization_node.hpp" +#include "transformations/rt_info/disable_constant_folding.hpp" + +using namespace ov; + +namespace { + +const element::TypeVector kLowPrecisionTypes{element::u8, element::i8, element::u4, element::i4}; + +} // namespace + +TEST_F(TransformationTestsF, LowPrecisionDequantize_Asymmetric) { + const Shape weights_shape{4, 16}; + + { + auto weights = op::v0::Constant::create(element::u8, weights_shape, {1}); + auto zero_point = op::v0::Constant::create(element::u8, Shape{}, {127}); + auto scale = op::v0::Constant::create(element::f32, Shape{}, {0.2f}); + + auto out = decomposition::low_precision_dequantize(weights, scale, zero_point); + model = std::make_shared(OutputVector{out}, ParameterVector{}); + } + + manager.register_pass(kLowPrecisionTypes); + manager.register_pass(); + + { + auto weights = op::v0::Constant::create(element::u8, weights_shape, {1}); + auto convert = std::make_shared(weights, element::f32); + disable_constant_folding(convert); + + auto zero_point = op::v0::Constant::create(element::u8, Shape{}, {127}); + auto zp_convert = std::make_shared(zero_point, element::f32); + disable_constant_folding(zp_convert); + + auto subtract = std::make_shared(convert, zp_convert); + mark_as_dequantization_node(subtract); + + auto scale = op::v0::Constant::create(element::f32, Shape{}, {0.2f}); + auto multiply = std::make_shared(subtract, scale); + mark_as_dequantization_node(multiply); + + model_ref = std::make_shared(OutputVector{multiply}, ParameterVector{}); + } + + comparator.enable(FunctionsComparator::CmpValues::CONST_VALUES); + comparator.enable(FunctionsComparator::CmpValues::RUNTIME_KEYS); +} + +TEST_F(TransformationTestsF, LowPrecisionDequantize_Symmetric) { + const Shape weights_shape{4, 16}; + + { + auto weights = op::v0::Constant::create(element::i8, weights_shape, {-2}); + auto scale = op::v0::Constant::create(element::f32, Shape{}, {0.2f}); + + auto out = decomposition::low_precision_dequantize(weights, scale); + model = std::make_shared(OutputVector{out}, ParameterVector{}); + } + + manager.register_pass(kLowPrecisionTypes); + manager.register_pass(); + + { + auto weights = op::v0::Constant::create(element::i8, weights_shape, {-2}); + auto convert = std::make_shared(weights, element::f32); + disable_constant_folding(convert); + + auto scale = op::v0::Constant::create(element::f32, Shape{}, {0.2f}); + auto multiply = std::make_shared(convert, scale); + mark_as_dequantization_node(multiply); + + model_ref = std::make_shared(OutputVector{multiply}, ParameterVector{}); + } + + comparator.enable(FunctionsComparator::CmpValues::CONST_VALUES); + comparator.enable(FunctionsComparator::CmpValues::RUNTIME_KEYS); +} + +TEST_F(TransformationTestsF, LowPrecisionDequantize_U4Asymmetric) { + // Group-quantised u4 weights — the AWQ/GPTQ shape used by the PyTorch frontend. + const Shape weights_shape{2, 8, 16}; + const Shape scale_shape{2, 1, 16}; + + { + auto weights = op::v0::Constant::create(element::u4, weights_shape, {0}); + auto zero_point = op::v0::Constant::create(element::u4, Shape{}, {8}); + auto scale = op::v0::Constant::create(element::f16, scale_shape, {0.1f}); + + auto out = decomposition::low_precision_dequantize(weights, scale, zero_point); + model = std::make_shared(OutputVector{out}, ParameterVector{}); + } + + manager.register_pass(kLowPrecisionTypes); + manager.register_pass(); + + { + auto weights = op::v0::Constant::create(element::u4, weights_shape, {0}); + auto convert = std::make_shared(weights, element::f16); + disable_constant_folding(convert); + + auto zero_point = op::v0::Constant::create(element::u4, Shape{}, {8}); + auto zp_convert = std::make_shared(zero_point, element::f16); + disable_constant_folding(zp_convert); + + auto subtract = std::make_shared(convert, zp_convert); + mark_as_dequantization_node(subtract); + + auto scale = op::v0::Constant::create(element::f16, scale_shape, {0.1f}); + auto multiply = std::make_shared(subtract, scale); + mark_as_dequantization_node(multiply); + + model_ref = std::make_shared(OutputVector{multiply}, ParameterVector{}); + } + + comparator.enable(FunctionsComparator::CmpValues::CONST_VALUES); + comparator.enable(FunctionsComparator::CmpValues::RUNTIME_KEYS); +} + +// Reshape branch — explicit output_shape that differs from the Multiply output. +// ONNX blocked DequantizeLinear reshapes a [num_blocks, block_size, ...] grid +// back to the input rank. +TEST_F(TransformationTestsF, LowPrecisionDequantize_WithReshape) { + const Shape grid_shape{2, 4, 16}; // num_blocks, block_size, channels + const Shape scale_shape{2, 1, 16}; + const std::vector target_shape{8, 16}; // num_blocks * block_size, channels + + { + auto weights = op::v0::Constant::create(element::u8, grid_shape, {0}); + auto zero_point = op::v0::Constant::create(element::u8, Shape{}, {8}); + auto scale = op::v0::Constant::create(element::f32, scale_shape, {0.1f}); + auto out_shape = op::v0::Constant::create(element::i32, Shape{2}, target_shape); + + auto out = decomposition::low_precision_dequantize(weights, scale, zero_point, out_shape); + model = std::make_shared(OutputVector{out}, ParameterVector{}); + } + + manager.register_pass(kLowPrecisionTypes); + manager.register_pass(); + + { + auto weights = op::v0::Constant::create(element::u8, grid_shape, {0}); + auto convert = std::make_shared(weights, element::f32); + disable_constant_folding(convert); + + auto zero_point = op::v0::Constant::create(element::u8, Shape{}, {8}); + auto zp_convert = std::make_shared(zero_point, element::f32); + disable_constant_folding(zp_convert); + + auto subtract = std::make_shared(convert, zp_convert); + mark_as_dequantization_node(subtract); + + auto scale = op::v0::Constant::create(element::f32, scale_shape, {0.1f}); + auto multiply = std::make_shared(subtract, scale); + mark_as_dequantization_node(multiply); + + auto out_shape = op::v0::Constant::create(element::i32, Shape{2}, target_shape); + auto reshape = std::make_shared(multiply, out_shape, /*special_zero=*/false); + + model_ref = std::make_shared(OutputVector{reshape}, ParameterVector{}); + } + + comparator.enable(FunctionsComparator::CmpValues::CONST_VALUES); + comparator.enable(FunctionsComparator::CmpValues::RUNTIME_KEYS); +} + +// When output_shape matches the Multiply output shape, the helper should skip +// the trailing Reshape entirely. +TEST_F(TransformationTestsF, LowPrecisionDequantize_NoopReshapeSkipped) { + const Shape weights_shape{4, 16}; + const std::vector target_shape{4, 16}; + + { + auto weights = op::v0::Constant::create(element::u8, weights_shape, {1}); + auto zero_point = op::v0::Constant::create(element::u8, Shape{}, {127}); + auto scale = op::v0::Constant::create(element::f32, Shape{}, {0.2f}); + auto out_shape = op::v0::Constant::create(element::i32, Shape{2}, target_shape); + + auto out = decomposition::low_precision_dequantize(weights, scale, zero_point, out_shape); + model = std::make_shared(OutputVector{out}, ParameterVector{}); + } + + manager.register_pass(kLowPrecisionTypes); + manager.register_pass(); + + { + auto weights = op::v0::Constant::create(element::u8, weights_shape, {1}); + auto convert = std::make_shared(weights, element::f32); + disable_constant_folding(convert); + + auto zero_point = op::v0::Constant::create(element::u8, Shape{}, {127}); + auto zp_convert = std::make_shared(zero_point, element::f32); + disable_constant_folding(zp_convert); + + auto subtract = std::make_shared(convert, zp_convert); + mark_as_dequantization_node(subtract); + + auto scale = op::v0::Constant::create(element::f32, Shape{}, {0.2f}); + auto multiply = std::make_shared(subtract, scale); + mark_as_dequantization_node(multiply); + + // No Reshape — helper detects output_shape == current shape and skips it. + model_ref = std::make_shared(OutputVector{multiply}, ParameterVector{}); + } + + comparator.enable(FunctionsComparator::CmpValues::CONST_VALUES); + comparator.enable(FunctionsComparator::CmpValues::RUNTIME_KEYS); +} diff --git a/src/common/transformations/tests/decompositions/rms_norm_decomposition_fuse_test.cpp b/src/common/transformations/tests/decompositions/rms_norm_decomposition_fuse_test.cpp new file mode 100644 index 000000000000..e29951d9d6c4 --- /dev/null +++ b/src/common/transformations/tests/decompositions/rms_norm_decomposition_fuse_test.cpp @@ -0,0 +1,55 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// +// Tests that verify reference decompositions stored in +// `src/common/decompositions/` are always recognised and folded back into +// their corresponding internal fused op by the matching transformation. +// +// These tests act as a guard for both directions: +// * decomposition authors must keep the produced sub-graph fusable; +// * fusion authors must keep accepting the canonical decomposition shape. + +#include + +#include + +#include "common_test_utils/ov_test_utils.hpp" +#include "openvino/core/model.hpp" +#include "openvino/decompositions/rms_norm.hpp" +#include "openvino/op/constant.hpp" +#include "openvino/op/parameter.hpp" +#include "openvino/pass/manager.hpp" +#include "ov_ops/rms.hpp" +#include "transformations/common_optimizations/rms_fusion.hpp" + +using namespace testing; +using namespace ov::pass; + +TEST_F(TransformationTestsF, DecompositionRmsNorm_FusedByRMSFusion_WithGamma) { + const ov::Shape input_shape{1, 2, 6}; + const std::vector gamma_values{0.029f, 0.014f, 0.003f, 0.013f, 0.015f, 0.009f}; + const float eps_value = 1e-5f; + + { + auto input = std::make_shared(ov::element::f32, input_shape); + auto axes = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{1}, {-1}); + auto eps = ov::op::v0::Constant::create(ov::element::f32, ov::Shape{}, {eps_value}); + auto gamma = ov::op::v0::Constant::create(ov::element::f32, ov::Shape{6}, gamma_values); + + ov::pass::NodeRegistry reg; + auto rms = ov::decomposition::rms_norm(reg, input, axes, eps, gamma); + + model = std::make_shared(ov::OutputVector{rms}, ov::ParameterVector{input}); + manager.register_pass(/*force_tail_convert=*/false); + } + { + auto input = std::make_shared(ov::element::f32, input_shape); + auto gamma = ov::op::v0::Constant::create(ov::element::f32, ov::Shape{6}, gamma_values); + auto rms = std::make_shared(input, gamma, eps_value, ov::element::f32); + model_ref = std::make_shared(ov::OutputVector{rms}, ov::ParameterVector{input}); + } + + comparator.enable(FunctionsComparator::CmpValues::ACCURACY); + comparator.enable(FunctionsComparator::CmpValues::CONST_VALUES); + comparator.enable(FunctionsComparator::CmpValues::ATTRIBUTES); +} diff --git a/src/common/transformations/tests/decompositions/rope_decomposition_fuse_test.cpp b/src/common/transformations/tests/decompositions/rope_decomposition_fuse_test.cpp new file mode 100644 index 000000000000..32377a9fd86c --- /dev/null +++ b/src/common/transformations/tests/decompositions/rope_decomposition_fuse_test.cpp @@ -0,0 +1,58 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// +// Tests that verify reference decompositions stored in +// `src/common/decompositions/` are always recognised and folded back into +// their corresponding internal fused op by the matching transformation. + +#include + +#include + +#include "common_test_utils/ov_test_utils.hpp" +#include "openvino/core/model.hpp" +#include "openvino/decompositions/rope.hpp" +#include "openvino/op/parameter.hpp" +#include "openvino/pass/manager.hpp" +#include "ov_ops/rotary_positional_embeddings.hpp" +#include "transformations/common_optimizations/fuse_rotary_positional_embeddings.hpp" + +using namespace testing; +using namespace ov::pass; + +TEST_F(TransformationTestsF, DecompositionRope_FusedByRoPEFusion) { + disable_rt_info_check(); + + const size_t batch = 2; + const size_t num_heads = 4; + const size_t seq_length = 16; + const size_t head_size = 64; + const int64_t half_head_size = static_cast(head_size / 2); + + const ov::Shape x_shape{batch, num_heads, seq_length, head_size}; + const ov::Shape cos_sin_shape{batch, 1, seq_length, head_size / 2}; + + { + auto x = std::make_shared(ov::element::f32, x_shape); + auto cos = std::make_shared(ov::element::f32, cos_sin_shape); + auto sin = std::make_shared(ov::element::f32, cos_sin_shape); + + ov::pass::NodeRegistry reg; + auto rope = ov::decomposition::rope(reg, x, cos, sin, half_head_size); + + model = std::make_shared(ov::OutputVector{rope}, ov::ParameterVector{x, cos, sin}); + manager.register_pass(); + } + { + auto x = std::make_shared(ov::element::f32, x_shape); + auto cos = std::make_shared(ov::element::f32, cos_sin_shape); + auto sin = std::make_shared(ov::element::f32, cos_sin_shape); + + ov::op::internal::RoPE::Config config; + config.rotary_ndims = head_size; + config.cos_sin_ndims = head_size / 2; + auto rope = std::make_shared(ov::OutputVector{x, cos, sin}, config); + + model_ref = std::make_shared(ov::OutputVector{rope}, ov::ParameterVector{x, cos, sin}); + } +} diff --git a/src/common/transformations/tests/op_conversions/convert_fc_to_compressed_test.cpp b/src/common/transformations/tests/op_conversions/convert_fc_to_compressed_test.cpp index 798b8dcd0950..56c0ca5ed0f6 100644 --- a/src/common/transformations/tests/op_conversions/convert_fc_to_compressed_test.cpp +++ b/src/common/transformations/tests/op_conversions/convert_fc_to_compressed_test.cpp @@ -14,6 +14,7 @@ #include "openvino/op/parameter.hpp" #include "openvino/op/reshape.hpp" #include "openvino/op/subtract.hpp" +#include "openvino/op/transpose.hpp" #include "openvino/pass/manager.hpp" #include "ov_ops/fully_connected.hpp" #include "ov_ops/fully_connected_compressed.hpp" @@ -131,3 +132,101 @@ const auto params = std::vector{ } // namespace INSTANTIATE_TEST_SUITE_P(TransformationTests, ConvertFCToCompressed, ::testing::ValuesIn(params)); + +// Regression test: when the matched Transpose acts on the weights tensor and +// scale / zero-point are rank-1 per-output-channel Constants, the old code +// path tried to apply the rank-2 weight perm to a rank-1 input, which +// crashes Transpose shape inference. The fixed `apply_transpose` lambda +// promotes rank-1 per-channel Constants to rank-2 [N, 1] without inserting a +// runtime op, while still transposing the rank-2 weights. +TEST_F(TransformationTestsF, ConvertFCToCompressedRank1ScaleZpWithTranspose) { + const size_t OC = 5; + const size_t IC = 2048; + const auto compressed_type = ov::element::u8; + const std::vector supported_activation_types{ov::element::f32}; + const std::vector supported_weights_types{compressed_type}; + + manager.register_pass(supported_activation_types, + supported_weights_types); + { + auto input = std::make_shared(ov::element::f32, ov::PartialShape{10, IC}); + // Weights laid out as [IC, OC] so that a Transpose([1, 0]) yields [OC, IC] + // (matching the FullyConnected input layout convention). + auto weights_const = ov::op::v0::Constant::create(compressed_type, ov::Shape{IC, OC}, {1}); + auto wei_convert = std::make_shared(weights_const, ov::element::f32); + // Rank-1 per-output-channel zero-point. + auto zp_const = ov::op::v0::Constant::create(compressed_type, ov::Shape{OC}, {1}); + auto zp_convert = std::make_shared(zp_const, ov::element::f32); + auto wei_sub = std::make_shared(wei_convert, zp_convert); + // Rank-1 per-output-channel scale. + auto scale_const = ov::op::v0::Constant::create(ov::element::f32, ov::Shape{OC}, {1}); + auto wei_scale = std::make_shared(wei_sub, scale_const); + auto perm = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{2}, std::vector{1, 0}); + auto wei_t = std::make_shared(wei_scale, perm); + auto bias = std::make_shared(ov::element::f32, ov::Shape{0}); + auto fc = std::make_shared(input, wei_t, bias); + + model = std::make_shared(ov::OutputVector{fc}, ov::ParameterVector{input}); + } + { + auto input = std::make_shared(ov::element::f32, ov::PartialShape{10, IC}); + // Weights Constant stays in its original [IC, OC] layout; the transformation + // re-clones the rank-2 Transpose with the same perm. + auto weights_const = ov::op::v0::Constant::create(compressed_type, ov::Shape{IC, OC}, {1}); + auto perm = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{2}, std::vector{1, 0}); + auto weights_t = std::make_shared(weights_const, perm); + // Rank-1 [OC] -> rank-2 [OC, 1] Constant reshape baked in (no runtime op). + auto scale_const = ov::op::v0::Constant::create(ov::element::f32, ov::Shape{OC, 1}, {1}); + auto zp_const = ov::op::v0::Constant::create(compressed_type, ov::Shape{OC, 1}, {1}); + auto bias = std::make_shared(ov::element::f32, ov::Shape{0}); + auto fc_compressed = + std::make_shared(input, weights_t, bias, scale_const, zp_const); + model_ref = std::make_shared(ov::OutputVector{fc_compressed}, ov::ParameterVector{input}); + } +} + +// Regression test: rank-0 (scalar / per-tensor) scale and zero-point with weights +// Transpose. The old code path applied the rank-2 weight perm to a rank-0 input +// which crashes Transpose shape inference. The fixed `apply_transpose` returns +// rank-0 Constants unchanged while still transposing the rank-2 weights. +TEST_F(TransformationTestsF, ConvertFCToCompressedRank0ScaleZpWithTranspose) { + const size_t OC = 5; + const size_t IC = 2048; + const auto compressed_type = ov::element::u8; + const std::vector supported_activation_types{ov::element::f32}; + const std::vector supported_weights_types{compressed_type}; + + manager.register_pass(supported_activation_types, + supported_weights_types); + { + auto input = std::make_shared(ov::element::f32, ov::PartialShape{10, IC}); + auto weights_const = ov::op::v0::Constant::create(compressed_type, ov::Shape{IC, OC}, {1}); + auto wei_convert = std::make_shared(weights_const, ov::element::f32); + // Rank-0 (scalar) per-tensor zero-point. + auto zp_const = ov::op::v0::Constant::create(compressed_type, ov::Shape{}, {1}); + auto zp_convert = std::make_shared(zp_const, ov::element::f32); + auto wei_sub = std::make_shared(wei_convert, zp_convert); + // Rank-0 (scalar) per-tensor scale. + auto scale_const = ov::op::v0::Constant::create(ov::element::f32, ov::Shape{}, {1}); + auto wei_scale = std::make_shared(wei_sub, scale_const); + auto perm = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{2}, std::vector{1, 0}); + auto wei_t = std::make_shared(wei_scale, perm); + auto bias = std::make_shared(ov::element::f32, ov::Shape{0}); + auto fc = std::make_shared(input, wei_t, bias); + + model = std::make_shared(ov::OutputVector{fc}, ov::ParameterVector{input}); + } + { + auto input = std::make_shared(ov::element::f32, ov::PartialShape{10, IC}); + auto weights_const = ov::op::v0::Constant::create(compressed_type, ov::Shape{IC, OC}, {1}); + auto perm = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{2}, std::vector{1, 0}); + auto weights_t = std::make_shared(weights_const, perm); + // Rank-0 scale / zp pass through apply_transpose unchanged. + auto scale_const = ov::op::v0::Constant::create(ov::element::f32, ov::Shape{}, {1}); + auto zp_const = ov::op::v0::Constant::create(compressed_type, ov::Shape{}, {1}); + auto bias = std::make_shared(ov::element::f32, ov::Shape{0}); + auto fc_compressed = + std::make_shared(input, weights_t, bias, scale_const, zp_const); + model_ref = std::make_shared(ov::OutputVector{fc_compressed}, ov::ParameterVector{input}); + } +} diff --git a/src/common/transformations/tests/op_conversions/convert_weight_compressed_conv1x1_to_matmul_test.cpp b/src/common/transformations/tests/op_conversions/convert_weight_compressed_conv1x1_to_matmul_test.cpp index 20344930bafe..8aea8529fd71 100644 --- a/src/common/transformations/tests/op_conversions/convert_weight_compressed_conv1x1_to_matmul_test.cpp +++ b/src/common/transformations/tests/op_conversions/convert_weight_compressed_conv1x1_to_matmul_test.cpp @@ -41,20 +41,23 @@ struct Conv1x1ToMatmulTestParams { bool with_convert; bool with_param_weight; bool with_act_new_reshape; + bool with_batched_input; std::string activation_op_type; }; std::shared_ptr gen_model(const Conv1x1ToMatmulTestParams& p) { - auto input = std::make_shared( - ov::element::f16, - (p.activation_op_type == "Reshape" && p.with_act_new_reshape) ? ov::Shape{1, 1, 2, 5} : ov::Shape{1, 1, 1, 10}); + int input_batch = p.with_batched_input ? 4 : 1; + auto input = std::make_shared(ov::element::f16, + (p.activation_op_type == "Reshape" && p.with_act_new_reshape) + ? ov::Shape{(size_t)input_batch, 1, 2, 5} + : ov::Shape{(size_t)input_batch, 1, 1, 10}); std::shared_ptr act_node; if (p.activation_op_type == "Transpose") { auto transpose_const = ov::opset1::Constant::create(ov::element::i32, ov::Shape{4}, {0, 3, 1, 2}); act_node = std::make_shared(input, transpose_const); } else { - auto reshape_const = ov::opset1::Constant::create(ov::element::i32, ov::Shape{4}, {1, 10, 1, 1}); + auto reshape_const = ov::opset1::Constant::create(ov::element::i32, ov::Shape{4}, {input_batch, 10, 1, 1}); act_node = std::make_shared(input, reshape_const, false); } @@ -118,7 +121,7 @@ std::shared_ptr gen_model(const Conv1x1ToMatmulTestParams& p) { auto transpose_const = ov::opset1::Constant::create(ov::element::i32, ov::Shape{4}, {0, 2, 3, 1}); out_node = std::make_shared(current_node, transpose_const); } else { - auto reshape_const = ov::opset1::Constant::create(ov::element::i32, ov::Shape{4}, {1, 1, 1, 15}); + auto reshape_const = ov::opset1::Constant::create(ov::element::i32, ov::Shape{4}, {input_batch, 1, 1, 15}); out_node = std::make_shared(current_node, reshape_const, false); } @@ -126,9 +129,11 @@ std::shared_ptr gen_model(const Conv1x1ToMatmulTestParams& p) { } std::shared_ptr gen_model_ref(const Conv1x1ToMatmulTestParams& p) { - auto input = std::make_shared( - ov::element::f16, - (p.activation_op_type == "Reshape" && p.with_act_new_reshape) ? ov::Shape{1, 1, 2, 5} : ov::Shape{1, 1, 1, 10}); + int input_batch = p.with_batched_input ? 4 : 1; + auto input = std::make_shared(ov::element::f16, + (p.activation_op_type == "Reshape" && p.with_act_new_reshape) + ? ov::Shape{(size_t)input_batch, 1, 2, 5} + : ov::Shape{(size_t)input_batch, 1, 1, 10}); std::shared_ptr weights_node; ov::ParameterVector params = {input}; @@ -170,7 +175,7 @@ std::shared_ptr gen_model_ref(const Conv1x1ToMatmulTestParams& p) { std::shared_ptr act_node = input; if (p.activation_op_type == "Reshape" && p.with_act_new_reshape) { - auto reshape_const = ov::opset1::Constant::create(ov::element::i64, ov::Shape{4}, {1, 1, 1, 10}); + auto reshape_const = ov::opset1::Constant::create(ov::element::i64, ov::Shape{4}, {1, 1, input_batch, 10}); act_node = std::make_shared(input, reshape_const, false); } auto matmul = std::make_shared(act_node, mul, false, true); @@ -178,7 +183,7 @@ std::shared_ptr gen_model_ref(const Conv1x1ToMatmulTestParams& p) { if (p.with_bias) { auto bias_const = ov::opset1::Constant::create(ov::element::f16, ov::Shape{1, 1, 1, 15}, {1}); - current_node = std::make_shared(matmul, bias_const); + current_node = std::make_shared(current_node, bias_const); } if (p.with_convert) { current_node = std::make_shared(current_node, ov::element::f32); @@ -186,7 +191,7 @@ std::shared_ptr gen_model_ref(const Conv1x1ToMatmulTestParams& p) { std::shared_ptr out_node; if (p.activation_op_type == "Reshape") { - auto reshape_const = ov::opset1::Constant::create(ov::element::i32, ov::Shape{4}, {1, 1, 1, 15}); + auto reshape_const = ov::opset1::Constant::create(ov::element::i32, ov::Shape{4}, {input_batch, 1, 1, 15}); out_node = std::make_shared(current_node, reshape_const, false); } else { out_node = current_node; @@ -198,16 +203,17 @@ std::shared_ptr gen_model_ref(const Conv1x1ToMatmulTestParams& p) { class ConvertWeightCompressedConv1x1ToMatmulTest : public TransformationTestsF, - public WithParamInterface> { + public WithParamInterface> { public: static std::string get_test_case_name( - const testing::TestParamInfo>& obj) { + const testing::TestParamInfo>& obj) { const auto& [with_group_quant, with_zp, with_bias, with_convert, with_param_weight, with_act_new_reshape, + with_batched_input, activation_op_type] = obj.param; std::ostringstream result; @@ -217,6 +223,7 @@ class ConvertWeightCompressedConv1x1ToMatmulTest result << "with_convert=" << with_convert << "_"; result << "with_param_weight=" << with_param_weight << "_"; result << "with_act_new_reshape=" << with_act_new_reshape << "_"; + result << "with_batched_input=" << with_batched_input << "_"; result << "activation_op_type=" << activation_op_type; return result.str(); } @@ -230,6 +237,7 @@ class ConvertWeightCompressedConv1x1ToMatmulTest with_convert, with_param_weight, with_act_new_reshape, + with_batched_input, activation_op_type] = GetParam(); Conv1x1ToMatmulTestParams params{with_group_quant, with_zp, @@ -237,6 +245,7 @@ class ConvertWeightCompressedConv1x1ToMatmulTest with_convert, with_param_weight, with_act_new_reshape, + with_batched_input, activation_op_type}; model = gen_model(params); model_ref = gen_model_ref(params); @@ -254,6 +263,7 @@ INSTANTIATE_TEST_SUITE_P(TransformationTests, ::testing::Bool(), ::testing::Bool(), ::testing::Bool(), + ::testing::Bool(), ::testing::Values("Transpose", "Reshape")), ConvertWeightCompressedConv1x1ToMatmulTest::get_test_case_name); diff --git a/src/common/transformations/tests/op_conversions/sdpa_to_paged_attention_test.cpp b/src/common/transformations/tests/op_conversions/sdpa_to_paged_attention_test.cpp index 77870c91ae34..62f42bfba6c9 100644 --- a/src/common/transformations/tests/op_conversions/sdpa_to_paged_attention_test.cpp +++ b/src/common/transformations/tests/op_conversions/sdpa_to_paged_attention_test.cpp @@ -58,11 +58,12 @@ #include "openvino/op/unsqueeze.hpp" #include "openvino/op/util/variable.hpp" #include "openvino/op/variadic_split.hpp" -#include "openvino/pass/visualize_tree.hpp" -#include "transformations/sdpa_to_paged_attention/position_ids_replacer.hpp" -#include "transformations/sdpa_to_paged_attention/prev_sequence_length_pattern.hpp" -#include "transformations/sdpa_to_paged_attention/state_management_pattern.hpp" -#include "transformations/sdpa_to_paged_attention/total_sequence_length_pattern.hpp" +#include "transformations/paged_attention/eliminate_conv_padding_mask_gating.hpp" +#include "transformations/paged_attention/position_ids_replacer.hpp" +#include "transformations/paged_attention/prev_sequence_length_pattern.hpp" +#include "transformations/paged_attention/state_management_pattern.hpp" +#include "transformations/paged_attention/total_sequence_length_pattern.hpp" +#include "transformations/rt_info/keep_const_precision.hpp" #include "transformations/utils/gen_pattern.hpp" using namespace ov; @@ -665,6 +666,67 @@ TEST_P(SDPAToPATest, SDPAToPA_Qwen7bChat_General) { disable_rt_info_check(); } +TEST(SDPAToPAKeepConstPrecisionTest, Qwen7bChat_KVCacheParamsMarkedKeepConstPrecision) { + for (const auto& model_precision : std::vector{element::f16, element::f32}) { + auto beam_idx = makeOP({}, {{"shape", PartialShape{DYN}}, el_type_i64}); + auto position_ids = makeOP({}, {{"shape", PartialShape{DYN, DYN}}, el_type_i64}); + auto attention_mask = makeOP({}, {{"shape", PartialShape{DYN, DYN}}, el_type_i64}); + auto input_ids = makeOP({}, {{"shape", PartialShape{DYN, DYN}}, el_type_i64}); + NodeVector input_nodes{input_ids, attention_mask, position_ids, beam_idx}; + auto params = nodes_to_params(input_nodes); + + beam_idx->output(0).add_names({"beam_idx"}); + position_ids->output(0).add_names({"position_ids"}); + attention_mask->output(0).add_names({"attention_mask"}); + input_ids->output(0).add_names({"input_ids"}); + + auto embeddings = Qwen7bChatSDPA::gen_embeddings(input_ids); + auto qkv_proj = Qwen7bChatSDPA::gen_qkv_proj(embeddings); + + auto k_cache = Qwen7bChatSDPA::gen_cache(input_ids, beam_idx, "K_cache"); + auto v_cache = Qwen7bChatSDPA::gen_cache(input_ids, beam_idx, "V_cache"); + + auto current_seq_len = Qwen7bChatSDPA::gen_current_len(input_ids); + auto past_seq_len = Qwen7bChatSDPA::gen_past_len(k_cache); + auto total_seq_len = Qwen7bChatSDPA::gen_total_len(current_seq_len, past_seq_len); + + auto neg_cur_seq_len = Qwen7bChatSDPA::neg_mul(current_seq_len); + auto head_size = shared_ptr(); + auto rope_emb_sin = + Qwen7bChatSDPA::gen_rope_emb_sin(total_seq_len, neg_cur_seq_len, head_size, model_precision); + auto rope_emb_cos = Qwen7bChatSDPA::gen_rope_emb_cos(total_seq_len, neg_cur_seq_len, model_precision); + + auto rope_q = Qwen7bChatSDPA::gen_rope(QKV::Q, qkv_proj, head_size, rope_emb_sin, rope_emb_cos); + auto rope_k = Qwen7bChatSDPA::gen_rope(QKV::K, qkv_proj, head_size, rope_emb_sin, rope_emb_cos); + + auto total_seq_len_2 = Qwen7bChatSDPA::gen_total_seq_len_2(past_seq_len, rope_k); + auto past_seq_len_2 = Qwen7bChatSDPA::gen_past_seq_len_2(total_seq_len_2, rope_q); + + auto Q = Qwen7bChatSDPA::gen_Q(past_seq_len_2, total_seq_len_2, rope_q); + auto K = Qwen7bChatSDPA::gen_K(k_cache, rope_k); + auto V = Qwen7bChatSDPA::gen_V(v_cache, qkv_proj); + + auto attention_mask_to_sdpa = Qwen7bChatSDPA::gen_attention_mask(Q, attention_mask, total_seq_len_2); + auto sdpa = std::make_shared(Q, K, V, attention_mask_to_sdpa, false); + auto res = std::make_shared(sdpa); + + auto transformed_model = std::make_shared(ResultVector{res}, params); + ov::pass::Manager local_manager; + local_manager.register_pass(); + local_manager.run_passes(transformed_model); + + size_t kv_cache_params_count = 0; + for (const auto& parameter : transformed_model->get_parameters()) { + const std::string& parameter_name = parameter->get_friendly_name(); + if (parameter_name.rfind("key_cache.", 0) == 0 || parameter_name.rfind("value_cache.", 0) == 0) { + kv_cache_params_count += 1; + EXPECT_TRUE(is_keep_const_precision(parameter)) << "Parameter is not marked: " << parameter_name; + } + } + EXPECT_GT(kv_cache_params_count, 0); + } +} + TEST_F(SDPAToPATest, SDPAToPA_Qwen7bChat_TotalSequenceLengthPattern) { { // Inputs to SDPA transformer: @@ -782,6 +844,250 @@ TEST_F(SDPAToPATest, SDPAToPA_Qwen7bChat_PositionIDsReplacerQwenPattern) { disable_rt_info_check(); } +TEST_F(SDPAToPATest, SDPAToPA_PositionIDsReplacerLFM2_DequantStartBranch) { + { + auto max_context_len = std::make_shared(element::i64, PartialShape{}); + auto prev_seq_len = std::make_shared(element::i64, PartialShape{}); + auto curr_seq_len = std::make_shared(element::i64, PartialShape{}); + auto lhs = std::make_shared(element::f32, PartialShape{1, 1, 1}); + auto position_ids = std::make_shared(element::i64, PartialShape{1, DYN}); + + auto start_subtract = std::make_shared(max_context_len, prev_seq_len); + auto start = std::make_shared(start_subtract, element::i64); + auto end = std::make_shared(start, curr_seq_len); + auto step = v0::Constant::create(element::i64, Shape{}, {1}); + auto range = std::make_shared(start, end, step, element::i64); + range->set_friendly_name("lfm2_range"); + + auto unsqueeze_axis = v0::Constant::create(element::i64, Shape{1}, {0}); + auto unsqueeze_1 = std::make_shared(range, unsqueeze_axis); + auto unsqueeze_2 = std::make_shared(unsqueeze_1, unsqueeze_axis); + auto convert = std::make_shared(unsqueeze_2, element::f32); + auto matmul = std::make_shared(lhs, convert, false, false); + auto transpose = + std::make_shared(matmul, v0::Constant::create(element::i64, Shape{3}, {0, 2, 1})); + auto concat = std::make_shared(OutputVector{transpose, transpose}, 2); + auto cos = std::make_shared(concat); + auto sin = std::make_shared(concat); + auto q = std::make_shared(element::f32, PartialShape::dynamic()); + auto mul_l = std::make_shared(cos, q); + auto mul_r = std::make_shared(sin, q); + auto add = std::make_shared(mul_l, mul_r); + auto result = std::make_shared(add); + + model = + std::make_shared(ResultVector{result}, + ParameterVector{max_context_len, prev_seq_len, curr_seq_len, lhs, position_ids, q}); + manager.register_pass(position_ids); + } + + { + auto max_context_len = std::make_shared(element::i64, PartialShape{}); + auto prev_seq_len = std::make_shared(element::i64, PartialShape{}); + auto curr_seq_len = std::make_shared(element::i64, PartialShape{}); + auto lhs = std::make_shared(element::f32, PartialShape{1, 1, 1}); + auto position_ids = std::make_shared(element::i64, PartialShape{1, DYN}); + + auto pos_ids_shape = v0::Constant::create(element::i64, Shape{1}, {-1}); + auto position_ids_1d = std::make_shared(position_ids, pos_ids_shape, false); + position_ids_1d->set_friendly_name("lfm2_range_position_ids"); + + auto unsqueeze_axis = v0::Constant::create(element::i64, Shape{1}, {0}); + auto unsqueeze_1 = std::make_shared(position_ids_1d, unsqueeze_axis); + auto unsqueeze_2 = std::make_shared(unsqueeze_1, unsqueeze_axis); + auto convert = std::make_shared(unsqueeze_2, element::f32); + auto matmul = std::make_shared(lhs, convert, false, false); + auto transpose = + std::make_shared(matmul, v0::Constant::create(element::i64, Shape{3}, {0, 2, 1})); + auto concat = std::make_shared(OutputVector{transpose, transpose}, 2); + auto concat_transpose = + std::make_shared(concat, v0::Constant::create(element::i64, Shape{3}, {1, 0, 2})); + auto cos = std::make_shared(concat_transpose); + auto sin = std::make_shared(concat_transpose); + auto q = std::make_shared(element::f32, PartialShape::dynamic()); + auto mul_l = std::make_shared(cos, q); + auto mul_r = std::make_shared(sin, q); + auto add = std::make_shared(mul_l, mul_r); + + model_ref = + std::make_shared(OutputVector{add}, + ParameterVector{max_context_len, prev_seq_len, curr_seq_len, lhs, position_ids, q}); + } + + comparator.enable(FunctionsComparator::ATTRIBUTES); +} + +TEST_F(SDPAToPATest, SDPAToPA_PositionIDsReplacerLFM2_DirectParameterBranch) { + { + auto max_context_len = std::make_shared(element::i64, PartialShape{}); + auto curr_seq_len = std::make_shared(element::i64, PartialShape{}); + auto lhs = std::make_shared(element::f32, PartialShape{1, 1, 1}); + auto position_ids = std::make_shared(element::i64, PartialShape{DYN}); + + auto end = std::make_shared(max_context_len, curr_seq_len); + auto step = v0::Constant::create(element::i64, Shape{}, {1}); + auto range = std::make_shared(max_context_len, end, step, element::i64); + range->set_friendly_name("lfm2_range"); + + auto shape_3d = v0::Constant::create(element::i64, Shape{3}, {1, 1, -1}); + auto reshape = std::make_shared(range, shape_3d, false); + auto convert = std::make_shared(reshape, element::f32); + auto matmul = std::make_shared(lhs, convert, false, false); + auto concat = std::make_shared(OutputVector{matmul, matmul}, 2); + auto cos = std::make_shared(concat); + auto sin = std::make_shared(concat); + auto q = std::make_shared(element::f32, PartialShape::dynamic()); + auto mul_l = std::make_shared(cos, q); + auto mul_r = std::make_shared(sin, q); + auto add = std::make_shared(mul_l, mul_r); + auto result = std::make_shared(add); + + model = std::make_shared(ResultVector{result}, + ParameterVector{max_context_len, curr_seq_len, lhs, position_ids, q}); + manager.register_pass(position_ids); + } + + { + auto max_context_len = std::make_shared(element::i64, PartialShape{}); + auto curr_seq_len = std::make_shared(element::i64, PartialShape{}); + auto lhs = std::make_shared(element::f32, PartialShape{1, 1, 1}); + auto position_ids = std::make_shared(element::i64, PartialShape{DYN}); + + auto shape_3d = v0::Constant::create(element::i64, Shape{3}, {1, 1, -1}); + auto reshape = std::make_shared(position_ids, shape_3d, false); + auto convert = std::make_shared(reshape, element::f32); + auto matmul = std::make_shared(lhs, convert, false, false); + auto concat = std::make_shared(OutputVector{matmul, matmul}, 2); + auto concat_transpose = + std::make_shared(concat, v0::Constant::create(element::i64, Shape{3}, {1, 0, 2})); + auto cos = std::make_shared(concat_transpose); + auto sin = std::make_shared(concat_transpose); + auto q = std::make_shared(element::f32, PartialShape::dynamic()); + auto mul_l = std::make_shared(cos, q); + auto mul_r = std::make_shared(sin, q); + auto add = std::make_shared(mul_l, mul_r); + + model_ref = std::make_shared(OutputVector{add}, + ParameterVector{max_context_len, curr_seq_len, lhs, position_ids, q}); + } + + comparator.enable(FunctionsComparator::ATTRIBUTES); +} + +TEST_F(SDPAToPATest, SDPAToPA_PositionIDsReplacerLFM2_DirectParameterBranchRank3ConcatTranspose) { + { + auto max_context_len = std::make_shared(element::i64, PartialShape{}); + auto curr_seq_len = std::make_shared(element::i64, PartialShape{}); + auto lhs = std::make_shared(element::f32, PartialShape{1, 1, 1}); + auto position_ids = std::make_shared(element::i64, PartialShape{DYN}); + + auto end = std::make_shared(max_context_len, curr_seq_len); + auto step = v0::Constant::create(element::i64, Shape{}, {1}); + auto range = std::make_shared(max_context_len, end, step, element::i64); + range->set_friendly_name("lfm2_range"); + + auto shape_3d = v0::Constant::create(element::i64, Shape{3}, {1, 1, -1}); + auto reshape = std::make_shared(range, shape_3d, false); + auto convert = std::make_shared(reshape, element::f32); + auto matmul = std::make_shared(lhs, convert, false, false); + auto concat = std::make_shared(OutputVector{matmul, matmul}, 2); + auto cos = std::make_shared(concat); + auto sin = std::make_shared(concat); + auto q = std::make_shared(element::f32, PartialShape::dynamic()); + auto mul_l = std::make_shared(cos, q); + auto mul_r = std::make_shared(sin, q); + auto add = std::make_shared(mul_l, mul_r); + auto result = std::make_shared(add); + + model = std::make_shared(ResultVector{result}, + ParameterVector{max_context_len, curr_seq_len, lhs, position_ids, q}); + manager.register_pass(position_ids); + } + + { + auto max_context_len = std::make_shared(element::i64, PartialShape{}); + auto curr_seq_len = std::make_shared(element::i64, PartialShape{}); + auto lhs = std::make_shared(element::f32, PartialShape{1, 1, 1}); + auto position_ids = std::make_shared(element::i64, PartialShape{DYN}); + + auto shape_3d = v0::Constant::create(element::i64, Shape{3}, {1, 1, -1}); + auto reshape = std::make_shared(position_ids, shape_3d, false); + auto convert = std::make_shared(reshape, element::f32); + auto matmul = std::make_shared(lhs, convert, false, false); + auto concat = std::make_shared(OutputVector{matmul, matmul}, 2); + auto concat_transpose = + std::make_shared(concat, v0::Constant::create(element::i64, Shape{3}, {1, 0, 2})); + auto cos = std::make_shared(concat_transpose); + auto sin = std::make_shared(concat_transpose); + auto q = std::make_shared(element::f32, PartialShape::dynamic()); + auto mul_l = std::make_shared(cos, q); + auto mul_r = std::make_shared(sin, q); + auto add = std::make_shared(mul_l, mul_r); + + model_ref = std::make_shared(OutputVector{add}, + ParameterVector{max_context_len, curr_seq_len, lhs, position_ids, q}); + } + + comparator.enable(FunctionsComparator::ATTRIBUTES); +} + +TEST_F(SDPAToPATest, SDPAToPA_PositionIDsReplacerLFM2_DirectParameterBranchRank4ConcatTranspose) { + { + auto max_context_len = std::make_shared(element::i64, PartialShape{}); + auto curr_seq_len = std::make_shared(element::i64, PartialShape{}); + auto lhs = std::make_shared(element::f32, PartialShape{1, 1, 1, 1}); + auto position_ids = std::make_shared(element::i64, PartialShape{DYN}); + + auto end = std::make_shared(max_context_len, curr_seq_len); + auto step = v0::Constant::create(element::i64, Shape{}, {1}); + auto range = std::make_shared(max_context_len, end, step, element::i64); + range->set_friendly_name("lfm2_range"); + + auto shape_4d = v0::Constant::create(element::i64, Shape{4}, {1, 1, 1, -1}); + auto reshape = std::make_shared(range, shape_4d, false); + auto convert = std::make_shared(reshape, element::f32); + auto matmul = std::make_shared(lhs, convert, false, false); + auto concat = std::make_shared(OutputVector{matmul, matmul}, 3); + auto cos = std::make_shared(concat); + auto sin = std::make_shared(concat); + auto q = std::make_shared(element::f32, PartialShape::dynamic()); + auto mul_l = std::make_shared(cos, q); + auto mul_r = std::make_shared(sin, q); + auto add = std::make_shared(mul_l, mul_r); + auto result = std::make_shared(add); + + model = std::make_shared(ResultVector{result}, + ParameterVector{max_context_len, curr_seq_len, lhs, position_ids, q}); + manager.register_pass(position_ids); + } + + { + auto max_context_len = std::make_shared(element::i64, PartialShape{}); + auto curr_seq_len = std::make_shared(element::i64, PartialShape{}); + auto lhs = std::make_shared(element::f32, PartialShape{1, 1, 1, 1}); + auto position_ids = std::make_shared(element::i64, PartialShape{DYN}); + + auto shape_4d = v0::Constant::create(element::i64, Shape{4}, {1, 1, 1, -1}); + auto reshape = std::make_shared(position_ids, shape_4d, false); + auto convert = std::make_shared(reshape, element::f32); + auto matmul = std::make_shared(lhs, convert, false, false); + auto concat = std::make_shared(OutputVector{matmul, matmul}, 3); + auto concat_transpose = + std::make_shared(concat, v0::Constant::create(element::i64, Shape{4}, {2, 1, 0, 3})); + auto cos = std::make_shared(concat_transpose); + auto sin = std::make_shared(concat_transpose); + auto q = std::make_shared(element::f32, PartialShape::dynamic()); + auto mul_l = std::make_shared(cos, q); + auto mul_r = std::make_shared(sin, q); + auto add = std::make_shared(mul_l, mul_r); + + model_ref = std::make_shared(OutputVector{add}, + ParameterVector{max_context_len, curr_seq_len, lhs, position_ids, q}); + } + + comparator.enable(FunctionsComparator::ATTRIBUTES); +} + // TODO: split the models in blocks the way it's done for Qwen and make the code not to be such a clutter // TODO: write a test for StateManagementPattern only (because changes for Alibi are inside it) // TODO: align precisions, check the copying of "fuse_names" attr in SDPAToPagedAttention @@ -3275,7 +3581,8 @@ TEST_F(SDPAToPATest, SDPAToPA_LFM2) { auto MatMul11 = makeOP({Convert21, Convert22}, {{"transpose_a", false}, {"transpose_b", false}}); auto Transpose10 = makeOP({MatMul11, {0, 2, 1}}); auto Concat8 = makeOP({Transpose10, Transpose10}, {{"axis", -1}}); - auto Cos0 = makeOP({Concat8}); + auto Transpose11_rope = makeOP({Concat8, {1, 0, 2}}); + auto Cos0 = makeOP({Transpose11_rope}); auto Unsqueeze7 = makeOP({Cos0, 1}); auto Multiply20 = makeOP({Transpose9, Unsqueeze7}, {{"auto_broadcast", "numpy"}}); auto Slice4 = makeOP({Transpose9, {2}, {LLONG_MAX}, {1}, {3}}); @@ -3283,7 +3590,7 @@ TEST_F(SDPAToPATest, SDPAToPA_LFM2) { auto Multiply21 = makeOP({Slice4, Convert23}, {{"auto_broadcast", "numpy"}}); auto Slice5 = makeOP({Transpose9, {0}, {2}, {1}, {3}}); auto Concat9 = makeOP({Multiply21, Slice5}, {{"axis", -1}}); - auto Sin0 = makeOP({Concat8}); + auto Sin0 = makeOP({Transpose11_rope}); auto Unsqueeze8 = makeOP({Sin0, 1}); auto Multiply22 = makeOP({Concat9, Unsqueeze8}, {{"auto_broadcast", "numpy"}}); auto Add18 = makeOP({Multiply20, Multiply22}, {{"auto_broadcast", "numpy"}}); @@ -4086,7 +4393,8 @@ TEST_F(SDPAToPATest, SDPAToPA_LFM2) { auto MatMul11 = makeOP({Convert23, Convert24}, {{"transpose_a", false}, {"transpose_b", false}}); auto Transpose10 = makeOP({MatMul11, {0, 2, 1}}); auto Concat7 = makeOP({Transpose10, Transpose10}, {{"axis", -1}}); - auto Cos0 = makeOP({Concat7}); + auto Transpose10_rope = makeOP({Concat7, {1, 0, 2}}); + auto Cos0 = makeOP({Transpose10_rope}); auto Unsqueeze8 = makeOP({Cos0, 1}); auto Multiply20 = makeOP({Transpose9, Unsqueeze8}, {{"auto_broadcast", "numpy"}}); auto Slice4 = makeOP({Transpose9, {2}, {LLONG_MAX}, {1}, {3}}); @@ -4094,7 +4402,7 @@ TEST_F(SDPAToPATest, SDPAToPA_LFM2) { auto Multiply21 = makeOP({Slice4, Convert25}, {{"auto_broadcast", "numpy"}}); auto Slice5 = makeOP({Transpose9, {0}, {2}, {1}, {3}}); auto Concat8 = makeOP({Multiply21, Slice5}, {{"axis", -1}}); - auto Sin0 = makeOP({Concat7}); + auto Sin0 = makeOP({Transpose10_rope}); auto Unsqueeze9 = makeOP({Sin0, 1}); auto Multiply22 = makeOP({Concat8, Unsqueeze9}, {{"auto_broadcast", "numpy"}}); auto Add18 = makeOP({Multiply20, Multiply22}, {{"auto_broadcast", "numpy"}}); @@ -5499,6 +5807,382 @@ TEST_F(SDPAToPATest, SDPAToPA_Gemma3_TokenTypeIds) { } } +TEST(SDPAToPA, Gemma3n_SharedKVCache_TwoLayersSameReadValue) { + // Replicates real Gemma 3n architecture: + // - inputs_embeds [?,?,2048], 2 KV heads, 8 Q heads, head_dim=256, GQA ratio=4 + // - RMSNorm → K/V/Q projections → KV cache → GQA broadcast → SDPA + // - Layers 18 and 20 share the same K/V cache (same ReadValue variable_id) + + const int hidden_size = 2048; + const int kv_heads = 2; + const int q_heads = 8; + const int head_dim = 256; + const int kv_proj_size = kv_heads * head_dim; // 512 + const int q_proj_size = q_heads * head_dim; // 2048 + const int gqa_ratio = q_heads / kv_heads; // 4 + + // Linear projection (MatMul → Reshape → Transpose) + // Returns transposed output with shape [batch, heads, seq, head_dim] + auto make_projection = [&](std::shared_ptr input, int out_size, int heads) { + auto weight = makeConst(element::f32, ov::Shape({(size_t)out_size, (size_t)hidden_size}), MOCK_VALUE); + auto matmul = makeOP({input, weight}, {{"transpose_a", false}, {"transpose_b", true}}); + auto reshape = makeOP({matmul, {0, 0, heads, head_dim}}, {{"special_zero", true}}); + return makeOP({reshape, {0, 2, 1, 3}}); + }; + + // KV cache path (Variable → ReadValue → Gather(beam_idx) → Concat(past, cur)) + // Returns {concat, past, assign, variable} + struct KVCacheResult { + std::shared_ptr concat; + std::shared_ptr past; + std::shared_ptr assign; + }; + auto make_kv_cache = [&](std::shared_ptr cur, + const std::string& var_id, + std::shared_ptr batch_gather, + std::shared_ptr beam) -> KVCacheResult { + auto var = std::make_shared( + ov::op::util::VariableInfo{ov::PartialShape{DYN, kv_heads, DYN, head_dim}, ov::element::f32, var_id}); + auto init_shape = + makeOP({batch_gather, {(int64_t)kv_heads}, {0l}, {(int64_t)head_dim}}, {{"axis", 0}}); + auto init = makeOP({0.0f, init_shape}, {{"mode", "numpy"}}); + std::shared_ptr read = std::make_shared(init, var); + auto past = makeOP({read, beam, 0}, {{"batch_dims", 0}}); + auto concat = makeOP({past, cur}, {{"axis", -2}}); + auto assign = std::make_shared(concat, var); + return {concat, past, assign}; + }; + + // GQA broadcast (Unsqueeze → Broadcast → Reshape to expand KV heads to Q heads) + auto make_gqa_broadcast = [&](std::shared_ptr kv_concat, std::shared_ptr bcast_shape) { + auto unsqueeze = makeOP({kv_concat, 2}); + auto broadcast = makeOP({unsqueeze, bcast_shape}, {{"mode", "bidirectional"}}); + return makeOP({broadcast, {0, q_heads, -1, head_dim}}, {{"special_zero", true}}); + }; + + // Stub mask subgraph: only ensures position_ids and attention_mask are connected to the graph. + // The transformation removes attention_mask and rewires position_ids, so it doesn't inspect mask internals. + auto make_attn_mask = + [&](std::shared_ptr pos_ids, std::shared_ptr attn_mask, std::shared_ptr kv_past) { + auto shape_pos = makeOP({pos_ids}, {{"output_type", "i64"}}); + auto cur_len = makeOP({shape_pos, 1, 0}, {{"batch_dims", 0}}); + auto cur_len_1d = makeOP({cur_len, {1}}, {{"special_zero", false}}); + auto cur_len_scalar = makeOP({cur_len_1d, 0}); + + auto shape_past = makeOP({kv_past}, {{"output_type", "i64"}}); + auto past_len = makeOP({shape_past, 2, 0}, {{"batch_dims", 0}}); + + auto total_len = makeOP({cur_len_scalar, past_len}, {{"auto_broadcast", "numpy"}}); + auto total_len_unsq = makeOP({total_len, 0}); + auto batch_dim = makeOP({shape_pos, {0}, 0}, {{"batch_dims", 0}}); + auto bcast_shape = makeOP({batch_dim, {1l}, cur_len_1d, total_len_unsq}, {{"axis", 0}}); + + auto mask_f32 = makeOP({attn_mask}, {{"destination_type", "f32"}}); + auto mask_bcast = makeOP({mask_f32, bcast_shape}, {{"mode", "bidirectional"}}); + auto past_len_1d = makeOP({past_len, {1}}, {{"special_zero", false}}); + auto slice_end = makeOP({past_len_1d, cur_len_1d}, {{"auto_broadcast", "numpy"}}); + return makeOP({mask_bcast, {0}, slice_end, {1}, {3}}); + }; + + // RMSNorm: x / sqrt(mean(x^2) + eps) * weight + auto make_rms_norm = [&](std::shared_ptr input, int size) { + auto power = makeOP({input, single_val(3, 2.0f)}, {{"auto_broadcast", "numpy"}}); + auto mean = makeOP({power, {-1}}, {{"keep_dims", true}}); + auto add_eps = makeOP({mean, single_val(3, 1e-6f)}, {{"auto_broadcast", "numpy"}}); + auto sqrt = makeOP({add_eps}); + auto div = makeOP({input, sqrt}, {{"auto_broadcast", "numpy"}, {"m_pythondiv", true}}); + auto weight = makeConst(element::f32, ov::Shape({1, 1, (size_t)size}), MOCK_VALUE); + return makeOP({div, weight}, {{"auto_broadcast", "numpy"}}); + }; + + auto beam_idx = make_param(PartialShape{DYN}, element::i32, "beam_idx"); + auto position_ids = make_param(PartialShape{DYN, DYN}, element::i64, "position_ids"); + auto attention_mask = make_param(PartialShape{DYN, DYN}, element::i64, "attention_mask"); + auto inputs_embeds = make_param(PartialShape{DYN, DYN, hidden_size}, element::f32, "inputs_embeds"); + auto params = nodes_to_params({beam_idx, position_ids, attention_mask, inputs_embeds}); + + auto ShapeOf0 = makeOP({inputs_embeds}, {{"output_type", "i64"}}); + auto Gather0 = makeOP({ShapeOf0, {0}, 0}, {{"batch_dims", 0}}); + + auto ln_out = make_rms_norm(inputs_embeds, hidden_size); + + // K/V projections and cache + auto K_cur = make_projection(ln_out, kv_proj_size, kv_heads); + auto V_cur = make_projection(ln_out, kv_proj_size, kv_heads); + + auto [k_concat, k_past, k_assign] = make_kv_cache(K_cur, "past_key_values.18.keypresent.18.key", Gather0, beam_idx); + auto [v_concat, v_past, v_assign] = + make_kv_cache(V_cur, "past_key_values.18.valuepresent.18.value", Gather0, beam_idx); + + // GQA broadcast shape (shared between K and V) + auto k_shape = makeOP({k_concat}, {{"output_type", "i64"}}); + auto k_gather_dims = makeOP({k_shape, {0, 1}, 0}, {{"batch_dims", 0}}); + auto k_gather_dims2 = makeOP({k_shape, {2, 3}, 0}, {{"batch_dims", 0}}); + auto bcast_shape_kv = makeOP({k_gather_dims, {(int64_t)gqa_ratio}, k_gather_dims2}, {{"axis", 0}}); + + auto K_shared = make_gqa_broadcast(k_concat, bcast_shape_kv); + auto V_shared = make_gqa_broadcast(v_concat, bcast_shape_kv); + + auto Slice_mask = make_attn_mask(position_ids, attention_mask, k_past); + + // Two SDPA layers sharing the same K/V, each with own Q projection + auto Q_18 = make_projection(ln_out, q_proj_size, q_heads); + auto sdpa_18 = + makeOP({Q_18, K_shared, V_shared, Slice_mask, 1.0f}, {{"causal", false}}); + + auto Q_20 = make_projection(ln_out, q_proj_size, q_heads); + auto sdpa_20 = + makeOP({Q_20, K_shared, V_shared, Slice_mask, 1.0f}, {{"causal", false}}); + + auto add_outputs = makeOP({sdpa_18, sdpa_20}, {numpy_broadcast}); + auto res = make_shared(add_outputs); + + auto model = std::make_shared(OutputVector{res}, SinkVector{k_assign, v_assign}, params); + + ov::pass::Manager pass_manager; + pass_manager.register_pass(); + pass_manager.run_passes(model); + + std::vector> pa_nodes; + for (const auto& op : model->get_ordered_ops()) { + if (auto pa = ov::as_type_ptr(op)) { + pa_nodes.push_back(pa); + } + } + + ASSERT_EQ(pa_nodes.size(), 2u) << "Expected 2 PagedAttention nodes (owner + shared)"; + + // Identify owner and shared by write_kv_cache attribute + std::shared_ptr owner_pa, shared_pa; + for (const auto& pa : pa_nodes) { + if (pa->get_write_kv_cache()) { + ASSERT_EQ(owner_pa, nullptr) << "Expected exactly one owner PA (write_kv_cache=true)"; + owner_pa = pa; + } else { + ASSERT_EQ(shared_pa, nullptr) << "Expected exactly one shared PA (write_kv_cache=false)"; + shared_pa = pa; + } + } + ASSERT_NE(owner_pa, nullptr) << "No owner PA found (write_kv_cache=true)"; + ASSERT_NE(shared_pa, nullptr) << "No shared PA found (write_kv_cache=false)"; + + // Both PAs must reference the SAME key_cache and value_cache Parameters + auto owner_k = owner_pa->input(3).get_source_output().get_node_shared_ptr(); + auto owner_v = owner_pa->input(4).get_source_output().get_node_shared_ptr(); + auto shared_k = shared_pa->input(3).get_source_output().get_node_shared_ptr(); + auto shared_v = shared_pa->input(4).get_source_output().get_node_shared_ptr(); + EXPECT_EQ(owner_k, shared_k) << "Owner and shared PA should share the same key_cache Parameter"; + EXPECT_EQ(owner_v, shared_v) << "Owner and shared PA should share the same value_cache Parameter"; + + // Only 1 unique key_cache and 1 unique value_cache Parameter should exist + size_t key_cache_count = 0, value_cache_count = 0; + for (const auto& param : model->get_parameters()) { + if (param->get_friendly_name().find("key_cache") != std::string::npos) + key_cache_count++; + if (param->get_friendly_name().find("value_cache") != std::string::npos) + value_cache_count++; + } + EXPECT_EQ(key_cache_count, 1u) << "Expected 1 key_cache parameter (shared)"; + EXPECT_EQ(value_cache_count, 1u) << "Expected 1 value_cache parameter (shared)"; +} + +TEST(SDPAToPA, SingleLayerSlidingWindow) { + // Simplified Gemma3-270 model with a single attention layer. + // The key part is the sliding window mask subgraph (BitwiseAnd chain) + // that gptoss_gemma3_sliding_window_pattern() must match to extract the window size. + + const int num_heads = 4; + const int head_dim = 256; + const int hidden_size = num_heads * head_dim; // 1024 + const int64_t sliding_window_offset = -512; + + auto input_ids = make_param(PartialShape{DYN, DYN}, element::i64, "input_ids"); + auto attention_mask = make_param(PartialShape{DYN, DYN}, element::i64, "attention_mask"); + auto position_ids = make_param(PartialShape{DYN, DYN}, element::i64, "position_ids"); + auto beam_idx = make_param(PartialShape{DYN}, element::i32, "beam_idx"); + auto params = nodes_to_params({input_ids, attention_mask, position_ids, beam_idx}); + + // Embedding (simplified) + auto embed_weight = makeConst(element::f32, ov::Shape{32000, (size_t)hidden_size}, MOCK_VALUE); + auto embeddings = makeOP({embed_weight, input_ids, 0}, {{"batch_dims", 0}}); + + // Shared shape info (used by mask and KV cache helpers) + auto shape_pos = makeOP({position_ids}, {{"output_type", "i64"}}); + auto batch_dim = makeOP({shape_pos, {0}, 0}, {{"batch_dims", 0}}); + + // Gemma3 sliding window mask subgraph + auto make_gemma3_sliding_window_mask = [&](std::shared_ptr pos_ids, + std::shared_ptr attn_mask, + int64_t sw_offset) { + auto shape_pos = makeOP({pos_ids}, {{"output_type", "i64"}}); + auto batch_dim = makeOP({shape_pos, {0}, 0}, {{"batch_dims", 0}}); + auto cur_len_scalar = makeOP({shape_pos, 1, 0}, {{"batch_dims", 0}}); + + // kv_idx: [1, 1, total_len] + auto kv_range = makeOP({0, cur_len_scalar, 1}, {{"output_type", "i64"}}); + auto kv_unsq1 = makeOP({kv_range, 0}); + auto kv_idx = makeOP({kv_unsq1, 0}); + + // q_idx: [1, 1, cur_len, 1] + auto q_range = makeOP({0, cur_len_scalar, 1}, {{"output_type", "i64"}}); + auto q_unsq1 = makeOP({q_range, 0}); + auto q_unsq2 = makeOP({q_unsq1, 0}); + auto q_idx = makeOP({q_unsq2, 3}); + + // Causal mask: LessEqual(kv_idx, q_idx) + auto less_equal = makeOP({kv_idx, q_idx}, {numpy_broadcast}); + + // Sliding window: Greater(kv_idx, q_idx + offset) + auto offset_const = makeConst(element::i64, ov::Shape({1, 1, 1, 1}), {(int)sw_offset}); + auto add_offset = makeOP({q_idx, offset_const}, {numpy_broadcast}); + auto greater = makeOP({kv_idx, add_offset}, {numpy_broadcast}); + + // BitwiseAnd chain for sliding window + auto const_true_1 = makeConst(element::boolean, ov::Shape({}), {1}); + auto bitwise_and_0 = makeOP({const_true_1, greater}, {{"auto_broadcast", "numpy"}}); + auto bitwise_and_1 = makeOP({bitwise_and_0, less_equal}, {{"auto_broadcast", "numpy"}}); + auto const_true_2 = makeConst(element::boolean, ov::Shape({}), {1}); + auto bitwise_and_2 = makeOP({const_true_2, bitwise_and_1}, {{"auto_broadcast", "numpy"}}); + + // attention_mask → boolean reshape [batch, 1, 1, seq] + auto attn_mask_bool = makeOP({attn_mask}, {{"destination_type", "boolean"}}); + auto attn_mask_reshape = makeOP({attn_mask_bool, {0, 1, 1, -1}}, {{"special_zero", true}}); + + // Sliding window mask: combined with attention_mask + auto bitwise_and_3 = makeOP({bitwise_and_2, attn_mask_reshape}, {{"auto_broadcast", "numpy"}}); + + // Broadcast shape [batch, 1, cur_len, total_len] + auto cur_len_unsq = makeOP({cur_len_scalar, 0}); + auto bcast_shape = makeOP({batch_dim, {1l}, cur_len_unsq, cur_len_unsq}, {{"axis", 0}}); + + auto sw_broadcast = makeOP({bitwise_and_3, bcast_shape}, {{"mode", "bidirectional"}}); + return makeOP({sw_broadcast, 0.0f, -65504.0f}, {numpy_broadcast}); + }; + + auto sw_select = make_gemma3_sliding_window_mask(position_ids, attention_mask, sliding_window_offset); + + struct KVCacheResult { + std::shared_ptr concat; + std::shared_ptr assign; + }; + auto make_kv_cache = [&](std::shared_ptr cur, const std::string& var_id) -> KVCacheResult { + auto var = std::make_shared( + ov::op::util::VariableInfo{ov::PartialShape{DYN, num_heads, DYN, head_dim}, ov::element::f32, var_id}); + auto init_shape = + makeOP({batch_dim, {(int64_t)num_heads}, {0l}, {(int64_t)head_dim}}, {{"axis", 0}}); + auto init = makeOP({0.0f, init_shape}, {{"mode", "numpy"}}); + std::shared_ptr read = std::make_shared(init, var); + auto past = makeOP({read, beam_idx, 0}, {{"batch_dims", 0}}); + auto concat = makeOP({past, cur}, {{"axis", -2}}); + auto assign = std::make_shared(concat, var); + return {concat, assign}; + }; + + // Projection helper (simplified: no RoPE, no GQA, no bias) + auto make_projection = [&](std::shared_ptr input, int out_size, int heads) { + auto weight = makeConst(element::f32, ov::Shape({(size_t)out_size, (size_t)hidden_size}), MOCK_VALUE); + auto matmul = makeOP({input, weight}, {{"transpose_a", false}, {"transpose_b", true}}); + auto reshape = makeOP({matmul, {0, 0, heads, head_dim}}, {{"special_zero", true}}); + return makeOP({reshape, {0, 2, 1, 3}}); + }; + + // Single layer: Sliding window attention + auto Q_0 = make_projection(embeddings, hidden_size, num_heads); + auto K_0 = make_projection(embeddings, hidden_size, num_heads); + auto V_0 = make_projection(embeddings, hidden_size, num_heads); + + auto kv_0 = make_kv_cache(K_0, "past_key_values.0.key"); + auto vv_0 = make_kv_cache(V_0, "past_key_values.0.value"); + + auto sdpa_0 = + makeOP({Q_0, kv_0.concat, vv_0.concat, sw_select, 1.0f}, {{"causal", false}}); + + // Output (simplified: no output projection, no residual, no MLP) + auto res = std::make_shared(sdpa_0); + + auto model = std::make_shared(OutputVector{res}, SinkVector{kv_0.assign, vv_0.assign}, params); + + ov::pass::Manager pass_manager; + pass_manager.register_pass(); + pass_manager.run_passes(model); + + std::shared_ptr pa; + for (const auto& op : model->get_ordered_ops()) { + if (auto node = ov::as_type_ptr(op)) { + pa = node; + } + } + ASSERT_NE(pa, nullptr); + + // Check sliding_window value (input port 10) + // Should have offset * -1 = 512 + auto sw_input = pa->input_value(10); + auto mul = ov::as_type_ptr(sw_input.get_node_shared_ptr()); + ASSERT_NE(mul, nullptr) << "PA sliding_window should be Multiply node (offset * -1)"; + + // Verify the -1 constant + auto neg_one = ov::as_type_ptr(mul->input_value(1).get_node_shared_ptr()); + ASSERT_NE(neg_one, nullptr); + EXPECT_EQ(neg_one->cast_vector()[0], -1); + + // Trace through Convert → Squeeze → original constant + auto convert_node = mul->input_value(0).get_node_shared_ptr(); + std::shared_ptr squeeze_node; + if (ov::as_type_ptr(convert_node)) { + squeeze_node = convert_node->input_value(0).get_node_shared_ptr(); + } else { + squeeze_node = convert_node; + } + + std::shared_ptr offset_node; + if (auto sq = ov::as_type_ptr(squeeze_node)) { + offset_node = ov::as_type_ptr(sq->input_value(0).get_node_shared_ptr()); + } else { + offset_node = ov::as_type_ptr(squeeze_node); + } + ASSERT_NE(offset_node, nullptr) << "Could not find original offset constant"; + EXPECT_EQ(offset_node->cast_vector()[0], sliding_window_offset); +} + +TEST_F(SDPAToPATest, SDPAToPA_LFM2_EliminateConvPaddingMaskGating) { + { + auto attention_mask = make_param(PartialShape{DYN, DYN}, element::i32, "attention_mask"); + auto slice = makeOP({attention_mask, {0}, {1}, {1}, {1}}); + auto unsqueeze = makeOP({slice, 1}); + auto convert = makeOP({unsqueeze}, {{"destination_type", "f32"}}); + auto multiply = makeOP({convert, 1024.0f}, {{"auto_broadcast", "numpy"}}); + auto add = makeOP({multiply, 1024.0f}, {{"auto_broadcast", "numpy"}}); + auto multiply_gate_param = make_param(PartialShape{DYN, DYN, DYN}, element::f32, "gate_param"); + auto multiply_gate = makeOP({multiply_gate_param, add}, {{"auto_broadcast", "numpy"}}); + + auto matmul_param = make_param(PartialShape{48, 16}, element::f32, "weights"); + auto matmul = + makeOP({matmul_param, multiply_gate}, {{"transpose_a", false}, {"transpose_b", true}}); + auto res = makeOP({matmul}); + + auto params = nodes_to_params({attention_mask, matmul_param, multiply_gate_param}); + model = std::make_shared(OutputVector{res}, ParameterVector{params}); + + ov::pass::Manager pass_manager; + pass_manager.set_per_pass_validation(false); + pass_manager.register_pass(); + pass_manager.run_passes(model); + + model->remove_parameter(params[0]); + } + { + auto multiply_gate_param = make_param(PartialShape{DYN, DYN, DYN}, element::f32, "gate_param"); + auto matmul_param = make_param(PartialShape{48, 16}, element::f32, "weights"); + auto matmul = + makeOP({matmul_param, multiply_gate_param}, {{"transpose_a", false}, {"transpose_b", true}}); + auto res = makeOP({matmul}); + + auto params = nodes_to_params({matmul_param, multiply_gate_param}); + + model_ref = std::make_shared(OutputVector{res}, ParameterVector{params}); + } +} + /* As there's often a need to cover specific model's architecutres in these tests, please, make sure you name the tests in the following manner: diff --git a/src/common/transformations/tests/pa_kv_reorder_fusion_test.cpp b/src/common/transformations/tests/pa_kv_reorder_fusion_test.cpp new file mode 100644 index 000000000000..f55357eaae4f --- /dev/null +++ b/src/common/transformations/tests/pa_kv_reorder_fusion_test.cpp @@ -0,0 +1,333 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "openvino/pass/pa_kv_reorder_fusion.hpp" + +#include + +#include + +#include "common_test_utils/ov_test_utils.hpp" +#include "openvino/core/model.hpp" +#include "openvino/op/concat.hpp" +#include "openvino/op/constant.hpp" +#include "openvino/op/gather.hpp" +#include "openvino/op/pa_kv_reorder.hpp" +#include "openvino/op/parameter.hpp" +#include "openvino/op/result.hpp" +#include "openvino/op/scatter_update.hpp" +#include "openvino/pass/manager.hpp" + +namespace ov::test { + +TEST(PaKVReorderFusionTest, PassInstantiation) { + auto pass = std::make_shared(); + ASSERT_NE(pass, nullptr); +} + +TEST(PaKVReorderFusionTest, EmptyModel) { + auto model = std::make_shared(ResultVector{}, ParameterVector{}); + + pass::Manager manager; + manager.register_pass(); + manager.run_passes(model); + + ASSERT_EQ(model->get_results().size(), 0); +} + +TEST(PaKVReorderFusionTest, NoPatternToFuse) { + auto input = std::make_shared(element::f32, Shape{1, 3, 224, 224}); + auto result = std::make_shared(input); + auto model = std::make_shared(ResultVector{result}, ParameterVector{input}); + + size_t ops_before = model->get_ops().size(); + + pass::Manager manager; + manager.register_pass(); + manager.run_passes(model); + + size_t ops_after = model->get_ops().size(); + ASSERT_EQ(ops_before, ops_after); +} + +TEST(PaKVReorderFusionTest, FusionPattern) { + auto key_cache = std::make_shared(element::f32, Shape{4, 8, 32, 64}); + key_cache->set_friendly_name("key_cache.0_clone_for_k_update"); + + auto value_cache = std::make_shared(element::f32, Shape{4, 8, 32, 64}); + value_cache->set_friendly_name("value_cache.0_clone_for_v_update"); + + auto block_update_indices = std::make_shared(element::i32, Shape{4}); + block_update_indices->set_friendly_name("block_update_indices"); + + auto block_indices = std::make_shared(element::i32, Shape{4}); + block_indices->set_friendly_name("block_indices"); + + auto block_indices_begins = std::make_shared(element::i32, Shape{2}); + block_indices_begins->set_friendly_name("block_indices_begins"); + + auto block_update_indices_begins = std::make_shared(element::i32, Shape{2}); + block_update_indices_begins->set_friendly_name("block_update_indices_begins"); + + auto axis = op::v0::Constant::create(element::i32, Shape{}, {0}); + + auto key_gather = std::make_shared(key_cache, block_update_indices, axis); + auto key_scatter = std::make_shared(key_cache, block_indices, key_gather, axis); + key_scatter->set_friendly_name("updated_key_cache_0"); + + auto value_gather = std::make_shared(value_cache, block_update_indices, axis); + auto value_scatter = std::make_shared(value_cache, block_indices, value_gather, axis); + value_scatter->set_friendly_name("updated_value_cache_0"); + + auto concat = std::make_shared(OutputVector{key_scatter, value_scatter}, 0); + auto result = std::make_shared(concat); + + auto model = std::make_shared(ResultVector{result}, + ParameterVector{key_cache, + value_cache, + block_indices, + block_indices_begins, + block_update_indices, + block_update_indices_begins}); + + pass::Manager manager; + manager.register_pass(); + manager.run_passes(model); + + bool found_pa_kv_reorder = false; + int gather_count = 0; + int scatter_count = 0; + + for (const auto& op : model->get_ops()) { + if (std::dynamic_pointer_cast(op)) { + found_pa_kv_reorder = true; + } + if (std::dynamic_pointer_cast(op)) { + gather_count++; + } + if (std::dynamic_pointer_cast(op)) { + scatter_count++; + } + } + + ASSERT_TRUE(found_pa_kv_reorder) << "PaKVReorder op should be created after fusion"; + ASSERT_EQ(gather_count, 0) << "Gather ops should be removed after fusion"; + ASSERT_EQ(scatter_count, 0) << "ScatterUpdate ops should be removed after fusion"; +} + +TEST_F(TransformationTestsF, PaKVReorderFusion_basic) { + disable_result_friendly_names_check(); + { + auto key_cache = std::make_shared(element::f16, PartialShape::dynamic(4)); + key_cache->set_friendly_name("key_cache.0_clone_for_k_update"); + auto value_cache = std::make_shared(element::f16, PartialShape::dynamic(4)); + value_cache->set_friendly_name("value_cache.0_clone_for_v_update"); + + auto block_indices = std::make_shared(element::i32, PartialShape{2}); + block_indices->set_friendly_name("block_indices"); + auto block_indices_begins = std::make_shared(element::i32, PartialShape{2}); + block_indices_begins->set_friendly_name("block_indices_begins"); + + auto block_update_indices = std::make_shared(element::i32, PartialShape{2}); + block_update_indices->set_friendly_name("block_update_indices"); + auto block_update_indices_begins = std::make_shared(element::i32, PartialShape{2}); + block_update_indices_begins->set_friendly_name("block_update_indices_begins"); + + auto gather_axis = op::v0::Constant::create(element::i64, Shape{}, {0}); + auto scatter_axis = op::v0::Constant::create(element::i64, Shape{}, {0}); + + auto key_gather = std::make_shared(key_cache, block_update_indices, gather_axis); + auto value_gather = std::make_shared(value_cache, block_update_indices, gather_axis); + + auto key_scatter = std::make_shared(key_cache, block_indices, key_gather, scatter_axis); + auto value_scatter = + std::make_shared(value_cache, block_indices, value_gather, scatter_axis); + + auto concat = std::make_shared(OutputVector{key_scatter, value_scatter}, 0); + auto result = std::make_shared(concat); + + model = std::make_shared(ResultVector{result}, + ParameterVector{key_cache, + value_cache, + block_indices, + block_indices_begins, + block_update_indices, + block_update_indices_begins}); + + manager.register_pass(); + } + + { + auto key_cache = std::make_shared(element::f16, PartialShape::dynamic(4)); + key_cache->set_friendly_name("key_cache.0_clone_for_k_update"); + auto value_cache = std::make_shared(element::f16, PartialShape::dynamic(4)); + value_cache->set_friendly_name("value_cache.0_clone_for_v_update"); + + auto block_indices = std::make_shared(element::i32, PartialShape{2}); + block_indices->set_friendly_name("block_indices"); + auto block_indices_begins = std::make_shared(element::i32, PartialShape{2}); + block_indices_begins->set_friendly_name("block_indices_begins"); + + auto block_update_indices = std::make_shared(element::i32, PartialShape{2}); + block_update_indices->set_friendly_name("block_update_indices"); + auto block_update_indices_begins = std::make_shared(element::i32, PartialShape{2}); + block_update_indices_begins->set_friendly_name("block_update_indices_begins"); + + auto pa_kv_reorder = std::make_shared(key_cache, + value_cache, + block_indices, + block_indices_begins, + block_update_indices, + block_update_indices_begins); + pa_kv_reorder->set_friendly_name("pa_kv_reorder_0"); + auto result = std::make_shared(pa_kv_reorder); + + model_ref = std::make_shared(ResultVector{result}, + ParameterVector{key_cache, + value_cache, + block_indices, + block_indices_begins, + block_update_indices, + block_update_indices_begins}); + comparator.enable(FunctionsComparator::ATTRIBUTES); + } +} + +TEST_F(TransformationTestsF, PaKVReorderFusion_skip_on_mismatched_block_indices) { + disable_result_friendly_names_check(); + auto key_cache = std::make_shared(element::f16, PartialShape::dynamic(4)); + key_cache->set_friendly_name("key_cache.0_clone_for_k_update"); + auto value_cache = std::make_shared(element::f16, PartialShape::dynamic(4)); + value_cache->set_friendly_name("value_cache.0_clone_for_v_update"); + + auto block_indices_k = std::make_shared(element::i32, PartialShape{2}); + block_indices_k->set_friendly_name("block_indices_k"); + auto block_indices_v = std::make_shared(element::i32, PartialShape{2}); + block_indices_v->set_friendly_name("block_indices_v"); + + auto block_indices_begins = std::make_shared(element::i32, PartialShape{2}); + block_indices_begins->set_friendly_name("block_indices_begins"); + auto block_update_indices = std::make_shared(element::i32, PartialShape{2}); + block_update_indices->set_friendly_name("block_update_indices"); + auto block_update_indices_begins = std::make_shared(element::i32, PartialShape{2}); + block_update_indices_begins->set_friendly_name("block_update_indices_begins"); + + auto gather_axis = op::v0::Constant::create(element::i64, Shape{}, {0}); + auto scatter_axis = op::v0::Constant::create(element::i64, Shape{}, {0}); + + auto key_gather = std::make_shared(key_cache, block_update_indices, gather_axis); + auto value_gather = std::make_shared(value_cache, block_update_indices, gather_axis); + + auto key_scatter = std::make_shared(key_cache, block_indices_k, key_gather, scatter_axis); + auto value_scatter = + std::make_shared(value_cache, block_indices_v, value_gather, scatter_axis); + + auto concat = std::make_shared(OutputVector{key_scatter, value_scatter}, 0); + auto result = std::make_shared(concat); + + model = std::make_shared(ResultVector{result}, + ParameterVector{key_cache, + value_cache, + block_indices_k, + block_indices_v, + block_indices_begins, + block_update_indices, + block_update_indices_begins}); + + manager.register_pass(); + + model_ref = model->clone(); + comparator.enable(FunctionsComparator::ATTRIBUTES); +} + +TEST(PaKVReorderOpTest, OpCreation) { + auto key_cache = std::make_shared(element::f32, Shape{4, 8, 32, 64}); + auto value_cache = std::make_shared(element::f32, Shape{4, 8, 32, 64}); + auto block_indices = std::make_shared(element::i32, Shape{8}); + auto block_indices_begins = std::make_shared(element::i32, Shape{2}); + auto block_update_indices = std::make_shared(element::i32, Shape{4}); + auto block_update_indices_begins = std::make_shared(element::i32, Shape{2}); + + auto pa_kv_reorder = std::make_shared(key_cache, + value_cache, + block_indices, + block_indices_begins, + block_update_indices, + block_update_indices_begins); + + ASSERT_NE(pa_kv_reorder, nullptr); + ASSERT_EQ(pa_kv_reorder->get_input_size(), 6); + ASSERT_EQ(pa_kv_reorder->get_output_size(), 1); +} + +TEST(PaKVReorderOpTest, ModelWithOp) { + auto key_cache = std::make_shared(element::f32, Shape{4, 8, 32, 64}); + key_cache->set_friendly_name("key_cache"); + + auto value_cache = std::make_shared(element::f32, Shape{4, 8, 32, 64}); + value_cache->set_friendly_name("value_cache"); + + auto block_indices = std::make_shared(element::i32, Shape{8}); + block_indices->set_friendly_name("block_indices"); + + auto block_indices_begins = std::make_shared(element::i32, Shape{2}); + block_indices_begins->set_friendly_name("block_indices_begins"); + + auto block_update_indices = std::make_shared(element::i32, Shape{4}); + block_update_indices->set_friendly_name("block_update_indices"); + + auto block_update_indices_begins = std::make_shared(element::i32, Shape{2}); + block_update_indices_begins->set_friendly_name("block_update_indices_begins"); + + auto pa_kv_reorder = std::make_shared(key_cache, + value_cache, + block_indices, + block_indices_begins, + block_update_indices, + block_update_indices_begins); + + auto result = std::make_shared(pa_kv_reorder->output(0)); + + auto model = std::make_shared(ResultVector{result}, + ParameterVector{key_cache, + value_cache, + block_indices, + block_indices_begins, + block_update_indices, + block_update_indices_begins}); + + ASSERT_NE(model, nullptr); + ASSERT_EQ(model->get_parameters().size(), 6); + ASSERT_EQ(model->get_results().size(), 1); + + bool found_pa_kv_reorder = false; + for (const auto& op : model->get_ops()) { + if (std::dynamic_pointer_cast(op)) { + found_pa_kv_reorder = true; + break; + } + } + ASSERT_TRUE(found_pa_kv_reorder) << "PaKVReorder op not found in model"; +} + +TEST(PaKVReorderOpTest, TypeInfo) { + auto key_cache = std::make_shared(element::f32, Shape{4, 8, 32, 64}); + auto value_cache = std::make_shared(element::f32, Shape{4, 8, 32, 64}); + auto block_indices = std::make_shared(element::i32, Shape{8}); + auto block_indices_begins = std::make_shared(element::i32, Shape{2}); + auto block_update_indices = std::make_shared(element::i32, Shape{4}); + auto block_update_indices_begins = std::make_shared(element::i32, Shape{2}); + + auto pa_kv_reorder = std::make_shared(key_cache, + value_cache, + block_indices, + block_indices_begins, + block_update_indices, + block_update_indices_begins); + + auto type_info = pa_kv_reorder->get_type_info(); + ASSERT_STREQ(type_info.name, "PaKVReorder"); +} + +} // namespace ov::test diff --git a/src/common/transformations/tests/sdpa_to_paged_attention_test.cpp b/src/common/transformations/tests/sdpa_to_paged_attention_test.cpp index ee6b23c4cc60..ba196a2a19a6 100644 --- a/src/common/transformations/tests/sdpa_to_paged_attention_test.cpp +++ b/src/common/transformations/tests/sdpa_to_paged_attention_test.cpp @@ -18,7 +18,7 @@ #include "openvino/op/scaled_dot_product_attention.hpp" #include "openvino/op/shape_of.hpp" #include "openvino/pass/manager.hpp" -#include "transformations/sdpa_to_paged_attention/total_sequence_length_pattern.hpp" +#include "transformations/paged_attention/total_sequence_length_pattern.hpp" using namespace ov; diff --git a/src/common/transformations/tests/utils/convert_precision.cpp b/src/common/transformations/tests/utils/convert_precision.cpp index d860340cccaa..9463d2dc75ca 100644 --- a/src/common/transformations/tests/utils/convert_precision.cpp +++ b/src/common/transformations/tests/utils/convert_precision.cpp @@ -77,7 +77,8 @@ #include "openvino/pass/visualize_tree.hpp" #include "ov_ops/type_relaxed.hpp" #include "transformations/common_optimizations/disable_shapeof_constant_folding.hpp" -#include "transformations/rt_info/disable_fp16_compression.hpp" +#include "transformations/rt_info/disable_precision_conversion.hpp" +#include "transformations/rt_info/keep_const_precision.hpp" #include "transformations/rt_info/original_precision_attribute.hpp" #include "transformations/utils/utils.hpp" @@ -376,13 +377,13 @@ TEST(TransformationTests, ConvertPrecision_Range_i32_to_i64) { auto add = std::make_shared(reshape, add_const); auto res = std::make_shared(add); - ov::disable_fp16_compression(start); - ov::disable_fp16_compression(reduction_axes); - ov::disable_fp16_compression(stop); - ov::disable_fp16_compression(step); - ov::disable_fp16_compression(range); - ov::disable_fp16_compression(add_const); - ov::disable_fp16_compression(add); + disable_conversion(start, ov::element::f16); + disable_conversion(reduction_axes, ov::element::f16); + disable_conversion(stop, ov::element::f16); + disable_conversion(step, ov::element::f16); + disable_conversion(range, ov::element::f16); + disable_conversion(add_const, ov::element::f16); + disable_conversion(add, ov::element::f16); model = std::make_shared(OutputVector{res}, ParameterVector{param}); @@ -2438,6 +2439,72 @@ TEST(TransformationTests, ConvertPrecisionExplicitConvertsForParameterAndResult) ASSERT_EQ("sine.0", results[0]->get_input_node_ptr(0)->get_friendly_name()); } +TEST(TransformationTests, ConvertPrecisionSkipsParameterWithKeepConstPrecision) { + shared_ptr model, model_ref; + pass::Manager manager; + { + auto marked_param = std::make_shared(element::f16, Shape{2, 2}); + enable_keep_const_precision(marked_param); + auto relu = std::make_shared(marked_param); + auto result = std::make_shared(relu); + model = std::make_shared(result, ParameterVector{marked_param}); + + type_to_fuse_map empty_type_to_fuse_map = {}; + bool keep_precision_sensitive_in_fp32 = false; + bool convert_input_output_precision = false; + manager.register_pass(precisions_map{{element::f16, element::f32}}, + empty_type_to_fuse_map, + keep_precision_sensitive_in_fp32, + convert_input_output_precision); + manager.run_passes(model); + } + + { + auto marked_param = std::make_shared(element::f16, Shape{2, 2}); + enable_keep_const_precision(marked_param); + auto relu = std::make_shared(marked_param); + auto result = std::make_shared(relu); + model_ref = std::make_shared(result, ParameterVector{marked_param}); + } + + const FunctionsComparator func_comparator = FunctionsComparator::with_default(); + FunctionsComparator::Result result = func_comparator(model_ref, model); + ASSERT_TRUE(result.valid) << result.message; +} + +TEST(TransformationTests, ConvertPrecisionConvertsUnmarkedParameters) { + shared_ptr model, model_ref; + pass::Manager manager; + { + auto param = std::make_shared(element::f16, Shape{2, 2}); + auto relu = std::make_shared(param); + auto result = std::make_shared(relu); + model = std::make_shared(result, ParameterVector{param}); + + type_to_fuse_map empty_type_to_fuse_map = {}; + bool keep_precision_sensitive_in_fp32 = false; + bool convert_input_output_precision = false; + manager.register_pass(precisions_map{{element::f16, element::f32}}, + empty_type_to_fuse_map, + keep_precision_sensitive_in_fp32, + convert_input_output_precision); + manager.run_passes(model); + } + + { + auto param = std::make_shared(element::f16, Shape{2, 2}); + auto convert_before = std::make_shared(param, element::f32); + auto relu = std::make_shared(convert_before); + auto convert_after = std::make_shared(relu, element::f16); + auto result = std::make_shared(convert_after); + model_ref = std::make_shared(result, ParameterVector{param}); + } + + const FunctionsComparator func_comparator = FunctionsComparator::with_default(); + FunctionsComparator::Result result = func_comparator(model_ref, model); + ASSERT_TRUE(result.valid) << result.message; +} + TEST(TransformationTests, ConvertPrecisionExplicitConvertsMultiParam) { shared_ptr model, model_ref; pass::Manager manager; diff --git a/src/common/transformations/tests/utils/extract_subgraph_test.cpp b/src/common/transformations/tests/utils/extract_subgraph_test.cpp new file mode 100644 index 000000000000..1881d06f2444 --- /dev/null +++ b/src/common/transformations/tests/utils/extract_subgraph_test.cpp @@ -0,0 +1,347 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "transformations/utils/extract_subgraph.hpp" + +#include + +#include +#include + +#include "common_test_utils/graph_comparator.hpp" +#include "openvino/core/model.hpp" +#include "openvino/op/add.hpp" +#include "openvino/op/assign.hpp" +#include "openvino/op/multiply.hpp" +#include "openvino/op/parameter.hpp" +#include "openvino/op/read_value.hpp" +#include "openvino/op/relu.hpp" +#include "openvino/op/result.hpp" +#include "openvino/op/sigmoid.hpp" +#include "openvino/op/util/variable.hpp" + +namespace ov::test { + +// Builds: param_a -> relu -> add -> result +// param_b -^ +static std::shared_ptr build_linear_model() { + auto param_a = std::make_shared(ov::element::f32, ov::Shape{1, 4}); + param_a->set_friendly_name("A"); + + auto relu = std::make_shared(param_a); + relu->set_friendly_name("Relu"); + + auto param_b = std::make_shared(ov::element::f32, ov::Shape{1, 4}); + param_b->set_friendly_name("B"); + + auto add = std::make_shared(relu, param_b); + add->set_friendly_name("Add"); + + auto result = std::make_shared(add); + result->set_friendly_name("Result"); + + return std::make_shared(ov::ResultVector{result}, ov::ParameterVector{param_a, param_b}); +} + +// Builds a branching model: +// param_a -> relu ---------> mul -> result +// param_b -> sigmoid -------^ +static std::shared_ptr build_branch_model() { + auto param_a = std::make_shared(ov::element::f32, ov::Shape{2, 3}); + param_a->set_friendly_name("A"); + + auto relu = std::make_shared(param_a); + relu->set_friendly_name("Relu"); + + auto param_b = std::make_shared(ov::element::f32, ov::Shape{2, 3}); + param_b->set_friendly_name("B"); + + auto sigmoid = std::make_shared(param_b); + sigmoid->set_friendly_name("Sigmoid"); + + auto mul = std::make_shared(relu, sigmoid); + mul->set_friendly_name("Mul"); + + auto result = std::make_shared(mul); + result->set_friendly_name("Result"); + + return std::make_shared(ov::ResultVector{result}, ov::ParameterVector{param_a, param_b}); +} + +TEST(ExtractSubgraphTest, CoreOverload_SingleOp) { + auto model = build_linear_model(); + + ov::op::v0::Relu* relu_ptr = nullptr; + for (const auto& op : model->get_ordered_ops()) { + if (op->get_friendly_name() == "Relu") + relu_ptr = ov::as_type(op.get()); + } + ASSERT_NE(relu_ptr, nullptr); + + auto subgraph = ov::util::extract_subgraph({relu_ptr->input(0)}, {relu_ptr->output(0)}); + + // Build the expected model: param -> relu -> result + auto expected_param = std::make_shared(ov::element::f32, ov::PartialShape{1, 4}); + auto expected_relu = std::make_shared(expected_param); + expected_relu->set_friendly_name("Relu"); + auto expected_result = std::make_shared(expected_relu); + auto expected = std::make_shared(ov::ResultVector{expected_result}, ov::ParameterVector{expected_param}); + + const auto cmp = FunctionsComparator::all_flags_enabled(); + const auto res = cmp.compare(subgraph, expected); + EXPECT_TRUE(res.valid) << res.message; +} + +TEST(ExtractSubgraphTest, CoreOverload_TwoInputsOneOutput) { + auto model = build_linear_model(); + + ov::op::v1::Add* add_ptr = nullptr; + for (const auto& op : model->get_ordered_ops()) { + if (op->get_friendly_name() == "Add") + add_ptr = ov::as_type(op.get()); + } + ASSERT_NE(add_ptr, nullptr); + + auto subgraph = ov::util::extract_subgraph({add_ptr->input(0), add_ptr->input(1)}, {add_ptr->output(0)}); + + auto p0 = std::make_shared(ov::element::f32, ov::PartialShape{1, 4}); + auto p1 = std::make_shared(ov::element::f32, ov::PartialShape{1, 4}); + auto expected_add = std::make_shared(p0, p1); + expected_add->set_friendly_name("Add"); + auto expected_result = std::make_shared(expected_add); + auto expected = std::make_shared(ov::ResultVector{expected_result}, ov::ParameterVector{p0, p1}); + + const auto cmp = FunctionsComparator::all_flags_enabled().enable(FunctionsComparator::ATTRIBUTES); + const auto res = cmp.compare(subgraph, expected); + EXPECT_TRUE(res.valid) << res.message; +} + +TEST(ExtractSubgraphTest, CoreOverload_OriginalModelUnchanged) { + auto model = build_linear_model(); + const size_t original_op_count = model->get_ordered_ops().size(); + + ov::op::v0::Relu* relu_ptr = nullptr; + for (const auto& op : model->get_ordered_ops()) { + if (op->get_friendly_name() == "Relu") + relu_ptr = ov::as_type(op.get()); + } + ASSERT_NE(relu_ptr, nullptr); + + ov::util::extract_subgraph({relu_ptr->input(0)}, {relu_ptr->output(0)}); + + EXPECT_EQ(model->get_ordered_ops().size(), original_op_count); + EXPECT_EQ(model->get_parameters().size(), 2u); + EXPECT_EQ(model->get_results().size(), 1u); + EXPECT_TRUE(ov::is_type(relu_ptr->get_input_node_shared_ptr(0))); +} + +TEST(ExtractSubgraphTest, MultimapOverload_SingleOp) { + auto model = build_linear_model(); + + const std::multimap inputs_map = {{"Relu", 0}}; + const std::multimap outputs_map = {{"Relu", 0}}; + + auto subgraph = ov::util::extract_subgraph(model, inputs_map, outputs_map); + + auto expected_param = std::make_shared(ov::element::f32, ov::PartialShape{1, 4}); + auto expected_relu = std::make_shared(expected_param); + expected_relu->set_friendly_name("Relu"); + auto expected_result = std::make_shared(expected_relu); + auto expected = std::make_shared(ov::ResultVector{expected_result}, ov::ParameterVector{expected_param}); + + const auto cmp = FunctionsComparator::all_flags_enabled(); + const auto res = cmp.compare(subgraph, expected); + EXPECT_TRUE(res.valid) << res.message; +} + +TEST(ExtractSubgraphTest, MultimapOverload_MultiInputChain) { + auto model = build_linear_model(); + + // Extract the whole Relu->Add chain: cut before Relu and before Add.input(1), output at Add + const std::multimap inputs_map = {{"Relu", 0}, {"Add", 1}}; + const std::multimap outputs_map = {{"Add", 0}}; + + auto subgraph = ov::util::extract_subgraph(model, inputs_map, outputs_map); + + // Expected: p0 -> relu -> add(p1) -> result + auto p0 = std::make_shared(ov::element::f32, ov::PartialShape{1, 4}); + auto p1 = std::make_shared(ov::element::f32, ov::PartialShape{1, 4}); + auto expected_relu = std::make_shared(p0); + expected_relu->set_friendly_name("Relu"); + auto expected_add = std::make_shared(expected_relu, p1); + expected_add->set_friendly_name("Add"); + auto expected_result = std::make_shared(expected_add); + auto expected = std::make_shared(ov::ResultVector{expected_result}, ov::ParameterVector{p0, p1}); + + const auto cmp = FunctionsComparator::all_flags_enabled(); + const auto res = cmp.compare(subgraph, expected); + EXPECT_TRUE(res.valid) << res.message; +} + +TEST(ExtractSubgraphTest, MultimapOverload_BranchingModel_BothBranches) { + auto model = build_branch_model(); + + // Extract only Mul: cut both Mul inputs to Parameters and use Mul output as the subgraph output + const std::multimap inputs_map = {{"Mul", 0}, {"Mul", 1}}; + const std::multimap outputs_map = {{"Mul", 0}}; + + auto subgraph = ov::util::extract_subgraph(model, inputs_map, outputs_map); + + auto p0 = std::make_shared(ov::element::f32, ov::PartialShape{2, 3}); + auto p1 = std::make_shared(ov::element::f32, ov::PartialShape{2, 3}); + auto expected_mul = std::make_shared(p0, p1); + expected_mul->set_friendly_name("Mul"); + auto expected_result = std::make_shared(expected_mul); + auto expected = std::make_shared(ov::ResultVector{expected_result}, ov::ParameterVector{p0, p1}); + + const auto cmp = FunctionsComparator::all_flags_enabled(); + const auto res = cmp.compare(subgraph, expected); + EXPECT_TRUE(res.valid) << res.message; +} + +TEST(ExtractSubgraphTest, MultimapOverload_BranchingModel_SingleBranch) { + auto model = build_branch_model(); + + // Extract only the Relu branch up to Mul input + const std::multimap inputs_map = {{"Relu", 0}}; + const std::multimap outputs_map = {{"Relu", 0}}; + + auto subgraph = ov::util::extract_subgraph(model, inputs_map, outputs_map); + + auto expected_param = std::make_shared(ov::element::f32, ov::PartialShape{2, 3}); + auto expected_relu = std::make_shared(expected_param); + expected_relu->set_friendly_name("Relu"); + auto expected_result = std::make_shared(expected_relu); + auto expected = std::make_shared(ov::ResultVector{expected_result}, ov::ParameterVector{expected_param}); + + const auto cmp = FunctionsComparator::all_flags_enabled(); + const auto res = cmp.compare(subgraph, expected); + EXPECT_TRUE(res.valid) << res.message; +} + +TEST(ExtractSubgraphTest, MultimapOverload_UnknownInputNameThrows) { + auto model = build_linear_model(); + + const std::multimap inputs_map = {{"NonExistentNode", 0}}; + const std::multimap outputs_map = {{"Add", 0}}; + + EXPECT_THROW(ov::util::extract_subgraph(model, inputs_map, outputs_map), ov::Exception); +} + +TEST(ExtractSubgraphTest, MultimapOverload_UnknownOutputNameThrows) { + auto model = build_linear_model(); + + const std::multimap inputs_map = {{"Add", 0}}; + const std::multimap outputs_map = {{"NonExistentNode", 0}}; + + EXPECT_THROW(ov::util::extract_subgraph(model, inputs_map, outputs_map), ov::Exception); +} + +// Builds a stateful model with a sink: +// param -> relu -> result +// \-> assign (sink, writes to variable) +// read_value (reads from same variable) feeds into relu +// +// Simplified topology: +// read_value(variable) -> relu -> result +// \-> assign(variable) +static std::shared_ptr build_stateful_model() { + auto variable = std::make_shared( + ov::op::util::VariableInfo{ov::Shape{1, 4}, ov::element::f32, "var_0"}); + + auto param = std::make_shared(ov::element::f32, ov::Shape{1, 4}); + param->set_friendly_name("Param"); + + auto read_value = std::make_shared(param, variable); + read_value->set_friendly_name("ReadValue"); + + auto relu = std::make_shared(read_value); + relu->set_friendly_name("Relu"); + + auto assign = std::make_shared(relu, variable); + assign->set_friendly_name("Assign"); + + auto result = std::make_shared(relu); + result->set_friendly_name("Result"); + + return std::make_shared(ov::ResultVector{result}, + ov::SinkVector{assign}, + ov::ParameterVector{param}, + ov::op::util::VariableVector{variable}); +} + +TEST(ExtractSubgraphTest, CoreOverload_StatefulModel_SinkIncluded) { + auto model = build_stateful_model(); + + // Extract subgraph: cut at Relu input, output at Relu output + // The Assign sink consumes Relu's output, so it should be included in the extracted model + ov::op::v0::Relu* relu_ptr = nullptr; + for (const auto& op : model->get_ordered_ops()) { + if (op->get_friendly_name() == "Relu") + relu_ptr = ov::as_type(op.get()); + } + ASSERT_NE(relu_ptr, nullptr); + + auto subgraph = ov::util::extract_subgraph({relu_ptr->input(0)}, {relu_ptr->output(0)}); + + EXPECT_EQ(subgraph->get_sinks().size(), 1u); + EXPECT_EQ(subgraph->get_results().size(), 1u); + EXPECT_EQ(subgraph->get_parameters().size(), 1u); + + bool has_assign = false; + for (const auto& op : subgraph->get_ordered_ops()) { + if (ov::is_type(op)) { + has_assign = true; + } + } + EXPECT_TRUE(has_assign); +} + +TEST(ExtractSubgraphTest, MultimapOverload_StatefulModel_SinkIncluded) { + auto model = build_stateful_model(); + + // Extract Relu with its sink (Assign consumes Relu output) + const std::multimap inputs_map = {{"Relu", 0}}; + const std::multimap outputs_map = {{"Relu", 0}}; + + auto subgraph = ov::util::extract_subgraph(model, inputs_map, outputs_map); + + EXPECT_EQ(subgraph->get_sinks().size(), 1u); + EXPECT_EQ(subgraph->get_results().size(), 1u); + EXPECT_EQ(subgraph->get_parameters().size(), 1u); + + auto expected_var = std::make_shared( + ov::op::util::VariableInfo{ov::Shape{1, 4}, ov::element::f32, "var_0"}); + auto expected_param = std::make_shared(ov::element::f32, ov::PartialShape{1, 4}); + auto expected_relu = std::make_shared(expected_param); + expected_relu->set_friendly_name("Relu"); + auto expected_assign = std::make_shared(expected_relu, expected_var); + expected_assign->set_friendly_name("Assign"); + auto expected_result = std::make_shared(expected_relu); + auto expected = std::make_shared(ov::ResultVector{expected_result}, + ov::SinkVector{expected_assign}, + ov::ParameterVector{expected_param}, + ov::op::util::VariableVector{expected_var}); + + const auto cmp = FunctionsComparator::all_flags_enabled(); + const auto res = cmp.compare(subgraph, expected); + EXPECT_TRUE(res.valid) << res.message; +} + +TEST(ExtractSubgraphTest, MultimapOverload_StatefulModel_SinkExcluded) { + auto model = build_stateful_model(); + + // Extract only ReadValue (before Relu). The Assign doesn't consume ReadValue's output + // directly, so it should NOT be in the extracted subgraph. + const std::multimap inputs_map = {{"ReadValue", 0}}; + const std::multimap outputs_map = {{"ReadValue", 0}}; + + auto subgraph = ov::util::extract_subgraph(model, inputs_map, outputs_map); + + EXPECT_EQ(subgraph->get_sinks().size(), 0u); + EXPECT_EQ(subgraph->get_results().size(), 1u); + EXPECT_EQ(subgraph->get_parameters().size(), 1u); +} + +} // namespace ov::test diff --git a/src/common/util/include/openvino/util/common_util.hpp b/src/common/util/include/openvino/util/common_util.hpp index 38c002ed43e8..c766e7ce2bd0 100644 --- a/src/common/util/include/openvino/util/common_util.hpp +++ b/src/common/util/include/openvino/util/common_util.hpp @@ -7,37 +7,68 @@ #include #include #include +#include #include #include #include #include +#include #include #include #include -namespace ov { -namespace util { - +namespace ov::util { /** - * @brief Join container's elements to string using user string as separator. + * @brief Helper struct to join container's elements to string using user string as separator. + * The output string is generated in operator<< and can be used in any context where std::string is expected. + * Example: + * std::vector vec = {1, 2, 3}; + * std::cout << Joined{vec, ","} << std::endl; // Output: 1,2,3 * - * @param container Element to make joined string. - * @param sep User string used as separator. Default ", ". - * @return Joined elements as string. + * @tparam Container */ template -std::string join(const Container& container, const std::string& sep = ", ") { - std::ostringstream ss; - auto first = std::begin(container); - const auto last = std::end(container); - if (first != last) { - ss << *first; - ++first; - for (; first != last; ++first) { - ss << sep << *first; +struct Joined { + const Container& c; + std::string_view sep; + + friend std::ostream& operator<<(std::ostream& os, const Joined& jv) { + auto first = std::begin(jv.c); + const auto last = std::end(jv.c); + if (first != last) { + os << *first; + for (++first; first != last; ++first) + os << jv.sep << *first; } + return os; + } + + operator std::string() const { + std::ostringstream ss; + ss << *this; + return ss.str(); + } +}; + +/** + * @brief Util to create a string from a container's elements joined by a separator. + * + * @note If R is std::string the container's elements are joined and returned as a std::string. + * If R is std::ostream the container's elements are joined and returned as a Joined<> + * for lazy evaluation in stream context. + * + * @param c The input container. + * @param sep The separator to be used between the elements. Default is ", ". + * @return A string representation of the container's elements joined by the specified separator. + */ +template +auto join(const Container& c, std::string_view sep = ", ") { + if constexpr (std::is_same_v) { + return static_cast(Joined{c, sep}); + } else if constexpr (std::is_same_v) { + return Joined{c, sep}; + } else { } - return ss.str(); } /** @@ -49,16 +80,16 @@ std::string join(const Container& container, const std::string& sep = ", ") { * - std::vector{} -> "[ ]" * * @param v Vector to be converted - * @return String contains + * @return String containing the vector elements */ template std::string vector_to_string(const std::vector& v) { return "[ " + ov::util::join(v) + " ]"; } -std::string to_lower(const std::string& s); +std::string to_lower(std::string_view s); -std::string to_upper(const std::string& s); +std::string to_upper(std::string_view s); inline size_t hash_combine(size_t val, const size_t seed) { return seed ^ (val + 0x9e3779b9 + (seed << 6) + (seed >> 2)); @@ -104,43 +135,33 @@ constexpr uint64_t u64_hash_combine(uint64_t seed, std::initializer_list(strlen(with)); - int so = static_cast(src.length()) - wl; - if (so < 0) - return false; - return 0 == strncmp(with, &src[so], wl); +constexpr bool ends_with(std::string_view src, std::string_view with) { + return src.size() >= with.size() && src.substr(src.size() - with.size()) == with; } -/** - * @brief check string/wstring end with given substring - * @param src - string/wstring to check - * @param with - given substring - * @return true if string end with given substring - */ template -inline bool ends_with(const std::basic_string& str, const std::basic_string& suffix) { - return str.length() >= suffix.length() && 0 == str.compare(str.length() - suffix.length(), suffix.length(), suffix); +constexpr bool ends_with(std::basic_string_view src, std::basic_string_view with) { + return src.size() >= with.size() && src.substr(src.size() - with.size()) == with; } - -std::vector split(const std::string& s, char delimiter, bool trim = false); +/** @} */ template -T ceil_div(const T& x, const T& y) { +constexpr T ceil_div(const T& x, const T& y) { return (x == 0 ? 0 : (1 + (x - 1) / y)); } @@ -183,7 +194,7 @@ T ceil_div(const T& x, const T& y) { * @return True if value found in the container, false otherwise. */ template -bool contains(const R& container, const V& value) { +constexpr bool contains(const R& container, const V& value) { return std::find(std::begin(container), std::end(container), value) != std::end(container); } @@ -192,17 +203,17 @@ bool contains(const R& container, const V& value) { * @param vec - vector with values * @return result of multiplication */ -template -T product(const std::vector& vec) { - return vec.empty() ? T{0} : std::accumulate(vec.begin(), vec.end(), T{1}, std::multiplies()); +template +auto product(const Container& container) { + using T = typename Container::value_type; + return container.empty() ? T{0} : std::accumulate(container.begin(), container.end(), T{1}, std::multiplies()); } /** - * @brief Associative containers doesnt work with remove_if algorithm - * @tparam ContainerT - * @tparam PredicateT - * @param data An associative container - * @param predicate A predicate to remove values conditionally + * @brief Removes elements from the container that satisfy the given predicate. + * + * @param data The container from which to remove elements. + * @param predicate A callable that check element and return true if the element should be removed, or false otherwise. */ template inline void erase_if(Container& data, const PredicateT& predicate) { @@ -215,7 +226,17 @@ inline void erase_if(Container& data, const PredicateT& predicate) { } } -std::string filter_lines_by_prefix(const std::string& str, const std::string& prefix); +/** + * @brief Filters lines in a string view based on a given prefix. + * + * This function iterates through each line in the input string view and returns a string containing + * only the lines that start with the specified `prefix`. + * + * @param sv The input string view containing multiple lines. + * @param prefix The prefix to filter lines by. + * @return A string containing only the lines that start with the given prefix. + */ +std::string filter_lines_by_prefix(std::string_view sv, std::string_view prefix); template constexpr std::array, std::common_type_t, T>, sizeof...(Args)> make_array( @@ -273,6 +294,40 @@ class StringViewStreamBuf : public std::streambuf { } }; +/** + * @brief Adds two integral values + * + * The result value is not valid if overflow detected. + * + * @param T Type of values to add. Must be an integral type. + * @param x First value to add. + * @param y Second value to add. + * @param result Reference to store result value. + * @return True if overflow occurs, false otherwise + */ +template +constexpr bool add_overflow(T x, T y, T& result) { + static_assert(std::is_integral_v, "T must be an integral type"); +#if defined(__GNUC__) || defined(__clang__) + return __builtin_add_overflow(x, y, &result); +#else + constexpr auto max = std::numeric_limits::max(); + + if constexpr (std::is_unsigned_v) { + if (x > max - y) { + return true; + } + } else { + constexpr auto min = std::numeric_limits::lowest(); + if ((y > 0 && x > max - y) || (y < 0 && x < min - y)) { + return true; + } + } + result = x + y; + return false; +#endif +} + /** * @brief Multiplies two integral values * @@ -307,5 +362,123 @@ constexpr bool mul_overflow(T x, T y, T& result) { return false; #endif } -} // namespace util -} // namespace ov + +/** + * @brief This function attempts to parse the input string view `sv` into a number of type `T`. + * + * @tparam T The type of the number to convert to. Must be an arithmetic type. + * @param sv The string view to convert. + * @return std::optional The parsed number if successful, or `std::nullopt` if the conversion fails. + */ +template +std::optional view_to_number(std::string_view sv) noexcept { + static_assert(std::is_arithmetic_v, "T must be an arithmetic type"); + if constexpr (std::is_integral_v) { + T value{}; + const auto result = std::from_chars(sv.data(), sv.data() + sv.size(), value); + return result.ec == std::errc() ? std::make_optional(value) : std::nullopt; + } else { + if (sv == "inf") { + return std::make_optional(std::numeric_limits::infinity()); + } else if (sv == "-inf") { + return std::make_optional(-std::numeric_limits::infinity()); + } else if (sv == "nan") { + return std::make_optional(std::numeric_limits::quiet_NaN()); + } else { + StringViewStreamBuf buf{sv}; + std::istream stream{&buf}; + stream.imbue(std::locale::classic()); + T value{}; + stream >> value; + return stream ? std::make_optional(value) : std::nullopt; + } + } +} + +/** + * @brief Transforms a string view into a container. + * + * This function splits the input string view `sv` using the specified separator `sep` and inserts the parsed values + * into the provided `result` container. + * An optional unary transformation can be applied to the field before insertion. + * + * @param sv The input string view to parse. + * @param output_it The iterator to store the parsed values. + * @param sep The separator used to split the string view. + * @param unary The callable used to transform each field. Defaults to no transformation. + * @return Iterator The output iterator after inserting the parsed values. + */ +template +constexpr Iterator view_transform(std::string_view sv, Iterator output_it, std::string_view sep, UnaryOp unary = {}) { + for (bool has_next = !sv.empty(); has_next; ++output_it) { + const auto sep_pos = sv.find(sep); + if constexpr (const auto field = sv.substr(0, sep_pos); std::is_same_v) { + *output_it = field; + } else { + *output_it = unary(field); + } + has_next = sep_pos != std::string_view::npos; + sv = has_next ? sv.substr(sep_pos + sep.size()) : std::string_view{}; + } + return output_it; +} + +/** + * @brief Transforms a string view into a container. + * + * This function splits the input string view `sv` using the specified separator `sep` and inserts the parsed values + * into the provided `result` container if predicate returns true for the field. + * An optional unary transformation can be applied to the field before insertion. + * + * @param sv The input string view to parse. + * @param output_it The iterator to store the parsed values. + * @param sep The separator used to split the string view. + * @param predicate The callable used to validate each field. + * @param unary The callable used to transform each field. Defaults to no transformation. + * @return Iterator The output iterator after inserting the parsed values. + */ +template +constexpr Iterator view_transform_if(std::string_view sv, + Iterator output_it, + std::string_view sep, + Predicate predicate, + UnaryOp unary = {}) { + for (bool has_next = !sv.empty(); has_next; ++output_it) { + const auto sep_pos = sv.find(sep); + if (const auto field = sv.substr(0, sep_pos); predicate(field)) { + if constexpr (std::is_same_v) { + *output_it = field; + } else { + *output_it = unary(field); + } + } + has_next = sep_pos != std::string_view::npos; + sv = has_next ? sv.substr(sep_pos + sep.size()) : std::string_view{}; + } + return output_it; +} + +/** + * @brief Splits a string view into a vector of string views, optionally validating each field. + * + * This function splits the input string view `sv` using the specified separator `sep` and inserts the parsed values + * into the provided `result` container. + * An optional `predicate` can be provided to validate each field before insertion. + * + * @tparam Predicate A callable type used to validate each field. Defaults to `std::nullptr_t`, no validation. + * @param sv The input string view to parse. + * @param sep The separator used to split the string view. Defaults to ",". + * @param predicate The callable used to validate each field. Defaults to no validation. + * @return Container The container with the parsed string views. + */ +template +std::vector split(std::string_view sv, std::string_view sep = ",", Predicate predicate = {}) { + std::vector result{}; + if constexpr (std::is_same_v) { + view_transform(sv, std::back_inserter(result), sep); + } else { + view_transform_if(sv, std::back_inserter(result), sep, predicate); + } + return result; +} +} // namespace ov::util diff --git a/src/common/util/include/openvino/util/file_util.hpp b/src/common/util/include/openvino/util/file_util.hpp index 66931b852b03..8dd3a1245714 100644 --- a/src/common/util/include/openvino/util/file_util.hpp +++ b/src/common/util/include/openvino/util/file_util.hpp @@ -93,10 +93,15 @@ inline auto path_to_string(const std::filesystem::path& path) -> decltype(path_t return path_to_string(path.native()); } -/// \brief Remove path components which would allow traversing up a directory tree. -/// \param path A path to file -/// \return A sanitized path -std::string sanitize_path(const std::string& path); +/** + * @brief Resolves and validates a path relative to a base directory to prevent path traversal. + * + * @param base Base directory. If empty, the current working directory is used. + * @param relative_path Path relative to @p dir . + * @return Absolute, normalized path within @p dir. + * @throw std::runtime_error if the resolved path escapes the base directory. + */ +std::filesystem::path sanitize_path(const std::filesystem::path& base, const std::filesystem::path& relative_path); /** * @brief Interface function to get absolute path of file @@ -171,7 +176,6 @@ inline bool file_exists(const Path& path) noexcept { std::filesystem::path get_directory(const std::filesystem::path& path); std::filesystem::path path_join(std::initializer_list&& paths); -std::wstring path_join_w(std::initializer_list&& paths); /** * @brief Iterates over files in given directory and applies provided function to each file found. diff --git a/src/common/util/include/openvino/util/log.hpp b/src/common/util/include/openvino/util/log.hpp index ab1292a17d4b..7598b84e684e 100644 --- a/src/common/util/include/openvino/util/log.hpp +++ b/src/common/util/include/openvino/util/log.hpp @@ -40,7 +40,7 @@ class LogHelper { std::stringstream m_stream; }; -#ifdef ENABLE_OPENVINO_DEBUG +#if defined(ENABLE_DEBUG_CAPS) || defined(ENABLE_OPENVINO_DEBUG) /* Template function _write_all_to_stream has duplicates * It's defined in: * intel_cpu/src/utils/debug_capabilities and src/core/include/openvino/core/except.hpp @@ -59,6 +59,29 @@ static inline std::ostream& _write_all_to_stream(std::ostream& os, const T& arg, # define OPENVINO_LOG_STREAM(OPENVINO_HELPER_LOG_TYPE) \ ::ov::util::LogHelper(::ov::util::LOG_TYPE::OPENVINO_HELPER_LOG_TYPE, __FILE__, __LINE__).stream() +static inline bool is_terminal_output() { +# ifdef _WIN32 + // No Windows support for colored logs for now. + return false; +# else + static const bool stdout_to_terminal = isatty(fileno(stdout)); + return stdout_to_terminal; +# endif +} + +# define OPENVINO_RESET (ov::util::is_terminal_output() ? "\033[0m" : "") +# define OPENVINO_RED (ov::util::is_terminal_output() ? "\033[31m" : "") +# define OPENVINO_GREEN (ov::util::is_terminal_output() ? "\033[1;32m" : "") +# define OPENVINO_YELLOW (ov::util::is_terminal_output() ? "\033[33m" : "") +# define OPENVINO_BLOCK_BEG "{" +# define OPENVINO_BLOCK_END "}" +# define OPENVINO_BLOCK_BODY "│" +# define OPENVINO_BLOCK_BODY_RIGHT "├─" + +#endif + +#ifdef ENABLE_OPENVINO_DEBUG + # define OPENVINO_ERR(...) \ do { \ ov::util::_write_all_to_stream(OPENVINO_LOG_STREAM(_LOG_TYPE_ERROR), __VA_ARGS__); \ @@ -79,29 +102,27 @@ static inline std::ostream& _write_all_to_stream(std::ostream& os, const T& arg, ov::util::_write_all_to_stream(OPENVINO_LOG_STREAM(_LOG_TYPE_DEBUG), __VA_ARGS__); \ } while (0) +#else +# define OPENVINO_ERR(...) \ + do { \ + } while (0) +# define OPENVINO_WARN(...) \ + do { \ + } while (0) +# define OPENVINO_INFO(...) \ + do { \ + } while (0) +# define OPENVINO_DEBUG(...) \ + do { \ + } while (0) +#endif + +#ifdef ENABLE_DEBUG_CAPS + static const bool logging_enabled = ov::util::getenv_bool("OV_MATCHER_LOGGING"); static const std::unordered_set matchers_to_log = ov::util::split_by_delimiter(ov::util::getenv_string("OV_MATCHERS_TO_LOG"), ','); -static inline bool is_terminal_output() { -# ifdef _WIN32 - // No Windows support for colored logs for now. - return false; -# else - static const bool stdout_to_terminal = isatty(fileno(stdout)); - return stdout_to_terminal; -# endif -} - -# define OPENVINO_RESET (ov::util::is_terminal_output() ? "\033[0m" : "") -# define OPENVINO_RED (ov::util::is_terminal_output() ? "\033[31m" : "") -# define OPENVINO_GREEN (ov::util::is_terminal_output() ? "\033[1;32m" : "") -# define OPENVINO_YELLOW (ov::util::is_terminal_output() ? "\033[33m" : "") -# define OPENVINO_BLOCK_BEG "{" -# define OPENVINO_BLOCK_END "}" -# define OPENVINO_BLOCK_BODY "│" -# define OPENVINO_BLOCK_BODY_RIGHT "├─" - # define OPENVINO_LOG_MATCHING(matcher_ptr, ...) \ do { \ if (ov::util::logging_enabled) { \ @@ -122,18 +143,6 @@ static inline bool is_terminal_output() { } while (0) #else -# define OPENVINO_ERR(...) \ - do { \ - } while (0) -# define OPENVINO_WARN(...) \ - do { \ - } while (0) -# define OPENVINO_INFO(...) \ - do { \ - } while (0) -# define OPENVINO_DEBUG(...) \ - do { \ - } while (0) # define OPENVINO_LOG_MATCHING(matcher_ptr, ...) \ do { \ } while (0) diff --git a/src/common/util/include/openvino/util/memory.hpp b/src/common/util/include/openvino/util/memory.hpp new file mode 100644 index 000000000000..d578af6c94d5 --- /dev/null +++ b/src/common/util/include/openvino/util/memory.hpp @@ -0,0 +1,114 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#pragma once + +#include +#include +#include +#include +#include + +namespace ov::util { + +/** + * @brief Rounds @p size up to the nearest multiple of @p alignment. + * + * @param size Value to round up. + * @param alignment Alignment boundary. Must be a power of two and greater than zero. + * @return Smallest value >= @p size that is a multiple of @p alignment. + */ +constexpr size_t align_size_up(size_t size, size_t alignment) noexcept { + return (size + alignment - 1) & ~(alignment - 1); +} + +/** + * @brief Rounds @p size down to the nearest multiple of @p alignment. + * + * @param alignment Alignment boundary. Must be a power of two and greater than zero. + * @return Largest value <= @p size that is a multiple of @p alignment. + */ +constexpr size_t align_size_down(size_t size, size_t alignment) noexcept { + return size & ~(alignment - 1); +} + +/** @brief Represents a memory region aligned to a power-of-two boundary. */ +struct AlignedRegion { + uintptr_t m_address = 0; //!< Aligned base address (rounded down to boundary) + size_t m_length = 0; //!< Total length of the aligned region including the gap + size_t m_gap = 0; //!< Gap from the aligned address to the original unaligned address +}; + +/** + * @brief Aligns a memory region to a power-of-two boundary (rounded down). + * + * Computes the largest aligned address <= @p base and the gap between that + * aligned address and @p base, returning a region large enough to cover + * [base, base + raw_len). + * + * @param base The original (potentially unaligned) base address. + * @param raw_len The length of the region starting at @p base. + * @param alignment The alignment boundary. Must be a power of two and greater than zero. + * @return AlignedRegion covering at least [base, base + raw_len). + */ +constexpr AlignedRegion align_region(uintptr_t base, size_t raw_len, size_t alignment) noexcept { + const auto aligned = base & ~(static_cast(alignment) - 1); + const auto gap = static_cast(base - aligned); + return {aligned, raw_len + gap, gap}; +} + +/** + * @brief Allocates @p size bytes of uninitialized memory on the specified @p alignment boundary. + * + * + * @param size Number of bytes to allocate. Must be greater than zero. + * @param alignment Desired alignment in bytes. Must be a power of two. + * Passing `0` applies no specific alignment constraint (`alignof(std::max_align_t)` is used). + * @return Pointer to the allocated memory, or `nullptr` on failure. + */ +void* aligned_alloc(size_t size, size_t alignment) noexcept; + +/** + * @brief Releases memory previously allocated by @ref aligned_alloc. + * + * @param ptr Pointer returned by @ref aligned_alloc. Passing `nullptr` is a no-op. + */ +void aligned_free(void* ptr) noexcept; + +/** + * @brief Reserves virtual address space of the given size without backing it with physical memory. + * The region is inaccessible until vm_commit() is called. Release with vm_release() when no longer needed. + * @param size Size in bytes to reserve. Must be greater than 0. + * @param ec Set to the OS error code on failure, cleared on success. + * @return Pointer to the reserved region, or nullptr on failure. + */ +void* vm_reserve(size_t size, std::error_code& ec) noexcept; + +/** + * @brief Commits a previously reserved region, making it readable and writable. + * @param ptr Pointer returned by vm_reserve(). + * @param size Size in bytes to commit. Must be greater than 0. + * @param ec Set to the OS error code on failure, cleared on success. + */ +void vm_commit(void* ptr, size_t size, std::error_code& ec) noexcept; + +/** + * @brief Decommits a committed region: revokes access and returns physical pages to the OS. + * The virtual address range remains reserved and can be committed again with vm_commit(). + * @param ptr Pointer returned by vm_reserve(). Must not be nullptr. + * @param size Size in bytes to decommit. Must be greater than 0. + * @pre ptr != nullptr && size > 0; violated preconditions are a programming error (assert fires in debug). + */ +void vm_decommit(void* ptr, size_t size) noexcept; + +/** + * @brief Releases the reserved virtual address range. Can be called without a prior vm_decommit(). + * After this call the pointer is invalid and must not be used. + * @param ptr Pointer returned by vm_reserve(). Must not be nullptr. + * @param size Size in bytes originally passed to vm_reserve(). Must be greater than 0. + * @pre ptr != nullptr && size > 0; violated preconditions are a programming error (assert fires in debug). + */ +void vm_release(void* ptr, size_t size) noexcept; + +} // namespace ov::util diff --git a/src/common/util/include/openvino/util/mmap_object.hpp b/src/common/util/include/openvino/util/mmap_object.hpp index d874b2a5e807..ce0bc69e1c82 100644 --- a/src/common/util/include/openvino/util/mmap_object.hpp +++ b/src/common/util/include/openvino/util/mmap_object.hpp @@ -11,6 +11,7 @@ #include #include +#include #include #include @@ -27,6 +28,10 @@ using FileHandle = void*; // Linux/Unix uses int for file descriptors using FileHandle = int; #endif +/** + * @brief Generic constant to indicate automatic size calculation is required. + */ +inline constexpr auto auto_size = std::numeric_limits::max(); /** * @brief This class represents a mapped memory. @@ -39,13 +44,17 @@ class MappedMemory { virtual size_t size() const noexcept = 0; virtual uint64_t get_id() const noexcept = 0; virtual ~MappedMemory() = default; + virtual void hint_evict(size_t offset = 0, size_t size = auto_size) noexcept = 0; + /** + * @brief Hint that the given region of the mapping will be accessed soon. + * + * @param offset Offset within the mapping where prefetching starts. + * @param size Number of bytes to prefetch. Defaults to the rest of the + * mapping when set to auto_size. + */ + virtual void hint_prefetch(size_t offset = 0, size_t size = auto_size) = 0; }; -/** - * @brief Generic constant to indicate automatic size calculation is required. - */ -inline constexpr auto auto_size = std::numeric_limits::max(); - /** * @brief Returns mapped memory for a file from provided path. * Instead of reading files, we can map the memory via mmap for Linux @@ -54,11 +63,15 @@ inline constexpr auto auto_size = std::numeric_limits::max(); * @param path Path to a file which memory will be mmaped. * @param offset Offset in the file where the mapping starts. * @param size Size of the mapping. If size is std::numeric_limits::max(), maps from offset to EOF. + * @param no_placeholder When true, skip the Windows 10+ placeholder/VEH mechanism and use the legacy + * single-call MapViewOfFile path instead. This guarantees a uniform AllocationBase + * across the whole mapping, required for NPU zero-copy blob import. On Linux ignored. * @return MappedMemory shared ptr object which keep mmaped memory and control the lifetime. */ std::shared_ptr load_mmap_object(const std::filesystem::path& path, size_t offset = 0, - size_t size = auto_size); + size_t size = auto_size, + bool no_placeholder = false); /** * @brief Returns mapped memory for a file from provided file handle (cross-platform). diff --git a/src/common/util/include/openvino/util/parallel_io.hpp b/src/common/util/include/openvino/util/parallel_io.hpp new file mode 100644 index 000000000000..8c6cd6fcc220 --- /dev/null +++ b/src/common/util/include/openvino/util/parallel_io.hpp @@ -0,0 +1,90 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +/** + * @brief A header file for definition of platform-specific parallel I/O primitives. + * @file parallel_io.hpp + */ + +#pragma once + +#include +#include + +#include "openvino/util/mmap_object.hpp" + +namespace ov::util { + +#ifndef _WIN32 +inline constexpr FileHandle INVALID_HANDLE_VALUE = -1; +#endif + +inline constexpr size_t default_parallel_io_threshold = 4UL * 1024 * 1024; ///< 4 MB default threshold for parallel I/O +inline constexpr size_t default_parallel_io_min_chunk = 2UL * 1024 * 1024; ///< 2 MB minimum chunk size per thread +/** @brief Default upper bound for ParallelReadStreamBuf::prefetch() requests. + * Sized to cover a typical large-model deserialization burst in a single window while keeping + * prefetch allocation and dispatch cost bounded. Reads outside the window fall back to file I/O. + */ +inline constexpr size_t default_parallel_io_prefetch_cap = 32UL * 1024 * 1024; + +/** + * @brief Open a file for reading and retrieve its size. + * + * On Linux, uses open(O_RDONLY | O_CLOEXEC) + fstat. + * On Windows, uses CreateFileW(GENERIC_READ) + GetFileSizeEx. + * + * @param path Path to the file to open. + * @param file_offset Header offset to validate (must be within [0, file_size]). + * @param out_handle [out] The opened file handle / descriptor. + * @param out_size [out] Total size of the file in bytes. + * @throws std::runtime_error If the file cannot be opened or its size cannot be queried. + * @throws std::out_of_range If file_offset is outside [0, file_size]. + */ +void get_file_handle_and_size(const std::filesystem::path& path, + std::streamoff file_offset, + FileHandle& out_handle, + std::streamoff& out_size); + +/** + * @brief Close a platform file handle. + * + * On Linux, calls close(fd). On Windows, calls CloseHandle(handle). + * Safe to call with an invalid handle (no-op). + * + * @param handle The file handle to close. + */ +void close_file_handle(FileHandle handle); + +/** + * @brief Open a file for reading (lightweight, for per-thread file handles). + * + * Each worker thread in a parallel read should open its own file handle so that + * the OS readahead state is independent per thread. + * + * On Linux, uses open(O_RDONLY | O_CLOEXEC). + * On Windows, uses CreateFileW(GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE). + * + * @param path Path to the file. + * @return A valid file handle, or the platform-specific invalid value on failure. + */ +FileHandle open_file_for_read(const std::filesystem::path& path); + +/** + * @brief Read bytes from a file at a given absolute offset (single-threaded). + * + * On Linux, uses pread() in a loop. + * On Windows, uses SetFilePointerEx + ReadFile in a loop. + * + * @note This function is @b not thread-safe for a given handle. Callers that + * perform parallel reads must open a separate handle per thread (see + * open_file_for_read). + * + * @param handle File handle / descriptor. + * @param dst Destination buffer. + * @param size Number of bytes to read. + * @param file_offset Absolute byte offset in the file. + * @return true if all bytes were read successfully, false on I/O error. + */ +bool positional_read(FileHandle handle, char* dst, size_t size, size_t file_offset); +} // namespace ov::util \ No newline at end of file diff --git a/src/common/util/include/openvino/util/parallel_read_streambuf.hpp b/src/common/util/include/openvino/util/parallel_read_streambuf.hpp new file mode 100644 index 000000000000..2661e3b9ad7e --- /dev/null +++ b/src/common/util/include/openvino/util/parallel_read_streambuf.hpp @@ -0,0 +1,105 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +/** + * @brief A header file for definition of a parallel I/O streambuf for file-based reads. + * @file parallel_read_streambuf.hpp + */ + +#pragma once + +#include +#include +#include + +#include "openvino/util/parallel_io.hpp" + +namespace ov::util { + +/** + * @brief A std::streambuf that reads from a file using parallel I/O for large + * reads, bypassing the OS page cache pressure that mmap+memcpy incurs. + * + * For reads >= threshold bytes, the read is split across N threads where each + * thread issues its own independent positional read operation using + * platform-specific APIs. Smaller reads fall through to a single positional call. + * + * Usage: + * @code + * ParallelReadStreamBuf buf(cache_path, blob_offset_in_file); + * std::istream stream(&buf); + * cldnn::BinaryInputBuffer ib(stream, engine); + * ib >> ...; + * @endcode + */ +class ParallelReadStreamBuf : public std::streambuf { +public: + /** + * @param path Path to the file to read. + * @param header_offset Initial file position (absolute offset from the start + * of the file; the stream starts reading from here). + * @param threshold Minimum read size to trigger parallel I/O. + */ + explicit ParallelReadStreamBuf(const std::filesystem::path& path, + std::streamoff header_offset = 0, + size_t threshold = default_parallel_io_threshold); + + ~ParallelReadStreamBuf() override; + + ParallelReadStreamBuf(const ParallelReadStreamBuf&) = delete; + ParallelReadStreamBuf& operator=(const ParallelReadStreamBuf&) = delete; + + /** + * @brief Preload @p size bytes starting at the current logical position into + * an internal buffer using one parallel positional read. + * + * After a successful prefetch, subsequent xsgetn()/underflow() calls that + * fall inside [current_pos, current_pos + size) are served from memory via + * memcpy instead of issuing per-call pread(). Reads that fall outside the + * prefetched window transparently fall back to the normal file-IO path and + * invalidate the prefetched window. + * + * Intended call site: the producer of a long serialized region (e.g. + * program::weights_load in the GPU plugin) calls prefetch() once at the + * start of the region to collapse thousands of small ib >> ... small-reads + * into a single bulk parallel pread. + * + * @param size Number of bytes to preload. Clamped to remaining file size. + * @return true if the prefetch read succeeded (buffer is now valid), false + * otherwise (buffer is left empty; reads fall back to file I/O). + */ + bool prefetch(std::streamsize size); + +protected: + std::streamsize xsgetn(char_type* dst, std::streamsize n) override; + int_type underflow() override; + pos_type seekoff(off_type off, std::ios_base::seekdir way, std::ios_base::openmode which) override; + pos_type seekpos(pos_type pos, std::ios_base::openmode which) override; + std::streamsize showmanyc() override; + +private: + bool single_read(char* dst, size_t size, size_t file_offset); + bool parallel_read(char* dst, size_t size, size_t file_offset); + + static constexpr size_t UNDERFLOW_BUF = 8192; ///< batch size for char-by-char reads + + std::filesystem::path m_path; + FileHandle m_handle; ///< platform file handle + + std::streamoff m_file_offset = 0; ///< absolute file offset of next byte to read + std::streamoff m_header_offset = 0; ///< absolute file offset of logical stream start + std::streamoff m_file_size = 0; + size_t m_threshold = default_parallel_io_threshold; + std::unique_ptr m_underflow_buf; ///< lazily allocated buffer for underflow() + + std::unique_ptr m_prefetch_buf; ///< host-side prefetch buffer; null when capacity is zero + std::streamoff m_prefetch_begin = 0; ///< absolute file offset of m_prefetch_buf[0] + size_t m_prefetch_size = 0; ///< valid bytes in m_prefetch_buf (0 = window invalid) + size_t m_prefetch_capacity = 0; ///< allocated capacity of m_prefetch_buf in bytes + + bool serve_from_prefetch(char* dst, size_t size, std::streamoff abs_offset); + void invalidate_prefetch(); +}; + +} // namespace ov::util \ No newline at end of file diff --git a/src/common/util/include/openvino/util/weights_path.hpp b/src/common/util/include/openvino/util/weights_path.hpp deleted file mode 100644 index 97dccb10a3fc..000000000000 --- a/src/common/util/include/openvino/util/weights_path.hpp +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (C) 2018-2026 Intel Corporation -// SPDX-License-Identifier: Apache-2.0 -// - -#pragma once - -#include "openvino/util/common_util.hpp" - -namespace ov { -namespace util { - -bool validate_weights_path(const std::string& weights_path); - -} // namespace util -} // namespace ov diff --git a/src/common/util/include/openvino/util/xml_parse_utils.hpp b/src/common/util/include/openvino/util/xml_parse_utils.hpp index 1c10077bf595..8849e4e48c2d 100644 --- a/src/common/util/include/openvino/util/xml_parse_utils.hpp +++ b/src/common/util/include/openvino/util/xml_parse_utils.hpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -28,13 +29,11 @@ */ #define FOREACH_CHILD(c, p, tag) for (auto c = p.child(tag); !c.empty(); c = c.next_sibling(tag)) -namespace ov { -namespace util { +namespace ov::util::pugixml { /** * @brief XML helpers function to extract values from `pugi::xml_node` */ -namespace pugixml { /** * @brief Gets the integer attribute from `pugi::xml_node` @@ -242,6 +241,13 @@ inline ParseResult parse_xml(const std::filesystem::path& file_path) { return {std::move(nullptr), std::string("Error loading XML file: ") + e.what()}; } } -} // namespace pugixml -} // namespace util -} // namespace ov + +/** + * @brief Get the string view on attribute value. + * + * @param node The XML node. + * @param name The attribute name. + * @return String view on attribute value if attribute exists, std::nullopt otherwise. + */ +std::optional get_attribute_view(const pugi::xml_node& node, std::string_view name); +} // namespace ov::util::pugixml diff --git a/src/common/util/src/common_util.cpp b/src/common/util/src/common_util.cpp index d388c90bf374..0a360d81b3c6 100644 --- a/src/common/util/src/common_util.cpp +++ b/src/common/util/src/common_util.cpp @@ -5,50 +5,28 @@ #include "openvino/util/common_util.hpp" #include +#include -std::string ov::util::to_lower(const std::string& s) { - std::string rc = s; - std::transform(rc.begin(), rc.end(), rc.begin(), ::tolower); +std::string ov::util::to_lower(std::string_view s) { + std::string rc{s}; + std::transform(rc.begin(), rc.end(), rc.begin(), [](unsigned char c) { + return std::tolower(c); + }); return rc; } -std::string ov::util::to_upper(const std::string& s) { - std::string rc = s; - std::transform(rc.begin(), rc.end(), rc.begin(), ::toupper); +std::string ov::util::to_upper(std::string_view s) { + std::string rc{s}; + std::transform(rc.begin(), rc.end(), rc.begin(), [](unsigned char c) { + return std::toupper(c); + }); return rc; } -std::vector ov::util::split(const std::string& src, char delimiter, bool do_trim) { - size_t pos; - std::string token; - size_t start = 0; - std::vector rc; - while ((pos = src.find(delimiter, start)) != std::string::npos) { - token = src.substr(start, pos - start); - start = pos + 1; - if (do_trim) { - token = trim(token); - } - rc.push_back(token); - } - if (start <= src.size()) { - token = src.substr(start); - if (do_trim) { - token = trim(token); - } - rc.push_back(token); - } - return rc; -} - -std::string ov::util::filter_lines_by_prefix(const std::string& str, const std::string& prefix) { - auto lines = ov::util::split(str, '\n'); - std::stringstream res; - const char* const prefix_c = prefix.c_str(); - for (const auto& line : lines) { - if (line.find(prefix_c) == 0) { - res << line + '\n'; - } - } +std::string ov::util::filter_lines_by_prefix(std::string_view sv, std::string_view prefix) { + std::ostringstream res; + view_transform_if(sv, std::ostream_iterator(res, "\n"), "\n", [&prefix](auto&& field) { + return field.find(prefix) == 0; + }); return res.str(); } diff --git a/src/common/util/src/file_util.cpp b/src/common/util/src/file_util.cpp index 891c20f57f35..45b4d6137882 100644 --- a/src/common/util/src/file_util.cpp +++ b/src/common/util/src/file_util.cpp @@ -47,15 +47,10 @@ std::filesystem::path path_join(Container&& paths) { return joined_path; } -// TODO: Remove string() / wstring() casts on function call site std::filesystem::path ov::util::path_join(std::initializer_list&& paths) { return ::path_join<>(std::move(paths)); } -std::wstring ov::util::path_join_w(std::initializer_list&& paths) { - return ::path_join<>(std::move(paths)).wstring(); -} - namespace { void process_dir_entry(const std::filesystem::directory_entry& dir_entry, const std::function& func) { @@ -104,12 +99,20 @@ void ov::util::recursive_iterate_files(const std::filesystem::path& path, } } -std::string ov::util::sanitize_path(const std::string& path) { - const auto colon_pos = path.find(':'); - const auto sanitized_path = path.substr(colon_pos == std::string::npos ? 0 : colon_pos + 1); - const std::string to_erase = "/.\\"; - const auto start = sanitized_path.find_first_not_of(to_erase); - return (start == std::string::npos) ? "" : sanitized_path.substr(start); +std::filesystem::path ov::util::sanitize_path(const std::filesystem::path& base, + const std::filesystem::path& relative_path) { + const auto base_dir = base.empty() ? std::filesystem::current_path() : base; + const auto base_canon = std::filesystem::weakly_canonical(base_dir); + auto merged_canon = std::filesystem::weakly_canonical(base_dir / relative_path); + + if (const auto rel = merged_canon.lexically_relative(base_canon); rel.empty() || *rel.begin() == "..") { + std::stringstream ss; + ss << "Path '" << path_to_string(relative_path) << "' resolves to '" << path_to_string(merged_canon) + << "' which is outside the base directory '" << path_to_string(base_canon) << "'"; + throw std::runtime_error(ss.str()); + } + + return ov::util::get_absolute_file_path(merged_canon); } std::filesystem::path ov::util::get_absolute_file_path(const std::filesystem::path& path) { diff --git a/src/common/util/src/os/lin/lin_memory.cpp b/src/common/util/src/os/lin/lin_memory.cpp new file mode 100644 index 000000000000..df92832c4059 --- /dev/null +++ b/src/common/util/src/os/lin/lin_memory.cpp @@ -0,0 +1,65 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include + +#include +#include +#include +#include +#include +#include + +#include "openvino/util/memory.hpp" + +namespace ov::util { + +void* aligned_alloc(size_t size, size_t alignment) noexcept { + if (alignment == 0) { + alignment = alignof(std::max_align_t); + } + return std::aligned_alloc(alignment, align_size_up(size, alignment)); +} + +void aligned_free(void* ptr) noexcept { + std::free(ptr); +} + +void* vm_reserve(size_t size, std::error_code& ec) noexcept { + const auto p = mmap(nullptr, size, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (p == MAP_FAILED) { + ec = std::error_code(errno, std::system_category()); + return nullptr; + } + ec = {}; + return p; +} + +void vm_commit(void* ptr, size_t size, std::error_code& ec) noexcept { + if (mprotect(ptr, size, PROT_READ | PROT_WRITE) == -1) { + ec = std::error_code(errno, std::system_category()); + } else { + ec = {}; + } +} + +void vm_decommit(void* ptr, size_t size) noexcept { + assert(ptr != nullptr && size > 0); +#if defined(__linux__) + std::ignore = mprotect(ptr, size, PROT_NONE); + std::ignore = madvise(ptr, size, MADV_DONTNEED); +#elif defined(__APPLE__) && defined(MADV_FREE_REUSABLE) + std::ignore = mprotect(ptr, size, PROT_NONE); + std::ignore = madvise(ptr, size, MADV_FREE_REUSABLE); +#else + std::ignore = mmap(ptr, size, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0); +#endif +} + +void vm_release(void* ptr, size_t size) noexcept { + assert(ptr != nullptr && size > 0); + std::ignore = munmap(ptr, size); +} + +} // namespace ov::util diff --git a/src/common/util/src/os/lin/lin_mmap_object.cpp b/src/common/util/src/os/lin/lin_mmap_object.cpp index 9319fcffbf4a..76739faec458 100644 --- a/src/common/util/src/os/lin/lin_mmap_object.cpp +++ b/src/common/util/src/os/lin/lin_mmap_object.cpp @@ -8,12 +8,17 @@ #include #include +#include +#include #include #include #include +#include +#include #include "openvino/util/common_util.hpp" #include "openvino/util/file_util.hpp" +#include "openvino/util/memory.hpp" #include "openvino/util/mmap_object.hpp" namespace ov { @@ -22,8 +27,87 @@ int64_t get_system_page_size() { static auto page_size = static_cast(sysconf(_SC_PAGE_SIZE)); return page_size; } + +/** + * @brief Creates a memory region for mmap operations. + * + * @param offset The offset within the mmap region. + * @param size The size of the region. + * @return AlignedRegion The aligned memory region. + */ +inline util::AlignedRegion make_mmap_region(size_t offset, size_t size) { + const auto page_size = static_cast(util::get_system_page_size()); + return util::align_region(static_cast(offset), size, page_size); +} + +/** + * @brief Creates a memory region for madvise operations. + * + * @param data The base address of the mapped memory region. + * @param mapping_size The size of the mapped memory region. + * @param offset The offset within the mapped memory region. + * @param size The size of the region. + * @return AlignedRegion The aligned memory region. + */ +inline util::AlignedRegion make_madvise_region(const void* data, size_t mapping_size, size_t offset, size_t size) { + const auto page_size = static_cast(util::get_system_page_size()); + if (data == nullptr || mapping_size == 0 || offset >= mapping_size || size < page_size) { + return {}; + } else { + const auto available = mapping_size - offset; + const auto raw_len = (size == auto_size) ? available : std::min(size, available); + return util::align_region(reinterpret_cast(data) + offset, raw_len, page_size); + } +} } // namespace util +namespace { +/** + * @brief Touches memory pages in parallel to trigger page faults and populate the page cache. + * + * Spawns worker threads that each read one byte per page in their assigned range. + * * No-op if the region length is below the prefault threshold. Below that threshold the overhead + * of spawning threads exceeds the benefit. + * + * @param region Page-aligned memory region to prefault. + * @param prefault_threshold Minimum region length in bytes to trigger parallel prefaulting (default 4 MiB). + */ +void populate_pages(const util::AlignedRegion& region, size_t prefault_threshold = 4 * 1024 * 1024) { + if (region.m_length < prefault_threshold) + return; + + const auto page = static_cast(util::get_system_page_size()); + const size_t pages = (region.m_length + page - 1) / page; + + const size_t hw_threads = std::thread::hardware_concurrency(); + constexpr size_t min_chunk_size = 1 * 1024 * 1024; // 1 MiB per thread minimum + constexpr size_t max_prefault_threads = 10; + const size_t num_threads = + std::min({hw_threads, pages, max_prefault_threads, std::max(1, region.m_length / min_chunk_size)}); + + std::vector threads; + const auto base = reinterpret_cast(region.m_address); + + for (size_t tid = 0; tid < num_threads; ++tid) { + threads.emplace_back([&, tid] { + const size_t begin_page = pages * tid / num_threads; + const size_t end_page = pages * (tid + 1) / num_threads; + volatile uint8_t local = 0; // prevents compiler from optimizing the loop away as a no-op + + for (size_t p = begin_page; p < end_page; ++p) { + const size_t off = p * page; + if (off < region.m_length) { + local += base[off]; + } + } + }); + } + for (auto& t : threads) { + t.join(); + } +} +} // namespace + class HandleHolder { int m_handle = -1; void reset() noexcept { @@ -98,15 +182,14 @@ class MapHolder final : public MappedMemory { } if (m_size > 0) { - const auto page_size = util::get_system_page_size(); - const auto aligned_offset = (offset / page_size) * page_size; - m_mapped_view_size = offset + m_size - aligned_offset; - m_mapped_view = mmap(nullptr, m_mapped_view_size, PROT_READ, MAP_SHARED, fd, aligned_offset); + const auto& [aligned_offset, length, gap] = util::make_mmap_region(offset, m_size); + m_mapped_view_size = length; + m_mapped_view = mmap(nullptr, length, PROT_READ, MAP_SHARED, fd, aligned_offset); if (m_mapped_view == MAP_FAILED) { throw std::runtime_error("Can not create file mapping for " + std::to_string(fd) + ", err=" + std::strerror(errno)); } - m_data = static_cast(m_mapped_view) + (offset - aligned_offset); + m_data = static_cast(m_mapped_view) + gap; } m_id = util::u64_hash_combine(static_cast(sb.st_ino), {static_cast(sb.st_dev), offset, size}); @@ -129,9 +212,32 @@ class MapHolder final : public MappedMemory { size_t size() const noexcept override { return m_size; } + + void hint_evict(size_t offset, size_t size) noexcept override { + if (m_mapped_view != MAP_FAILED) { + if (const auto region = util::make_madvise_region(m_data, m_size, offset, size); region.m_length > 0) { + std::ignore = madvise(reinterpret_cast(region.m_address), region.m_length, MADV_DONTNEED); + } + } + } + + void hint_prefetch(size_t offset, size_t size) override { + if (m_data == nullptr || offset >= m_size) { + return; + } + + if (const auto region = util::make_madvise_region(m_data, m_size, offset, size); region.m_length > 0) { + std::ignore = madvise(reinterpret_cast(region.m_address), region.m_length, MADV_SEQUENTIAL); + std::ignore = madvise(reinterpret_cast(region.m_address), region.m_length, MADV_WILLNEED); + ov::populate_pages(region); + } + } }; -std::shared_ptr load_mmap_object(const std::filesystem::path& path, size_t offset, size_t size) { +std::shared_ptr load_mmap_object(const std::filesystem::path& path, + size_t offset, + size_t size, + bool /* no_placeholder */) { auto holder = std::make_shared(); holder->set(path, offset, size); return holder; diff --git a/src/common/util/src/os/lin/parallel_io.cpp b/src/common/util/src/os/lin/parallel_io.cpp new file mode 100644 index 000000000000..0c42d1597d97 --- /dev/null +++ b/src/common/util/src/os/lin/parallel_io.cpp @@ -0,0 +1,69 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "openvino/util/parallel_io.hpp" + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "openvino/util/file_util.hpp" + +namespace ov::util { + +void get_file_handle_and_size(const std::filesystem::path& path, + std::streamoff file_offset, + FileHandle& out_handle, + std::streamoff& out_size) { + out_handle = ::open(path.c_str(), O_RDONLY | O_CLOEXEC); + if (out_handle == -1) { + throw std::runtime_error("Cannot open file: " + ov::util::path_to_string(path)); + } + struct stat st = {}; + if (::fstat(out_handle, &st) != 0) { + ::close(out_handle); + out_handle = -1; + throw std::runtime_error("Cannot stat file: " + ov::util::path_to_string(path)); + } + out_size = static_cast(st.st_size); + if (file_offset < 0 || file_offset > out_size) { + ::close(out_handle); + out_handle = -1; + throw std::out_of_range("header_offset is out of range for file: " + ov::util::path_to_string(path)); + } +} + +void close_file_handle(FileHandle handle) { + if (handle != -1) { + ::close(handle); + } +} + +FileHandle open_file_for_read(const std::filesystem::path& path) { + return ::open(path.c_str(), O_RDONLY | O_CLOEXEC); +} + +bool positional_read(FileHandle handle, char* dst, size_t size, size_t file_offset) { + char* cur = dst; + size_t remaining = size; + off_t cur_offset = static_cast(file_offset); + while (remaining > 0) { + const ssize_t n = ::pread(handle, cur, remaining, cur_offset); + if (n <= 0) { + return false; + } + cur += n; + cur_offset += n; + remaining -= static_cast(n); + } + return true; +} + +} // namespace ov::util diff --git a/src/common/util/src/wstring_convert_util.cpp b/src/common/util/src/os/lin/wstring_convert_util.cpp similarity index 85% rename from src/common/util/src/wstring_convert_util.cpp rename to src/common/util/src/os/lin/wstring_convert_util.cpp index cd900a7cdd27..5f360e9fc5a3 100644 --- a/src/common/util/src/wstring_convert_util.cpp +++ b/src/common/util/src/os/lin/wstring_convert_util.cpp @@ -7,10 +7,6 @@ #include #include -#ifdef _WIN32 -# include -#endif - namespace ov::util { #ifdef OPENVINO_ENABLE_UNICODE_PATH_SUPPORT @@ -20,12 +16,6 @@ constexpr auto codepoint_3rd_shift = 12U; constexpr auto codepoint_4th_shift = 18U; std::string wstring_to_string(const std::wstring_view wstr) { -# ifdef _WIN32 - const auto wstr_size = static_cast(wstr.size()); - const auto size_needed = WideCharToMultiByte(CP_UTF8, 0, wstr.data(), wstr_size, NULL, 0, NULL, NULL); - std::string result(size_needed, 0); - WideCharToMultiByte(CP_UTF8, 0, wstr.data(), wstr_size, result.data(), size_needed, NULL, NULL); -# else std::string result; result.reserve(wstr.size() * (sizeof(wchar_t) >= 4 ? 4 : 3)); // Worst case for UTF-8 @@ -55,19 +45,11 @@ std::string wstring_to_string(const std::wstring_view wstr) { } } result.shrink_to_fit(); -# endif return result; } std::wstring string_to_wstring(const std::string_view string) { const char* str = string.data(); -# ifdef _WIN32 - const auto str_size = static_cast(string.size()); - const auto size_needed = MultiByteToWideChar(CP_UTF8, 0, str, str_size, NULL, 0); - std::wstring result(size_needed, 0); - MultiByteToWideChar(CP_UTF8, 0, str, str_size, result.data(), size_needed); -# else - const auto check_utf8_seq_size = [](const char* first, const char* last, const std::ptrdiff_t seq_size) { if (seq_size > std::distance(first, last)) { throw std::runtime_error("Invalid UTF-8 sequence"); @@ -104,8 +86,6 @@ std::wstring string_to_wstring(const std::string_view string) { result.push_back(static_cast(codepoint)); } - -# endif return result; } #endif // OPENVINO_ENABLE_UNICODE_PATH_SUPPORT diff --git a/src/common/util/src/os/win/parallel_io.cpp b/src/common/util/src/os/win/parallel_io.cpp new file mode 100644 index 000000000000..fd5bf2f6b201 --- /dev/null +++ b/src/common/util/src/os/win/parallel_io.cpp @@ -0,0 +1,92 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "openvino/util/parallel_io.hpp" + +// clang-format off +#ifndef NOMINMAX +# define NOMINMAX +#endif +#include +// clang-format on + +#include +#include + +#include "openvino/util/file_util.hpp" + +namespace ov::util { + +void get_file_handle_and_size(const std::filesystem::path& path, + std::streamoff file_offset, + FileHandle& out_handle, + std::streamoff& out_size) { + out_handle = CreateFileW(path.c_str(), + GENERIC_READ, + FILE_SHARE_READ | FILE_SHARE_WRITE, + nullptr, + OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL, + nullptr); + if (out_handle == INVALID_HANDLE_VALUE) { + throw std::runtime_error("Cannot open file: " + ov::util::path_to_string(path)); + } + LARGE_INTEGER file_size = {}; + if (!GetFileSizeEx(out_handle, &file_size)) { + CloseHandle(out_handle); + out_handle = INVALID_HANDLE_VALUE; + throw std::runtime_error("Cannot get file size: " + ov::util::path_to_string(path)); + } + out_size = static_cast(file_size.QuadPart); + if (file_offset < 0 || file_offset > out_size) { + CloseHandle(out_handle); + out_handle = INVALID_HANDLE_VALUE; + throw std::out_of_range("header_offset is out of range for file: " + ov::util::path_to_string(path)); + } +} + +void close_file_handle(FileHandle handle) { + if (handle != INVALID_HANDLE_VALUE) { + CloseHandle(handle); + } +} + +FileHandle open_file_for_read(const std::filesystem::path& path) { + return CreateFileW(path.native().c_str(), + GENERIC_READ, + FILE_SHARE_READ | FILE_SHARE_WRITE, + nullptr, + OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL, + nullptr); +} + +bool positional_read(FileHandle handle, char* dst, size_t size, size_t file_offset) { + char* cur = dst; + size_t remaining = size; + size_t cur_offset = file_offset; + while (remaining > 0) { + const DWORD to_read = static_cast((std::min)(remaining, static_cast(UINT_MAX - 1024u))); + // Use OVERLAPPED to specify the file offset directly, making the read + // positional and independent of the file pointer. This is the Windows + // equivalent of Linux pread() and is safe for concurrent use on + // separate handles (no TOCTOU between SetFilePointerEx + ReadFile). + OVERLAPPED ov = {}; + ov.Offset = static_cast(cur_offset & 0xFFFFFFFFULL); + ov.OffsetHigh = static_cast((cur_offset >> 32) & 0xFFFFFFFFULL); + DWORD bytes_read = 0; + if (!ReadFile(handle, cur, to_read, &bytes_read, &ov)) { + return false; + } + if (bytes_read == 0) { + return false; + } + cur += bytes_read; + cur_offset += bytes_read; + remaining -= bytes_read; + } + return true; +} + +} // namespace ov::util \ No newline at end of file diff --git a/src/common/util/src/os/win/win_memory.cpp b/src/common/util/src/os/win/win_memory.cpp new file mode 100644 index 000000000000..8d03e1d78737 --- /dev/null +++ b/src/common/util/src/os/win/win_memory.cpp @@ -0,0 +1,60 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#ifndef NOMINMAX +# define NOMINMAX +#endif + +#include +#include + +#include +#include +#include +#include + +#include "openvino/util/memory.hpp" + +namespace ov::util { + +void* aligned_alloc(size_t size, size_t alignment) noexcept { + if (alignment == 0) { + alignment = alignof(std::max_align_t); + } + return _aligned_malloc(size, alignment); +} + +void aligned_free(void* ptr) noexcept { + _aligned_free(ptr); +} + +void* vm_reserve(size_t size, std::error_code& ec) noexcept { + const auto p = VirtualAlloc(NULL, size, MEM_RESERVE, PAGE_NOACCESS); + if (p == NULL) { + ec = std::error_code(GetLastError(), std::system_category()); + return nullptr; + } + ec = {}; + return p; +} + +void vm_commit(void* ptr, size_t size, std::error_code& ec) noexcept { + if (VirtualAlloc(ptr, size, MEM_COMMIT, PAGE_READWRITE) == NULL) { + ec = std::error_code(GetLastError(), std::system_category()); + } else { + ec = {}; + } +} + +void vm_decommit(void* ptr, size_t size) noexcept { + assert(ptr != nullptr && size > 0); + std::ignore = VirtualFree(ptr, size, MEM_DECOMMIT); +} + +void vm_release(void* ptr, size_t) noexcept { + assert(ptr != nullptr); + std::ignore = VirtualFree(ptr, 0, MEM_RELEASE); +} + +} // namespace ov::util diff --git a/src/common/util/src/os/win/win_mmap_object.cpp b/src/common/util/src/os/win/win_mmap_object.cpp index 4fc05d059724..beb9a0030aa7 100644 --- a/src/common/util/src/os/win/win_mmap_object.cpp +++ b/src/common/util/src/os/win/win_mmap_object.cpp @@ -2,35 +2,245 @@ // SPDX-License-Identifier: Apache-2.0 // +#include +#include +#include +#include +#include #include +#include #include "openvino/util/common_util.hpp" #include "openvino/util/file_util.hpp" +#include "openvino/util/memory.hpp" #include "openvino/util/mmap_object.hpp" -// clang-format-off +// clang-format off #ifndef NOMINMAX # define NOMINMAX #endif + #include -// clang-format-on +// clang-format on + +// Placeholder memory API flags (Windows 10 RS4 / 1803+). +#ifndef MEM_PRESERVE_PLACEHOLDER +# define MEM_PRESERVE_PLACEHOLDER 0x00000002 +#endif +#ifndef MEM_REPLACE_PLACEHOLDER +# define MEM_REPLACE_PLACEHOLDER 0x00004000 +#endif +#ifndef MEM_RESERVE_PLACEHOLDER +# define MEM_RESERVE_PLACEHOLDER 0x00040000 +#endif namespace ov { namespace util { + int64_t get_system_page_size() { static auto page_size = []() { - SYSTEM_INFO sysInfo; - GetSystemInfo(&sysInfo); - return static_cast(sysInfo.dwPageSize); + SYSTEM_INFO sys_info; + GetSystemInfo(&sys_info); + return static_cast(sys_info.dwPageSize); }(); return page_size; } + +size_t get_system_alloc_granularity() { + static auto alloc_gran = []() { + SYSTEM_INFO sys_info; + GetSystemInfo(&sys_info); + return static_cast(sys_info.dwAllocationGranularity); + }(); + return alloc_gran; +} + } // namespace util +class MapHolder; + +// Function pointers for Windows 10 1803+ placeholder memory API. +// Using void* for MEM_EXTENDED_PARAMETER* since we always pass nullptr/0. +using PFNVirtualAlloc2 = PVOID(WINAPI*)(HANDLE, PVOID, SIZE_T, ULONG, ULONG, void*, ULONG); +using PFNMapViewOfFile3 = PVOID(WINAPI*)(HANDLE, HANDLE, PVOID, ULONG64, SIZE_T, ULONG, ULONG, void*, ULONG); +using PFNUnmapViewOfFile2 = BOOL(WINAPI*)(HANDLE, PVOID, ULONG); + +/** @brief Helper class to load placeholder APIs dynamically and check availability at runtime. */ +struct PlaceholderAPI { + PFNVirtualAlloc2 m_virtual_alloc2{}; + PFNMapViewOfFile3 m_map_view_of_file3{}; + PFNUnmapViewOfFile2 m_unmap_view_of_file2{}; + bool m_available{}; + + static const PlaceholderAPI& instance() { + static const PlaceholderAPI s = load(); + return s; + } + +private: + static PlaceholderAPI load() { + PlaceholderAPI a; + if (const HMODULE h = ::GetModuleHandleW(L"kernelbase.dll")) { + a.m_virtual_alloc2 = reinterpret_cast(::GetProcAddress(h, "VirtualAlloc2")); + a.m_map_view_of_file3 = reinterpret_cast(::GetProcAddress(h, "MapViewOfFile3")); + a.m_unmap_view_of_file2 = reinterpret_cast(::GetProcAddress(h, "UnmapViewOfFile2")); + a.m_available = a.m_virtual_alloc2 && a.m_map_view_of_file3 && a.m_unmap_view_of_file2; + } + return a; + } +}; + +// Replaces the placeholder at exactly `base` with a file section view starting at `offset`. +// MapViewOfFile3 forwards the ULONG64 Offset directly to NtMapViewOfSectionEx which accepts +// the full 64-bit range, so files larger than 2 GiB are handled correctly. +static PVOID replace_placeholder(const PlaceholderAPI& api, + HANDLE section, + HANDLE proc, + char* base, + ULONG64 offset, + size_t size) { + return api + .m_map_view_of_file3(section, proc, base, offset, size, MEM_REPLACE_PLACEHOLDER, PAGE_READONLY, nullptr, 0); +} + +// Returns the address just past the end of the described memory region. +static char* region_end(const MEMORY_BASIC_INFORMATION& mbi) { + return static_cast(mbi.BaseAddress) + mbi.RegionSize; +} + +// Queries the region containing addr. Returns false if VirtualQuery fails. +static bool query_region(void* addr, MEMORY_BASIC_INFORMATION& mbi) { + return ::VirtualQuery(addr, &mbi, sizeof(mbi)) != 0; +} + +/** + * @brief VEH-based registry of MapHolders for on-demand remapping of evicted slots. + * + * When hint_evict() evicts a slot by unmapping it as a PAGE_NOACCESS placeholder, the VEH below will catch + * the resulting access violation and ask this registry to find the owning MapHolder and re-map the slot on demand. + */ +struct MmapVehRegistry { + struct Entry { + MapHolder* m_holder; ///< Pointer to the MapHolder owning this VA range + char* m_base; ///< Base address of the placeholder VA reservation + size_t m_total_va_size; ///< Total size of the VA reservation in bytes + }; + + /** + * @brief Mutex protecting m_ranges and m_veh_handle. Shared lock for find() (concurrent faults), + * exclusive lock for add()/remove() (registration changes). + */ + mutable std::shared_mutex m_mtx{}; + std::map m_ranges{}; // Maps VA reservation base address to MapHolder and size + PVOID m_veh_handle{}; + + /** + * @brief Returns the process-wide singleton registry instance. + * + * Singleton is required because: + * - Vectored Exception Handler (VEH) is process-wide and must be registered once + * - Provides centralized VA-to-MapHolder routing for access violation faults + * - Enables thread-safe concurrent fault handling via shared_mutex (multiple threads can simultaneously fault + * and remap different regions without blocking each other) + * + * @return Reference to the global MmapVehRegistry instance. + */ + static MmapVehRegistry& instance() { + static MmapVehRegistry reg; + return reg; + } + + /** + * @brief Registers a MapHolder's VA reservation range in the global registry. + * + * Lazily registers the process-wide VEH on first call (when m_veh_handle is nullptr). + * Subsequent calls only update m_ranges without re-registering the VEH. + * + * @param h Pointer to the MapHolder to register (must remain valid until remove() is called) + * @param base Base address of the MapHolder's VA reservation (used as map key) + * @param total_va_size Total size of the VA reservation in bytes + * @return true if registered successfully (VEH is active); false if AddVectoredExceptionHandler failed. + */ + bool add(MapHolder* h, char* base, size_t total_va_size) { + std::unique_lock lock(m_mtx); + if (!m_veh_handle) { + // Register as first VEH so it runs before debugger/CRT handlers. + m_veh_handle = ::AddVectoredExceptionHandler(1, MmapVehRegistry::veh); + if (!m_veh_handle) { + return false; + } + } + m_ranges[reinterpret_cast(base)] = Entry{h, base, total_va_size}; + return true; + } + + /** + * @brief Unregisters a MapHolder's VA reservation range from the global registry. + * + * If this was the last entry (m_ranges becomes empty), also unregisters the process-wide + * VEH by calling RemoveVectoredExceptionHandler() and sets m_veh_handle to nullptr. + * + * @param base Base address of the VA reservation to remove (must match the key used in add()) + */ + void remove(char* base) { + std::unique_lock lock(m_mtx); + m_ranges.erase(reinterpret_cast(base)); + if (m_ranges.empty() && m_veh_handle) { + ::RemoveVectoredExceptionHandler(m_veh_handle); + m_veh_handle = nullptr; + } + } + + /** + * @brief Returns true if the process-wide VEH is currently registered. + * + * Used to guard hint_evict(): eviction must not proceed if the VEH is not active, + * because placeholders created by evict_chunk() would trigger unhandled access violations. + */ + bool has_veh() const { + std::shared_lock lock(m_mtx); + return m_veh_handle != nullptr; + } + + /** + * @brief Finds the registry entry containing the given fault address. + * + * Uses upper_bound() to efficiently locate the entry whose VA range contains fault_addr. + * Algorithm: Find the first entry with key > fault_addr, step back one entry, then verify + * that fault_addr < (entry.m_base + entry.m_total_va_size). + * + * @param fault_addr The faulting virtual address (from EXCEPTION_ACCESS_VIOLATION) + * @return Pointer to Entry if fault_addr falls within a registered VA range; nullptr otherwise. + */ + const Entry* find(uintptr_t fault_addr) const { + if (auto it = m_ranges.upper_bound(fault_addr); it == m_ranges.begin()) { + return nullptr; + } else { + --it; + const auto& e = it->second; + const auto end = reinterpret_cast(e.m_base) + e.m_total_va_size; + return (fault_addr < end) ? &e : nullptr; + } + } + +private: + /** + * @brief Vectored Exception Handler callback for EXCEPTION_ACCESS_VIOLATION. + * + * Registered as first-chance VEH (priority 1) so it runs before debugger/CRT handlers. + * On access violation, extracts the faulting address, finds the owning MapHolder via find(), and calls + * try_remap_slot() to restore the evicted page. + * + * @param ep Exception pointers provided by Windows SEH dispatcher + * @return EXCEPTION_CONTINUE_EXECUTION if handled, EXCEPTION_CONTINUE_SEARCH if not our fault + */ + static LONG NTAPI veh(PEXCEPTION_POINTERS ep); +}; + class HandleHolder { HANDLE m_handle = INVALID_HANDLE_VALUE; void reset() { - if (m_handle != INVALID_HANDLE_VALUE) { + if (valid()) { ::CloseHandle(m_handle); m_handle = INVALID_HANDLE_VALUE; } @@ -44,12 +254,11 @@ class HandleHolder { } HandleHolder& operator=(const HandleHolder&) = delete; HandleHolder& operator=(HandleHolder&& other) noexcept { - if (this == &other) { - return *this; + if (this != &other) { + reset(); + m_handle = other.m_handle; + other.m_handle = INVALID_HANDLE_VALUE; } - reset(); - m_handle = other.m_handle; - other.m_handle = INVALID_HANDLE_VALUE; return *this; } @@ -57,35 +266,29 @@ class HandleHolder { reset(); } - HANDLE get() const noexcept { + constexpr HANDLE get() const noexcept { return m_handle; } + + constexpr bool valid() const { + return valid(m_handle); + } + + static constexpr bool valid(HANDLE h) { + return h != INVALID_HANDLE_VALUE && h != nullptr; + } }; class MapHolder : public ov::MappedMemory { public: MapHolder() = default; + ~MapHolder() override; - ~MapHolder() { - if (m_mapped_view) { - ::UnmapViewOfFile(m_mapped_view); - } - } - - void set(const std::filesystem::path& path, size_t offset, size_t size) { - // Note that file can't be changed (renamed/deleted) until it's unmapped. FILE_SHARE_DELETE flag allow - // rename/deletion, but it doesn't work with FAT32 filesystem (works on NTFS) - const auto h = - ::CreateFileW(path.c_str(), GENERIC_READ, FILE_SHARE_READ, 0, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0); - map(path, h, offset, size); - m_id = util::get_id_for_file(path, offset, size); - } - - void set_from_handle(HANDLE h, size_t offset, size_t size) { - map("", h, offset, size); - set_id(h, offset, size); - } + void set(const std::filesystem::path& path, size_t offset, size_t size, bool no_placeholder = false); + void set_from_handle(FileHandle handle, size_t offset, size_t size); + bool try_remap_slot(uintptr_t fault_addr); + // ov::MappedMemory interface char* data() noexcept override { return static_cast(m_data); } @@ -97,79 +300,574 @@ class MapHolder : public ov::MappedMemory { return m_id; } + void hint_evict(size_t offset, size_t size) noexcept override; + + void hint_prefetch(size_t /*offset*/, size_t /*size*/) override {} + private: - void set_id(const HANDLE h, const size_t offset, const size_t size) { - if (FILE_ID_INFO info; GetFileInformationByHandleEx(h, FileIdInfo, &info, sizeof(info))) { - static_assert(sizeof(info.FileId) == 16); - uint64_t fid_l, fid_r; - std::memcpy(&fid_l, &info.FileId, sizeof(fid_l)); - std::memcpy(&fid_r, reinterpret_cast(&info.FileId) + sizeof(fid_l), sizeof(fid_r)); - m_id = util::u64_hash_combine(offset, {size, info.VolumeSerialNumber, fid_l, fid_r}); - } else { - throw std::runtime_error{"Cannot obtain file id info for handle " + - std::to_string(reinterpret_cast(h))}; + /** + * @brief Remaps a placeholder region by replacing it with a file-backed view. + * + * @param proc Process handle (typically GetCurrentProcess()). + * @param base Base address of the placeholder region to remap (must be within m_view_base range). + * @param size Size of the placeholder region in bytes (must match the placeholder's region size). + * @return true if succeeded and returned the expected address; false otherwise + */ + bool remap_placeholder(HANDLE proc, char* base, size_t size); + + void set_id(HANDLE h, size_t offset, size_t size); + + /** @brief Core setup shared by set() and set_from_handle(). */ + void setup(HANDLE file_handle, size_t offset, size_t size, bool no_placeholder); + + /** @brief Try to establish the placeholder mapping. + * Returns true on success; caller falls back to legacy path on false. + */ + bool try_placeholder_setup(size_t aligned_offset, size_t head_pad, size_t total_va_size, size_t file_size); + + /** @brief Legacy single-call MapViewOfFile path (no partial-release support). */ + void legacy_setup(size_t aligned_offset, size_t head_pad, size_t size); + + /** + * @brief Computes the clamped, gran-aligned VA range to evict. + * Validates all preconditions (placeholder path, API availability, offset bounds). + * @return {range_begin, range_end} into the placeholder VA, or {nullptr, nullptr} if nothing to evict. + */ + std::pair compute_evict_range(size_t offset, size_t size) const noexcept; + + /** + * @brief Evicts [evict_begin, evict_end) bytes within one already-queried mapped chunk [chunk_base, chunk_end). + * Unmaps the whole chunk as a placeholder, splits off the kept before/after pieces, remaps them, + * then splits the eviction range into individual 64 KiB placeholders so each fault remaps exactly one granule. + */ + void evict_chunk(HANDLE proc, char* chunk_base, char* chunk_end, char* evict_begin, char* evict_end) noexcept; + + /** + * @brief Creates a pagefile-backed anonymous section of tail_size bytes, copies tail_data_size file bytes + * (from file_tail_offset) into it, and maps it read-only into tail_placeholder. + * @return HandleHolder for the anonymous section (valid = success). + * On failure, tail_placeholder is left as a placeholder for the caller to free. + */ + HandleHolder fill_anon_tail(const PlaceholderAPI& api, + HANDLE proc, + char* tail_placeholder, + size_t tail_size, + size_t file_tail_offset, + size_t tail_data_size); + + void* m_data{}; //!< pointer exposed to callers + size_t m_size{}; //!< user-visible byte count + uint64_t m_id{std::numeric_limits::max()}; + + HandleHolder m_handle{}; //!< section object from CreateFileMappingW + HandleHolder m_file_handle{}; //!< file HANDLE kept open to block DeleteFile (set-by-path only) + HandleHolder m_anon_handle{}; //!< pagefile-backed section for the partial last 64KB tail (if needed) + size_t m_aligned_offset{}; //!< gran-aligned file offset of placeholder base + char* m_view_base{}; //!< base VA of the placeholder reservation (placeholder path) or MapViewOfFile base (legacy + //!< path); nullptr when unmapped + size_t m_total_va_size{}; //!< total VA reservation size in bytes + size_t m_file_mapped_size{}; //!< bytes of VA backed by the file section (≤ m_total_va_size; tail is anonymous) + + /** + * @brief Guards VirtualQuery/Unmap/Map sequences in hint_evict, try_remap_slot, and the destructor. + * Plain (non-recursive): all three callers exclusively use kernel-mode Win32 calls while holding + * the lock, so the VEH cannot fire on the same thread and re-enter try_remap_slot. + */ + std::mutex m_slot_mutex; +}; + +LONG NTAPI MmapVehRegistry::veh(PEXCEPTION_POINTERS ep) { + // Only handle read access violations (ExceptionInformation[0] == 0). + // Short-circuit: ExceptionInformation[0] is only valid for EXCEPTION_ACCESS_VIOLATION + // (NumberParameters may be 0 for other codes). Write/execute faults (values 1/8) must not + // be remapped: doing so leaves the fault condition unchanged and causes an infinite fault loop. + if (ep->ExceptionRecord->ExceptionCode != EXCEPTION_ACCESS_VIOLATION || + ep->ExceptionRecord->ExceptionInformation[0] != 0) { + return EXCEPTION_CONTINUE_SEARCH; + } + const auto fault_addr = + ep->ExceptionRecord->ExceptionInformation[1]; // ULONG_PTR == uintptr_t on all Windows targets + auto& reg = instance(); + std::shared_lock lock(reg.m_mtx); + const Entry* e = reg.find(fault_addr); + return (e && e->m_holder->try_remap_slot(fault_addr)) ? EXCEPTION_CONTINUE_EXECUTION : EXCEPTION_CONTINUE_SEARCH; +} + +MapHolder::~MapHolder() { + if (m_view_base && m_total_va_size != 0) { + // Placeholder path: unregister VEH, unmap all views, free all VA allocations. + const auto& api = PlaceholderAPI::instance(); + const auto proc = GetCurrentProcess(); + + // Unregister from VEH registry BEFORE unmapping so the VEH cannot + // fire for this holder after we start tearing down its state. + MmapVehRegistry::instance().remove(m_view_base); + + // Single-pass enumeration: unmap mapped regions and collect allocation bases. + // This reduces VirtualQuery calls from 2N to N. + std::vector allocations_to_free; + + if (std::lock_guard lock(m_slot_mutex); api.m_available) { + char* current = m_view_base; + const char* end = m_view_base + m_total_va_size; + + while (current < end) { + MEMORY_BASIC_INFORMATION mbi{}; + if (!query_region(current, mbi)) + break; + if (mbi.Type == MEM_MAPPED) { + // Pass AllocationBase: UnmapViewOfFile2 requires the view's base address + // as originally mapped; BaseAddress may be a sub-region mid-view. + api.m_unmap_view_of_file2(proc, mbi.AllocationBase, MEM_PRESERVE_PLACEHOLDER); + } + void* alloc_base = mbi.AllocationBase; + if (allocations_to_free.empty() || allocations_to_free.back() != alloc_base) + allocations_to_free.push_back(alloc_base); + current = region_end(mbi); + } } + + // Now free each unique allocation. + for (void* alloc_base : allocations_to_free) { + ::VirtualFree(alloc_base, 0, MEM_RELEASE); + } + } else if (m_view_base) { + // Legacy path (MapViewOfFile without placeholders). + ::UnmapViewOfFile(m_view_base); } +} - void map(const std::filesystem::path& path, const HANDLE h, const size_t offset, const size_t size) { - if (h == INVALID_HANDLE_VALUE) { - throw std::runtime_error("Can not open file " + util::path_to_string(path) + - " for mapping. Ensure that file exists and has appropriate permissions"); +void MapHolder::set_id(HANDLE h, size_t offset, size_t size) { + if (FILE_ID_INFO info; GetFileInformationByHandleEx(h, FileIdInfo, &info, sizeof(info))) { + static_assert(sizeof(info.FileId) == sizeof(uint64_t[2])); + uint64_t fid[2]; + std::memcpy(fid, &info.FileId, sizeof(fid)); + m_id = util::u64_hash_combine(offset, {size, info.VolumeSerialNumber, fid[0], fid[1]}); + } else if (BY_HANDLE_FILE_INFORMATION info; ::GetFileInformationByHandle(h, &info)) { + // GetFileInformationByHandleEx/FileIdInfo is unavailable (network FS, ReFS, older FS). + // Fall back to the legacy NTFS/FAT file index + volume serial, which are stable across separate opens of the + // same file (unlike a raw HANDLE value). + const uint64_t file_index = (static_cast(info.nFileIndexHigh) << 32) | info.nFileIndexLow; + m_id = util::u64_hash_combine(offset, {size, info.dwVolumeSerialNumber, file_index}); + } else { + // Last-resort fallback when no stable file identity metadata is available (e.g. exotic virtual filesystems). + // HANDLE values are process-local and change across opens, so weight sharing may not work in this case. + m_id = util::u64_hash_combine(offset, {size, std::hash{}(h)}); + } +} + +HandleHolder MapHolder::fill_anon_tail(const PlaceholderAPI& api, + HANDLE proc, + char* tail_placeholder, + size_t tail_size, + size_t file_tail_offset, + size_t tail_data_size) { + const auto hi = static_cast(tail_size >> 32); + const auto lo = static_cast(tail_size & 0xFFFFFFFF); + HandleHolder anon{::CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_READWRITE, hi, lo, nullptr)}; + if (!anon.valid()) + return HandleHolder{}; + + if (tail_data_size > 0) { + const auto off = static_cast(file_tail_offset); + const auto src = ::MapViewOfFile(m_handle.get(), + FILE_MAP_READ, + static_cast(off >> 32), + static_cast(off & 0xFFFFFFFF), + tail_data_size); + if (!src) { + return HandleHolder{}; + } + auto dst = ::MapViewOfFile(anon.get(), FILE_MAP_WRITE, 0, 0, tail_data_size); + if (!dst) { + ::UnmapViewOfFile(src); + return HandleHolder{}; } - m_handle = HandleHolder(h); + std::memcpy(dst, src, tail_data_size); + ::UnmapViewOfFile(dst); + ::UnmapViewOfFile(src); + } + + auto tv = api.m_map_view_of_file3(anon.get(), + proc, + tail_placeholder, + 0, + tail_size, + MEM_REPLACE_PLACEHOLDER, + PAGE_READONLY, + nullptr, + 0); + return (tv != tail_placeholder) ? HandleHolder{} : std::move(anon); +} + +bool MapHolder::try_placeholder_setup(size_t aligned_offset, size_t head_pad, size_t total_va_size, size_t file_size) { + const auto& api = PlaceholderAPI::instance(); + if (!api.m_available) { + return false; + } + + const auto gran = util::get_system_alloc_granularity(); + + // NtMapViewOfSectionEx rejects a view that extends past raw file_size even by one byte + // (STATUS_INVALID_VIEW_SIZE, 0xC000001F) — use raw file_size, not the page-rounded section size. + // When the file is not a multiple of 64 KB, fill the sub-granule tail with an anonymous section. + const size_t available = (file_size > aligned_offset) ? file_size - aligned_offset : 0; + const size_t actual_map_size = + (available >= total_va_size) ? total_va_size : util::align_size_down(available, gran); + if (actual_map_size == 0) { + return false; + } + + const size_t tail_size = total_va_size - actual_map_size; + const auto proc = GetCurrentProcess(); + + // VirtualFree(MEM_RELEASE | MEM_PRESERVE_PLACEHOLDER) requires dwSize < total reservation. + // Over-allocate by one granule: when tail_size == 0 the dummy granule is freed after the + // split; when tail_size > 0 it IS the tail placeholder. + const size_t alloc_size = actual_map_size + (tail_size > 0 ? tail_size : gran); + auto base = static_cast(api.m_virtual_alloc2(proc, + nullptr, + alloc_size, + MEM_RESERVE | MEM_RESERVE_PLACEHOLDER, + PAGE_NOACCESS, + nullptr, + 0)); + if (!base) { + return false; + } - LARGE_INTEGER file_size_large; - if (::GetFileSizeEx(m_handle.get(), &file_size_large) == 0) { - throw std::runtime_error("Can not get file size for " + util::path_to_string(path) + ". Error " + - std::to_string(::GetLastError())); + // Split [base, base+actual_map_size) from the residual placeholder so we can map just the file portion. + // The residual [base+actual_map_size, base+alloc_size) stays non-empty because alloc_size > actual_map_size. + if (!::VirtualFree(base, actual_map_size, MEM_RELEASE | MEM_PRESERVE_PLACEHOLDER)) { + ::VirtualFree(base, 0, MEM_RELEASE); + return false; + } + + // Map the entire file-backed portion as a SINGLE view — one AllocationBase for the whole file content. + // This makes the mapping compatible with NPU Level Zero zero-copy import (ZE_GRAPH_FLAG_INPUT_GRAPH_PERSISTENT) + // before any granule is evicted. + auto v = + replace_placeholder(api, m_handle.get(), proc, base, static_cast(aligned_offset), actual_map_size); + if (v != base) { + ::VirtualFree(base, 0, MEM_RELEASE); + ::VirtualFree(base + actual_map_size, 0, MEM_RELEASE); + return false; + } + + if (tail_size > 0) { + // Fill [base+actual_map_size, base+alloc_size) with a pagefile-backed anonymous section + // containing the partial last file bytes followed by zero padding. + const size_t tail_data = (head_pad + m_size > actual_map_size) ? head_pad + m_size - actual_map_size : 0; + HandleHolder anon = + fill_anon_tail(api, proc, base + actual_map_size, tail_size, aligned_offset + actual_map_size, tail_data); + if (!anon.valid()) { + api.m_unmap_view_of_file2(proc, base, MEM_PRESERVE_PLACEHOLDER); + ::VirtualFree(base, 0, MEM_RELEASE); + ::VirtualFree(base + actual_map_size, 0, MEM_RELEASE); + return false; } - const auto file_size = static_cast(file_size_large.QuadPart); - m_size = (size == auto_size) ? file_size - offset : size; - if (offset + m_size > file_size || offset + m_size < offset) { - throw std::runtime_error("Requested mapping range exceeds file size for " + util::path_to_string(path)); + m_anon_handle = std::move(anon); + } else { + // No tail: the extra dummy granule was only needed to satisfy the split requirement; release it now. + ::VirtualFree(base + actual_map_size, 0, MEM_RELEASE); + } + + if (!MmapVehRegistry::instance().add(this, base, total_va_size)) { + // AddVectoredExceptionHandler failed — tear down placeholder and let caller fall back to legacy. + if (tail_size > 0) { + api.m_unmap_view_of_file2(proc, base + actual_map_size, MEM_PRESERVE_PLACEHOLDER); + ::VirtualFree(base + actual_map_size, 0, MEM_RELEASE); + m_anon_handle = HandleHolder{}; } + api.m_unmap_view_of_file2(proc, base, MEM_PRESERVE_PLACEHOLDER); + ::VirtualFree(base, 0, MEM_RELEASE); + return false; + } + m_view_base = base; + m_total_va_size = total_va_size; + m_file_mapped_size = actual_map_size; + m_data = base + head_pad; + return true; +} - if (m_size > 0) { - m_mapping = HandleHolder(::CreateFileMapping(m_handle.get(), 0, PAGE_READONLY, 0, 0, 0)); - if (m_mapping.get() == INVALID_HANDLE_VALUE) { - throw std::runtime_error("Can not create file mapping for " + util::path_to_string(path)); - } +void MapHolder::legacy_setup(size_t aligned_offset, size_t head_pad, size_t size) { + if (auto view = ::MapViewOfFile(m_handle.get(), + FILE_MAP_READ, + static_cast(aligned_offset >> 32), + static_cast(aligned_offset & 0xFFFFFFFF), + head_pad + size)) { + m_view_base = static_cast(view); + m_data = m_view_base + head_pad; + } else { + throw std::runtime_error{"MapViewOfFile failed: " + std::to_string(::GetLastError())}; + } +} - SYSTEM_INFO system_info; - ::GetSystemInfo(&system_info); - const auto aligned_offset = - (offset / system_info.dwAllocationGranularity) * system_info.dwAllocationGranularity; - const auto aligned_size = offset + m_size - aligned_offset; - m_mapped_view = ::MapViewOfFile(m_mapping.get(), - FILE_MAP_READ, - aligned_offset >> 32, - aligned_offset & 0xffffffff, - aligned_size); - if (!m_mapped_view) { - throw std::runtime_error("Can not create map view for " + util::path_to_string(path) + ". Error " + - std::to_string(::GetLastError())); - } - m_data = reinterpret_cast(m_mapped_view) + (offset - aligned_offset); +void MapHolder::setup(HANDLE file_handle, size_t offset, size_t size, bool no_placeholder) { + LARGE_INTEGER file_size_li{}; + if (!::GetFileSizeEx(file_handle, &file_size_li)) { + throw std::runtime_error{"GetFileSizeEx failed: " + std::to_string(::GetLastError())}; + } + const auto file_size = static_cast(file_size_li.QuadPart); + + m_size = (size == auto_size) ? file_size - offset : size; + if (offset + m_size > file_size || offset + m_size < offset) { + throw std::runtime_error{"Requested mapping range exceeds file size"}; + } + + const auto gran = util::get_system_alloc_granularity(); + const auto& [r_offset, r_length, head_pad] = util::align_region(static_cast(offset), m_size, gran); + m_aligned_offset = r_offset; + // Round up to 64KB - required for VirtualFree MEM_PRESERVE_PLACEHOLDER split. + const size_t total_va_size = util::align_size_up(r_length, gran); + + set_id(file_handle, offset, size); + + if (m_size == 0) { + return; + } + + // Create a read-only file-mapping object for the whole file. + m_handle = HandleHolder{::CreateFileMappingW(file_handle, nullptr, PAGE_READONLY, 0, 0, nullptr)}; + if (!m_handle.valid()) { + throw std::runtime_error{"CreateFileMappingW failed: " + std::to_string(::GetLastError())}; + } + + // When no_placeholder is set, skip the placeholder/VEH path to guarantee a single uniform AllocationBase + // (required for NPU zero-copy blob import). Otherwise prefer placeholder for RSS reduction. + if (no_placeholder || !try_placeholder_setup(m_aligned_offset, head_pad, total_va_size, file_size)) { + legacy_setup(m_aligned_offset, head_pad, m_size); + } +} + +void MapHolder::set(const std::filesystem::path& path, size_t offset, size_t size, bool no_placeholder) { + auto fh = ::CreateFileW(path.c_str(), + GENERIC_READ, + FILE_SHARE_READ | FILE_SHARE_DELETE, + nullptr, + OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL | FILE_FLAG_RANDOM_ACCESS, + nullptr); + if (fh == INVALID_HANDLE_VALUE) { + throw std::runtime_error{"Cannot open file: " + ov::util::path_to_string(path) + + " error: " + std::to_string(::GetLastError())}; + } + + HandleHolder fh_holder{fh}; + setup(fh, offset, size, no_placeholder); + // Keep the file handle alive so the section object can always resolve page faults + // back to the original file data, even if the caller deletes or renames the file. + // FILE_SHARE_DELETE allows std::filesystem::remove() to succeed while the mapping is alive. + m_file_handle = std::move(fh_holder); +} + +void MapHolder::set_from_handle(FileHandle handle, size_t offset, size_t size) { + if (!HandleHolder::valid(static_cast(handle))) { + throw std::runtime_error{"Invalid handle provided to load_mmap_object"}; + } + // Duplicate the caller's handle so MapHolder independently owns its lifetime. + // The original handle remains valid for the caller to reuse. + HANDLE dup = INVALID_HANDLE_VALUE; + if (!::DuplicateHandle(::GetCurrentProcess(), + static_cast(handle), + ::GetCurrentProcess(), + &dup, + 0, + FALSE, + DUPLICATE_SAME_ACCESS)) { + throw std::runtime_error{"DuplicateHandle failed: " + std::to_string(::GetLastError())}; + } + HandleHolder owned{dup}; + setup(owned.get(), offset, size, false); + // owned goes out of scope here: file handle closed. + // m_handle (section object) keeps the file data accessible independently. +} + +bool MapHolder::remap_placeholder(HANDLE proc, char* base, size_t size) { + const auto& api = PlaceholderAPI::instance(); + const size_t va_offset = base - m_view_base; + const auto file_off = static_cast(m_aligned_offset + va_offset); + + const auto v = replace_placeholder(api, m_handle.get(), proc, base, file_off, size); + return v == base; +} + +bool MapHolder::try_remap_slot(uintptr_t fault_addr) { + const auto& api = PlaceholderAPI::instance(); + if (!api.m_available || !m_view_base || m_total_va_size == 0) { + return false; + } + + const auto base = reinterpret_cast(m_view_base); + + if (fault_addr < base || fault_addr >= base + m_total_va_size) { + return false; + } + + std::lock_guard lock(m_slot_mutex); + + // Query the memory region containing the fault address. + MEMORY_BASIC_INFORMATION mbi{}; + if (!query_region(reinterpret_cast(fault_addr), mbi)) + return false; + + // If already mapped (MEM_MAPPED), another thread beat us to it. + if (mbi.Type == MEM_MAPPED) + return true; + + // Use AllocationBase as the true placeholder start. VirtualQuery's BaseAddress may report a sub-region within + // the placeholder if pages have different internal states (e.g. residual PTE differences after unmap), + // but AllocationBase always reflects the actual placeholder boundary used by VirtualFree(MEM_PRESERVE_PLACEHOLDER). + auto placeholder_base = static_cast(mbi.AllocationBase); + + // Determine full placeholder extent by scanning forward from AllocationBase. A single placeholder allocation + // may be reported as multiple VirtualQuery regions with different page-level attributes, + // but they share the same AllocationBase. + size_t placeholder_size = 0; + char* scan = placeholder_base; + const char* va_end = m_view_base + m_total_va_size; + while (scan < va_end) { + MEMORY_BASIC_INFORMATION scan_mbi{}; + if (!query_region(scan, scan_mbi) || scan_mbi.AllocationBase != placeholder_base || + scan_mbi.Type == MEM_MAPPED) { + break; } + placeholder_size += scan_mbi.RegionSize; + scan = region_end(scan_mbi); + } + if (placeholder_size == 0) { + return false; } -private: - void* m_mapped_view = nullptr; - void* m_data = nullptr; - size_t m_size = 0; - uint64_t m_id = std::numeric_limits::max(); - HandleHolder m_handle; - HandleHolder m_mapping; -}; + const auto proc = GetCurrentProcess(); + return remap_placeholder(proc, placeholder_base, placeholder_size); +} + +std::pair MapHolder::compute_evict_range(size_t offset, size_t size) const noexcept { + // Require placeholder path, API availability, and a live VEH (eviction without VEH + // would leave inaccessible placeholders on the next access). + if (!m_view_base || m_total_va_size == 0 || !PlaceholderAPI::instance().m_available || + !MmapVehRegistry::instance().has_veh()) + return {}; + + // Clamp offset and size to [0, m_size) to prevent size_t overflow in subsequent arithmetic. + const size_t clamped_offset = std::min(offset, m_size); + const size_t available = m_size - clamped_offset; + const auto effective_size = (size == auto_size) ? available : std::min(size, available); + if (effective_size == 0) + return {}; + + const auto gran = util::get_system_alloc_granularity(); + + // Convert user [offset, size) to a VA range relative to m_view_base. + const size_t head_pad = static_cast(static_cast(m_data) - m_view_base); + const size_t va_begin_raw = head_pad + clamped_offset; + if (va_begin_raw >= m_total_va_size) + return {}; + + const size_t va_end = std::min(head_pad + clamped_offset + effective_size, m_total_va_size); + + // Outward gran-rounding: expand to cover all granules overlapping the requested range. + // The anonymous tail (m_anon_handle) is never evictable — cap at m_file_mapped_size. + const size_t safe_begin = util::align_size_down(va_begin_raw, gran); + const size_t safe_end = std::min(util::align_size_up(va_end, gran), m_file_mapped_size); + if (safe_begin >= safe_end) + return {}; + + return {m_view_base + safe_begin, m_view_base + safe_end}; +} + +void MapHolder::evict_chunk(HANDLE proc, + char* chunk_base, + char* chunk_end, + char* evict_begin, + char* evict_end) noexcept { + const auto& api = PlaceholderAPI::instance(); + if (!api.m_unmap_view_of_file2(proc, chunk_base, MEM_PRESERVE_PLACEHOLDER)) + return; + + const size_t before_size = static_cast(evict_begin - chunk_base); + const size_t after_size = static_cast(chunk_end - evict_end); + + // Split the placeholder to isolate the evicted middle. + if (before_size > 0 && after_size > 0) { + if (::VirtualFree(chunk_base, before_size, MEM_RELEASE | MEM_PRESERVE_PLACEHOLDER)) + ::VirtualFree(evict_begin, + static_cast(evict_end - evict_begin), + MEM_RELEASE | MEM_PRESERVE_PLACEHOLDER); + } else if (before_size > 0) { + ::VirtualFree(chunk_base, before_size, MEM_RELEASE | MEM_PRESERVE_PLACEHOLDER); + } else if (after_size > 0) { + ::VirtualFree(evict_begin, + static_cast(evict_end - evict_begin), + MEM_RELEASE | MEM_PRESERVE_PLACEHOLDER); + } + + // Split the eviction range into individual 64 KiB placeholders so each VEH fault remaps exactly one granule + // instead of the whole evicted range at once. + const auto gran = util::get_system_alloc_granularity(); + for (char* g = evict_begin; g + gran < evict_end; g += gran) { + ::VirtualFree(g, gran, MEM_RELEASE | MEM_PRESERVE_PLACEHOLDER); + } + + // Remap the before/after pieces so they remain accessible. + if (before_size > 0) { + remap_placeholder(proc, chunk_base, before_size); + } + if (after_size > 0) { + remap_placeholder(proc, evict_end, after_size); + } +} + +void MapHolder::hint_evict(size_t offset, size_t size) noexcept { + const auto [range_begin, range_end] = compute_evict_range(offset, size); + if (!range_begin) + return; + + const auto proc = GetCurrentProcess(); + std::lock_guard lock(m_slot_mutex); + + char* scan = range_begin; + while (scan < range_end) { + MEMORY_BASIC_INFORMATION mbi{}; + if (!query_region(scan, mbi)) + break; + if (mbi.Type != MEM_MAPPED) { + // Already evicted or non-file region (e.g. anonymous tail) — skip. + scan = region_end(mbi); + continue; + } + + auto chunk_base = static_cast(mbi.AllocationBase); + auto chunk_end = region_end(mbi); + const char* max_end = m_view_base + m_total_va_size; + + // Scan forward to find the full extent of this mapped chunk + // (handles the case where VirtualQuery splits a large view into multiple regions). + for (auto s = chunk_end; s < max_end;) { + MEMORY_BASIC_INFORMATION mbi_next{}; + if (!query_region(s, mbi_next) || mbi_next.Type != MEM_MAPPED || mbi_next.AllocationBase != chunk_base) + break; + chunk_end = region_end(mbi_next); + s = chunk_end; + } + + evict_chunk(proc, chunk_base, chunk_end, scan, std::min(range_end, chunk_end)); + scan = chunk_end; + } +} -std::shared_ptr load_mmap_object(const std::filesystem::path& path, size_t offset, size_t size) { +std::shared_ptr load_mmap_object(const std::filesystem::path& path, + size_t offset, + size_t size, + bool no_placeholder) { auto holder = std::make_shared(); - holder->set(path, offset, size); + holder->set(path, offset, size, no_placeholder); return holder; } std::shared_ptr load_mmap_object(FileHandle handle, size_t offset, size_t size) { - if (handle == INVALID_HANDLE_VALUE || handle == nullptr) { + if (!HandleHolder::valid(static_cast(handle))) { throw std::runtime_error("Invalid handle provided to load_mmap_object"); } auto holder = std::make_shared(); diff --git a/src/common/util/src/os/win/wstring_convert_util.cpp b/src/common/util/src/os/win/wstring_convert_util.cpp new file mode 100644 index 000000000000..2bcb8b576e2c --- /dev/null +++ b/src/common/util/src/os/win/wstring_convert_util.cpp @@ -0,0 +1,29 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "openvino/util/wstring_convert_util.hpp" + +#include +#include + +namespace ov::util { +#ifdef OPENVINO_ENABLE_UNICODE_PATH_SUPPORT +std::string wstring_to_string(const std::wstring_view wstr) { + const auto wstr_size = static_cast(wstr.size()); + const auto size_needed = WideCharToMultiByte(CP_UTF8, 0, wstr.data(), wstr_size, NULL, 0, NULL, NULL); + std::string result(size_needed, 0); + WideCharToMultiByte(CP_UTF8, 0, wstr.data(), wstr_size, result.data(), size_needed, NULL, NULL); + return result; +} + +std::wstring string_to_wstring(const std::string_view string) { + const char* str = string.data(); + const auto str_size = static_cast(string.size()); + const auto size_needed = MultiByteToWideChar(CP_UTF8, 0, str, str_size, NULL, 0); + std::wstring result(size_needed, 0); + MultiByteToWideChar(CP_UTF8, 0, str, str_size, result.data(), size_needed); + return result; +} +#endif // OPENVINO_ENABLE_UNICODE_PATH_SUPPORT +} // namespace ov::util diff --git a/src/common/util/src/parallel_read_streambuf.cpp b/src/common/util/src/parallel_read_streambuf.cpp new file mode 100644 index 000000000000..3d3bfbafdad1 --- /dev/null +++ b/src/common/util/src/parallel_read_streambuf.cpp @@ -0,0 +1,336 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "openvino/util/parallel_read_streambuf.hpp" + +#ifdef _WIN32 +# ifndef NOMINMAX +# define NOMINMAX +# endif +# include +#endif + +#include +#include +#include +#include +#include +#include +#include + +#include "openvino/util/file_util.hpp" +#include "openvino/util/parallel_io.hpp" + +namespace ov::util { + +ParallelReadStreamBuf::ParallelReadStreamBuf(const std::filesystem::path& path, + std::streamoff header_offset, + size_t threshold) + : m_path(path), + m_file_offset(header_offset), + m_header_offset(header_offset), + m_threshold(threshold) { + get_file_handle_and_size(path, m_file_offset, m_handle, m_file_size); +} + +ParallelReadStreamBuf::~ParallelReadStreamBuf() { + close_file_handle(m_handle); +} + +// xsgetn: main hot path - called by sgetn() for all bulk reads +std::streamsize ParallelReadStreamBuf::xsgetn(char_type* dst, std::streamsize n) { + if (n <= 0) + return 0; + + std::streamsize total = 0; + + // Drain any chars previously buffered by underflow() + if (gptr() != nullptr && gptr() < egptr()) { + const std::streamsize avail = static_cast(egptr() - gptr()); + const std::streamsize from_buf = (std::min)(n, avail); + std::memcpy(dst, gptr(), static_cast(from_buf)); + // Safe: from_buf <= UNDERFLOW_BUF (8192), always fits in int. + static_assert(UNDERFLOW_BUF <= static_cast((std::numeric_limits::max)()), + "UNDERFLOW_BUF must fit in int for gbump()"); + gbump(static_cast(from_buf)); + total += from_buf; + dst += from_buf; + n -= from_buf; + } + + if (n <= 0 || m_file_offset >= m_file_size) { + return total; + } + + const std::streamoff remaining = m_file_size - m_file_offset; + const std::streamsize to_read = static_cast((std::min)(static_cast(n), remaining)); + + const size_t bytes = static_cast(to_read); + const size_t offset = static_cast(m_file_offset); + + // Prefetch fast path: if the whole request sits inside the prefetched window, + // serve it from memory with one memcpy instead of issuing a pread. + if (serve_from_prefetch(dst, bytes, m_file_offset)) { + m_file_offset += to_read; + total += to_read; + return total; + } + + bool ok = (bytes >= m_threshold) ? parallel_read(dst, bytes, offset) : single_read(dst, bytes, offset); + + if (ok) { + m_file_offset += to_read; + total += to_read; + } + + return total; +} + +// underflow: called for single-char peek / non-bulk reads (e.g. std::getline) +ParallelReadStreamBuf::int_type ParallelReadStreamBuf::underflow() { + if (m_file_offset >= m_file_size) { + return traits_type::eof(); + } + if (!m_underflow_buf) { + m_underflow_buf = std::make_unique(UNDERFLOW_BUF); + } + // Read a batch of up to UNDERFLOW_BUF bytes so that character-by-character + // consumers (std::getline, operator>>) don't issue one pread per char. + const size_t to_read = + static_cast((std::min)(static_cast(UNDERFLOW_BUF), m_file_size - m_file_offset)); + // Prefetch fast path: fill the underflow buffer from the prefetched window + // if possible, avoiding a pread per 8 KiB chunk of character-by-character + // consumption (operator>>, std::getline). + if (!serve_from_prefetch(m_underflow_buf.get(), to_read, m_file_offset)) { + if (!single_read(m_underflow_buf.get(), to_read, static_cast(m_file_offset))) { + return traits_type::eof(); + } + } + // Advance m_file_offset past the bytes we just read into the get area. + // m_file_offset now points to the byte after egptr(), consistent with + // the seekoff(0, cur) formula: logical_pos = m_file_offset - (egptr - gptr). + m_file_offset += static_cast(to_read); + setg(m_underflow_buf.get(), m_underflow_buf.get(), m_underflow_buf.get() + to_read); + return traits_type::to_int_type(m_underflow_buf[0]); +} + +ParallelReadStreamBuf::pos_type ParallelReadStreamBuf::seekoff(off_type off, + std::ios_base::seekdir way, + std::ios_base::openmode /* which */) { + // All internal positions (m_file_offset, m_file_size, m_header_offset) are + // absolute byte offsets from the start of the file. The public-facing + // stream positions are *logical* offsets: 0 == header_offset in the file. + std::streamoff new_pos = 0; + if (way == std::ios_base::beg) { + // off is a logical offset; translate to absolute file offset. + new_pos = m_header_offset + off; + } else if (way == std::ios_base::cur) { + // Account for the buffered chars from underflow() not yet consumed. + const std::streamsize ahead = (gptr() != nullptr) ? static_cast(egptr() - gptr()) : 0; + new_pos = m_file_offset - ahead + off; // stays absolute + // Pure tell (off == 0): return current position without any side effects + // on the get area or m_file_offset. Discarding the underflow buffer on a + // tell would force the next read to re-issue a pread for data that is + // already buffered, breaking interleaved getline()+tellg() patterns. + if (off == 0) { + if (new_pos < m_header_offset || new_pos > m_file_size) + return pos_type(off_type(-1)); + return pos_type(new_pos - m_header_offset); + } + } else { + new_pos = m_file_size + off; // stays absolute + } + + // Reject seeks before the logical stream start or past the file end. + if (new_pos < m_header_offset || new_pos > m_file_size) { + return pos_type(off_type(-1)); + } + + setg(nullptr, nullptr, nullptr); // invalidate get-area + // If the new absolute position is outside the prefetched window, drop the + // prefetch buffer so we don't serve stale or partial data from it. + if (m_prefetch_size > 0) { + const std::streamoff end = m_prefetch_begin + static_cast(m_prefetch_size); + if (new_pos < m_prefetch_begin || new_pos >= end) { + invalidate_prefetch(); + } + } + m_file_offset = new_pos; + // Return the logical position (0 == start of exposed stream). + return pos_type(m_file_offset - m_header_offset); +} + +ParallelReadStreamBuf::pos_type ParallelReadStreamBuf::seekpos(pos_type pos, std::ios_base::openmode /* which */) { + return seekoff(off_type(pos), std::ios_base::beg, std::ios_base::in); +} + +std::streamsize ParallelReadStreamBuf::showmanyc() { + // Report both buffered characters (in the get area) and remaining + // bytes in the underlying file. + // Per [streambuf.virt.get]/6: return -1 when the next call to + // underflow() would return traits_type::eof() (i.e. stream truly + // exhausted). Return 0 when availability is unknown. Here both + // the file and the get-area are fully accounted for, so -1 at + // total==0 is correct — underflow() would indeed return EOF. + std::streamsize buffered = 0; + if (gptr() != nullptr && egptr() != nullptr && egptr() > gptr()) { + buffered = static_cast(egptr() - gptr()); + } + std::streamoff remaining_off = m_file_size - m_file_offset; + if (remaining_off < 0) { + remaining_off = 0; + } + const std::streamsize remaining = remaining_off > 0 ? static_cast(remaining_off) : 0; + const std::streamsize total = buffered + remaining; + return total > 0 ? total : static_cast(-1); +} + +// Prefetch a region of @p size bytes starting at the current logical position +// into an internal buffer. Returns true if the prefetch succeeded and the +// buffer is now ready to serve subsequent small reads. +bool ParallelReadStreamBuf::prefetch(std::streamsize size) { + if (size <= 0) { + return false; + } + + // Account for bytes still sitting in the underflow get-area. The logical + // cursor that the caller cares about is the position of the next byte the + // caller will consume, which is m_file_offset - (egptr - gptr). + const std::streamoff ahead = (gptr() != nullptr) ? static_cast(egptr() - gptr()) : 0; + const std::streamoff abs_begin = m_file_offset - ahead; + + if (abs_begin < 0 || abs_begin >= m_file_size) { + return false; + } + + const std::streamoff remaining = m_file_size - abs_begin; + const size_t to_load = static_cast((std::min)(static_cast(size), remaining)); + if (to_load == 0) { + return false; + } + + // Drop any pre-existing prefetch window; we fill a fresh one below. + invalidate_prefetch(); + + // Reuse an existing allocation when possible to avoid repeated malloc/free + // across consecutive prefetch() calls in the same stream lifetime. + if (!m_prefetch_buf || m_prefetch_capacity < to_load) { + m_prefetch_buf = std::make_unique(to_load); + m_prefetch_capacity = to_load; + } + const size_t abs_begin_sz = static_cast(abs_begin); + + // Use the same parallel_read path as the normal large-read code so that + // hw_threads, chunking, and per-thread fds all match the tuned behavior. + const bool ok = (to_load >= m_threshold) ? parallel_read(m_prefetch_buf.get(), to_load, abs_begin_sz) + : single_read(m_prefetch_buf.get(), to_load, abs_begin_sz); + if (!ok) { + return false; + } + + m_prefetch_begin = abs_begin; + m_prefetch_size = to_load; + return true; +} + +// Serve a request from the prefetch buffer if the full range is covered. +// Returns true when the buffer was used; on false the caller must fall back to +// the regular file-IO path (which will also invalidate the stale window via +// seekoff semantics when a subsequent xsgetn crosses the boundary). +bool ParallelReadStreamBuf::serve_from_prefetch(char* dst, size_t size, std::streamoff abs_offset) { + if (m_prefetch_size == 0 || !m_prefetch_buf) { + return false; + } + const std::streamoff begin = m_prefetch_begin; + const std::streamoff end = begin + static_cast(m_prefetch_size); + if (abs_offset < begin || static_cast(abs_offset + static_cast(size)) > end) { + // Partial overlap falls through to file I/O; serving a partial slice here + // would force xsgetn to issue a second pread for the tail, negating the + // amortization benefit we are chasing. + return false; + } + const size_t local = static_cast(abs_offset - begin); + std::memcpy(dst, m_prefetch_buf.get() + local, size); + return true; +} + +void ParallelReadStreamBuf::invalidate_prefetch() { + m_prefetch_begin = 0; + m_prefetch_size = 0; +} + +// Single-threaded positional read +bool ParallelReadStreamBuf::single_read(char* dst, size_t size, size_t file_offset) { + return positional_read(m_handle, dst, size, file_offset); +} + +// Parallel positional read +bool ParallelReadStreamBuf::parallel_read(char* dst, size_t size, size_t file_offset) { + const size_t hw_threads = (std::max)(size_t{1}, static_cast(std::thread::hardware_concurrency())); + const size_t max_by_size = size / (1024 * 1024); // 1 thread per MB + const size_t num_threads = (std::max)(size_t{1}, (std::min)(hw_threads, max_by_size)); + + if (num_threads == 1) { + return single_read(dst, size, file_offset); + } + + // Round chunk_size UP to the next 4 KiB boundary so that every thread's + // start offset is page-aligned (better I/O coalescing on NVMe/direct I/O). + // Because rounding up means num_threads * chunk_size >= size, two extra + // guards are required: + // 1. Non-last threads: cap read to min(chunk_size, size - cur_offset) so + // they never stride past EOF when the aligned chunk extends beyond it. + // 2. Last thread: use (size - cur_offset) to capture every remaining byte + // including the fragment that lies beyond (num_threads-1) * chunk_size + // but before size. Using chunk_size here would silently drop those bytes. + size_t chunk_size = size / num_threads; + chunk_size = (chunk_size + 4095u) & ~size_t{4095u}; + + std::atomic success{true}; + // Each worker opens its own file descriptor so that Linux's per-file- + // description readahead state (file_ra_state / f_ra) is independent per + // thread. Sharing a single fd causes concurrent pread() calls to corrupt + // each other's sequential readahead prediction, collapsing throughput from + // ~3.5 GB/s sequential to ~0.5 GB/s. + std::vector workers; + workers.reserve(num_threads); + for (size_t ithr = 0; ithr < num_threads; ++ithr) { + try { + workers.emplace_back([&, ithr]() { + const size_t cur_offset = ithr * chunk_size; + if (cur_offset >= size) { + return; // page-alignment rounding created a surplus worker slot + } + // Last thread: read everything remaining (includes the fragment that + // falls between (num_threads-1)*chunk_size and size). + // Non-last threads: cap to min(chunk_size, size - cur_offset) so we + // never read past eof when alignment pushed the chunk boundary beyond it. + const size_t read_size = + (ithr == num_threads - 1) ? (size - cur_offset) : (std::min)(chunk_size, size - cur_offset); + char* const ptr = dst + cur_offset; + const size_t thread_file_offset = file_offset + cur_offset; + + FileHandle t_handle = open_file_for_read(m_path); + if (t_handle == INVALID_HANDLE_VALUE) { + success = false; + return; + } + + if (!positional_read(t_handle, ptr, read_size, thread_file_offset)) { + success = false; + } + close_file_handle(t_handle); + }); // workers.emplace_back + } catch (...) { + success = false; + break; + } + } + for (auto& t : workers) { + t.join(); + } + return success.load(); +} + +} // namespace ov::util diff --git a/src/common/util/src/weights_path.cpp b/src/common/util/src/weights_path.cpp deleted file mode 100644 index 10232b5f114c..000000000000 --- a/src/common/util/src/weights_path.cpp +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright (C) 2018-2026 Intel Corporation -// SPDX-License-Identifier: Apache-2.0 -// - -#include "openvino/util/weights_path.hpp" - -bool ov::util::validate_weights_path(const std::string& weights_path) { - if (weights_path.empty() || !ov::util::ends_with(weights_path, ".bin")) { - return false; - } - - return true; -} diff --git a/src/common/util/src/xml_parse_utils.cpp b/src/common/util/src/xml_parse_utils.cpp index b248b3a25cc8..02d8c0620b92 100644 --- a/src/common/util/src/xml_parse_utils.cpp +++ b/src/common/util/src/xml_parse_utils.cpp @@ -10,8 +10,17 @@ #include #include -namespace ov { -namespace util { +namespace ov::util { +namespace { + +inline pugi::xml_attribute get_node_attr(const pugi::xml_node& node, std::string_view name) { +#ifdef PUGIXML_HAS_STRING_VIEW + return node.attribute(name); +#else + return node.attribute(std::string(name).c_str()); +#endif +} +} // namespace int pugixml::get_int_attr(const pugi::xml_node& node, const char* str) { auto attr = node.attribute(str); @@ -232,5 +241,14 @@ int pugixml::get_int_child(const pugi::xml_node& node, const char* str, int defV return atoi(child.child_value()); } -} // namespace util -} // namespace ov +std::optional pugixml::get_attribute_view(const pugi::xml_node& node, std::string_view name) { + if (!node) { + return std::nullopt; + } else if (const auto attr = get_node_attr(node, name); !attr.empty()) { + return std::make_optional(attr.value()); + } else { + return std::nullopt; + } +} + +} // namespace ov::util diff --git a/src/common/zero_loader/CMakeLists.txt b/src/common/zero_loader/CMakeLists.txt index bc9d2cdc5c4c..8fc2927d11a6 100644 --- a/src/common/zero_loader/CMakeLists.txt +++ b/src/common/zero_loader/CMakeLists.txt @@ -10,9 +10,9 @@ file(GLOB_RECURSE PUBLIC_HEADERS ${CMAKE_CURRENT_SOURCE_DIR}/include/*.hpp) add_library(${TARGET_NAME} STATIC ${LIBRARY_SRC} ${PUBLIC_HEADERS}) add_library(openvino::zero_loader ALIAS ${TARGET_NAME}) -target_link_libraries(${TARGET_NAME} PUBLIC LevelZero::Headers openvino::runtime::dev PRIVATE openvino::util) +target_link_libraries(${TARGET_NAME} PUBLIC LevelZero::Headers PRIVATE openvino::util) target_include_directories(${TARGET_NAME} - PUBLIC $ + PUBLIC $ ) ov_developer_package_export_targets(TARGET level_zero_headers) ov_install_static_lib(level_zero_headers ${OV_CPACK_COMP_CORE}) diff --git a/src/common/zero_loader/include/openvino/zero_api.hpp b/src/common/zero_loader/include/openvino/zero_api.hpp index 8f10028875fa..6921dd51046b 100644 --- a/src/common/zero_loader/include/openvino/zero_api.hpp +++ b/src/common/zero_loader/include/openvino/zero_api.hpp @@ -9,8 +9,6 @@ #include -#include "openvino/core/except.hpp" - namespace ov { // clang-format off @@ -85,7 +83,13 @@ namespace ov { symbol_statement(zeKernelSetArgumentValue) \ symbol_statement(zeCommandListCreateImmediate) \ symbol_statement(zeKernelSetGroupSize) \ - symbol_statement(zeCommandListAppendLaunchKernel) + symbol_statement(zeCommandListAppendLaunchKernel) \ + symbol_statement(zeImageCreate) \ + symbol_statement(zeImageDestroy) \ + symbol_statement(zeCommandListAppendImageCopy) \ + symbol_statement(zeCommandListAppendImageCopyFromMemory) \ + symbol_statement(zeCommandListAppendImageCopyToMemory) \ + symbol_statement(zeDeviceGetStatus) /** * @def weak_symbols_list @@ -96,7 +100,10 @@ namespace ov { symbol_statement(zeCommandListUpdateMutableCommandsExp) \ symbol_statement(zeInitDrivers) \ symbol_statement(zelGetLoaderVersion) \ - symbol_statement(zelSetDriverTeardown) + symbol_statement(zelSetDriverTeardown) \ + symbol_statement(zeDeviceGetRuntimeRequirements) \ + symbol_statement(zeDeviceGetRuntimeRequirementsKey) \ + symbol_statement(zeDeviceValidateRuntimeRequirements) // clang-format on /** @@ -132,9 +139,9 @@ class ZeroApi { #define symbol_statement(symbol) \ template \ inline typename std::invoke_result::type wrapped_##symbol(Args... args) { \ - const auto& ptr = ZeroApi::get_instance(); \ + const auto& ptr = ZeroApi::get_instance(); \ if (ptr->symbol == nullptr) { \ - OPENVINO_THROW("Unsupported symbol " #symbol); \ + return ZE_RESULT_ERROR_UNSUPPORTED_FEATURE; \ } \ return ptr->symbol(std::forward(args)...); \ } diff --git a/src/common/zero_loader/src/zero.api.cpp b/src/common/zero_loader/src/zero.api.cpp index 99c71607ebcd..af02f7284de4 100644 --- a/src/common/zero_loader/src/zero.api.cpp +++ b/src/common/zero_loader/src/zero.api.cpp @@ -16,24 +16,16 @@ namespace ov { ZeroApi::ZeroApi() { const std::filesystem::path baseName = "ze_loader"; - try { - auto libpath = ov::util::make_plugin_library_name({}, baseName); + auto libpath = ov::util::make_plugin_library_name({}, baseName); #if !defined(_WIN32) && !defined(ANDROID) - libpath += LIB_ZE_LOADER_SUFFIX; + libpath += LIB_ZE_LOADER_SUFFIX; #endif - this->lib = ov::util::load_shared_object(libpath); - } catch (const std::runtime_error& error) { - OPENVINO_THROW(error.what()); - } + this->lib = ov::util::load_shared_object(libpath); - try { #define symbol_statement(symbol) \ this->symbol = reinterpret_cast(ov::util::get_symbol(lib, #symbol)); - symbols_list(); + symbols_list(); #undef symbol_statement - } catch (const std::runtime_error& error) { - OPENVINO_THROW(error.what()); - } #define symbol_statement(symbol) \ try { \ diff --git a/src/core/dev_api/openvino/core/log_util.hpp b/src/core/dev_api/openvino/core/log_util.hpp index a2adf277282a..49882157ac9d 100644 --- a/src/core/dev_api/openvino/core/log_util.hpp +++ b/src/core/dev_api/openvino/core/log_util.hpp @@ -23,7 +23,7 @@ using LogCallback = std::function; OPENVINO_API void log_message(std::string_view message); -#ifdef ENABLE_OPENVINO_DEBUG +#ifdef ENABLE_DEBUG_CAPS class OPENVINO_API LevelString { private: @@ -679,7 +679,7 @@ bool is_verbose_logging(); } \ } while (0); -#else // ENABLE_OPENVINO_DEBUG +#else // ENABLE_DEBUG_CAPS # define OPENVINO_LOG_NODE1(...) \ do { \ @@ -827,5 +827,5 @@ bool is_verbose_logging(); do { \ } while (0) -#endif // ENABLE_OPENVINO_DEBUG +#endif // ENABLE_DEBUG_CAPS } // namespace ov::util diff --git a/src/core/dev_api/openvino/core/shape_util.hpp b/src/core/dev_api/openvino/core/shape_util.hpp index 50619b2ac59d..8ad0c779b367 100644 --- a/src/core/dev_api/openvino/core/shape_util.hpp +++ b/src/core/dev_api/openvino/core/shape_util.hpp @@ -8,7 +8,6 @@ #include "openvino/core/shape.hpp" #include "openvino/op/util/attr_types.hpp" -#include "openvino/util/common_util.hpp" namespace ov { namespace util { diff --git a/src/core/dev_api/openvino/core/weight_sharing_util.hpp b/src/core/dev_api/openvino/core/weight_sharing_util.hpp index 5bb873c9ffa5..caf5a51a7c0f 100644 --- a/src/core/dev_api/openvino/core/weight_sharing_util.hpp +++ b/src/core/dev_api/openvino/core/weight_sharing_util.hpp @@ -123,6 +123,14 @@ struct OPENVINO_API Extension { * @return Return map of weight metadata. */ static WeightRegistry get_weight_registry(const Model& model); + + /** @brief Hint to evict the constant's buffer from physical memory. + * + * @note Will try to evict data from constant buffer if possible. + * + * @param constant Constant node to evict buffer for. + */ + static void hint_evict(ov::op::v0::Constant& constant) noexcept; }; /** @brief Get the source buffer for a given source id. diff --git a/src/core/dev_api/openvino/op/moe.hpp b/src/core/dev_api/openvino/op/moe.hpp index 9d23f0e66925..2efde56433b7 100644 --- a/src/core/dev_api/openvino/op/moe.hpp +++ b/src/core/dev_api/openvino/op/moe.hpp @@ -24,19 +24,24 @@ class OPENVINO_API MOE : public ov::op::Op { MOE(const OutputVector& args) : Op(args) {} enum class Expert_type { GEMM2_BIAS_SWIGLU_CLAMP, GEMM3_SWIGLU }; + enum class Activation_type { + SWIGLU, // Swish gate activation (default; matches existing behavior) + GEGLU_TANH, // Gelu gate with Tanh approximation + GEGLU_ERF, // Gelu gate with ERF (exact) activation + }; struct Config { Expert_type expert_type{Expert_type::GEMM2_BIAS_SWIGLU_CLAMP}; float expert_alpha{0.0f}; // Expert attribute for clamp bounds float expert_beta{1.0f}; // Expert attribute for swish beta + size_t gate_idx{0}; // gate (swish) lane in interleaved gate/up; GEMM2 only + Activation_type activation_type{Activation_type::SWIGLU}; // Gate activation; GEMM3 only }; /// \brief Constructs a MOE operation with config only /// \param args The input tensors, in the following order: /// 0: hidden_states - input tensor with hidden representations - /// 1: routing_weights - normalized weights for selected experts. - /// Legacy form: [num_experts, ...] (scattered via ScatterElementsUpdate). - /// Compact form: [topk, batch*seq, 1] (post-BGM, selected experts only). + /// 1: routing_weights - [..., topk] normalized weights for selected experts. /// 2: router_topk_output_indices - [..., topk] indices of selected top-k experts /// 3: w0_weight - expert weights for first projection, shape [num_experts, inter_size, hidden_size] or /// [num_experts, hidden_size, 2 * inter_size] if fused @@ -67,6 +72,7 @@ class OPENVINO_API MOE : public ov::op::Op { namespace ov { std::ostream& operator<<(std::ostream& s, const ov::op::internal::MOE::Expert_type& type); +std::ostream& operator<<(std::ostream& s, const ov::op::internal::MOE::Activation_type& type); template <> class AttributeAdapter @@ -78,4 +84,15 @@ class AttributeAdapter OPENVINO_RTTI("AttributeAdapter"); ~AttributeAdapter() override = default; }; + +template <> +class AttributeAdapter + : public EnumAttributeAdapterBase { +public: + AttributeAdapter(ov::op::internal::MOE::Activation_type& value) + : EnumAttributeAdapterBase(value) {} + + OPENVINO_RTTI("AttributeAdapter"); + ~AttributeAdapter() override = default; +}; } // namespace ov diff --git a/src/core/dev_api/openvino/op/pa_kv_reorder.hpp b/src/core/dev_api/openvino/op/pa_kv_reorder.hpp new file mode 100644 index 000000000000..01a772ecfe80 --- /dev/null +++ b/src/core/dev_api/openvino/op/pa_kv_reorder.hpp @@ -0,0 +1,35 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#pragma once + +#include "openvino/op/op.hpp" + +namespace ov { +namespace op { +namespace internal { + +class OPENVINO_API PaKVReorder : public ov::op::Op { +public: + OPENVINO_OP("PaKVReorder"); + + PaKVReorder() = default; + + PaKVReorder(const Output& key_cache, + const Output& value_cache, + const Output& block_indices, + const Output& block_indices_begins, + const Output& block_update_indices, + const Output& block_update_indices_begins); + + bool visit_attributes(ov::AttributeVisitor& visitor) override; + + void validate_and_infer_types() override; + + std::shared_ptr clone_with_new_inputs(const ov::OutputVector& new_args) const override; +}; + +} // namespace internal +} // namespace op +} // namespace ov diff --git a/src/core/dev_api/openvino/op/paged_attention.hpp b/src/core/dev_api/openvino/op/paged_attention.hpp index d066840581fe..e559fa525152 100644 --- a/src/core/dev_api/openvino/op/paged_attention.hpp +++ b/src/core/dev_api/openvino/op/paged_attention.hpp @@ -1,6 +1,7 @@ // Copyright (C) 2018-2026 Intel Corporation // SPDX-License-Identifier: Apache-2.0 // + #pragma once #include "openvino/op/op.hpp" @@ -10,20 +11,66 @@ namespace op { // This is an experimental operation that is implemented in the plugins. // Do not use in user applications, backward compatibility is not guaranteed in future releases. + +/// \brief Paged attention op for memory-efficient sequence processing. +/// +/// \ingroup ov_ops_cpp_api class OPENVINO_API PagedAttentionExtension : public ov::op::Op { public: OPENVINO_OP("PagedAttentionExtension"); PagedAttentionExtension() = default; - PagedAttentionExtension(const ov::OutputVector& args); + /// \brief Constructs a PagedAttentionExtension. + /// + /// \param args 28 inputs (see spec for layout): + /// (B_token = total tokens in the call, B_seq = number of sequences, + /// H = query heads, Hk = key/value heads, S = head size) + /// + /// 0 query [B_token, H * S] required + /// 1 key [B_token, Hk * S] required + /// 2 value [B_token, Hk * S] required + /// 3 key_cache [num_blocks, Hk, Bs, S] required + /// 4 value_cache [num_blocks, Hk, Bs, S] required + /// 5 past_lens [B_seq], i32 required + /// 6 subsequence_begins [B_seq + 1], i32 required + /// 7 block_indices [total_blocks], i32 required + /// 8 block_indices_begins [B_seq + 1], i32 required + /// 9 scale [] scalar required + /// 10 sliding_window [] scalar, i32 required (0 = unlimited) + /// 11 alibi_slopes [H] or empty required (empty = disabled) + /// 12 max_context_len [] scalar, i32 required (0 = unlimited) + /// 13 score_aggregation_window [] scalar or [B_seq], i32 required (0 = disabled) + /// 14 rotated_block_indices [Nrot] or empty, i32 required (empty = disabled) + /// 15 rotation_deltas [Nrot] or [Nrot, Bs], i32 required (empty = disabled) + /// 16 rotation_trig_lut [C, S] or [C*S] required (empty = disabled) + /// 17 xattention_threshold [] or [B_seq] required (empty = disabled) + /// 18 xattention_block_size [] scalar, i32 required (0 = disabled) + /// 19 xattention_stride [] scalar, i32 required + /// 20 sinks [1, H, 1, 1] or empty required (empty = disabled) + /// 21 adaptive_rkv_start_size [] scalar, i32 required (0 = no protection zone) + /// 22 adaptive_rkv_evictable_sizes [B_seq], i32 optional + /// 23 adaptive_rkv_diversity_block_set_indices [num_adaptive_rkv_blocks] optional + /// 24 adaptive_rkv_diversity_block_set_indices_begins [B_seq + 1], i32 optional + /// 25 token_type_ids [B_token] or [B_token, 1], i32 optional + /// 26 qq_bias [total_bias_bytes], u8 optional + /// 27 qq_bias_begins [B_seq + 1], i32 optional + explicit PagedAttentionExtension(const ov::OutputVector& args, bool write_kv_cache = true); + void validate_and_infer_types() override; std::shared_ptr clone_with_new_inputs(const ov::OutputVector& new_args) const override; + bool visit_attributes(ov::AttributeVisitor& visitor) override; + const ov::element::Type get_out_type(int index) const; void set_out_type(int index, const ov::element::Type& output_type); + bool get_write_kv_cache() const { + return m_write_kv_cache; + } + protected: std::vector m_output_type = {ov::element::dynamic, ov::element::dynamic, ov::element::dynamic}; + bool m_write_kv_cache = true; }; } // namespace op diff --git a/src/core/dev_api/openvino/op/paged_causal_conv1d.hpp b/src/core/dev_api/openvino/op/paged_causal_conv1d.hpp new file mode 100644 index 000000000000..45618b058cfb --- /dev/null +++ b/src/core/dev_api/openvino/op/paged_causal_conv1d.hpp @@ -0,0 +1,58 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// +#pragma once + +#include "openvino/op/op.hpp" + +namespace ov::op::internal { +/// \note PagedCausalConv1D op class is under development and subject to change +/// +/// \brief Operator performing paged causal 1D convolution for linear attention models. +/// +/// Computes causal convolution over input embeddings using a block-based conv state table +/// managed by the paged execution pipeline. The conv state table is updated in-place by the kernel. +/// \ingroup ov_ops_cpp_api +class OPENVINO_API PagedCausalConv1D : public ov::op::Op { +public: + OPENVINO_OP("PagedCausalConv1D"); + + PagedCausalConv1D() = default; + /// \brief Constructs a PagedCausalConv1D operation. + /// + /// \param input_embeds Input embeddings [batch_size_in_tokens, hidden_size]. + /// \param conv_state_table Physical block table containing conv_cache states + /// [num_physical_blocks, hidden_size, kernel_size]. + /// \param conv_weight Convolution weight [out_channels, hidden_size/group_size, conv_kernel_size]. + /// \param conv_bias Convolution bias [out_channels] or [0] (empty = no bias). + /// \param subsequence_begins Start indices of tokens from current sequences [batch_size_in_sequences+1], + /// element type i32 or i64. + /// \param la_block_indices Logical block slots containing physical indices along 0-th dim in conv_state table + /// [num_logical_blocks], element type i32 or i64. + /// \param la_block_indices_begins Defines how block indices are split among sequences [batch_size_in_sequences+1], + /// element type i32 or i64. + /// \param processed_tokens Number of tokens already handled per sequence [batch_size_in_sequences], + /// element type i32 or i64. + /// \param cache_interval Interval between tokens to cache conv_state [batch_size_in_sequences], + /// element type i32 or i64. + PagedCausalConv1D(const Output& input_embeds, + const Output& conv_state_table, + const Output& conv_weight, + const Output& conv_bias, + const Output& subsequence_begins, + const Output& la_block_indices, + const Output& la_block_indices_begins, + const Output& processed_tokens, + const Output& cache_interval); + + /// \brief Constructs a PagedCausalConv1D operation from input vector. + /// + /// \param args Input tensor vector (9 inputs in order listed above). + PagedCausalConv1D(const ov::OutputVector& args); + + void validate_and_infer_types() override; + bool visit_attributes(AttributeVisitor& visitor) override; + std::shared_ptr clone_with_new_inputs(const ov::OutputVector& new_args) const override; +}; + +} // namespace ov::op::internal diff --git a/src/core/dev_api/openvino/op/paged_gated_delta_net.hpp b/src/core/dev_api/openvino/op/paged_gated_delta_net.hpp new file mode 100644 index 000000000000..5226209269c8 --- /dev/null +++ b/src/core/dev_api/openvino/op/paged_gated_delta_net.hpp @@ -0,0 +1,95 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// +#pragma once + +#include "openvino/op/op.hpp" + +namespace ov::op::internal { +/// \note PagedGatedDeltaNet op class is under development and subject to change +/// +/// \brief Operator performing paged Gated Delta Net computation for linear attention models. +/// +/// Computes gated delta net attention over input tokens using a block-based recurrent state table +/// managed by the paged execution pipeline. The recurrent state table is updated in-place by the kernel. +/// +/// This operation uses grouped-query linear attention. The number of groups is +/// ``num_groups = v_num_heads / num_heads``. Each query and key head is shared by +/// ``num_groups`` consecutive value heads, with the mapping ``h_q = h_v / num_groups``. +/// \ingroup ov_ops_cpp_api +class OPENVINO_API PagedGatedDeltaNet : public ov::op::Op { +public: + OPENVINO_OP("PagedGatedDeltaNet"); + + PagedGatedDeltaNet() = default; + /// \brief Constructs a PagedGatedDeltaNet operation. + /// + /// \param query Query tensor [batch_size_in_tokens, num_heads, key_head_dim]. + /// num_heads is the number of query/key heads; v_num_heads must be divisible by num_heads. + /// \param key Key tensor [batch_size_in_tokens, num_heads, key_head_dim]. + /// \param value Value tensor [batch_size_in_tokens, v_num_heads, value_head_dim]. + /// v_num_heads >= num_heads; the ratio v_num_heads/num_heads gives the number of GQA groups. + /// \param recurrent_state_table Block table containing recurrent states + /// [num_blocks, v_num_heads, value_head_dim, key_head_dim]. + /// \param gate Gate tensor [batch_size_in_tokens, v_num_heads]. + /// \param beta Beta tensor [batch_size_in_tokens, v_num_heads]. + /// \param subsequence_begins Start indices of tokens from current sequences [batch_size_in_sequences+1], + /// element type i32 or i64. + /// \param la_block_indices Block index along 0-th dim in recurrent_state table [num_blocks], + /// element type i32 or i64. + /// \param la_block_indices_begins Defines how block indices are split among sequences [batch_size_in_sequences+1], + /// element type i32 or i64. + /// \param processed_tokens Number of tokens already handled per sequence [batch_size_in_sequences], + /// element type i32 or i64. + /// \param cache_interval Interval between tokens to cache state [batch_size_in_sequences], + /// element type i32 or i64. + /// \param use_qk_l2norm Enables q/k L2-normalization inside this op. + /// \param q_l2_norm_eps Positive floating-point epsilon used for query L2-normalization. + /// \param k_l2_norm_eps Positive floating-point epsilon used for key L2-normalization. + PagedGatedDeltaNet(const Output& query, + const Output& key, + const Output& value, + const Output& recurrent_state_table, + const Output& gate, + const Output& beta, + const Output& subsequence_begins, + const Output& la_block_indices, + const Output& la_block_indices_begins, + const Output& processed_tokens, + const Output& cache_interval, + bool use_qk_l2norm = false, + float q_l2_norm_eps = 1e-6F, + float k_l2_norm_eps = 1e-6F); + + /// \brief Constructs a PagedGatedDeltaNet operation from input vector. + /// + /// \param args Input tensor vector (11 inputs in order listed above). + /// \param use_qk_l2norm Enables q/k L2-normalization inside this op. + /// \param q_l2_norm_eps Positive floating-point epsilon used for query L2-normalization. + /// \param k_l2_norm_eps Positive floating-point epsilon used for key L2-normalization. + PagedGatedDeltaNet(const ov::OutputVector& args, + bool use_qk_l2norm = false, + float q_l2_norm_eps = 1e-6F, + float k_l2_norm_eps = 1e-6F); + + void validate_and_infer_types() override; + bool visit_attributes(AttributeVisitor& visitor) override; + std::shared_ptr clone_with_new_inputs(const ov::OutputVector& new_args) const override; + + bool get_use_qk_l2norm() const { + return m_use_qk_l2norm; + } + float get_q_l2_norm_eps() const { + return m_q_l2_norm_eps; + } + float get_k_l2_norm_eps() const { + return m_k_l2_norm_eps; + } + +private: + bool m_use_qk_l2norm = false; + float m_q_l2_norm_eps = 1e-6F; + float m_k_l2_norm_eps = 1e-6F; +}; + +} // namespace ov::op::internal diff --git a/src/core/dev_api/openvino/runtime/aligned_buffer.hpp b/src/core/dev_api/openvino/runtime/aligned_buffer.hpp index e2f77771f8d2..fe45f9dd3b84 100644 --- a/src/core/dev_api/openvino/runtime/aligned_buffer.hpp +++ b/src/core/dev_api/openvino/runtime/aligned_buffer.hpp @@ -4,6 +4,7 @@ #pragma once +#include #include #include "openvino/core/attribute_adapter.hpp" @@ -12,6 +13,7 @@ namespace ov { class AlignedBuffer; + class OPENVINO_API IBufferDescriptor { public: virtual size_t get_id() const = 0; @@ -20,10 +22,7 @@ class OPENVINO_API IBufferDescriptor { virtual ~IBufferDescriptor(); }; -/// \brief Allocates a block of memory on the specified alignment. The actual size of the -/// allocated memory is larger than the requested size by the alignment, so allocating 1 -/// byte -/// on 64 byte alignment will allocate 65 bytes. +/// \brief Allocates a block of memory which starts at an aligned address determined by \p alignment. class OPENVINO_API AlignedBuffer { public: // Allocator objects and the allocation interfaces are owned by the @@ -41,20 +40,25 @@ class OPENVINO_API AlignedBuffer { return m_byte_size; } void* get_ptr(size_t offset) const { + hint_prefetch(); return m_aligned_buffer + offset; } void* get_ptr() { + hint_prefetch(); return m_aligned_buffer; } const void* get_ptr() const { + hint_prefetch(); return m_aligned_buffer; } template T* get_ptr() { + hint_prefetch(); return reinterpret_cast(m_aligned_buffer); } template const T* get_ptr() const { + hint_prefetch(); return reinterpret_cast(m_aligned_buffer); } @@ -68,7 +72,17 @@ class OPENVINO_API AlignedBuffer { AlignedBuffer(const AlignedBuffer&) = delete; AlignedBuffer& operator=(const AlignedBuffer&) = delete; + virtual void hint_evict() noexcept; + + /// \brief Ensures the buffer is available and populated with actual data. + virtual void hint_prefetch() const; + protected: + virtual void hint_evict(size_t offset, size_t size) noexcept; + static void invoke_evict(AlignedBuffer& buffer, size_t offset, size_t size) noexcept; + + static void invoke_hint_prefetch(const AlignedBuffer& buffer); + char* m_allocated_buffer; char* m_aligned_buffer; size_t m_byte_size; @@ -80,6 +94,6 @@ class OPENVINO_API AttributeAdapter> public: AttributeAdapter(std::shared_ptr& value); ~AttributeAdapter() override; - OPENVINO_RTTI("AttributeAdapter"); + OPENVINO_RTTI("AttributeAdapter>"); }; } // namespace ov diff --git a/src/core/dev_api/openvino/runtime/lazy_buffer.hpp b/src/core/dev_api/openvino/runtime/lazy_buffer.hpp new file mode 100644 index 000000000000..12e0b2339989 --- /dev/null +++ b/src/core/dev_api/openvino/runtime/lazy_buffer.hpp @@ -0,0 +1,67 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#pragma once + +#include +#include +#include +#include + +#include "openvino/runtime/aligned_buffer.hpp" + +namespace ov { + +/** \brief LazyBuffer is lazy loaded AlignedBuffer which provides a view on a file w/o memory mapping. */ +class OPENVINO_API LazyBuffer : public AlignedBuffer { +public: + /** + * @brief Constructs a LazyBuffer which provides a view on a file. The file content is loaded to memory when + * get_ptr() is called for the first time after object creation or after hint_evict() is called. The file content is + * loaded at aligned addresses, so the actual allocated memory may be larger than the requested byte size. + * @param file_path Path to the file to load + * @param offset Offset in the file to start the view + * @param byte_size Size of the view in bytes + * @throws AssertFailure if the file does not exist or the file size is smaller than the requested view. + */ + LazyBuffer(std::filesystem::path file_path, size_t offset, size_t byte_size); + + LazyBuffer(LazyBuffer&&) noexcept; + LazyBuffer& operator=(LazyBuffer&&) noexcept; + ~LazyBuffer() override; + + LazyBuffer(const LazyBuffer&) = delete; + LazyBuffer& operator=(const LazyBuffer&) = delete; + + /** + * @brief Gets aligned pointer to reserved buffer without loading data into it. + */ + void* get_reserved_ptr() const noexcept { + return m_aligned_buffer; + } + + /** + * @brief Evicts the buffer from memory. After this call, next call to hint_prefetch() will load the file content + * again. + */ + void hint_evict() noexcept override; + + /** + * @brief Loads the file content if it is not loaded yet. The content is loaded at aligned addresses, + * so the actual allocated memory may be larger than the requested byte size. + * @throws AssertFailure if the file cannot be opened or read. In this case, the buffer remains unloaded. + */ + void hint_prefetch() const override; + +protected: + void hint_evict(size_t offset, size_t size) noexcept override; + +private: + std::filesystem::path m_file_path; + size_t m_offset{0}; + + mutable std::atomic m_loaded{false}; + mutable std::mutex m_loading; +}; +} // namespace ov diff --git a/src/core/dev_api/openvino/runtime/shared_buffer.hpp b/src/core/dev_api/openvino/runtime/shared_buffer.hpp index 98b959ad2ca1..895689ee17fa 100644 --- a/src/core/dev_api/openvino/runtime/shared_buffer.hpp +++ b/src/core/dev_api/openvino/runtime/shared_buffer.hpp @@ -7,6 +7,7 @@ #include #include "openvino/runtime/aligned_buffer.hpp" +#include "openvino/util/common_util.hpp" #include "openvino/util/mmap_object.hpp" namespace ov { @@ -26,36 +27,80 @@ class SharedBufferBase : public ov::AlignedBuffer { virtual ~SharedBufferBase() { m_aligned_buffer = nullptr; - m_allocated_buffer = nullptr; m_byte_size = 0; } + virtual void hint_evict() noexcept override { + if constexpr (std::is_same_v, T>) { + if (m_shared_object) { + m_shared_object->hint_evict(get_offset(), m_byte_size); + } + } else if constexpr (std::is_same_v, T>) { + if (m_shared_object) { + invoke_evict(*m_shared_object, get_offset(), m_byte_size); + } + } else { + } + } + protected: + template + struct is_aligned_buffer_ptr : std::false_type {}; + template + struct is_aligned_buffer_ptr> : std::is_base_of {}; + template + static constexpr bool is_aligned_buffer_ptr_v = is_aligned_buffer_ptr::value; + + virtual void hint_evict(size_t offset, size_t size) noexcept override { + if constexpr (std::is_same_v, T>) { + if (m_shared_object) { + m_shared_object->hint_evict(offset, size); + } + } else { + } + } + + void hint_prefetch() const override { + if constexpr (is_aligned_buffer_ptr_v) { + if (this->m_shared_object) { + AlignedBuffer::invoke_hint_prefetch(*this->m_shared_object); + } + } + } + // protected to not create SharedBufferBase directly SharedBufferBase(char* data, size_t size, const T& shared_object, const std::shared_ptr& descriptor) - : _shared_object(shared_object), - m_source_buffer(descriptor ? descriptor->get_source_buffer() : nullptr) { - m_allocated_buffer = nullptr; + : m_shared_object{shared_object}, + m_source_buffer{descriptor ? descriptor->get_source_buffer() : nullptr}, + m_descriptor{[&] { + if (descriptor) { + uintptr_t offset{}; + if (m_source_buffer) { + const auto src_start = reinterpret_cast(m_source_buffer->get_ptr()); + const auto src_end = src_start + static_cast(m_source_buffer->size()); + const auto current = reinterpret_cast(data); + const auto data_in_src_range = (current >= src_start) && + !ov::util::add_overflow(current, size, offset) && + (offset <= src_end); + OPENVINO_ASSERT(data_in_src_range, "SharedBuffer data range is outside source buffer range"); + offset = current - src_start; + } + return create_base_descriptor(descriptor->get_id(), offset, m_source_buffer); + } else { + return std::shared_ptr{}; + } + }()} { m_aligned_buffer = data; m_byte_size = size; - if (descriptor) { - if (m_source_buffer) { - auto source_start = reinterpret_cast(m_source_buffer->get_ptr()); - auto current = reinterpret_cast(m_aligned_buffer); - OPENVINO_ASSERT(current >= source_start && current <= source_start + m_source_buffer->size(), - "SharedBuffer data pointer is outside source buffer range"); - } - m_descriptor = create_base_descriptor(descriptor->get_id(), get_offset(), m_source_buffer); - } } SharedBufferBase(char* data, size_t size, const T& shared_object) - : _shared_object(shared_object), - m_source_buffer(nullptr) { - m_allocated_buffer = nullptr; + : m_shared_object{shared_object}, + m_source_buffer{}, + m_descriptor{} { m_aligned_buffer = data; m_byte_size = size; } @@ -69,42 +114,32 @@ class SharedBufferBase : public ov::AlignedBuffer { } // Owns the underlying data and keeps it alive for the lifetime of this buffer - T _shared_object; + T m_shared_object; // Points to the root AlignedBuffer used for offset calculation and // accessible externally via get_descriptor()->get_source_buffer(); - // may or may not reference the same data as _shared_object + // may or may not reference the same data as m_shared_object std::shared_ptr m_source_buffer; std::shared_ptr m_descriptor; }; template class SharedBuffer : public SharedBufferBase { - template - struct is_aligned_buffer_ptr : std::false_type {}; - template - struct is_aligned_buffer_ptr> : std::is_base_of {}; - template - static constexpr bool is_aligned_buffer_ptr_v = is_aligned_buffer_ptr::value; + static std::shared_ptr get_or_make_descriptor(const T& shared_object) { + if constexpr (std::is_same_v>) { + return detail::create_mmap_descriptor(shared_object); + } else if constexpr (SharedBufferBase::template is_aligned_buffer_ptr_v) { + return shared_object ? shared_object->get_descriptor() : nullptr; + } else { + return nullptr; + } + } public: SharedBuffer(char* data, size_t size, const T& shared_object, const std::shared_ptr& descriptor) : SharedBufferBase(data, size, shared_object, descriptor) {} - // For non-AlignedBuffer, non-MappedMemory types: no auto-descriptor - template < - typename U = T, - std::enable_if_t && !std::is_same_v>, int> = 0> - SharedBuffer(char* data, size_t size, const T& shared_object) : SharedBufferBase(data, size, shared_object) {} - - // For AlignedBuffer-derived shared_ptr types: auto-inherit descriptor - template , int> = 0> - SharedBuffer(char* data, size_t size, const T& shared_object) - : SharedBufferBase(data, size, shared_object, shared_object ? shared_object->get_descriptor() : nullptr) {} - - // For MappedMemory: auto-create mmap descriptor - template >, int> = 0> SharedBuffer(char* data, size_t size, const T& shared_object) - : SharedBufferBase(data, size, shared_object, detail::create_mmap_descriptor(shared_object)) {} + : SharedBuffer(data, size, shared_object, get_or_make_descriptor(shared_object)) {} }; /// \brief SharedStreamBuffer class to store pointer to pre-allocated buffer and provide streambuf interface. @@ -175,19 +210,4 @@ class SharedStreamBuffer : public std::streambuf { const size_t m_size; size_t m_offset; }; - -/// \brief OwningSharedStreamBuffer is a SharedStreamBuffer which owns its shared object. -class OwningSharedStreamBuffer : public SharedStreamBuffer { -public: - OwningSharedStreamBuffer(std::shared_ptr buffer) - : SharedStreamBuffer(static_cast(buffer->get_ptr()), buffer->size()), - m_shared_obj(buffer) {} - - std::shared_ptr get_buffer() { - return m_shared_obj; - } - -protected: - std::shared_ptr m_shared_obj; -}; } // namespace ov diff --git a/src/core/include/openvino/core/deprecated.hpp b/src/core/include/openvino/core/deprecated.hpp index e3c26858f789..a6c687d45664 100644 --- a/src/core/include/openvino/core/deprecated.hpp +++ b/src/core/include/openvino/core/deprecated.hpp @@ -15,7 +15,11 @@ // #ifndef OPENVINO_DEPRECATED -# define OPENVINO_DEPRECATED(msg) [[deprecated(msg)]] +# if defined(__GNUC__) && !defined(__clang__) +# define OPENVINO_DEPRECATED(msg) __attribute__((deprecated(msg))) +# else +# define OPENVINO_DEPRECATED(msg) [[deprecated(msg)]] +# endif #endif #ifndef OPENVINO_ENUM_DEPRECATED # define OPENVINO_ENUM_DEPRECATED(msg) OPENVINO_DEPRECATED(msg) diff --git a/src/core/include/openvino/core/dimension.hpp b/src/core/include/openvino/core/dimension.hpp index 043fc53c7033..47d1c6c1e4f7 100644 --- a/src/core/include/openvino/core/dimension.hpp +++ b/src/core/include/openvino/core/dimension.hpp @@ -35,8 +35,20 @@ class OPENVINO_API Dimension { /// \brief Construct a dimension from string. /// \param str String to parse to dimension. + /// \deprecated Use string view version of constructor instead. It will be removed in 2027.0 release. +#ifdef IN_OV_COMPONENT + OPENVINO_DEPRECATED("This ctor is deprecated, use string_view version. Will be removed in 2027.0 release") +#endif Dimension(const std::string& str); + /// \brief Construct a dimension from string view. + /// \param sv String view to parse to dimension. + /// \{ + explicit Dimension(std::string_view sv); + template , std::string_view>>> + explicit Dimension(T&& sv) : Dimension(std::string_view(sv)) {} + /// \} + /// \brief Construct a dynamic dimension with range [0, ...] Dimension() = default; diff --git a/src/core/include/openvino/core/type.hpp b/src/core/include/openvino/core/type.hpp index 09e5be9cb649..1e40044dd479 100644 --- a/src/core/include/openvino/core/type.hpp +++ b/src/core/include/openvino/core/type.hpp @@ -83,7 +83,11 @@ class ConversionExtensionBase; template constexpr bool use_ov_dynamic_cast() { +#if defined(__ANDROID__) + return true; +#else return std::is_base_of_v; +#endif } /// \brief Tests if value is a pointer/shared_ptr that can be statically cast to a diff --git a/src/core/include/openvino/core/version.hpp b/src/core/include/openvino/core/version.hpp index 13507fa2b5e2..86312307056e 100644 --- a/src/core/include/openvino/core/version.hpp +++ b/src/core/include/openvino/core/version.hpp @@ -20,7 +20,7 @@ */ #define OPENVINO_VERSION_MAJOR 2026 -#define OPENVINO_VERSION_MINOR 2 +#define OPENVINO_VERSION_MINOR 3 #define OPENVINO_VERSION_PATCH 0 namespace ov { diff --git a/src/core/include/openvino/op/one_hot.hpp b/src/core/include/openvino/op/one_hot.hpp index 6f740c5724fd..f6f994e4b92a 100644 --- a/src/core/include/openvino/op/one_hot.hpp +++ b/src/core/include/openvino/op/one_hot.hpp @@ -14,7 +14,7 @@ namespace v1 { /// \ingroup ov_ops_cpp_api class OPENVINO_API OneHot : public util::OneHotBase { public: - OPENVINO_OP("OneHot", "opset1", op::Op); + OPENVINO_OP("OneHot", "opset1", util::OneHotBase); /// \brief Constructs a one-hot operation. OneHot() = default; @@ -48,7 +48,7 @@ namespace v16 { /// \ingroup ov_ops_cpp_api class OPENVINO_API OneHot : public util::OneHotBase { public: - OPENVINO_OP("OneHot", "opset16", op::Op); + OPENVINO_OP("OneHot", "opset16", util::OneHotBase); /// \brief Lists the supported negative indices modes for this version of the operator. /// See the specification for the description of how negative indices are handled. diff --git a/src/core/include/openvino/op/op.hpp b/src/core/include/openvino/op/op.hpp index b2064675b805..63f32996b60b 100644 --- a/src/core/include/openvino/op/op.hpp +++ b/src/core/include/openvino/op/op.hpp @@ -21,7 +21,7 @@ _OPENVINO_RTTI_OP_WITH_TYPE)(__VA_ARGS__)) \ /* Add accessibility for Op to the method: evaluate from the Base class \ Usually C++ allows to use virtual methods of Base class from Derived class but if they have \ - the same name and not all of them are overrided in Derived class, the only overrided methods \ + the same name and not all of them are overridden in Derived class, the only overridden methods \ will be available from Derived class. We need to explicitly cast Derived to Base class to \ have an access to remaining methods or use this using. */ \ using ov::op::Op::evaluate; \ diff --git a/src/core/include/openvino/op/segment_max.hpp b/src/core/include/openvino/op/segment_max.hpp index 12355c2ce309..6cde0947deef 100644 --- a/src/core/include/openvino/op/segment_max.hpp +++ b/src/core/include/openvino/op/segment_max.hpp @@ -37,7 +37,7 @@ class OPENVINO_API SegmentMax : public ov::op::Op { void validate_and_infer_types() override; std::shared_ptr clone_with_new_inputs(const OutputVector& new_args) const override; - const op::FillMode get_fill_mode() const; + op::FillMode get_fill_mode() const; private: op::FillMode m_fill_mode{}; diff --git a/src/core/include/openvino/op/squeeze.hpp b/src/core/include/openvino/op/squeeze.hpp index aa940e009e36..19ca742b1ccd 100644 --- a/src/core/include/openvino/op/squeeze.hpp +++ b/src/core/include/openvino/op/squeeze.hpp @@ -14,7 +14,7 @@ namespace v0 { /// \ingroup ov_ops_cpp_api class OPENVINO_API Squeeze : public util::SqueezeBase { public: - OPENVINO_OP("Squeeze", "opset1"); + OPENVINO_OP("Squeeze", "opset1", util::SqueezeBase); Squeeze(); /// \brief Constructs a squeeze v0 operation. @@ -43,7 +43,7 @@ namespace v15 { /// \ingroup ov_ops_cpp_api class OPENVINO_API Squeeze : public util::SqueezeBase { public: - OPENVINO_OP("Squeeze", "opset15"); + OPENVINO_OP("Squeeze", "opset15", util::SqueezeBase); Squeeze(); /// \brief Constructs a squeeze v15 operation. diff --git a/src/core/include/openvino/pass/pa_kv_reorder_fusion.hpp b/src/core/include/openvino/pass/pa_kv_reorder_fusion.hpp new file mode 100644 index 000000000000..639e91d87768 --- /dev/null +++ b/src/core/include/openvino/pass/pa_kv_reorder_fusion.hpp @@ -0,0 +1,30 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#pragma once + +#include "openvino/core/type/element_type.hpp" +#include "openvino/pass/pass.hpp" + +namespace ov { +namespace pass { + +/// Fuses paired key/value cache reorder paths into a single PaKVReorder op. +/// +/// This transformation performs graph pattern matching and replacement. +/// An optional cache_precision parameter can be provided to set the element type +/// of the key/value cache Parameters during fusion. +class OPENVINO_API PaKVReorderFusion : public ov::pass::ModelPass { +public: + OPENVINO_MODEL_PASS_RTTI("PaKVReorderFusion"); + PaKVReorderFusion() = default; + explicit PaKVReorderFusion(ov::element::Type cache_precision); + bool run_on_model(const std::shared_ptr& m) override; + +private: + ov::element::Type m_cache_precision = ov::element::dynamic; +}; + +} // namespace pass +} // namespace ov diff --git a/src/core/include/openvino/pass/sdpa_to_paged_attention.hpp b/src/core/include/openvino/pass/sdpa_to_paged_attention.hpp index 603b2aa9904e..373969bb22f7 100644 --- a/src/core/include/openvino/pass/sdpa_to_paged_attention.hpp +++ b/src/core/include/openvino/pass/sdpa_to_paged_attention.hpp @@ -4,13 +4,145 @@ #pragma once +#include #include +#include #include +#include "openvino/core/model.hpp" +#include "openvino/core/type/element_type.hpp" +#include "openvino/op/parameter.hpp" +#include "openvino/op/result.hpp" #include "openvino/pass/pass.hpp" namespace ov { namespace pass { +namespace paged_attention { + +struct Options { + bool use_per_layer_block_indices_inputs; + bool use_score_outputs; + bool allow_score_aggregation; + bool allow_cache_rotation; + bool allow_xattention; + bool allow_adaptive_rkv; + bool allow_qq_bias; +}; + +template +class NamedNodeStore { +public: + NamedNodeStore() = default; + + explicit NamedNodeStore(VectorT current_nodes) : m_current_nodes(std::move(current_nodes)) {} + + std::shared_ptr get(const std::string& name) const { + return find(name); + } + + bool exists(const std::string& name) const { + return find(name) != nullptr; + } + + std::shared_ptr operator[](const std::string& name) const { + auto node = find(name); + OPENVINO_ASSERT(node, "Missing model node: ", name); + return node; + } + + bool remove(const std::string& name) { + auto node = find(name); + if (!node) { + return false; + } + m_nodes.erase(std::remove(m_nodes.begin(), m_nodes.end(), node), m_nodes.end()); + return true; + } + + VectorT& items() { + return m_nodes; + } + + const VectorT& items() const { + return m_nodes; + } + + std::shared_ptr find(const std::string& name) const { + auto find_in = [&](const VectorT& nodes) -> std::shared_ptr { + auto it = std::find_if(nodes.begin(), nodes.end(), [&](const std::shared_ptr& node) { + return node && node->get_friendly_name() == name; + }); + return it == nodes.end() ? nullptr : *it; + }; + + if (auto current = find_in(m_current_nodes)) { + return current; + } + + return find_in(m_nodes); + } + +protected: + VectorT m_nodes; + VectorT m_current_nodes; +}; + +struct PaParams : public NamedNodeStore { + PaParams() = default; + + explicit PaParams(ov::ParameterVector current_params) + : NamedNodeStore(std::move(current_params)) {} + + std::shared_ptr add(const std::string& name, + const ov::element::Type& element_type, + const ov::PartialShape& shape) { + auto existing = find(name); + if (existing) { + OPENVINO_ASSERT(existing->get_element_type() == element_type, + "Existing parameter element type mismatch for '", + name, + "'."); + OPENVINO_ASSERT(existing->get_partial_shape() == shape, + "Existing parameter shape mismatch for '", + name, + "'."); + return existing; + } + auto param = std::make_shared(element_type, shape); + param->set_friendly_name(name); + OPENVINO_ASSERT(param->get_output_size() == 1); + param->get_output_tensor(0).set_names({name}); + m_nodes.push_back(param); + return param; + } +}; + +struct PaResults : public NamedNodeStore { + PaResults() = default; + + explicit PaResults(ov::ResultVector current_results) + : NamedNodeStore(std::move(current_results)) {} + + std::shared_ptr add(const std::string& name, const ov::Output& output) { + auto existing = find(name); + if (existing) { + OPENVINO_ASSERT(existing->get_output_size() == 1, + "Result '", + name, + "' is expected to have a single output."); + const auto& names = existing->get_output_tensor(0).get_names(); + OPENVINO_ASSERT(names.count(name) != 0, "Result '", name, "' does not contain the expected tensor name."); + return existing; + } + auto result = std::make_shared(output); + result->set_friendly_name(name); + OPENVINO_ASSERT(result->get_output_size() == 1); + result->get_output_tensor(0).set_names({name}); + m_nodes.push_back(result); + return result; + } +}; +} // namespace paged_attention /** * @brief The transformation replaces KV-cache processing part in LLMs by PagedAttention operation. * NOTE: @@ -34,13 +166,9 @@ class OPENVINO_API SDPAToPagedAttention : public ModelPass { bool run_on_model(const std::shared_ptr& model) override; private: - bool m_use_per_layer_block_indices_inputs; - bool m_use_score_outputs; - bool m_allow_score_aggregation; - bool m_allow_cache_rotation; - bool m_allow_xattention; - bool m_allow_adaptive_rkv; - bool m_allow_qq_bias; + paged_attention::PaParams m_params; + paged_attention::PaResults m_results; + paged_attention::Options m_options; }; } // namespace pass } // namespace ov diff --git a/src/core/reference/include/openvino/reference/convert.hpp b/src/core/reference/include/openvino/reference/convert.hpp index 17f00f540128..68ec1c72630d 100644 --- a/src/core/reference/include/openvino/reference/convert.hpp +++ b/src/core/reference/include/openvino/reference/convert.hpp @@ -82,7 +82,61 @@ void convert(const bfloat16* arg, float* out, size_t count); template <> void convert(const int32_t* arg, float16* out, size_t count); -// Count how many f32 values is out of normal finite numbers range when converted to f16 +/// @brief Maximum tolerated |abs(src - round_trip_f16(src))| for an in-range element. +/// If any element exceeds this threshold, the whole tensor is kept in its +/// original precision (FP32 / FP64 — acts as a tensor-wide veto in +/// check_f16_compression()). +inline constexpr double f16_compression_max_abs_error = 1.0; +/// @brief Maximum tolerated relative round-trip error abs_diff / |src| for an in-range element when compressing a +/// single scalar value. Used by the f16 scalar compression path (e.g. for RoPE constants), which is more sensitive to +/// precision +inline constexpr double f16_scalar_compression_max_rel_error = 1e-4; +/// @brief Maximum tolerated relative round-trip error abs_diff / |src| for an in-range element when compressing a +/// whole tensor. Elements above this threshold are accumulated into the combined rejection count used together with +/// f16_compression_keep_threshold. This is a looser threshold than f16_scalar_compression_max_rel_error to allow more +/// aggressive compression at the tensor level, where individual high-relative-error elements may be "diluted" by many +/// low-relative-error elements. +inline constexpr double f16_tensor_compression_max_rel_error = 1e-3; +/// @brief Proportion of combined rejections (out-of-FP16-range + high relative-error) +/// at which the Constant is kept in its original precision (FP32 / FP64), i.e. +/// no FP16 compression is applied. +inline constexpr float f16_compression_keep_threshold = 0.75f; + +/// @brief Result of a single-pass FP16-compression feasibility check produced by +/// check_f16_compression(). Used by CompressFloatConstantsImpl to decide +/// whether a floating-point Constant may be safely compressed to FP16. +struct CompressionCheckResult { + /// Combined count of rejected elements: values outside the finite FP16 + /// range PLUS in-range values whose FP16 conversion exceeds + /// f16_tensor_compression_max_rel_error. Compared against + /// f16_compression_keep_threshold to keep the Constant in its original + /// precision. For a pure out-of-range count, use count_out_of_f16_range(). + size_t rejected_count; + /// Early-bail flag: true iff any in-range element has |abs error| greater + /// than f16_compression_max_abs_error after the FP16 round-trip. When set, + /// rejected_count is not guaranteed to be complete. + bool has_lossy; +}; + +/// Single-pass combined FP16-compression feasibility check. +/// +/// Bails immediately if any in-range value has significant precision loss +/// (|round-trip error| > f16_compression_max_abs_error), setting +/// CompressionCheckResult::has_lossy. Otherwise accumulates a combined +/// rejection count (out-of-FP16-range + high relative-error) in +/// CompressionCheckResult::rejected_count, to be compared against +/// f16_compression_keep_threshold. The relative-error threshold is selected +/// internally from the input size: single-element inputs use +/// f16_scalar_compression_max_rel_error, while larger tensors use +/// f16_tensor_compression_max_rel_error. +/// JIT/AVX-512 (or AVX2+F16C) accelerated on x86 for tensors; single-element +/// inputs fall back to a scalar loop. +CompressionCheckResult check_f16_compression(const float* arg, size_t count); + +/// Counts elements in `arg` that fall outside the finite FP16 range (subnormal +/// when rounded, or larger in magnitude than float16::max()). Distinct from +/// check_f16_compression(), which also accounts for relative-error rejections. +/// Kept as a backward-compatible helper for external consumers of this header. size_t count_out_of_f16_range(const float* arg, size_t count); // Convert values from f32 to f16 with clamping to f16 min/max when value is out of normal finite numbers range diff --git a/src/core/reference/include/openvino/reference/pad.hpp b/src/core/reference/include/openvino/reference/pad.hpp index 6df01bbbf48e..e8e8cfbc42b2 100644 --- a/src/core/reference/include/openvino/reference/pad.hpp +++ b/src/core/reference/include/openvino/reference/pad.hpp @@ -4,6 +4,9 @@ #pragma once +#include +#include + #include "openvino/core/coordinate_diff.hpp" #include "openvino/core/shape.hpp" #include "openvino/op/util/attr_types.hpp" // for op::PadMode @@ -19,5 +22,13 @@ void pad(const char* data, const CoordinateDiff& padding_below, const CoordinateDiff& padding_above, const op::PadMode pad_mode); -} + +void pad(const std::string* data, + const std::string_view pad_value, + std::string* out, + const Shape& data_shape, + const Shape& out_shape, + const CoordinateDiff& padding_below, + const CoordinateDiff& padding_above); +} // namespace reference } // namespace ov diff --git a/src/core/reference/include/openvino/reference/paged_attention.hpp b/src/core/reference/include/openvino/reference/paged_attention.hpp new file mode 100644 index 000000000000..2b7304da8294 --- /dev/null +++ b/src/core/reference/include/openvino/reference/paged_attention.hpp @@ -0,0 +1,845 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "openvino/core/except.hpp" +#include "openvino/core/shape.hpp" +#include "openvino/core/type/bfloat16.hpp" +#include "openvino/core/type/element_type.hpp" +#include "openvino/core/type/float16.hpp" +#include "openvino/reference/adaptive_rkv_diversity.hpp" +#include "openvino/reference/utils/paged_cache_manager.hpp" + +namespace ov { +namespace reference { + +namespace detail { + +inline float read_scalar_as_f32(const void* ptr, const ov::element::Type& et) { + OPENVINO_ASSERT(ptr != nullptr, "read_scalar_as_f32: null pointer"); + if (et == ov::element::f32) { + return *static_cast(ptr); + } + if (et == ov::element::f64) { + return static_cast(*static_cast(ptr)); + } + if (et == ov::element::f16) { + return static_cast(*static_cast(ptr)); + } + if (et == ov::element::bf16) { + return static_cast(*static_cast(ptr)); + } + OPENVINO_THROW("PagedAttention reference: unsupported scalar type: ", et); +} + +inline float read_at_as_f32(const void* base, const ov::element::Type& et, std::size_t idx) { + OPENVINO_ASSERT(base != nullptr, "read_at_as_f32: null pointer"); + if (et == ov::element::f32) { + return static_cast(base)[idx]; + } + if (et == ov::element::f64) { + return static_cast(static_cast(base)[idx]); + } + if (et == ov::element::f16) { + return static_cast(static_cast(base)[idx]); + } + if (et == ov::element::bf16) { + return static_cast(static_cast(base)[idx]); + } + OPENVINO_THROW("PagedAttention reference: unsupported array type: ", et); +} + +/// Softmax with optional attention-sink support. +/// When sink_val != nullptr it participates in max/exp-sum but produces no output weight. +inline void softmax_inplace(std::vector& scores, const float* sink_val = nullptr) { + if (scores.empty()) { + return; + } + float m = *std::max_element(scores.begin(), scores.end()); + if (sink_val != nullptr) { + m = std::max(m, *sink_val); + } + float sum = 0.f; + for (float& s : scores) { + s = std::exp(s - m); + sum += s; + } + if (sink_val != nullptr) { + sum += std::exp(*sink_val - m); + } + const float inv = (sum > 0.f) ? (1.f / sum) : 0.f; + for (float& s : scores) { + s *= inv; + } +} + +inline void apply_rotary_inplace(std::vector& vec, + const void* trig_lut, + const ov::element::Type& trig_et, + std::size_t trig_row, + std::size_t head_size) { + if (trig_lut == nullptr || head_size < 2 || (head_size % 2) != 0) { + return; + } + const std::size_t half = head_size / 2; + const std::size_t row_base = trig_row * head_size; + // Split-half (LLaMA-style) pairing: first half [0..half) paired with second half [half..head_size) + // Trig LUT row: [cos_0..cos_{half-1}, sin_0..sin_{half-1}] + for (std::size_t d = 0; d < half; ++d) { + const float x0 = vec[d]; + const float x1 = vec[d + half]; + const float c = read_at_as_f32(trig_lut, trig_et, row_base + d); + const float s = read_at_as_f32(trig_lut, trig_et, row_base + half + d); + vec[d] = x0 * c - x1 * s; + vec[d + half] = x0 * s + x1 * c; + } +} + +inline std::vector parse_subsequence_ranges(const int32_t* subseq_begins, + std::size_t subseq_count, + std::size_t seq_count, + std::size_t batch_tokens) { + // Returns a vector of size (seq_count + 1) with begins and a final end + std::vector begins(seq_count + 1, 0); + if (subseq_begins == nullptr || subseq_count == 0 || seq_count == 0) { + begins[0] = 0; + begins[seq_count] = batch_tokens; + return begins; + } + + const std::size_t usable = std::min(subseq_count, seq_count); + for (std::size_t s = 0; s < usable; ++s) { + const std::int32_t v = subseq_begins[s]; + begins[s] = (v < 0) ? 0 : static_cast(v); + } + + if (subseq_count >= seq_count + 1) { + const std::int32_t vend = subseq_begins[seq_count]; + begins[seq_count] = (vend < 0) ? batch_tokens : static_cast(vend); + } else { + begins[seq_count] = batch_tokens; + } + + // Clamp and monotonic fix + begins[0] = std::min(begins[0], batch_tokens); + for (std::size_t s = 1; s <= seq_count; ++s) { + begins[s] = std::min(begins[s], batch_tokens); + if (begins[s] < begins[s - 1]) { + begins[s] = begins[s - 1]; + } + } + return begins; +} + +} // namespace detail + +// Reference implementation of ov::op::PagedAttentionExtension. +// Supports GQA, ALiBi, RoPE re-rotation, attention sinks, +// xattention sparse prefill, and adaptive RKV diversity scoring. +// +// pool_kernel (default 7): max-pool window for per-head score aggregation. +// Not yet an operator input but should be eventually. +template +void paged_attention(std::uintptr_t node_key, + ov::reference::paged_attention_cache::PagedCacheManager* cache_manager, + T* out, + T* out_scores, + T* out_diversity_scores, + const T* query, + const T* key, + const T* value, + const T* key_cache_init, + const T* value_cache_init, + const int32_t* past_lens, + const int32_t* subsequence_begins, + const int32_t* block_indices_init, + std::size_t block_indices_count, + const int32_t* block_indices_begins_init, + std::size_t block_indices_begins_count, + const void* scale, + const ov::element::Type& scale_et, + const int32_t* sliding_window, + const void* alibi_slopes, + const ov::element::Type& alibi_et, + const ov::Shape& alibi_shape, + const int32_t* max_context_len, + const int32_t* score_aggregation_window, + std::size_t score_aggregation_window_count, + const int32_t* rotated_block_indices, + std::size_t rotated_block_count, + const int32_t* rotation_deltas, + const ov::Shape& rotation_deltas_shape, + const void* rotation_trig_lut, + const ov::element::Type& trig_lut_et, + const ov::Shape& trig_lut_shape, + const void* xattention_threshold, + const ov::element::Type& xattention_threshold_et, + const int32_t* xattention_block_size, + const int32_t* xattention_stride, + const void* sinks, + const ov::element::Type& sinks_et, + const int32_t* adaptive_rkv_start_size, + const int32_t* adaptive_rkv_evictable_sizes, + const int32_t* adaptive_rkv_diversity_block_set_indices, + const int32_t* adaptive_rkv_diversity_block_set_indices_begins, + const int32_t* token_type_ids, + std::size_t token_type_ids_count, + const ov::Shape& query_shape, + const ov::Shape& key_shape, + const ov::Shape& value_shape, + const ov::Shape& key_cache_shape, + const ov::Shape& value_cache_shape, + const ov::Shape& past_lens_shape, + const ov::Shape& subseq_shape, + std::size_t pool_kernel = 7) { + OPENVINO_ASSERT(cache_manager != nullptr, "PagedAttention reference: cache_manager is null"); + OPENVINO_ASSERT(query != nullptr && key != nullptr && value != nullptr, "PagedAttention reference: null Q/K/V"); + OPENVINO_ASSERT(out != nullptr, "PagedAttention reference: output is null"); + + // Not used by the reference -- it reads key data directly from the cache manager + (void)adaptive_rkv_diversity_block_set_indices; + (void)adaptive_rkv_diversity_block_set_indices_begins; + + // Sinks: per-head virtual token in the softmax (shifts max/denominator, no output weight) + const bool has_sinks = (sinks != nullptr); + + // Xattention: sparse prefill via strided Q*K products with block-level masking + const bool has_xattn = (xattention_threshold != nullptr); + + OPENVINO_ASSERT(query_shape.size() == 2 && key_shape.size() == 2 && value_shape.size() == 2, + "PagedAttention reference: expected Q/K/V rank 2"); + OPENVINO_ASSERT(past_lens_shape.size() == 1 && subseq_shape.size() == 1, + "PagedAttention reference: expected past_lens/subsequence_begins rank 1"); + + const std::size_t batch_tokens = static_cast(query_shape[0]); + const std::size_t query_features = static_cast(query_shape[1]); + const std::size_t key_features = static_cast(key_shape[1]); + const std::size_t value_features = static_cast(value_shape[1]); + + const std::size_t seq_count = static_cast(past_lens_shape[0]); + const std::size_t subseq_count = static_cast(subseq_shape[0]); + + // Register operator once per node and copy init cache + cache_manager->ensure_operator(node_key, + key_cache_init, + value_cache_init, + key_cache_shape, + value_cache_shape, + block_indices_init, + block_indices_count, + block_indices_begins_init, + block_indices_begins_count, + past_lens, + seq_count); + + const std::size_t head_size = cache_manager->key_head_size(node_key); + const std::size_t kv_heads = cache_manager->num_kv_heads(node_key); + const std::size_t v_head_size = cache_manager->value_head_size(node_key); + + OPENVINO_ASSERT(head_size > 0 && kv_heads > 0 && v_head_size > 0, "PagedAttention reference: invalid cache layout"); + OPENVINO_ASSERT(key_features == kv_heads * head_size, + "PagedAttention reference: key feature dim mismatch with cache layout"); + OPENVINO_ASSERT(value_features == kv_heads * v_head_size, + "PagedAttention reference: value feature dim mismatch with cache layout"); + OPENVINO_ASSERT((query_features % head_size) == 0, + "PagedAttention reference: query features not divisible by head_size"); + + const std::size_t q_heads = query_features / head_size; + OPENVINO_ASSERT((q_heads % kv_heads) == 0, + "PagedAttention reference: expected num_heads to be divisible by num_kv_heads (GQA)"); + const std::size_t group = q_heads / kv_heads; + + const std::size_t out_features = q_heads * v_head_size; + const float scale_f = detail::read_scalar_as_f32(scale, scale_et); + const int32_t sliding_window_i = sliding_window ? sliding_window[0] : 0; + const int32_t max_context_i = max_context_len ? max_context_len[0] : 0; + const std::size_t alibi_len = alibi_shape.empty() ? 0 : static_cast(alibi_shape[0]); + + // Initialize per-sequence view of current lengths + cache_manager->begin_step(node_key, past_lens, seq_count); + + // Parse token-to-sequence partition + const auto seq_begins = detail::parse_subsequence_ranges(subsequence_begins, subseq_count, seq_count, batch_tokens); + + // Bidirectional image attention: precompute image group boundaries from token_type_ids. + // For each image token, record the inclusive begin and exclusive end of its contiguous group. + const bool has_token_types = (token_type_ids != nullptr && token_type_ids_count > 0); + std::vector image_group_begin(batch_tokens, -1); + std::vector image_group_end(batch_tokens, -1); + if (has_token_types) { + for (std::size_t s = 0; s < seq_count; ++s) { + auto se = seq_begins[s + 1]; + for (auto i = seq_begins[s]; i < se;) { + if (token_type_ids[i] != 1) { + ++i; + continue; + } + auto grp_begin = i; + while (i < se && token_type_ids[i] == 1) { + ++i; + } + for (auto j = grp_begin; j < i; ++j) { + image_group_begin[j] = static_cast(grp_begin); + image_group_end[j] = static_cast(i); + } + } + } + } + + // Prepare mapping for rotated blocks (block_id -> rotated_index) + std::unordered_map rotated_map; + rotated_map.reserve(rotated_block_count); + for (std::size_t i = 0; i < rotated_block_count; ++i) { + rotated_map.emplace(rotated_block_indices[i], static_cast(i)); + } + + const bool has_trig = (rotation_trig_lut != nullptr) && (!trig_lut_shape.empty()); + const std::size_t trig_rows = trig_lut_shape.size() == 2 ? static_cast(trig_lut_shape[0]) + : trig_lut_shape.size() == 1 ? 1 + : 0; + const bool deltas_is_2d = rotation_deltas_shape.size() == 2; + const std::size_t deltas_stride = deltas_is_2d ? static_cast(rotation_deltas_shape[1]) : 0; + + // out_scores: concatenation of [past_len + new_len] for each sequence + std::vector scores_acc; + // Per-head score buffer for max-pool + head-average (eviction scoring) + std::vector scores_per_head; + std::size_t total_score_len = 0; + const bool need_scores = (out_scores != nullptr); + if (need_scores) { + std::size_t total = 0; + for (std::size_t s = 0; s < seq_count; ++s) { + const std::size_t past = past_lens ? static_cast(past_lens[s]) : 0; + const std::size_t new_len = seq_begins[s + 1] - seq_begins[s]; + total += past + new_len; + } + total_score_len = total; + scores_acc.assign(total, 0.f); + scores_per_head.assign(q_heads * total, 0.f); + } + + std::vector logits; + std::vector out_head; + std::vector key_buf; // re-used for rotary re-rotation + + // Read per-head sink values from [1,H,1,1]. + // H is the number of query heads (not KV heads), so this is safe in GQA. + std::vector sink_vals; + if (has_sinks) { + sink_vals.resize(q_heads, 0.f); + for (std::size_t h = 0; h < q_heads; ++h) { + sink_vals[h] = detail::read_at_as_f32(sinks, sinks_et, h); + } + } + + // Xattention sparse mask per head: xattn_mask[h][q_blk][k_blk] + // Only active for multi-token prefill with a single sequence. + const int32_t xattn_block_sz = (xattention_block_size != nullptr) ? xattention_block_size[0] : 0; + const int32_t xattn_stride = (xattention_stride != nullptr) ? xattention_stride[0] : 0; + // xattn_mask layout: [q_heads][num_q_blocks][num_k_blocks] + std::vector>> xattn_mask; + + // Prefix offsets for out_scores concatenation + std::vector score_prefix(seq_count + 1, 0); + if (!scores_acc.empty()) { + for (std::size_t s = 0; s < seq_count; ++s) { + const std::size_t past = past_lens ? static_cast(past_lens[s]) : 0; + const std::size_t new_len = seq_begins[s + 1] - seq_begins[s]; + score_prefix[s + 1] = score_prefix[s] + past + new_len; + } + } + + for (std::size_t s = 0; s < seq_count; ++s) { + const std::size_t t_begin = seq_begins[s]; + const std::size_t t_end = seq_begins[s + 1]; + if (t_begin >= t_end) { + continue; + } + OPENVINO_ASSERT(t_end <= batch_tokens, "PagedAttention reference: subsequence range exceeds batch_tokens"); + + const std::int32_t past = past_lens ? past_lens[s] : 0; + OPENVINO_ASSERT(past >= 0, "PagedAttention reference: negative past_lens"); + const std::size_t new_len = t_end - t_begin; + const int32_t t_begin_i = static_cast(t_begin); + const int32_t new_len_i = static_cast(new_len); + + // Score aggregation window: [] broadcasts to all sequences, [B_seq] is per-sequence. + // Negative = unbounded, 0 = disabled. + const std::size_t saw_idx = + (score_aggregation_window_count > 1) ? std::min(s, score_aggregation_window_count - 1) : 0; + const int32_t score_window_i = score_aggregation_window ? score_aggregation_window[saw_idx] : 0; + + // Build xattention sparse mask (prefill only, new_len > 1, single sequence) + const bool do_xattn = has_xattn && new_len > 1 && seq_count == 1 && xattn_block_sz > 0 && xattn_stride > 0; + xattn_mask.clear(); + if (do_xattn) { + // Phase 1: strided Q*K dot products over the new tokens only + const float threshold_f = detail::read_scalar_as_f32(xattention_threshold, xattention_threshold_et); + // mask covers only the new tokens; past cached tokens are always attended to + const std::size_t total_len = new_len; + const std::size_t num_q_blocks = + (total_len + static_cast(xattn_block_sz) - 1) / static_cast(xattn_block_sz); + const std::size_t num_k_blocks = num_q_blocks; + const std::size_t q_strided = + (total_len + static_cast(xattn_stride) - 1) / static_cast(xattn_stride); + const std::size_t k_strided = q_strided; + const std::size_t num_per_block = + static_cast(xattn_block_sz) / static_cast(xattn_stride); + + xattn_mask.resize(q_heads); + for (std::size_t h = 0; h < q_heads; ++h) { + const std::size_t kvh = h / group; + + // Strided attention matrix [q_strided x k_strided] + std::vector attn_strided(q_strided * k_strided, 0.f); + + for (int32_t off = 0; off < xattn_stride; ++off) { + for (std::size_t qi = 0; qi < q_strided; ++qi) { + // Query token index: we sample the (xattn_stride-1-off + qi*stride)-th Q + const int64_t q_tok_idx = static_cast(xattn_stride) - 1 - off + + static_cast(qi) * static_cast(xattn_stride); + if (q_tok_idx < 0 || static_cast(q_tok_idx) >= total_len) + continue; + const std::size_t q_abs = t_begin + static_cast(q_tok_idx); + const T* qptr = query + q_abs * query_features + h * head_size; + + // Causal: attend up to qi+1 strided positions + const std::size_t k_causal = qi + 1; + for (std::size_t ki = 0; ki < std::min(k_causal, k_strided); ++ki) { + const int64_t k_tok_idx = static_cast(off) + + static_cast(ki) * static_cast(xattn_stride); + if (k_tok_idx < 0 || static_cast(k_tok_idx) >= total_len) + continue; + + // Read key from cache or from current input + const std::size_t k_pos = static_cast(k_tok_idx); + // all keys for strided estimation come from the new input tokens + const T* kptr_raw = key + (t_begin + k_pos) * key_features + kvh * head_size; + float dot = 0.f; + for (std::size_t d = 0; d < head_size; ++d) { + dot += static_cast(qptr[d]) * static_cast(kptr_raw[d]); + } + attn_strided[qi * k_strided + ki] += dot; + } + } + } + + // Scale and causal softmax on strided attention + const float xscale = scale_f / static_cast(xattn_stride); + for (std::size_t qi = 0; qi < q_strided; ++qi) { + const std::size_t k_causal = qi + 1; + float* row = attn_strided.data() + qi * k_strided; + // Apply scale + for (std::size_t ki = 0; ki < k_strided; ++ki) { + row[ki] *= xscale; + } + // Causal mask: set positions after causal boundary to -inf + for (std::size_t ki = k_causal; ki < k_strided; ++ki) { + row[ki] = -std::numeric_limits::infinity(); + } + // Softmax over this row + float m = -std::numeric_limits::infinity(); + for (std::size_t ki = 0; ki < k_strided; ++ki) + m = std::max(m, row[ki]); + float sm = 0.f; + for (std::size_t ki = 0; ki < k_strided; ++ki) { + row[ki] = std::exp(row[ki] - m); + sm += row[ki]; + } + if (sm > 0.f) { + float inv = 1.f / sm; + for (std::size_t ki = 0; ki < k_strided; ++ki) + row[ki] *= inv; + } + } + + // Phase 2: block aggregation - sum num_per_block * num_per_block windows + std::vector block_sums(num_q_blocks * num_k_blocks, 0.f); + for (std::size_t qb = 0; qb < num_q_blocks; ++qb) { + for (std::size_t kb = 0; kb < num_k_blocks; ++kb) { + float bsum = 0.f; + for (std::size_t dq = 0; dq < num_per_block; ++dq) { + const std::size_t qi = qb * num_per_block + dq; + if (qi >= q_strided) + continue; + for (std::size_t dk = 0; dk < num_per_block; ++dk) { + const std::size_t ki = kb * num_per_block + dk; + if (ki >= k_strided) + continue; + bsum += attn_strided[qi * k_strided + ki]; + } + } + block_sums[qb * num_k_blocks + kb] = bsum; + } + } + + // Phase 3: threshold masking - greedy top-block selection + xattn_mask[h].resize(num_q_blocks, std::vector(num_k_blocks, false)); + for (std::size_t qb = 0; qb < num_q_blocks; ++qb) { + const float* brow = block_sums.data() + qb * num_k_blocks; + float total_sum = 0.f; + for (std::size_t kb = 0; kb <= qb && kb < num_k_blocks; ++kb) { + total_sum += brow[kb]; + } + const float required = total_sum * threshold_f; + + // Build (value, index) pairs + std::vector> vals; + vals.reserve(num_k_blocks); + for (std::size_t kb = 0; kb < num_k_blocks; ++kb) { + vals.emplace_back(brow[kb], kb); + } + // Block 0 (first column) and diagonal (qb) always selected + // Swap them to positions 0 and 1, then sort rest descending + if (qb > 1 && qb < vals.size()) { + std::swap(vals[1], vals[qb]); + } + if (vals.size() > 2) { + std::sort(vals.begin() + 2, vals.end(), [](const auto& a, const auto& b) { + return a.first > b.first; + }); + } + + // Cumulative-sum block selection (top-p style). + // Block 0 and diagonal are always kept. + // cumsum tracks the prefix sum of entries in sorted order; + // the causal gate (vals[i].second > qb) rejects future blocks + // without affecting cumsum. + float cumsum = 0.f; + for (std::size_t i = 0; i < vals.size(); ++i) { + bool keep = false; + if (i == 0) { + // First column always kept + keep = true; + } else if (i == 1 && qb >= 1) { + // Diagonal always kept + keep = true; + cumsum = vals[0].first; + } else { + keep = (cumsum < required); + cumsum += vals[i - 1].first; + } + // Causal: only keep blocks where k_block <= q_block + if (vals[i].second > qb) + keep = false; + xattn_mask[h][qb][vals[i].second] = keep; + } + } + } + } + + // Base offset for out_scores for this sequence (concatenation order is sequence order) + const std::size_t score_base = score_prefix[s]; + + // Populate cache with all new tokens before computing attention. + // This ordering is required for bidirectional image groups (token_type_ids) + // which attend to future positions within the same group. + for (std::size_t i = 0; i < new_len; ++i) { + cache_manager->write_token_kv(node_key, + s, + past + static_cast(i), + key + (t_begin + i) * key_features, + value + (t_begin + i) * value_features); + } + + for (std::size_t i = 0; i < new_len; ++i) { + const std::size_t token = t_begin + i; + const std::int32_t qpos = past + static_cast(i); + + // Determine attention window + int32_t ncausal = qpos + 1; + const int32_t causal_pos = ncausal; + const bool is_image = has_token_types && token_type_ids[token] == 1; + + if (is_image) { + ncausal = std::min(past + (image_group_end[token] - t_begin_i), past + new_len_i); + } + + int32_t start = 0; + if (max_context_i > 0) + start = std::max(start, causal_pos - max_context_i); + if (sliding_window_i > 0) + start = std::max(start, causal_pos - sliding_window_i); + if (is_image) + start = std::min(start, past + (image_group_begin[token] - t_begin_i)); + if (start < 0) + start = 0; + const std::int32_t end = ncausal - 1; + const std::size_t ctx_len = (end >= start) ? static_cast(end - start + 1) : 0; + if (ctx_len == 0) { + // No context (shouldn't happen for causal), output zeros + std::fill(out + token * out_features, out + (token + 1) * out_features, T(0)); + continue; + } + + // Score window semantics: + // 0 = disabled, >0 = last N tokens, <0 = all tokens + const bool include_in_scores = + (scores_acc.empty() || score_window_i == 0) + ? false + : (score_window_i < 0 ? true : (static_cast(new_len - i) <= score_window_i)); + + const T* qrow = query + token * query_features; + + for (std::size_t h = 0; h < q_heads; ++h) { + const std::size_t kvh = h / group; + const float slope = (alibi_slopes == nullptr || alibi_len == 0) + ? 0.f + : detail::read_at_as_f32(alibi_slopes, + alibi_et, + (alibi_len == 1) ? 0 : std::min(h, alibi_len - 1)); + + const T* qptr = qrow + h * head_size; + + logits.assign(ctx_len, 0.f); + out_head.assign(v_head_size, 0.f); + + for (std::size_t t = 0; t < ctx_len; ++t) { + const std::int32_t kpos = start + static_cast(t); + ov::reference::paged_attention_cache::PagedCacheManager::TokenAddress addr; + const bool ok = cache_manager->resolve_token(node_key, s, kpos, addr); + if (!ok) { + logits[t] = -std::numeric_limits::infinity(); + continue; + } + + const T* kptr = cache_manager->key_ptr(node_key, addr, kvh); + if (kptr == nullptr) { + logits[t] = -std::numeric_limits::infinity(); + continue; + } + + // RoPE re-rotation for cached positions only; new tokens are unrotated + float dot = 0.f; + const auto it = rotated_map.find(addr.block); + if (has_trig && rotation_deltas != nullptr && it != rotated_map.end() && kpos < past) { + const int32_t rot_idx = it->second; + std::size_t trig_row = 0; + if (deltas_is_2d) { + const std::size_t off = static_cast(addr.offset); + const std::size_t di = static_cast(rot_idx) * deltas_stride + off; + const std::size_t deltas_total = static_cast(rotation_deltas_shape[0]) * + static_cast(rotation_deltas_shape[1]); + const int32_t raw = (di < deltas_total) ? rotation_deltas[di] : 0; + trig_row = raw < 0 ? 0 : static_cast(raw); + } else { + const std::size_t deltas_total = rotation_deltas_shape.size() == 1 + ? static_cast(rotation_deltas_shape[0]) + : 0; + const std::size_t di = static_cast(rot_idx); + const int32_t raw = (di < deltas_total) ? rotation_deltas[di] : 0; + trig_row = raw < 0 ? 0 : static_cast(raw); + } + if (trig_rows > 0) { + trig_row = std::min(trig_row, trig_rows - 1); + } + + key_buf.assign(head_size, 0.f); + for (std::size_t d = 0; d < head_size; ++d) { + key_buf[d] = static_cast(kptr[d]); + } + detail::apply_rotary_inplace(key_buf, rotation_trig_lut, trig_lut_et, trig_row, head_size); + for (std::size_t d = 0; d < head_size; ++d) { + dot += static_cast(qptr[d]) * key_buf[d]; + } + } else { + for (std::size_t d = 0; d < head_size; ++d) { + dot += static_cast(qptr[d]) * static_cast(kptr[d]); + } + } + + float l = dot * scale_f; + if (alibi_slopes != nullptr) { + l += slope * static_cast(kpos - qpos); + } + logits[t] = l; + } + + // Apply xattention sparse mask (new-to-new only; cached keys always included) + if (do_xattn && h < xattn_mask.size()) { + for (std::size_t t = 0; t < ctx_len; ++t) { + const std::int32_t kpos = start + static_cast(t); + if (kpos < past) + continue; // past cached tokens always included + const std::size_t q_blk = i / static_cast(xattn_block_sz); + const std::size_t k_blk = + static_cast(kpos - past) / static_cast(xattn_block_sz); + if (q_blk < xattn_mask[h].size() && k_blk < xattn_mask[h][q_blk].size()) { + if (!xattn_mask[h][q_blk][k_blk]) { + logits[t] = -std::numeric_limits::infinity(); + } + } + } + } + + // Softmax with optional attention sink + const float* sink_ptr = has_sinks ? &sink_vals[h] : nullptr; + detail::softmax_inplace(logits, sink_ptr); + + // Truncate weights to T precision (matches CPU which stores softmax output in compute type) + for (auto& w : logits) { + w = static_cast(static_cast(w)); + } + + for (std::size_t t = 0; t < ctx_len; ++t) { + const std::int32_t kpos = start + static_cast(t); + ov::reference::paged_attention_cache::PagedCacheManager::TokenAddress addr; + if (!cache_manager->resolve_token(node_key, s, kpos, addr)) { + continue; + } + const T* vptr = cache_manager->value_ptr(node_key, addr, kvh); + if (!vptr) { + continue; + } + const float w = logits[t]; + for (std::size_t d = 0; d < v_head_size; ++d) { + out_head[d] += w * static_cast(vptr[d]); + } + + if (include_in_scores) { + // Accumulate attention weight at each key position in the [past..past+new) timeline + const std::size_t idx = score_base + static_cast(kpos); + if (idx < scores_acc.size()) { + scores_acc[idx] += w; + } + // Also store per-head for max-pool + head-average + if (idx < total_score_len) { + scores_per_head[h * total_score_len + idx] += w; + } + } + } + + T* out_ptr = out + token * out_features + h * v_head_size; + for (std::size_t d = 0; d < v_head_size; ++d) { + out_ptr[d] = static_cast(out_head[d]); + } + } + } + } + + if (out_scores != nullptr) { + for (std::size_t i = 0; i < scores_acc.size(); ++i) { + out_scores[i] = static_cast(scores_acc[i]); + } + } + + // Feed scores back into the cache manager for eviction. + // Apply per-head max-pool + head-average for better importance estimation. + if (!scores_acc.empty()) { + if (pool_kernel > 1 && total_score_len > 0) { + // Per-sequence, per-head max-pool then average across heads + std::vector pooled(total_score_len, 0.f); + const std::size_t half_k = pool_kernel / 2; + + for (std::size_t h = 0; h < q_heads; ++h) { + const float* head_row = scores_per_head.data() + h * total_score_len; + // Max-pool with kernel pool_kernel, stride 1, pad half_k, per-sequence + std::size_t seg_start = 0; + for (std::size_t s = 0; s < seq_count; ++s) { + const std::size_t past = past_lens ? static_cast(past_lens[s]) : 0; + const std::size_t new_len = seq_begins[s + 1] - seq_begins[s]; + const std::size_t seg_len = past + new_len; + for (std::size_t t = 0; t < seg_len; ++t) { + const std::size_t lo = (t >= half_k) ? (t - half_k) : 0; + const std::size_t hi = std::min(t + half_k + 1, seg_len); + float mx = -std::numeric_limits::infinity(); + for (std::size_t j = lo; j < hi; ++j) { + mx = std::max(mx, head_row[seg_start + j]); + } + pooled[seg_start + t] += mx; // accumulate across heads + } + seg_start += seg_len; + } + } + // Divide by q_heads to get head-average + const float inv_heads = 1.f / static_cast(q_heads); + for (std::size_t i = 0; i < total_score_len; ++i) { + pooled[i] *= inv_heads; + } + cache_manager->update_attention_scores(node_key, pooled.data(), pooled.size(), past_lens, seq_count); + } else { + cache_manager->update_attention_scores(node_key, + scores_acc.data(), + scores_acc.size(), + past_lens, + seq_count); + } + } + + // Adaptive RKV diversity: compute per-block diversity scores for eviction zones + const bool has_adaptive_rkv = (adaptive_rkv_evictable_sizes != nullptr); + if (has_adaptive_rkv && out_diversity_scores != nullptr) { + const std::size_t cache_block_size = cache_manager->block_size(node_key); + const int32_t start_size_i = adaptive_rkv_start_size ? adaptive_rkv_start_size[0] : 0; + const std::size_t start_size = (start_size_i < 0) ? 0 : static_cast(start_size_i); + + std::size_t out_offset = 0; + for (std::size_t s = 0; s < seq_count; ++s) { + const int32_t evict_size_i = adaptive_rkv_evictable_sizes[s]; + if (evict_size_i <= 0) { + continue; + } + const std::size_t evict_size = static_cast(evict_size_i); + + // Total tokens for this sequence (past + newly appended) + const std::size_t past = past_lens ? static_cast(past_lens[s]) : 0; + const std::size_t new_len = seq_begins[s + 1] - seq_begins[s]; + const std::size_t total_tokens = past + new_len; + + if (total_tokens < start_size + evict_size) { + // Not enough tokens for the eviction zone - skip + continue; + } + + // Assemble key data from cache: [kv_heads, total_tokens, head_size] + std::vector key_data(kv_heads * total_tokens * head_size, T(0)); + for (std::size_t kvh = 0; kvh < kv_heads; ++kvh) { + for (std::size_t pos = 0; pos < total_tokens; ++pos) { + ov::reference::paged_attention_cache::PagedCacheManager::TokenAddress addr; + if (cache_manager->resolve_token(node_key, s, static_cast(pos), addr)) { + const T* kptr = cache_manager->key_ptr(node_key, addr, kvh); + if (kptr) { + T* dst = key_data.data() + kvh * total_tokens * head_size + pos * head_size; + std::copy(kptr, kptr + head_size, dst); + } + } + } + } + + ov::Shape key_shape_3d = {kv_heads, total_tokens, head_size}; + ov::reference::AdaptiveRKVDiversityCalculator calc(start_size, evict_size, cache_block_size); + auto diversity = calc.calculate_block_diversity(key_data.data(), key_shape_3d); + + // Flatten [evict_size/block_size][evict_size] into the output buffer + for (std::size_t b = 0; b < diversity.size(); ++b) { + for (std::size_t t = 0; t < diversity[b].size(); ++t) { + out_diversity_scores[out_offset++] = diversity[b][t]; + } + } + + // Feed diversity matrix into cache manager for eviction decisions + const std::size_t n_evict_blocks = diversity.size(); + std::vector flat_div(n_evict_blocks * evict_size, 0.f); + for (std::size_t b = 0; b < n_evict_blocks; ++b) { + for (std::size_t t = 0; t < diversity[b].size() && t < evict_size; ++t) { + flat_div[b * evict_size + t] = static_cast(diversity[b][t]); + } + } + const std::size_t start_blk_off = start_size / cache_block_size; + cache_manager + ->update_diversity_scores(node_key, s, flat_div.data(), n_evict_blocks, evict_size, start_blk_off); + } + } +} + +} // namespace reference +} // namespace ov diff --git a/src/core/reference/include/openvino/reference/utils/paged_cache_manager.hpp b/src/core/reference/include/openvino/reference/utils/paged_cache_manager.hpp new file mode 100644 index 000000000000..4202157dd9cc --- /dev/null +++ b/src/core/reference/include/openvino/reference/utils/paged_cache_manager.hpp @@ -0,0 +1,313 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "openvino/core/except.hpp" +#include "openvino/core/shape.hpp" +#include "openvino/core/type/element_type.hpp" +#include "openvino/runtime/aligned_buffer.hpp" + +namespace ov { +namespace reference { +namespace paged_attention_cache { + +// Eviction strategy when the block pool is full +enum class EvictionPolicy { + FIFO, // evict oldest block of the longest sequence + SCORE, // evict block with lowest attention score + ADAPTIVE_RKV // evict block with lowest key-diversity score +}; + +// CacheManager used by the reference implementation of PagedAttentionExtension +class PagedCacheManager { +public: + struct TokenAddress { + std::int32_t block = -1; + std::int32_t offset = 0; + }; + + // Defaults: SCORE eviction, 64 MB cap, attention_mass_p=0.9. + // These params are not yet exposed through the model/operator. + explicit PagedCacheManager(ov::element::Type elem_type, + EvictionPolicy policy = EvictionPolicy::SCORE, + std::size_t max_cache_bytes = 64UL * 1024UL * 1024UL, + float attention_mass_p = 0.9f); + PagedCacheManager(const PagedCacheManager&) = delete; + PagedCacheManager& operator=(const PagedCacheManager&) = delete; + + // Register or find operator state for a PA node + bool ensure_operator(std::uintptr_t node_key, + const void* key_cache_init, + const void* value_cache_init, + const ov::Shape& key_cache_shape, + const ov::Shape& value_cache_shape, + const std::int32_t* block_indices_init, + std::size_t block_indices_count, + const std::int32_t* block_indices_begins_init, + std::size_t block_indices_begins_count, + const std::int32_t* past_lens_init, + std::size_t past_lens_count); + + // Update per-sequence lengths at the start of a step. + // past_lens holds the logical length of each sequence before appending new tokens. + void begin_step(std::uintptr_t node_key, const std::int32_t* past_lens, std::size_t seq_count); + + // Ensure storage for a token position within a sequence. + // Allocates or reuses blocks as needed. + TokenAddress ensure_token(std::uintptr_t node_key, std::size_t seq_idx, std::int32_t token_pos); + + // Resolve an existing token address. + // Returns false if the token was trimmed or is beyond the logical length. + bool resolve_token(std::uintptr_t node_key, + std::size_t seq_idx, + std::int32_t token_pos, + TokenAddress& out_addr) const; + + // Write key/value vectors for a token into the cache + // + // key_row layout: [num_kv_heads * key_head_size] + // value_row layout: [num_kv_heads * value_head_size] + template + void write_token_kv(std::uintptr_t node_key, + std::size_t seq_idx, + std::int32_t token_pos, + const T* key_row, + const T* value_row); + + // Access pointers for an already resolved token + template + const T* key_ptr(std::uintptr_t node_key, TokenAddress addr, std::size_t kv_head) const; + + template + const T* value_ptr(std::uintptr_t node_key, TokenAddress addr, std::size_t kv_head) const; + + // Cache layout getters (per node) + std::size_t num_blocks(std::uintptr_t node_key) const; + std::size_t block_size(std::uintptr_t node_key) const; + std::size_t num_kv_heads(std::uintptr_t node_key) const; + std::size_t key_head_size(std::uintptr_t node_key) const; + std::size_t value_head_size(std::uintptr_t node_key) const; + + ov::element::Type element_type() const noexcept { + return m_elem_type; + } + + EvictionPolicy eviction_policy() const noexcept { + return m_policy; + } + + float attention_mass_p() const noexcept { + return m_attention_mass_p; + } + + // Feed attention scores back so SCORE eviction can pick the lowest-score block. + // scores is a flat [total_tokens] buffer matching out_scores layout. + void update_attention_scores(std::uintptr_t node_key, + const float* scores, + std::size_t scores_len, + const std::int32_t* past_lens, + std::size_t seq_count); + + // Feed the 2D diversity matrix so ADAPTIVE_RKV eviction can use it. + // diversity_matrix is row-major [num_blocks_in_zone, eviction_size]. + void update_diversity_scores(std::uintptr_t node_key, + std::size_t seq_idx, + const float* diversity_matrix, + std::size_t num_blocks_in_zone, + std::size_t eviction_size, + std::size_t start_block_offset); + +private: + struct SequenceState { + std::int32_t logical_length = 0; // expected by external past_lens + std::int32_t trim_front = 0; // number of tokens trimmed from front (multiple of block_size) + std::deque blocks; // physical block IDs for [trim_front, trim_front + blocks*block_size) + + // per-block attention scores (same order as blocks deque) + std::deque block_scores; + + // Raw diversity matrix [n_blocks, evict_size], refreshed each step. + std::vector diversity_matrix; + std::size_t diversity_n_blocks = 0; + std::size_t diversity_evict_size = 0; + std::size_t diversity_start_block = 0; // block index offset in seq's deque + }; + + enum class KeyCacheLayout { + BLOCK_MAJOR, + HEAD_MAJOR, + }; + + struct OperatorState { + // layout + std::size_t num_blocks = 0; + std::size_t block_size = 0; + std::size_t num_kv_heads = 0; + std::size_t key_head_size = 0; + std::size_t value_head_size = 0; + KeyCacheLayout key_cache_layout = KeyCacheLayout::BLOCK_MAJOR; + + // bytes per block + std::size_t key_block_bytes = 0; + std::size_t value_block_bytes = 0; + + // owned storage (copied once from init tensors) + ov::AlignedBuffer key_cache; + ov::AlignedBuffer value_cache; + + // free list and per-seq state + std::vector block_used; // 0/1 + std::vector free_blocks; // stack + std::vector sequences; + }; + + OperatorState& get_state(std::uintptr_t node_key); + const OperatorState& get_state(std::uintptr_t node_key) const; + + static std::size_t tensor_byte_size(const ov::Shape& shape, std::size_t elem_bytes); + + void init_sequences_from_block_tables(OperatorState& st, + const std::int32_t* block_indices, + std::size_t block_indices_count, + const std::int32_t* block_indices_begins, + std::size_t begins_count, + const std::int32_t* past_lens, + std::size_t past_lens_count); + + std::int32_t allocate_block(OperatorState& st, std::size_t requester_seq); + std::int32_t steal_block_fifo(OperatorState& st, std::size_t requester_seq); + std::int32_t steal_block_by_score(OperatorState& st, std::size_t requester_seq); + std::int32_t steal_block_by_diversity(OperatorState& st, std::size_t requester_seq); + std::int32_t steal_block(OperatorState& st, std::size_t requester_seq); + + static std::size_t elem_bytes_or_throw(ov::element::Type et); + + static void validate_cache_rank4_or_throw(const ov::Shape& key_cache_shape, const ov::Shape& value_cache_shape); + + static void parse_cache_layout_or_throw(OperatorState& st, + const ov::Shape& key_cache_shape, + const ov::Shape& value_cache_shape, + std::size_t elem_bytes); + + template + static void memcpy_typed(void* dst, const void* src, std::size_t count) { + std::memcpy(dst, src, count * sizeof(T)); + } + + // compute base pointers into internal storage + template + static T* key_block_base(OperatorState& st, std::int32_t block_id, std::size_t kv_head) { + auto* base = static_cast(st.key_cache.get_ptr()); + const std::size_t block_stride = st.num_kv_heads * st.block_size * st.key_head_size; + const std::size_t kv_stride = st.block_size * st.key_head_size; + return base + static_cast(block_id) * block_stride + kv_head * kv_stride; + } + + template + static T* value_block_base(OperatorState& st, std::int32_t block_id, std::size_t kv_head) { + auto* base = static_cast(st.value_cache.get_ptr()); + const std::size_t block_stride = st.num_kv_heads * st.block_size * st.value_head_size; + const std::size_t kv_stride = st.block_size * st.value_head_size; + return base + static_cast(block_id) * block_stride + kv_head * kv_stride; + } + + template + static const T* key_block_base(const OperatorState& st, std::int32_t block_id, std::size_t kv_head) { + auto* base = static_cast(st.key_cache.get_ptr()); + const std::size_t block_stride = st.num_kv_heads * st.block_size * st.key_head_size; + const std::size_t kv_stride = st.block_size * st.key_head_size; + return base + static_cast(block_id) * block_stride + kv_head * kv_stride; + } + + template + static const T* value_block_base(const OperatorState& st, std::int32_t block_id, std::size_t kv_head) { + auto* base = static_cast(st.value_cache.get_ptr()); + const std::size_t block_stride = st.num_kv_heads * st.block_size * st.value_head_size; + const std::size_t kv_stride = st.block_size * st.value_head_size; + return base + static_cast(block_id) * block_stride + kv_head * kv_stride; + } + +private: + ov::element::Type m_elem_type; + EvictionPolicy m_policy; + std::size_t m_max_cache_bytes; + float m_attention_mass_p; + std::unordered_map m_ops; +}; + +// ---------------- template impl ---------------- + +template +void PagedCacheManager::write_token_kv(std::uintptr_t node_key, + std::size_t seq_idx, + std::int32_t token_pos, + const T* key_row, + const T* value_row) { + auto& st = get_state(node_key); + if (seq_idx >= st.sequences.size()) { + OPENVINO_THROW("PagedCacheManager::write_token_kv: seq_idx out of range"); + } + + const auto addr = ensure_token(node_key, seq_idx, token_pos); + if (addr.block < 0) { + OPENVINO_THROW("PagedCacheManager::write_token_kv: failed to allocate block"); + } + + // Copy per-head key/value vectors into block storage. + for (std::size_t kvh = 0; kvh < st.num_kv_heads; ++kvh) { + T* kdst = key_block_base(st, addr.block, kvh) + static_cast(addr.offset) * st.key_head_size; + const T* ksrc = key_row + kvh * st.key_head_size; + std::memcpy(kdst, ksrc, st.key_head_size * sizeof(T)); + + T* vdst = value_block_base(st, addr.block, kvh) + static_cast(addr.offset) * st.value_head_size; + const T* vsrc = value_row + kvh * st.value_head_size; + std::memcpy(vdst, vsrc, st.value_head_size * sizeof(T)); + } + + // Maintains logical length + auto& seq = st.sequences[seq_idx]; + if (token_pos >= seq.logical_length) { + seq.logical_length = token_pos + 1; + } +} + +template +const T* PagedCacheManager::key_ptr(std::uintptr_t node_key, TokenAddress addr, std::size_t kv_head) const { + const auto& st = get_state(node_key); + if (addr.block < 0) { + return nullptr; + } + if (kv_head >= st.num_kv_heads) { + return nullptr; + } + const T* base = key_block_base(st, addr.block, kv_head); + return base + static_cast(addr.offset) * st.key_head_size; +} + +template +const T* PagedCacheManager::value_ptr(std::uintptr_t node_key, TokenAddress addr, std::size_t kv_head) const { + const auto& st = get_state(node_key); + if (addr.block < 0) { + return nullptr; + } + if (kv_head >= st.num_kv_heads) { + return nullptr; + } + const T* base = value_block_base(st, addr.block, kv_head); + return base + static_cast(addr.offset) * st.value_head_size; +} + +} // namespace paged_attention_cache +} // namespace reference +} // namespace ov diff --git a/src/core/reference/include/openvino/reference/utils/paged_cache_manager_helper.hpp b/src/core/reference/include/openvino/reference/utils/paged_cache_manager_helper.hpp new file mode 100644 index 000000000000..8629602d717b --- /dev/null +++ b/src/core/reference/include/openvino/reference/utils/paged_cache_manager_helper.hpp @@ -0,0 +1,53 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// +// Helpers to attach/retrieve a PagedCacheManager handle on PA nodes via rt_info. + +#pragma once + +#include + +#include "openvino/core/node.hpp" +#include "openvino/reference/utils/paged_cache_manager.hpp" + +namespace ov { +namespace reference { +namespace paged_attention_cache { + +/// rt_info key for the cache manager handle. +inline constexpr const char* CACHE_MANAGER_RT_INFO_KEY = "__paged_cache_manager"; + +using CacheManagerHandle = std::shared_ptr; + +/// Create a type-erased PagedCacheManager handle. +inline CacheManagerHandle make_cache_handle(ov::element::Type et) { + auto* mgr = new PagedCacheManager(et); + return CacheManagerHandle(static_cast(mgr), [](void* p) { + delete static_cast(p); + }); +} + +/// Store a cache manager handle on a node. +inline void set_cache_manager(ov::Node* node, CacheManagerHandle handle) { + node->get_rt_info()[CACHE_MANAGER_RT_INFO_KEY] = std::move(handle); +} + +/// Get the cache manager handle from a node, or nullptr if none. +inline CacheManagerHandle get_cache_manager(const ov::Node* node) { + auto& rt = node->get_rt_info(); + auto it = rt.find(CACHE_MANAGER_RT_INFO_KEY); + if (it == rt.end()) { + return nullptr; + } + return it->second.as(); +} + +/// Get the concrete PagedCacheManager pointer, or nullptr. +inline PagedCacheManager* get_cache_manager_ptr(const ov::Node* node) { + auto h = get_cache_manager(node); + return h ? static_cast(h.get()) : nullptr; +} + +} // namespace paged_attention_cache +} // namespace reference +} // namespace ov diff --git a/src/core/reference/src/op/convert.cpp b/src/core/reference/src/op/convert.cpp index 27f56a4b71bb..91b23ee85f41 100644 --- a/src/core/reference/src/op/convert.cpp +++ b/src/core/reference/src/op/convert.cpp @@ -20,6 +20,26 @@ namespace reference { namespace { #ifdef OV_CORE_USE_XBYAK_JIT +// vcvtps2ph immediate: force round-to-nearest-even and suppress all FP +// exceptions, so the rounding is independent of caller MXCSR state and +// bit-identical to static_cast(float). Equivalent to +// _MM_FROUND_TO_NEAREST_INT | _MM_FROUND_NO_EXC from . +// (Not 0x04 — that is _MM_FROUND_CUR_DIRECTION, i.e. "use MXCSR".) +inline constexpr uint8_t kVcvtps2phRneNoExc = 0x08; + +// Shared FP16 range constants used by multiple JIT kernels (fp32<->fp16 conversion, +// fp16 compression check). ov::float16::operator float() is not constexpr so the +// ov::float16-derived values must be initialised at runtime; the scalar-error / +// mask thresholds are plain literals and stay constexpr. We keep them as named +// file-scope constants to avoid duplicating std::numeric_limits<> lookups and +// literal values across every JIT body. +inline const float kF16MaxPos = std::numeric_limits::max(); +inline const float kF16MaxNeg = std::numeric_limits::lowest(); +inline const float kF16MinPos = ov::float16::from_bits(0x0001); +inline const float kF16MinNeg = -ov::float16::from_bits(0x0001); + +inline constexpr uint32_t kAbsMaskVal = 0x7FFFFFFFu; + template void jit_convert_vec(jit::Generator&, const Xbyak::RegExp&, const Xbyak::RegExp&) {} @@ -36,7 +56,7 @@ void jit_convert_vec(jit::Generator& gen, const Xbyak::RegExp& gen.movq(u8vec, gen.qword[src]); gen.vpmovzxbd(i32vec, u8vec); gen.vcvtdq2ps(fvec, i32vec); - gen.vcvtps2ph(f16vec, fvec, 0); + gen.vcvtps2ph(f16vec, fvec, kVcvtps2phRneNoExc); gen.vzeroupper(); gen.vmovdqu(gen.xword[dst], f16vec); } @@ -57,7 +77,7 @@ void jit_convert_vec(jit::Generator& gen, const Xbyak::RegExp& s auto f32vec = gen.ymm4; gen.vmovups(f32vec, gen.yword[src]); - gen.vcvtps2ph(f16vec, f32vec, 0); + gen.vcvtps2ph(f16vec, f32vec, kVcvtps2phRneNoExc); gen.vmovdqu(gen.xword[dst], f16vec); } @@ -66,10 +86,10 @@ void jit_convert_vec(jit::Generator& gen, const Xbyak::RegExp const auto f32vec = gen.ymm4; const auto f16vec = gen.xmm3; - gen.vpmovzxwd(f32vec, gen.yword[src]); // load bf16 into tmp - gen.vpslld(f32vec, f32vec, 16); // convert bf16->f32 by bit shift - gen.vcvtps2ph(f16vec, f32vec, 0); // convert f32 -> f16 - gen.vmovdqu(gen.xword[dst], f16vec); // move result to destination + gen.vpmovzxwd(f32vec, gen.yword[src]); // load bf16 into tmp + gen.vpslld(f32vec, f32vec, 16); // convert bf16->f32 by bit shift + gen.vcvtps2ph(f16vec, f32vec, kVcvtps2phRneNoExc); // convert f32 -> f16 + gen.vmovdqu(gen.xword[dst], f16vec); // move result to destination } template <> @@ -80,12 +100,12 @@ void jit_convert_vec(jit::Generator& gen, const Xbyak:: auto upper_bound = gen.ymm5; auto lower_bound = gen.ymm6; - gen.vpmovzxwd(f32vec, gen.yword[src]); // load bf16 into tmp - gen.vpslld(f32vec, f32vec, 16); // convert bf16->f32 by bit shift - gen.vminps(f32vec, f32vec, upper_bound); // clamp f16 max - gen.vmaxps(f32vec, f32vec, lower_bound); // clamp f16 lowest - gen.vcvtps2ph(f16vec, f32vec, 0); // convert f32 -> f16 - gen.vmovdqu(gen.xword[dst], f16vec); // move result to destination + gen.vpmovzxwd(f32vec, gen.yword[src]); // load bf16 into tmp + gen.vpslld(f32vec, f32vec, 16); // convert bf16->f32 by bit shift + gen.vminps(f32vec, f32vec, upper_bound); // clamp f16 max + gen.vmaxps(f32vec, f32vec, lower_bound); // clamp f16 lowest + gen.vcvtps2ph(f16vec, f32vec, kVcvtps2phRneNoExc); // convert f32 -> f16 + gen.vmovdqu(gen.xword[dst], f16vec); // move result to destination } template <> @@ -103,14 +123,14 @@ void jit_convert_vec_prepare(jit::Generator& gen) { auto lower_bound = gen.ymm6; auto addr = gen.r15; - static const float f16_max = std::numeric_limits::max(); - static const float f16_min = std::numeric_limits::lowest(); - static const float upper_bounds[8] = {f16_max, f16_max, f16_max, f16_max, f16_max, f16_max, f16_max, f16_max}; - static const float lower_bounds[8] = {f16_min, f16_min, f16_min, f16_min, f16_min, f16_min, f16_min, f16_min}; + static const float upper_bounds[8] = + {kF16MaxPos, kF16MaxPos, kF16MaxPos, kF16MaxPos, kF16MaxPos, kF16MaxPos, kF16MaxPos, kF16MaxPos}; + static const float lower_bounds[8] = + {kF16MaxNeg, kF16MaxNeg, kF16MaxNeg, kF16MaxNeg, kF16MaxNeg, kF16MaxNeg, kF16MaxNeg, kF16MaxNeg}; - gen.mov(addr, (size_t)upper_bounds); + gen.mov(addr, reinterpret_cast(upper_bounds)); gen.vmovdqu(upper_bound, gen.yword[addr]); - gen.mov(addr, (size_t)lower_bounds); + gen.mov(addr, reinterpret_cast(lower_bounds)); gen.vmovdqu(lower_bound, gen.yword[addr]); } @@ -129,7 +149,7 @@ void jit_convert_vec(jit::Generator& gen, const Xbyak::Reg gen.vmovups(f32vec, gen.yword[src]); gen.vminps(f32vec, f32vec, upper_bound); gen.vmaxps(f32vec, f32vec, lower_bound); - gen.vcvtps2ph(f16vec, f32vec, 0); + gen.vcvtps2ph(f16vec, f32vec, kVcvtps2phRneNoExc); gen.vmovdqu(gen.xword[dst], f16vec); } @@ -138,11 +158,11 @@ void jit_convert_vec_prepare(jit::Generator& gen) { auto order = gen.ymm1; auto addr = gen.r15; - static const int8_t offsets[32] = {0, 4, 8, 12, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, 0, 4, 8, 12, -1, -1, -1, -1, -1, -1, -1, -1}; + static constexpr int8_t offsets[32] = {0, 4, 8, 12, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, 0, 4, 8, 12, -1, -1, -1, -1, -1, -1, -1, -1}; - gen.mov(addr, (size_t)offsets); // get offsets[] address - gen.vmovdqu(order, gen.yword[addr]); // save offsets[] to ymm register + gen.mov(addr, reinterpret_cast(offsets)); // get offsets[] address + gen.vmovdqu(order, gen.yword[addr]); // save offsets[] to ymm register } template <> @@ -268,7 +288,7 @@ class jit_convert_array : public jit::Generator { // Since the JIT code always resides in memory, and ASAN's memory management may remove executable // permissions, we need to restore executable permissions for the generated code. generator.setProtectModeRE(false); - return (fn_t)generator.getCode(); + return generator.getCode(); } return nullptr; } @@ -283,107 +303,6 @@ void jit_count_out_of_range_vec(jit::Generator&, const Xbyak::RegExp&); template void jit_count_out_of_range_vec_finalize(jit::Generator&, const Xbyak::RegExp&) {} -template <> -void jit_count_out_of_range_vec_prepare(jit::Generator& gen) { - auto accum_vec = gen.ymm4; - auto f16_max_pos_vec = gen.ymm5; - auto f16_max_neg_vec = gen.ymm6; - auto f16_min_pos_vec = gen.ymm7; - auto f16_min_neg_vec = gen.ymm8; - auto f16_zero_vec = gen.ymm9; - auto i32_ones_vec = gen.ymm10; - auto addr = gen.r15; - - static const float f16_max_pos = std::numeric_limits::max(); - static const float f16_max_neg = std::numeric_limits::lowest(); - static const float f16_min_pos = ov::float16::from_bits(0x0001); - static const float f16_min_neg = -ov::float16::from_bits(0x0001); - static const int32_t i32_one = 1; - - static const float max_pos_bounds[8] = - {f16_max_pos, f16_max_pos, f16_max_pos, f16_max_pos, f16_max_pos, f16_max_pos, f16_max_pos, f16_max_pos}; - static const float max_neg_bounds[8] = - {f16_max_neg, f16_max_neg, f16_max_neg, f16_max_neg, f16_max_neg, f16_max_neg, f16_max_neg, f16_max_neg}; - static const float min_pos_bounds[8] = - {f16_min_pos, f16_min_pos, f16_min_pos, f16_min_pos, f16_min_pos, f16_min_pos, f16_min_pos, f16_min_pos}; - static const float min_neg_bounds[8] = - {f16_min_neg, f16_min_neg, f16_min_neg, f16_min_neg, f16_min_neg, f16_min_neg, f16_min_neg, f16_min_neg}; - static const int32_t i32_ones[8] = {i32_one, i32_one, i32_one, i32_one, i32_one, i32_one, i32_one, i32_one}; - - auto load_vec = [&gen, &addr](Xbyak::Ymm vec, size_t ptr) { - gen.mov(addr, ptr); - gen.vmovdqu(vec, gen.yword[addr]); - }; - - load_vec(f16_max_pos_vec, (size_t)max_pos_bounds); - load_vec(f16_max_neg_vec, (size_t)max_neg_bounds); - load_vec(f16_min_pos_vec, (size_t)min_pos_bounds); - load_vec(f16_min_neg_vec, (size_t)min_neg_bounds); - load_vec(i32_ones_vec, (size_t)i32_ones); - gen.vxorps(f16_zero_vec, f16_zero_vec, f16_zero_vec); - gen.vxorps(accum_vec, accum_vec, accum_vec); -} - -template <> -void jit_count_out_of_range_vec(jit::Generator& gen, const Xbyak::RegExp& data) { - auto data_vec = gen.ymm1; - auto mask_vec = gen.ymm2; - auto mask_vec_xmm = gen.xmm2; - auto tmp_vec = gen.ymm3; - auto accum_vec = gen.ymm4; - auto f16_max_pos_vec = gen.ymm5; - auto f16_max_neg_vec = gen.ymm6; - auto f16_min_pos_vec = gen.ymm7; - auto f16_min_neg_vec = gen.ymm8; - auto f16_zero_vec = gen.ymm9; - auto i32_ones_vec = gen.ymm10; - - const unsigned char _cmp_lt_os = 1; - const unsigned char _cmp_neq_uq = 4; - const unsigned char _cmp_gt_os = 6; - - // std::abs(data) < ov::float16::from_bits(0x0001) - gen.vmovups(data_vec, gen.yword[data]); - gen.vcmpps(tmp_vec, data_vec, f16_min_pos_vec, _cmp_lt_os); - gen.vcmpps(mask_vec, data_vec, f16_min_neg_vec, _cmp_gt_os); - gen.vandps(mask_vec, mask_vec, tmp_vec); - - // data != 0.0f - gen.vcmpps(tmp_vec, data_vec, f16_zero_vec, _cmp_neq_uq); - gen.vandps(mask_vec, mask_vec, tmp_vec); - - // data > std::numeric_limits::max() - gen.vcmpps(tmp_vec, data_vec, f16_max_pos_vec, _cmp_gt_os); - gen.vorps(mask_vec, mask_vec, tmp_vec); - - // data < std::numeric_limits::lowest() - gen.vcmpps(tmp_vec, data_vec, f16_max_neg_vec, _cmp_lt_os); - gen.vorps(mask_vec, mask_vec, tmp_vec); - - // addition to i64 accumulator - gen.vandps(mask_vec, mask_vec, i32_ones_vec); - gen.vphaddd(mask_vec, mask_vec, mask_vec); - gen.vpermq(mask_vec, mask_vec, 0x08); - gen.vpmovsxdq(mask_vec, mask_vec_xmm); - gen.vpaddq(accum_vec, accum_vec, mask_vec); -} - -template <> -void jit_count_out_of_range_vec_finalize(jit::Generator& gen, const Xbyak::RegExp& dst) { - auto tmp_vec_xmm0 = gen.xmm2; // reuse mask_vec - auto tmp_vec_xmm1 = gen.xmm3; // reuse tmp_vec - auto accum_vec_ymm = gen.ymm4; - auto accum_vec_xmm = gen.xmm4; - - // horizontal sum of four i64 values - gen.vextractf128(tmp_vec_xmm0, accum_vec_ymm, 0); - gen.vextractf128(tmp_vec_xmm1, accum_vec_ymm, 1); - gen.vpaddq(accum_vec_xmm, tmp_vec_xmm0, tmp_vec_xmm1); - gen.vpermilpd(tmp_vec_xmm0, accum_vec_xmm, 0x01); - gen.vpaddq(accum_vec_xmm, accum_vec_xmm, tmp_vec_xmm0); - gen.vmovq(gen.qword[dst], accum_vec_xmm); -} - class jit_count_out_of_range : public jit::Generator { typedef struct context { struct { @@ -476,10 +395,462 @@ class jit_count_out_of_range : public jit::Generator { // Since the JIT code always resides in memory, and ASAN's memory management may remove executable // permissions, we need to restore executable permissions for the generated code. generator.setProtectModeRE(false); - return (fn_t)generator.getCode(); + return generator.getCode(); + } + return nullptr; + } +}; + +// Combined single-pass JIT kernel for AVX-512: counts out-of-range AND detects lossy f16 compression. +// Processes 16 floats per iteration using zmm + opmask + F16C (vcvtps2ph/vcvtph2ps). Bails immediately +// if any in-range element has significant precision loss (abs error > 1.0). Tail handled via masked load. +class jit_check_f16_compression_avx512 : public jit::Generator { +public: + typedef struct { + const void* src; + void* rejected_dst; // size_t* — output: combined out-of-range + high-relative-error count + void* lossy_dst; // size_t* — output: lossy count (0 or non-zero) + const size_t count; + float max_relative_error; + float max_abs_error; + } args_t; + + typedef void (*fn_t)(const args_t*); + + static fn_t get() { + if (is_x64() && mayiuse(jit::avx512_core) && mayiuse(jit::fp16)) { + static jit_check_f16_compression_avx512 generator; + + // Since the JIT code always resides in memory, and ASAN's memory management may remove executable + // permissions, we need to restore executable permissions for the generated code. + generator.setProtectModeRE(false); + return generator.getCode(); } return nullptr; } + +private: + jit_check_f16_compression_avx512() : jit::Generator(jit::avx512_core) { + using namespace Xbyak; + + const uint32_t vlen = 16u; // 16 floats per zmm + + // GP register allocation + const auto& reg_src = rax; + const auto& reg_rejected_dst = rbx; // callee-saved + const auto& reg_lossy_dst = r12; // callee-saved + const auto& reg_count = rdx; + const auto& reg_rejected_accum = + r14; // callee-saved — 64-bit combined rejection accumulator (OOR + high-rel-err) + const auto& reg_main_iters = r8; + const auto& reg_idx = rsi; + const auto& reg_tmp = r9; + const auto& reg_addr = r15; // callee-saved — used to load constants + + // ZMM register allocation + const auto& data_vec = zmm0; // chunk of 16 floats + const auto& rt_vec = zmm1; // f32->f16->f32 roundtripped + const auto& rt_ymm = ymm1; // low 256-bit alias for vcvtps2ph dest + const auto& diff_vec = zmm2; // |data - roundtripped| + const auto& abs_data_vec = zmm3; // |data| + const auto& rel_thresh_vec = zmm4; // |data| * 1e-4 + const auto& abs_mask_vec = zmm5; // 0x7FFFFFFF broadcast + const auto& abs_err_vec = zmm6; // 1.0f broadcast + const auto& rel_err_vec = zmm7; // 1e-4f broadcast + const auto& f16_max_pos_vec = zmm8; + const auto& f16_max_neg_vec = zmm9; + const auto& f16_min_pos_vec = zmm10; + const auto& f16_min_neg_vec = zmm11; + const auto& zero_vec = zmm12; + + // Opmasks (k0 reserved by ISA — represents "no mask") + const auto& k_oor = k1; // OOR per chunk (subnormal | overflow), then |= relative-error + const auto& k_lossy = k2; // (abs_diff > 1.0) AND NOT k_oor — early exit + const auto& k_rel = k3; // (abs_diff > |data|*1e-4) AND NOT k_oor + const auto& k_tail = k4; // tail mask (built once before tail iteration) + const auto& k_tmp1 = k5; + + Label main_loop, tail_block, normal_exit, lossy_exit, done; + + preamble(); + xor_(reg_rejected_accum, reg_rejected_accum); + + auto bcast = [&, this](const Xbyak::Zmm& vec, const void* p) { + mov(reg_addr, reinterpret_cast(p)); + vbroadcastss(vec, dword[reg_addr]); + }; + + bcast(f16_max_pos_vec, &kF16MaxPos); + bcast(f16_max_neg_vec, &kF16MaxNeg); + bcast(f16_min_pos_vec, &kF16MinPos); + bcast(f16_min_neg_vec, &kF16MinNeg); + bcast(abs_mask_vec, &kAbsMaskVal); + vpxorq(zero_vec, zero_vec, zero_vec); + + // --- Load args --- + mov(reg_src, ptr[param + offsetof(args_t, src)]); + mov(reg_rejected_dst, ptr[param + offsetof(args_t, rejected_dst)]); + mov(reg_lossy_dst, ptr[param + offsetof(args_t, lossy_dst)]); + mov(reg_count, ptr[param + offsetof(args_t, count)]); + vbroadcastss(rel_err_vec, dword[param + offsetof(args_t, max_relative_error)]); + vbroadcastss(abs_err_vec, dword[param + offsetof(args_t, max_abs_error)]); + + constexpr uint8_t cmp_lt_os = 1; + constexpr uint8_t cmp_neq_uq = 4; + constexpr uint8_t cmp_gt_os = 6; + + // Per-chunk emission. If `masked` is true, all opmasks are AND-ed with k_tail + // to defensively clear out-of-tail lanes (masked load already zeroed them, but + // this guards against any non-zero residual from sign-bit operations on -0.0). + auto emit_chunk = [&, this](bool masked) { + // === Build OOR mask in k_oor === + // subnormal-when-rounded: |data| < min_pos AND data != 0 + vcmpps(k_tmp1, data_vec, f16_min_pos_vec, cmp_lt_os); // data < min_pos + vcmpps(k_oor | k_tmp1, + data_vec, + f16_min_neg_vec, + cmp_gt_os); // merge-masked: k_oor = k_tmp1 & (data > min_neg) + vcmpps(k_tmp1, data_vec, zero_vec, cmp_neq_uq); // data != 0 + kandw(k_oor, k_oor, k_tmp1); + // overflow: data > max_pos OR data < max_neg + vcmpps(k_tmp1, data_vec, f16_max_pos_vec, cmp_gt_os); + korw(k_oor, k_oor, k_tmp1); + vcmpps(k_tmp1, data_vec, f16_max_neg_vec, cmp_lt_os); + korw(k_oor, k_oor, k_tmp1); + if (masked) { + kandw(k_oor, k_oor, k_tail); + } + + // === Roundtrip f32 -> f16 -> f32 === + vcvtps2ph(rt_ymm, data_vec, kVcvtps2phRneNoExc); + vcvtph2ps(rt_vec, rt_ymm); + + // === abs_diff = |data - roundtripped| === + vsubps(diff_vec, data_vec, rt_vec); + vpandd(diff_vec, diff_vec, abs_mask_vec); // mask off sign bit + + // === Lossy check: (abs_diff > 1.0) AND NOT k_oor -> early exit === + vcmpps(k_lossy, diff_vec, abs_err_vec, cmp_gt_os); + kandnw(k_lossy, k_oor, k_lossy); // k_lossy = ~k_oor & k_lossy + if (masked) { + kandw(k_lossy, k_lossy, k_tail); + } + kortestw(k_lossy, k_lossy); + jnz(lossy_exit, T_NEAR); + + // === Relative-error check: (abs_diff > |data| * max_relative_error) AND NOT k_oor === + vpandd(abs_data_vec, data_vec, abs_mask_vec); + vmulps(rel_thresh_vec, abs_data_vec, rel_err_vec); + vcmpps(k_rel, diff_vec, rel_thresh_vec, cmp_gt_os); + kandnw(k_rel, k_oor, k_rel); + korw(k_oor, k_oor, k_rel); + if (masked) { + kandw(k_oor, k_oor, k_tail); + } + + // === Accumulate popcount(k_oor) into reg_rejected_accum === + // Branchless 16-bit SWAR popcount — avoids POPCNT (not gated by mayiuse(avx512_core)). + kmovw(reg_tmp.cvt32(), k_oor); // x = k_oor (16 bits in low word) + mov(reg_addr.cvt32(), reg_tmp.cvt32()); // tmp2 = x + shr(reg_addr.cvt32(), 1); + and_(reg_addr.cvt32(), 0x5555); + sub(reg_tmp.cvt32(), reg_addr.cvt32()); // x -= (x >> 1) & 0x5555 + mov(reg_addr.cvt32(), reg_tmp.cvt32()); + and_(reg_addr.cvt32(), 0x3333); + shr(reg_tmp.cvt32(), 2); + and_(reg_tmp.cvt32(), 0x3333); + add(reg_tmp.cvt32(), reg_addr.cvt32()); // x = (x & 0x3333) + ((x >> 2) & 0x3333) + mov(reg_addr.cvt32(), reg_tmp.cvt32()); + shr(reg_addr.cvt32(), 4); + add(reg_tmp.cvt32(), reg_addr.cvt32()); + and_(reg_tmp.cvt32(), 0x0F0F); // x = (x + (x >> 4)) & 0x0F0F + mov(reg_addr.cvt32(), reg_tmp.cvt32()); + shr(reg_addr.cvt32(), 8); + add(reg_tmp.cvt32(), reg_addr.cvt32()); // low byte = popcount (max 16) + and_(reg_tmp, 0xFF); // zero upper bits before 64-bit add + add(reg_rejected_accum, reg_tmp); + }; + + // --- Main loop: 16 elements per iteration --- + mov(reg_main_iters, reg_count); + shr(reg_main_iters, 4); + xor_(reg_idx, reg_idx); + + L(main_loop); + cmp(reg_idx, reg_main_iters); + jge(tail_block, T_NEAR); + + vmovups(data_vec, zword[reg_src]); + emit_chunk(/*masked=*/false); + add(reg_src, vlen * sizeof(float)); + inc(reg_idx); + jmp(main_loop, T_NEAR); + + // --- Tail: count & 15 elements via opmask --- + L(tail_block); + and_(reg_count, 15); + jz(normal_exit, T_NEAR); + + // k_tail = (1 << reg_count) - 1, using baseline SHL (BMI2 bzhi not gated by mayiuse(avx512_core)). + // rcx is free at this point: args_t pointer was consumed in the preamble. + mov(rcx, reg_count); + mov(reg_tmp, 1); + shl(reg_tmp, cl); + dec(reg_tmp); + kmovw(k_tail, reg_tmp.cvt32()); + + // Masked zeroing-load: out-of-mask lanes read as +0.0 (benign — pass none of the predicates) + vmovups(data_vec | k_tail | T_z, zword[reg_src]); + emit_chunk(/*masked=*/true); + + // --- Normal exit: write OOR accumulator and lossy=0 --- + L(normal_exit); + mov(qword[reg_rejected_dst], reg_rejected_accum); + xor_(reg_tmp, reg_tmp); + mov(qword[reg_lossy_dst], reg_tmp); + jmp(done, T_NEAR); + + // --- Lossy exit: partial OOR count is meaningless, write 0; lossy=1 --- + L(lossy_exit); + xor_(reg_tmp, reg_tmp); + mov(qword[reg_rejected_dst], reg_tmp); + mov(reg_tmp, 1); + mov(qword[reg_lossy_dst], reg_tmp); + + L(done); + postamble(); + } +}; + +// Combined single-pass JIT kernel: counts out-of-range AND detects lossy f16 compression. +// Processes 8 floats per iteration using AVX2+F16C. Bails immediately if any in-range +// element has significant precision loss (abs error > 1.0). +class jit_check_f16_compression : public jit::Generator { +public: + typedef struct { + const void* src; + void* rejected_dst; // size_t* — output: combined out-of-range + high-relative-error count + void* lossy_dst; // size_t* — output: lossy count (0 or non-zero) + const size_t count; + float max_relative_error; + float max_abs_error; + } args_t; + + typedef void (*fn_t)(const args_t*); + + static fn_t get() { + if (is_x64() && mayiuse(jit::avx2) && mayiuse(jit::fp16)) { + static jit_check_f16_compression generator; + + // Since the JIT code always resides in memory, and ASAN's memory management may remove executable + // permissions, we need to restore executable permissions for the generated code. + generator.setProtectModeRE(false); + return generator.getCode(); + } + return nullptr; + } + +private: + jit_check_f16_compression() { + using namespace Xbyak; + + const uint32_t vlen = 8u; + + // GP register allocation + const auto& reg_src = rax; + const auto& reg_rejected_dst = rbx; + const auto& reg_lossy_dst = r12; // callee-saved, preserved by preamble + const auto& reg_sz = rdx; + const auto& reg_saved_rsp = r13; // callee-saved, for stack cleanup on lossy exit + + // YMM register allocation + // Range check constants (same as jit_count_out_of_range) + const auto& data_vec = ymm1; + const auto& mask_vec = ymm2; // OOR mask per chunk + const auto& mask_vec_xmm = xmm2; + const auto& tmp_vec = ymm3; + const auto& rejected_accum = ymm4; + const auto& rejected_accum_xmm = xmm4; + const auto& f16_max_pos_vec = ymm5; + const auto& f16_max_neg_vec = ymm6; + const auto& f16_min_pos_vec = ymm7; + const auto& f16_min_neg_vec = ymm8; + const auto& f16_zero_vec = ymm9; + const auto& i32_ones_vec = ymm10; + // Lossy check constants + const auto& abs_err_vec = ymm11; // 1.0f broadcast + const auto& abs_mask_vec = ymm0; // 0x7FFFFFFF broadcast (for fabs) + const auto& rel_err_vec = ymm12; // 1e-4f broadcast (for relative error threshold) + // Temps for lossy computation (reused per chunk) + const auto& diff_vec = ymm14; + const auto& rt_vec_xmm = xmm15; // for f32->f16->f32 roundtrip + + Label exit_lossy, exit_normal, done; + + preamble(); + mov(reg_saved_rsp, rsp); // save rsp for lossy exit stack cleanup + + // --- Load constants --- + // 256-bit (8-lane) broadcast arrays for AVX2. + // Arrays filled from FP16-derived constants (non-constexpr — see kF16* above) are static const; + // arrays filled from literal/constexpr thresholds are static constexpr. + static const float max_pos_bounds[8] = + {kF16MaxPos, kF16MaxPos, kF16MaxPos, kF16MaxPos, kF16MaxPos, kF16MaxPos, kF16MaxPos, kF16MaxPos}; + static const float max_neg_bounds[8] = + {kF16MaxNeg, kF16MaxNeg, kF16MaxNeg, kF16MaxNeg, kF16MaxNeg, kF16MaxNeg, kF16MaxNeg, kF16MaxNeg}; + static const float min_pos_bounds[8] = + {kF16MinPos, kF16MinPos, kF16MinPos, kF16MinPos, kF16MinPos, kF16MinPos, kF16MinPos, kF16MinPos}; + static const float min_neg_bounds[8] = + {kF16MinNeg, kF16MinNeg, kF16MinNeg, kF16MinNeg, kF16MinNeg, kF16MinNeg, kF16MinNeg, kF16MinNeg}; + static constexpr int32_t i32_ones[8] = {1, 1, 1, 1, 1, 1, 1, 1}; + static constexpr uint32_t abs_masks[8] = + {kAbsMaskVal, kAbsMaskVal, kAbsMaskVal, kAbsMaskVal, kAbsMaskVal, kAbsMaskVal, kAbsMaskVal, kAbsMaskVal}; + + auto load_vec = [this](Ymm vec, size_t ptr) { + mov(r15, ptr); + vmovdqu(vec, yword[r15]); + }; + + load_vec(f16_max_pos_vec, reinterpret_cast(max_pos_bounds)); + load_vec(f16_max_neg_vec, reinterpret_cast(max_neg_bounds)); + load_vec(f16_min_pos_vec, reinterpret_cast(min_pos_bounds)); + load_vec(f16_min_neg_vec, reinterpret_cast(min_neg_bounds)); + load_vec(i32_ones_vec, reinterpret_cast(i32_ones)); + load_vec(abs_mask_vec, reinterpret_cast(abs_masks)); + vxorps(f16_zero_vec, f16_zero_vec, f16_zero_vec); + vxorps(rejected_accum, rejected_accum, rejected_accum); + + // Load args + mov(reg_src, ptr[param + offsetof(args_t, src)]); + mov(reg_rejected_dst, ptr[param + offsetof(args_t, rejected_dst)]); + mov(reg_lossy_dst, ptr[param + offsetof(args_t, lossy_dst)]); + mov(reg_sz, ptr[param + offsetof(args_t, count)]); + vbroadcastss(rel_err_vec, dword[param + offsetof(args_t, max_relative_error)]); + vbroadcastss(abs_err_vec, dword[param + offsetof(args_t, max_abs_error)]); + + constexpr uint8_t cmp_lt_os = 1; + constexpr uint8_t cmp_neq_uq = 4; + constexpr uint8_t cmp_gt_os = 6; + + // Kernel lambda: emits the combined check code for 8 elements at src_ptr. + // Called twice (main loop + tail) — each call emits a copy of the instructions. + auto emit_kernel = [&](const RegExp& src_ptr) { + // Load 8 floats + vmovups(data_vec, yword[src_ptr]); + + // === Out-of-range check (same algorithm as jit_count_out_of_range_vec) === + // subnormal: data in (-f16_min_pos, f16_min_pos) and != 0 + vcmpps(tmp_vec, data_vec, f16_min_pos_vec, cmp_lt_os); + vcmpps(mask_vec, data_vec, f16_min_neg_vec, cmp_gt_os); + vandps(mask_vec, mask_vec, tmp_vec); + vcmpps(tmp_vec, data_vec, f16_zero_vec, cmp_neq_uq); + vandps(mask_vec, mask_vec, tmp_vec); + // overflow: data > f16_max or data < f16_lowest + vcmpps(tmp_vec, data_vec, f16_max_pos_vec, cmp_gt_os); + vorps(mask_vec, mask_vec, tmp_vec); + vcmpps(tmp_vec, data_vec, f16_max_neg_vec, cmp_lt_os); + vorps(mask_vec, mask_vec, tmp_vec); + // mask_vec now has per-element OOR mask (0xFFFFFFFF for OOR, 0 for in-range) + + // === Lossy check for in-range elements === + // Roundtrip: f32 -> f16 -> f32 (f16 part written to low xmm of rt_vec_xmm) + vcvtps2ph(rt_vec_xmm, data_vec, kVcvtps2phRneNoExc); // f32 -> f16 (8 values) + vcvtph2ps(diff_vec, rt_vec_xmm); // f16 -> f32 (roundtripped) + + // abs_diff = |data - roundtripped| + vsubps(diff_vec, data_vec, diff_vec); + vandps(diff_vec, diff_vec, abs_mask_vec); // diff_vec = |diff| + + // lossy = |diff| > abs_error (1.0) + vcmpps(tmp_vec, diff_vec, abs_err_vec, cmp_gt_os); // |diff| > 1.0 + + // Exclude out-of-range elements: keep only in-range lossy + vandnps(tmp_vec, mask_vec, tmp_vec); // tmp_vec = NOT(oor) AND lossy + + // Early exit if ANY element is lossy + vtestps(tmp_vec, tmp_vec); // ZF=1 if all zeros + jnz(exit_lossy, T_NEAR); // bail if any lossy + + // === Relative error check: count elements where |diff| > |value| * max_relative_error === + // (equivalent to abs_diff / |value| > max_relative_error, but avoids division) + vandps(tmp_vec, data_vec, abs_mask_vec); // tmp_vec = |data| + vmulps(tmp_vec, tmp_vec, rel_err_vec); // tmp_vec = |data| * max_relative_error + vcmpps(tmp_vec, diff_vec, tmp_vec, cmp_gt_os); // 1 where |diff| > |data| * max_relative_error + vandnps(tmp_vec, mask_vec, tmp_vec); // exclude OOR (avoid double-count) + vorps(mask_vec, mask_vec, tmp_vec); // combine OOR + relative-error + + // === Accumulate combined OOR + relative-error count === + vandps(mask_vec, mask_vec, i32_ones_vec); + vphaddd(mask_vec, mask_vec, mask_vec); + vpermq(mask_vec, mask_vec, 0x08); + vpmovsxdq(mask_vec, mask_vec_xmm); + vpaddq(rejected_accum, rejected_accum, mask_vec); + }; + + // --- Main loop: 8 elements per iteration --- + Label main_loop, main_loop_exit; + xor_(rsi, rsi); + mov(r8, reg_sz); + shr(r8, 3); + + L(main_loop); + cmp(rsi, r8); + jge(main_loop_exit, T_NEAR); + + emit_kernel(reg_src); + add(reg_src, static_cast(sizeof(float) * vlen)); + + add(rsi, 1); + jmp(main_loop, T_NEAR); + L(main_loop_exit); + + // --- Tail: remaining elements (< 8), zero-padded --- + shl(rsi, 3); + sub(reg_sz, rsi); + test(reg_sz, reg_sz); + jz(exit_normal, T_NEAR); + + sub(rsp, vlen * sizeof(float)); + mov(r8, rsp); + + vpxor(mask_vec, mask_vec, mask_vec); // zero + vmovups(yword[r8], mask_vec); // zero-pad buffer + + copy(r8, reg_src, reg_sz); // copy remaining elements + emit_kernel(r8); // process (zeros won't trigger OOR or lossy) + + add(rsp, vlen * sizeof(float)); // free stack (only reached if no lossy) + + // --- Normal exit: no lossy elements found --- + L(exit_normal); + { + // Horizontal sum of rejected_accum (4 x i64) -> single i64 + const auto& tmp0 = xmm2; // reuse mask_vec + const auto& tmp1 = xmm3; // reuse tmp_vec + vextractf128(tmp0, rejected_accum, 0); + vextractf128(tmp1, rejected_accum, 1); + vpaddq(rejected_accum_xmm, tmp0, tmp1); + vpermilpd(tmp0, rejected_accum_xmm, 0x01); + vpaddq(rejected_accum_xmm, rejected_accum_xmm, tmp0); + vmovq(qword[reg_rejected_dst], rejected_accum_xmm); + + // lossy = 0 + xor_(rsi, rsi); + mov(qword[reg_lossy_dst], rsi); + } + jmp(done, T_NEAR); + + // --- Lossy exit: bail immediately (jumped from kernel via jnz) --- + L(exit_lossy); + mov(rsp, reg_saved_rsp); // restore rsp (handles both main loop and tail cases) + xor_(rsi, rsi); + mov(qword[reg_rejected_dst], rsi); // rejected_count = 0 (meaningless, caller checks lossy first) + mov(rsi, 1); + mov(qword[reg_lossy_dst], rsi); // lossy = 1 + + L(done); + postamble(); + } }; #endif // OV_CORE_USE_XBYAK_JIT @@ -550,22 +921,83 @@ void convert_from_bf16_to_f16_with_clamp(const bfloat16* arg, float16* out, size // CVS-125496: duplicate and stub for ARM, provide optimized solution } -size_t count_out_of_f16_range(const float* arg, size_t count) { +namespace { +// Predicate: true iff the FP16 representation of `v` falls outside the finite FP16 +// normal range (subnormal when rounded, or magnitude larger than float16::max()). +// Shared between check_f16_compression() slow-path and count_out_of_f16_range(). +inline bool is_out_of_f16_range(float v) { + return (std::abs(v) < float16::from_bits(0x0001) && v != 0.0f) || v > std::numeric_limits::max() || + v < std::numeric_limits::lowest(); +} +} // namespace + +#ifdef OV_CORE_USE_XBYAK_JIT +namespace { +// Invoke a compiled FP16-compression JIT kernel (AVX-512 or AVX2+F16C) and +// marshal its two size_t outputs into a CompressionCheckResult. Shared by +// both JIT dispatches in check_f16_compression(). +template +CompressionCheckResult run_check_f16_compression_jit(typename Jit::fn_t fn, + const float* arg, + size_t count, + float max_relative_error, + float max_abs_error) { + CompressionCheckResult result{0, false}; + size_t lossy_count = 0; + typename Jit::args_t args = {arg, &result.rejected_count, &lossy_count, count, max_relative_error, max_abs_error}; + fn(&args); + result.has_lossy = lossy_count > 0; + return result; +} +} // namespace +#endif // OV_CORE_USE_XBYAK_JIT + +CompressionCheckResult check_f16_compression(const float* arg, size_t count) { + const double max_relative_error = + count == 1 ? f16_scalar_compression_max_rel_error : f16_tensor_compression_max_rel_error; + const double max_abs_error = f16_compression_max_abs_error; #ifdef OV_CORE_USE_XBYAK_JIT if (util::may_i_use_dynamic_code()) { - if (auto converter = jit_count_out_of_range::get()) { - size_t num_out_of_range = 0; - jit_count_out_of_range::args_t args = {arg, &num_out_of_range, count}; - converter(&args); - return num_out_of_range; + if (auto fn = jit_check_f16_compression_avx512::get()) { + return run_check_f16_compression_jit( + fn, + arg, + count, + static_cast(max_relative_error), + static_cast(max_abs_error)); + } + if (auto fn = jit_check_f16_compression::get()) { + return run_check_f16_compression_jit(fn, + arg, + count, + static_cast(max_relative_error), + static_cast(max_abs_error)); } } #endif // OV_CORE_USE_XBYAK_JIT - const auto is_out_of_f16_range = [](const float v) { - return (std::abs(v) < float16::from_bits(0x0001) && v != 0.0f) || (v > std::numeric_limits::max()) || - (v < std::numeric_limits::lowest()); - }; + CompressionCheckResult result{0, false}; + for (size_t i = 0; i < count; ++i) { + const float v = arg[i]; + if (is_out_of_f16_range(v)) { + ++result.rejected_count; + } else { + const double roundtripped = static_cast(static_cast(static_cast(v))); + const double abs_diff = std::abs(v - roundtripped); + if (abs_diff > max_abs_error) { + return {0, true}; + } + if (v != 0.0f && abs_diff / std::abs(v) > max_relative_error) { + ++result.rejected_count; + } + } + } + return result; +} +size_t count_out_of_f16_range(const float* arg, size_t count) { + // Backward-compatible helper: strict FP16-range count only (no relative-error accounting). + // The in-tree FP16 compression path uses check_f16_compression(); this wrapper exists for + // external developer-package consumers that linked against the pre-PR symbol. return std::count_if(arg, arg + count, is_out_of_f16_range); } diff --git a/src/core/reference/src/op/experimental_detectron_proposal_single_image.cpp b/src/core/reference/src/op/experimental_detectron_proposal_single_image.cpp index b94f2a73e2bf..50906241ed40 100644 --- a/src/core/reference/src/op/experimental_detectron_proposal_single_image.cpp +++ b/src/core/reference/src/op/experimental_detectron_proposal_single_image.cpp @@ -262,7 +262,7 @@ void experimental_detectron_proposals_single_image( img_W, min_box_H, min_box_W, - static_cast(std::log(1000. / 16.)), + static_cast(std::log(1000. / 16.)), 1.0f); std::partial_sort(proposals.begin(), proposals.begin() + pre_nms_topn, diff --git a/src/core/reference/src/op/generate_proposal.cpp b/src/core/reference/src/op/generate_proposal.cpp index c72e982a60e0..1f9a00ed4351 100644 --- a/src/core/reference/src/op/generate_proposal.cpp +++ b/src/core/reference/src/op/generate_proposal.cpp @@ -277,7 +277,7 @@ static void generate_proposals_single_image(const std::vector& im_info, img_W, min_box_H, min_box_W, - static_cast(std::log(1000. / 16.)), + static_cast(std::log(1000. / 16.)), coordinates_offset); std::partial_sort(proposals.begin(), proposals.begin() + pre_nms_topn, diff --git a/src/core/reference/src/op/pad.cpp b/src/core/reference/src/op/pad.cpp index 066f8b26e791..d7d21f46a506 100644 --- a/src/core/reference/src/op/pad.cpp +++ b/src/core/reference/src/op/pad.cpp @@ -206,5 +206,28 @@ void pad(const char* data, const op::PadMode pad_mode) { impl::pad(data, pad_value, out, elem_size, data_shape, out_shape, padding_below, padding_above, pad_mode); } + +void pad(const std::string* data, + std::string_view pad_value, + std::string* out, + const Shape& data_shape, + const Shape& out_shape, + const CoordinateDiff& padding_below, + const CoordinateDiff& padding_above) { + Coordinate in_coord(data_shape.size()); + for (const auto& out_coord : CoordinateTransformBasic{out_shape}) { + bool in_bounds = true; + for (size_t ax = 0; ax < data_shape.size(); ++ax) { + const ptrdiff_t c = static_cast(out_coord[ax]) - padding_below[ax]; + if (c < 0 || c >= static_cast(data_shape[ax])) { + in_bounds = false; + break; + } + in_coord[ax] = static_cast(c); + } + out[coordinate_index(out_coord, out_shape)] = + in_bounds ? data[coordinate_index(in_coord, data_shape)] : pad_value; + } +} } // namespace reference } // namespace ov diff --git a/src/core/reference/src/op/utils/paged_cache_manager.cpp b/src/core/reference/src/op/utils/paged_cache_manager.cpp new file mode 100644 index 000000000000..0c15b752bbff --- /dev/null +++ b/src/core/reference/src/op/utils/paged_cache_manager.cpp @@ -0,0 +1,688 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "openvino/reference/utils/paged_cache_manager.hpp" + +#include +#include +#include + +#include "openvino/core/except.hpp" + +namespace ov { +namespace reference { +namespace paged_attention_cache { + +namespace { +inline std::size_t safe_mul(std::size_t a, std::size_t b) { + if (a == 0 || b == 0) { + return 0; + } + if (a > (std::numeric_limits::max() / b)) { + OPENVINO_THROW("PagedCacheManager: tensor size overflow"); + } + return a * b; +} +} // namespace + +PagedCacheManager::PagedCacheManager(ov::element::Type elem_type, + EvictionPolicy policy, + std::size_t max_cache_bytes, + float attention_mass_p) + : m_elem_type(elem_type), + m_policy(policy), + m_max_cache_bytes(max_cache_bytes), + m_attention_mass_p(attention_mass_p) {} + +std::size_t PagedCacheManager::elem_bytes_or_throw(ov::element::Type et) { + const auto sz = et.size(); + if (sz == 0) { + OPENVINO_THROW("PagedCacheManager: unsupported element type for cache"); + } + return sz; +} + +void PagedCacheManager::validate_cache_rank4_or_throw(const ov::Shape& key_cache_shape, + const ov::Shape& value_cache_shape) { + if (key_cache_shape.size() != 4 || value_cache_shape.size() != 4) { + OPENVINO_THROW("PagedCacheManager: reference cache manager expects key_cache/value_cache rank 4 ", + "[num_blocks, num_kv_heads, block_size, head_size]. Got ranks ", + key_cache_shape.size(), + " and ", + value_cache_shape.size()); + } +} + +std::size_t PagedCacheManager::tensor_byte_size(const ov::Shape& shape, std::size_t elem_bytes) { + std::size_t prod = 1; + for (const auto d : shape) { + prod = safe_mul(prod, static_cast(d)); + } + return safe_mul(prod, elem_bytes); +} + +void PagedCacheManager::parse_cache_layout_or_throw(OperatorState& st, + const ov::Shape& key_cache_shape, + const ov::Shape& value_cache_shape, + std::size_t elem_bytes) { + validate_cache_rank4_or_throw(key_cache_shape, value_cache_shape); + + st.num_blocks = static_cast(key_cache_shape[0]); + st.num_kv_heads = static_cast(key_cache_shape[1]); + st.block_size = static_cast(value_cache_shape[2]); + st.value_head_size = static_cast(value_cache_shape[3]); + + if (value_cache_shape[0] != key_cache_shape[0] || value_cache_shape[1] != key_cache_shape[1]) { + OPENVINO_THROW("PagedCacheManager: key_cache and value_cache layout mismatch"); + } + + if (key_cache_shape[2] == value_cache_shape[2]) { + st.key_head_size = static_cast(key_cache_shape[3]); + st.key_cache_layout = KeyCacheLayout::BLOCK_MAJOR; + } else if (key_cache_shape[3] == value_cache_shape[2]) { + st.key_head_size = static_cast(key_cache_shape[2]); + st.key_cache_layout = KeyCacheLayout::HEAD_MAJOR; + } else { + OPENVINO_THROW("PagedCacheManager: unsupported key_cache layout"); + } + + if (st.num_blocks == 0 || st.num_kv_heads == 0 || st.block_size == 0 || st.key_head_size == 0 || + st.value_head_size == 0) { + OPENVINO_THROW("PagedCacheManager: cache shape has zero dimension"); + } + + st.key_block_bytes = safe_mul(safe_mul(st.num_kv_heads, st.block_size), safe_mul(st.key_head_size, elem_bytes)); + st.value_block_bytes = safe_mul(safe_mul(st.num_kv_heads, st.block_size), safe_mul(st.value_head_size, elem_bytes)); +} + +PagedCacheManager::OperatorState& PagedCacheManager::get_state(std::uintptr_t node_key) { + auto it = m_ops.find(node_key); + if (it == m_ops.end()) { + OPENVINO_THROW("PagedCacheManager: operator state not found for node_key"); + } + return it->second; +} + +const PagedCacheManager::OperatorState& PagedCacheManager::get_state(std::uintptr_t node_key) const { + auto it = m_ops.find(node_key); + if (it == m_ops.end()) { + OPENVINO_THROW("PagedCacheManager: operator state not found for node_key"); + } + return it->second; +} + +void PagedCacheManager::init_sequences_from_block_tables(OperatorState& st, + const std::int32_t* block_indices, + std::size_t block_indices_count, + const std::int32_t* block_indices_begins, + std::size_t begins_count, + const std::int32_t* past_lens, + std::size_t past_lens_count) { + const std::size_t seq_count = past_lens_count; + st.sequences.assign(seq_count, SequenceState{}); + + // If no block tables were provided, initialize empty sequences + if (block_indices == nullptr || block_indices_begins == nullptr || begins_count != (seq_count + 1)) { + for (std::size_t s = 0; s < seq_count; ++s) { + st.sequences[s].logical_length = past_lens ? past_lens[s] : 0; + } + return; + } + + // Fill per-sequence block lists based on provided tables + for (std::size_t s = 0; s < seq_count; ++s) { + const std::int32_t begin = block_indices_begins[s]; + const std::int32_t end = block_indices_begins[s + 1]; + if (begin < 0 || end < begin) { + continue; + } + const std::size_t ubegin = static_cast(begin); + const std::size_t uend = static_cast(end); + if (ubegin > block_indices_count || uend > block_indices_count) { + continue; + } + + auto& seq = st.sequences[s]; + seq.logical_length = past_lens ? past_lens[s] : 0; + + for (std::size_t i = ubegin; i < uend; ++i) { + const std::int32_t bid = block_indices[i]; + if (bid >= 0) { + seq.blocks.push_back(bid); + if (static_cast(bid) < st.block_used.size()) { + st.block_used[static_cast(bid)] = 1; + } + } + } + + // Ensure trim_front is block-aligned and within logical length + seq.trim_front = 0; + } +} + +std::int32_t PagedCacheManager::steal_block_fifo(OperatorState& st, std::size_t requester_seq) { + // Take the oldest block from the sequence with the most blocks + std::size_t best_seq = std::numeric_limits::max(); + std::size_t best_blocks = 0; + + for (std::size_t s = 0; s < st.sequences.size(); ++s) { + if (s == requester_seq) { + // prefer not to steal from the requester if possible + continue; + } + const auto& seq = st.sequences[s]; + if (seq.blocks.empty()) { + continue; + } + const std::size_t blocks = seq.blocks.size(); + if (blocks > best_blocks) { + best_blocks = blocks; + best_seq = s; + } + } + + if (best_seq == std::numeric_limits::max()) { + // as a last resort, steal from the requester itself + best_seq = requester_seq; + } + + auto& victim = st.sequences[best_seq]; + if (victim.blocks.empty()) { + OPENVINO_THROW("PagedCacheManager: cannot steal a block (no victim blocks)"); + } + + const std::int32_t bid = victim.blocks.front(); + victim.blocks.pop_front(); + if (!victim.block_scores.empty()) { + victim.block_scores.pop_front(); + } + victim.diversity_matrix.clear(); + victim.diversity_n_blocks = 0; + victim.trim_front += static_cast(st.block_size); + return bid; +} + +std::int32_t PagedCacheManager::steal_block_by_score(OperatorState& st, std::size_t requester_seq) { + // Evict the front block with the lowest attention score. + // Falls back to FIFO if no scores have been recorded yet. + std::size_t best_seq = std::numeric_limits::max(); + float best_score = std::numeric_limits::max(); + bool found_any = false; + + for (std::size_t s = 0; s < st.sequences.size(); ++s) { + auto& seq = st.sequences[s]; + if (seq.blocks.empty() || seq.block_scores.empty()) { + continue; + } + float penalty = (s == requester_seq) ? 1e12f : 0.f; + float score = seq.block_scores.front() + penalty; + if (score < best_score) { + best_score = score; + best_seq = s; + found_any = true; + } + } + + if (!found_any) { + // no scores recorded yet, fall back to FIFO + return steal_block_fifo(st, requester_seq); + } + + auto& victim = st.sequences[best_seq]; + const std::int32_t bid = victim.blocks.front(); + victim.blocks.pop_front(); + if (!victim.block_scores.empty()) { + victim.block_scores.pop_front(); + } + victim.diversity_matrix.clear(); + victim.diversity_n_blocks = 0; + victim.trim_front += static_cast(st.block_size); + return bid; +} + +std::int32_t PagedCacheManager::steal_block_by_diversity(OperatorState& st, std::size_t requester_seq) { + // Adaptive R-KV eviction: use attention-mass gating + diversity scores + // to pick the least important non-retained front block. + // Falls back to score eviction if no diversity data was fed. + + struct Candidate { + std::size_t seq; + float filtered_div; + }; + std::vector candidates; + + for (std::size_t s = 0; s < st.sequences.size(); ++s) { + auto& seq = st.sequences[s]; + if (seq.blocks.empty()) { + continue; + } + + // Need diversity matrix to participate + if (seq.diversity_matrix.empty() || seq.diversity_n_blocks == 0 || seq.diversity_evict_size == 0) { + continue; + } + + // Attention-mass gating: find which blocks are "retained" (score >= p of total) + const std::size_t n_blks = seq.diversity_n_blocks; + const std::size_t evict_sz = seq.diversity_evict_size; + const std::size_t start_blk = seq.diversity_start_block; + + // Gather per-block attention scores for the eviction zone + std::vector> scored_blocks; // (score, zone_index) + scored_blocks.reserve(n_blks); + for (std::size_t i = 0; i < n_blks; ++i) { + float sc = 0.f; + const std::size_t deque_idx = start_blk + i; + if (deque_idx < seq.block_scores.size()) { + sc = seq.block_scores[deque_idx]; + } + scored_blocks.emplace_back(sc, i); + } + + // Sort descending by score + std::sort(scored_blocks.begin(), scored_blocks.end(), [](const auto& a, const auto& b) { + return a.first > b.first; + }); + + // Greedily select blocks covering >= p fraction of total attention mass + float total_score = 0.f; + for (auto& sb : scored_blocks) + total_score += sb.first; + const float target = total_score * m_attention_mass_p; + + std::vector retained(n_blks, false); + float cumsum = 0.f; + for (auto& sb : scored_blocks) { + if (cumsum >= target && target > 0.f) + break; + retained[sb.second] = true; + cumsum += sb.first; + } + + // Check if front block is retained (skip if so) + bool front_is_retained = false; + if (start_blk == 0 && n_blks > 0) { + front_is_retained = retained[0]; + } + + // If the front block is retained (attention-important), skip this sequence + if (front_is_retained) { + continue; + } + + // Compute filtered diversity for the front block (mean over retained columns) + float div_score = 0.f; + if (start_blk == 0 && n_blks > 0) { + // Front block is zone block 0; its row is diversity_matrix[0, :]. + // Mean over columns corresponding to retained blocks' tokens only + float sum = 0.f; + std::size_t count = 0; + for (std::size_t bi = 0; bi < n_blks; ++bi) { + if (!retained[bi]) + continue; + // Columns for block bi: [bi * block_size, (bi+1) * block_size) + const std::size_t col_start = bi * st.block_size; + const std::size_t col_end = std::min(col_start + st.block_size, evict_sz); + for (std::size_t c = col_start; c < col_end; ++c) { + if (c < evict_sz) { + sum += seq.diversity_matrix[0 * evict_sz + c]; + count++; + } + } + } + div_score = (count > 0) ? (sum / static_cast(count)) : 0.f; + } + + float penalty = (s == requester_seq) ? 1e12f : 0.f; + candidates.push_back({s, div_score + penalty}); + } + + if (candidates.empty()) { + // no diversity data available, fall back to score eviction + return steal_block_by_score(st, requester_seq); + } + + // Pick candidate with lowest filtered diversity + std::size_t best_seq = candidates[0].seq; + float best_div = candidates[0].filtered_div; + for (std::size_t i = 1; i < candidates.size(); ++i) { + if (candidates[i].filtered_div < best_div) { + best_div = candidates[i].filtered_div; + best_seq = candidates[i].seq; + } + } + + auto& victim = st.sequences[best_seq]; + const std::int32_t bid = victim.blocks.front(); + victim.blocks.pop_front(); + if (!victim.block_scores.empty()) { + victim.block_scores.pop_front(); + } + victim.diversity_matrix.clear(); + victim.diversity_n_blocks = 0; + victim.trim_front += static_cast(st.block_size); + return bid; +} + +std::int32_t PagedCacheManager::steal_block(OperatorState& st, std::size_t requester_seq) { + switch (m_policy) { + case EvictionPolicy::SCORE: + return steal_block_by_score(st, requester_seq); + case EvictionPolicy::ADAPTIVE_RKV: + return steal_block_by_diversity(st, requester_seq); + case EvictionPolicy::FIFO: + default: + return steal_block_fifo(st, requester_seq); + } +} + +std::int32_t PagedCacheManager::allocate_block(OperatorState& st, std::size_t requester_seq) { + if (m_max_cache_bytes > 0) { + const std::size_t bytes_per_block = st.key_block_bytes + st.value_block_bytes; + const std::size_t max_blocks = + (bytes_per_block > 0) ? std::max(1, m_max_cache_bytes / bytes_per_block) : st.num_blocks; + const std::size_t active = st.num_blocks - st.free_blocks.size(); + if (active >= max_blocks) { + return steal_block(st, requester_seq); + } + } + + if (!st.free_blocks.empty()) { + const std::int32_t bid = st.free_blocks.back(); + st.free_blocks.pop_back(); + if (bid >= 0 && static_cast(bid) < st.block_used.size()) { + st.block_used[static_cast(bid)] = 1; + } + return bid; + } + // No free blocks left: evict one block from a victim sequence + return steal_block(st, requester_seq); +} + +bool PagedCacheManager::ensure_operator(std::uintptr_t node_key, + const void* key_cache_init, + const void* value_cache_init, + const ov::Shape& key_cache_shape, + const ov::Shape& value_cache_shape, + const std::int32_t* block_indices_init, + std::size_t block_indices_count, + const std::int32_t* block_indices_begins_init, + std::size_t block_indices_begins_count, + const std::int32_t* past_lens_init, + std::size_t past_lens_count) { + if (m_ops.find(node_key) != m_ops.end()) { + return false; + } + + OperatorState st; + const std::size_t elem_bytes = elem_bytes_or_throw(m_elem_type); + parse_cache_layout_or_throw(st, key_cache_shape, value_cache_shape, elem_bytes); + + // Allocate and copy initial cache tensors (once). + const std::size_t key_bytes = tensor_byte_size(key_cache_shape, elem_bytes); + const std::size_t value_bytes = tensor_byte_size(value_cache_shape, elem_bytes); + st.key_cache = ov::AlignedBuffer{key_bytes}; + st.value_cache = ov::AlignedBuffer{value_bytes}; + + if (key_cache_init) { + if (st.key_cache_layout == KeyCacheLayout::BLOCK_MAJOR) { + std::memcpy(st.key_cache.get_ptr(), key_cache_init, key_bytes); + } else { + auto* dst = static_cast(st.key_cache.get_ptr()); + const auto* src = static_cast(key_cache_init); + for (std::size_t b = 0; b < st.num_blocks; ++b) { + for (std::size_t h = 0; h < st.num_kv_heads; ++h) { + const std::size_t src_head_base = (b * st.num_kv_heads + h) * st.key_head_size * st.block_size; + const std::size_t dst_head_base = (b * st.num_kv_heads + h) * st.block_size * st.key_head_size; + for (std::size_t t = 0; t < st.block_size; ++t) { + for (std::size_t d = 0; d < st.key_head_size; ++d) { + const std::size_t src_elem = src_head_base + d * st.block_size + t; + const std::size_t dst_elem = dst_head_base + t * st.key_head_size + d; + std::memcpy(dst + dst_elem * elem_bytes, src + src_elem * elem_bytes, elem_bytes); + } + } + } + } + } + } else { + std::memset(st.key_cache.get_ptr(), 0, key_bytes); + } + + if (value_cache_init) { + std::memcpy(st.value_cache.get_ptr(), value_cache_init, value_bytes); + } else { + std::memset(st.value_cache.get_ptr(), 0, value_bytes); + } + + // Initialize free list and per-seq state + st.block_used.assign(st.num_blocks, 0); + + init_sequences_from_block_tables(st, + block_indices_init, + block_indices_count, + block_indices_begins_init, + block_indices_begins_count, + past_lens_init, + past_lens_count); + + // Build free block stack + st.free_blocks.reserve(st.num_blocks); + for (std::size_t b = 0; b < st.num_blocks; ++b) { + if (!st.block_used[b]) { + st.free_blocks.push_back(static_cast(b)); + } + } + + m_ops.emplace(node_key, std::move(st)); + return true; +} + +void PagedCacheManager::begin_step(std::uintptr_t node_key, const std::int32_t* past_lens, std::size_t seq_count) { + auto& st = get_state(node_key); + if (seq_count == 0) { + OPENVINO_THROW("PagedCacheManager::begin_step: seq_count is 0"); + } + if (st.sequences.size() != seq_count) { + // For reference: allow resizing on first call (or if graph shape changes) + st.sequences.assign(seq_count, SequenceState{}); + } + + for (std::size_t s = 0; s < seq_count; ++s) { + const std::int32_t new_len = past_lens ? past_lens[s] : 0; + auto& seq = st.sequences[s]; + + // If the external timeline was reset/truncated, reset this sequence state + if (new_len < seq.logical_length) { + for (const std::int32_t bid : seq.blocks) { + if (bid >= 0 && static_cast(bid) < st.num_blocks && st.block_used[bid]) { + st.block_used[bid] = 0; + st.free_blocks.push_back(bid); + } + } + seq.blocks.clear(); + seq.block_scores.clear(); + seq.diversity_matrix.clear(); + seq.diversity_n_blocks = 0; + seq.diversity_evict_size = 0; + seq.diversity_start_block = 0; + seq.trim_front = 0; + seq.logical_length = new_len; + continue; + } + + seq.logical_length = new_len; + + // If past_lens exceeds retained blocks, the caller will append and ensure_token will allocate + if (seq.trim_front > seq.logical_length) { + seq.trim_front = std::max(0, seq.logical_length - (std::int32_t)(st.block_size)); + seq.trim_front = (seq.trim_front / (std::int32_t)st.block_size) * (std::int32_t)st.block_size; + } + } +} + +PagedCacheManager::TokenAddress PagedCacheManager::ensure_token(std::uintptr_t node_key, + std::size_t seq_idx, + std::int32_t token_pos) { + auto& st = get_state(node_key); + if (seq_idx >= st.sequences.size()) { + OPENVINO_THROW("PagedCacheManager::ensure_token: seq_idx out of range"); + } + + auto& seq = st.sequences[seq_idx]; + + if (token_pos < 0) { + OPENVINO_THROW("PagedCacheManager::ensure_token: token_pos is negative"); + } + + // If token_pos is behind the trimmed window, it cannot be represented + if (token_pos < seq.trim_front) { + return TokenAddress{-1, 0}; + } + + const std::int32_t bs = static_cast(st.block_size); + // Recompute relative position after any evictions that may have advanced trim_front + // This is needed because stealing from the requester's own front block shifts trim_front + auto recompute = [&]() -> std::pair { + const std::int32_t r = token_pos - seq.trim_front; + const std::int32_t bi = (bs > 0) ? (r / bs) : 0; + return {bi, (bs > 0) ? (r % bs) : 0}; + }; + + auto [block_index, off] = recompute(); + + // guard against infinite loops if the pool is too small for the requested position + std::size_t max_iters = st.num_blocks + 1; + while (seq.blocks.size() <= static_cast(block_index) && max_iters-- > 0) { + const std::int32_t bid = allocate_block(st, seq_idx); + seq.blocks.push_back(bid); + // trim_front may have changed if we stole from ourselves + std::tie(block_index, off) = recompute(); + } + + if (seq.blocks.size() <= static_cast(block_index)) { + return TokenAddress{-1, 0}; + } + + return TokenAddress{seq.blocks[static_cast(block_index)], off}; +} + +bool PagedCacheManager::resolve_token(std::uintptr_t node_key, + std::size_t seq_idx, + std::int32_t token_pos, + TokenAddress& out_addr) const { + const auto& st = get_state(node_key); + if (seq_idx >= st.sequences.size()) { + return false; + } + const auto& seq = st.sequences[seq_idx]; + + if (token_pos < seq.trim_front || token_pos < 0 || token_pos >= seq.logical_length) { + return false; + } + + const std::int32_t rel = token_pos - seq.trim_front; + const std::int32_t bs = static_cast(st.block_size); + if (bs <= 0) { + return false; + } + + const std::int32_t block_index = rel / bs; + const std::int32_t off = rel % bs; + + if (block_index < 0 || static_cast(block_index) >= seq.blocks.size()) { + return false; + } + + out_addr.block = seq.blocks[static_cast(block_index)]; + out_addr.offset = off; + return out_addr.block >= 0; +} + +std::size_t PagedCacheManager::num_blocks(std::uintptr_t node_key) const { + return get_state(node_key).num_blocks; +} + +std::size_t PagedCacheManager::block_size(std::uintptr_t node_key) const { + return get_state(node_key).block_size; +} + +std::size_t PagedCacheManager::num_kv_heads(std::uintptr_t node_key) const { + return get_state(node_key).num_kv_heads; +} + +std::size_t PagedCacheManager::key_head_size(std::uintptr_t node_key) const { + return get_state(node_key).key_head_size; +} + +std::size_t PagedCacheManager::value_head_size(std::uintptr_t node_key) const { + return get_state(node_key).value_head_size; +} + +void PagedCacheManager::update_attention_scores(std::uintptr_t node_key, + const float* scores, + std::size_t scores_len, + const std::int32_t* past_lens, + std::size_t seq_count) { + auto& st = get_state(node_key); + if (seq_count > st.sequences.size()) { + seq_count = st.sequences.size(); + } + const std::int32_t bs = static_cast(st.block_size); + if (bs <= 0 || scores == nullptr || scores_len == 0) { + return; + } + + // scores layout: flat [past_lens[0]+new_0, past_lens[1]+new_1, ...] + // accumulate per-token scores into per-block sums + std::size_t offset = 0; + for (std::size_t s = 0; s < seq_count; ++s) { + auto& seq = st.sequences[s]; + const std::size_t total_len = (past_lens && s < seq_count) ? static_cast(seq.logical_length) : 0; + if (total_len == 0 || seq.blocks.empty()) { + offset += total_len; + continue; + } + + // make sure block_scores has the right size + seq.block_scores.resize(seq.blocks.size(), 0.f); + + // accumulate per-token attention scores into per-block sums + for (std::size_t t = 0; t < total_len && (offset + t) < scores_len; ++t) { + const std::int32_t token_pos = static_cast(t); + if (token_pos < seq.trim_front) { + continue; + } + const std::int32_t rel = token_pos - seq.trim_front; + const std::size_t block_idx = static_cast(rel / bs); + if (block_idx < seq.block_scores.size()) { + seq.block_scores[block_idx] += scores[offset + t]; + } + } + offset += total_len; + } +} + +void PagedCacheManager::update_diversity_scores(std::uintptr_t node_key, + std::size_t seq_idx, + const float* diversity_matrix, + std::size_t num_blocks_in_zone, + std::size_t eviction_size, + std::size_t start_block_offset) { + auto& st = get_state(node_key); + if (seq_idx >= st.sequences.size() || diversity_matrix == nullptr || num_blocks_in_zone == 0 || + eviction_size == 0) { + return; + } + + auto& seq = st.sequences[seq_idx]; + const std::size_t total = num_blocks_in_zone * eviction_size; + seq.diversity_matrix.assign(diversity_matrix, diversity_matrix + total); + seq.diversity_n_blocks = num_blocks_in_zone; + seq.diversity_evict_size = eviction_size; + seq.diversity_start_block = start_block_offset; +} + +} // namespace paged_attention_cache +} // namespace reference +} // namespace ov diff --git a/src/core/shape_inference/include/element_visitor.hpp b/src/core/shape_inference/include/element_visitor.hpp index b9f1ebde1b08..59c8e7a1dd15 100644 --- a/src/core/shape_inference/include/element_visitor.hpp +++ b/src/core/shape_inference/include/element_visitor.hpp @@ -176,7 +176,7 @@ bool is_type_list_not_empty(Args&&... args) { * OpenVINO conditional compilation feature. * * @param region Region name for ITT which will be combined with TYPE_LIST_ prefix. - * @param types List ov::element IfTypeOf class e.g. OV_PP_ET_LIST(f16, i8) to pack as one paramater. + * @param types List ov::element IfTypeOf class e.g. OV_PP_ET_LIST(f16, i8) to pack as one parameter. * @param visitor Class name of visitor which will be used by IfTypeOf::visit(_VA_ARGS_) function. * @param ... List of parameters must match parameter list of `visit` function. * diff --git a/src/core/shape_inference/include/gated_delta_net_shape_inference.hpp b/src/core/shape_inference/include/gated_delta_net_shape_inference.hpp index a54773d1852d..238b5c7c2ec1 100644 --- a/src/core/shape_inference/include/gated_delta_net_shape_inference.hpp +++ b/src/core/shape_inference/include/gated_delta_net_shape_inference.hpp @@ -31,8 +31,8 @@ std::vector shape_infer(const GatedDeltaNet* op, const std::vector& NODE_SHAPE_INFER_CHECK(op, input_shapes, - q_head_num.compatible(k_head_num) && q_head_num.compatible(v_head_num), - "The number of heads in query key and value should be the same, but got ", + q_head_num.compatible(k_head_num), + "The number of heads in query and key should be the same, but got ", q_head_num, " and ", k_head_num); @@ -50,8 +50,8 @@ std::vector shape_infer(const GatedDeltaNet* op, const std::vector& NODE_SHAPE_INFER_CHECK(op, input_shapes, - gate_head_num.compatible(beta_head_num) && gate_head_num.compatible(q_head_num), - "The number of heads in gate, beta, and query should be the same, but got ", + gate_head_num.compatible(beta_head_num) && gate_head_num.compatible(v_head_num), + "The number of heads in gate, beta, and value should be the same, but got ", gate_head_num, " and ", beta_head_num); diff --git a/src/core/shape_inference/include/pa_kv_reorder_shape_inference.hpp b/src/core/shape_inference/include/pa_kv_reorder_shape_inference.hpp new file mode 100644 index 000000000000..a4bbca3f45f6 --- /dev/null +++ b/src/core/shape_inference/include/pa_kv_reorder_shape_inference.hpp @@ -0,0 +1,23 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#pragma once + +#include "openvino/op/pa_kv_reorder.hpp" +#include "utils.hpp" + +namespace ov::op::internal { +template > +std::vector shape_infer(const PaKVReorder* op, const std::vector& input_shapes) { + NODE_VALIDATION_CHECK(op, input_shapes.size() == 6); + + NODE_SHAPE_INFER_CHECK(op, input_shapes, input_shapes[0].rank().compatible(4)); + NODE_SHAPE_INFER_CHECK(op, input_shapes, input_shapes[1].rank().compatible(4)); + for (size_t i = 2; i < 6; ++i) { + NODE_SHAPE_INFER_CHECK(op, input_shapes, input_shapes[i].rank().compatible(1)); + } + + return {TRShape{1}}; +} +} // namespace ov::op::internal diff --git a/src/core/shape_inference/include/paged_attention_shape_inference.hpp b/src/core/shape_inference/include/paged_attention_shape_inference.hpp new file mode 100644 index 000000000000..ebf81e874e3f --- /dev/null +++ b/src/core/shape_inference/include/paged_attention_shape_inference.hpp @@ -0,0 +1,140 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#pragma once + +#include + +#include "dimension_util.hpp" +#include "openvino/op/paged_attention.hpp" +#include "utils.hpp" + +namespace ov { +namespace op { +template > +std::vector shape_infer(const PagedAttentionExtension* op, + const std::vector& input_shapes, + const ITensorAccessor& ta = make_tensor_accessor()) { + NODE_VALIDATION_CHECK(op, input_shapes.size() == 28, "Expected 28 inputs but got ", input_shapes.size()); + auto output_shapes = std::vector(3); + + // Output[0] feature dim is num_heads * v_head_size. + // Derived as Q*V/K which cancels the shared Hkv*Dk. + auto out_ps = input_shapes[0]; + const auto& key_ps = input_shapes[1]; + const auto& value_ps = input_shapes[2]; + const auto& past_lens_ps = input_shapes[5]; + const auto& evictable_sizes_ps = input_shapes[22]; + + // Compute for output shape + if (out_ps.rank().is_static() && out_ps.rank().get_length() >= 2 && out_ps[1].is_static()) { + if (key_ps.rank().is_static() && key_ps.rank().get_length() >= 2 && key_ps[1].is_static() && + value_ps.rank().is_static() && value_ps.rank().get_length() >= 2 && value_ps[1].is_static()) { + const auto q = out_ps[1].get_length(); + const auto k = key_ps[1].get_length(); + const auto v = value_ps[1].get_length(); + NODE_VALIDATION_CHECK(op, + k != 0 && q * v % k == 0, + "Output feature dimension (q * v) must be divisible by the key " + "feature dimension (k); got q=", + q, + ", v=", + v, + ", k=", + k); + out_ps[1] = q * v / k; + NODE_VALIDATION_CHECK(op, + !ov::util::dim::is_empty(out_ps[1]), + "The last dimension of output should not be empty."); + } else { + out_ps[1] = Dimension::dynamic(); + } + } + output_shapes[0] = out_ps; + + auto& scores_ps = output_shapes[1]; + // Compute for scores shape + if (past_lens_ps.rank().is_static() && key_ps.rank().is_static() && key_ps.rank().get_length() >= 1 && + key_ps[0].is_static()) { + const auto& past_lens = get_input_const_data_as(op, 5, ta); + if (past_lens.has_value()) { + const auto token_count = key_ps[0].get_length(); + const auto past_sum = std::accumulate(past_lens.value().begin(), past_lens.value().end(), 0); + const auto computed_dim = token_count + past_sum; + scores_ps.push_back(computed_dim); + } else { + scores_ps.push_back(Dimension::dynamic()); + } + } else { + scores_ps.push_back(Dimension::dynamic()); + } + + // Output[2]: flat 1D Adaptive-RKV diversity scores. + // Size = sum(evictable_sizes[s]^2 / adaptive_rkv_block_size); dynamic if unknown. + auto& diversity_ps = output_shapes[2]; + auto width_dim = Dimension::dynamic(); + + const auto& key_cache_ps = input_shapes[3]; // [num_blocks, Hkv, block_size, S] + const bool block_size_known = + key_cache_ps.rank().is_static() && key_cache_ps.rank().get_length() >= 3 && key_cache_ps[2].is_static(); + + auto infer_adaptive_rkv_diversity_block_size = [&]() -> std::optional { + if (!block_size_known) { + return std::nullopt; + } + + int64_t block_size = key_cache_ps[2].get_length(); + if (!(key_cache_ps.rank().is_static() && key_cache_ps.rank().get_length() >= 4 && key_cache_ps[1].is_static() && + key_cache_ps[3].is_static() && key_ps.rank().is_static() && key_ps.rank().get_length() >= 2 && + key_ps[1].is_static())) { + return block_size; + } + + const auto key_cache_et = op->get_input_element_type(3); + const auto key_cache_bitwidth = key_cache_et.bitwidth(); + if (key_cache_bitwidth != 8 && key_cache_bitwidth != 4) { + return block_size; + } + + const int64_t hkv = key_cache_ps[1].get_length(); + const int64_t key_feature_size = key_ps[1].get_length(); + if (hkv == 0 || key_feature_size % hkv != 0) { + return block_size; + } + + const int64_t head_size = key_feature_size / hkv; + const bool by_channel_layout = key_cache_ps[3].get_length() == head_size; + if (!by_channel_layout) { + return block_size; + } + + const int64_t params_count = (key_cache_et == element::i8 || key_cache_et == element::i4) ? 1 : 2; + const int64_t sub_byte_multiplier = 8 / key_cache_bitwidth; + return block_size - static_cast(sizeof(float)) * params_count * sub_byte_multiplier; + }; + + if (block_size_known && evictable_sizes_ps.rank().is_static() && evictable_sizes_ps.rank().get_length() == 1) { + const auto& evictable_sizes = get_input_const_data_as(op, 22, ta); + if (evictable_sizes.has_value() && !evictable_sizes.value().empty()) { + const auto inferred_block_size = infer_adaptive_rkv_diversity_block_size(); + if (!inferred_block_size.has_value() || inferred_block_size.value() <= 0) { + diversity_ps.push_back(width_dim); + return output_shapes; + } + const int64_t block_size = inferred_block_size.value(); + int64_t total = 0; + for (const auto es : evictable_sizes.value()) { + total += static_cast(es) * static_cast(es) / block_size; + } + width_dim = total; + } + } + + diversity_ps.push_back(width_dim); + + return output_shapes; +} + +} // namespace op +} // namespace ov diff --git a/src/core/shape_inference/include/paged_causal_conv1d_shape_inference.hpp b/src/core/shape_inference/include/paged_causal_conv1d_shape_inference.hpp new file mode 100644 index 000000000000..ff681e63280b --- /dev/null +++ b/src/core/shape_inference/include/paged_causal_conv1d_shape_inference.hpp @@ -0,0 +1,94 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#pragma once + +#include "openvino/op/paged_causal_conv1d.hpp" +#include "utils.hpp" + +namespace ov::op::internal { +template > +std::vector shape_infer(const PagedCausalConv1D* op, const std::vector& input_shapes) { + NODE_VALIDATION_CHECK(op, input_shapes.size() == 9); + + NODE_SHAPE_INFER_CHECK(op, input_shapes, input_shapes[0].rank().compatible(2)); + NODE_SHAPE_INFER_CHECK(op, input_shapes, input_shapes[1].rank().compatible(3)); + NODE_SHAPE_INFER_CHECK(op, input_shapes, input_shapes[2].rank().compatible(3)); + NODE_SHAPE_INFER_CHECK(op, input_shapes, input_shapes[3].rank().compatible(1)); + NODE_SHAPE_INFER_CHECK(op, input_shapes, input_shapes[4].rank().compatible(1)); + NODE_SHAPE_INFER_CHECK(op, input_shapes, input_shapes[5].rank().compatible(1)); + NODE_SHAPE_INFER_CHECK(op, input_shapes, input_shapes[6].rank().compatible(1)); + NODE_SHAPE_INFER_CHECK(op, input_shapes, input_shapes[7].rank().compatible(1)); + NODE_SHAPE_INFER_CHECK(op, input_shapes, input_shapes[8].rank().compatible(1)); + + const auto input_embeds_rank_is_static = input_shapes[0].rank().is_static(); + const auto conv_state_table_rank_is_static = input_shapes[1].rank().is_static(); + const auto conv_weight_rank_is_static = input_shapes[2].rank().is_static(); + const auto conv_bias_rank_is_static = input_shapes[3].rank().is_static(); + const auto subsequence_begins_rank_is_static = input_shapes[4].rank().is_static(); + const auto la_block_indices_begins_rank_is_static = input_shapes[6].rank().is_static(); + const auto processed_tokens_rank_is_static = input_shapes[7].rank().is_static(); + const auto cache_interval_rank_is_static = input_shapes[8].rank().is_static(); + + if (input_embeds_rank_is_static && conv_state_table_rank_is_static) { + NODE_SHAPE_INFER_CHECK(op, + input_shapes, + input_shapes[0][1].compatible(input_shapes[1][1]), + "The hidden_size dimensions of input_embeds and conv_state_table inputs must be " + "compatible."); + } + + if (conv_state_table_rank_is_static && conv_weight_rank_is_static) { + NODE_SHAPE_INFER_CHECK(op, + input_shapes, + input_shapes[1][2].compatible(input_shapes[2][2]), + "The kernel_size dimensions of conv_state_table and conv_weight inputs must be " + "compatible."); + } + + if (input_embeds_rank_is_static && conv_weight_rank_is_static) { + NODE_SHAPE_INFER_CHECK(op, + input_shapes, + input_shapes[2][0].compatible(input_shapes[0][1]), + "The out_channels dimension of conv_weight must be compatible with the hidden_size " + "dimension of input_embeds."); + } + + if (conv_bias_rank_is_static && conv_weight_rank_is_static) { + NODE_SHAPE_INFER_CHECK( + op, + input_shapes, + input_shapes[3][0].compatible(input_shapes[2][0]) || input_shapes[3][0].compatible(ov::Dimension(0)), + "The size of conv_bias must be compatible with the out_channels dimension of " + "conv_weight or equal to 0 (no bias)."); + } + + if (subsequence_begins_rank_is_static && la_block_indices_begins_rank_is_static) { + NODE_SHAPE_INFER_CHECK(op, + input_shapes, + input_shapes[4][0].compatible(input_shapes[6][0]), + "The size of subsequence_begins must be compatible with the size of " + "la_block_indices_begins."); + } + + if (subsequence_begins_rank_is_static && processed_tokens_rank_is_static) { + NODE_SHAPE_INFER_CHECK(op, + input_shapes, + (input_shapes[7][0] + 1).compatible(input_shapes[4][0]), + "The size of processed_tokens must be batch_size_in_sequences (subsequence_begins " + "size - 1)."); + } + + if (subsequence_begins_rank_is_static && cache_interval_rank_is_static) { + NODE_SHAPE_INFER_CHECK(op, + input_shapes, + (input_shapes[8][0] + 1).compatible(input_shapes[4][0]), + "The size of cache_interval must be batch_size_in_sequences (subsequence_begins " + "size - 1)."); + } + + // output_embeds has the same shape as input_embeds: [batch_size_in_tokens, hidden_size] + return {input_shapes[0]}; +} +} // namespace ov::op::internal \ No newline at end of file diff --git a/src/core/shape_inference/include/paged_gated_delta_net_shape_inference.hpp b/src/core/shape_inference/include/paged_gated_delta_net_shape_inference.hpp new file mode 100644 index 000000000000..2b22249eb9d7 --- /dev/null +++ b/src/core/shape_inference/include/paged_gated_delta_net_shape_inference.hpp @@ -0,0 +1,88 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#pragma once + +#include "openvino/op/paged_gated_delta_net.hpp" +#include "utils.hpp" + +namespace ov::op::internal { + +template > +std::vector shape_infer(const PagedGatedDeltaNet* op, const std::vector& input_shapes) { + NODE_VALIDATION_CHECK(op, input_shapes.size() == 11); + + NODE_SHAPE_INFER_CHECK(op, input_shapes, input_shapes[0].rank().compatible(3)); + NODE_SHAPE_INFER_CHECK(op, input_shapes, input_shapes[1].rank().compatible(3)); + NODE_SHAPE_INFER_CHECK(op, input_shapes, input_shapes[2].rank().compatible(3)); + NODE_SHAPE_INFER_CHECK(op, input_shapes, input_shapes[3].rank().compatible(4)); + NODE_SHAPE_INFER_CHECK(op, input_shapes, input_shapes[4].rank().compatible(2)); + NODE_SHAPE_INFER_CHECK(op, input_shapes, input_shapes[5].rank().compatible(2)); + NODE_SHAPE_INFER_CHECK(op, input_shapes, input_shapes[6].rank().compatible(1)); + NODE_SHAPE_INFER_CHECK(op, input_shapes, input_shapes[7].rank().compatible(1)); + NODE_SHAPE_INFER_CHECK(op, input_shapes, input_shapes[8].rank().compatible(1)); + NODE_SHAPE_INFER_CHECK(op, input_shapes, input_shapes[9].rank().compatible(1)); + NODE_SHAPE_INFER_CHECK(op, input_shapes, input_shapes[10].rank().compatible(1)); + + // [batch_size_in_tokens, num_heads, key_head_dim] + const auto& query_ps = input_shapes[0]; + const auto& key_ps = input_shapes[1]; + // [batch_size_in_tokens, v_num_heads, value_head_dim] + const auto& value_ps = input_shapes[2]; + // [num_blocks, v_num_heads, value_head_dim, key_head_dim] + const auto& state_ps = input_shapes[3]; + // [batch_size_in_tokens, v_num_heads] + const auto& gate_ps = input_shapes[4]; + const auto& beta_ps = input_shapes[5]; + + const auto query_rank_is_static = query_ps.rank().is_static(); + const auto key_rank_is_static = key_ps.rank().is_static(); + const auto value_rank_is_static = value_ps.rank().is_static(); + const auto state_rank_is_static = state_ps.rank().is_static(); + const auto gate_rank_is_static = gate_ps.rank().is_static(); + const auto beta_rank_is_static = beta_ps.rank().is_static(); + + if (query_rank_is_static && key_rank_is_static) { + NODE_SHAPE_INFER_CHECK(op, + input_shapes, + query_ps[1].compatible(key_ps[1]), + "The number of heads in query and key inputs must be equal."); + + NODE_SHAPE_INFER_CHECK(op, + input_shapes, + key_ps[2].compatible(query_ps[2]), + "The head size of query and key inputs must be equal."); + } + + if (value_rank_is_static && gate_rank_is_static && beta_rank_is_static) { + NODE_SHAPE_INFER_CHECK(op, + input_shapes, + gate_ps[1].compatible(value_ps[1]) && beta_ps[1].compatible(value_ps[1]), + "The number of heads in gate, beta, and value inputs must be equal."); + } + + if (state_rank_is_static && value_rank_is_static && key_rank_is_static) { + // [num_blocks, v_num_heads, value_head_dim, key_head_dim] + NODE_SHAPE_INFER_CHECK(op, + input_shapes, + state_ps[1].compatible(value_ps[1]), + "The number of heads in recurrent_state_table and value inputs must be equal."); + + NODE_SHAPE_INFER_CHECK( + op, + input_shapes, + state_ps[2].compatible(value_ps[2]), + "The value dimension of recurrent_state_table and the head size of value input must be equal."); + + NODE_SHAPE_INFER_CHECK( + op, + input_shapes, + state_ps[3].compatible(key_ps[2]), + "The key dimension of recurrent_state_table and the head size of key input must be equal."); + } + + // output: [batch_size_in_tokens, v_num_heads, value_head_dim] — same shape as value input + return {value_ps}; +} +} // namespace ov::op::internal diff --git a/src/core/src/axis_set.cpp b/src/core/src/axis_set.cpp index b8ffe05ed17c..9b72785b6f8b 100644 --- a/src/core/src/axis_set.cpp +++ b/src/core/src/axis_set.cpp @@ -31,10 +31,7 @@ std::vector ov::AxisSet::to_vector() const { } std::ostream& ov::operator<<(std::ostream& s, const AxisSet& axis_set) { - s << "AxisSet{"; - s << ov::util::join(axis_set); - s << "}"; - return s; + return s << "AxisSet{" << ov::util::join(axis_set) << "}"; } const std::vector& ov::AttributeAdapter::get() { diff --git a/src/core/src/axis_vector.cpp b/src/core/src/axis_vector.cpp index 661cf66bb299..04d7568cb3ae 100644 --- a/src/core/src/axis_vector.cpp +++ b/src/core/src/axis_vector.cpp @@ -7,10 +7,7 @@ #include "openvino/util/common_util.hpp" std::ostream& ov::operator<<(std::ostream& s, const AxisVector& axis_vector) { - s << "AxisVector{"; - s << ov::util::join(axis_vector); - s << "}"; - return s; + return s << "AxisVector{" << ov::util::join(axis_vector) << "}"; } ov::AxisVector::AxisVector(const std::initializer_list& axes) : std::vector(axes) {} diff --git a/src/core/src/coordinate.cpp b/src/core/src/coordinate.cpp index d4a2955887ab..fc3d4ae1490c 100644 --- a/src/core/src/coordinate.cpp +++ b/src/core/src/coordinate.cpp @@ -7,10 +7,7 @@ #include "openvino/util/common_util.hpp" std::ostream& ov::operator<<(std::ostream& s, const Coordinate& coordinate) { - s << "Coordinate{"; - s << ov::util::join(coordinate); - s << "}"; - return s; + return s << "Coordinate{" << ov::util::join(coordinate) << "}"; } ov::Coordinate::Coordinate() = default; diff --git a/src/core/src/coordinate_diff.cpp b/src/core/src/coordinate_diff.cpp index 3b2c92914f66..20c625e25c32 100644 --- a/src/core/src/coordinate_diff.cpp +++ b/src/core/src/coordinate_diff.cpp @@ -7,10 +7,7 @@ #include "openvino/util/common_util.hpp" std::ostream& ov::operator<<(std::ostream& s, const CoordinateDiff& coordinate_diff) { - s << "CoordinateDiff{"; - s << ov::util::join(coordinate_diff); - s << "}"; - return s; + return s << "CoordinateDiff{" << ov::util::join(coordinate_diff) << "}"; } ov::CoordinateDiff::CoordinateDiff(const std::initializer_list& diffs) diff --git a/src/core/src/descriptor/tensor.cpp b/src/core/src/descriptor/tensor.cpp index 4ee5ad4c973d..0aa4cc3ce50c 100644 --- a/src/core/src/descriptor/tensor.cpp +++ b/src/core/src/descriptor/tensor.cpp @@ -261,7 +261,7 @@ void copy_tensor_names(Tensor& dst, const Tensor& src) { } std::ostream& operator<<(std::ostream& out, const Tensor& tensor) { - out << "Tensor(" << util::join(tensor.get_names()) << ")"; + out << "Tensor(" << util::join(tensor.get_names()) << ")"; return out; } } // namespace descriptor diff --git a/src/core/src/dimension.cpp b/src/core/src/dimension.cpp index a10caee1eaf4..5137b9b1e865 100644 --- a/src/core/src/dimension.cpp +++ b/src/core/src/dimension.cpp @@ -41,22 +41,10 @@ std::shared_ptr merge_symbols(std::shared_ptr lhs, return nullptr; } -int64_t stringToInt64(const std::string& valStr) { - int64_t ret{0}; - std::istringstream ss(valStr); - if (!ss.eof()) { - ss >> ret; - } - return ret; -} - -bool check_all_digits(const std::string& value) { - auto val = ov::util::trim(value); - for (const auto& c : val) { - if (!std::isdigit(c) || c == '-') - return false; - } - return true; +bool check_all_digits(std::string_view value) { + return std::all_of(value.begin(), value.end(), [](unsigned char c) { + return std::isdigit(c) || (c == '-'); + }); } Dimension::value_type dimension_length(Interval::value_type vt) { return vt == Interval::s_max ? -1 : vt; @@ -85,45 +73,38 @@ Dimension::Dimension(value_type dimension) Dimension::Dimension(value_type min_dimension, value_type max_dimension) : m_dimension(min_dimension == -1 ? 0 : min_dimension, max_dimension == -1 ? Interval::s_max : max_dimension) {} -Dimension::Dimension(const std::string& value) { - auto val = ov::util::trim(value); - if (val == "?" || val == "-1") { - m_dimension = {0, Interval::s_max}; - return; - } - if (val.find("..") == std::string::npos) { - OPENVINO_ASSERT(check_all_digits(val), "Cannot parse dimension: \"" + val + "\""); - m_dimension = {stringToInt64(val)}; +Dimension::Dimension(const std::string& value) : Dimension(std::string_view(value)) {} + +Dimension::Dimension(std::string_view sv) { + auto trimmed_value = ov::util::trim(sv); + if (trimmed_value == "?" || trimmed_value == "-1") { return; } - std::string min_value_str = val.substr(0, val.find("..")); - min_value_str = ov::util::trim(min_value_str); + const auto interval_pos = trimmed_value.find(".."); + auto dim_str = trimmed_value.substr(0, interval_pos); - int64_t min_value; - if (min_value_str.empty()) - min_value = 0; - else { - OPENVINO_ASSERT(check_all_digits(min_value_str), "Cannot parse min bound: \"" + min_value_str + "\""); - min_value = stringToInt64(min_value_str); - } - - std::string max_value_str = val.substr(val.find("..") + 2); - max_value_str = ov::util::trim(max_value_str); + const auto no_interval = interval_pos == std::string_view::npos; + OPENVINO_ASSERT(check_all_digits(dim_str), + "Cannot parse ", + (no_interval) ? "dimension: \"" : "min bound: \"", + dim_str, + "\""); + const auto lower = util::view_to_number(dim_str).value_or(0); - int64_t max_value; - if (max_value_str.empty()) - max_value = Interval::s_max; - else { - OPENVINO_ASSERT(check_all_digits(max_value_str), "Cannot parse max bound: \"" + max_value_str + "\""); - max_value = stringToInt64(max_value_str); + if (no_interval) { + m_dimension = Interval(lower); + } else { + dim_str = trimmed_value.substr(interval_pos + 2); + OPENVINO_ASSERT(check_all_digits(dim_str), "Cannot parse max bound: \"", dim_str, "\""); + const auto upper = util::view_to_number(dim_str).value_or(Interval::s_max); + m_dimension = Interval(lower, upper); } - m_dimension = Interval(min_value, max_value); } std::string Dimension::to_string() const { std::stringstream dim_str_stream; - dim_str_stream << Dimension(m_dimension); + dim_str_stream << *this; return dim_str_stream.str(); } diff --git a/src/core/src/layout.cpp b/src/core/src/layout.cpp index 06b6d2ac2fd8..0b1abf8d9556 100644 --- a/src/core/src/layout.cpp +++ b/src/core/src/layout.cpp @@ -37,7 +37,7 @@ static const std::map& dim_aliases() { return DIM_ALIASES; } -static std::string to_internal_name(const std::string& dim_name) { +static std::string to_internal_name(std::string_view dim_name) { auto name = ov::util::to_upper(dim_name); auto it = dim_aliases().find(name); if (it != dim_aliases().end()) { @@ -46,17 +46,21 @@ static std::string to_internal_name(const std::string& dim_name) { return name; } -static void validate_name(const std::string& dim_name) { +static void validate_name(std::string_view dim_name) { OPENVINO_ASSERT(!dim_name.empty(), "Layout dimension name can't be empty"); bool has_alphanumeric = false; for (const auto& c : dim_name) { bool is_alnum = std::isalnum(c); has_alphanumeric |= is_alnum; OPENVINO_ASSERT(is_alnum || c == '_', - "Layout name is invalid (" + dim_name + "). Only english letters, digits and _ is allowed"); + "Layout name is invalid (", + dim_name, + "). Only english letters, digits and _ is allowed"); } OPENVINO_ASSERT(has_alphanumeric, - "Layout name is invalid (" + dim_name + "). Name shall have alphanumeric characters"); + "Layout name is invalid (", + dim_name, + "). Name shall have alphanumeric characters"); } Layout::Layout() : m_dynamic(true), m_left_size(0), m_right_size(0) {} @@ -81,11 +85,11 @@ Layout::Layout(const std::string& layout_str) { m_dynamic = false; return; } - auto is_advanced_syntax = [](const std::string& layout) { + auto is_advanced_syntax = [](const auto& layout) { return layout.length() >= 2 && layout.front() == '[' && layout.back() == ']'; }; - auto assign_name = [&](const std::string& name, int64_t index) { + auto assign_name = [&](std::string_view name, int64_t index) { auto dim_name = to_internal_name(name); validate_name(name); OPENVINO_ASSERT(m_names.count(dim_name) == 0, @@ -101,18 +105,18 @@ Layout::Layout(const std::string& layout_str) { std::istringstream ss(sub_name); std::string name; while (std::getline(ss, name, ',')) { - name = ov::util::trim(name); - if (name != "?") { - assign_name(name, index); + auto trim_name = ov::util::trim(name); + if (trim_name != "?") { + assign_name(trim_name, index); } - index++; + ++index; } return index; }; layout = layout.substr(1, layout.length() - 2); // remove [] auto ellipsis = layout.find(ELLIPSIS); if (ellipsis == std::string::npos) { - auto last_index = parse_commas(layout); + auto last_index = parse_commas(static_cast(layout)); m_dynamic = false; m_left_size = last_index; } else { @@ -121,16 +125,18 @@ Layout::Layout(const std::string& layout_str) { auto left_layout = ov::util::trim(layout.substr(0, ellipsis)); if (!left_layout.empty()) { OPENVINO_ASSERT(left_layout.at(left_layout.length() - 1) == ',', - "Layout: Invalid left side (" + layout + ")"); + "Layout: Invalid left side (", + layout, + ")"); left_layout = left_layout.substr(0, left_layout.length() - 1); - left_index = parse_commas(left_layout); + left_index = parse_commas(static_cast(left_layout)); } auto right_layout = ov::util::trim(layout.substr(ellipsis + ELLIPSIS_LEN)); if (!right_layout.empty()) { - OPENVINO_ASSERT(right_layout.at(0) == ',', "Layout: Invalid right side (" + layout + ")"); + OPENVINO_ASSERT(right_layout.at(0) == ',', "Layout: Invalid right side (", layout, ")"); right_layout = right_layout.substr(1, right_layout.length() - 1); right_index = std::count(right_layout.begin(), right_layout.end(), ',') + 1; - parse_commas(right_layout, -right_index); + parse_commas(static_cast(right_layout), -right_index); } m_dynamic = true; m_left_size = left_index; diff --git a/src/core/src/log_util.cpp b/src/core/src/log_util.cpp index 03f08e598dcb..13251d8e0c0b 100644 --- a/src/core/src/log_util.cpp +++ b/src/core/src/log_util.cpp @@ -12,7 +12,7 @@ namespace ov { namespace util { -#ifdef ENABLE_OPENVINO_DEBUG +#ifdef ENABLE_DEBUG_CAPS // Switch on verbose matching logging using OV_VERBOSE_LOGGING=true static const bool verbose = ov::util::getenv_bool("OV_VERBOSE_LOGGING"); diff --git a/src/core/src/op/constant.cpp b/src/core/src/op/constant.cpp index 6aa8aee640d7..74912f399e35 100644 --- a/src/core/src/op/constant.cpp +++ b/src/core/src/op/constant.cpp @@ -18,6 +18,7 @@ #include "openvino/core/type/element_iterator.hpp" #include "openvino/core/type/float16.hpp" #include "openvino/core/type/nf4.hpp" +#include "openvino/core/weight_sharing_util.hpp" #include "openvino/reference/convert.hpp" #include "openvino/reference/utils/type_util.hpp" #include "openvino/runtime/shared_buffer.hpp" diff --git a/src/core/src/op/fake_convert.cpp b/src/core/src/op/fake_convert.cpp index 248e0b5e1b8c..c13c1df7b63d 100644 --- a/src/core/src/op/fake_convert.cpp +++ b/src/core/src/op/fake_convert.cpp @@ -9,6 +9,7 @@ #include "itt.hpp" #include "openvino/reference/convert.hpp" #include "openvino/reference/fake_convert.hpp" +#include "openvino/util/common_util.hpp" namespace ov::op::v13 { namespace { diff --git a/src/core/src/op/gated_delta_net.cpp b/src/core/src/op/gated_delta_net.cpp index 569a7f344b5a..32d19b176df3 100644 --- a/src/core/src/op/gated_delta_net.cpp +++ b/src/core/src/op/gated_delta_net.cpp @@ -13,11 +13,11 @@ namespace { // Validates input rank and type for a node input. -inline void input_check(const ov::Node* node, - size_t idx, - const std::string_view input_name, - std::initializer_list&& allowed_ranks, - const std::vector& allowed_types) { +inline void gdn_input_check(const ov::Node* node, + size_t idx, + const std::string_view input_name, + std::initializer_list&& allowed_ranks, + const std::vector& allowed_types) { using namespace ov; using namespace ov::util; using namespace ov::element; @@ -91,12 +91,12 @@ void GatedDeltaNet::validate_and_infer_types() { NODE_VALIDATION_CHECK(this, get_input_size() == 6, "GatedDeltaNet expects 6 inputs, but it has ", get_input_size()); // format: Node*, input_idx, name, {rank_list}, {type_list} - input_check(this, 0, "query", {4}, {ov::element::f32, ov::element::f16, ov::element::bf16}); - input_check(this, 1, "key", {4}, {ov::element::f32, ov::element::f16, ov::element::bf16}); - input_check(this, 2, "value", {4}, {ov::element::f32, ov::element::f16, ov::element::bf16}); - input_check(this, 3, "recurrent_state", {4}, {ov::element::f32, ov::element::f16, ov::element::bf16}); - input_check(this, 4, "gate", {3}, {ov::element::f32, ov::element::f16, ov::element::bf16}); - input_check(this, 5, "beta", {3}, {ov::element::f32, ov::element::f16, ov::element::bf16}); + gdn_input_check(this, 0, "query", {4}, {ov::element::f32, ov::element::f16, ov::element::bf16}); + gdn_input_check(this, 1, "key", {4}, {ov::element::f32, ov::element::f16, ov::element::bf16}); + gdn_input_check(this, 2, "value", {4}, {ov::element::f32, ov::element::f16, ov::element::bf16}); + gdn_input_check(this, 3, "recurrent_state", {4}, {ov::element::f32, ov::element::f16, ov::element::bf16}); + gdn_input_check(this, 4, "gate", {3}, {ov::element::f32, ov::element::f16, ov::element::bf16}); + gdn_input_check(this, 5, "beta", {3}, {ov::element::f32, ov::element::f16, ov::element::bf16}); const auto output_shapes = shape_infer(this, ov::util::get_node_input_partial_shapes(*this)); set_output_type(0, get_input_element_type(0), output_shapes[0]); set_output_type(1, get_input_element_type(3), output_shapes[1]); diff --git a/src/core/src/op/moe.cpp b/src/core/src/op/moe.cpp index 39a73b26ad23..47e5ccafc3c2 100644 --- a/src/core/src/op/moe.cpp +++ b/src/core/src/op/moe.cpp @@ -31,8 +31,36 @@ std::shared_ptr MOE::clone_with_new_inputs(const ov::OutputVector& new void MOE::validate_and_infer_types() { OV_OP_SCOPE(internal_MOE_validate_and_infer_types); - // TODO: Add inputs validation + if (m_config.expert_type == Expert_type::GEMM2_BIAS_SWIGLU_CLAMP) { + NODE_VALIDATION_CHECK(this, + m_config.activation_type == Activation_type::SWIGLU, + "GEMM2_BIAS_SWIGLU_CLAMP expert type only supports SWIGLU activation"); + } + // Check that routing_weights and router_topk_output_indices have the same shape + const auto& routing_weights_shape = get_input_partial_shape(1); + const auto& topk_indices_shape = get_input_partial_shape(2); + NODE_VALIDATION_CHECK(this, + routing_weights_shape == topk_indices_shape, + "routing_weights shape ", + routing_weights_shape, + " must be compatible with router_topk_output_indices shape ", + topk_indices_shape); + + // Check that all expert weight inputs (index >= 3) have the same first dimension (num_experts). + const size_t base_inputs_count = (m_config.expert_type == Expert_type::GEMM3_SWIGLU) ? 6 : 7; + NODE_VALIDATION_CHECK(this, get_input_partial_shape(3).is_static(), "Weights must have static shape."); + const auto& num_experts = get_input_shape(3)[0]; + for (size_t i = 4; i < base_inputs_count; i++) { + const auto& ps = get_input_partial_shape(i); + NODE_VALIDATION_CHECK(this, ps.is_static(), "Weights must have static shape."); + // Note: dynamic element type means empty zero point input + if (get_input_element_type(i) != ov::element::dynamic) { + NODE_VALIDATION_CHECK(this, + num_experts == get_input_shape(i)[0], + "All weight inputs must have the same first dimension (num_experts)."); + } + } set_output_type(0, get_input_element_type(0), get_input_partial_shape(0)); } @@ -41,6 +69,8 @@ bool MOE::visit_attributes(ov::AttributeVisitor& visitor) { visitor.on_attribute("expert_type", m_config.expert_type); visitor.on_attribute("expert_alpha", m_config.expert_alpha); visitor.on_attribute("expert_beta", m_config.expert_beta); + visitor.on_attribute("gate_idx", m_config.gate_idx); + visitor.on_attribute("activation_type", m_config.activation_type); return true; } @@ -52,6 +82,10 @@ std::ostream& operator<<(std::ostream& s, const ov::op::internal::MOE::Expert_ty return s << as_string(type); } +std::ostream& operator<<(std::ostream& s, const ov::op::internal::MOE::Activation_type& type) { + return s << as_string(type); +} + template <> OPENVINO_API EnumNames& EnumNames::get() { static auto enum_names = EnumNames( @@ -62,4 +96,17 @@ OPENVINO_API EnumNames& EnumNames +OPENVINO_API EnumNames& +EnumNames::get() { + static auto enum_names = EnumNames( + "ov::op::internal::MOE::Activation_type", + { + {"swiglu", ov::op::internal::MOE::Activation_type::SWIGLU}, + {"geglu_tanh", ov::op::internal::MOE::Activation_type::GEGLU_TANH}, + {"geglu_erf", ov::op::internal::MOE::Activation_type::GEGLU_ERF}, + }); + return enum_names; +} } // namespace ov diff --git a/src/core/src/op/pa_kv_reorder.cpp b/src/core/src/op/pa_kv_reorder.cpp new file mode 100644 index 000000000000..6d7a4f048f91 --- /dev/null +++ b/src/core/src/op/pa_kv_reorder.cpp @@ -0,0 +1,59 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "openvino/op/pa_kv_reorder.hpp" + +#include "itt.hpp" +#include "openvino/core/validation_util.hpp" +#include "pa_kv_reorder_shape_inference.hpp" + +namespace ov::op::internal { + +PaKVReorder::PaKVReorder(const Output& key_cache, + const Output& value_cache, + const Output& block_indices, + const Output& block_indices_begins, + const Output& block_update_indices, + const Output& block_update_indices_begins) + : Op({key_cache, + value_cache, + block_indices, + block_indices_begins, + block_update_indices, + block_update_indices_begins}) { + constructor_validate_and_infer_types(); +} + +bool PaKVReorder::visit_attributes(ov::AttributeVisitor& /*visitor*/) { + return true; +} + +void PaKVReorder::validate_and_infer_types() { + OV_OP_SCOPE(PaKVReorder_validate_and_infer_types); + + for (size_t i = 2; i < 6; ++i) { + NODE_VALIDATION_CHECK(this, + get_input_element_type(i).is_dynamic() || get_input_element_type(i) == ov::element::i32, + "Input ", + i, + " (indices) must be i32, got ", + get_input_element_type(i)); + } + + const auto output_shapes = shape_infer(this, ov::util::get_node_input_partial_shapes(*this)); + set_output_type(0, ov::element::u8, output_shapes[0]); +} + +std::shared_ptr PaKVReorder::clone_with_new_inputs(const ov::OutputVector& new_args) const { + OV_OP_SCOPE(PaKVReorder_clone_with_new_inputs); + check_new_args_count(this, new_args); + return std::make_shared(new_args.at(0), + new_args.at(1), + new_args.at(2), + new_args.at(3), + new_args.at(4), + new_args.at(5)); +} + +} // namespace ov::op::internal diff --git a/src/core/src/op/pad.cpp b/src/core/src/op/pad.cpp index dd556a6f7aa4..bd2906940660 100644 --- a/src/core/src/op/pad.cpp +++ b/src/core/src/op/pad.cpp @@ -24,11 +24,7 @@ op::v1::Pad::Pad(const Output& arg, const Output& pads_begin, const Output& pads_end, op::PadMode pad_mode) - : op::util::PadBase(arg, - pads_begin, - pads_end, - op::v0::Constant::create(arg.get_element_type(), Shape{}, {0}), - pad_mode) { + : op::util::PadBase(arg, pads_begin, pads_end, pad_mode) { constructor_validate_and_infer_types(); } @@ -69,11 +65,7 @@ op::v12::Pad::Pad(const Output& arg, const Output& pads_begin, const Output& pads_end, op::PadMode pad_mode) - : op::util::PadBase(arg, - pads_begin, - pads_end, - op::v0::Constant::create(arg.get_element_type(), ov::Shape{}, {0}), - pad_mode) { + : op::util::PadBase(arg, pads_begin, pads_end, pad_mode) { constructor_validate_and_infer_types(); } diff --git a/src/core/src/op/paged_attention.cpp b/src/core/src/op/paged_attention.cpp index d70d165c071a..c524154ea690 100644 --- a/src/core/src/op/paged_attention.cpp +++ b/src/core/src/op/paged_attention.cpp @@ -8,12 +8,12 @@ #include "itt.hpp" #include "openvino/core/validation_util.hpp" #include "openvino/op/op.hpp" +#include "paged_attention_shape_inference.hpp" namespace { -// Validates input rank and type for a node input. -// We consider that dynamic rank/type are always valid case. -// Empty {} means any rank/type +// Validates input rank and type. Dynamic rank/type always passes. +// Empty {} means any rank/type. inline void input_check(const ov::Node* node, size_t idx, const std::string_view input_name, @@ -31,7 +31,7 @@ inline void input_check(const ov::Node* node, }; auto type_check = [&](const Type& type) { - auto it = std::find(allowed_types.begin(), allowed_types.end(), tp); + auto it = std::find(allowed_types.begin(), allowed_types.end(), type); return type.is_dynamic() || allowed_types.empty() || it != allowed_types.end(); }; @@ -71,7 +71,9 @@ std::vector get_real_types() { namespace ov { namespace op { -PagedAttentionExtension::PagedAttentionExtension(const ov::OutputVector& args) : ov::op::Op(args) { +PagedAttentionExtension::PagedAttentionExtension(const ov::OutputVector& args, bool write_kv_cache) + : ov::op::Op(args), + m_write_kv_cache(write_kv_cache) { constructor_validate_and_infer_types(); } @@ -111,57 +113,36 @@ void PagedAttentionExtension::validate_and_infer_types() { input_check(this, 23, "adaptive_rkv_diversity_block_set_indices", {1}, {element::i32}); input_check(this, 24, "adaptive_rkv_diversity_block_set_indices_begins", {1}, {element::i32}); input_check(this, 25, "token_type_ids", {1, 2}, {element::i32}); - - // input to support query-query bias attention input_check(this, 26, "qq_bias", {1}, {element::u8}); input_check(this, 27, "qq_bias_begins", {1}, {element::i32}); - // value head_size may be not same with key - auto out_ps = get_input_partial_shape(0); - const auto& key_ps = get_input_partial_shape(1); - const auto& value_ps = get_input_partial_shape(2); - if (out_ps.rank().is_static()) { - if (key_ps.rank().is_static() && value_ps.rank().is_static() && key_ps[1].is_static()) { - // The dim of out_ps[1] should be `num_heads * v_head_size`, it can be got from: - // because: - // q: query_ps[1] = num_heads * head_size - // k: key_ps[1] = num_kv_heads * head_size - // v: value_ps[1] = num_kv_heads * v_head_size - // therefore: - // q * v / k = (num_heads * head_size) * (num_kv_heads * v_head_size) / - // (num_kv_heads * head_size) = num_heads * v_head_size - out_ps[1] = out_ps[1] * value_ps[1] / key_ps[1].get_length(); - NODE_VALIDATION_CHECK(this, - !ov::util::dim::is_empty(out_ps[1]), - "The last dimension of output should not be empty."); - } else { - out_ps[1] = Dimension::dynamic(); - } - } - if (m_output_type[0].is_dynamic()) { - set_output_type(0, get_input_element_type(0), out_ps); - } else { - set_output_type(0, m_output_type[0], out_ps); - } - - if (m_output_type[1].is_dynamic()) { - set_output_type(1, get_input_element_type(0), {Dimension::dynamic()}); - } else { - set_output_type(1, m_output_type[1], {Dimension::dynamic()}); - } - if (m_output_type[2].is_dynamic()) { - set_output_type(2, get_input_element_type(0), {Dimension::dynamic()}); - } else { - set_output_type(2, m_output_type[2], {Dimension::dynamic()}); + const auto input_shapes = ov::util::get_node_input_partial_shapes(*this); + const auto output_shapes = shape_infer(this, input_shapes); + // Use m_output_type overrides when set, otherwise fall back to query type + for (int i = 0; i < 3; ++i) { + const auto et = m_output_type[i].is_dynamic() ? get_input_element_type(0) : m_output_type[i]; + set_output_type(i, et, output_shapes[i]); } } std::shared_ptr PagedAttentionExtension::clone_with_new_inputs(const ov::OutputVector& new_args) const { - return std::make_shared(new_args); + OV_OP_SCOPE(PagedAttentionExtension_clone_with_new_inputs); + check_new_args_count(this, new_args); + return std::make_shared(new_args, m_write_kv_cache); +} + +bool PagedAttentionExtension::visit_attributes(ov::AttributeVisitor& visitor) { + visitor.on_attribute("write_kv_cache", m_write_kv_cache); + return true; +} + +const ov::element::Type PagedAttentionExtension::get_out_type(int index) const { + OPENVINO_ASSERT(index < 3, "Output index should be 0, 1 or 2, but got " + std::to_string(index)); + return m_output_type[index]; } void PagedAttentionExtension::set_out_type(int index, const ov::element::Type& output_type) { - OPENVINO_ASSERT(index < 2, "Output index should be 0 or 1, but got " + std::to_string(index)); + OPENVINO_ASSERT(index < 3, "Output index should be 0, 1 or 2, but got " + std::to_string(index)); m_output_type[index] = output_type; } diff --git a/src/core/src/op/paged_causal_conv1d.cpp b/src/core/src/op/paged_causal_conv1d.cpp new file mode 100644 index 000000000000..3d4538da4b95 --- /dev/null +++ b/src/core/src/op/paged_causal_conv1d.cpp @@ -0,0 +1,87 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "openvino/op/paged_causal_conv1d.hpp" + +#include "itt.hpp" +#include "openvino/core/validation_util.hpp" +#include "openvino/op/op.hpp" +#include "paged_causal_conv1d_shape_inference.hpp" + +namespace ov::op::internal { + +PagedCausalConv1D::PagedCausalConv1D(const Output& input_embeds, + const Output& conv_state_table, + const Output& conv_weight, + const Output& conv_bias, + const Output& subsequence_begins, + const Output& la_block_indices, + const Output& la_block_indices_begins, + const Output& processed_tokens, + const Output& cache_interval) + : Op({input_embeds, + conv_state_table, + conv_weight, + conv_bias, + subsequence_begins, + la_block_indices, + la_block_indices_begins, + processed_tokens, + cache_interval}) { + constructor_validate_and_infer_types(); +} + +PagedCausalConv1D::PagedCausalConv1D(const ov::OutputVector& args) : ov::op::Op(args) { + constructor_validate_and_infer_types(); +} + +void PagedCausalConv1D::validate_and_infer_types() { + OV_OP_SCOPE(PagedCausalConv1D_validate_and_infer_types); + + NODE_VALIDATION_CHECK(this, get_input_size() == 9); + + // input_embeds (0), conv_weight (2), conv_bias (3) participate in the convolution MAC and + // therefore must share a common float element type; it also determines the output precision. + // conv_state_table (1) is an in-place state cache and is allowed to use an independent float + // element type so plugins can maintain a lower-precision state without breaking in-place semantics. + ov::element::Type common_float_type = get_input_element_type(0); + const bool float_types_merge = + ov::element::Type::merge(common_float_type, common_float_type, get_input_element_type(2)) && + ov::element::Type::merge(common_float_type, common_float_type, get_input_element_type(3)); + NODE_VALIDATION_CHECK(this, + float_types_merge, + "PagedCausalConv1D expects input_embeds, conv_weight, and conv_bias to have the same " + "element type."); + NODE_VALIDATION_CHECK(this, + common_float_type.is_dynamic() || common_float_type == ov::element::f32 || + common_float_type == ov::element::f16 || common_float_type == ov::element::bf16, + "Float inputs must have f32, f16, or bf16 element type."); + const auto& state_et = get_input_element_type(1); + NODE_VALIDATION_CHECK(this, + state_et.is_dynamic() || state_et == ov::element::f32 || state_et == ov::element::f16 || + state_et == ov::element::bf16, + "Float inputs must have f32, f16, or bf16 element type."); + for (size_t i = 4; i < 9; ++i) { + const auto& et = get_input_element_type(i); + NODE_VALIDATION_CHECK(this, + et.is_dynamic() || et == ov::element::i32 || et == ov::element::i64, + "Integer inputs must have i32 or i64 element type."); + } + + const auto output_shapes = shape_infer(this, ov::util::get_node_input_partial_shapes(*this)); + set_output_type(0, common_float_type, output_shapes[0]); +} + +bool PagedCausalConv1D::visit_attributes(AttributeVisitor& visitor) { + OV_OP_SCOPE(PagedCausalConv1D_visit_attributes); + return true; +} + +std::shared_ptr PagedCausalConv1D::clone_with_new_inputs(const ov::OutputVector& new_args) const { + OV_OP_SCOPE(PagedCausalConv1D_clone_with_new_inputs); + check_new_args_count(this, new_args); + return std::make_shared(new_args); +} + +} // namespace ov::op::internal diff --git a/src/core/src/op/paged_gated_delta_net.cpp b/src/core/src/op/paged_gated_delta_net.cpp new file mode 100644 index 000000000000..d70640fe5cf5 --- /dev/null +++ b/src/core/src/op/paged_gated_delta_net.cpp @@ -0,0 +1,116 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "openvino/op/paged_gated_delta_net.hpp" + +#include "itt.hpp" +#include "openvino/core/validation_util.hpp" +#include "openvino/op/op.hpp" +#include "paged_gated_delta_net_shape_inference.hpp" + +namespace ov::op::internal { + +PagedGatedDeltaNet::PagedGatedDeltaNet(const Output& query, + const Output& key, + const Output& value, + const Output& recurrent_state_table, + const Output& gate, + const Output& beta, + const Output& subsequence_begins, + const Output& la_block_indices, + const Output& la_block_indices_begins, + const Output& processed_tokens, + const Output& cache_interval, + bool use_qk_l2norm, + float q_l2_norm_eps, + float k_l2_norm_eps) + : Op({query, + key, + value, + recurrent_state_table, + gate, + beta, + subsequence_begins, + la_block_indices, + la_block_indices_begins, + processed_tokens, + cache_interval}), + m_use_qk_l2norm(use_qk_l2norm), + m_q_l2_norm_eps(q_l2_norm_eps), + m_k_l2_norm_eps(k_l2_norm_eps) { + constructor_validate_and_infer_types(); +} + +PagedGatedDeltaNet::PagedGatedDeltaNet(const ov::OutputVector& args, + bool use_qk_l2norm, + float q_l2_norm_eps, + float k_l2_norm_eps) + : ov::op::Op(args), + m_use_qk_l2norm(use_qk_l2norm), + m_q_l2_norm_eps(q_l2_norm_eps), + m_k_l2_norm_eps(k_l2_norm_eps) { + constructor_validate_and_infer_types(); +} + +void PagedGatedDeltaNet::validate_and_infer_types() { + OV_OP_SCOPE(PagedGatedDeltaNet_validate_and_infer_types); + + NODE_VALIDATION_CHECK(this, get_input_size() == 11); + + // query (0), key (1), value (2), gate (4), and beta (5) participate in the main arithmetic + // and therefore must share a common float element type; it also determines the output precision. + // recurrent_state_table (3) is an in-place state cache and is allowed to use an independent + // float element type so plugins can maintain lower-precision state without breaking in-place semantics. + ov::element::Type common_float_type = get_input_element_type(0); + const bool float_types_merge = + ov::element::Type::merge(common_float_type, common_float_type, get_input_element_type(1)) && + ov::element::Type::merge(common_float_type, common_float_type, get_input_element_type(2)) && + ov::element::Type::merge(common_float_type, common_float_type, get_input_element_type(4)) && + ov::element::Type::merge(common_float_type, common_float_type, get_input_element_type(5)); + NODE_VALIDATION_CHECK(this, + float_types_merge, + "PagedGatedDeltaNet expects query, key, value, gate, and beta to have the same element " + "type."); + NODE_VALIDATION_CHECK(this, + common_float_type.is_dynamic() || common_float_type == ov::element::f32 || + common_float_type == ov::element::f16 || common_float_type == ov::element::bf16, + "Float inputs must have f32, f16, or bf16 element type."); + const auto& state_et = get_input_element_type(3); + NODE_VALIDATION_CHECK(this, + state_et.is_dynamic() || state_et == ov::element::f32 || state_et == ov::element::f16 || + state_et == ov::element::bf16, + "Float inputs must have f32, f16, or bf16 element type."); + for (size_t i = 6; i < 11; ++i) { + const auto& et = get_input_element_type(i); + NODE_VALIDATION_CHECK(this, + et.is_dynamic() || et == ov::element::i32 || et == ov::element::i64, + "Integer inputs must have i32 or i64 element type."); + } + + NODE_VALIDATION_CHECK(this, + m_q_l2_norm_eps > 0.0f, + "Attribute 'q_l2_norm_eps' must be a positive floating-point number."); + NODE_VALIDATION_CHECK(this, + m_k_l2_norm_eps > 0.0f, + "Attribute 'k_l2_norm_eps' must be a positive floating-point number."); + + const auto output_shapes = shape_infer(this, ov::util::get_node_input_partial_shapes(*this)); + set_output_type(0, common_float_type, output_shapes[0]); +} + +bool PagedGatedDeltaNet::visit_attributes(AttributeVisitor& visitor) { + OV_OP_SCOPE(PagedGatedDeltaNet_visit_attributes); + visitor.on_attribute("use_qk_l2norm", m_use_qk_l2norm); + visitor.on_attribute("q_l2_norm_eps", m_q_l2_norm_eps); + visitor.on_attribute("k_l2_norm_eps", m_k_l2_norm_eps); + return true; +} + +std::shared_ptr PagedGatedDeltaNet::clone_with_new_inputs(const ov::OutputVector& new_args) const { + OV_OP_SCOPE(PagedGatedDeltaNet_clone_with_new_inputs); + check_new_args_count(this, new_args); + return std::make_shared(new_args, m_use_qk_l2norm, m_q_l2_norm_eps, m_k_l2_norm_eps); +} + +} // namespace ov::op::internal diff --git a/src/core/src/op/segment_max.cpp b/src/core/src/op/segment_max.cpp index 865553b856c1..18d8b91160c0 100644 --- a/src/core/src/op/segment_max.cpp +++ b/src/core/src/op/segment_max.cpp @@ -65,7 +65,7 @@ std::shared_ptr SegmentMax::clone_with_new_inputs(const ov::OutputVector& } } -const op::FillMode SegmentMax::get_fill_mode() const { +op::FillMode SegmentMax::get_fill_mode() const { return m_fill_mode; } diff --git a/src/core/src/op/shape_of.cpp b/src/core/src/op/shape_of.cpp index 981a3fd99f5f..fea68cf21ffe 100644 --- a/src/core/src/op/shape_of.cpp +++ b/src/core/src/op/shape_of.cpp @@ -70,10 +70,10 @@ bool evaluate_bound(const Node* const node, ov::TensorVector& outputs, const boo // but node output type is transferred from v3 and can be i32 (dimension inf bound is i32 max) if (node->get_output_element_type(0) == element::i32) { const auto in_shape_rank = in_shape.size(); - constexpr auto max_et_val = static_cast(std::numeric_limits::max()); + constexpr int64_t max_et_val = std::numeric_limits::max(); const auto get_val = is_upper ? &Interval::get_max_val : &Interval::get_min_val; - const auto limit_val = is_upper ? max_et_val : static_cast(0); + const auto limit_val = is_upper ? max_et_val : 0; TensorVector inputs{ {element::boolean, Shape{in_shape_rank}}, // mask diff --git a/src/core/src/op/util/interpolate_base.cpp b/src/core/src/op/util/interpolate_base.cpp index 739a2fee55cb..641cc88b904b 100644 --- a/src/core/src/op/util/interpolate_base.cpp +++ b/src/core/src/op/util/interpolate_base.cpp @@ -57,8 +57,8 @@ void InterpolateBase::validate_and_infer_types() { NODE_VALIDATION_CHECK(this, input_et == element::f32 || input_et == element::f16 || input_et == element::i8 || input_et == element::bf16 || input_et == element::u8 || input_et == element::i64 || - input_et == element::i32 || input_et == element::dynamic, - "Input element type must be f32, f16, bf16, i8, u8, i64, i32"); + input_et == element::i32 || input_et == element::f64 || input_et == element::dynamic, + "Input element type must be f32, f16, i8, bf16, u8, i64, i32, f64"); } void InterpolateBase::validate_scales_element_type(const element::Type& et) const { diff --git a/src/core/src/op/util/pad_base.cpp b/src/core/src/op/util/pad_base.cpp index ad8f4c8cdd56..d4c5c265d246 100644 --- a/src/core/src/op/util/pad_base.cpp +++ b/src/core/src/op/util/pad_base.cpp @@ -28,7 +28,12 @@ op::util::PadBase::PadBase(const Output& arg, const Output& pads_begin, const Output& pads_end, op::PadMode pad_mode) - : Op({arg, pads_begin, pads_end, op::v0::Constant::create(arg.get_element_type(), Shape{}, {0})}), + : Op({arg, + pads_begin, + pads_end, + arg.get_element_type() == element::string + ? op::v0::Constant::create(arg.get_element_type(), Shape{}, std::initializer_list{""}) + : op::v0::Constant::create(arg.get_element_type(), Shape{}, {0})}), m_pad_mode{pad_mode} { mark_as_precision_sensitive(input(1)); mark_as_precision_sensitive(input(2)); @@ -83,6 +88,10 @@ void op::util::PadBase::validate_and_infer_types() { ")."); } + NODE_VALIDATION_CHECK(this, + arg_element_type != element::string || m_pad_mode == op::PadMode::CONSTANT, + "Only CONSTANT pad mode is supported for element::string"); + NODE_VALIDATION_CHECK(this, pads_begin_element_type.is_integral_number(), "pads_begin must be an integral number, but is: ", @@ -101,42 +110,46 @@ void op::util::PadBase::validate_and_infer_types() { bool op::util::PadBase::evaluate_pad(TensorVector& outputs, const TensorVector& inputs) const { const auto& data = inputs[0]; - const auto elem_size = data.get_element_type().size(); + const auto& data_shape = data.get_shape(); - const char* pad_value = nullptr; - const std::vector pad_zero_value(elem_size, 0); - if (get_input_size() == 4) { - pad_value = static_cast(inputs[3].data()); + const CoordinateDiff pads_begin = op::v0::Constant(inputs[1]).cast_vector(); + const CoordinateDiff pads_end = op::v0::Constant(inputs[2]).cast_vector(); + + ov::Shape out_shape(data_shape.size()); + for (size_t i = 0; i < data_shape.size(); ++i) + out_shape[i] = ov::util::dim::padded(data_shape[i], pads_begin[i] + pads_end[i]); + outputs[0].set_shape(out_shape); + + if (data.get_element_type() == element::string) { + const std::string_view pad_str = + (get_input_size() == 4) ? std::string_view{*inputs[3].data()} : std::string_view{}; + + ov::reference::pad(data.data(), + pad_str, + outputs[0].data(), + data_shape, + out_shape, + pads_begin, + pads_end); } else { - pad_value = pad_zero_value.data(); + const auto elem_size = data.get_element_type().size(); + const std::vector pad_zero_value(elem_size, 0); + const char* pad_value = + (get_input_size() == 4) ? static_cast(inputs[3].data()) : pad_zero_value.data(); + + // compute pads_begin and pads_end CoordinateDiffs from pads_begin + // and pads_end shapes and reshape output to determine shape + // (in case pads_begin and pads_end are Parameters, output is dynamic with static rank). + ov::reference::pad(static_cast(inputs[0].data()), + pad_value, + static_cast(outputs[0].data()), + elem_size, + data_shape, + out_shape, + pads_begin, + pads_end, + get_pad_mode()); } - - // compute pads_begin and pads_end CoordinateDiffs from pads_begin - // and pads_end shapes and reshape output to determine shape - // (in case pads_begin and pads_end are Parameters, output is dynamic with static rank). - - op::v0::Constant pads_begin_const(inputs[1]); - CoordinateDiff pads_begin_coord(pads_begin_const.cast_vector()); - op::v0::Constant pads_end_const(inputs[2]); - CoordinateDiff pads_end_coord(pads_end_const.cast_vector()); - - const auto& data_shape = data.get_shape(); - ov::Shape padded_shape(data_shape.size()); - for (size_t i = 0; i < data_shape.size(); ++i) { - padded_shape[i] = data_shape[i] + pads_begin_coord[i] + pads_end_coord[i]; - } - outputs[0].set_shape(padded_shape); - - ov::reference::pad(static_cast(inputs[0].data()), - pad_value, - static_cast(outputs[0].data()), - elem_size, - data_shape, - padded_shape, - pads_begin_coord, - pads_end_coord, - get_pad_mode()); - return true; } diff --git a/src/core/src/partial_shape.cpp b/src/core/src/partial_shape.cpp index 2cd52f3d7198..75dadf02177d 100644 --- a/src/core/src/partial_shape.cpp +++ b/src/core/src/partial_shape.cpp @@ -28,20 +28,24 @@ ov::PartialShape::PartialShape(const Shape& shape) ov::PartialShape::PartialShape(const std::string& value) { auto val = ov::util::trim(value); - if (val[0] == '[' && val[val.size() - 1] == ']') - val = val.substr(1, val.size() - 2); - val = ov::util::trim(val); - if (val == "...") { - m_rank_is_static = false; - m_dimensions = std::vector(); - return; + if (!val.empty() && val[0] == '[' && val[val.size() - 1] == ']') { + val.remove_prefix(1); + val.remove_suffix(1); + val = ov::util::trim(val); } - m_rank_is_static = true; - std::stringstream ss(val); - std::string field; - while (getline(ss, field, ',')) { - OPENVINO_ASSERT(!field.empty(), "Cannot get vector of dimensions! \"" + value + "\" is incorrect"); - m_dimensions.emplace_back(field); + + if (m_rank_is_static = (val != "..."); m_rank_is_static) { + util::view_transform_if( + val, + std::back_inserter(m_dimensions), + ",", + [&value](auto&& dim_field) { + OPENVINO_ASSERT(!dim_field.empty(), "Cannot get vector of dimensions! \"", value, "\" is incorrect"); + return true; + }, + [](auto&& dim_field) { + return Dimension{dim_field}; + }); } } @@ -150,16 +154,7 @@ ov::PartialShape ov::operator+(const PartialShape& s1, const PartialShape& s2) { std::ostream& ov::operator<<(std::ostream& str, const PartialShape& shape) { if (shape.m_rank_is_static) { - str << "["; - bool first = true; - for (auto& d : shape.m_dimensions) { - if (!first) { - str << ","; - } - str << d; - first = false; - } - return (str << "]"); + return str << "[" << ov::util::join(shape.m_dimensions, ",") << "]"; } else { return (str << "[...]"); } diff --git a/src/core/src/pass/constant_folding.cpp b/src/core/src/pass/constant_folding.cpp index 6057b6902e43..37d8ffd986f3 100644 --- a/src/core/src/pass/constant_folding.cpp +++ b/src/core/src/pass/constant_folding.cpp @@ -8,6 +8,7 @@ #include "openvino/core/constant_fold_utils.hpp" #include "openvino/core/rt_info.hpp" #include "openvino/core/rt_info/weightless_caching_attributes.hpp" +#include "openvino/core/weight_sharing_util.hpp" #include "openvino/op/constant.hpp" #include "openvino/op/convert.hpp" #include "openvino/op/util/op_types.hpp" @@ -161,6 +162,14 @@ bool ov::pass::ConstantFolding::run_on_model(const std::shared_ptr& m // Propagate runtime info attributes to replacement copy_runtime_info(original_node, replacement_ptr); ov::copy_weightless_cache_attr(original_node, replacement_ptr); + // Evict data if original node is constant or convert with constant input + if (auto constant = ov::as_type_ptr(original_node)) { + ov::wsh::Extension::hint_evict(*constant); + } else if (auto convert = ov::as_type_ptr(original_node)) { + if (auto const_input = ov::as_type(convert->get_input_node_ptr(0))) { + ov::wsh::Extension::hint_evict(*const_input); + } + } rewritten = true; } diff --git a/src/plugins/intel_gpu/src/plugin/transformations/pa_kv_reorder_fusion.cpp b/src/core/src/pass/pa_kv_reorder_fusion.cpp similarity index 67% rename from src/plugins/intel_gpu/src/plugin/transformations/pa_kv_reorder_fusion.cpp rename to src/core/src/pass/pa_kv_reorder_fusion.cpp index c2c687c00756..8e406ae23bbe 100644 --- a/src/plugins/intel_gpu/src/plugin/transformations/pa_kv_reorder_fusion.cpp +++ b/src/core/src/pass/pa_kv_reorder_fusion.cpp @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 // -#include "pa_kv_reorder_fusion.hpp" +#include "openvino/pass/pa_kv_reorder_fusion.hpp" #include #include @@ -10,17 +10,18 @@ #include #include -#include "intel_gpu/op/pa_kv_reorder.hpp" #include "openvino/core/graph_util.hpp" #include "openvino/core/rt_info.hpp" #include "openvino/op/concat.hpp" #include "openvino/op/constant.hpp" #include "openvino/op/gather.hpp" +#include "openvino/op/pa_kv_reorder.hpp" #include "openvino/op/parameter.hpp" +#include "openvino/op/result.hpp" #include "openvino/op/scatter_update.hpp" -#include "transformations/utils/utils.hpp" -namespace ov::intel_gpu { +namespace ov { +namespace pass { namespace { struct CacheReorderPath { @@ -96,6 +97,11 @@ std::optional parse_cache_reorder_path(const std::shared_ptrget_friendly_name()); if (!role_and_index.has_value()) { role_and_index = parse_cache_role_and_index(scatter->get_friendly_name()); @@ -116,7 +122,8 @@ std::optional parse_cache_reorder_path(const std::shared_ptr get_parameter_by_name(const std::shared_ptr& m, const std::string& parameter_name) { +std::shared_ptr get_parameter_by_name(const std::shared_ptr& m, + const std::string& parameter_name) { for (const auto& parameter : m->get_parameters()) { if (parameter && parameter->get_friendly_name() == parameter_name) { return parameter; @@ -126,28 +133,9 @@ std::shared_ptr get_parameter_by_name(const std::shared_p return nullptr; } -ov::element::Type format_cache_precision(const ov::element::Type& cache_precision, const ov::element::Type& infer_precision) { - return cache_precision == ov::element::f16 && infer_precision == ov::element::bf16 ? infer_precision : cache_precision; -} - -bool is_compressed_cache_type(const ov::element::Type& type) { - return type == ov::element::i8 || type == ov::element::u8 || type == ov::element::i4 || type == ov::element::u4; -} - -size_t infer_scales_zp_size(const ov::element::Type& cache_type, const ov::element::Type& infer_precision) { - if (!is_compressed_cache_type(cache_type)) { - return 0; - } - - OPENVINO_ASSERT(cache_type.size() > 0 && infer_precision.size() > 0, "[GPU] Invalid element size for pa_kv_reorder metadata inference"); - if (cache_type == ov::element::i4 || cache_type == ov::element::u4) { - return 4 * infer_precision.size(); - } - return (2 * infer_precision.size()) / cache_type.size(); -} - -std::vector> get_joint_concat_consumers(const ov::Output& key_scatter_output, - const ov::Output& value_scatter_output) { +std::vector> get_joint_concat_consumers( + const ov::Output& key_scatter_output, + const ov::Output& value_scatter_output) { std::vector> concats; for (const auto& target_input : key_scatter_output.get_target_inputs()) { auto concat = ov::as_type_ptr(target_input.get_node()->shared_from_this()); @@ -166,15 +154,15 @@ std::vector> get_joint_concat_consumers(cons continue; } - bool has_result_consumer = false; + bool all_consumers_are_results = true; for (const auto& concat_target : concat->output(0).get_target_inputs()) { - if (ov::as_type_ptr(concat_target.get_node()->shared_from_this())) { - has_result_consumer = true; + if (!ov::as_type_ptr(concat_target.get_node()->shared_from_this())) { + all_consumers_are_results = false; break; } } - if (has_result_consumer) { + if (all_consumers_are_results) { concats.push_back(concat); } } @@ -184,6 +172,8 @@ std::vector> get_joint_concat_consumers(cons } // namespace +PaKVReorderFusion::PaKVReorderFusion(ov::element::Type cache_precision) : m_cache_precision(cache_precision) {} + bool PaKVReorderFusion::run_on_model(const std::shared_ptr& m) { auto block_indices_begins = get_parameter_by_name(m, "block_indices_begins"); auto block_update_indices_begins = get_parameter_by_name(m, "block_update_indices_begins"); @@ -216,45 +206,28 @@ bool PaKVReorderFusion::run_on_model(const std::shared_ptr& m) { } const auto& value_path = value_paths.at(index); - if (!same_output(key_path.block_indices, value_path.block_indices) || !same_output(key_path.block_update_indices, value_path.block_update_indices)) { + if (!same_output(key_path.block_indices, value_path.block_indices) || + !same_output(key_path.block_update_indices, value_path.block_update_indices)) { continue; } - const auto concat_consumers = get_joint_concat_consumers(key_path.scatter->output(0), value_path.scatter->output(0)); + const auto concat_consumers = + get_joint_concat_consumers(key_path.scatter->output(0), value_path.scatter->output(0)); if (concat_consumers.empty()) { continue; } - auto pa_kv_reorder = std::make_shared(key_path.cache->output(0), - value_path.cache->output(0), - key_path.block_indices, - block_indices_begins->output(0), - key_path.block_update_indices, - block_update_indices_begins->output(0)); - - constexpr const char* rt_is_key_by_channel = "pa_kv_reorder.is_key_by_channel"; - constexpr const char* rt_scales_zp_size = "pa_kv_reorder.scales_zp_size"; - constexpr const char* rt_key_dim_order = "pa_kv_reorder.key_cache_dim_order"; - constexpr const char* rt_value_dim_order = "pa_kv_reorder.value_cache_dim_order"; - - const auto key_cache_type = format_cache_precision(m_key_cache_precision, m_inference_precision); - const auto value_cache_type = format_cache_precision(m_value_cache_precision, m_inference_precision); - const auto key_scales_zp_size = infer_scales_zp_size(key_cache_type, m_inference_precision); - const auto value_scales_zp_size = infer_scales_zp_size(value_cache_type, m_inference_precision); - OPENVINO_ASSERT(key_scales_zp_size == value_scales_zp_size, - "[GPU] PaKVReorderFusion expects equal scales/zp size for key/value cache, but got ", - key_scales_zp_size, - " and ", - value_scales_zp_size); - // update key/value type - key_path.cache->set_element_type(key_cache_type); - value_path.cache->set_element_type(value_cache_type); - const bool is_key_by_channel = m_key_cache_quant_by_channel; - - pa_kv_reorder->get_rt_info()[rt_is_key_by_channel] = is_key_by_channel; - pa_kv_reorder->get_rt_info()[rt_scales_zp_size] = key_scales_zp_size; - pa_kv_reorder->get_rt_info()[rt_key_dim_order] = m_key_cache_dim_order; - pa_kv_reorder->get_rt_info()[rt_value_dim_order] = m_value_cache_dim_order; + if (m_cache_precision != ov::element::dynamic) { + key_path.cache->set_element_type(m_cache_precision); + value_path.cache->set_element_type(m_cache_precision); + } + + auto pa_kv_reorder = std::make_shared(key_path.cache->output(0), + value_path.cache->output(0), + key_path.block_indices, + block_indices_begins->output(0), + key_path.block_update_indices, + block_update_indices_begins->output(0)); ov::copy_runtime_info(key_path.cache, pa_kv_reorder); pa_kv_reorder->set_friendly_name("pa_kv_reorder_" + index); @@ -280,4 +253,5 @@ bool PaKVReorderFusion::run_on_model(const std::shared_ptr& m) { return rewritten; } -} // namespace ov::intel_gpu +} // namespace pass +} // namespace ov diff --git a/src/core/src/pass/sdpa_to_paged_attention.cpp b/src/core/src/pass/sdpa_to_paged_attention.cpp index 099573088375..c1480e92063f 100644 --- a/src/core/src/pass/sdpa_to_paged_attention.cpp +++ b/src/core/src/pass/sdpa_to_paged_attention.cpp @@ -5,6 +5,8 @@ #include "openvino/pass/sdpa_to_paged_attention.hpp" #include "openvino/cc/pass/itt.hpp" +#include "openvino/core/graph_util.hpp" +#include "openvino/op/assign.hpp" #include "openvino/op/constant.hpp" #include "openvino/op/gather.hpp" #include "openvino/op/scaled_dot_product_attention.hpp" @@ -12,16 +14,43 @@ #include "openvino/op/subtract.hpp" #include "openvino/op/unsqueeze.hpp" #include "openvino/pass/manager.hpp" +#include "transformations/common_optimizations/fuse_gated_delta_net.hpp" #include "transformations/common_optimizations/sdpa_fusion.hpp" #include "transformations/op_conversions/convert_slice_to_strided_slice.hpp" -#include "transformations/sdpa_to_paged_attention/position_ids_replacer.hpp" -#include "transformations/sdpa_to_paged_attention/prev_sequence_length_pattern.hpp" -#include "transformations/sdpa_to_paged_attention/state_management_pattern.hpp" -#include "transformations/sdpa_to_paged_attention/total_sequence_length_pattern.hpp" +#include "transformations/paged_attention/eliminate_conv_padding_mask_gating.hpp" +#include "transformations/paged_attention/paged_causal_conv1d_fusion.hpp" +#include "transformations/paged_attention/paged_gated_delta_net_fusion.hpp" +#include "transformations/paged_attention/position_ids_replacer.hpp" +#include "transformations/paged_attention/prev_sequence_length_pattern.hpp" +#include "transformations/paged_attention/state_management_pattern.hpp" +#include "transformations/paged_attention/total_sequence_length_pattern.hpp" #include "transformations/utils/print_model.hpp" #include "transformations/utils/utils.hpp" using namespace ov::op; +using ov::pass::paged_attention::PaParams; +using ov::pass::paged_attention::PaResults; + +namespace { + +std::shared_ptr get_parameter(const std::shared_ptr& model, const std::string& name) { + for (const auto& param : model->inputs()) { + const auto& names = param.get_names(); + if (names.count(name)) { + if (auto casted_param = ov::as_type_ptr(param.get_node_shared_ptr())) { + return casted_param; + } else { + OPENVINO_THROW("The model is in the inconsistent state. Found input '", + name, + "', but couldn't cast it to v0::Parameter."); + } + } + } + + return nullptr; +} + +} // namespace ov::pass::SDPAToPagedAttention::SDPAToPagedAttention(bool use_per_layer_block_indices_inputs, bool use_score_outputs, @@ -30,22 +59,13 @@ ov::pass::SDPAToPagedAttention::SDPAToPagedAttention(bool use_per_layer_block_in bool allow_xattention, bool allow_adaptive_rkv, bool allow_qq_bias) - : m_use_per_layer_block_indices_inputs(use_per_layer_block_indices_inputs), - m_use_score_outputs(use_score_outputs), - m_allow_score_aggregation(allow_score_aggregation), - m_allow_cache_rotation(allow_cache_rotation), - m_allow_xattention(allow_xattention), - m_allow_adaptive_rkv(allow_adaptive_rkv), - m_allow_qq_bias(allow_qq_bias) {} - -static std::shared_ptr named_parameter(std::shared_ptr node, const char* name) { - // Set name for both node and output tensor (should be only one tensor, and any other names will be overriden by a - // given single name) - node->set_friendly_name(name); - OPENVINO_ASSERT(node->get_output_size() == 1); - node->get_output_tensor(0).set_names({name}); - return node; -} + : m_options{use_per_layer_block_indices_inputs, + use_score_outputs, + allow_score_aggregation, + allow_cache_rotation, + allow_xattention, + allow_adaptive_rkv, + allow_qq_bias} {} bool ov::pass::SDPAToPagedAttention::run_on_model(const std::shared_ptr& model) { RUN_ON_MODEL_SCOPE(SDPAToPagedAttention); @@ -60,72 +80,16 @@ bool ov::pass::SDPAToPagedAttention::run_on_model(const std::shared_ptr> optional_model_wide_params; - - auto max_context_len = - named_parameter(std::make_shared(element::i32, PartialShape{}), "max_context_len"); - ParameterVector model_wide_params{ - named_parameter(std::make_shared(element::i32, PartialShape{-1}), "past_lens"), - named_parameter(std::make_shared(element::i32, PartialShape{-1}), "subsequence_begins"), - named_parameter(std::make_shared(element::i32, PartialShape{-1}), "block_indices_begins"), - }; - if (!m_use_per_layer_block_indices_inputs) { - auto block_indices = - named_parameter(std::make_shared(element::i32, PartialShape{-1}), "block_indices"); - model_wide_params.insert(model_wide_params.begin() + 2, block_indices); - } - - if (m_allow_score_aggregation) { - optional_model_wide_params["score_aggregation_window"] = - named_parameter(std::make_shared(element::i32, PartialShape{-1}), - "score_aggregation_window"); + m_params = PaParams{model->get_parameters()}; + m_results = PaResults{model->get_results()}; + auto max_context_len = m_params.add("max_context_len", element::i32, PartialShape{}); + m_params.add("past_lens", element::i32, PartialShape{-1}); + m_params.add("subsequence_begins", element::i32, PartialShape{-1}); + m_params.add("block_indices_begins", element::i32, PartialShape{-1}); + if (!m_options.use_per_layer_block_indices_inputs) { + m_params.add("block_indices", element::i32, PartialShape{-1}); } - if (m_allow_cache_rotation) { - optional_model_wide_params["model_rotation_trig_lut"] = - named_parameter(std::make_shared(element::f32, PartialShape{-1, -1}), "rotation_trig_lut"); - } - - if (m_allow_xattention) { - optional_model_wide_params["xattention_block_size"] = - named_parameter(std::make_shared(element::i32, PartialShape{}), "xattention_block_size"); - optional_model_wide_params["xattention_stride"] = - named_parameter(std::make_shared(element::i32, PartialShape{}), "xattention_stride"); - } - - if (m_allow_adaptive_rkv) { - optional_model_wide_params["adaptive_rkv_start_size"] = - named_parameter(std::make_shared(element::i32, PartialShape{}), "adaptive_rkv_start_size"); - optional_model_wide_params["adaptive_rkv_evictable_sizes"] = - named_parameter(std::make_shared(element::i32, PartialShape{-1}), - "adaptive_rkv_evictable_sizes"); - } - - if (m_allow_qq_bias) { - optional_model_wide_params["qq_bias"] = - named_parameter(std::make_shared(element::u8, PartialShape{-1}), "qq_bias"); - optional_model_wide_params["qq_bias_begins"] = - named_parameter(std::make_shared(element::i32, PartialShape{-1}), "qq_bias_begins"); - } - - auto get_parameter = [=](const std::shared_ptr& model, - const std::string& name) -> std::shared_ptr { - for (const auto& param : model->inputs()) { - const auto& names = param.get_names(); - if (names.count(name)) { - if (auto casted_param = ov::as_type_ptr(param.get_node_shared_ptr())) { - return casted_param; - } else { - OPENVINO_THROW("The model is in the inconsistent state. Found input '", - name, - "', but couldn't cast it to v0::Parameter."); - } - } - } - - return nullptr; - }; - std::shared_ptr input_ids_node; for (const auto& name : {"input_ids", "inputs_embeds"}) { if ((input_ids_node = get_parameter(model, name))) { @@ -148,29 +112,12 @@ bool ov::pass::SDPAToPagedAttention::run_on_model(const std::shared_ptr var_ids_to_remove; - ResultVector score_results; - ResultVector adaptive_rkv_diversity_results; - - if (auto token_type_ids_param = get_parameter(model, "token_type_ids")) { - token_type_ids_param->validate_and_infer_types(); - optional_model_wide_params["token_type_ids"] = std::move(token_type_ids_param); - } - - std::shared_ptr position_ids; - if (!get_parameter(model, "position_ids")) { - position_ids = named_parameter(std::make_shared(element::i64, PartialShape{-1}), "position_ids"); - model->add_parameters({position_ids}); + std::shared_ptr position_ids = m_params.get("position_ids"); + if (!position_ids) { + position_ids = m_params.add("position_ids", element::i64, PartialShape{-1}); } else { - position_ids = ov::as_type_ptr(model->input("position_ids").get_node_shared_ptr()); const auto& position_ids_shape = position_ids->get_partial_shape(); if (position_ids_shape.rank().is_static() && position_ids_shape.rank().get_length() == 2) { @@ -195,31 +142,15 @@ bool ov::pass::SDPAToPagedAttention::run_on_model(const std::shared_ptr(kv_parameters, - model_wide_params, - layer_index, - max_context_len->output(0), - block_indices_inputs_for_each_layer, - score_results, - m_use_per_layer_block_indices_inputs, - m_use_score_outputs, - m_allow_cache_rotation, - m_allow_score_aggregation, - m_allow_xattention, - m_allow_adaptive_rkv, - m_allow_qq_bias, - rotated_block_indices_inputs_for_each_layer, - rotation_deltas_inputs_for_each_layer, - xattention_threshold_inputs_for_each_layer, - adaptive_rkv_diversity_block_set_indices_inputs_for_each_layer, - adaptive_rkv_diversity_block_set_indices_begins_inputs_for_each_layer, - adaptive_rkv_diversity_results, - optional_model_wide_params, - var_ids_to_remove); + manager.register_pass(); // This pass is required to ensure that all GatedDeltaNet + // nodes are in the expected form before running + // PagedGatedDeltaNetFusion. + manager.register_pass(m_params, m_results, m_options, var_ids_to_remove); + manager.register_pass(); + manager.register_pass(m_params, var_ids_to_remove); + manager.register_pass(m_params, var_ids_to_remove); manager.register_pass(processed_input_ids, max_context_len, position_ids); manager.register_pass(max_context_len); manager.register_pass(max_context_len); @@ -227,6 +158,7 @@ bool ov::pass::SDPAToPagedAttention::run_on_model(const std::shared_ptr(unsqueezed_position_ids); manager.register_pass(unsqueezed_position_ids); manager.register_pass(position_ids); + manager.register_pass(position_ids); manager.run_passes(model); { @@ -265,45 +197,9 @@ bool ov::pass::SDPAToPagedAttention::run_on_model(const std::shared_ptradd_parameters(block_indices_inputs_for_each_layer); - } - - if (m_use_score_outputs) { - model->add_results(score_results); - } - - if (m_allow_score_aggregation) { - model->add_parameters({optional_model_wide_params["score_aggregation_window"]}); - } - - if (m_allow_cache_rotation) { - model->add_parameters(rotated_block_indices_inputs_for_each_layer); - model->add_parameters(rotation_deltas_inputs_for_each_layer); - model->add_parameters({optional_model_wide_params["model_rotation_trig_lut"]}); - } - - if (m_allow_xattention) { - model->add_parameters(xattention_threshold_inputs_for_each_layer); - model->add_parameters({optional_model_wide_params["xattention_block_size"]}); - model->add_parameters({optional_model_wide_params["xattention_stride"]}); - } - if (m_allow_adaptive_rkv) { - model->add_parameters({optional_model_wide_params["adaptive_rkv_start_size"]}); - model->add_parameters({optional_model_wide_params["adaptive_rkv_evictable_sizes"]}); - model->add_parameters(adaptive_rkv_diversity_block_set_indices_inputs_for_each_layer); - model->add_parameters(adaptive_rkv_diversity_block_set_indices_begins_inputs_for_each_layer); - model->add_results(adaptive_rkv_diversity_results); - } - - if (m_allow_qq_bias) { - model->add_parameters({optional_model_wide_params["qq_bias"]}); - model->add_parameters({optional_model_wide_params["qq_bias_begins"]}); - } - - model->add_parameters(kv_parameters); - model->add_parameters(model_wide_params); - model->add_parameters({std::move(max_context_len)}); + model->add_results(m_results.items()); + model->add_parameters(m_params.items()); model->validate_nodes_and_infer_types(); + return true; } diff --git a/src/core/src/pass/serialize.cpp b/src/core/src/pass/serialize.cpp index e6d663ed6b65..4fae5226d287 100644 --- a/src/core/src/pass/serialize.cpp +++ b/src/core/src/pass/serialize.cpp @@ -26,8 +26,9 @@ #include "openvino/xml_util/constant_writer.hpp" #include "openvino/xml_util/xml_serialize_util.hpp" #include "pugixml.hpp" +#include "transformations/fp16_compression/convert_legacy_precision_attribute.hpp" #include "transformations/hash.hpp" -#include "transformations/rt_info/disable_fp16_compression.hpp" +#include "transformations/rt_info/disable_precision_conversion.hpp" #include "transformations/rt_info/primitives_priority_attribute.hpp" namespace { @@ -43,15 +44,12 @@ std::filesystem::path provide_bin_path(const std::filesystem::path& xml_path) { return bin_path; } -void convert_py_rt_info(ov::Model& model) { - // TODO xxx-105807: if rt_info is set in python api as a string ['precise_0'] = '', - // we need to convert value to a class in order to have rt_info in the IR. The code below will convert - // ['precise_0'] = '' into => rt_info['precise_0'] = DisableFP16Compression{} - for (auto& node : model.get_ops()) { - if (fp16_compression_is_disabled(node)) { - disable_fp16_compression(node); - } - } +inline void convert_py_rt_info(std::shared_ptr model) { + // TODO xxx-105807/187630: if rt_info is set in python api as a string ['precise_0'] = '', + // we need to convert value to a class in order to have rt_info in the IR. The code below will convert + // ['precise_0'] = '' into => rt_info['precise_0'] = DisablePrecisionConversion{} + // Once the issue is resolved, no conversion should take place and the code below should be removed + ov::pass::ConvertLegacyPrecisionAttribute().run_on_model(model); } void serialize_func(std::ostream& xml_file, @@ -115,7 +113,7 @@ bool pass::Serialize::run_on_model(const std::shared_ptr& model) { OPENVINO_ASSERT(model, "ov::Model not provided"); model->validate_nodes_and_infer_types(); - convert_py_rt_info(*model); + convert_py_rt_info(model); if (m_xml_file && m_bin_file) { serialize_func(*m_xml_file, *m_bin_file, model, m_version); diff --git a/src/core/src/pass/stateful_to_stateless.cpp b/src/core/src/pass/stateful_to_stateless.cpp index 3e7002499e35..89dc9a129c53 100644 --- a/src/core/src/pass/stateful_to_stateless.cpp +++ b/src/core/src/pass/stateful_to_stateless.cpp @@ -33,6 +33,10 @@ struct Variable { // to hold compiled once regex for all Variable instances const std::regex naming_convention = std::regex(R"((past_key_values\.(\d+)\.(key|value))(present\.(\d+)\.(key|value)))"); + // Pattern 2: Convention for models with Linear Attention (e.g., + // cache_params.past.key.0cache_params.present.key.0) + const std::regex lin_cache_naming_convention = std::regex( + R"((cache_params)\.past\.(key|value|conv|ssm)\.(\d+)cache_params\.present\.(key|value|conv|ssm)\.(\d+))"); }; Variable(const Context& context, const std::string& variable_name) : variable_name(variable_name) { @@ -48,6 +52,41 @@ struct Variable { } else { index = -1; } + } else if (std::regex_match(variable_name, match, context.lin_cache_naming_convention)) { + // Linear Attention pattern: cache_params.past.key.0cache_params.present.key.0 + // cache_params.past.value.0cache_params.present.value.0 + // cache_params.past.ssm.0cache_params.present.ssm.0 OR/AND + // cache_params.past.conv.0cache_params.present.conv.0 + std::string prefix = match[1].str(); + std::string past_type = match[2].str(); + std::string past_idx = match[3].str(); + std::string present_type = match[4].str(); + std::string present_idx = match[5].str(); + + auto create_name = [](const std::string& type, + const std::string& idx, + const std::string& original_prefix, + const std::string& past_or_present) { + if (type == "key" || type == "value") { + std::string prefix = past_or_present == "past" ? "past_key_values" : "present"; + return prefix + "." + idx + "." + type; + } else if (type == "conv" || type == "ssm") { + return original_prefix + "." + past_or_present + "." + type + "." + idx; + } + OPENVINO_THROW("Incorrect type of state: ", type); + }; + + input_name = create_name(past_type, past_idx, prefix, "past"); + output_name = create_name(present_type, present_idx, prefix, "present"); + + if (past_idx == present_idx && past_idx.length() <= std::numeric_limits::digits10 && + past_type == present_type && (past_type == "key" || past_type == "value")) { + // NB: We enumerate only key and value caches to restore KVCache order. + // Conv and SSM caches will be placed after them in their natural order in the model. + index = std::stoi(past_idx) * 2 + int(past_type == "value"); // order key before value + } else { + index = -1; + } } else { // Variable name doesn't follow the expected naming convention. It doens't prevent forming // a correct stateless model but doesn't give a way to restore all names and inputs/outputs ordering @@ -95,6 +134,7 @@ bool ov::pass::StatefulToStateless::run_on_model(const std::shared_ptr> future_params; // to collect nodes, each with a single output that will be replaced by new parameters + std::unordered_set processed_variable_ids; // Track which ReadValues we've processed if (beam_idx) { for (const ov::Input& input : beam_idx->get_output_target_inputs(0)) { if (auto gather = ov::as_type_ptr(input.get_node()->shared_from_this())) { @@ -105,13 +145,30 @@ bool ov::pass::StatefulToStateless::run_on_model(const std::shared_ptrget_variable_id(); variables.push_back(Variable(context, variable_name)); future_params[variable_name] = gather; + processed_variable_ids.insert(variable_name); } } + model->remove_parameter(beam_idx); } else { OPENVINO_THROW( "Stateful models without `beam_idx` input are not supported in StatefulToStateless transformation"); } - model->remove_parameter(beam_idx); + + // Process ReadValues that are NOT connected via beam_idx: Conv and SSM caches in models with Linear Attention. + for (const auto& op : model->get_ops()) { + if (auto read_value = ov::as_type_ptr(op)) { + auto variable_name = read_value->get_variable_id(); + std::smatch match; + if ((processed_variable_ids.find(variable_name) == processed_variable_ids.end()) && + std::regex_match(variable_name, match, context.lin_cache_naming_convention)) { + variables.push_back(Variable(context, variable_name)); + // For models with Linear Attention, ReadValue for Conv and SSM caches connects directly to the useful + // Ops after. + future_params[variable_name] = read_value; + processed_variable_ids.insert(variable_name); + } + } + } typedef std::shared_ptr PAssign; std::unordered_map assigns_by_var_id; diff --git a/src/core/src/pass/visualize_tree.cpp b/src/core/src/pass/visualize_tree.cpp index cecf37deb85e..9d9d8fdf92ea 100644 --- a/src/core/src/pass/visualize_tree.cpp +++ b/src/core/src/pass/visualize_tree.cpp @@ -612,7 +612,7 @@ std::string ov::pass::VisualizeTree::get_attributes(std::shared_ptr node) } std::stringstream ss; - ss << " " << node->get_name() << " [" << ov::util::join(attributes, " ") << "]\n"; + ss << " " << node->get_name() << " [" << ov::util::join(attributes, " ") << "]\n"; return ss.str(); } diff --git a/src/core/src/preprocess/preprocess_impls.cpp b/src/core/src/preprocess/preprocess_impls.cpp index bd348f62539a..cc164e54730b 100644 --- a/src/core/src/preprocess/preprocess_impls.cpp +++ b/src/core/src/preprocess/preprocess_impls.cpp @@ -437,7 +437,7 @@ void OutputInfo::OutputInfoImpl::dump(std::ostream& str) const { str << "Output "; if (!start_out_node_names.empty()) { - str << "\"" << util::join(start_out_node_names) << "\""; + str << "\"" << util::join(start_out_node_names) << "\""; } str << ":" << std::endl; str << " Model's data tensor: "; diff --git a/src/core/src/preprocess/preprocess_impls.hpp b/src/core/src/preprocess/preprocess_impls.hpp index 43bf22bb9b5f..c5c7615cc875 100644 --- a/src/core/src/preprocess/preprocess_impls.hpp +++ b/src/core/src/preprocess/preprocess_impls.hpp @@ -125,7 +125,7 @@ class TensorInfoImplBase { m_names_compatiblity_mode = compatiblity_mode; } - const bool get_names_compatibility_mode() const { + bool get_names_compatibility_mode() const { return m_names_compatiblity_mode; } diff --git a/src/core/src/runtime/aligned_buffer.cpp b/src/core/src/runtime/aligned_buffer.cpp index 1cbbfc418150..632fd5606cca 100644 --- a/src/core/src/runtime/aligned_buffer.cpp +++ b/src/core/src/runtime/aligned_buffer.cpp @@ -6,48 +6,32 @@ #include #include +#include -#include "openvino/core/memory_util.hpp" +#include "openvino/util/memory.hpp" namespace ov { IBufferDescriptor::~IBufferDescriptor() = default; -AlignedBuffer::AlignedBuffer() : m_allocated_buffer(nullptr), m_aligned_buffer(nullptr), m_byte_size(0) {} +AlignedBuffer::AlignedBuffer() : m_aligned_buffer(nullptr), m_byte_size(0) {} -AlignedBuffer::AlignedBuffer(size_t byte_size, size_t alignment) : m_byte_size(byte_size) { - m_byte_size = std::max(1, byte_size); - size_t allocation_size = m_byte_size + alignment; - m_allocated_buffer = new char[allocation_size]; - m_aligned_buffer = m_allocated_buffer; - m_aligned_buffer += util::align_padding_size(alignment, reinterpret_cast(m_allocated_buffer)); +AlignedBuffer::AlignedBuffer(size_t byte_size, size_t alignment) : m_byte_size(std::max(1, byte_size)) { + m_aligned_buffer = static_cast(util::aligned_alloc(m_byte_size, alignment)); } AlignedBuffer::AlignedBuffer(AlignedBuffer&& other) - : m_allocated_buffer(other.m_allocated_buffer), - m_aligned_buffer(other.m_aligned_buffer), - m_byte_size(other.m_byte_size) { - other.m_allocated_buffer = nullptr; - other.m_aligned_buffer = nullptr; - other.m_byte_size = 0; -} + : m_aligned_buffer(std::exchange(other.m_aligned_buffer, nullptr)), + m_byte_size(std::exchange(other.m_byte_size, 0)) {} AlignedBuffer::~AlignedBuffer() { - if (m_allocated_buffer != nullptr) { - delete[] m_allocated_buffer; - } + util::aligned_free(m_aligned_buffer); // safe with nullptr } AlignedBuffer& AlignedBuffer::operator=(AlignedBuffer&& other) { if (this != &other) { - if (m_allocated_buffer != nullptr) { - delete[] m_allocated_buffer; - } - m_allocated_buffer = other.m_allocated_buffer; - m_aligned_buffer = other.m_aligned_buffer; - m_byte_size = other.m_byte_size; - other.m_allocated_buffer = nullptr; - other.m_aligned_buffer = nullptr; - other.m_byte_size = 0; + util::aligned_free(m_aligned_buffer); + m_aligned_buffer = std::exchange(other.m_aligned_buffer, nullptr); + m_byte_size = std::exchange(other.m_byte_size, 0); } return *this; } @@ -60,4 +44,18 @@ AttributeAdapter>::~AttributeAdapter() = default; std::shared_ptr AlignedBuffer::get_descriptor() const { return nullptr; } + +void AlignedBuffer::hint_evict() noexcept {} + +void AlignedBuffer::hint_evict(size_t offset, size_t size) noexcept {} + +void AlignedBuffer::invoke_evict(AlignedBuffer& buffer, size_t offset, size_t size) noexcept { + buffer.hint_evict(offset, size); +} + +void AlignedBuffer::hint_prefetch() const {} + +void AlignedBuffer::invoke_hint_prefetch(const AlignedBuffer& buffer) { + buffer.hint_prefetch(); +} } // namespace ov diff --git a/src/core/src/runtime/compute_hash.cpp b/src/core/src/runtime/compute_hash.cpp index f59ee04df530..5e12e795229c 100644 --- a/src/core/src/runtime/compute_hash.cpp +++ b/src/core/src/runtime/compute_hash.cpp @@ -834,15 +834,27 @@ size_t compute_hash(const void* src, size_t size) { constexpr uint64_t min_wa_per_thread = 131072lu; // 2^17 const uint64_t size_u64 = static_cast(size); if (size_u64 >= min_wa_per_thread * 2lu) { - static auto first_thr_kernel = Generator::mayiuse(avx512_core) - ? jit::ComputeHash::create({jit::FIRST_THREAD}) - : jit::ComputeHash::create({jit::FIRST_THREAD}); - static auto n_thr_kernel = Generator::mayiuse(avx512_core) - ? jit::ComputeHash::create({jit::N_THREAD}) - : jit::ComputeHash::create({jit::N_THREAD}); - static auto final_fold_kernel = Generator::mayiuse(avx512_core) - ? jit::ComputeHash::create({jit::FINAL_FOLD}) - : jit::ComputeHash::create({jit::FINAL_FOLD}); + // MSVC>=19.51 miscompiles static local initialization with ternary + non-trivial return type. + // Using IIFE with if/else as a workaround. Remove when MSVC fixes the bug. + // https://developercommunity.visualstudio.com/t/11105892 + static auto first_thr_kernel = [] { + if (Generator::mayiuse(avx512_core)) + return jit::ComputeHash::create({jit::FIRST_THREAD}); + else + return jit::ComputeHash::create({jit::FIRST_THREAD}); + }(); + static auto n_thr_kernel = [] { + if (Generator::mayiuse(avx512_core)) + return jit::ComputeHash::create({jit::N_THREAD}); + else + return jit::ComputeHash::create({jit::N_THREAD}); + }(); + static auto final_fold_kernel = [] { + if (Generator::mayiuse(avx512_core)) + return jit::ComputeHash::create({jit::FINAL_FOLD}); + else + return jit::ComputeHash::create({jit::FINAL_FOLD}); + }(); static const uint64_t max_thr_num = 2lu; uint64_t thr_num = std::min(size_u64 / min_wa_per_thread, max_thr_num); @@ -883,9 +895,12 @@ size_t compute_hash(const void* src, size_t size) { (*final_fold_kernel)(&args); } else { - static auto single_thr_kernel = Generator::mayiuse(avx512_core) - ? jit::ComputeHash::create({jit::SINGLE_THREAD}) - : jit::ComputeHash::create({jit::SINGLE_THREAD}); + static auto single_thr_kernel = [] { + if (Generator::mayiuse(avx512_core)) + return jit::ComputeHash::create({jit::SINGLE_THREAD}); + else + return jit::ComputeHash::create({jit::SINGLE_THREAD}); + }(); jit::ComputeHashCallArgs args; args.src_ptr = src; diff --git a/src/core/src/runtime/lazy_buffer.cpp b/src/core/src/runtime/lazy_buffer.cpp new file mode 100644 index 000000000000..97ea52e74c6e --- /dev/null +++ b/src/core/src/runtime/lazy_buffer.cpp @@ -0,0 +1,110 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "openvino/runtime/lazy_buffer.hpp" + +#include +#include +#include + +#include "openvino/core/except.hpp" +#include "openvino/core/memory_util.hpp" +#include "openvino/util/file_util.hpp" +#include "openvino/util/memory.hpp" +#include "openvino/util/parallel_read_streambuf.hpp" + +namespace ov { +LazyBuffer::LazyBuffer(std::filesystem::path file_path, size_t offset, size_t byte_size) + : AlignedBuffer(), + m_file_path{std::move(file_path)}, + m_offset{offset}, + m_loaded{false} { + m_byte_size = byte_size; + + const auto file_size = util::file_size(m_file_path); + OPENVINO_ASSERT(file_size >= 0, "Failed to get file size for ", m_file_path); + + const bool offset_fits = m_offset <= static_cast(file_size); + const bool size_fits = m_byte_size <= static_cast(file_size) - m_offset; + OPENVINO_ASSERT(offset_fits && size_fits, + "Requested region ", + m_offset, + "+", + m_byte_size, + " exceeds file size ", + file_size, + " for file: ", + m_file_path); + + std::error_code ec; + m_aligned_buffer = static_cast(util::vm_reserve(m_byte_size, ec)); + OPENVINO_ASSERT(m_aligned_buffer != nullptr, "Failed to reserve memory for LazyBuffer. Error: ", ec.message()); +} + +LazyBuffer::~LazyBuffer() { + if (m_aligned_buffer) { + util::vm_release(m_aligned_buffer, m_byte_size); + m_aligned_buffer = nullptr; + } + m_byte_size = 0; +} + +LazyBuffer::LazyBuffer(LazyBuffer&& other) noexcept + : AlignedBuffer(std::move(other)), + m_file_path{std::move(other.m_file_path)}, + m_offset{std::exchange(other.m_offset, 0)}, + m_loaded{other.m_loaded.exchange(false, std::memory_order_relaxed)} {} + +LazyBuffer& LazyBuffer::operator=(LazyBuffer&& other) noexcept { + if (this != &other) { + AlignedBuffer::operator=(std::move(other)); + m_file_path = std::move(other.m_file_path); + m_offset = std::exchange(other.m_offset, 0); + m_loaded = other.m_loaded.exchange(false, std::memory_order_relaxed); + } + return *this; +} + +void LazyBuffer::hint_prefetch() const { + if (!m_loaded.load(std::memory_order_acquire)) { + std::lock_guard lock{m_loading}; + if (m_loaded.load(std::memory_order_relaxed)) { + return; + } + + std::error_code ec; + util::vm_commit(m_aligned_buffer, m_byte_size, ec); + OPENVINO_ASSERT(!ec, "Failed to commit memory for LazyBuffer. Error: ", ec.message()); + + try { + util::ParallelReadStreamBuf par_buf(m_file_path, static_cast(m_offset)); + std::istream file(&par_buf); + OPENVINO_ASSERT(file, "Failed to open file: ", m_file_path); + file.read(m_aligned_buffer, m_byte_size); + OPENVINO_ASSERT(file, "Failed to read data from file: ", m_file_path); + m_loaded.store(true, std::memory_order_release); + } catch (...) { + util::vm_decommit(m_aligned_buffer, m_byte_size); + throw; + } + } +} + +void LazyBuffer::hint_evict() noexcept { + hint_evict(0, m_byte_size); +} + +void LazyBuffer::hint_evict(size_t offset, size_t size) noexcept { + if (m_loaded.load(std::memory_order_acquire)) { + try { + std::lock_guard lock{m_loading}; + if (m_loaded.load(std::memory_order_relaxed)) { + util::vm_decommit(m_aligned_buffer, m_byte_size); + m_loaded.store(false, std::memory_order_release); + } + } catch (...) { + } + } +} +} // namespace ov diff --git a/src/core/src/runtime/shared_buffer.cpp b/src/core/src/runtime/shared_buffer.cpp index 2a2fb9df8ac2..f2574ed778cb 100644 --- a/src/core/src/runtime/shared_buffer.cpp +++ b/src/core/src/runtime/shared_buffer.cpp @@ -7,6 +7,7 @@ #include namespace ov { + class SharedBufferDescriptor : public IBufferDescriptor { public: SharedBufferDescriptor(size_t id, size_t offset, const std::shared_ptr& source_buffer) diff --git a/src/core/src/runtime/string_aligned_buffer.cpp b/src/core/src/runtime/string_aligned_buffer.cpp index 81c366c7c654..78f35a7e96df 100644 --- a/src/core/src/runtime/string_aligned_buffer.cpp +++ b/src/core/src/runtime/string_aligned_buffer.cpp @@ -161,7 +161,7 @@ StringAlignedBuffer::StringAlignedBuffer(size_t num_elements, size_t byte_size, } StringAlignedBuffer::~StringAlignedBuffer() { - if (m_allocated_buffer) { + if (m_aligned_buffer) { const auto first = get_ptr(); for_each(first, first + m_num_elements, [](std::string& s) { using std::string; @@ -171,7 +171,6 @@ StringAlignedBuffer::~StringAlignedBuffer() { } SharedStringAlignedBuffer::SharedStringAlignedBuffer(char* ptr, size_t size) { - m_allocated_buffer = ptr; m_aligned_buffer = ptr; m_byte_size = size; m_num_elements = size / ov::element::string.size(); @@ -179,7 +178,7 @@ SharedStringAlignedBuffer::SharedStringAlignedBuffer(char* ptr, size_t size) { SharedStringAlignedBuffer::~SharedStringAlignedBuffer() { // to prevent deallocation in parent dtors. - m_allocated_buffer = nullptr; + m_aligned_buffer = nullptr; } AttributeAdapter>::AttributeAdapter( diff --git a/src/core/src/runtime/tensor.cpp b/src/core/src/runtime/tensor.cpp index c9abf18e6277..0f9761fc4ad9 100644 --- a/src/core/src/runtime/tensor.cpp +++ b/src/core/src/runtime/tensor.cpp @@ -21,6 +21,7 @@ #include "openvino/runtime/shared_buffer.hpp" #include "openvino/util/file_util.hpp" #include "openvino/util/mmap_object.hpp" +#include "openvino/util/parallel_read_streambuf.hpp" namespace ov { @@ -190,10 +191,10 @@ Shape resolve_static_shape(size_t available_size, return static_shape; } -void read_tensor_via_ifstream(const std::filesystem::path& file_name, Tensor& tensor, size_t offset) { +void read_tensor_via_istream(const std::filesystem::path& file_name, Tensor& tensor, size_t offset) { OPENVINO_ASSERT(tensor.get_element_type() != ov::element::string); - std::ifstream fin(file_name, std::ios::binary); - fin.seekg(offset); + ov::util::ParallelReadStreamBuf buffer(file_name, offset, tensor.get_byte_size()); + std::istream fin(&buffer); const auto bytes_to_read = static_cast(tensor.get_byte_size()); fin.read(static_cast(tensor.data()), bytes_to_read); OPENVINO_ASSERT(fin.gcount() == bytes_to_read, "Cannot read ", bytes_to_read, " bytes from ", file_name); @@ -210,20 +211,6 @@ ov::Tensor wrap_obj_to_viewtensor(const std::shared_ptr& shared_obj, return view_tensor; } -Tensor read_tensor_data_mmap_impl(std::shared_ptr mapped_memory, - const ov::element::Type& element_type, - const ov::PartialShape& partial_shape, - size_t offset_in_bytes) { - const auto static_shape = resolve_static_shape(mapped_memory->size(), element_type, partial_shape); - const auto shared_buffer = - std::make_shared>>(mapped_memory->data(), - mapped_memory->size(), - mapped_memory); - auto tensor = wrap_obj_to_viewtensor(shared_buffer, shared_buffer->get_ptr(), element_type, static_shape); - set_tensor_source_id(tensor, mapped_memory->get_id()); - return tensor; -} - size_t get_size_for_mapping(const ov::element::Type& element_type, const ov::PartialShape& partial_shape) { if (partial_shape.is_static()) { const auto memory_size = ov::util::get_memory_size_safe(element_type, partial_shape.get_shape()); @@ -237,6 +224,18 @@ size_t get_size_for_mapping(const ov::element::Type& element_type, const ov::Par return auto_size; } } + +Tensor read_tensor_data_mmap_impl(std::shared_ptr mapped_memory, + const element::Type& element_type, + const PartialShape& partial_shape) { + const auto static_shape = resolve_static_shape(mapped_memory->size(), element_type, partial_shape); + const auto shared_buffer = std::make_shared>>(mapped_memory->data(), + mapped_memory->size(), + mapped_memory); + auto tensor = wrap_obj_to_viewtensor(shared_buffer, shared_buffer->get_ptr(), element_type, static_shape); + set_tensor_source_id(tensor, mapped_memory->get_id()); + return tensor; +} } // namespace Tensor read_tensor_data(const std::filesystem::path& file_name, @@ -247,10 +246,9 @@ Tensor read_tensor_data(const std::filesystem::path& file_name, OPENVINO_ASSERT(element_type != ov::element::string); if (mmap) { const auto size = get_size_for_mapping(element_type, partial_shape); - return read_tensor_data_mmap_impl(load_mmap_object(file_name, offset_in_bytes, size), + return read_tensor_data_mmap_impl(load_mmap_object(file_name, offset_in_bytes, size, /*no_placeholder=*/true), element_type, - partial_shape, - offset_in_bytes); + partial_shape); } else { const auto file_size = std::filesystem::file_size(file_name); OPENVINO_ASSERT(offset_in_bytes <= file_size, @@ -261,7 +259,7 @@ Tensor read_tensor_data(const std::filesystem::path& file_name, const auto available_size = static_cast(file_size - offset_in_bytes); const auto static_shape = resolve_static_shape(available_size, element_type, partial_shape); const auto tensor = std::make_shared(element_type, static_shape); - read_tensor_via_ifstream(file_name, *tensor.get(), offset_in_bytes); + read_tensor_via_istream(file_name, *tensor.get(), offset_in_bytes); auto result = wrap_obj_to_viewtensor(tensor, tensor->data(), element_type, static_shape); set_tensor_source_id(result, util::get_id_for_file(file_name, offset_in_bytes, tensor->get_byte_size())); return result; @@ -276,7 +274,6 @@ Tensor read_tensor_data(ov::FileHandle file_handle, const auto size = get_size_for_mapping(element_type, partial_shape); return read_tensor_data_mmap_impl(load_mmap_object(file_handle, offset_in_bytes, size), element_type, - partial_shape, - offset_in_bytes); + partial_shape); } } // namespace ov diff --git a/src/core/src/shape.cpp b/src/core/src/shape.cpp index 349a1311e8e1..7eb0b56ceb3b 100644 --- a/src/core/src/shape.cpp +++ b/src/core/src/shape.cpp @@ -9,23 +9,9 @@ #include "openvino/util/common_util.hpp" std::ostream& ov::operator<<(std::ostream& s, const Shape& shape) { - s << "["; - s << ov::util::join(shape, ","); - s << "]"; - return s; + return s << "[" << ov::util::join(shape, ",") << "]"; } -namespace { -size_t stringToSizeT(const std::string& valStr) { - size_t ret{0}; - std::istringstream ss(valStr); - if (!ss.eof()) { - ss >> ret; - } - return ret; -} -} // namespace - ov::Shape::Shape() : std::vector() {} ov::Shape::Shape(const std::initializer_list& axis_lengths) : std::vector(axis_lengths) {} @@ -38,17 +24,23 @@ ov::Shape::Shape(size_t n, size_t initial_value) : std::vector(n, initia ov::Shape::Shape(const std::string& value) { auto val = ov::util::trim(value); - if (val[0] == '[' && val[val.size() - 1] == ']') - val = val.substr(1, val.size() - 2); - val = ov::util::trim(val); - std::vector dims; - std::stringstream ss(val); - std::string field; - while (getline(ss, field, ',')) { - OPENVINO_ASSERT(!field.empty(), "Cannot get vector of dimensions! \"" + value + "\" is incorrect"); - dims.insert(dims.end(), stringToSizeT(field)); + if (!val.empty() && val[0] == '[' && val[val.size() - 1] == ']') { + val.remove_prefix(1); + val.remove_suffix(1); + val = ov::util::trim(val); } - *this = dims; + + util::view_transform_if( + val, + std::back_inserter(*this), + ",", + [](auto&& dim_field) { + OPENVINO_ASSERT(!dim_field.empty(), "Cannot get vector of dimensions! \"", dim_field, "\" is incorrect"); + return true; + }, + [](auto&& dim_field) { + return util::view_to_number(util::trim(dim_field)).value_or(0); + }); } ov::Shape::~Shape() = default; diff --git a/src/core/src/shape_util.cpp b/src/core/src/shape_util.cpp index ed029e794d66..ebfaf602a61c 100644 --- a/src/core/src/shape_util.cpp +++ b/src/core/src/shape_util.cpp @@ -8,6 +8,7 @@ #include "openvino/core/partial_shape.hpp" #include "openvino/core/validation_util.hpp" +#include "openvino/util/common_util.hpp" namespace ov { template diff --git a/src/core/src/strides.cpp b/src/core/src/strides.cpp index 804af1b4ca41..5a920cb861d9 100644 --- a/src/core/src/strides.cpp +++ b/src/core/src/strides.cpp @@ -7,10 +7,7 @@ #include "openvino/util/common_util.hpp" std::ostream& ov::operator<<(std::ostream& s, const ov::Strides& strides) { - s << "Strides{"; - s << ov::util::join(strides); - s << "}"; - return s; + return s << "Strides{" << ov::util::join(strides) << "}"; } ov::Strides::Strides() : std::vector() {} diff --git a/src/core/src/weight_sharing_util.cpp b/src/core/src/weight_sharing_util.cpp index 1f231fbf30f5..cf35aa540651 100644 --- a/src/core/src/weight_sharing_util.cpp +++ b/src/core/src/weight_sharing_util.cpp @@ -99,6 +99,14 @@ std::shared_ptr Extension::get_constant_source_buffer(const o return desc ? desc->get_source_buffer() : nullptr; } +void Extension::hint_evict(ov::op::v0::Constant& constant) noexcept { + if (constant.m_data) { + if (constant.m_data->get_descriptor()) { + constant.m_data->hint_evict(); + } + } +} + std::shared_ptr get_source_buffer(const Context& shared_context, const DataID source_id) { const auto& weights = shared_context.m_cache_sources; if (auto weight_it = weights.find(source_id); weight_it != weights.end()) { diff --git a/src/core/src/xml_util/xml_serialize_util.cpp b/src/core/src/xml_util/xml_serialize_util.cpp index 2241d2655173..0cef0158fed1 100644 --- a/src/core/src/xml_util/xml_serialize_util.cpp +++ b/src/core/src/xml_util/xml_serialize_util.cpp @@ -30,7 +30,7 @@ #include "openvino/pass/constant_folding.hpp" #include "openvino/runtime/string_aligned_buffer.hpp" #include "openvino/xml_util/constant_writer.hpp" -#include "transformations/rt_info/disable_fp16_compression.hpp" +#include "transformations/rt_info/disable_precision_conversion.hpp" namespace ov::util { diff --git a/src/core/tests/CMakeLists.txt b/src/core/tests/CMakeLists.txt index 4dcf6030c8fc..362a3c2ac508 100644 --- a/src/core/tests/CMakeLists.txt +++ b/src/core/tests/CMakeLists.txt @@ -62,6 +62,12 @@ target_compile_definitions(${TARGET_NAME} FRONTEND_LIB_SUFFIX="${FRONTEND_NAME_SUFFIX}${OV_BUILD_POSTFIX}${CMAKE_SHARED_LIBRARY_SUFFIX}" ) +if(ENABLE_LTO AND CMAKE_COMPILER_IS_GNUCXX AND CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL 15) + # GCC 15's lto1 emits false-positive -Wstringop-overflow/-Wstringop-overread warnings during the LTO link phase + # when openvino_xml_util objects are processed. Suppress at the linker level for this test binary only. + target_link_options(${TARGET_NAME} PRIVATE -Wno-stringop-overflow -Wno-stringop-overread) +endif() + if(RISCV64) # for std::atomic_bool target_link_libraries(${TARGET_NAME} PRIVATE atomic) diff --git a/src/core/tests/aligned_buffer.cpp b/src/core/tests/aligned_buffer.cpp index 92d4599da51e..7ddb6bffb0a5 100644 --- a/src/core/tests/aligned_buffer.cpp +++ b/src/core/tests/aligned_buffer.cpp @@ -6,10 +6,11 @@ #include "gtest/gtest.h" -using namespace ov; +namespace ov::test { TEST(aligned_buffer, alignment) { AlignedBuffer buffer(100, 64); + ASSERT_NE(buffer.get_ptr(), nullptr); size_t addr = reinterpret_cast(buffer.get_ptr()) % 64; EXPECT_EQ(addr, 0); } @@ -33,3 +34,4 @@ TEST(aligned_buffer, move) { EXPECT_NE(buffer2.get_ptr(), nullptr); } } +} // namespace ov::test diff --git a/src/core/tests/bfloat16.cpp b/src/core/tests/bfloat16.cpp index 6389fa66a627..5b307ed7a76e 100644 --- a/src/core/tests/bfloat16.cpp +++ b/src/core/tests/bfloat16.cpp @@ -7,7 +7,9 @@ #include #include +#include #include +#include #include "common_test_utils/float_util.hpp" #include "openvino/runtime/aligned_buffer.hpp" diff --git a/src/core/tests/control_dependencies.cpp b/src/core/tests/control_dependencies.cpp index eef812564cab..fa4ec2ee6664 100644 --- a/src/core/tests/control_dependencies.cpp +++ b/src/core/tests/control_dependencies.cpp @@ -88,7 +88,7 @@ TEST(control_dependencies, cdep_ops) { auto cdop = make_shared(OutputVector{A}, std::set>{absn}); auto f = make_shared(cdop, ParameterVector{A, B}); - test_ordered_ops(f, NodeVector{absn}); + EXPECT_TRUE(test_ordered_ops(f, NodeVector{absn})); } TEST(control_dependencies, two_cdep_ops) { @@ -100,7 +100,7 @@ TEST(control_dependencies, two_cdep_ops) { auto cdop = make_shared(OutputVector{A}, std::set>{absn, absn_c}); auto f = make_shared(cdop, ParameterVector{A, B, C}); - test_ordered_ops(f, NodeVector{absn, absn_c}); + EXPECT_TRUE(test_ordered_ops(f, NodeVector{absn, absn_c})); } TEST(control_dependencies, two_cdep_ops_op_on_top) { @@ -112,7 +112,7 @@ TEST(control_dependencies, two_cdep_ops_op_on_top) { auto absn_cdop = make_shared(cdop); auto f = make_shared(absn_cdop, ParameterVector{A, B}); - test_ordered_ops(f, NodeVector{absn, absn_b}); + EXPECT_TRUE(test_ordered_ops(f, NodeVector{absn, absn_b})); } TEST(control_dependencies, clone_function_cdop) { @@ -121,7 +121,7 @@ TEST(control_dependencies, clone_function_cdop) { auto cdop = make_shared(OutputVector{A}, std::set>{absn}); auto f = make_shared(cdop, ParameterVector{A}); - test_ordered_ops(f, NodeVector{absn}); + EXPECT_TRUE(test_ordered_ops(f, NodeVector{absn})); auto clone = f->clone(); auto matcher = std::make_shared(cdop); auto cdop_clone = clone->get_results().at(0)->input_value(0).get_node_shared_ptr(); diff --git a/src/core/tests/dimension.cpp b/src/core/tests/dimension.cpp index 098442a2dc76..2259c88dcf45 100644 --- a/src/core/tests/dimension.cpp +++ b/src/core/tests/dimension.cpp @@ -7,8 +7,7 @@ #include "openvino/core/partial_shape.hpp" #include "openvino/core/symbol.hpp" -using namespace std; -using namespace ov; +namespace ov::test { TEST(dimension, broadcast_merge_static_1_and_10) { Dimension result; @@ -167,3 +166,57 @@ TEST(dimension, dimension_symbolic_equality) { ov::symbol::set_equal(A, D); EXPECT_TRUE(ov::symbol::are_equal(B, C)); } + +TEST(dimension, from_string_static) { + Dimension dim("10"); + EXPECT_TRUE(dim.is_static()); + EXPECT_EQ(dim.get_length(), 10); + EXPECT_EQ(dim.get_min_length(), 10); + EXPECT_EQ(dim.get_max_length(), 10); +} + +TEST(dimension, from_string_dynamic) { + { + Dimension dim("?"); + EXPECT_TRUE(dim.is_dynamic()); + EXPECT_EQ(dim.get_min_length(), 0); + EXPECT_EQ(dim.get_max_length(), -1); + } + { + Dimension dim("-1"); + EXPECT_TRUE(dim.is_dynamic()); + EXPECT_EQ(dim.get_min_length(), 0); + EXPECT_EQ(dim.get_max_length(), -1); + } +} + +TEST(dimension, from_string_interval) { + { + Dimension dim("2..10"); + EXPECT_TRUE(dim.is_dynamic()); + EXPECT_EQ(dim.get_min_length(), 2); + EXPECT_EQ(dim.get_max_length(), 10); + } + { + Dimension dim("..10"); + EXPECT_TRUE(dim.is_dynamic()); + EXPECT_EQ(dim.get_min_length(), 0); + EXPECT_EQ(dim.get_max_length(), 10); + } + { + Dimension dim("2.."); + EXPECT_TRUE(dim.is_dynamic()); + EXPECT_EQ(dim.get_min_length(), 2); + EXPECT_EQ(dim.get_max_length(), -1); + } +} + +TEST(dimension, from_string_interval_invalid) { + { + Dimension dim("12..2"); + EXPECT_TRUE(dim.is_dynamic()); + EXPECT_EQ(dim.get_min_length(), -1); + EXPECT_EQ(dim.get_max_length(), -1); + } +} +} // namespace ov::test diff --git a/src/core/tests/lazy_buffer.cpp b/src/core/tests/lazy_buffer.cpp new file mode 100644 index 000000000000..93b9791ea6ba --- /dev/null +++ b/src/core/tests/lazy_buffer.cpp @@ -0,0 +1,142 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "openvino/runtime/lazy_buffer.hpp" + +#include + +#include +#include + +#include "common_test_utils/common_utils.hpp" +#include "common_test_utils/test_assertions.hpp" + +using namespace testing; + +namespace ov::test { +class LazyBufferTest : public Test { +protected: + std::filesystem::path m_file_path; + std::vector m_test_data; + + void SetUp() override { + m_file_path = utils::generateTestFilePrefix(); + } + + void TearDown() override { + std::filesystem::remove(m_file_path); + } + + void write_test_data(size_t size) { + m_test_data.resize(size); + std::iota(m_test_data.begin(), m_test_data.end(), 0); + std::ofstream os(m_file_path, std::ios::binary); + os.write(m_test_data.data(), m_test_data.size()); + } + + void overwrite_test_data(size_t offset, const std::vector& data) { + ASSERT_LE(offset + data.size(), m_test_data.size()); + std::copy(data.begin(), data.end(), m_test_data.begin() + offset); + + std::fstream fs(m_file_path, std::ios::binary | std::ios::in | std::ios::out); + ASSERT_TRUE(fs.is_open()); + fs.seekp(offset); + fs.write(data.data(), data.size()); + ASSERT_TRUE(fs.good()); + } +}; + +TEST_F(LazyBufferTest, incorrect_file) { + OV_EXPECT_THROW(std::ignore = std::make_unique(std::filesystem::path{"no_file"}, 1, 2), + AssertFailure, + HasSubstr("Failed to get file size")); + + write_test_data(4); + + const auto test_params = + std::vector>{{0, 5}, {1, 4}, {4, 2}, {0, std::numeric_limits::max()}}; + for (const auto& [offset, size] : test_params) { + OV_EXPECT_THROW(std::ignore = std::make_unique(m_file_path, offset, size), + AssertFailure, + HasSubstr("exceeds file size")); + } +} + +TEST_F(LazyBufferTest, read_file) { + write_test_data(457); + const auto test_params = std::vector>{{0, 10}, + {5, 20}, + {50, 100}, + {0, m_test_data.size()}, + {14, 15}, + {128, 256}}; + for (const auto& [offset, size] : test_params) { + auto lazy_b = LazyBuffer{m_file_path, offset, size}; + AlignedBuffer& buffer = lazy_b; + char* data_ptr = nullptr; + ASSERT_NO_THROW((data_ptr = buffer.get_ptr())); + ASSERT_NE(data_ptr, nullptr); + ASSERT_EQ(buffer.size(), size); + EXPECT_THAT(std::string_view(data_ptr, size), ElementsAreArray(m_test_data.data() + offset, size)); + } +} + +TEST_F(LazyBufferTest, load_on_first_get_ptr) { + write_test_data(128); + + constexpr size_t offset = 37; + constexpr size_t size = 9; + const std::vector first_rewrite{'L', 'A', 'Z', 'Y', ' ', 'D', 'A', 'T', 'A'}; + const std::vector second_rewrite{'O', 'V', 'E', 'R', 'W', 'R', 'I', 'T', 'E'}; + + std::unique_ptr buffer = std::make_unique(m_file_path, offset, size); + + // If constructor eagerly reads file data, get_ptr() would return original bytes instead of first_rewrite. + overwrite_test_data(offset, first_rewrite); + + char* first_ptr = nullptr; + ASSERT_NO_THROW((first_ptr = buffer->get_ptr())); + ASSERT_NE(first_ptr, nullptr); + ASSERT_TRUE(std::equal(first_ptr, first_ptr + size, first_rewrite.begin())); + + // Once loaded, subsequent get_ptr() calls should return cached memory and ignore later file overwrites. + overwrite_test_data(offset, second_rewrite); + + char* second_ptr = nullptr; + ASSERT_NO_THROW((second_ptr = buffer->get_ptr())); + ASSERT_EQ(second_ptr, first_ptr); + EXPECT_THAT(first_rewrite, ElementsAreArray(second_ptr, size)); +} + +TEST_F(LazyBufferTest, evict_and_reload) { + write_test_data(128); + + constexpr size_t offset = 31; + constexpr size_t size = 9; + const std::vector first_rewrite{'L', 'A', 'Z', 'Y', ' ', 'D', 'A', 'T', 'A'}; + const std::vector second_rewrite{'O', 'V', 'E', 'R', 'W', 'R', 'I', 'T', 'E'}; + + const auto buffer = std::make_unique(m_file_path, offset, size); + + overwrite_test_data(offset, first_rewrite); + char* first_ptr = nullptr; + ASSERT_NO_THROW((first_ptr = buffer->get_ptr())); + ASSERT_NE(first_ptr, nullptr); + ASSERT_THAT(first_rewrite, ElementsAreArray(first_ptr, size)); + + // After evict(), get_ptr() should load the same file content again + buffer->hint_evict(); + ASSERT_NO_THROW((first_ptr = buffer->get_ptr())); + ASSERT_NE(first_ptr, nullptr); + ASSERT_THAT(first_rewrite, ElementsAreArray(first_ptr, size)); + + // After evict(), get_ptr() should load the file content again and reflect the second overwrite. + buffer->hint_evict(); + overwrite_test_data(offset, second_rewrite); + char* second_ptr = nullptr; + ASSERT_NO_THROW((second_ptr = buffer->get_ptr())); + ASSERT_EQ(second_ptr, first_ptr); + EXPECT_THAT(second_rewrite, ElementsAreArray(second_ptr, size)); +} +} // namespace ov::test diff --git a/src/core/tests/mmap_object.cpp b/src/core/tests/mmap_object.cpp index 5d9d6b8b3a7a..ec4dfb3eadbd 100644 --- a/src/core/tests/mmap_object.cpp +++ b/src/core/tests/mmap_object.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include "common_test_utils/common_utils.hpp" @@ -178,34 +179,38 @@ TEST_P(RangedMappingTest, compare_id) { std::filesystem::copy_file(m_file_path, other_file_path, ec); ASSERT_FALSE(ec) << "Failed to copy file \"" << m_file_path << "\" for test setup: " << ec.message(); - std::shared_ptr mm_1, mm_2, other_mm_1, other_mm_2, mm_1_; - if (use_file_path) { - mm_1 = load_mmap_object(m_file_path, offset_1, size_1); - mm_2 = load_mmap_object(m_file_path, offset_2, size_2); - other_mm_1 = load_mmap_object(other_file_path, offset_1, size_1); - other_mm_2 = load_mmap_object(other_file_path, offset_2, size_2); - mm_1_ = load_mmap_object(m_file_path, offset_1, size_1); - } else { - const auto handle = utils::open_ro_file(m_file_path); - mm_1 = load_mmap_object(handle, offset_1, size_1); - mm_2 = load_mmap_object(handle, offset_2, size_2); - const auto other_handle = utils::open_ro_file(other_file_path); - other_mm_1 = load_mmap_object(other_handle, offset_1, size_1); - other_mm_2 = load_mmap_object(other_handle, offset_2, size_2); - const auto handle_ = utils::open_ro_file(m_file_path); - mm_1_ = load_mmap_object(handle_, offset_1, size_1); - } + { + std::shared_ptr mm_1, mm_2, other_mm_1, other_mm_2, mm_1_; + if (use_file_path) { + mm_1 = load_mmap_object(m_file_path, offset_1, size_1); + mm_2 = load_mmap_object(m_file_path, offset_2, size_2); + other_mm_1 = load_mmap_object(other_file_path, offset_1, size_1); + other_mm_2 = load_mmap_object(other_file_path, offset_2, size_2); + mm_1_ = load_mmap_object(m_file_path, offset_1, size_1); + } else { + const auto handle = utils::open_ro_file(m_file_path); + mm_1 = load_mmap_object(handle, offset_1, size_1); + mm_2 = load_mmap_object(handle, offset_2, size_2); + const auto other_handle = utils::open_ro_file(other_file_path); + other_mm_1 = load_mmap_object(other_handle, offset_1, size_1); + other_mm_2 = load_mmap_object(other_handle, offset_2, size_2); + const auto handle_ = utils::open_ro_file(m_file_path); + mm_1_ = load_mmap_object(handle_, offset_1, size_1); + } - ASSERT_NE(mm_1, nullptr); - ASSERT_NE(mm_2, nullptr); - ASSERT_NE(other_mm_1, nullptr); - ASSERT_NE(other_mm_2, nullptr); - ASSERT_NE(mm_1_, nullptr); + ASSERT_NE(mm_1, nullptr); + ASSERT_NE(mm_2, nullptr); + ASSERT_NE(other_mm_1, nullptr); + ASSERT_NE(other_mm_2, nullptr); + ASSERT_NE(mm_1_, nullptr); + + EXPECT_NE(mm_1->get_id(), mm_2->get_id()); + EXPECT_NE(mm_1->get_id(), other_mm_1->get_id()); + EXPECT_NE(mm_2->get_id(), other_mm_2->get_id()); + EXPECT_EQ(mm_1->get_id(), mm_1_->get_id()); + } - EXPECT_NE(mm_1->get_id(), mm_2->get_id()); - EXPECT_NE(mm_1->get_id(), other_mm_1->get_id()); - EXPECT_NE(mm_2->get_id(), other_mm_2->get_id()); - EXPECT_EQ(mm_1->get_id(), mm_1_->get_id()); + std::filesystem::remove(other_file_path); } static const auto pg_sz = []() { @@ -226,4 +231,293 @@ INSTANTIATE_TEST_SUITE_P(MappedMemory, ::testing::ValuesIn(std::vector{true, false})), RangedMappingTest::test_name); +class HintEvictTest : public ::testing::Test { +protected: + std::filesystem::path m_file_path; + std::vector m_expected; + // 10 granules (10 × 64 KiB): partial_evict uses quarter = 160 KiB + static constexpr size_t k_hint_evict_file_size = 64 * 1024 * 10; + + void SetUp() override { + m_expected.resize(k_hint_evict_file_size); + for (size_t i = 0; i < k_hint_evict_file_size; ++i) { + // Prime-modulo pattern: easy to spot any byte corruption in failure output. + m_expected[i] = static_cast(i % 251); + } + m_file_path = utils::generateTestFilePrefix() + "_hint_evict"; + std::ofstream out(m_file_path, std::ios::binary); + ASSERT_TRUE(out.is_open()) << "Failed to create temp file: " << m_file_path; + out.write(reinterpret_cast(m_expected.data()), k_hint_evict_file_size); + ASSERT_TRUE(out.good()); + } + + void TearDown() override { + std::filesystem::remove(m_file_path); + } + + static std::vector read_mapped(MappedMemory& mm) { + return {reinterpret_cast(mm.data()), reinterpret_cast(mm.data()) + mm.size()}; + } +}; + +TEST_F(HintEvictTest, full_evict_then_read_matches_original) { + auto mm = load_mmap_object(m_file_path); + ASSERT_NE(mm, nullptr); + ASSERT_EQ(mm->size(), k_hint_evict_file_size); + + // Verify initial content before eviction. + ASSERT_EQ(read_mapped(*mm), m_expected); + + // Evict all mapped pages. + mm->hint_evict(0, auto_size); + + // All bytes must be transparently restored and unchanged. + EXPECT_EQ(read_mapped(*mm), m_expected); +} + +TEST_F(HintEvictTest, partial_evict_then_read_matches_original) { + auto mm = load_mmap_object(m_file_path); + ASSERT_NE(mm, nullptr); + ASSERT_EQ(mm->size(), k_hint_evict_file_size); + + const size_t quarter = k_hint_evict_file_size / 4; + mm->hint_evict(quarter, k_hint_evict_file_size / 2); + + EXPECT_EQ(read_mapped(*mm), m_expected); +} + +TEST_F(HintEvictTest, multiple_evict_cycles_are_idempotent) { + // Use a page-aligned but not granularity-aligned offset to exercise head_pad on each cycle. + constexpr size_t k_offset = 4096; + auto mm = load_mmap_object(m_file_path, k_offset, auto_size); + ASSERT_NE(mm, nullptr); + ASSERT_EQ(mm->size(), k_hint_evict_file_size - k_offset); + + const std::vector expected_slice(m_expected.begin() + k_offset, m_expected.end()); + + for (int cycle = 0; cycle < 3; ++cycle) { + mm->hint_evict(0, auto_size); + EXPECT_EQ(read_mapped(*mm), expected_slice) << "Data mismatch after evict cycle " << cycle; + } +} + +TEST_F(HintEvictTest, evict_then_read_via_file_handle_matches_original) { + const auto handle = utils::open_ro_file(m_file_path); + auto mm = load_mmap_object(handle, 0, auto_size); + ASSERT_NE(mm, nullptr); + ASSERT_EQ(mm->size(), k_hint_evict_file_size); + + mm->hint_evict(0, auto_size); + + EXPECT_EQ(read_mapped(*mm), m_expected); +} + +TEST_F(HintEvictTest, evict_with_anonymous_tail_matches_original) { + // Append extra bytes so the file size is not a multiple of the 64 KiB granularity. + constexpr size_t k_extra = 4096; + m_expected.resize(k_hint_evict_file_size + k_extra); + for (size_t i = k_hint_evict_file_size; i < m_expected.size(); ++i) + m_expected[i] = static_cast(i % 251); + { + std::ofstream out(m_file_path, std::ios::binary | std::ios::trunc); + ASSERT_TRUE(out.is_open()); + out.write(reinterpret_cast(m_expected.data()), m_expected.size()); + ASSERT_TRUE(out.good()); + } + + auto mm = load_mmap_object(m_file_path); + ASSERT_NE(mm, nullptr); + ASSERT_EQ(mm->size(), m_expected.size()); + + mm->hint_evict(0, auto_size); + + EXPECT_EQ(read_mapped(*mm), m_expected); +} + +TEST_F(HintEvictTest, evict_with_nonzero_offset_matches_original) { + // Use an offset that is page-aligned but NOT granularity-aligned. + constexpr size_t k_offset = 4096; + ASSERT_LT(k_offset, k_hint_evict_file_size); + + auto mm = load_mmap_object(m_file_path, k_offset, auto_size); + ASSERT_NE(mm, nullptr); + ASSERT_EQ(mm->size(), k_hint_evict_file_size - k_offset); + + const std::vector expected_slice(m_expected.begin() + k_offset, m_expected.end()); + + mm->hint_evict(0, auto_size); + + EXPECT_EQ(read_mapped(*mm), expected_slice); +} + +class HintPrefetchTest : public ::testing::Test { +protected: + std::filesystem::path m_file_path; + + void TearDown() override { + std::filesystem::remove(m_file_path); + } + + static std::vector read_mapped(MappedMemory& mm) { + return {reinterpret_cast(mm.data()), reinterpret_cast(mm.data()) + mm.size()}; + } +}; + +TEST_F(HintPrefetchTest, parallel_prefault_whole_file) { + m_file_path = std::filesystem::path(utils::generateTestFilePrefix() + "_prefault_test.bin"); + constexpr size_t file_size = 5 * 1024 * 1024; // 5 MiB (above 4 MiB threshold) + std::vector data(file_size); + for (size_t i = 0; i < file_size; ++i) + data[i] = static_cast(i % 251); + + { + std::ofstream f(m_file_path, std::ios::binary); + f.write(reinterpret_cast(data.data()), data.size()); + } + + { + auto mapped = load_mmap_object(m_file_path); + ASSERT_NE(mapped, nullptr); + EXPECT_EQ(mapped->size(), file_size); + + EXPECT_NO_THROW(mapped->hint_prefetch()); + + EXPECT_EQ(read_mapped(*mapped), data); + } +} + +TEST_F(HintPrefetchTest, parallel_prefault_partial_region) { + m_file_path = std::filesystem::path(utils::generateTestFilePrefix() + "_prefault_partial.bin"); + constexpr size_t file_size = 8 * 1024 * 1024; // 8 MB + constexpr size_t prefault_offset = 1 * 1024 * 1024; + constexpr size_t prefault_size = 5 * 1024 * 1024; + std::vector data(file_size); + for (size_t i = 0; i < file_size; ++i) + data[i] = static_cast(i % 251); + + { + std::ofstream f(m_file_path, std::ios::binary); + f.write(reinterpret_cast(data.data()), data.size()); + } + + { + auto mapped = load_mmap_object(m_file_path); + ASSERT_NE(mapped, nullptr); + + EXPECT_NO_THROW(mapped->hint_prefetch(prefault_offset, prefault_size)); + + EXPECT_EQ(read_mapped(*mapped), data); + } +} + +TEST_F(HintPrefetchTest, parallel_prefault_below_threshold_is_noop) { + m_file_path = std::filesystem::path(utils::generateTestFilePrefix() + "_prefault_small.bin"); + constexpr size_t file_size = 1024; // 1 KB - below threshold + std::vector data(file_size); + for (size_t i = 0; i < file_size; ++i) + data[i] = static_cast(i % 251); + + { + std::ofstream f(m_file_path, std::ios::binary); + f.write(reinterpret_cast(data.data()), data.size()); + } + + { + auto mapped = load_mmap_object(m_file_path); + ASSERT_NE(mapped, nullptr); + EXPECT_NO_THROW(mapped->hint_prefetch()); // no optimization + EXPECT_EQ(read_mapped(*mapped), data); + } +} + +TEST_F(HintPrefetchTest, parallel_prefault_with_file_offset) { + m_file_path = std::filesystem::path(utils::generateTestFilePrefix() + "_prefault_offset.bin"); + constexpr size_t file_size = 10 * 1024 * 1024; // 10 MB + constexpr size_t map_offset = 2 * 1024 * 1024; // Map starting at 2 MB into file + std::vector data(file_size); + for (size_t i = 0; i < file_size; ++i) + data[i] = static_cast(i % 251); + + { + std::ofstream f(m_file_path, std::ios::binary); + f.write(reinterpret_cast(data.data()), data.size()); + } + + { + auto mapped = load_mmap_object(m_file_path, map_offset); + ASSERT_NE(mapped, nullptr); + EXPECT_EQ(mapped->size(), file_size - map_offset); + + EXPECT_NO_THROW(mapped->hint_prefetch()); + + EXPECT_EQ(read_mapped(*mapped), std::vector(data.begin() + map_offset, data.end())); + } +} + +TEST_F(HintPrefetchTest, hint_prefetch_with_both_offsets) { + m_file_path = std::filesystem::path(utils::generateTestFilePrefix() + "_prefault_both_offsets.bin"); + constexpr size_t file_size = 12 * 1024 * 1024; // 12 MB + constexpr size_t map_offset = 2 * 1024 * 1024; // Map starting at 2 MB into file + constexpr size_t pop_offset = 1 * 1024 * 1024; // Populate starting at 1 MB into mapping + constexpr size_t pop_size = 5 * 1024 * 1024; // Populate 5 MB + std::vector data(file_size); + for (size_t i = 0; i < file_size; ++i) + data[i] = static_cast(i % 251); + + { + std::ofstream f(m_file_path, std::ios::binary); + f.write(reinterpret_cast(data.data()), data.size()); + } + + { + auto mapped = load_mmap_object(m_file_path, map_offset); + ASSERT_NE(mapped, nullptr); + EXPECT_EQ(mapped->size(), file_size - map_offset); + + EXPECT_NO_THROW(mapped->hint_prefetch(pop_offset, pop_size)); + + EXPECT_EQ(read_mapped(*mapped), std::vector(data.begin() + map_offset, data.end())); + } +} + +// Investigates whether calling hint_prefetch(offset, size) and POSIX_FADV_SEQUENTIAL +// on a subregion of an already-cached file evicts pages *outside* that region +TEST_F(HintPrefetchTest, hint_prefetch_sequential_eviction_check) { +#ifndef __linux__ + GTEST_SKIP() << "utils::count_resident_pages is not implemented on this platform yet CVS-186579"; +#endif + constexpr size_t file_size = 128 * 1024 * 1024; + + constexpr size_t prefetch_offset = 80 * 1024 * 1024; + constexpr size_t prefetch_size = 16 * 1024 * 1024; + + constexpr size_t prefix_mb = 64; + constexpr size_t prefix_size = prefix_mb * 1024 * 1024; + + const size_t page = static_cast(util::get_system_page_size()); + const size_t total_prefix_pages = prefix_size / page; + + m_file_path = std::filesystem::path(utils::generateTestFilePrefix() + "_file.bin"); + { + std::vector data(file_size); + for (size_t i = 0; i < file_size; ++i) + data[i] = static_cast(i % 251); + std::ofstream f(m_file_path, std::ios::binary); + f.write(reinterpret_cast(data.data()), data.size()); + } + + auto mapped = load_mmap_object(m_file_path); + volatile char sink = 0; + for (size_t i = 0; i < prefix_size; i += page) { + sink += mapped->data()[i]; + } + const size_t pages_before = utils::count_resident_pages(mapped->data(), prefix_size); + ASSERT_EQ(pages_before, total_prefix_pages) + << "Expected all " << total_prefix_pages << " prefix pages resident after warmup, but found " << pages_before; + + mapped->hint_prefetch(prefetch_offset, prefetch_size); + const size_t pages_after = utils::count_resident_pages(mapped->data(), prefix_size); + EXPECT_EQ(pages_after, pages_before) << "hint_prefetch evicted pages."; +} + } // namespace ov::test diff --git a/src/core/tests/pass/serialization/deterministicity.cpp b/src/core/tests/pass/serialization/deterministicity.cpp index 8b02573d2f69..fd03657c85ec 100644 --- a/src/core/tests/pass/serialization/deterministicity.cpp +++ b/src/core/tests/pass/serialization/deterministicity.cpp @@ -76,7 +76,7 @@ class SerializationDeterministicityTest : public ov::test::TestsCommon, public D TEST_F(SerializationDeterministicityTest, BasicModel) { const std::string model = - ov::test::utils::getModelFromTestModelZoo(ov::util::path_join({SERIALIZED_ZOO, "ir/add_abc.onnx"}).string()); + ov::test::utils::getModelFromTestModelZoo(ov::util::path_join({SERIALIZED_ZOO, "ir", "add_abc.onnx"})); auto expected = ov::test::readModel(model, ""); ov::pass::Serialize(m_out_xml_path_1, m_out_bin_path_1).run_on_model(expected); @@ -93,7 +93,7 @@ TEST_F(SerializationDeterministicityTest, BasicModel) { TEST_F(SerializationDeterministicityTest, ModelWithMultipleLayers) { const std::string model = - ov::test::utils::getModelFromTestModelZoo(ov::util::path_join({SERIALIZED_ZOO, "ir/addmul_abc.onnx"}).string()); + ov::test::utils::getModelFromTestModelZoo(ov::util::path_join({SERIALIZED_ZOO, "ir", "addmul_abc.onnx"})); auto expected = ov::test::readModel(model, ""); ov::pass::Serialize(m_out_xml_path_1, m_out_bin_path_1).run_on_model(expected); @@ -112,9 +112,9 @@ TEST_F(SerializationDeterministicityTest, ModelWithMultipleLayers) { TEST_F(SerializationDeterministicityTest, ModelWithMultipleOutputs) { const std::string model = ov::test::utils::getModelFromTestModelZoo( - ov::util::path_join({SERIALIZED_ZOO, "ir/split_equal_parts_2d.xml"}).string()); + ov::util::path_join({SERIALIZED_ZOO, "ir", "split_equal_parts_2d.xml"})); const std::string weights = ov::test::utils::getModelFromTestModelZoo( - ov::util::path_join({SERIALIZED_ZOO, "ir/split_equal_parts_2d.bin"}).string()); + ov::util::path_join({SERIALIZED_ZOO, "ir", "split_equal_parts_2d.bin"})); auto expected = ov::test::readModel(model, weights); ov::pass::Serialize(m_out_xml_path_1, m_out_bin_path_1).run_on_model(expected); @@ -131,9 +131,9 @@ TEST_F(SerializationDeterministicityTest, ModelWithMultipleOutputs) { TEST_F(SerializationDeterministicityTest, ModelWithConstants) { const std::string model = ov::test::utils::getModelFromTestModelZoo( - ov::util::path_join({SERIALIZED_ZOO, "ir/add_abc_initializers.xml"}).string()); + ov::util::path_join({SERIALIZED_ZOO, "ir", "add_abc_initializers.xml"})); const std::string weights = ov::test::utils::getModelFromTestModelZoo( - ov::util::path_join({SERIALIZED_ZOO, "ir/add_abc_initializers.bin"}).string()); + ov::util::path_join({SERIALIZED_ZOO, "ir", "add_abc_initializers.bin"})); auto expected = ov::test::readModel(model, weights); ov::pass::Serialize(m_out_xml_path_1, m_out_bin_path_1).run_on_model(expected); @@ -149,8 +149,8 @@ TEST_F(SerializationDeterministicityTest, ModelWithConstants) { } TEST_F(SerializationDeterministicityTest, ModelWithVariable) { - const auto model = ov::test::utils::getModelFromTestModelZoo( - ov::util::path_join({SERIALIZED_ZOO, "ir/dynamic_variable.xml"}).string()); + const auto model = + ov::test::utils::getModelFromTestModelZoo(ov::util::path_join({SERIALIZED_ZOO, "ir", "dynamic_variable.xml"})); auto expected = ov::test::readModel(model, ""); ov::pass::Serialize(m_out_xml_path_1, m_out_bin_path_1).run_on_model(expected); diff --git a/src/core/tests/pass/serialization/from_model.cpp b/src/core/tests/pass/serialization/from_model.cpp index cede103348ea..675671011105 100644 --- a/src/core/tests/pass/serialization/from_model.cpp +++ b/src/core/tests/pass/serialization/from_model.cpp @@ -5,16 +5,19 @@ #include #include +#include #include "common_test_utils/common_utils.hpp" #include "common_test_utils/graph_comparator.hpp" #include "common_test_utils/test_common.hpp" #include "openvino/op/add.hpp" +#include "openvino/op/clamp.hpp" #include "openvino/op/if.hpp" #include "openvino/op/relu.hpp" #include "openvino/op/split.hpp" #include "openvino/op/subtract.hpp" #include "openvino/pass/serialize.hpp" +#include "openvino/runtime/core.hpp" #include "openvino/util/file_util.hpp" #include "read_ir.hpp" @@ -146,6 +149,28 @@ INSTANTIATE_TEST_SUITE_P(IRSerializationFromModel, SerializationFromModelTest, testing::ValuesIn(get_models()), SerializationFromModelTest::getTestCaseName); + +// Clamp with inf bounds: verifies that -inf/inf attributes survive XML round-trip +TEST(IRSerializationFromModel, ClampWithInfBounds) { + using namespace ov; + auto param = std::make_shared(element::f32, Shape{2, 4}); + param->output(0).get_tensor().set_names({"X"}); + auto clamp = std::make_shared(param, + -std::numeric_limits::infinity(), + std::numeric_limits::infinity()); + auto res = std::make_shared(clamp); + auto expected = std::make_shared(OutputVector{res}, ParameterVector{param}); + + std::stringstream model_ss, weights_ss; + pass::Serialize(model_ss, weights_ss).run_on_model(expected); + + ov::Core core; + auto result = core.read_model(model_ss.str(), ov::Tensor()); + + const auto fc = FunctionsComparator::with_default().enable(FunctionsComparator::ATTRIBUTES); + const auto cmp = fc.compare(result, expected); + EXPECT_TRUE(cmp.valid) << cmp.message; +} } // namespace class SerializationFromModelTest_large : public ov::test::TestsCommon, public testing::WithParamInterface { diff --git a/src/core/tests/pass/serialization/rt_info_serialization.cpp b/src/core/tests/pass/serialization/rt_info_serialization.cpp index 0353c5da31e5..5c37222ba6ce 100644 --- a/src/core/tests/pass/serialization/rt_info_serialization.cpp +++ b/src/core/tests/pass/serialization/rt_info_serialization.cpp @@ -125,10 +125,16 @@ TEST_F(RTInfoSerializationTest, all_attributes_latest) { TEST_F(RTInfoSerializationTest, rt_info_precise_test) { auto init_info = [](ov::RTMap& info) { + OPENVINO_SUPPRESS_DEPRECATED_START info[ov::DisableFP16Compression::get_type_info_static()] = ov::DisableFP16Compression{}; + OPENVINO_SUPPRESS_DEPRECATED_END }; auto check_info = [](const ov::RTMap& info) { - const std::string& key = ov::DisableFP16Compression::get_type_info_static(); + OPENVINO_SUPPRESS_DEPRECATED_START + const std::string& legacy_key = ov::DisableFP16Compression::get_type_info_static(); + OPENVINO_SUPPRESS_DEPRECATED_END + const std::string& key = ov::DisablePrecisionConversion::get_type_info_static(); + ASSERT_FALSE(info.count(legacy_key)); ASSERT_TRUE(info.count(key)); }; @@ -151,6 +157,165 @@ TEST_F(RTInfoSerializationTest, rt_info_precise_test) { check_info(matmul->get_rt_info()); } +TEST_F(RTInfoSerializationTest, rt_info_disable_precision_conversion_roundtrip) { + // Set the new attribute directly (not via legacy), serialize, read back, verify contents + std::shared_ptr model; + { + auto data_1 = std::make_shared(ov::element::f32, ov::Shape{1, 10}); + auto data_2 = std::make_shared(ov::element::f32, ov::Shape{10, 1}); + auto matmul = std::make_shared(data_1, data_2); + ov::disable_conversion(matmul, ov::element::f16); + ov::disable_conversion(matmul, ov::element::f32, ov::element::bf16); + ov::disable_conversion(matmul, ov::element::f32, ov::element::f16); + auto result = std::make_shared(matmul); + model = std::make_shared(ov::ResultVector{result}, ov::ParameterVector{data_1, data_2}); + } + + ov::pass::Manager m; + m.register_pass(m_out_xml_path, m_out_bin_path); + m.run_passes(model); + + auto f = getWithIRFrontend(m_out_xml_path, m_out_bin_path); + ASSERT_NE(nullptr, f); + + auto matmul = f->get_results()[0]->get_input_node_ptr(0); + const auto& rt_info = matmul->get_rt_info(); + + OPENVINO_SUPPRESS_DEPRECATED_START + ASSERT_FALSE(rt_info.count(ov::DisableFP16Compression::get_type_info_static())); + OPENVINO_SUPPRESS_DEPRECATED_END + + ASSERT_TRUE(rt_info.count(ov::DisablePrecisionConversion::get_type_info_static())); + + const auto& attr = + rt_info.at(ov::DisablePrecisionConversion::get_type_info_static()).as(); + const auto& precisions = attr.m_disabled_precisions; + + // Check dynamic -> {f16} + auto it_dynamic = precisions.find(ov::element::dynamic); + ASSERT_NE(it_dynamic, precisions.end()); + ASSERT_EQ(it_dynamic->second.size(), 1); + ASSERT_TRUE(it_dynamic->second.count(ov::element::f16)); + + // Check f32 -> {bf16, f16} + auto it_f32 = precisions.find(ov::element::f32); + ASSERT_NE(it_f32, precisions.end()); + ASSERT_EQ(it_f32->second.size(), 2); + ASSERT_TRUE(it_f32->second.count(ov::element::bf16)); + ASSERT_TRUE(it_f32->second.count(ov::element::f16)); +} + +// Pins the serialization format "from:to1,to2;from2:to3,to4". Do not change without updating model compatibility. +TEST(RTInfoSerialization, disable_precision_conversion_xml_format) { + std::string ref_ir_xml = R"V0G0N( + + + + + + + 1 + 10 + + + + + + + + 10 + 1 + + + + + + + + + + + 1 + 10 + + + 10 + 1 + + + + + 1 + 1 + + + + + + + 1 + 1 + + + + + + + + + + + +)V0G0N"; + + ov::Core core; + auto model = core.read_model(ref_ir_xml, ov::Tensor()); + ASSERT_NE(nullptr, model); + + auto matmul = model->get_results()[0]->get_input_node_ptr(0); + const auto& rt_info = matmul->get_rt_info(); + + ASSERT_TRUE(rt_info.count(ov::DisablePrecisionConversion::get_type_info_static())); + + const auto& attr = + rt_info.at(ov::DisablePrecisionConversion::get_type_info_static()).as(); + const auto& precisions = attr.m_disabled_precisions; + + // Verify dynamic -> {f16} + auto it_dynamic = precisions.find(ov::element::dynamic); + ASSERT_NE(it_dynamic, precisions.end()); + ASSERT_EQ(it_dynamic->second.size(), 1); + ASSERT_TRUE(it_dynamic->second.count(ov::element::f16)); + + // Verify f32 -> {bf16, f16} + auto it_f32 = precisions.find(ov::element::f32); + ASSERT_NE(it_f32, precisions.end()); + ASSERT_EQ(it_f32->second.size(), 2); + ASSERT_TRUE(it_f32->second.count(ov::element::bf16)); + ASSERT_TRUE(it_f32->second.count(ov::element::f16)); + + // Verify serialization produces the same XML + { + auto data_1 = std::make_shared(ov::element::f32, ov::Shape{1, 10}); + auto data_2 = std::make_shared(ov::element::f32, ov::Shape{10, 1}); + auto matmul = std::make_shared(data_1, data_2); + ov::disable_conversion(matmul, ov::element::f16); + ov::disable_conversion(matmul, ov::element::f32, ov::element::bf16); + ov::disable_conversion(matmul, ov::element::f32, ov::element::f16); + auto result = std::make_shared(matmul); + auto built_model = std::make_shared(ov::ResultVector{result}, ov::ParameterVector{data_1, data_2}); + built_model->set_friendly_name("TestModel"); + data_1->set_friendly_name("input_1"); + data_2->set_friendly_name("input_2"); + matmul->set_friendly_name("matmul"); + result->set_friendly_name("result"); + + std::stringstream model_ss, weights_ss; + ov::pass::Serialize(model_ss, weights_ss).run_on_model(built_model); + EXPECT_EQ(ref_ir_xml, model_ss.str()); + } +} + TEST_F(RTInfoSerializationTest, all_attributes_v10) { auto init_info = [](ov::RTMap& info) { info[ov::FusedNames::get_type_info_static()] = ov::FusedNames("add"); diff --git a/src/core/tests/pass/serialization/serialize.cpp b/src/core/tests/pass/serialization/serialize.cpp index a200601faef5..f859b753a361 100644 --- a/src/core/tests/pass/serialization/serialize.cpp +++ b/src/core/tests/pass/serialization/serialize.cpp @@ -120,10 +120,10 @@ class SerializationTest : public ov::test::TestsCommon, public testing::WithPara void SetUp() override { m_model_path = ov::test::utils::getModelFromTestModelZoo( - ov::util::path_join({SERIALIZED_ZOO, "ir/", std::get<0>(GetParam())}).string()); + ov::util::path_join({SERIALIZED_ZOO, "ir", std::get<0>(GetParam())})); if (!std::get<1>(GetParam()).empty()) { m_binary_path = ov::test::utils::getModelFromTestModelZoo( - ov::util::path_join({SERIALIZED_ZOO, "ir/", std::get<1>(GetParam())}).string()); + ov::util::path_join({SERIALIZED_ZOO, "ir", std::get<1>(GetParam())})); } std::string filePrefix = ov::test::utils::generateTestFilePrefix(); diff --git a/src/core/tests/reference/paged_cache_manager.cpp b/src/core/tests/reference/paged_cache_manager.cpp new file mode 100644 index 000000000000..dc534fbf4ae9 --- /dev/null +++ b/src/core/tests/reference/paged_cache_manager.cpp @@ -0,0 +1,2056 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "openvino/reference/utils/paged_cache_manager.hpp" + +#include + +#include +#include +#include +#include + +#include "openvino/reference/paged_attention.hpp" + +using namespace ov::reference::paged_attention_cache; + +namespace { + +// helper to build a zero-filled cache shape [num_blocks, kv_heads, block_size, head_size] +struct CacheLayout { + std::size_t num_blocks; + std::size_t kv_heads; + std::size_t block_size; + std::size_t head_size; + + ov::Shape shape() const { + return {num_blocks, kv_heads, block_size, head_size}; + } + std::size_t elems() const { + return num_blocks * kv_heads * block_size * head_size; + } +}; + +// shorthand node key +// it is a random value that pretends to be a PagedAttention operator ID +constexpr std::uintptr_t NODE = 67; + +// register a simple operator state and return the manager +std::unique_ptr make_manager(CacheLayout layout, + EvictionPolicy policy, + std::vector& key_data, + std::vector& val_data, + std::size_t seq_count = 1) { + key_data.assign(layout.elems(), 0.f); + val_data.assign(layout.elems(), 0.f); + + auto mgr = std::make_unique(ov::element::f32, policy, /*max_cache_bytes=*/0); + + // initial past_lens = 0 for each sequence + std::vector past(seq_count, 0); + // no initial block indices + mgr->ensure_operator(NODE, + key_data.data(), + val_data.data(), + layout.shape(), + layout.shape(), + nullptr, + 0, + nullptr, + 0, + past.data(), + seq_count); + return mgr; +} + +} // namespace + +// Basic lifecycle: register, begin_step, ensure_token, resolve_token +TEST(PagedCacheManagerTest, BasicAllocateAndResolve) { + CacheLayout layout{4, 1, 2, 4}; + std::vector kd, vd; + auto mgr = make_manager(layout, EvictionPolicy::FIFO, kd, vd); + + std::int32_t past = 0; + mgr->begin_step(NODE, &past, 1); + + // write a token at position 0 + std::vector krow(layout.kv_heads * layout.head_size, 1.f); + std::vector vrow(layout.kv_heads * layout.head_size, 2.f); + mgr->write_token_kv(NODE, 0, 0, krow.data(), vrow.data()); + + // resolve it + PagedCacheManager::TokenAddress addr; + ASSERT_TRUE(mgr->resolve_token(NODE, 0, 0, addr)); + EXPECT_GE(addr.block, 0); + + // read back key data + const float* kp = mgr->key_ptr(NODE, addr, 0); + ASSERT_NE(kp, nullptr); + EXPECT_FLOAT_EQ(kp[0], 1.f); + + // read back value data + const float* vp = mgr->value_ptr(NODE, addr, 0); + ASSERT_NE(vp, nullptr); + EXPECT_FLOAT_EQ(vp[0], 2.f); +} + +TEST(PagedCacheManagerTest, SupportsGpuKeyCacheLayout) { + constexpr std::size_t num_blocks = 3; + constexpr std::size_t kv_heads = 2; + constexpr std::size_t block_size = 2; + constexpr std::size_t key_head_size = 4; + constexpr std::size_t value_head_size = 3; + + const ov::Shape key_shape{num_blocks, kv_heads, key_head_size, block_size}; + const ov::Shape value_shape{num_blocks, kv_heads, block_size, value_head_size}; + std::vector key_data(num_blocks * kv_heads * key_head_size * block_size, 0.f); + std::vector value_data(num_blocks * kv_heads * block_size * value_head_size, 0.f); + + auto key_offset = [=](std::size_t block, std::size_t head, std::size_t dim, std::size_t token) { + return (((block * kv_heads + head) * key_head_size + dim) * block_size) + token; + }; + auto value_offset = [=](std::size_t block, std::size_t head, std::size_t token, std::size_t dim) { + return (((block * kv_heads + head) * block_size + token) * value_head_size) + dim; + }; + + for (std::size_t block = 0; block < num_blocks; ++block) { + for (std::size_t head = 0; head < kv_heads; ++head) { + for (std::size_t token = 0; token < block_size; ++token) { + for (std::size_t dim = 0; dim < key_head_size; ++dim) { + const auto value = 1000 * block + 100 * head + 10 * token + dim; + key_data[key_offset(block, head, dim, token)] = static_cast(value); + } + for (std::size_t dim = 0; dim < value_head_size; ++dim) { + const auto value = 2000 * block + 100 * head + 10 * token + dim; + value_data[value_offset(block, head, token, dim)] = static_cast(value); + } + } + } + } + + auto mgr = std::make_unique(ov::element::f32, EvictionPolicy::FIFO, /*max_cache_bytes=*/0); + std::vector block_indices{0, 1}; + std::vector block_begins{0, 2}; + std::vector past_lens{static_cast(block_indices.size() * block_size)}; + + ASSERT_TRUE(mgr->ensure_operator(NODE, + key_data.data(), + value_data.data(), + key_shape, + value_shape, + block_indices.data(), + block_indices.size(), + block_begins.data(), + block_begins.size(), + past_lens.data(), + past_lens.size())); + + EXPECT_EQ(mgr->block_size(NODE), block_size); + EXPECT_EQ(mgr->key_head_size(NODE), key_head_size); + EXPECT_EQ(mgr->value_head_size(NODE), value_head_size); + + PagedCacheManager::TokenAddress addr; + ASSERT_TRUE(mgr->resolve_token(NODE, 0, 2, addr)); + EXPECT_EQ(addr.block, 1); + EXPECT_EQ(addr.offset, 0); + + const float* cached_key = mgr->key_ptr(NODE, addr, 1); + ASSERT_NE(cached_key, nullptr); + for (std::size_t dim = 0; dim < key_head_size; ++dim) { + EXPECT_FLOAT_EQ(cached_key[dim], static_cast(1100 + dim)); + } + + const float* cached_value = mgr->value_ptr(NODE, addr, 1); + ASSERT_NE(cached_value, nullptr); + for (std::size_t dim = 0; dim < value_head_size; ++dim) { + EXPECT_FLOAT_EQ(cached_value[dim], static_cast(2100 + dim)); + } + + std::vector key_row{1.f, 2.f, 3.f, 4.f, 5.f, 6.f, 7.f, 8.f}; + std::vector value_row{11.f, 12.f, 13.f, 14.f, 15.f, 16.f}; + mgr->write_token_kv(NODE, 0, 4, key_row.data(), value_row.data()); + + ASSERT_TRUE(mgr->resolve_token(NODE, 0, 4, addr)); + const float* written_key = mgr->key_ptr(NODE, addr, 1); + ASSERT_NE(written_key, nullptr); + for (std::size_t dim = 0; dim < key_head_size; ++dim) { + EXPECT_FLOAT_EQ(written_key[dim], key_row[key_head_size + dim]); + } + + const float* written_value = mgr->value_ptr(NODE, addr, 1); + ASSERT_NE(written_value, nullptr); + for (std::size_t dim = 0; dim < value_head_size; ++dim) { + EXPECT_FLOAT_EQ(written_value[dim], value_row[value_head_size + dim]); + } +} + +// FIFO eviction: fills all blocks then forces eviction from oldest +TEST(PagedCacheManagerTest, FIFOEvictionWorks) { + // 4 blocks, block_size=2 -> can hold 8 tokens total + CacheLayout layout{4, 1, 2, 4}; + std::vector kd, vd; + auto mgr = make_manager(layout, EvictionPolicy::FIFO, kd, vd, 2); + + EXPECT_EQ(mgr->eviction_policy(), EvictionPolicy::FIFO); + + // fill seq 0 with 6 tokens (3 blocks) + std::int32_t past0 = 0; + std::int32_t past1 = 0; + std::int32_t pasts[2] = {past0, past1}; + mgr->begin_step(NODE, pasts, 2); + + std::vector krow(layout.kv_heads * layout.head_size, 0.f); + std::vector vrow(layout.kv_heads * layout.head_size, 0.f); + for (int t = 0; t < 6; t++) { + krow[0] = static_cast(t + 100); + mgr->write_token_kv(NODE, 0, t, krow.data(), vrow.data()); + } + + // fill seq 1 with 2 tokens (1 block) -> total 4 blocks used up + for (int t = 0; t < 2; t++) { + krow[0] = static_cast(t + 200); + mgr->write_token_kv(NODE, 1, t, krow.data(), vrow.data()); + } + + // now all 4 blocks are used, write one more token for seq 1 which needs a new block + // this should evict the oldest block from seq 0 (the longest) + krow[0] = 999.f; + mgr->write_token_kv(NODE, 1, 2, krow.data(), vrow.data()); + + // seq 0 tokens 0 and 1 (the first block) should be evicted + PagedCacheManager::TokenAddress addr; + EXPECT_FALSE(mgr->resolve_token(NODE, 0, 0, addr)); + EXPECT_FALSE(mgr->resolve_token(NODE, 0, 1, addr)); + + // seq 0 tokens 2-5 should still be accessible + EXPECT_TRUE(mgr->resolve_token(NODE, 0, 2, addr)); + EXPECT_TRUE(mgr->resolve_token(NODE, 0, 5, addr)); + + // seq 1 token 2 should be accessible (the new one) + EXPECT_TRUE(mgr->resolve_token(NODE, 1, 2, addr)); + const float* kp = mgr->key_ptr(NODE, addr, 0); + ASSERT_NE(kp, nullptr); + EXPECT_FLOAT_EQ(kp[0], 999.f); +} + +// SCORE eviction: picks the sequence whose front block has lowest score +TEST(PagedCacheManagerTest, ScoreEvictionPicksLowestScoreFrontBlock) { + // 4 blocks, block_size=2, 1 head, head_size=4 + CacheLayout layout{4, 1, 2, 4}; + std::vector kd, vd; + auto mgr = make_manager(layout, EvictionPolicy::SCORE, kd, vd, 2); + + EXPECT_EQ(mgr->eviction_policy(), EvictionPolicy::SCORE); + + std::int32_t pasts[2] = {0, 0}; + mgr->begin_step(NODE, pasts, 2); + + std::vector krow(layout.kv_heads * layout.head_size, 0.f); + std::vector vrow(layout.kv_heads * layout.head_size, 0.f); + + // seq 0: 4 tokens -> 2 blocks + for (int t = 0; t < 4; t++) { + krow[0] = static_cast(t); + mgr->write_token_kv(NODE, 0, t, krow.data(), vrow.data()); + } + // seq 1: 4 tokens -> 2 blocks, now all 4 blocks used + for (int t = 0; t < 4; t++) { + krow[0] = static_cast(t + 10); + mgr->write_token_kv(NODE, 1, t, krow.data(), vrow.data()); + } + + // feed attention scores so that seq 0 front block has LOW score and seq 1 front block has HIGH score + // layout: [seq0_4_tokens, seq1_4_tokens] + pasts[0] = 4; + pasts[1] = 4; + // seq 0 block 0 tokens: 0.1 each -> front block score 0.2 + // seq 0 block 1 tokens: 5.0 each -> block score 10.0 + // seq 1 block 0 tokens: 3.0 each -> front block score 6.0 + // seq 1 block 1 tokens: 3.0 each -> block score 6.0 + std::vector scores = {0.1f, 0.1f, 5.0f, 5.0f, 3.f, 3.f, 3.f, 3.f}; + mgr->update_attention_scores(NODE, scores.data(), scores.size(), pasts, 2); + + // force a new allocation for seq 1 which will trigger eviction + // should evict seq 0 front block (score 0.2) not seq 1 front block (score 6.0) + krow[0] = 777.f; + mgr->write_token_kv(NODE, 1, 4, krow.data(), vrow.data()); + + // seq 0 tokens 0,1 should be evicted (front block of seq 0 had lowest score) + PagedCacheManager::TokenAddress addr; + EXPECT_FALSE(mgr->resolve_token(NODE, 0, 0, addr)); + EXPECT_FALSE(mgr->resolve_token(NODE, 0, 1, addr)); + + // seq 0 tokens 2,3 should survive + EXPECT_TRUE(mgr->resolve_token(NODE, 0, 2, addr)); + EXPECT_TRUE(mgr->resolve_token(NODE, 0, 3, addr)); + + // all seq 1 tokens should be intact + EXPECT_TRUE(mgr->resolve_token(NODE, 1, 0, addr)); + EXPECT_TRUE(mgr->resolve_token(NODE, 1, 3, addr)); + EXPECT_TRUE(mgr->resolve_token(NODE, 1, 4, addr)); +} + +// SCORE eviction falls back to FIFO when no scores are recorded +TEST(PagedCacheManagerTest, ScoreEvictionFallsBackToFifo) { + // 4 blocks, block_size=2, 2 sequences + CacheLayout layout{4, 1, 2, 4}; + std::vector kd, vd; + auto mgr = make_manager(layout, EvictionPolicy::SCORE, kd, vd, 2); + + std::int32_t pasts[2] = {0, 0}; + mgr->begin_step(NODE, pasts, 2); + + std::vector krow(layout.kv_heads * layout.head_size, 0.f); + std::vector vrow(layout.kv_heads * layout.head_size, 0.f); + + // seq 0: 6 tokens -> 3 blocks + for (int t = 0; t < 6; t++) { + krow[0] = static_cast(t); + mgr->write_token_kv(NODE, 0, t, krow.data(), vrow.data()); + } + // seq 1: 2 tokens -> 1 block, total 4 blocks used + for (int t = 0; t < 2; t++) { + mgr->write_token_kv(NODE, 1, t, krow.data(), vrow.data()); + } + + // no scores fed, force eviction by adding a 5th block for seq 1 + krow[0] = 888.f; + mgr->write_token_kv(NODE, 1, 2, krow.data(), vrow.data()); + + // FIFO fallback should evict oldest block from seq 0 (longest) + PagedCacheManager::TokenAddress addr; + EXPECT_FALSE(mgr->resolve_token(NODE, 0, 0, addr)); + EXPECT_FALSE(mgr->resolve_token(NODE, 0, 1, addr)); + EXPECT_TRUE(mgr->resolve_token(NODE, 0, 2, addr)); + EXPECT_TRUE(mgr->resolve_token(NODE, 1, 2, addr)); +} + +// ADAPTIVE_RKV eviction: evicts from the sequence whose front block is least diverse +TEST(PagedCacheManagerTest, AdaptiveRKVEvictionPicksLeastDiverse) { + // 4 blocks, block_size=2, eviction_size=4 tokens + CacheLayout layout{4, 1, 2, 4}; + std::vector kd, vd; + auto mgr = make_manager(layout, EvictionPolicy::ADAPTIVE_RKV, kd, vd, 2); + + EXPECT_EQ(mgr->eviction_policy(), EvictionPolicy::ADAPTIVE_RKV); + + std::int32_t pasts[2] = {0, 0}; + mgr->begin_step(NODE, pasts, 2); + + std::vector krow(layout.kv_heads * layout.head_size, 0.f); + std::vector vrow(layout.kv_heads * layout.head_size, 0.f); + + // seq 0: 4 tokens -> 2 blocks + for (int t = 0; t < 4; t++) { + krow[0] = static_cast(t); + mgr->write_token_kv(NODE, 0, t, krow.data(), vrow.data()); + } + // seq 1: 4 tokens -> 2 blocks, all 4 blocks used + for (int t = 0; t < 4; t++) { + krow[0] = static_cast(t + 10); + mgr->write_token_kv(NODE, 1, t, krow.data(), vrow.data()); + } + + // Feed attention scores: seq 0 block 1 is important, block 0 is not. + // seq 1 both blocks are equally important. + // scores layout: [seq0: 4 tokens][seq1: 4 tokens] + std::int32_t pasts_for_scores[2] = {4, 4}; + // seq0: tok0 tok1 tok2 tok3 seq1: tok0 tok1 tok2 tok3 + float scores[8] = {0.1f, 0.1f, 5.0f, 5.0f, 2.0f, 2.0f, 2.0f, 2.0f}; + mgr->update_attention_scores(NODE, scores, 8, pasts_for_scores, 2); + + // Feed 2D diversity matrices [2 blocks, 4 tokens]. + // seq 0: front block row has low diversity to retained set + // seq 1: front block row has high diversity + // eviction_size = 4 (2 blocks * 2 tokens/block) + float div0[8] = {// block 0 row: diversity to each of the 4 tokens + 0.1f, + 0.1f, + 0.1f, + 0.1f, + // block 1 row: + 5.0f, + 5.0f, + 5.0f, + 5.0f}; + float div1[8] = {// block 0 row: + 3.0f, + 3.0f, + 3.0f, + 3.0f, + // block 1 row: + 3.0f, + 3.0f, + 3.0f, + 3.0f}; + mgr->update_diversity_scores(NODE, 0, div0, 2, 4, 0); + mgr->update_diversity_scores(NODE, 1, div1, 2, 4, 0); + + // force eviction by writing token 4 for seq 1 + krow[0] = 666.f; + mgr->write_token_kv(NODE, 1, 4, krow.data(), vrow.data()); + + // seq 0 front block (tokens 0,1) should be evicted: + // - seq 0 block 0 has low attention (0.2 total), low diversity (0.1) + // - seq 1 block 0 has moderate attention (4.0), moderate diversity (3.0) + // With attention-mass gating (p=0.9), seq 0's block 1 (score 10.0) covers 98%+ of + // attention mass, so block 0 is NOT retained -> eligible for eviction. + PagedCacheManager::TokenAddress addr; + EXPECT_FALSE(mgr->resolve_token(NODE, 0, 0, addr)); + EXPECT_FALSE(mgr->resolve_token(NODE, 0, 1, addr)); + EXPECT_TRUE(mgr->resolve_token(NODE, 0, 2, addr)); // block 1 survived + EXPECT_TRUE(mgr->resolve_token(NODE, 1, 0, addr)); // seq 1 intact + EXPECT_TRUE(mgr->resolve_token(NODE, 1, 4, addr)); // new token accessible +} + +// ADAPTIVE_RKV falls back to SCORE then FIFO if no diversity data +TEST(PagedCacheManagerTest, AdaptiveRKVFallsBackToScoreThenFifo) { + // 4 blocks, 2 sequences + CacheLayout layout{4, 1, 2, 4}; + std::vector kd, vd; + auto mgr = make_manager(layout, EvictionPolicy::ADAPTIVE_RKV, kd, vd, 2); + + std::int32_t pasts[2] = {0, 0}; + mgr->begin_step(NODE, pasts, 2); + + std::vector krow(layout.kv_heads * layout.head_size, 0.f); + std::vector vrow(layout.kv_heads * layout.head_size, 0.f); + + // seq 0: 6 tokens -> 3 blocks + for (int t = 0; t < 6; t++) { + mgr->write_token_kv(NODE, 0, t, krow.data(), vrow.data()); + } + // seq 1: 2 tokens -> 1 block, total 4 blocks + for (int t = 0; t < 2; t++) { + mgr->write_token_kv(NODE, 1, t, krow.data(), vrow.data()); + } + + // no diversity, no scores -> should fall all the way back to FIFO + krow[0] = 111.f; + mgr->write_token_kv(NODE, 1, 2, krow.data(), vrow.data()); + + // FIFO should evict from seq 0 (longest) + PagedCacheManager::TokenAddress addr; + EXPECT_FALSE(mgr->resolve_token(NODE, 0, 0, addr)); + EXPECT_TRUE(mgr->resolve_token(NODE, 0, 2, addr)); + EXPECT_TRUE(mgr->resolve_token(NODE, 1, 2, addr)); +} + +// Sequence reset clears blocks and score/diversity state +TEST(PagedCacheManagerTest, SequenceResetClearsState) { + CacheLayout layout{4, 1, 2, 4}; + std::vector kd, vd; + auto mgr = make_manager(layout, EvictionPolicy::SCORE, kd, vd, 1); + + std::int32_t past = 0; + mgr->begin_step(NODE, &past, 1); + + std::vector krow(4, 1.f); + std::vector vrow(4, 1.f); + for (int t = 0; t < 4; t++) { + mgr->write_token_kv(NODE, 0, t, krow.data(), vrow.data()); + } + + // feed some scores + past = 4; + std::vector scores = {1.f, 2.f, 3.f, 4.f}; + mgr->update_attention_scores(NODE, scores.data(), scores.size(), &past, 1); + + // reset the sequence by setting past_lens to 0 + past = 0; + mgr->begin_step(NODE, &past, 1); + + // all old tokens should be gone + PagedCacheManager::TokenAddress addr; + EXPECT_FALSE(mgr->resolve_token(NODE, 0, 0, addr)); + EXPECT_FALSE(mgr->resolve_token(NODE, 0, 3, addr)); + + // should be able to write new tokens without issues (blocks were freed) + mgr->write_token_kv(NODE, 0, 0, krow.data(), vrow.data()); + EXPECT_TRUE(mgr->resolve_token(NODE, 0, 0, addr)); +} + +// Multiple evictions in a row dont corrupt memory +TEST(PagedCacheManagerTest, RepeatedEvictionsNoCorruption) { + // small pool: 3 blocks of size 2 + CacheLayout layout{3, 2, 2, 4}; + std::vector kd, vd; + auto mgr = make_manager(layout, EvictionPolicy::FIFO, kd, vd, 1); + + std::int32_t past = 0; + mgr->begin_step(NODE, &past, 1); + + std::vector krow(layout.kv_heads * layout.head_size, 0.f); + std::vector vrow(layout.kv_heads * layout.head_size, 0.f); + + // write 20 tokens, which forces many evictions with only 3 blocks (6 token slots) + for (int t = 0; t < 20; t++) { + for (std::size_t j = 0; j < krow.size(); j++) { + krow[j] = static_cast(t * 100 + j); + } + mgr->write_token_kv(NODE, 0, t, krow.data(), vrow.data()); + } + + // only the last ~6 tokens should be resolvable (3 blocks * 2 tokens/block) + // earlier ones were evicted + PagedCacheManager::TokenAddress addr; + int resolvable = 0; + for (int t = 0; t < 20; t++) { + if (mgr->resolve_token(NODE, 0, t, addr)) { + resolvable++; + // check that we can read back the key data without crashing + const float* kp = mgr->key_ptr(NODE, addr, 0); + ASSERT_NE(kp, nullptr); + EXPECT_FLOAT_EQ(kp[0], static_cast(t * 100)); + } + } + + // we expect roughly 6 tokens to be resolvable (3 blocks * 2) + EXPECT_GE(resolvable, 4); + EXPECT_LE(resolvable, 8); // some slack for boundary effects +} + +// max_cache_bytes triggers eviction before physical blocks run out +TEST(PagedCacheManagerTest, MaxCacheBytesTriggersEviction) { + // 8 physical blocks, block_size=2, 1 kv head, head_size=4 (float=4B) + // key_block_bytes = value_block_bytes = 1*2*4*4 = 32 bytes + // bytes_per_block = 64 -> max_cache_bytes=192 allows 3 active blocks + CacheLayout layout{8, 1, 2, 4}; + std::vector kd(layout.elems(), 0.f); + std::vector vd(layout.elems(), 0.f); + + auto mgr = std::make_unique(ov::element::f32, + EvictionPolicy::FIFO, + /*max_cache_bytes=*/192); + + std::vector past(2, 0); + mgr->ensure_operator(NODE, + kd.data(), + vd.data(), + layout.shape(), + layout.shape(), + nullptr, + 0, + nullptr, + 0, + past.data(), + 2); + + std::int32_t pasts[2] = {0, 0}; + mgr->begin_step(NODE, pasts, 2); + + std::vector krow(layout.kv_heads * layout.head_size, 0.f); + std::vector vrow(layout.kv_heads * layout.head_size, 0.f); + + // seq 0: 4 tokens -> 2 blocks + for (int t = 0; t < 4; t++) { + krow[0] = static_cast(t + 100); + mgr->write_token_kv(NODE, 0, t, krow.data(), vrow.data()); + } + + // seq 1: 2 tokens -> 1 block. Total 3 active blocks = budget limit + for (int t = 0; t < 2; t++) { + krow[0] = static_cast(t + 200); + mgr->write_token_kv(NODE, 1, t, krow.data(), vrow.data()); + } + + // Writing one more token for seq 1 needs a 4th block, but budget allows only 3, + // so eviction fires even though 5 physical blocks are still free + krow[0] = 999.f; + mgr->write_token_kv(NODE, 1, 2, krow.data(), vrow.data()); + + // FIFO evicts oldest block from longest sequence + // (seq 0 front block -> tokens 0,1) + PagedCacheManager::TokenAddress addr; + EXPECT_FALSE(mgr->resolve_token(NODE, 0, 0, addr)); + EXPECT_FALSE(mgr->resolve_token(NODE, 0, 1, addr)); + EXPECT_TRUE(mgr->resolve_token(NODE, 0, 2, addr)); + EXPECT_TRUE(mgr->resolve_token(NODE, 0, 3, addr)); + EXPECT_TRUE(mgr->resolve_token(NODE, 1, 2, addr)); + const float* kp = mgr->key_ptr(NODE, addr, 0); + ASSERT_NE(kp, nullptr); + EXPECT_FLOAT_EQ(kp[0], 999.f); +} + +// Default constructor uses SCORE policy +TEST(PagedCacheManagerTest, DefaultConstructorUsesScore) { + PagedCacheManager mgr(ov::element::f32); + EXPECT_EQ(mgr.eviction_policy(), EvictionPolicy::SCORE); +} + +// Reference PA kernel with tiny cache forces eviction, no corruption +TEST(PagedCacheManagerTest, ReferenceKernelEvictionNoCorruption) { + // minimal config: 1 kv head, head_size=4, block_size=2, only 3 blocks + // with 3 blocks we can hold 6 tokens, so a sequence of 8 forces eviction + const std::size_t num_blocks = 3; + const std::size_t kv_heads = 1; + const std::size_t blk_size = 2; + const std::size_t head_size = 4; + const std::size_t q_heads = 1; + const std::size_t q_features = q_heads * head_size; + const std::size_t kv_features = kv_heads * head_size; + + // initial (empty) key and value caches + const std::size_t cache_elems = num_blocks * kv_heads * blk_size * head_size; + std::vector key_cache(cache_elems, 0.f); + std::vector val_cache(cache_elems, 0.f); + ov::Shape cache_shape = {num_blocks, kv_heads, blk_size, head_size}; + + // create a manager with SCORE policy + auto mgr = std::make_unique(ov::element::f32, EvictionPolicy::SCORE, 0); + + // empty block indices for initial registration + std::vector block_idx = {0, 1, 2}; + std::vector block_begins = {0, 3}; + + // run two steps: first step with 6 tokens (fills cache), second step with 2 more (forces eviction) + constexpr std::uintptr_t nk = 99; + + // --- step 1: prompt with 6 tokens --- + const std::size_t step1_tokens = 6; + std::vector q1(step1_tokens * q_features); + std::vector k1(step1_tokens * kv_features); + std::vector v1(step1_tokens * kv_features); + for (std::size_t i = 0; i < q1.size(); i++) + q1[i] = 0.1f * static_cast(i + 1); + for (std::size_t i = 0; i < k1.size(); i++) + k1[i] = 0.2f * static_cast(i + 1); + for (std::size_t i = 0; i < v1.size(); i++) + v1[i] = 0.3f * static_cast(i + 1); + + std::int32_t past1 = 0; + std::int32_t subseq1[2] = {0, static_cast(step1_tokens)}; + float scale_val = 1.0f / std::sqrt(static_cast(head_size)); + std::int32_t score_window = 0; + + std::vector out1(step1_tokens * q_features, -999.f); + + ov::reference::paged_attention(nk, + mgr.get(), + out1.data(), + nullptr, + nullptr, + q1.data(), + k1.data(), + v1.data(), + key_cache.data(), + val_cache.data(), + &past1, + subseq1, + block_idx.data(), + block_idx.size(), + block_begins.data(), + block_begins.size(), + &scale_val, + ov::element::f32, + nullptr, + nullptr, + ov::element::Type{}, + {}, + nullptr, + &score_window, + 1, + nullptr, + 0, + nullptr, + {}, + nullptr, + ov::element::Type{}, + {}, + nullptr, + ov::element::Type{}, + nullptr, + nullptr, + nullptr, + ov::element::Type{}, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + 0, + {step1_tokens, q_features}, + {step1_tokens, kv_features}, + {step1_tokens, kv_features}, + cache_shape, + cache_shape, + {1}, + {2}); + + // check no NaN or inf in output + for (std::size_t i = 0; i < out1.size(); i++) { + EXPECT_TRUE(std::isfinite(out1[i])) << "step 1 output[" << i << "] is not finite"; + } + + // --- step 2: decode with 2 more tokens, forces eviction --- + const std::size_t step2_tokens = 2; + std::vector q2(step2_tokens * q_features); + std::vector k2(step2_tokens * kv_features); + std::vector v2(step2_tokens * kv_features); + for (std::size_t i = 0; i < q2.size(); i++) + q2[i] = 0.4f * static_cast(i + 1); + for (std::size_t i = 0; i < k2.size(); i++) + k2[i] = 0.5f * static_cast(i + 1); + for (std::size_t i = 0; i < v2.size(); i++) + v2[i] = 0.6f * static_cast(i + 1); + + std::int32_t past2 = static_cast(step1_tokens); + std::int32_t subseq2[2] = {0, static_cast(step2_tokens)}; + + std::vector out2(step2_tokens * q_features, -999.f); + + ov::reference::paged_attention(nk, + mgr.get(), + out2.data(), + nullptr, + nullptr, + q2.data(), + k2.data(), + v2.data(), + key_cache.data(), + val_cache.data(), + &past2, + subseq2, + block_idx.data(), + block_idx.size(), + block_begins.data(), + block_begins.size(), + &scale_val, + ov::element::f32, + nullptr, + nullptr, + ov::element::Type{}, + {}, + nullptr, + &score_window, + 1, + nullptr, + 0, + nullptr, + {}, + nullptr, + ov::element::Type{}, + {}, + nullptr, + ov::element::Type{}, + nullptr, + nullptr, + nullptr, + ov::element::Type{}, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + 0, + {step2_tokens, q_features}, + {step2_tokens, kv_features}, + {step2_tokens, kv_features}, + cache_shape, + cache_shape, + {1}, + {2}); + + // check no NaN, inf, or uninitialized values in output + for (std::size_t i = 0; i < out2.size(); i++) { + EXPECT_TRUE(std::isfinite(out2[i])) << "step 2 output[" << i << "] is not finite"; + EXPECT_NE(out2[i], -999.f) << "step 2 output[" << i << "] was never written"; + } +} + +// =================================================================== +// FIFO: additional coverage +// =================================================================== + +// Single sequence forces self-eviction when no other victim exists +TEST(PagedCacheManagerTest, FIFOStealsFromSelfAsLastResort) { + // 2 blocks, block_size=2, single sequence + CacheLayout layout{2, 1, 2, 4}; + std::vector kd, vd; + auto mgr = make_manager(layout, EvictionPolicy::FIFO, kd, vd, 1); + + std::int32_t past = 0; + mgr->begin_step(NODE, &past, 1); + + std::vector krow(layout.kv_heads * layout.head_size, 0.f); + std::vector vrow(layout.kv_heads * layout.head_size, 0.f); + + // fill 2 blocks (4 tokens) + for (int t = 0; t < 4; t++) { + krow[0] = static_cast(t + 1); + mgr->write_token_kv(NODE, 0, t, krow.data(), vrow.data()); + } + + // writing token 4 forces self-eviction of front block + krow[0] = 50.f; + mgr->write_token_kv(NODE, 0, 4, krow.data(), vrow.data()); + + PagedCacheManager::TokenAddress addr; + EXPECT_FALSE(mgr->resolve_token(NODE, 0, 0, addr)); + EXPECT_FALSE(mgr->resolve_token(NODE, 0, 1, addr)); + EXPECT_TRUE(mgr->resolve_token(NODE, 0, 2, addr)); + EXPECT_TRUE(mgr->resolve_token(NODE, 0, 4, addr)); + const float* kp = mgr->key_ptr(NODE, addr, 0); + ASSERT_NE(kp, nullptr); + EXPECT_FLOAT_EQ(kp[0], 50.f); +} + +// Data integrity: surviving blocks retain correct key/value data after eviction +TEST(PagedCacheManagerTest, FIFOEvictionPreservesSurvivingData) { + // 3 blocks, 2 kv_heads, block_size=2, head_size=4 + CacheLayout layout{3, 2, 2, 4}; + std::vector kd, vd; + auto mgr = make_manager(layout, EvictionPolicy::FIFO, kd, vd, 1); + + std::int32_t past = 0; + mgr->begin_step(NODE, &past, 1); + + // write 6 tokens (fills 3 blocks), each with distinct per-head key values + std::vector krow(layout.kv_heads * layout.head_size, 0.f); + std::vector vrow(layout.kv_heads * layout.head_size, 0.f); + for (int t = 0; t < 6; t++) { + for (std::size_t j = 0; j < krow.size(); j++) { + krow[j] = static_cast(t * 100 + j); + vrow[j] = static_cast(t * 1000 + j); + } + mgr->write_token_kv(NODE, 0, t, krow.data(), vrow.data()); + } + + // force eviction (front block tokens 0,1 evicted) + for (std::size_t j = 0; j < krow.size(); j++) { + krow[j] = static_cast(6 * 100 + j); + vrow[j] = static_cast(6 * 1000 + j); + } + mgr->write_token_kv(NODE, 0, 6, krow.data(), vrow.data()); + + // verify surviving tokens 2-6 have correct data in both kv heads + for (int t = 2; t <= 6; t++) { + PagedCacheManager::TokenAddress addr; + ASSERT_TRUE(mgr->resolve_token(NODE, 0, t, addr)) << "token " << t; + for (std::size_t h = 0; h < 2; h++) { + const float* kp = mgr->key_ptr(NODE, addr, h); + ASSERT_NE(kp, nullptr); + EXPECT_FLOAT_EQ(kp[0], static_cast(t * 100 + h * layout.head_size)) + << "key head " << h << " token " << t; + + const float* vp = mgr->value_ptr(NODE, addr, h); + ASSERT_NE(vp, nullptr); + EXPECT_FLOAT_EQ(vp[0], static_cast(t * 1000 + h * layout.head_size)) + << "value head " << h << " token " << t; + } + } +} + +// FIFO with equal-length sequences picks non-requester +TEST(PagedCacheManagerTest, FIFOEqualLengthPrefersNonRequester) { + // 4 blocks, block_size=2, 2 sequences each with 2 blocks + CacheLayout layout{4, 1, 2, 4}; + std::vector kd, vd; + auto mgr = make_manager(layout, EvictionPolicy::FIFO, kd, vd, 2); + + std::int32_t pasts[2] = {0, 0}; + mgr->begin_step(NODE, pasts, 2); + + std::vector krow(layout.kv_heads * layout.head_size, 0.f); + std::vector vrow(layout.kv_heads * layout.head_size, 0.f); + + // seq 0: 4 tokens -> 2 blocks + for (int t = 0; t < 4; t++) + mgr->write_token_kv(NODE, 0, t, krow.data(), vrow.data()); + // seq 1: 4 tokens -> 2 blocks + for (int t = 0; t < 4; t++) + mgr->write_token_kv(NODE, 1, t, krow.data(), vrow.data()); + + // seq 1 requests a new block -> evicts from seq 0 (non-requester) + mgr->write_token_kv(NODE, 1, 4, krow.data(), vrow.data()); + + PagedCacheManager::TokenAddress addr; + EXPECT_FALSE(mgr->resolve_token(NODE, 0, 0, addr)); // seq 0 front evicted + EXPECT_TRUE(mgr->resolve_token(NODE, 0, 2, addr)); // seq 0 back survived + EXPECT_TRUE(mgr->resolve_token(NODE, 1, 0, addr)); // seq 1 intact + EXPECT_TRUE(mgr->resolve_token(NODE, 1, 4, addr)); // new token ok +} + +// Score eviction penalizes the requester sequence (prefers non-requester) +TEST(PagedCacheManagerTest, ScoreEvictionPrefersNonRequester) { + // 4 blocks, 2 sequences. Seq 1 (the requester) has a much lower front-block score + // than seq 0, but the penalty should prevent self-eviction. + CacheLayout layout{4, 1, 2, 4}; + std::vector kd, vd; + auto mgr = make_manager(layout, EvictionPolicy::SCORE, kd, vd, 2); + + std::int32_t pasts[2] = {0, 0}; + mgr->begin_step(NODE, pasts, 2); + + std::vector krow(layout.kv_heads * layout.head_size, 0.f); + std::vector vrow(layout.kv_heads * layout.head_size, 0.f); + + for (int t = 0; t < 4; t++) + mgr->write_token_kv(NODE, 0, t, krow.data(), vrow.data()); + for (int t = 0; t < 4; t++) + mgr->write_token_kv(NODE, 1, t, krow.data(), vrow.data()); + + // seq 0 front block has medium score, seq 1 front block has very low score + pasts[0] = 4; + pasts[1] = 4; + float scores[8] = {5.f, 5.f, 5.f, 5.f, 0.01f, 0.01f, 5.f, 5.f}; + mgr->update_attention_scores(NODE, scores, 8, pasts, 2); + + // seq 1 requests a new block. Without the penalty, seq 1's front block (0.02) + // would be evicted, but the 1e12 penalty makes seq 0 (10.0) the victim instead. + mgr->write_token_kv(NODE, 1, 4, krow.data(), vrow.data()); + + PagedCacheManager::TokenAddress addr; + EXPECT_FALSE(mgr->resolve_token(NODE, 0, 0, addr)); // seq 0 front evicted + EXPECT_TRUE(mgr->resolve_token(NODE, 1, 0, addr)); // seq 1 front preserved + EXPECT_TRUE(mgr->resolve_token(NODE, 1, 4, addr)); +} + +// Score updates accumulate across multiple calls +TEST(PagedCacheManagerTest, ScoreEvictionAccumulatesScores) { + // 4 blocks, 2 sequences. + CacheLayout layout{4, 1, 2, 4}; + std::vector kd, vd; + auto mgr = make_manager(layout, EvictionPolicy::SCORE, kd, vd, 2); + + std::int32_t pasts[2] = {0, 0}; + mgr->begin_step(NODE, pasts, 2); + + std::vector krow(layout.kv_heads * layout.head_size, 0.f); + std::vector vrow(layout.kv_heads * layout.head_size, 0.f); + for (int t = 0; t < 4; t++) + mgr->write_token_kv(NODE, 0, t, krow.data(), vrow.data()); + for (int t = 0; t < 4; t++) + mgr->write_token_kv(NODE, 1, t, krow.data(), vrow.data()); + + pasts[0] = 4; + pasts[1] = 4; + + // First update: seq 0 front = 1.0, seq 1 front = 2.0 + float scores1[8] = {0.5f, 0.5f, 1.f, 1.f, 1.0f, 1.0f, 1.f, 1.f}; + mgr->update_attention_scores(NODE, scores1, 8, pasts, 2); + // Second update: add more to seq 0 front, making it 1.0 + 10.0 = 11.0 + float scores2[8] = {5.0f, 5.0f, 0.f, 0.f, 0.0f, 0.0f, 0.f, 0.f}; + mgr->update_attention_scores(NODE, scores2, 8, pasts, 2); + + // Now seq 0 front = 11.0, seq 1 front = 2.0. + // Seq 0 requests the new block. SCORE penalizes requester (seq 0) by +1e12, + // so seq 1's front (2.0) < seq 0's penalized front (11 + 1e12) -> seq 1 evicted. + mgr->write_token_kv(NODE, 0, 4, krow.data(), vrow.data()); + + PagedCacheManager::TokenAddress addr; + EXPECT_TRUE(mgr->resolve_token(NODE, 0, 0, addr)); // seq 0 front survived (requester, penalized) + EXPECT_FALSE(mgr->resolve_token(NODE, 1, 0, addr)); // seq 1 front evicted (2.0) + EXPECT_TRUE(mgr->resolve_token(NODE, 0, 4, addr)); +} + +// Score eviction with single sequence (forced self-eviction via score) +TEST(PagedCacheManagerTest, ScoreEvictionSingleSequenceSelfEvicts) { + CacheLayout layout{3, 1, 2, 4}; + std::vector kd, vd; + auto mgr = make_manager(layout, EvictionPolicy::SCORE, kd, vd, 1); + + std::int32_t past = 0; + mgr->begin_step(NODE, &past, 1); + + std::vector krow(layout.kv_heads * layout.head_size, 0.f); + std::vector vrow(layout.kv_heads * layout.head_size, 0.f); + for (int t = 0; t < 6; t++) { + krow[0] = static_cast(t); + mgr->write_token_kv(NODE, 0, t, krow.data(), vrow.data()); + } + + // feed scores: front block (tokens 0,1) has low score + past = 6; + float scores[6] = {0.1f, 0.1f, 5.0f, 5.0f, 5.0f, 5.0f}; + mgr->update_attention_scores(NODE, scores, 6, &past, 1); + + // force self-eviction + krow[0] = 99.f; + mgr->write_token_kv(NODE, 0, 6, krow.data(), vrow.data()); + + PagedCacheManager::TokenAddress addr; + EXPECT_FALSE(mgr->resolve_token(NODE, 0, 0, addr)); + EXPECT_FALSE(mgr->resolve_token(NODE, 0, 1, addr)); + EXPECT_TRUE(mgr->resolve_token(NODE, 0, 2, addr)); + EXPECT_TRUE(mgr->resolve_token(NODE, 0, 6, addr)); +} + +// Attention-mass gating protects a front block that is highly attended +TEST(PagedCacheManagerTest, AdaptiveRKVGatingProtectsRetainedFrontBlock) { + // 6 blocks, block_size=2, 3 sequences. + // Seq 0: front block has HIGH attention -> retained -> protected from eviction + // Seq 1: front block has LOW attention -> not retained -> eligible + // Seq 2: front block has LOW attention -> not retained -> eligible + // Expect seq 1 or 2 to be evicted (depending on diversity), not seq 0 + CacheLayout layout{6, 1, 2, 4}; + std::vector kd, vd; + auto mgr = make_manager(layout, EvictionPolicy::ADAPTIVE_RKV, kd, vd, 3); + + std::int32_t pasts[3] = {0, 0, 0}; + mgr->begin_step(NODE, pasts, 3); + + std::vector krow(layout.kv_heads * layout.head_size, 0.f); + std::vector vrow(layout.kv_heads * layout.head_size, 0.f); + + // each seq: 4 tokens -> 2 blocks, total 6 blocks + for (int s = 0; s < 3; s++) + for (int t = 0; t < 4; t++) + mgr->write_token_kv(NODE, static_cast(s), t, krow.data(), vrow.data()); + + // Attention scores: seq 0 front block is dominant (90% of mass) + std::int32_t pasts_s[3] = {4, 4, 4}; + // seq 0: block0=9.0 block1=1.0 -> p=0.9, target=9.0, block0 alone covers it -> retained + // seq 1: block0=0.1 block1=9.9 -> target=9.0, block1 covers it -> block0 NOT retained + // seq 2: block0=0.1 block1=9.9 -> same as seq 1 + float scores[12] = {4.5f, 4.5f, 0.5f, 0.5f, 0.05f, 0.05f, 4.95f, 4.95f, 0.05f, 0.05f, 4.95f, 4.95f}; + mgr->update_attention_scores(NODE, scores, 12, pasts_s, 3); + + // Diversity: seq 1 front has lower diversity than seq 2 front + float div0[8] = {5.f, 5.f, 5.f, 5.f, 5.f, 5.f, 5.f, 5.f}; + float div1[8] = {0.1f, 0.1f, 0.1f, 0.1f, 5.f, 5.f, 5.f, 5.f}; + float div2[8] = {3.f, 3.f, 3.f, 3.f, 5.f, 5.f, 5.f, 5.f}; + mgr->update_diversity_scores(NODE, 0, div0, 2, 4, 0); + mgr->update_diversity_scores(NODE, 1, div1, 2, 4, 0); + mgr->update_diversity_scores(NODE, 2, div2, 2, 4, 0); + + // force eviction from seq 2 + mgr->write_token_kv(NODE, 2, 4, krow.data(), vrow.data()); + + PagedCacheManager::TokenAddress addr; + // seq 0 must survive entirely (front block is retained by gating) + EXPECT_TRUE(mgr->resolve_token(NODE, 0, 0, addr)) << "seq 0 front block should be protected"; + EXPECT_TRUE(mgr->resolve_token(NODE, 0, 2, addr)); + + // seq 1 front block should be evicted (lowest diversity among non-retained fronts) + EXPECT_FALSE(mgr->resolve_token(NODE, 1, 0, addr)); + EXPECT_TRUE(mgr->resolve_token(NODE, 1, 2, addr)); + + // seq 2 survived + new token + EXPECT_TRUE(mgr->resolve_token(NODE, 2, 0, addr)); + EXPECT_TRUE(mgr->resolve_token(NODE, 2, 4, addr)); +} + +// When all front blocks are retained by gating, fall back to SCORE eviction +TEST(PagedCacheManagerTest, AdaptiveRKVFallsBackToScoreWhenAllRetained) { + // 4 blocks, 2 sequences. Both front blocks have dominant attention -> both retained + CacheLayout layout{4, 1, 2, 4}; + std::vector kd, vd; + auto mgr = make_manager(layout, EvictionPolicy::ADAPTIVE_RKV, kd, vd, 2); + + std::int32_t pasts[2] = {0, 0}; + mgr->begin_step(NODE, pasts, 2); + + std::vector krow(layout.kv_heads * layout.head_size, 0.f); + std::vector vrow(layout.kv_heads * layout.head_size, 0.f); + for (int t = 0; t < 4; t++) + mgr->write_token_kv(NODE, 0, t, krow.data(), vrow.data()); + for (int t = 0; t < 4; t++) + mgr->write_token_kv(NODE, 1, t, krow.data(), vrow.data()); + + // Both front blocks carry the majority of attention mass + std::int32_t pasts_s[2] = {4, 4}; + float scores[8] = {9.0f, 9.0f, 1.0f, 1.0f, 9.0f, 9.0f, 1.0f, 1.0f}; + mgr->update_attention_scores(NODE, scores, 8, pasts_s, 2); + + // Diversity data present, but both front blocks are retained by attention-mass gating + float div[8] = {0.5f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5f}; + mgr->update_diversity_scores(NODE, 0, div, 2, 4, 0); + mgr->update_diversity_scores(NODE, 1, div, 2, 4, 0); + + // force eviction from seq 1 (both candidates are empty -> falls back to SCORE) + // SCORE: seq 0 front=18.0, seq 1 front=18.0 (tied), but requester (seq 1) gets penalty + // -> seq 0 front block evicted + mgr->write_token_kv(NODE, 1, 4, krow.data(), vrow.data()); + + PagedCacheManager::TokenAddress addr; + EXPECT_FALSE(mgr->resolve_token(NODE, 0, 0, addr)); // evicted via SCORE fallback + EXPECT_TRUE(mgr->resolve_token(NODE, 0, 2, addr)); + EXPECT_TRUE(mgr->resolve_token(NODE, 1, 0, addr)); + EXPECT_TRUE(mgr->resolve_token(NODE, 1, 4, addr)); +} + +// Filtered column mean: only columns of retained blocks contribute to diversity score +TEST(PagedCacheManagerTest, AdaptiveRKVFilteredColumnMean) { + // 6 blocks, block_size=2, 2 sequences + // 3 blocks per seq (6 tokens each), eviction_size = 6, eviction zone = 3 blocks + CacheLayout layout{6, 1, 2, 4}; + std::vector kd, vd; + auto mgr = make_manager(layout, EvictionPolicy::ADAPTIVE_RKV, kd, vd, 2); + + std::int32_t pasts[2] = {0, 0}; + mgr->begin_step(NODE, pasts, 2); + + std::vector krow(layout.kv_heads * layout.head_size, 0.f); + std::vector vrow(layout.kv_heads * layout.head_size, 0.f); + for (int t = 0; t < 6; t++) + mgr->write_token_kv(NODE, 0, t, krow.data(), vrow.data()); + for (int t = 0; t < 6; t++) + mgr->write_token_kv(NODE, 1, t, krow.data(), vrow.data()); + + // Attention scores: both seqs have low front block, high blocks 1,2 + std::int32_t pasts_s[2] = {6, 6}; + // seq 0: block0=0.2, block1=5.0, block2=5.0 -> target=9.18, blocks 1+2 (10.0) cover it + // seq 1: block0=0.2, block1=5.0, block2=5.0 -> same + float scores[12] = {0.1f, 0.1f, 2.5f, 2.5f, 2.5f, 2.5f, 0.1f, 0.1f, 2.5f, 2.5f, 2.5f, 2.5f}; + mgr->update_attention_scores(NODE, scores, 12, pasts_s, 2); + + // Diversity matrices [3 blocks, 6 tokens]: + // Seq 0 front-block row: high in retained columns (blocks 1,2), low in non-retained (block 0) + // tokens: [blk0:t0 blk0:t1 | blk1:t2 blk1:t3 | blk2:t4 blk2:t5] + // row 0: [0.0 0.0 | 10.0 10.0 | 10.0 10.0 ] + // filtered mean (only retained blk1,blk2 columns): mean(10,10,10,10) = 10.0 + float div0[18] = { + 0.0f, + 0.0f, + 10.0f, + 10.0f, + 10.0f, + 10.0f, // block 0 row + 5.0f, + 5.0f, + 5.0f, + 5.0f, + 5.0f, + 5.0f, // block 1 row + 5.0f, + 5.0f, + 5.0f, + 5.0f, + 5.0f, + 5.0f, // block 2 row + }; + + // Seq 1 front-block row: low everywhere + // row 0: [0.0 0.0 | 1.0 1.0 | 1.0 1.0 ] + // filtered mean (retained blk1,blk2): mean(1,1,1,1) = 1.0 + float div1[18] = { + 0.0f, + 0.0f, + 1.0f, + 1.0f, + 1.0f, + 1.0f, // block 0 row + 5.0f, + 5.0f, + 5.0f, + 5.0f, + 5.0f, + 5.0f, // block 1 row + 5.0f, + 5.0f, + 5.0f, + 5.0f, + 5.0f, + 5.0f, // block 2 row + }; + + mgr->update_diversity_scores(NODE, 0, div0, 3, 6, 0); + mgr->update_diversity_scores(NODE, 1, div1, 3, 6, 0); + + // eviction via seq 0 requesting a new block + mgr->write_token_kv(NODE, 0, 6, krow.data(), vrow.data()); + + // Seq 1 front block has filtered diversity 1.0 < seq 0's 10.0 -> seq 1 is evicted + PagedCacheManager::TokenAddress addr; + EXPECT_TRUE(mgr->resolve_token(NODE, 0, 0, addr)); // seq 0 front survived (div=10.0) + EXPECT_FALSE(mgr->resolve_token(NODE, 1, 0, addr)); // seq 1 front evicted (div=1.0) + EXPECT_TRUE(mgr->resolve_token(NODE, 1, 2, addr)); // seq 1 back survived + EXPECT_TRUE(mgr->resolve_token(NODE, 0, 6, addr)); // new token ok +} + +// start_block_offset > 0: front block is before the eviction zone (in start area) +TEST(PagedCacheManagerTest, AdaptiveRKVStartBlockOffset) { + // 6 blocks, block_size=2, 2 sequences × 3 blocks each. + // start_block_offset=1 means the eviction zone begins at block index 1, so the + // front block of each sequence (index 0) is in the "start area" and has no + // diversity row. It gets div_score=0 and is always evictable (not retained by + // the diversity data check) + CacheLayout layout{6, 1, 2, 4}; + std::vector kd, vd; + auto mgr = make_manager(layout, EvictionPolicy::ADAPTIVE_RKV, kd, vd, 2); + + std::int32_t pasts[2] = {0, 0}; + mgr->begin_step(NODE, pasts, 2); + + std::vector krow(layout.kv_heads * layout.head_size, 0.f); + std::vector vrow(layout.kv_heads * layout.head_size, 0.f); + for (int t = 0; t < 6; t++) + mgr->write_token_kv(NODE, 0, t, krow.data(), vrow.data()); + for (int t = 0; t < 6; t++) + mgr->write_token_kv(NODE, 1, t, krow.data(), vrow.data()); + + // Feed attention scores + std::int32_t pasts_s[2] = {6, 6}; + // seq 0: low front + // seq 1: also low front + float scores[12] = {0.1f, 0.1f, 5.f, 5.f, 5.f, 5.f, 0.2f, 0.2f, 5.f, 5.f, 5.f, 5.f}; + mgr->update_attention_scores(NODE, scores, 12, pasts_s, 2); + + // Diversity covering blocks 1,2 (zone): 2 zone blocks, eviction_size=4, start_block_offset=1 + // Since front block (deque index 0) is outside the zone (start_blk=1), + // start_blk > 0 -> front_is_retained=false and div_score defaults to 0 + float div0[8] = {8.f, 8.f, 8.f, 8.f, 8.f, 8.f, 8.f, 8.f}; + float div1[8] = {8.f, 8.f, 8.f, 8.f, 8.f, 8.f, 8.f, 8.f}; + mgr->update_diversity_scores(NODE, 0, div0, 2, 4, /*start_block_offset=*/1); + mgr->update_diversity_scores(NODE, 1, div1, 2, 4, /*start_block_offset=*/1); + + // Both front blocks get div_score=0, penalty differentiates. + // Requester (seq 0) gets +1e12 penalty -> seq 1 front block evicted + mgr->write_token_kv(NODE, 0, 6, krow.data(), vrow.data()); + + PagedCacheManager::TokenAddress addr; + EXPECT_TRUE(mgr->resolve_token(NODE, 0, 0, addr)); + EXPECT_FALSE(mgr->resolve_token(NODE, 1, 0, addr)); // seq 1 front evicted + EXPECT_TRUE(mgr->resolve_token(NODE, 1, 2, addr)); + EXPECT_TRUE(mgr->resolve_token(NODE, 0, 6, addr)); +} + +// ADAPTIVE_RKV with scores but no diversity -> falls back to SCORE (not FIFO) +TEST(PagedCacheManagerTest, AdaptiveRKVFallsBackToScoreWithScoresNoDiversity) { + CacheLayout layout{4, 1, 2, 4}; + std::vector kd, vd; + auto mgr = make_manager(layout, EvictionPolicy::ADAPTIVE_RKV, kd, vd, 2); + + std::int32_t pasts[2] = {0, 0}; + mgr->begin_step(NODE, pasts, 2); + + std::vector krow(layout.kv_heads * layout.head_size, 0.f); + std::vector vrow(layout.kv_heads * layout.head_size, 0.f); + for (int t = 0; t < 4; t++) + mgr->write_token_kv(NODE, 0, t, krow.data(), vrow.data()); + for (int t = 0; t < 4; t++) + mgr->write_token_kv(NODE, 1, t, krow.data(), vrow.data()); + + // Feed scores only (no diversity). Seq 0 front = 0.2, seq 1 front = 8.0 + pasts[0] = 4; + pasts[1] = 4; + float scores[8] = {0.1f, 0.1f, 5.f, 5.f, 4.f, 4.f, 4.f, 4.f}; + mgr->update_attention_scores(NODE, scores, 8, pasts, 2); + + // diversity chain: no diversity data -> candidates empty -> fallback to steal_block_by_score + // SCORE picks seq 0 front (0.2) over seq 1 front (8.0) + mgr->write_token_kv(NODE, 1, 4, krow.data(), vrow.data()); + + PagedCacheManager::TokenAddress addr; + EXPECT_FALSE(mgr->resolve_token(NODE, 0, 0, addr)); // seq 0 evicted by SCORE fallback + EXPECT_TRUE(mgr->resolve_token(NODE, 0, 2, addr)); + EXPECT_TRUE(mgr->resolve_token(NODE, 1, 0, addr)); + EXPECT_TRUE(mgr->resolve_token(NODE, 1, 4, addr)); +} + +// Diversity matrix is cleared after eviction, next eviction must use fresh data or fallback +TEST(PagedCacheManagerTest, AdaptiveRKVDiversityClearedAfterEviction) { + CacheLayout layout{4, 1, 2, 4}; + std::vector kd, vd; + auto mgr = make_manager(layout, EvictionPolicy::ADAPTIVE_RKV, kd, vd, 2); + + std::int32_t pasts[2] = {0, 0}; + mgr->begin_step(NODE, pasts, 2); + + std::vector krow(layout.kv_heads * layout.head_size, 0.f); + std::vector vrow(layout.kv_heads * layout.head_size, 0.f); + for (int t = 0; t < 4; t++) + mgr->write_token_kv(NODE, 0, t, krow.data(), vrow.data()); + for (int t = 0; t < 4; t++) + mgr->write_token_kv(NODE, 1, t, krow.data(), vrow.data()); + + std::int32_t pasts_s[2] = {4, 4}; + float scores[8] = {0.1f, 0.1f, 5.f, 5.f, 2.f, 2.f, 2.f, 2.f}; + mgr->update_attention_scores(NODE, scores, 8, pasts_s, 2); + + float div0[8] = {0.1f, 0.1f, 0.1f, 0.1f, 5.f, 5.f, 5.f, 5.f}; + float div1[8] = {3.f, 3.f, 3.f, 3.f, 3.f, 3.f, 3.f, 3.f}; + mgr->update_diversity_scores(NODE, 0, div0, 2, 4, 0); + mgr->update_diversity_scores(NODE, 1, div1, 2, 4, 0); + + // First eviction: seq 0 front evicted (low diversity) + mgr->write_token_kv(NODE, 1, 4, krow.data(), vrow.data()); + + PagedCacheManager::TokenAddress addr; + EXPECT_FALSE(mgr->resolve_token(NODE, 0, 0, addr)); + + // Diversity was cleared for the evicted seq. Now seq 0 has 1 block left, seq 1 has 3. + // Force another eviction without feeding new diversity. + // Since diversity_matrix was cleared, fallback to SCORE should kick in. + // Need to also update scores for the new arrangement + pasts_s[0] = 4; // logical length still 4 + pasts_s[1] = 5; + // seq 0's remaining block (tokens 2,3) still has score 10.0 from before + // seq 1 has 3 blocks with scores. No new diversity -> SCORE fallback + mgr->write_token_kv(NODE, 1, 5, krow.data(), vrow.data()); + + // Should not crash; some eviction should occur + EXPECT_TRUE(mgr->resolve_token(NODE, 1, 5, addr)); +} + +// Adaptive RKV cache eviction tests. +// Verify attention_mass_p and pool_kernel behavior. + +// attention_mass_p controls what fraction of total attention mass +// the retained set must cover. Higher p = more blocks retained. +// +// Setup: 4 blocks, block_size=2. +// Seq 0 per-block scores: block0=1.0, block1=9.0 (total 10) +// Seq 1 per-block scores: block0=4.9, block1=5.1 (total 10) +// +// With p=0.9 (target=9.0): +// Seq 0: block1 alone (9.0) >= 9.0 -> block0 NOT retained (eligible) +// Seq 1: block1 (5.1) < 9.0, need both -> block0 IS retained (protected) +// Only seq 0 is a diversity candidate -> seq 0 front evicted +// +// With p=0.49 (target=4.9): +// Seq 0: block1 (9.0) >= 4.9 -> block0 NOT retained (eligible) +// Seq 1: block1 (5.1) >= 4.9 -> block0 NOT retained (eligible) +// Both are candidates. Seq 0 is the requester (+1e12 penalty) -> seq 1 front evicted. +TEST(PagedCacheManagerTest, AttentionMassPChangesRetentionThreshold) { + const CacheLayout layout{4, 1, 2, 4}; + // per-token scores: seq0 blk0=1.0, blk1=9.0 | seq1 blk0=4.9, blk1=5.1 + float scores[8] = {0.5f, 0.5f, 4.5f, 4.5f, 2.45f, 2.45f, 2.55f, 2.55f}; + // diversity matrices (2 blocks x evict_size=4). + // div0 row 0: all 5.0 (seq 0 front-block filtered mean = 5.0) + // div1 row 0: all 0.1 (seq 1 front-block filtered mean = 0.1 regardless of retained column) + float div0[8] = {5.0f, 5.0f, 5.0f, 5.0f, 5.0f, 5.0f, 5.0f, 5.0f}; + float div1[8] = {0.1f, 0.1f, 0.1f, 0.1f, 0.1f, 0.1f, 0.1f, 0.1f}; + + auto run = [&](float p) -> std::pair /* */ { + std::vector kd(layout.elems(), 0.f); + std::vector vd(layout.elems(), 0.f); + auto mgr = std::make_unique(ov::element::f32, + EvictionPolicy::ADAPTIVE_RKV, + /*max_cache_bytes=*/0, + /*attention_mass_p=*/p); + std::vector past_init(2, 0); + mgr->ensure_operator(NODE, + kd.data(), + vd.data(), + layout.shape(), + layout.shape(), + nullptr, + 0, + nullptr, + 0, + past_init.data(), + 2); + + std::int32_t pasts[2] = {0, 0}; + mgr->begin_step(NODE, pasts, 2); + + std::vector krow(layout.kv_heads * layout.head_size, 0.f); + std::vector vrow(layout.kv_heads * layout.head_size, 0.f); + for (int t = 0; t < 4; t++) + mgr->write_token_kv(NODE, 0, t, krow.data(), vrow.data()); + for (int t = 0; t < 4; t++) + mgr->write_token_kv(NODE, 1, t, krow.data(), vrow.data()); + + pasts[0] = 4; + pasts[1] = 4; + mgr->update_attention_scores(NODE, scores, 8, pasts, 2); + mgr->update_diversity_scores(NODE, 0, div0, 2, 4, 0); + mgr->update_diversity_scores(NODE, 1, div1, 2, 4, 0); + + // seq 0 requests a new block, triggering eviction + mgr->write_token_kv(NODE, 0, 4, krow.data(), vrow.data()); + + PagedCacheManager::TokenAddress addr; + bool s0 = mgr->resolve_token(NODE, 0, 0, addr); + bool s1 = mgr->resolve_token(NODE, 1, 0, addr); + return {s0, s1}; + }; + + // p=0.9: seq 1 front retained (protected), only seq 0 eligible -> seq 0 front evicted + auto [s0_hi, s1_hi] = run(0.9f); + EXPECT_FALSE(s0_hi) << "p=0.9: seq 0 front should be evicted (only eligible candidate)"; + EXPECT_TRUE(s1_hi) << "p=0.9: seq 1 front should be protected (retained by gating)"; + + // p=0.49: both fronts eligible; seq 0 penalized as requester -> seq 1 front evicted + auto [s0_lo, s1_lo] = run(0.49f); + EXPECT_TRUE(s0_lo) << "p=0.49: seq 0 front should survive (penalized as requester)"; + EXPECT_FALSE(s1_lo) << "p=0.49: seq 1 front should be evicted (lower filtered diversity)"; +} + +// pool_kernel max-pool smoothing changes per-block scores and thus eviction order +// +// Scenario: 1 sequence, 3 blocks (block_size=2), with all scores concentrated on +// a single spike token in block 0. +// raw scores: [10, 0, 0, 0, 0, 0] -> block sums: [10, 0, 0] +// +// With pool_kernel=1 (no pooling): block 0 has score 10, blocks 1,2 have 0. +// SCORE eviction evicts the lowest-score front block. After filling cache +// and requesting a new block, the single-seq self-eviction targets the +// lowest-score block. Block 0 = front; its score (10) is NOT the lowest. +// The code pops the deque front regardless of which block is "best" to evict: +// steal_block_by_score always evicts the front block of the victim sequence, +// so at eviction time, it compares front-block scores across sequences. +// With a single sequence, front block is the only candidate and is evicted +// no matter what. +// +// Actual test strategy: 2 sequences, score spike in seq 0 block 0. +// raw per-token scores: seq0=[10,0,0,0], seq1=[0.1,0.1,0.1,0.1] +// Without pooling (pk=1): seq0 front=10, seq1 front=0.2 -> evicts seq1 front +// With pooling (pk=3): max-pool spreads the spike from token 0 into token 1: +// seq0 pooled = [10,10,0,0] -> block score = 20 (front block gets 10 extra) +// seq1 pooled = [0.1,0.1,0.1,0.1] -> block score = 0.2 (mostly unchanged) +// Again seq1 front < seq0 front -> evicts seq1 front. Same outcome. +// +// To make pool_kernel flip the outcome, we need a spike at the boundary between +// blocks 0 and 1 that pooling pulls INTO block 0: +// raw: seq0=[0,0.1, 0.1,10], seq1=[2,2, 2,2] +// block sums without pooling: seq0_front=0.1, seq1_front=4.0 +// requester=seq0, penalty on seq0. seq1 front (4.0) < seq0 front (0.1+1e12) +// -> seq1 front evicted +// +// With pk=5 (half_k=2): max-pool pulls seq0 token 3 spike (10) into tokens 1,2: +// seq0 pooled = [0.1, 10, 10, 10] -> block0 sum = 10.1, block1 sum = 20 +// seq1 pooled = [2, 2, 2, 2] -> block0 sum = 4, block1 sum = 4 +// requester=seq0, penalty on seq0. seq1 front (4) < seq0 front (10.1+1e12) +// -> seq1 front still evicted (same outcome - penalty dominates). +// +// Therefore: to observe a flip, the requesting sequence must NOT be the one +// with the spike. Let seq 1 request, spike in seq 0. +// raw: seq0=[0.1, 0.1, 0.1, 10], seq1=[3, 3, 3, 3] +// Without pooling (pk=1): +// seq0 block sums: front=0.2, back=10.1 +// seq1 block sums: front=6, back=6 +// Seq 1 is requester -> penalty on seq1. Victim is lowest un-penalized front. +// seq0 front=0.2 < seq1 front=6+1e12 -> seq0 front evicted. +// +// With pk=5 (half_k=2): max-pool spreads token 3 (10) left: +// seq0 pooled per-token = [0.1, 10, 10, 10] -> block0=10.1, block1=20 +// seq1 pooled per-token = [3, 3, 3, 3] -> block0=6, block1=6 +// seq0 front=10.1 > seq1 front=6. +// Victim = min(seq0=10.1, seq1=6+1e12) = seq0 (10.1) -> seq0 front evicted. +// Same outcome! The penalty (1e12) on the requester is so large that the non- +// requester always wins unless both are equally penalized. +// +// Conclusion: Since steal_block_by_score adds +1e12 to the requester's front-block +// score, the requester is NEVER evicted unless it's the only sequence. Pooling can +// change the relative ordering only among non-requester sequences (or for single- +// sequence self-eviction where there IS no penalty distinction). +// To demonstrate pool_kernel's effect, use 3 sequences: one requester (penalized) and +// two victims whose relative front-block scores flip when pooling is applied +TEST(PagedCacheManagerTest, PoolKernelChangesEvictionVictimViaPA) { + // 6 blocks, block_size=2, 3 sequences × 2 blocks each. + // seq 0 (requester): uniform scores -> front=4.0 (always penalized, irrelevant) + // seq 1: spike at token 3 (block 1), front block is low without pooling but boosted with pooling + // seq 2: uniform moderate -> front=2.0 (stable) + // + // Without pooling (pk=1): + // seq1 front = 0.2, seq2 front = 2.0 -> seq1 front evicted + // With pooling (pk=3, half_k=1) the spike at token 3 spreads into token 2 (block 1): + // seq1 pooled = [0.1, max(0.1,0.1,10)=10, max(0.1,10,...)=10, 10] + // -> front block sum = 0.1 + 10 = 10.1 + // seq2 pooled = [1, 1, 1, 1] -> front = 2.0 + // Now seq2 front (2.0) < seq1 front (10.1) -> seq2 front evicted instead! + const std::size_t num_blocks = 6; + const std::size_t kv_heads = 1; + const std::size_t blk_size = 2; + const std::size_t head_size = 4; + const std::size_t q_heads = 1; + const std::size_t q_features = q_heads * head_size; + const std::size_t kv_features = kv_heads * head_size; + const std::size_t cache_elems = num_blocks * kv_heads * blk_size * head_size; + const ov::Shape cache_shape = {num_blocks, kv_heads, blk_size, head_size}; + + auto run = [&](std::size_t pk) -> std::tuple { + std::vector key_cache(cache_elems, 0.f); + std::vector val_cache(cache_elems, 0.f); + + auto mgr = std::make_unique(ov::element::f32, + EvictionPolicy::SCORE, + /*max_cache_bytes=*/0); + constexpr std::uintptr_t nk = 123; + + // Step 1: prompt with 4 tokens per sequence (12 total), fills all 6 blocks + const std::size_t tokens = 12; + std::vector q(tokens * q_features, 0.f); + std::vector k(tokens * kv_features, 0.f); + std::vector v(tokens * kv_features, 0.f); + + // Give each key token a distinct direction so attention weights form a pattern. + // We want: per-sequence accumulated scores ~ [0.1, 0.1, 0.1, 10] for seq 1 + // [1, 1, 1, 1] for seq 0 and seq 2. + // Easiest: make query for each token point in direction matching its own key, + // so self-attention is dominant. Then the "spike" token in seq 1 will have + // high attention from itself. + // + // Actually, to control raw per-token scores precisely, we'd need to engineer + // Q/K values carefully. Instead, let's test the pooling path indirectly: + // just run the PA kernel with pk=1 and pk=3, feed the SAME Q/K/V, and verify + // that the block_scores differ (and thus potentially the eviction victim). + // + // Simpler approach: test pool_kernel effect through the cache manager API + // directly, by manually calling update_attention_scores after doing the + // pooling ourselves, and verify the block_scores change the eviction result. + // But this wouldn't exercise pool_kernel in the PA kernel. + // + // Best approach: use the PA kernel path but with carefully crafted Q/K that + // produce a known score spike. + // Set all queries to [1,0,0,0] and all keys to [1,0,0,0] EXCEPT seq1 token 3 + // which gets key=[10,0,0,0]. Then that token gets ~10x the attention. + + // scale factor + float scale_val = 1.0f; // no scaling to keep things predictable + + // uniform Q: all [1,0,0,0] + for (std::size_t i = 0; i < tokens; i++) { + q[i * q_features] = 1.0f; + } + // uniform K: all [1,0,0,0] + for (std::size_t i = 0; i < tokens; i++) { + k[i * kv_features] = 1.0f; + } + // uniform V: all [1,0,0,0] + for (std::size_t i = 0; i < tokens; i++) { + v[i * kv_features] = 1.0f; + } + + // 3 sequences of length 4 + std::int32_t past_lens[3] = {0, 0, 0}; + std::int32_t subseq[4] = {0, 4, 8, 12}; + std::int32_t score_window = 0; + std::vector block_idx = {0, 1, 2, 3, 4, 5}; + std::vector block_begins_init = {0, 2, 4, 6}; + + std::vector out(tokens * q_features, 0.f); + std::vector out_scores(tokens, 0.f); + + ov::reference::paged_attention( + nk, + mgr.get(), + out.data(), + out_scores.data(), // non-null -> scores_per_head allocated -> pooling enabled + nullptr, + q.data(), + k.data(), + v.data(), + key_cache.data(), + val_cache.data(), + past_lens, + subseq, + block_idx.data(), + block_idx.size(), + block_begins_init.data(), + block_begins_init.size(), + &scale_val, + ov::element::f32, + nullptr, // sliding_window + nullptr, // alibi_slopes + ov::element::Type{}, + {}, + nullptr, // max_context_len + &score_window, + 1, + nullptr, // rotated_block_indices + 0, + nullptr, // rotation_deltas + {}, + nullptr, // rotation_trig_lut + ov::element::Type{}, + {}, + nullptr, // xattention_threshold + ov::element::Type{}, + nullptr, // xattention_block_size + nullptr, // xattention_stride + nullptr, // sinks + ov::element::Type{}, + nullptr, // adaptive_rkv_start_size + nullptr, // adaptive_rkv_evictable_sizes + nullptr, // adaptive_rkv_diversity_block_set_indices + nullptr, // adaptive_rkv_diversity_block_set_indices_begins + nullptr, // token_type_ids + 0, // token_type_ids_count + {tokens, q_features}, + {tokens, kv_features}, + {tokens, kv_features}, + cache_shape, + cache_shape, + {3}, + {4}, + pk); // pool_kernel + + // Step 2: seq 0 adds 1 token, forcing eviction (all 6 blocks used) + // New query/key/value for 1 extra token + std::vector q2(q_features, 0.f); + std::vector k2(kv_features, 0.f); + std::vector v2(kv_features, 0.f); + q2[0] = 1.0f; + k2[0] = 1.0f; + v2[0] = 1.0f; + + std::int32_t past2[3] = {4, 4, 4}; + std::int32_t subseq2[4] = {0, 1, 1, 1}; // only seq 0 has 1 new token + std::vector out2(q_features, 0.f); + // total_score_len = sum(past+new) for all seqs = (4+1) + (4+0) + (4+0) = 13 + std::vector out_scores2(13, 0.f); + + ov::reference::paged_attention(nk, + mgr.get(), + out2.data(), + out_scores2.data(), + nullptr, + q2.data(), + k2.data(), + v2.data(), + key_cache.data(), + val_cache.data(), + past2, + subseq2, + block_idx.data(), + block_idx.size(), + block_begins_init.data(), + block_begins_init.size(), + &scale_val, + ov::element::f32, + nullptr, + nullptr, + ov::element::Type{}, + {}, + nullptr, + &score_window, + 1, + nullptr, + 0, + nullptr, + {}, + nullptr, + ov::element::Type{}, + {}, + nullptr, + ov::element::Type{}, + nullptr, + nullptr, + nullptr, + ov::element::Type{}, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + 0, + {1, q_features}, + {1, kv_features}, + {1, kv_features}, + cache_shape, + cache_shape, + {3}, + {4}, + pk); // pool_kernel + + PagedCacheManager::TokenAddress addr; + bool s0 = mgr->resolve_token(nk, 0, 0, addr); + bool s1 = mgr->resolve_token(nk, 1, 0, addr); + bool s2 = mgr->resolve_token(nk, 2, 0, addr); + return {s0, s1, s2}; + }; + + // With pk=1 (no pooling) and pk=7 (heavy pooling), the scores fed to the cache + // manager differ because max-pool smooths the per-token scores. We verify that + // the code path runs without crashing and returns consistent results. + // With uniform Q and K the attention should be fairly uniform, so pooling won't + // dramatically change the outcome, but the important thing is that the pooling + // code path executes and the eviction still produces a valid result + auto [a0, a1, a2] = run(1); // no pooling + auto [b0, b1, b2] = run(7); // pooling with kernel 7 + + // With uniform Q/K, all front-block scores are approximately equal. + // Seq 0 is the requester and gets +1e12 penalty, so one of seq 1 or 2 is evicted + EXPECT_TRUE(a0) << "pk=1: seq 0 should survive (requester penalty protects it)"; + EXPECT_TRUE(b0) << "pk=7: seq 0 should survive (requester penalty protects it)"; + // At least one of seq 1, seq 2 should be evicted (one front block freed) + EXPECT_FALSE(a1 && a2) << "pk=1: one of seq 1/2 should have lost its front block"; + EXPECT_FALSE(b1 && b2) << "pk=7: one of seq 1/2 should have lost its front block"; +} + +// =================================================================== +// Bidirectional image attention via token_type_ids +// =================================================================== + +// Helper: run paged_attention with given token_type_ids and sliding_window, return output +static std::vector run_pa_with_token_types(const std::vector& q, + const std::vector& k, + const std::vector& v, + std::size_t num_tokens, + std::size_t q_heads, + std::size_t kv_heads, + std::size_t head_size, + const std::vector& token_type_ids, + int32_t sliding_window_val = 0) { + const std::size_t q_features = q_heads * head_size; + const std::size_t kv_features = kv_heads * head_size; + const std::size_t block_size = 4; + const std::size_t num_blocks = 8; + + ov::Shape cache_shape = {num_blocks, kv_heads, block_size, head_size}; + std::vector key_cache(num_blocks * kv_heads * block_size * head_size, 0.f); + std::vector val_cache(num_blocks * kv_heads * block_size * head_size, 0.f); + + auto mgr = std::make_unique(ov::element::f32, EvictionPolicy::FIFO, 0); + constexpr std::uintptr_t nk = 200; + + std::int32_t past = 0; + mgr->ensure_operator(nk, + key_cache.data(), + val_cache.data(), + cache_shape, + cache_shape, + nullptr, + 0, + nullptr, + 0, + &past, + 1); + + std::int32_t subseq[2] = {0, static_cast(num_tokens)}; + // scale=1.0 so softmax input = raw dot product, keeps expected values simple + float scale_val = 1.0f; + std::int32_t score_window = 0; + std::vector block_idx(num_blocks); + std::iota(block_idx.begin(), block_idx.end(), 0); + std::vector block_begins = {0, static_cast(num_blocks)}; + + const int32_t* sliding_ptr = (sliding_window_val > 0) ? &sliding_window_val : nullptr; + const int32_t* token_types_ptr = token_type_ids.empty() ? nullptr : token_type_ids.data(); + std::size_t token_types_count = token_type_ids.size(); + + std::vector out(num_tokens * q_features, -999.f); + + ov::reference::paged_attention(nk, + mgr.get(), + out.data(), + nullptr, + nullptr, + q.data(), + k.data(), + v.data(), + key_cache.data(), + val_cache.data(), + &past, + subseq, + block_idx.data(), + block_idx.size(), + block_begins.data(), + block_begins.size(), + &scale_val, + ov::element::f32, + sliding_ptr, + nullptr, + ov::element::Type{}, + {}, + nullptr, + &score_window, + 1, + nullptr, + 0, + nullptr, + {}, + nullptr, + ov::element::Type{}, + {}, + nullptr, + ov::element::Type{}, + nullptr, + nullptr, + nullptr, + ov::element::Type{}, + nullptr, + nullptr, + nullptr, + nullptr, + token_types_ptr, + token_types_count, + {num_tokens, q_features}, + {num_tokens, kv_features}, + {num_tokens, kv_features}, + cache_shape, + cache_shape, + {1}, + {2}); + return out; +} + +// Bidirectional attention: image tokens attend to the full image group. +// With uniform Q=[1,0,0,0], K=[1,0,0,0] (all dots=1), scale=1.0, softmax produces +// uniform weights 1/N over all N attended positions. Output = mean(V[attended]). +TEST(PagedAttentionRefTest, BidirectionalImageAttention_GroupTokensSameOutput) { + // 6 tokens: [text, image, image, image, text, text] + // Image tokens 1,2,3 form a group -> each attends to positions {0,1,2,3}. + // Text token 0 attends to {0}, text token 4 attends to {0..4}, text token 5 to {0..5}. + const std::size_t num_tokens = 6; + const std::size_t q_heads = 2; + const std::size_t kv_heads = 2; + const std::size_t head_size = 4; + const std::size_t q_features = q_heads * head_size; + const std::size_t kv_features = kv_heads * head_size; + + std::vector q(num_tokens * q_features, 0.f); + std::vector k(num_tokens * kv_features, 0.f); + std::vector v(num_tokens * kv_features, 0.f); + + for (std::size_t i = 0; i < num_tokens; i++) { + q[i * q_features] = 1.0f; + k[i * kv_features] = 1.0f; + // First value feature = position+1, rest zeros + v[i * kv_features] = static_cast(i + 1); + } + + std::vector token_types = {0, 1, 1, 1, 0, 0}; + + auto out = run_pa_with_token_types(q, k, v, num_tokens, q_heads, kv_heads, head_size, token_types); + + // All outputs should be finite + for (std::size_t i = 0; i < out.size(); i++) { + EXPECT_TRUE(std::isfinite(out[i])) << "output[" << i << "] is not finite"; + } + + // Image tokens 1,2,3: all attend to {0,1,2,3}, uniform weight -> mean = (1+2+3+4)/4 = 2.5 + const float img_expected = 2.5f; + EXPECT_NEAR(out[1 * q_features], img_expected, 0.01f); + EXPECT_NEAR(out[2 * q_features], img_expected, 0.01f); + EXPECT_NEAR(out[3 * q_features], img_expected, 0.01f); + + // Text token 0: attends to {0} -> output = v[0] = 1.0 + EXPECT_NEAR(out[0 * q_features], 1.0f, 0.01f); + + // Text token 4: causal, attends to {0,1,2,3,4} -> mean = (1+2+3+4+5)/5 = 3.0 + EXPECT_NEAR(out[4 * q_features], 3.0f, 0.01f); + + // Text token 5: causal, attends to {0..5} -> mean = (1+2+3+4+5+6)/6 = 3.5 + EXPECT_NEAR(out[5 * q_features], 3.5f, 0.01f); +} + +// Text tokens remain strictly causal even when token_type_ids is present. +TEST(PagedAttentionRefTest, BidirectionalImageAttention_TextTokensCausal) { + // 5 tokens: [image, image, text, text, text] + // Image group: {0,1}. Text tokens: {2,3,4}. + const std::size_t num_tokens = 5; + const std::size_t q_heads = 1; + const std::size_t kv_heads = 1; + const std::size_t head_size = 4; + const std::size_t q_features = q_heads * head_size; + const std::size_t kv_features = kv_heads * head_size; + + std::vector q(num_tokens * q_features, 0.f); + std::vector k(num_tokens * kv_features, 0.f); + std::vector v(num_tokens * kv_features, 0.f); + + for (std::size_t i = 0; i < num_tokens; i++) { + q[i * q_features] = 1.0f; + k[i * kv_features] = 1.0f; + v[i * kv_features] = static_cast(i + 1); + } + + std::vector token_types = {1, 1, 0, 0, 0}; + + auto out = run_pa_with_token_types(q, k, v, num_tokens, q_heads, kv_heads, head_size, token_types); + + for (std::size_t i = 0; i < out.size(); i++) { + EXPECT_TRUE(std::isfinite(out[i])) << "output[" << i << "] is not finite"; + } + + // Image tokens 0,1: attend to {0,1} -> mean = (1+2)/2 = 1.5 + EXPECT_NEAR(out[0 * q_features], 1.5f, 0.01f); + EXPECT_NEAR(out[1 * q_features], 1.5f, 0.01f); + + // Text token 2: causal, attends to {0,1,2} -> mean = (1+2+3)/3 = 2.0 + EXPECT_NEAR(out[2 * q_features], 2.0f, 0.01f); + + // Text token 3: causal, attends to {0,1,2,3} -> mean = (1+2+3+4)/4 = 2.5 + EXPECT_NEAR(out[3 * q_features], 2.5f, 0.01f); + + // Text token 4: causal, attends to {0,1,2,3,4} -> mean = (1+2+3+4+5)/5 = 3.0 + EXPECT_NEAR(out[4 * q_features], 3.0f, 0.01f); +} + +// The window is applied relative to the extended ncausal, not the default causal position. +TEST(PagedAttentionRefTest, BidirectionalImageAttention_SlidingWindow) { + // 8 tokens: [text, text, image, image, image, image, text, text] + // Image group: tokens 2,3,4,5 -> group end = 6 -> ncausal = 6. + // Sliding window = 3 -> start = max(0, 6-3) = 3, end = 5. + // All image tokens see KV positions {3,4,5}: uniform weight -> mean(v[3..5]). + const std::size_t num_tokens = 8; + const std::size_t q_heads = 1; + const std::size_t kv_heads = 1; + const std::size_t head_size = 4; + const std::size_t q_features = q_heads * head_size; + const std::size_t kv_features = kv_heads * head_size; + + std::vector q(num_tokens * q_features, 0.f); + std::vector k(num_tokens * kv_features, 0.f); + std::vector v(num_tokens * kv_features, 0.f); + + for (std::size_t i = 0; i < num_tokens; i++) { + q[i * q_features] = 1.0f; + k[i * kv_features] = 1.0f; + v[i * kv_features] = static_cast(i + 1) * 10.0f; + } + + std::vector token_types = {0, 0, 1, 1, 1, 1, 0, 0}; + int32_t sliding_window = 3; + + auto out = run_pa_with_token_types(q, k, v, num_tokens, q_heads, kv_heads, head_size, token_types, sliding_window); + + for (std::size_t i = 0; i < out.size(); i++) { + EXPECT_TRUE(std::isfinite(out[i])) << "output[" << i << "] is not finite"; + } + + // Sliding window start uses original causal_pos, pulled back to image_group_begin. + // Token 2: causal_pos=3, sw_start=0, group_begin=2, start=min(0,2)=0, ncausal=6. Sees {0..5}. + EXPECT_NEAR(out[2 * q_features], (10 + 20 + 30 + 40 + 50 + 60) / 6.f, 0.01f); // 35.0 + // Token 3: causal_pos=4, sw_start=1, group_begin=2, start=min(1,2)=1, ncausal=6. Sees {1..5}. + EXPECT_NEAR(out[3 * q_features], (20 + 30 + 40 + 50 + 60) / 5.f, 0.01f); // 40.0 + // Token 4: causal_pos=5, sw_start=2, group_begin=2, start=min(2,2)=2, ncausal=6. Sees {2..5}. + EXPECT_NEAR(out[4 * q_features], (30 + 40 + 50 + 60) / 4.f, 0.01f); // 45.0 + // Token 5: causal_pos=6, sw_start=3, group_begin=2, start=min(3,2)=2, ncausal=6. Sees {2..5}. + EXPECT_NEAR(out[5 * q_features], (30 + 40 + 50 + 60) / 4.f, 0.01f); // 45.0 + + // Text token 0: causal_pos=1, SW=3 -> start=0. Sees {0}. Out = 10. + EXPECT_NEAR(out[0 * q_features], 10.0f, 0.01f); + // Text token 1: causal_pos=2, SW=3 -> start=0. Sees {0,1}. Out = 15. + EXPECT_NEAR(out[1 * q_features], 15.0f, 0.01f); + // Text token 6: causal_pos=7, SW=3 -> start=4. Sees {4,5,6}. Out = 60. + EXPECT_NEAR(out[6 * q_features], 60.0f, 0.01f); + // Text token 7: causal_pos=8, SW=3 -> start=5. Sees {5,6,7}. Out = 70. + EXPECT_NEAR(out[7 * q_features], 70.0f, 0.01f); +} + +// Multiple image groups are handled independently. +TEST(PagedAttentionRefTest, BidirectionalImageAttention_MultipleGroups) { + // 8 tokens: [image, image, text, image, image, image, text, text] + // Group 1: tokens {0,1} -> ncausal=2 for both. Group 2: tokens {3,4,5} -> ncausal=6. + const std::size_t num_tokens = 8; + const std::size_t q_heads = 1; + const std::size_t kv_heads = 1; + const std::size_t head_size = 4; + const std::size_t q_features = q_heads * head_size; + const std::size_t kv_features = kv_heads * head_size; + + std::vector q(num_tokens * q_features, 0.f); + std::vector k(num_tokens * kv_features, 0.f); + std::vector v(num_tokens * kv_features, 0.f); + + for (std::size_t i = 0; i < num_tokens; i++) { + q[i * q_features] = 1.0f; + k[i * kv_features] = 1.0f; + v[i * kv_features] = static_cast(i + 1); + } + + std::vector token_types = {1, 1, 0, 1, 1, 1, 0, 0}; + + auto out = run_pa_with_token_types(q, k, v, num_tokens, q_heads, kv_heads, head_size, token_types); + + for (std::size_t i = 0; i < out.size(); i++) { + EXPECT_TRUE(std::isfinite(out[i])) << "output[" << i << "] is not finite"; + } + + // Group 1 (tokens 0,1): both attend to {0,1} -> mean = (1+2)/2 = 1.5 + EXPECT_NEAR(out[0 * q_features], 1.5f, 0.01f); + EXPECT_NEAR(out[1 * q_features], 1.5f, 0.01f); + + // Text token 2: causal, attends to {0,1,2} -> mean = (1+2+3)/3 = 2.0 + EXPECT_NEAR(out[2 * q_features], 2.0f, 0.01f); + + // Group 2 (tokens 3,4,5): all attend to {0,1,2,3,4,5} -> mean = (1+2+3+4+5+6)/6 = 3.5 + EXPECT_NEAR(out[3 * q_features], 3.5f, 0.01f); + EXPECT_NEAR(out[4 * q_features], 3.5f, 0.01f); + EXPECT_NEAR(out[5 * q_features], 3.5f, 0.01f); + + // Text token 6: causal, attends to {0..6} -> mean = (1+2+3+4+5+6+7)/7 = 4.0 + EXPECT_NEAR(out[6 * q_features], 4.0f, 0.01f); + + // Text token 7: causal, attends to {0..7} -> mean = (1+..+8)/8 = 4.5 + EXPECT_NEAR(out[7 * q_features], 4.5f, 0.01f); +} + +// All-text token_type_ids is equivalent to standard causal behavior +TEST(PagedAttentionRefTest, BidirectionalImageAttention_AllTextCausal) { + // 4 tokens, all text. Each causal: token i attends to {0..i}. + const std::size_t num_tokens = 4; + const std::size_t q_heads = 1; + const std::size_t kv_heads = 1; + const std::size_t head_size = 4; + const std::size_t q_features = q_heads * head_size; + const std::size_t kv_features = kv_heads * head_size; + + std::vector q(num_tokens * q_features, 0.f); + std::vector k(num_tokens * kv_features, 0.f); + std::vector v(num_tokens * kv_features, 0.f); + + for (std::size_t i = 0; i < num_tokens; i++) { + q[i * q_features] = 1.0f; + k[i * kv_features] = 1.0f; + v[i * kv_features] = static_cast(i + 1); + } + + std::vector all_text = {0, 0, 0, 0}; + auto out = run_pa_with_token_types(q, k, v, num_tokens, q_heads, kv_heads, head_size, all_text); + + for (std::size_t i = 0; i < out.size(); i++) { + EXPECT_TRUE(std::isfinite(out[i])) << "output[" << i << "] is not finite"; + } + + // Token 0: sees {0} -> 1.0 + EXPECT_NEAR(out[0 * q_features], 1.0f, 0.01f); + // Token 1: sees {0,1} -> (1+2)/2 = 1.5 + EXPECT_NEAR(out[1 * q_features], 1.5f, 0.01f); + // Token 2: sees {0,1,2} -> (1+2+3)/3 = 2.0 + EXPECT_NEAR(out[2 * q_features], 2.0f, 0.01f); + // Token 3: sees {0,1,2,3} -> (1+2+3+4)/4 = 2.5 + EXPECT_NEAR(out[3 * q_features], 2.5f, 0.01f); +} + +// Sliding window smaller than the group: group_begin pullback ensures later image tokens +// still see the group beginning even when the sliding window would clip it away. +TEST(PagedAttentionRefTest, BidirectionalImageAttention_GroupBeginPullback) { + // 6 tokens: [text, image, image, image, image, text] + // Image group: tokens {1,2,3,4}, sliding_window = 2. + const std::size_t num_tokens = 6; + const std::size_t q_heads = 1; + const std::size_t kv_heads = 1; + const std::size_t head_size = 4; + const std::size_t q_features = q_heads * head_size; + const std::size_t kv_features = kv_heads * head_size; + + std::vector q(num_tokens * q_features, 0.f); + std::vector k(num_tokens * kv_features, 0.f); + std::vector v(num_tokens * kv_features, 0.f); + + for (std::size_t i = 0; i < num_tokens; i++) { + q[i * q_features] = 1.0f; + k[i * kv_features] = 1.0f; + v[i * kv_features] = static_cast(i + 1) * 10.0f; + } + + std::vector token_types = {0, 1, 1, 1, 1, 0}; + int32_t sliding_window = 2; + + auto out = run_pa_with_token_types(q, k, v, num_tokens, q_heads, kv_heads, head_size, token_types, sliding_window); + + for (std::size_t i = 0; i < out.size(); i++) { + EXPECT_TRUE(std::isfinite(out[i])) << "output[" << i << "] is not finite"; + } + + // Token 0: text, causal_pos=1, SW=2, start=0. Sees {0}. Out = 10. + EXPECT_NEAR(out[0 * q_features], 10.0f, 0.01f); + + // Token 1: causal_pos=2, sw_start=0, group_begin=1, start=min(0,1)=0, ncausal=5. + // Sees {0,1,2,3,4}. Mean = (10+20+30+40+50)/5 = 30. + EXPECT_NEAR(out[1 * q_features], 30.0f, 0.01f); + + // Token 2: causal_pos=3, sw_start=1, group_begin=1, start=min(1,1)=1, ncausal=5. + // Sees {1,2,3,4}. Mean = (20+30+40+50)/4 = 35. + EXPECT_NEAR(out[2 * q_features], 35.0f, 0.01f); + + // Token 3: causal_pos=4, sw_start=2, group_begin=1, start=min(2,1)=1, ncausal=5. + // Sees {1,2,3,4}. Mean = (20+30+40+50)/4 = 35. + EXPECT_NEAR(out[3 * q_features], 35.0f, 0.01f); + + // Token 4: causal_pos=5, sw_start=3, group_begin=1, start=min(3,1)=1, ncausal=5. + // Sees {1,2,3,4}. Mean = (20+30+40+50)/4 = 35. + EXPECT_NEAR(out[4 * q_features], 35.0f, 0.01f); + + // Token 5: text, causal_pos=6, SW=2, start=4. Sees {4,5}. Mean = (50+60)/2 = 55. + EXPECT_NEAR(out[5 * q_features], 55.0f, 0.01f); +} diff --git a/src/core/tests/shared_buffer.cpp b/src/core/tests/shared_buffer.cpp index 0686263fd25c..89a26bd2b6ca 100644 --- a/src/core/tests/shared_buffer.cpp +++ b/src/core/tests/shared_buffer.cpp @@ -4,14 +4,18 @@ #include "openvino/runtime/shared_buffer.hpp" +#include + #include #include #include +#include #include "common_test_utils/common_utils.hpp" -#include "gtest/gtest.h" +#include "openvino/op/constant.hpp" #include "openvino/util/mmap_object.hpp" +namespace ov::test { using ov::SharedStreamBuffer; TEST(shared_stream_buffer, basic_read) { @@ -514,3 +518,88 @@ TEST_F(SharedBufferTest, specialization_overload_resolution) { } } } + +class MockMappedMemory : public ov::MappedMemory { +public: + explicit MockMappedMemory(size_t size) : m_data(size, '\0'), m_id(1) {} + + char* data() noexcept override { + return m_data.data(); + } + size_t size() const noexcept override { + return m_data.size(); + } + uint64_t get_id() const noexcept override { + return m_id; + } + + void hint_evict(size_t offset, size_t size) noexcept override { + hint_evict_mock(offset, size); + } + + MOCK_METHOD(void, hint_evict_mock, (size_t offset, size_t size)); + + MOCK_METHOD(void, hint_prefetch, (size_t offset, size_t size), (override)); + +private: + std::vector m_data; + uint64_t m_id; +}; + +TEST_F(SharedBufferTest, mmap_shared_buffer_calls_hint_evict_with_own_region) { + constexpr size_t mmap_size = 1024; + constexpr size_t buf_offset = 128; + constexpr size_t buf_size = 256; + + auto mock = std::make_shared(mmap_size); + auto buffer = std::make_shared>>(mock->data() + buf_offset, + buf_size, + mock); + + EXPECT_CALL(*mock, hint_evict_mock(buf_offset, buf_size)).Times(1); + buffer->hint_evict(); +} + +TEST_F(SharedBufferTest, mmap_shared_buffer_full_mapping) { + constexpr size_t mmap_size = 512; + + auto mock = std::make_shared(mmap_size); + auto buffer = std::make_shared>>(mock->data(), mmap_size, mock); + + EXPECT_CALL(*mock, hint_evict_mock(0u, mmap_size)).Times(1); + buffer->hint_evict(); +} + +TEST_F(SharedBufferTest, aligned_shared_buffer_propagates_to_mmap) { + constexpr size_t mmap_size = 2048; + constexpr size_t parent_offset = 64; + constexpr size_t child_offset = 32; // relative to parent data ptr + constexpr size_t child_size = 128; + + auto mock = std::make_shared(mmap_size); + + auto parent = std::make_shared>>(mock->data() + parent_offset, + mmap_size - parent_offset, + mock); + + auto child = std::make_shared>>( + parent->get_ptr() + child_offset, + child_size, + std::static_pointer_cast(parent)); + + // child lives at parent_offset + child_offset inside the mmap + EXPECT_CALL(*mock, hint_evict_mock(parent_offset + child_offset, child_size)).Times(1); + child->hint_evict(); +} + +TEST_F(SharedBufferTest, no_call_when_mmap_object_is_null) { + constexpr size_t buf_size = 64; + std::vector storage(buf_size); + + auto buffer = std::make_shared>>( + storage.data(), + buf_size, + std::shared_ptr{} /*null*/); + EXPECT_NO_THROW(buffer->hint_evict()); +} +} // namespace ov::test diff --git a/src/core/tests/type_prop/gated_delta_net.cpp b/src/core/tests/type_prop/gated_delta_net.cpp index 6ad8cfaafdac..042877696b42 100644 --- a/src/core/tests/type_prop/gated_delta_net.cpp +++ b/src/core/tests/type_prop/gated_delta_net.cpp @@ -114,7 +114,7 @@ TEST(type_prop, gated_delta_net_invalid_type) { testing::HasSubstr("Element type of `query` input should be in")); } -TEST(type_prop, gated_delta_net_head_num_mismatch_qkv) { +TEST(type_prop, gated_delta_net_head_num_mismatch_qk) { OV_EXPECT_THROW(std::ignore = make_gdn(element::f32, Shape{2, 5, 4, 8}, Shape{2, 5, 6, 8}, @@ -123,7 +123,7 @@ TEST(type_prop, gated_delta_net_head_num_mismatch_qkv) { Shape{2, 5, 4}, Shape{2, 5, 4}), NodeValidationFailure, - testing::HasSubstr("The number of heads in query key and value should be the same")); + testing::HasSubstr("The number of heads in query and key should be the same")); } TEST(type_prop, gated_delta_net_head_size_mismatch_qk) { @@ -147,7 +147,23 @@ TEST(type_prop, gated_delta_net_gate_beta_head_num_mismatch) { Shape{2, 5, 6}, Shape{2, 5, 4}), NodeValidationFailure, - testing::HasSubstr("The number of heads in gate, beta, and query should be the same")); + testing::HasSubstr("The number of heads in gate, beta, and value should be the same")); +} + +TEST(type_prop, gated_delta_net_gqa_v_num_heads_greater_than_num_heads) { + // GQA: query/key have 4 heads, value/gate/beta/state have 8 heads + const auto op = make_gdn(element::f32, + Shape{2, 5, 4, 8}, + Shape{2, 5, 4, 8}, + Shape{2, 5, 8, 16}, + Shape{2, 8, 8, 16}, + Shape{2, 5, 8}, + Shape{2, 5, 8}); + + EXPECT_EQ(op->get_output_size(), 2); + EXPECT_EQ(op->get_output_element_type(0), element::f32); + EXPECT_EQ(op->get_output_partial_shape(0), PartialShape(Shape{2, 5, 8, 16})); + EXPECT_EQ(op->get_output_partial_shape(1), PartialShape(Shape{2, 8, 8, 16})); } TEST(type_prop, gated_delta_net_state_shape_mismatch) { diff --git a/src/core/tests/type_prop/gather_matmul_compressed_test.cpp b/src/core/tests/type_prop/gather_matmul_compressed_test.cpp index 4a2e5934a613..7e8ca6c4a0b7 100644 --- a/src/core/tests/type_prop/gather_matmul_compressed_test.cpp +++ b/src/core/tests/type_prop/gather_matmul_compressed_test.cpp @@ -65,36 +65,6 @@ TEST_F(GatherMatmulCompressedTest, shape_grouped_4d) { EXPECT_EQ(op->get_output_partial_shape(0), (PartialShape{2, 64, 4096})); } -// ============================================================================ -// Negative tests — GatherMatmulCompressed validation failures -// ============================================================================ - -TEST_F(GatherMatmulCompressedTest, fail_scales_not_constant) { - auto a = make_param(element::f32, {1, 64, 2048}); - auto b = make_const(element::u8, {8, 4096, 2048}); - auto idx = make_param(element::i32, {64, 2}); - auto bias = make_empty_bias(); - auto scales = make_param(element::f32, {8, 4096, 1}); // Parameter, not Constant - auto zp = make_const(element::u8, {8, 4096, 1}); - - OV_EXPECT_THROW(std::ignore = make_op(a, b, idx, bias, scales, zp), - ov::NodeValidationFailure, - testing::HasSubstr("Input weight_scales must be a Constant")); -} - -TEST_F(GatherMatmulCompressedTest, fail_zp_not_constant) { - auto a = make_param(element::f32, {1, 64, 2048}); - auto b = make_const(element::u8, {8, 4096, 2048}); - auto idx = make_param(element::i32, {64, 2}); - auto bias = make_empty_bias(); - auto scales = make_const(element::f32, {8, 4096, 1}); - auto zp = make_param(element::u8, {8, 4096, 1}); // Parameter, not Constant - - OV_EXPECT_THROW(std::ignore = make_op(a, b, idx, bias, scales, zp), - ov::NodeValidationFailure, - testing::HasSubstr("Input weight_zero_points must be a Constant")); -} - // ============================================================================ // Clone tests // ============================================================================ diff --git a/src/core/tests/type_prop/interpolate.cpp b/src/core/tests/type_prop/interpolate.cpp index cb0706bc1954..8f5c50d0dbc9 100644 --- a/src/core/tests/type_prop/interpolate.cpp +++ b/src/core/tests/type_prop/interpolate.cpp @@ -26,8 +26,8 @@ TEST(type_prop, interpolate_v0_default_ctor) { op::v0::Interpolate::Attributes attrs; attrs.axes = AxisSet{2, 3}; - attrs.pads_begin = {0, 0, 0, 0}; - attrs.pads_end = {0, 0, 0, 0}; + attrs.pads_begin = std::vector{0, 0, 0, 0}; + attrs.pads_end = std::vector{0, 0, 0, 0}; auto interp = std::make_shared(); interp->set_arguments(OutputVector{image, target_shape}); @@ -45,8 +45,8 @@ TEST(type_prop, interpolate_v0_all_inputs_dynamic_rank) { op::v0::Interpolate::Attributes attrs; attrs.axes = AxisSet{2, 3}; - attrs.pads_begin = {0, 0, 0, 0}; - attrs.pads_end = {0, 0, 0, 0}; + attrs.pads_begin = std::vector{0, 0, 0, 0}; + attrs.pads_end = std::vector{0, 0, 0, 0}; auto interp = std::make_shared(image, target_shape, attrs); @@ -60,8 +60,8 @@ TEST(type_prop, interpolate_v0_all_inputs_static_rank) { op::v0::Interpolate::Attributes attrs; attrs.axes = AxisSet{2, 3}; - attrs.pads_begin = {0, 0, 0, 0}; - attrs.pads_end = {0, 0, 0, 0}; + attrs.pads_begin = std::vector{0, 0, 0, 0}; + attrs.pads_end = std::vector{0, 0, 0, 0}; auto interp = std::make_shared(image, target_shape, attrs); @@ -75,8 +75,8 @@ TEST(type_prop, interpolate_v0_target_shape_not_constant) { op::v0::Interpolate::Attributes attrs; attrs.axes = AxisSet{3, 1}; - attrs.pads_begin = {0, 0, 0, 0}; - attrs.pads_end = {0, 0, 0, 0}; + attrs.pads_begin = std::vector{0, 0, 0, 0}; + attrs.pads_end = std::vector{0, 0, 0, 0}; auto interp = std::make_shared(image, target_shape, attrs); @@ -96,8 +96,8 @@ TEST(type_prop, interpolate_v0_target_shape_as_shape_of) { op::v0::Interpolate::Attributes attrs; attrs.axes = AxisSet{3, 1}; - attrs.pads_begin = {0, 0, 1, 0}; - attrs.pads_end = {0, 2, 0, 0}; + attrs.pads_begin = std::vector{0, 0, 1, 0}; + attrs.pads_end = std::vector{0, 2, 0, 0}; auto interp = std::make_shared(image, target_shape, attrs); EXPECT_EQ(interp->get_element_type(), element::f64); @@ -145,8 +145,8 @@ TEST(type_prop, interpolate_v4) { attrs.coordinate_transformation_mode = CoordinateTransformMode::HALF_PIXEL; attrs.nearest_mode = Nearest_mode::ROUND_PREFER_FLOOR; attrs.antialias = false; - attrs.pads_begin = {0, 0, 0, 0}; - attrs.pads_end = {0, 0, 0, 0}; + attrs.pads_begin = std::vector{0, 0, 0, 0}; + attrs.pads_end = std::vector{0, 0, 0, 0}; attrs.cube_coeff = -0.75; auto interp = std::make_shared(image, target_shape, scales, axes, attrs); @@ -170,8 +170,8 @@ TEST(type_prop, interpolate_v4_non_constant_axes_scales) { attrs.coordinate_transformation_mode = CoordinateTransformMode::HALF_PIXEL; attrs.nearest_mode = Nearest_mode::ROUND_PREFER_FLOOR; attrs.antialias = false; - attrs.pads_begin = {0, 0, 0, 0}; - attrs.pads_end = {0, 0, 0, 0}; + attrs.pads_begin = std::vector{0, 0, 0, 0}; + attrs.pads_end = std::vector{0, 0, 0, 0}; attrs.cube_coeff = -0.75; auto interp = std::make_shared(image, target_shape, scales, axes, attrs); @@ -196,8 +196,8 @@ TEST(type_prop, interpolate_v4_non_constant_axes_sizes) { attrs.coordinate_transformation_mode = CoordinateTransformMode::HALF_PIXEL; attrs.nearest_mode = Nearest_mode::ROUND_PREFER_FLOOR; attrs.antialias = false; - attrs.pads_begin = {0, 0, 0, 0}; - attrs.pads_end = {0, 0, 0, 0}; + attrs.pads_begin = std::vector{0, 0, 0, 0}; + attrs.pads_end = std::vector{0, 0, 0, 0}; attrs.cube_coeff = -0.75; auto interp = std::make_shared(image, target_shape, scales, axes, attrs); @@ -218,8 +218,8 @@ TEST(type_prop, interpolate_v4_img_dynamic_rank) { attrs.coordinate_transformation_mode = CoordinateTransformMode::HALF_PIXEL; attrs.nearest_mode = Nearest_mode::ROUND_PREFER_FLOOR; attrs.antialias = false; - attrs.pads_begin = {0, 0, 0, 0}; - attrs.pads_end = {0, 0, 0, 0}; + attrs.pads_begin = std::vector{0, 0, 0, 0}; + attrs.pads_end = std::vector{0, 0, 0, 0}; attrs.cube_coeff = -0.75; auto interp = std::make_shared(image, target_shape, scales, axes, attrs); @@ -242,8 +242,8 @@ TEST(type_prop, interpolate_v4_partial_static_rank) { attrs.coordinate_transformation_mode = CoordinateTransformMode::HALF_PIXEL; attrs.nearest_mode = Nearest_mode::ROUND_PREFER_FLOOR; attrs.antialias = false; - attrs.pads_begin = {0, 1, 0, 0}; - attrs.pads_end = {0, 1, 0, 0}; + attrs.pads_begin = std::vector{0, 1, 0, 0}; + attrs.pads_end = std::vector{0, 1, 0, 0}; attrs.cube_coeff = -0.75; auto interp = std::make_shared(image, target_shape, scales, axes, attrs); @@ -268,8 +268,8 @@ TEST(type_prop, interpolate_v4_img_intervals_use_scales) { attrs.coordinate_transformation_mode = CoordinateTransformMode::HALF_PIXEL; attrs.nearest_mode = Nearest_mode::ROUND_PREFER_FLOOR; attrs.antialias = false; - attrs.pads_begin = {0, 0, 0, 1}; - attrs.pads_end = {1, 1, 0, 1}; + attrs.pads_begin = std::vector{0, 0, 0, 1}; + attrs.pads_end = std::vector{1, 1, 0, 1}; attrs.cube_coeff = -0.75; auto interp = std::make_shared(image, target_shape, scales, axes, attrs); @@ -296,8 +296,8 @@ TEST(type_prop, interpolate_v4_use_sizes_as_shape_of) { attrs.coordinate_transformation_mode = CoordinateTransformMode::HALF_PIXEL; attrs.nearest_mode = Nearest_mode::ROUND_PREFER_FLOOR; attrs.antialias = false; - attrs.pads_begin = {0, 0, 0, 0}; - attrs.pads_end = {0, 0, 0, 0}; + attrs.pads_begin = std::vector{0, 0, 0, 0}; + attrs.pads_end = std::vector{0, 0, 0, 0}; attrs.cube_coeff = -0.75; auto interp = std::make_shared(image, target_shape, scales, axes, attrs); @@ -322,8 +322,8 @@ TEST(type_prop, interpolate_v4_use_scales_interval_shapes) { attrs.coordinate_transformation_mode = CoordinateTransformMode::HALF_PIXEL; attrs.nearest_mode = Nearest_mode::ROUND_PREFER_FLOOR; attrs.antialias = false; - attrs.pads_begin = {0, 0, 0, 0, 0}; - attrs.pads_end = {0, 0, 0, 0, 0}; + attrs.pads_begin = std::vector{0, 0, 0, 0, 0}; + attrs.pads_end = std::vector{0, 0, 0, 0, 0}; attrs.cube_coeff = -0.75; auto interp = std::make_shared(image, target_shape, scales, axes, attrs); @@ -341,8 +341,8 @@ TEST(type_prop, interpolate_v4_target_shapes_gt_axes_number) { ov::op::util::InterpolateBase::InterpolateAttrs attrs; attrs.shape_calculation_mode = ov::op::util::InterpolateBase::ShapeCalcMode::SIZES; - attrs.pads_begin = {0, 0, 0, 0}; - attrs.pads_end = {0, 0, 0, 0}; + attrs.pads_begin = std::vector{0, 0, 0, 0}; + attrs.pads_end = std::vector{0, 0, 0, 0}; auto interp = std::make_shared(image, target_shape, scales, axes, attrs); EXPECT_EQ(interp->get_element_type(), element::f32); @@ -357,8 +357,8 @@ TEST(type_prop, interpolate_v4_scales_gt_axes_number) { ov::op::util::InterpolateBase::InterpolateAttrs attrs; attrs.shape_calculation_mode = ov::op::util::InterpolateBase::ShapeCalcMode::SCALES; - attrs.pads_begin = {0, 0, 0, 0}; - attrs.pads_end = {0, 0, 0, 0}; + attrs.pads_begin = std::vector{0, 0, 0, 0}; + attrs.pads_end = std::vector{0, 0, 0, 0}; auto interp = std::make_shared(image, target_shape, scales, axes, attrs); EXPECT_EQ(interp->get_element_type(), element::f32); @@ -374,8 +374,8 @@ TEST(type_prop, interpolate_v4_incorrect_mode) { ov::op::util::InterpolateBase::InterpolateAttrs attrs; attrs.shape_calculation_mode = ov::op::util::InterpolateBase::ShapeCalcMode::SCALES; attrs.mode = ov::op::util::InterpolateBase::InterpolateMode::BICUBIC_PILLOW; - attrs.pads_begin = {0, 0, 0, 0}; - attrs.pads_end = {0, 0, 0, 0}; + attrs.pads_begin = std::vector{0, 0, 0, 0}; + attrs.pads_end = std::vector{0, 0, 0, 0}; OV_EXPECT_THROW(auto interp = std::make_shared(image, target_shape, scales, axes, attrs), ov::NodeValidationFailure, @@ -395,8 +395,8 @@ TEST(type_prop, interpolate_v4_target_shape_not_1d) { ov::op::util::InterpolateBase::InterpolateAttrs attrs; attrs.shape_calculation_mode = ov::op::util::InterpolateBase::ShapeCalcMode::SIZES; attrs.mode = ov::op::util::InterpolateBase::InterpolateMode::NEAREST; - attrs.pads_begin = {0, 0, 0, 0}; - attrs.pads_end = {0, 0, 0, 0}; + attrs.pads_begin = std::vector{0, 0, 0, 0}; + attrs.pads_end = std::vector{0, 0, 0, 0}; OV_EXPECT_THROW(std::ignore = std::make_shared( image, @@ -426,8 +426,8 @@ TEST(type_prop, interpolate_v4_scales_not_1d) { ov::op::util::InterpolateBase::InterpolateAttrs attrs; attrs.shape_calculation_mode = ov::op::util::InterpolateBase::ShapeCalcMode::SCALES; attrs.mode = ov::op::util::InterpolateBase::InterpolateMode::NEAREST; - attrs.pads_begin = {0, 0, 0, 0}; - attrs.pads_end = {0, 0, 0, 0}; + attrs.pads_begin = std::vector{0, 0, 0, 0}; + attrs.pads_end = std::vector{0, 0, 0, 0}; OV_EXPECT_THROW(std::ignore = std::make_shared( image, @@ -457,8 +457,8 @@ TEST(type_prop, interpolate_v4_axes_not_1d) { ov::op::util::InterpolateBase::InterpolateAttrs attrs; attrs.shape_calculation_mode = ov::op::util::InterpolateBase::ShapeCalcMode::SCALES; attrs.mode = ov::op::util::InterpolateBase::InterpolateMode::NEAREST; - attrs.pads_begin = {0, 0, 0, 0}; - attrs.pads_end = {0, 0, 0, 0}; + attrs.pads_begin = std::vector{0, 0, 0, 0}; + attrs.pads_end = std::vector{0, 0, 0, 0}; OV_EXPECT_THROW(std::ignore = std::make_shared( image, @@ -639,6 +639,22 @@ TEST(type_prop, interpolate_v11_non_constant_axes_scales) { EXPECT_THAT(get_shape_symbols(interp->get_output_partial_shape(0)), Each(nullptr)); } +TEST(type_prop, interpolate_v11_f64) { + const auto image = std::make_shared(element::f64, Shape{1, 3, 30, 60}); + const auto scales = ov::op::v0::Constant::create(element::f32, Shape{2}, {0.5f, 0.5f}); + const auto axes = ov::op::v0::Constant::create(element::i64, Shape{2}, {2, 3}); + + ov::op::util::InterpolateBase::InterpolateAttrs attrs; + attrs.mode = ov::op::util::InterpolateBase::InterpolateMode::NEAREST; + attrs.shape_calculation_mode = ov::op::util::InterpolateBase::ShapeCalcMode::SCALES; + attrs.pads_begin = {0, 0, 0, 0}; + attrs.pads_end = {0, 0, 0, 0}; + auto interp = std::make_shared(image, scales, axes, attrs); + + EXPECT_EQ(interp->get_element_type(), element::f64); + EXPECT_EQ(interp->get_shape(), (Shape{1, 3, 15, 30})); +} + TEST(type_prop, interpolate_v11_scales_incorrect_et) { const auto image = std::make_shared(element::f32, Shape{1, 3, 30, 60}); const auto scales = ov::op::v0::Constant::create(element::i64, Shape{2}, {2, 2}); diff --git a/src/core/tests/type_prop/pa_kv_reorder.cpp b/src/core/tests/type_prop/pa_kv_reorder.cpp new file mode 100644 index 000000000000..d5018342f454 --- /dev/null +++ b/src/core/tests/type_prop/pa_kv_reorder.cpp @@ -0,0 +1,191 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "openvino/op/pa_kv_reorder.hpp" + +#include + +#include "common_test_utils/test_assertions.hpp" +#include "common_test_utils/type_prop.hpp" +#include "openvino/op/parameter.hpp" + +namespace ov::test { + +class TypePropPaKVReorderTest : public TypePropOpTest {}; + +TEST_F(TypePropPaKVReorderTest, static_shapes) { + const auto key_cache = std::make_shared(element::f16, PartialShape{100, 8, 64, 16}); + const auto value_cache = std::make_shared(element::f16, PartialShape{100, 8, 16, 64}); + const auto block_indices = std::make_shared(element::i32, PartialShape{50}); + const auto block_indices_begins = std::make_shared(element::i32, PartialShape{4}); + const auto block_update_indices = std::make_shared(element::i32, PartialShape{50}); + const auto block_update_indices_begins = std::make_shared(element::i32, PartialShape{4}); + + const auto op = make_op(key_cache, + value_cache, + block_indices, + block_indices_begins, + block_update_indices, + block_update_indices_begins); + + EXPECT_EQ(op->get_output_element_type(0), element::u8); + EXPECT_EQ(op->get_output_partial_shape(0), (PartialShape{1})); + EXPECT_EQ(op->get_output_size(), 1); +} + +TEST_F(TypePropPaKVReorderTest, dynamic_batch) { + const auto key_cache = + std::make_shared(element::f32, PartialShape{Dimension::dynamic(), 8, 64, 16}); + const auto value_cache = + std::make_shared(element::f32, PartialShape{Dimension::dynamic(), 8, 16, 64}); + const auto block_indices = std::make_shared(element::i32, PartialShape{Dimension::dynamic()}); + const auto block_indices_begins = + std::make_shared(element::i32, PartialShape{Dimension::dynamic()}); + const auto block_update_indices = + std::make_shared(element::i32, PartialShape{Dimension::dynamic()}); + const auto block_update_indices_begins = + std::make_shared(element::i32, PartialShape{Dimension::dynamic()}); + + const auto op = make_op(key_cache, + value_cache, + block_indices, + block_indices_begins, + block_update_indices, + block_update_indices_begins); + + EXPECT_EQ(op->get_output_element_type(0), element::u8); + EXPECT_EQ(op->get_output_partial_shape(0), (PartialShape{1})); +} + +TEST_F(TypePropPaKVReorderTest, fully_dynamic_shapes) { + const auto key_cache = std::make_shared(element::f16, PartialShape::dynamic(4)); + const auto value_cache = std::make_shared(element::f16, PartialShape::dynamic(4)); + const auto block_indices = std::make_shared(element::i32, PartialShape::dynamic(1)); + const auto block_indices_begins = std::make_shared(element::i32, PartialShape::dynamic(1)); + const auto block_update_indices = std::make_shared(element::i32, PartialShape::dynamic(1)); + const auto block_update_indices_begins = + std::make_shared(element::i32, PartialShape::dynamic(1)); + + const auto op = make_op(key_cache, + value_cache, + block_indices, + block_indices_begins, + block_update_indices, + block_update_indices_begins); + + EXPECT_EQ(op->get_output_element_type(0), element::u8); + EXPECT_EQ(op->get_output_partial_shape(0), (PartialShape{1})); +} + +TEST_F(TypePropPaKVReorderTest, dynamic_rank) { + const auto key_cache = std::make_shared(element::f16, PartialShape::dynamic()); + const auto value_cache = std::make_shared(element::f16, PartialShape::dynamic()); + const auto block_indices = std::make_shared(element::i32, PartialShape::dynamic()); + const auto block_indices_begins = std::make_shared(element::i32, PartialShape::dynamic()); + const auto block_update_indices = std::make_shared(element::i32, PartialShape::dynamic()); + const auto block_update_indices_begins = std::make_shared(element::i32, PartialShape::dynamic()); + + const auto op = make_op(key_cache, + value_cache, + block_indices, + block_indices_begins, + block_update_indices, + block_update_indices_begins); + + EXPECT_EQ(op->get_output_element_type(0), element::u8); + EXPECT_EQ(op->get_output_partial_shape(0), (PartialShape{1})); +} + +TEST_F(TypePropPaKVReorderTest, invalid_key_cache_rank) { + const auto key_cache = std::make_shared(element::f16, PartialShape{100, 8, 64}); + const auto value_cache = std::make_shared(element::f16, PartialShape{100, 8, 16, 64}); + const auto block_indices = std::make_shared(element::i32, PartialShape{50}); + const auto block_indices_begins = std::make_shared(element::i32, PartialShape{4}); + const auto block_update_indices = std::make_shared(element::i32, PartialShape{50}); + const auto block_update_indices_begins = std::make_shared(element::i32, PartialShape{4}); + + OV_EXPECT_THROW(std::ignore = make_op(key_cache, + value_cache, + block_indices, + block_indices_begins, + block_update_indices, + block_update_indices_begins), + NodeValidationFailure, + testing::HasSubstr("input_shapes[0].rank().compatible(4)")); +} + +TEST_F(TypePropPaKVReorderTest, invalid_value_cache_rank) { + const auto key_cache = std::make_shared(element::f16, PartialShape{100, 8, 64, 16}); + const auto value_cache = std::make_shared(element::f16, PartialShape{100, 8, 64}); + const auto block_indices = std::make_shared(element::i32, PartialShape{50}); + const auto block_indices_begins = std::make_shared(element::i32, PartialShape{4}); + const auto block_update_indices = std::make_shared(element::i32, PartialShape{50}); + const auto block_update_indices_begins = std::make_shared(element::i32, PartialShape{4}); + + OV_EXPECT_THROW(std::ignore = make_op(key_cache, + value_cache, + block_indices, + block_indices_begins, + block_update_indices, + block_update_indices_begins), + NodeValidationFailure, + testing::HasSubstr("input_shapes[1].rank().compatible(4)")); +} + +TEST_F(TypePropPaKVReorderTest, invalid_indices_rank) { + const auto key_cache = std::make_shared(element::f16, PartialShape{100, 8, 64, 16}); + const auto value_cache = std::make_shared(element::f16, PartialShape{100, 8, 16, 64}); + const auto block_indices = std::make_shared(element::i32, PartialShape{5, 10}); + const auto block_indices_begins = std::make_shared(element::i32, PartialShape{4}); + const auto block_update_indices = std::make_shared(element::i32, PartialShape{50}); + const auto block_update_indices_begins = std::make_shared(element::i32, PartialShape{4}); + + OV_EXPECT_THROW(std::ignore = make_op(key_cache, + value_cache, + block_indices, + block_indices_begins, + block_update_indices, + block_update_indices_begins), + NodeValidationFailure, + testing::HasSubstr("input_shapes[i].rank().compatible(1)")); +} + +TEST_F(TypePropPaKVReorderTest, invalid_indices_type) { + const auto key_cache = std::make_shared(element::f16, PartialShape{100, 8, 64, 16}); + const auto value_cache = std::make_shared(element::f16, PartialShape{100, 8, 16, 64}); + const auto block_indices = std::make_shared(element::f32, PartialShape{50}); + const auto block_indices_begins = std::make_shared(element::i32, PartialShape{4}); + const auto block_update_indices = std::make_shared(element::i32, PartialShape{50}); + const auto block_update_indices_begins = std::make_shared(element::i32, PartialShape{4}); + + OV_EXPECT_THROW(std::ignore = make_op(key_cache, + value_cache, + block_indices, + block_indices_begins, + block_update_indices, + block_update_indices_begins), + NodeValidationFailure, + testing::HasSubstr("(indices) must be i32")); +} + +TEST_F(TypePropPaKVReorderTest, i8_cache) { + const auto key_cache = std::make_shared(element::i8, PartialShape{100, 8, 64, 16}); + const auto value_cache = std::make_shared(element::i8, PartialShape{100, 8, 16, 64}); + const auto block_indices = std::make_shared(element::i32, PartialShape{50}); + const auto block_indices_begins = std::make_shared(element::i32, PartialShape{4}); + const auto block_update_indices = std::make_shared(element::i32, PartialShape{50}); + const auto block_update_indices_begins = std::make_shared(element::i32, PartialShape{4}); + + const auto op = make_op(key_cache, + value_cache, + block_indices, + block_indices_begins, + block_update_indices, + block_update_indices_begins); + + EXPECT_EQ(op->get_output_element_type(0), element::u8); + EXPECT_EQ(op->get_output_partial_shape(0), (PartialShape{1})); +} + +} // namespace ov::test diff --git a/src/core/tests/type_prop/pad.cpp b/src/core/tests/type_prop/pad.cpp index d42cc30c5c67..c97c8a61715a 100644 --- a/src/core/tests/type_prop/pad.cpp +++ b/src/core/tests/type_prop/pad.cpp @@ -324,3 +324,75 @@ REGISTER_TYPED_TEST_SUITE_P(PadTest, using PadOpTypes = Types; INSTANTIATE_TYPED_TEST_SUITE_P(type_prop, PadTest, PadOpTypes); + +template +class PadStringTest : public TypePropOpTest {}; +TYPED_TEST_SUITE_P(PadStringTest); + +TYPED_TEST_P(PadStringTest, pad_string_output_type_is_string) { + auto arg = make_shared(element::string, Shape{3, 4}); + auto pads_begin = make_shared(element::i64, Shape{2}, std::vector{1, 2}); + auto pads_end = make_shared(element::i64, Shape{2}, std::vector{0, 1}); + auto pad_value = make_shared(element::string, Shape{}, std::vector{""}); + + auto pad = this->make_op(arg, pads_begin, pads_end, pad_value, op::PadMode::CONSTANT); + + EXPECT_EQ(pad->get_output_element_type(0), element::string); + EXPECT_EQ(pad->get_output_partial_shape(0), PartialShape({4, 7})); +} + +TYPED_TEST_P(PadStringTest, pad_string_negative_pads_crop) { + auto arg = make_shared(element::string, Shape{4, 6}); + auto pads_begin = make_shared(element::i64, Shape{2}, std::vector{-1, 0}); + auto pads_end = make_shared(element::i64, Shape{2}, std::vector{0, -2}); + auto pad_value = make_shared(element::string, Shape{}, std::vector{""}); + + auto pad = this->make_op(arg, pads_begin, pads_end, pad_value, op::PadMode::CONSTANT); + + EXPECT_EQ(pad->get_output_element_type(0), element::string); + EXPECT_EQ(pad->get_output_partial_shape(0), PartialShape({3, 4})); +} + +TYPED_TEST_P(PadStringTest, pad_string_dynamic_shape) { + auto arg = make_shared(element::string, PartialShape{-1, -1}); + auto pads_begin = make_shared(element::i64, Shape{2}, std::vector{1, 0}); + auto pads_end = make_shared(element::i64, Shape{2}, std::vector{2, 1}); + auto pad_value = make_shared(element::string, Shape{}, std::vector{"x"}); + + auto pad = this->make_op(arg, pads_begin, pads_end, pad_value, op::PadMode::CONSTANT); + + EXPECT_EQ(pad->get_output_element_type(0), element::string); + EXPECT_EQ(pad->get_output_partial_shape(0), (PartialShape{{3, -1}, {1, -1}})); +} + +TYPED_TEST_P(PadStringTest, pad_string_multi_char_pad_value) { + auto arg = make_shared(element::string, Shape{2, 3}); + auto pads_begin = make_shared(element::i64, Shape{2}, std::vector{1, 2}); + auto pads_end = make_shared(element::i64, Shape{2}, std::vector{1, 0}); + auto pad_value = make_shared(element::string, Shape{}, std::vector{"hello world"}); + + auto pad = this->make_op(arg, pads_begin, pads_end, pad_value, op::PadMode::CONSTANT); + + EXPECT_EQ(pad->get_output_element_type(0), element::string); + EXPECT_EQ(pad->get_output_partial_shape(0), PartialShape({4, 5})); +} + +TYPED_TEST_P(PadStringTest, pad_string_pad_value_type_mismatch) { + auto arg = make_shared(element::string, Shape{3}); + auto pads_begin = make_shared(element::i64, Shape{1}); + auto pads_end = make_shared(element::i64, Shape{1}); + auto pad_value = make_shared(element::f32, Shape{}); + + OV_EXPECT_THROW(ignore = this->make_op(arg, pads_begin, pads_end, pad_value, op::PadMode::CONSTANT), + NodeValidationFailure, + HasSubstr("Argument element types do not match")); +} + +REGISTER_TYPED_TEST_SUITE_P(PadStringTest, + pad_string_output_type_is_string, + pad_string_negative_pads_crop, + pad_string_dynamic_shape, + pad_string_multi_char_pad_value, + pad_string_pad_value_type_mismatch); + +INSTANTIATE_TYPED_TEST_SUITE_P(type_prop, PadStringTest, PadOpTypes); diff --git a/src/core/tests/type_prop/paged_attention.cpp b/src/core/tests/type_prop/paged_attention.cpp index 766b21a4a325..90509d63a25b 100644 --- a/src/core/tests/type_prop/paged_attention.cpp +++ b/src/core/tests/type_prop/paged_attention.cpp @@ -6,12 +6,73 @@ #include +#include +#include +#include +#include + +#include "common_test_utils/test_assertions.hpp" +#include "common_test_utils/type_prop.hpp" #include "openvino/core/type/element_type.hpp" +#include "openvino/op/constant.hpp" #include "openvino/op/parameter.hpp" namespace ov { namespace testing { +using ::testing::HasSubstr; + +namespace { +ov::OutputVector make_valid_pa_args(const element::Type& t = element::f32) { + using ov::op::v0::Parameter; + + // rotation_trig_lut and xattention_threshold only accept f16/f32. + // Pin to f32 for bf16 tests. + const auto trig_t = (t == element::bf16) ? element::f32 : t; + + auto query = std::make_shared(t, PartialShape{3, 4}); + auto key = std::make_shared(t, PartialShape{3, 4}); + auto value = std::make_shared(t, PartialShape{3, 4}); + + auto key_cache = std::make_shared(t, PartialShape{4, 3, 32, 4}); + auto value_cache = std::make_shared(t, PartialShape{4, 3, 32, 4}); + + auto past_lens = std::make_shared(element::i32, PartialShape{3}); + auto subseq = std::make_shared(element::i32, PartialShape{4}); + auto block_idx = std::make_shared(element::i32, PartialShape{6}); + auto block_beg = std::make_shared(element::i32, PartialShape{4}); + + auto scale = std::make_shared(t, PartialShape{}); + auto slide = std::make_shared(element::i32, PartialShape{}); + auto alibi = std::make_shared(t, PartialShape{3}); + auto maxctx = std::make_shared(element::i32, PartialShape{}); + auto scorew = std::make_shared(element::i32, PartialShape{}); + + auto rotated = std::make_shared(element::i32, PartialShape{6}); + auto deltas = std::make_shared(element::i32, PartialShape{6}); + auto trig = std::make_shared(trig_t, PartialShape{6}); + + auto xthr = std::make_shared(trig_t, PartialShape{1}); + auto xbs = std::make_shared(element::i32, PartialShape{}); + auto xst = std::make_shared(element::i32, PartialShape{}); + + auto sinks = std::make_shared(t, PartialShape{1, 3, 1, 1}); + + auto arkv_start = std::make_shared(element::i32, PartialShape{}); + auto arkv_evict = std::make_shared(element::i32, PartialShape{3}); + auto arkv_idx = std::make_shared(element::i32, PartialShape{3}); + auto arkv_beg = std::make_shared(element::i32, PartialShape{3}); + + auto token_type_ids = std::make_shared(element::i32, PartialShape{0}); + + auto qq_bias = std::make_shared(element::u8, PartialShape{4}); + auto qq_bias_begins = std::make_shared(element::i32, PartialShape{2}); + return {query, key, value, key_cache, value_cache, past_lens, subseq, block_idx, block_beg, + scale, slide, alibi, maxctx, scorew, rotated, deltas, trig, xthr, + xbs, xst, sinks, arkv_start, arkv_evict, arkv_idx, arkv_beg, token_type_ids, qq_bias, + qq_bias_begins}; +} +} // namespace TEST(type_prop, paged_attention_static_eviction_per_block) { const auto query = std::make_shared(element::f32, PartialShape{3, 4}); const auto key = std::make_shared(element::f32, PartialShape{3, 4}); @@ -225,61 +286,191 @@ TEST(type_prop, paged_attention_dynamic_ranks_and_types) { } TEST(type_prop, paged_attention_invalid_rank_query) { - auto query = std::make_shared(element::f32, PartialShape{3}); - auto key = std::make_shared(element::f32, PartialShape{3, 4}); - auto value = std::make_shared(element::f32, PartialShape{3, 4}); - auto dummy = std::make_shared(element::f32, PartialShape{3, 4}); - auto scalar = std::make_shared(element::i32, PartialShape{}); - ov::OutputVector args = - {query, key, value, dummy, dummy, scalar, scalar, scalar, scalar, dummy, scalar, dummy, scalar}; - - EXPECT_THROW(std::ignore = std::make_shared(args), ov::NodeValidationFailure); + auto args = make_valid_pa_args(); + args[0] = std::make_shared(element::f32, PartialShape{3}); // query must be rank-2 + OV_EXPECT_THROW(std::ignore = std::make_shared(args), + ov::NodeValidationFailure, + HasSubstr("Rank of `query`")); } TEST(type_prop, paged_attention_invalid_type_scale) { - auto scale = std::make_shared(element::i32, PartialShape{}); - auto dummy2D = std::make_shared(element::f32, PartialShape{3, 4}); - auto dummy1D = std::make_shared(element::i32, PartialShape{3}); - auto dummyScalar = std::make_shared(element::i32, PartialShape{}); - - ov::OutputVector args = {dummy2D, - dummy2D, - dummy2D, - dummy2D, - dummy2D, - dummy1D, - dummy1D, - dummy1D, - dummy1D, - scale, - dummyScalar, - dummy2D, - dummyScalar}; - - EXPECT_THROW(std::ignore = std::make_shared(args), ov::NodeValidationFailure); + auto args = make_valid_pa_args(); + args[9] = std::make_shared(element::i32, PartialShape{}); // scale must be real type + OV_EXPECT_THROW(std::ignore = std::make_shared(args), + ov::NodeValidationFailure, + HasSubstr("Element type of `scale`")); } TEST(type_prop, paged_attention_invalid_rank_key_cache) { - auto key_cache = std::make_shared(element::f32, PartialShape{1}); - auto dummy = std::make_shared(element::f32, PartialShape{3, 4}); - auto dummy1D = std::make_shared(element::i32, PartialShape{3}); - auto dummyScalar = std::make_shared(element::i32, PartialShape{}); - - ov::OutputVector args = {dummy, - dummy, - dummy, - key_cache, - dummy, - dummy1D, - dummy1D, - dummy1D, - dummy1D, - dummyScalar, - dummyScalar, - dummy, - dummyScalar}; + auto args = make_valid_pa_args(); + args[3] = std::make_shared(element::f32, + PartialShape{3}); // key_cache allows rank 2..5; rank 1 is invalid + OV_EXPECT_THROW(std::ignore = std::make_shared(args), + ov::NodeValidationFailure, + HasSubstr("Rank of `key_cache`")); +} - EXPECT_THROW(std::ignore = std::make_shared(args), ov::NodeValidationFailure); +TEST(type_prop, paged_attention_invalid_input_count) { + auto args = make_valid_pa_args(); + args.pop_back(); // 27 instead of 28 + OV_EXPECT_THROW(std::ignore = std::make_shared(args), + ov::NodeValidationFailure, + HasSubstr("28 inputs")); +} + +TEST(type_prop, paged_attention_invalid_past_lens_rank) { + auto args = make_valid_pa_args(); + args[5] = std::make_shared(element::i32, PartialShape{3, 1}); // past_lens must be rank-1 + OV_EXPECT_THROW(std::ignore = std::make_shared(args), + ov::NodeValidationFailure, + HasSubstr("Rank of `past_lens`")); +} + +TEST(type_prop, paged_attention_invalid_type_past_lens) { + auto args = make_valid_pa_args(); + args[5] = std::make_shared(element::f32, PartialShape{3}); // past_lens must be i32 + OV_EXPECT_THROW(std::ignore = std::make_shared(args), + ov::NodeValidationFailure, + HasSubstr("Element type of `past_lens`")); +} + +// ---------- dtype parametrisation ---------- +class PagedAttentionTypePropDtype : public ::testing::TestWithParam {}; + +TEST_P(PagedAttentionTypePropDtype, OutputTypeMatchesInput) { + const auto t = GetParam(); + auto args = make_valid_pa_args(t); + const auto op = std::make_shared(args); + EXPECT_EQ(op->get_output_element_type(0), t); + EXPECT_EQ(op->get_output_element_type(1), t); + EXPECT_EQ(op->get_output_element_type(2), t); +} + +INSTANTIATE_TEST_SUITE_P(type_prop, + PagedAttentionTypePropDtype, + ::testing::Values(element::f32, element::bf16, element::f16), + [](const ::testing::TestParamInfo& info) { + return info.param.get_type_name(); + }); + +// ---------- GQA scenarios ---------- +TEST(type_prop, paged_attention_gqa_output_shape) { + // GQA: q_heads=6, kv_heads=2, head_size=4 => q=[B,24], k=[B,8], v=[B,8] + // Output features = q * v / k = 24 * 8 / 8 = 24 (= q_heads * v_head_size) + auto args = make_valid_pa_args(); + const auto B = 3; + args[0] = std::make_shared(element::f32, PartialShape{B, 24}); // Q: 6 heads * 4 + args[1] = std::make_shared(element::f32, PartialShape{B, 8}); // K: 2 heads * 4 + args[2] = std::make_shared(element::f32, PartialShape{B, 8}); // V: 2 heads * 4 + args[3] = std::make_shared(element::f32, PartialShape{4, 2, 32, 4}); // key_cache + args[4] = std::make_shared(element::f32, PartialShape{4, 2, 32, 4}); // value_cache + const auto op = std::make_shared(args); + EXPECT_EQ(op->get_output_partial_shape(0), (PartialShape{B, 24})); +} + +TEST(type_prop, paged_attention_gqa_mismatched_divisibility) { + // q * v / k must be integer; here q=5, k=4, v=4 => 5*4/4=5 (ok but unusual) + auto args = make_valid_pa_args(); + args[0] = std::make_shared(element::f32, PartialShape{3, 5}); + args[1] = std::make_shared(element::f32, PartialShape{3, 4}); + args[2] = std::make_shared(element::f32, PartialShape{3, 4}); + const auto op = std::make_shared(args); + EXPECT_EQ(op->get_output_partial_shape(0), (PartialShape{3, 5})); +} + +TEST(type_prop, paged_attention_output_feature_not_divisible) { + // q=5, k=3, v=4 => 5*4 = 20, 20 % 3 != 0 => should throw + auto args = make_valid_pa_args(); + args[0] = std::make_shared(element::f32, PartialShape{3, 5}); + args[1] = std::make_shared(element::f32, PartialShape{3, 3}); + args[2] = std::make_shared(element::f32, PartialShape{3, 4}); + OV_EXPECT_THROW(std::ignore = std::make_shared(args), + ov::NodeValidationFailure, + HasSubstr("divisible")); +} + +// ---------- output 1 & 2 validation ---------- +TEST(type_prop, paged_attention_outputs_1_and_2_shapes) { + auto args = make_valid_pa_args(); + const auto op = std::make_shared(args); + // Output 0: [B_token, out_features] + EXPECT_EQ(op->get_output_partial_shape(0), (PartialShape{3, 4})); + // Output 1 (scores): 1-D, dynamic without constant past_lens + EXPECT_EQ(op->get_output_partial_shape(1).rank().get_length(), 1); + // Output 2 (diversity): 1-D + EXPECT_EQ(op->get_output_partial_shape(2).rank().get_length(), 1); + // All outputs share query element type + EXPECT_EQ(op->get_output_element_type(0), element::f32); + EXPECT_EQ(op->get_output_element_type(1), element::f32); + EXPECT_EQ(op->get_output_element_type(2), element::f32); +} + +TEST(type_prop, paged_attention_adaptive_rkv_quantized_bychannel_diversity_shape) { + using namespace ov::op; + + const auto query = std::make_shared(element::f32, PartialShape{1, 64}); + const auto key = std::make_shared(element::f32, PartialShape{1, 64}); + const auto value = std::make_shared(element::f32, PartialShape{1, 64}); + const auto key_cache = std::make_shared(element::u4, PartialShape{4, 2, 48, 32}); + const auto value_cache = std::make_shared(element::u4, PartialShape{4, 2, 48, 32}); + const auto past_lens = std::make_shared(element::i32, Shape{1}, std::vector{0}); + const auto subsequence_begins = std::make_shared(element::i32, PartialShape{2}); + const auto block_indices = std::make_shared(element::i32, PartialShape{1}); + const auto block_indices_begins = std::make_shared(element::i32, PartialShape{2}); + const auto scale = std::make_shared(element::f32, PartialShape{}); + const auto sliding_window = std::make_shared(element::i32, PartialShape{}); + const auto alibi_slopes = std::make_shared(element::f32, PartialShape{0}); + const auto max_context_len = std::make_shared(element::i32, PartialShape{}); + const auto score_aggregation_window = std::make_shared(element::i32, PartialShape{}); + const auto rotated_block_indices = std::make_shared(element::i32, PartialShape{0}); + const auto rotation_deltas = std::make_shared(element::i32, PartialShape{0}); + const auto rotation_trig_lut = std::make_shared(element::f32, PartialShape{0}); + const auto xattention_threshold = std::make_shared(element::f32, PartialShape{0}); + const auto xattention_block_size = std::make_shared(element::i32, PartialShape{}); + const auto xattention_stride = std::make_shared(element::i32, PartialShape{}); + const auto sinks = std::make_shared(element::f32, PartialShape{0}); + const auto adaptive_rkv_start_size = std::make_shared(element::i32, Shape{}, std::vector{0}); + const auto adaptive_rkv_evictable_sizes = + std::make_shared(element::i32, Shape{1}, std::vector{32}); + const auto adaptive_rkv_diversity_block_set_indices = + std::make_shared(element::i32, PartialShape{1}); + const auto adaptive_rkv_diversity_block_set_indices_begins = + std::make_shared(element::i32, PartialShape{2}); + const auto token_type_ids = std::make_shared(element::i32, PartialShape{0}); + const auto qq_bias = std::make_shared(element::u8, PartialShape{0}); + const auto qq_bias_begins = std::make_shared(element::i32, PartialShape{0}); + + OutputVector args = {query, + key, + value, + key_cache, + value_cache, + past_lens, + subsequence_begins, + block_indices, + block_indices_begins, + scale, + sliding_window, + alibi_slopes, + max_context_len, + score_aggregation_window, + rotated_block_indices, + rotation_deltas, + rotation_trig_lut, + xattention_threshold, + xattention_block_size, + xattention_stride, + sinks, + adaptive_rkv_start_size, + adaptive_rkv_evictable_sizes, + adaptive_rkv_diversity_block_set_indices, + adaptive_rkv_diversity_block_set_indices_begins, + token_type_ids, + qq_bias, + qq_bias_begins}; + + const auto op = std::make_shared(args); + EXPECT_EQ(op->get_output_partial_shape(2), (PartialShape{32})); } static ov::OutputVector make_args_with_token_type(const std::shared_ptr& token_type_ids) { diff --git a/src/core/tests/type_prop/paged_causal_conv1d.cpp b/src/core/tests/type_prop/paged_causal_conv1d.cpp new file mode 100644 index 000000000000..2e4f3d96586a --- /dev/null +++ b/src/core/tests/type_prop/paged_causal_conv1d.cpp @@ -0,0 +1,570 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "openvino/op/paged_causal_conv1d.hpp" + +#include + +#include "common_test_utils/test_assertions.hpp" +#include "common_test_utils/type_prop.hpp" +#include "openvino/openvino.hpp" + +namespace ov::test { + +class TypePropPagedCausalConv1DTest : public TypePropOpTest {}; + +TEST_F(TypePropPagedCausalConv1DTest, f32_static_shapes) { + const auto input_embeds = std::make_shared(element::f32, Shape{10, 256}); + const auto conv_state_table = std::make_shared(element::f32, Shape{5, 256, 4}); + const auto conv_weight = std::make_shared(element::f32, Shape{256, 256, 4}); + const auto conv_bias = std::make_shared(element::f32, Shape{256}); + const auto subsequence_begins = std::make_shared(element::i32, Shape{3}); + const auto la_block_indices = std::make_shared(element::i32, Shape{5}); + const auto la_block_indices_begins = std::make_shared(element::i32, Shape{3}); + const auto processed_tokens = std::make_shared(element::i32, Shape{2}); + const auto cache_interval = std::make_shared(element::i32, Shape{2}); + + const auto op = make_op(OutputVector{input_embeds, + conv_state_table, + conv_weight, + conv_bias, + subsequence_begins, + la_block_indices, + la_block_indices_begins, + processed_tokens, + cache_interval}); + + EXPECT_EQ(op->get_output_size(), 1); + EXPECT_EQ(op->get_output_element_type(0), element::f32); + EXPECT_EQ(op->get_output_partial_shape(0), PartialShape(Shape{10, 256})); +} + +TEST_F(TypePropPagedCausalConv1DTest, f16_static_shapes) { + const auto input_embeds = std::make_shared(element::f16, Shape{10, 256}); + const auto conv_state_table = std::make_shared(element::f16, Shape{5, 256, 4}); + const auto conv_weight = std::make_shared(element::f16, Shape{256, 256, 4}); + const auto conv_bias = std::make_shared(element::f16, Shape{256}); + const auto subsequence_begins = std::make_shared(element::i32, Shape{3}); + const auto la_block_indices = std::make_shared(element::i32, Shape{5}); + const auto la_block_indices_begins = std::make_shared(element::i32, Shape{3}); + const auto processed_tokens = std::make_shared(element::i32, Shape{2}); + const auto cache_interval = std::make_shared(element::i32, Shape{2}); + + const auto op = make_op(OutputVector{input_embeds, + conv_state_table, + conv_weight, + conv_bias, + subsequence_begins, + la_block_indices, + la_block_indices_begins, + processed_tokens, + cache_interval}); + + EXPECT_EQ(op->get_output_element_type(0), element::f16); + EXPECT_EQ(op->get_output_partial_shape(0), PartialShape(Shape{10, 256})); +} + +TEST_F(TypePropPagedCausalConv1DTest, bf16_partial_shapes) { + const auto input_embeds = std::make_shared(element::bf16, PartialShape{-1, {128, 512}}); + const auto conv_state_table = std::make_shared(element::bf16, PartialShape{-1, {128, 512}, -1}); + const auto conv_weight = std::make_shared(element::bf16, PartialShape{-1, -1, -1}); + const auto conv_bias = std::make_shared(element::bf16, PartialShape{-1}); + const auto subsequence_begins = std::make_shared(element::i32, PartialShape{-1}); + const auto la_block_indices = std::make_shared(element::i32, PartialShape{-1}); + const auto la_block_indices_begins = std::make_shared(element::i32, PartialShape{-1}); + const auto processed_tokens = std::make_shared(element::i32, PartialShape{-1}); + const auto cache_interval = std::make_shared(element::i32, PartialShape{-1}); + + const auto op = make_op(OutputVector{input_embeds, + conv_state_table, + conv_weight, + conv_bias, + subsequence_begins, + la_block_indices, + la_block_indices_begins, + processed_tokens, + cache_interval}); + + EXPECT_EQ(op->get_output_element_type(0), element::bf16); + EXPECT_EQ(op->get_output_partial_shape(0), (PartialShape{-1, {128, 512}})); +} + +TEST_F(TypePropPagedCausalConv1DTest, invalid_input_embeds_rank) { + const auto input_embeds = std::make_shared(element::f32, Shape{10, 256, 1}); + const auto conv_state_table = std::make_shared(element::f32, Shape{5, 256, 4}); + const auto conv_weight = std::make_shared(element::f32, Shape{256, 256, 4}); + const auto conv_bias = std::make_shared(element::f32, Shape{256}); + const auto subsequence_begins = std::make_shared(element::i32, Shape{3}); + const auto la_block_indices = std::make_shared(element::i32, Shape{5}); + const auto la_block_indices_begins = std::make_shared(element::i32, Shape{3}); + const auto processed_tokens = std::make_shared(element::i32, Shape{2}); + const auto cache_interval = std::make_shared(element::i32, Shape{2}); + + OV_EXPECT_THROW(std::ignore = make_op(OutputVector{input_embeds, + conv_state_table, + conv_weight, + conv_bias, + subsequence_begins, + la_block_indices, + la_block_indices_begins, + processed_tokens, + cache_interval}), + NodeValidationFailure, + testing::HasSubstr("input_shapes[0].rank().compatible(2)")); +} + +TEST_F(TypePropPagedCausalConv1DTest, invalid_conv_state_table_rank) { + const auto input_embeds = std::make_shared(element::f32, Shape{10, 256}); + const auto conv_state_table = std::make_shared(element::f32, Shape{5, 256}); + const auto conv_weight = std::make_shared(element::f32, Shape{256, 256, 4}); + const auto conv_bias = std::make_shared(element::f32, Shape{256}); + const auto subsequence_begins = std::make_shared(element::i32, Shape{3}); + const auto la_block_indices = std::make_shared(element::i32, Shape{5}); + const auto la_block_indices_begins = std::make_shared(element::i32, Shape{3}); + const auto processed_tokens = std::make_shared(element::i32, Shape{2}); + const auto cache_interval = std::make_shared(element::i32, Shape{2}); + + OV_EXPECT_THROW(std::ignore = make_op(OutputVector{input_embeds, + conv_state_table, + conv_weight, + conv_bias, + subsequence_begins, + la_block_indices, + la_block_indices_begins, + processed_tokens, + cache_interval}), + NodeValidationFailure, + testing::HasSubstr("input_shapes[1].rank().compatible(3)")); +} + +TEST_F(TypePropPagedCausalConv1DTest, invalid_float_type) { + const auto input_embeds = std::make_shared(element::i32, Shape{10, 256}); + const auto conv_state_table = std::make_shared(element::i32, Shape{5, 256, 4}); + const auto conv_weight = std::make_shared(element::i32, Shape{256, 256, 4}); + const auto conv_bias = std::make_shared(element::i32, Shape{256}); + const auto subsequence_begins = std::make_shared(element::i32, Shape{3}); + const auto la_block_indices = std::make_shared(element::i32, Shape{5}); + const auto la_block_indices_begins = std::make_shared(element::i32, Shape{3}); + const auto processed_tokens = std::make_shared(element::i32, Shape{2}); + const auto cache_interval = std::make_shared(element::i32, Shape{2}); + + OV_EXPECT_THROW(std::ignore = make_op(OutputVector{input_embeds, + conv_state_table, + conv_weight, + conv_bias, + subsequence_begins, + la_block_indices, + la_block_indices_begins, + processed_tokens, + cache_interval}), + NodeValidationFailure, + testing::HasSubstr("Float inputs must have f32, f16, or bf16 element type.")); +} + +TEST_F(TypePropPagedCausalConv1DTest, state_float_type_independent_from_embeds) { + const auto input_embeds = std::make_shared(element::f32, Shape{10, 256}); + const auto conv_state_table = std::make_shared(element::f16, Shape{5, 256, 4}); + const auto conv_weight = std::make_shared(element::f32, Shape{256, 256, 4}); + const auto conv_bias = std::make_shared(element::f32, Shape{256}); + const auto subsequence_begins = std::make_shared(element::i32, Shape{3}); + const auto la_block_indices = std::make_shared(element::i32, Shape{5}); + const auto la_block_indices_begins = std::make_shared(element::i32, Shape{3}); + const auto processed_tokens = std::make_shared(element::i32, Shape{2}); + const auto cache_interval = std::make_shared(element::i32, Shape{2}); + + const auto op = make_op(OutputVector{input_embeds, + conv_state_table, + conv_weight, + conv_bias, + subsequence_begins, + la_block_indices, + la_block_indices_begins, + processed_tokens, + cache_interval}); + + EXPECT_EQ(op->get_output_size(), 1); + EXPECT_EQ(op->get_output_element_type(0), element::f32); + EXPECT_EQ(op->get_output_partial_shape(0), PartialShape(Shape{10, 256})); +} + +TEST_F(TypePropPagedCausalConv1DTest, float_type_mismatch_among_embeds_weight_bias) { + const auto input_embeds = std::make_shared(element::f32, Shape{10, 256}); + const auto conv_state_table = std::make_shared(element::f32, Shape{5, 256, 4}); + const auto conv_weight = std::make_shared(element::f16, Shape{256, 256, 4}); + const auto conv_bias = std::make_shared(element::f32, Shape{256}); + const auto subsequence_begins = std::make_shared(element::i32, Shape{3}); + const auto la_block_indices = std::make_shared(element::i32, Shape{5}); + const auto la_block_indices_begins = std::make_shared(element::i32, Shape{3}); + const auto processed_tokens = std::make_shared(element::i32, Shape{2}); + const auto cache_interval = std::make_shared(element::i32, Shape{2}); + + OV_EXPECT_THROW(std::ignore = make_op(OutputVector{input_embeds, + conv_state_table, + conv_weight, + conv_bias, + subsequence_begins, + la_block_indices, + la_block_indices_begins, + processed_tokens, + cache_interval}), + NodeValidationFailure, + testing::HasSubstr("PagedCausalConv1D expects input_embeds, conv_weight, and conv_bias to have " + "the same element type.")); +} + +TEST_F(TypePropPagedCausalConv1DTest, hidden_size_mismatch) { + const auto input_embeds = std::make_shared(element::f32, Shape{10, 256}); + const auto conv_state_table = std::make_shared(element::f32, Shape{5, 128, 4}); + const auto conv_weight = std::make_shared(element::f32, Shape{256, 256, 4}); + const auto conv_bias = std::make_shared(element::f32, Shape{256}); + const auto subsequence_begins = std::make_shared(element::i32, Shape{3}); + const auto la_block_indices = std::make_shared(element::i32, Shape{5}); + const auto la_block_indices_begins = std::make_shared(element::i32, Shape{3}); + const auto processed_tokens = std::make_shared(element::i32, Shape{2}); + const auto cache_interval = std::make_shared(element::i32, Shape{2}); + + OV_EXPECT_THROW(std::ignore = make_op(OutputVector{input_embeds, + conv_state_table, + conv_weight, + conv_bias, + subsequence_begins, + la_block_indices, + la_block_indices_begins, + processed_tokens, + cache_interval}), + NodeValidationFailure, + testing::HasSubstr("hidden_size dimensions of input_embeds and conv_state_table inputs must be " + "compatible")); +} + +TEST_F(TypePropPagedCausalConv1DTest, kernel_size_mismatch) { + const auto input_embeds = std::make_shared(element::f32, Shape{10, 256}); + const auto conv_state_table = std::make_shared(element::f32, Shape{5, 256, 4}); + const auto conv_weight = std::make_shared(element::f32, Shape{256, 256, 8}); + const auto conv_bias = std::make_shared(element::f32, Shape{256}); + const auto subsequence_begins = std::make_shared(element::i32, Shape{3}); + const auto la_block_indices = std::make_shared(element::i32, Shape{5}); + const auto la_block_indices_begins = std::make_shared(element::i32, Shape{3}); + const auto processed_tokens = std::make_shared(element::i32, Shape{2}); + const auto cache_interval = std::make_shared(element::i32, Shape{2}); + + OV_EXPECT_THROW(std::ignore = make_op(OutputVector{input_embeds, + conv_state_table, + conv_weight, + conv_bias, + subsequence_begins, + la_block_indices, + la_block_indices_begins, + processed_tokens, + cache_interval}), + NodeValidationFailure, + testing::HasSubstr("kernel_size dimensions of conv_state_table and conv_weight inputs must be " + "compatible")); +} + +TEST_F(TypePropPagedCausalConv1DTest, wrong_input_count) { + auto p = std::make_shared(element::f32, Shape{10, 256}); + + OV_EXPECT_THROW(std::ignore = make_op(OutputVector{p, p, p}), + NodeValidationFailure, + testing::HasSubstr("Check 'get_input_size() == 9'")); +} + +TEST_F(TypePropPagedCausalConv1DTest, out_channels_hidden_size_mismatch) { + const auto input_embeds = std::make_shared(element::f32, Shape{10, 256}); + const auto conv_state_table = std::make_shared(element::f32, Shape{5, 256, 4}); + const auto conv_weight = std::make_shared(element::f32, Shape{128, 256, 4}); + const auto conv_bias = std::make_shared(element::f32, Shape{128}); + const auto subsequence_begins = std::make_shared(element::i32, Shape{3}); + const auto la_block_indices = std::make_shared(element::i32, Shape{5}); + const auto la_block_indices_begins = std::make_shared(element::i32, Shape{3}); + const auto processed_tokens = std::make_shared(element::i32, Shape{2}); + const auto cache_interval = std::make_shared(element::i32, Shape{2}); + + OV_EXPECT_THROW(std::ignore = make_op(OutputVector{input_embeds, + conv_state_table, + conv_weight, + conv_bias, + subsequence_begins, + la_block_indices, + la_block_indices_begins, + processed_tokens, + cache_interval}), + NodeValidationFailure, + testing::HasSubstr("out_channels dimension of conv_weight must be compatible with the hidden_size " + "dimension of input_embeds")); +} + +TEST_F(TypePropPagedCausalConv1DTest, conv_bias_size_mismatch) { + const auto input_embeds = std::make_shared(element::f32, Shape{10, 256}); + const auto conv_state_table = std::make_shared(element::f32, Shape{5, 256, 4}); + const auto conv_weight = std::make_shared(element::f32, Shape{256, 256, 4}); + const auto conv_bias = std::make_shared(element::f32, Shape{128}); + const auto subsequence_begins = std::make_shared(element::i32, Shape{3}); + const auto la_block_indices = std::make_shared(element::i32, Shape{5}); + const auto la_block_indices_begins = std::make_shared(element::i32, Shape{3}); + const auto processed_tokens = std::make_shared(element::i32, Shape{2}); + const auto cache_interval = std::make_shared(element::i32, Shape{2}); + + OV_EXPECT_THROW(std::ignore = make_op(OutputVector{input_embeds, + conv_state_table, + conv_weight, + conv_bias, + subsequence_begins, + la_block_indices, + la_block_indices_begins, + processed_tokens, + cache_interval}), + NodeValidationFailure, + testing::HasSubstr("size of conv_bias must be compatible with the out_channels dimension of " + "conv_weight or equal to 0 (no bias)")); +} + +TEST_F(TypePropPagedCausalConv1DTest, i64_integer_inputs_accepted) { + const auto input_embeds = std::make_shared(element::f32, Shape{10, 256}); + const auto conv_state_table = std::make_shared(element::f32, Shape{5, 256, 4}); + const auto conv_weight = std::make_shared(element::f32, Shape{256, 256, 4}); + const auto conv_bias = std::make_shared(element::f32, Shape{256}); + const auto subsequence_begins = std::make_shared(element::i64, Shape{3}); + const auto la_block_indices = std::make_shared(element::i64, Shape{5}); + const auto la_block_indices_begins = std::make_shared(element::i64, Shape{3}); + const auto processed_tokens = std::make_shared(element::i64, Shape{2}); + const auto cache_interval = std::make_shared(element::i64, Shape{2}); + + const auto op = make_op(OutputVector{input_embeds, + conv_state_table, + conv_weight, + conv_bias, + subsequence_begins, + la_block_indices, + la_block_indices_begins, + processed_tokens, + cache_interval}); + + EXPECT_EQ(op->get_output_size(), 1); + EXPECT_EQ(op->get_output_element_type(0), element::f32); + EXPECT_EQ(op->get_output_partial_shape(0), PartialShape(Shape{10, 256})); +} + +TEST_F(TypePropPagedCausalConv1DTest, invalid_subsequence_begins_rank) { + const auto input_embeds = std::make_shared(element::f32, Shape{10, 256}); + const auto conv_state_table = std::make_shared(element::f32, Shape{5, 256, 4}); + const auto conv_weight = std::make_shared(element::f32, Shape{256, 256, 4}); + const auto conv_bias = std::make_shared(element::f32, Shape{256}); + const auto subsequence_begins = std::make_shared(element::i32, Shape{2, 2}); + const auto la_block_indices = std::make_shared(element::i32, Shape{5}); + const auto la_block_indices_begins = std::make_shared(element::i32, Shape{3}); + const auto processed_tokens = std::make_shared(element::i32, Shape{2}); + const auto cache_interval = std::make_shared(element::i32, Shape{2}); + + OV_EXPECT_THROW(std::ignore = make_op(OutputVector{input_embeds, + conv_state_table, + conv_weight, + conv_bias, + subsequence_begins, + la_block_indices, + la_block_indices_begins, + processed_tokens, + cache_interval}), + NodeValidationFailure, + testing::HasSubstr("input_shapes[4].rank().compatible(1)")); +} + +TEST_F(TypePropPagedCausalConv1DTest, invalid_integer_type) { + const auto input_embeds = std::make_shared(element::f32, Shape{10, 256}); + const auto conv_state_table = std::make_shared(element::f32, Shape{5, 256, 4}); + const auto conv_weight = std::make_shared(element::f32, Shape{256, 256, 4}); + const auto conv_bias = std::make_shared(element::f32, Shape{256}); + const auto subsequence_begins = std::make_shared(element::i16, Shape{3}); + const auto la_block_indices = std::make_shared(element::i16, Shape{5}); + const auto la_block_indices_begins = std::make_shared(element::i16, Shape{3}); + const auto processed_tokens = std::make_shared(element::i16, Shape{2}); + const auto cache_interval = std::make_shared(element::i16, Shape{2}); + + OV_EXPECT_THROW(std::ignore = make_op(OutputVector{input_embeds, + conv_state_table, + conv_weight, + conv_bias, + subsequence_begins, + la_block_indices, + la_block_indices_begins, + processed_tokens, + cache_interval}), + NodeValidationFailure, + testing::HasSubstr("Integer inputs must have i32 or i64 element type.")); +} + +TEST_F(TypePropPagedCausalConv1DTest, empty_conv_bias) { + const auto input_embeds = std::make_shared(element::f32, Shape{10, 256}); + const auto conv_state_table = std::make_shared(element::f32, Shape{5, 256, 4}); + const auto conv_weight = std::make_shared(element::f32, Shape{256, 256, 4}); + const auto conv_bias = std::make_shared(element::f32, Shape{0}); + const auto subsequence_begins = std::make_shared(element::i32, Shape{3}); + const auto la_block_indices = std::make_shared(element::i32, Shape{5}); + const auto la_block_indices_begins = std::make_shared(element::i32, Shape{3}); + const auto processed_tokens = std::make_shared(element::i32, Shape{2}); + const auto cache_interval = std::make_shared(element::i32, Shape{2}); + + const auto op = make_op(OutputVector{input_embeds, + conv_state_table, + conv_weight, + conv_bias, + subsequence_begins, + la_block_indices, + la_block_indices_begins, + processed_tokens, + cache_interval}); + + EXPECT_EQ(op->get_output_size(), 1); + EXPECT_EQ(op->get_output_element_type(0), element::f32); + EXPECT_EQ(op->get_output_partial_shape(0), PartialShape(Shape{10, 256})); +} + +TEST_F(TypePropPagedCausalConv1DTest, dynamic_rank_accepted) { + const auto input_embeds = std::make_shared(element::f32, PartialShape::dynamic()); + const auto conv_state_table = std::make_shared(element::f32, PartialShape::dynamic()); + const auto conv_weight = std::make_shared(element::f32, PartialShape::dynamic()); + const auto conv_bias = std::make_shared(element::f32, PartialShape::dynamic()); + const auto subsequence_begins = std::make_shared(element::i32, PartialShape{-1}); + const auto la_block_indices = std::make_shared(element::i32, PartialShape{-1}); + const auto la_block_indices_begins = std::make_shared(element::i32, PartialShape{-1}); + const auto processed_tokens = std::make_shared(element::i32, PartialShape{-1}); + const auto cache_interval = std::make_shared(element::i32, PartialShape{-1}); + + const auto op = make_op(OutputVector{input_embeds, + conv_state_table, + conv_weight, + conv_bias, + subsequence_begins, + la_block_indices, + la_block_indices_begins, + processed_tokens, + cache_interval}); + + EXPECT_EQ(op->get_output_size(), 1); +} + +TEST_F(TypePropPagedCausalConv1DTest, dynamic_type_accepted) { + const auto input_embeds = std::make_shared(element::dynamic, Shape{10, 256}); + const auto conv_state_table = std::make_shared(element::dynamic, Shape{5, 256, 4}); + const auto conv_weight = std::make_shared(element::dynamic, Shape{256, 256, 4}); + const auto conv_bias = std::make_shared(element::dynamic, Shape{256}); + const auto subsequence_begins = std::make_shared(element::i32, Shape{3}); + const auto la_block_indices = std::make_shared(element::i32, Shape{5}); + const auto la_block_indices_begins = std::make_shared(element::i32, Shape{3}); + const auto processed_tokens = std::make_shared(element::i32, Shape{2}); + const auto cache_interval = std::make_shared(element::i32, Shape{2}); + + const auto op = make_op(OutputVector{input_embeds, + conv_state_table, + conv_weight, + conv_bias, + subsequence_begins, + la_block_indices, + la_block_indices_begins, + processed_tokens, + cache_interval}); + + EXPECT_EQ(op->get_output_size(), 1); + EXPECT_EQ(op->get_output_element_type(0), element::dynamic); +} + +TEST_F(TypePropPagedCausalConv1DTest, logical_block_indices_may_exceed_physical_state_blocks) { + const auto input_embeds = std::make_shared(element::f32, Shape{5, 256}); + const auto conv_state_table = std::make_shared(element::f32, Shape{1, 256, 4}); + const auto conv_weight = std::make_shared(element::f32, Shape{256, 256, 4}); + const auto conv_bias = std::make_shared(element::f32, Shape{256}); + const auto subsequence_begins = std::make_shared(element::i32, Shape{2}); + const auto la_block_indices = std::make_shared(element::i32, Shape{2}); + const auto la_block_indices_begins = std::make_shared(element::i32, Shape{2}); + const auto processed_tokens = std::make_shared(element::i32, Shape{1}); + const auto cache_interval = std::make_shared(element::i32, Shape{1}); + + const auto op = make_op(OutputVector{input_embeds, + conv_state_table, + conv_weight, + conv_bias, + subsequence_begins, + la_block_indices, + la_block_indices_begins, + processed_tokens, + cache_interval}); + + EXPECT_EQ(op->get_output_element_type(0), element::f32); + EXPECT_EQ(op->get_output_partial_shape(0), PartialShape(Shape{5, 256})); +} + +TEST_F(TypePropPagedCausalConv1DTest, subsequence_begins_block_begins_mismatch) { + const auto input_embeds = std::make_shared(element::f32, Shape{10, 256}); + const auto conv_state_table = std::make_shared(element::f32, Shape{5, 256, 4}); + const auto conv_weight = std::make_shared(element::f32, Shape{256, 256, 4}); + const auto conv_bias = std::make_shared(element::f32, Shape{256}); + const auto subsequence_begins = std::make_shared(element::i32, Shape{3}); + const auto la_block_indices = std::make_shared(element::i32, Shape{5}); + const auto la_block_indices_begins = std::make_shared(element::i32, Shape{5}); + const auto processed_tokens = std::make_shared(element::i32, Shape{2}); + const auto cache_interval = std::make_shared(element::i32, Shape{2}); + + OV_EXPECT_THROW(std::ignore = make_op(OutputVector{input_embeds, + conv_state_table, + conv_weight, + conv_bias, + subsequence_begins, + la_block_indices, + la_block_indices_begins, + processed_tokens, + cache_interval}), + NodeValidationFailure, + testing::HasSubstr("size of subsequence_begins must be compatible with the size of " + "la_block_indices_begins")); +} + +TEST_F(TypePropPagedCausalConv1DTest, processed_tokens_size_mismatch) { + const auto input_embeds = std::make_shared(element::f32, Shape{10, 256}); + const auto conv_state_table = std::make_shared(element::f32, Shape{5, 256, 4}); + const auto conv_weight = std::make_shared(element::f32, Shape{256, 256, 4}); + const auto conv_bias = std::make_shared(element::f32, Shape{256}); + const auto subsequence_begins = std::make_shared(element::i32, Shape{3}); + const auto la_block_indices = std::make_shared(element::i32, Shape{5}); + const auto la_block_indices_begins = std::make_shared(element::i32, Shape{3}); + const auto processed_tokens = std::make_shared(element::i32, Shape{5}); + const auto cache_interval = std::make_shared(element::i32, Shape{2}); + + OV_EXPECT_THROW(std::ignore = make_op(OutputVector{input_embeds, + conv_state_table, + conv_weight, + conv_bias, + subsequence_begins, + la_block_indices, + la_block_indices_begins, + processed_tokens, + cache_interval}), + NodeValidationFailure, + testing::HasSubstr("size of processed_tokens must be batch_size_in_sequences")); +} + +TEST_F(TypePropPagedCausalConv1DTest, cache_interval_size_mismatch) { + const auto input_embeds = std::make_shared(element::f32, Shape{10, 256}); + const auto conv_state_table = std::make_shared(element::f32, Shape{5, 256, 4}); + const auto conv_weight = std::make_shared(element::f32, Shape{256, 256, 4}); + const auto conv_bias = std::make_shared(element::f32, Shape{256}); + const auto subsequence_begins = std::make_shared(element::i32, Shape{3}); + const auto la_block_indices = std::make_shared(element::i32, Shape{5}); + const auto la_block_indices_begins = std::make_shared(element::i32, Shape{3}); + const auto processed_tokens = std::make_shared(element::i32, Shape{2}); + const auto cache_interval = std::make_shared(element::i32, Shape{5}); + + OV_EXPECT_THROW(std::ignore = make_op(OutputVector{input_embeds, + conv_state_table, + conv_weight, + conv_bias, + subsequence_begins, + la_block_indices, + la_block_indices_begins, + processed_tokens, + cache_interval}), + NodeValidationFailure, + testing::HasSubstr("size of cache_interval must be batch_size_in_sequences")); +} +} // namespace ov::test diff --git a/src/core/tests/type_prop/paged_gated_delta_net.cpp b/src/core/tests/type_prop/paged_gated_delta_net.cpp new file mode 100644 index 000000000000..32542275461b --- /dev/null +++ b/src/core/tests/type_prop/paged_gated_delta_net.cpp @@ -0,0 +1,818 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "openvino/op/paged_gated_delta_net.hpp" + +#include + +#include "common_test_utils/test_assertions.hpp" +#include "common_test_utils/type_prop.hpp" +#include "openvino/openvino.hpp" + +namespace ov::test { + +class TypePropPagedGatedDeltaNetTest : public TypePropOpTest {}; + +TEST_F(TypePropPagedGatedDeltaNetTest, f32_static_shapes) { + const auto query = std::make_shared(element::f32, Shape{10, 4, 8}); + const auto key = std::make_shared(element::f32, Shape{10, 4, 8}); + const auto value = std::make_shared(element::f32, Shape{10, 4, 16}); + const auto state = std::make_shared(element::f32, Shape{5, 4, 16, 8}); + const auto gate = std::make_shared(element::f32, Shape{10, 4}); + const auto beta = std::make_shared(element::f32, Shape{10, 4}); + const auto subsequence_begins = std::make_shared(element::i32, Shape{3}); + const auto la_block_indices = std::make_shared(element::i32, Shape{5}); + const auto la_block_indices_begins = std::make_shared(element::i32, Shape{3}); + const auto processed_tokens = std::make_shared(element::i32, Shape{2}); + const auto cache_interval = std::make_shared(element::i32, Shape{2}); + + const auto op = make_op(OutputVector{query, + key, + value, + state, + gate, + beta, + subsequence_begins, + la_block_indices, + la_block_indices_begins, + processed_tokens, + cache_interval}); + + EXPECT_EQ(op->get_output_size(), 1); + EXPECT_EQ(op->get_output_element_type(0), element::f32); + EXPECT_EQ(op->get_output_partial_shape(0), PartialShape(Shape{10, 4, 16})); +} + +TEST_F(TypePropPagedGatedDeltaNetTest, f16_static_shapes) { + const auto query = std::make_shared(element::f16, Shape{10, 4, 8}); + const auto key = std::make_shared(element::f16, Shape{10, 4, 8}); + const auto value = std::make_shared(element::f16, Shape{10, 4, 16}); + const auto state = std::make_shared(element::f16, Shape{5, 4, 16, 8}); + const auto gate = std::make_shared(element::f16, Shape{10, 4}); + const auto beta = std::make_shared(element::f16, Shape{10, 4}); + const auto subsequence_begins = std::make_shared(element::i32, Shape{3}); + const auto la_block_indices = std::make_shared(element::i32, Shape{5}); + const auto la_block_indices_begins = std::make_shared(element::i32, Shape{3}); + const auto processed_tokens = std::make_shared(element::i32, Shape{2}); + const auto cache_interval = std::make_shared(element::i32, Shape{2}); + + const auto op = make_op(OutputVector{query, + key, + value, + state, + gate, + beta, + subsequence_begins, + la_block_indices, + la_block_indices_begins, + processed_tokens, + cache_interval}); + + EXPECT_EQ(op->get_output_element_type(0), element::f16); + EXPECT_EQ(op->get_output_partial_shape(0), PartialShape(Shape{10, 4, 16})); +} + +TEST_F(TypePropPagedGatedDeltaNetTest, bf16_partial_shapes) { + const auto query = std::make_shared(element::bf16, PartialShape{-1, {2, 8}, 64}); + const auto key = std::make_shared(element::bf16, PartialShape{-1, {2, 8}, 64}); + const auto value = std::make_shared(element::bf16, PartialShape{-1, {2, 8}, {32, 128}}); + const auto state = std::make_shared(element::bf16, PartialShape{-1, {2, 8}, {32, 128}, 64}); + const auto gate = std::make_shared(element::bf16, PartialShape{-1, {2, 8}}); + const auto beta = std::make_shared(element::bf16, PartialShape{-1, {2, 8}}); + const auto subsequence_begins = std::make_shared(element::i32, PartialShape{-1}); + const auto la_block_indices = std::make_shared(element::i32, PartialShape{-1}); + const auto la_block_indices_begins = std::make_shared(element::i32, PartialShape{-1}); + const auto processed_tokens = std::make_shared(element::i32, PartialShape{-1}); + const auto cache_interval = std::make_shared(element::i32, PartialShape{-1}); + + const auto op = make_op(OutputVector{query, + key, + value, + state, + gate, + beta, + subsequence_begins, + la_block_indices, + la_block_indices_begins, + processed_tokens, + cache_interval}); + + EXPECT_EQ(op->get_output_element_type(0), element::bf16); + EXPECT_EQ(op->get_output_partial_shape(0), (PartialShape{-1, {2, 8}, {32, 128}})); +} + +TEST_F(TypePropPagedGatedDeltaNetTest, query_incompatible_rank) { + const auto query = std::make_shared(element::f32, Shape{10, 4, 8, 1}); // rank 4 - invalid + const auto key = std::make_shared(element::f32, Shape{10, 4, 8}); + const auto value = std::make_shared(element::f32, Shape{10, 4, 16}); + const auto state = std::make_shared(element::f32, Shape{5, 4, 16, 8}); + const auto gate = std::make_shared(element::f32, Shape{10, 4}); + const auto beta = std::make_shared(element::f32, Shape{10, 4}); + const auto subsequence_begins = std::make_shared(element::i32, Shape{3}); + const auto la_block_indices = std::make_shared(element::i32, Shape{5}); + const auto la_block_indices_begins = std::make_shared(element::i32, Shape{3}); + const auto processed_tokens = std::make_shared(element::i32, Shape{2}); + const auto cache_interval = std::make_shared(element::i32, Shape{2}); + + OV_EXPECT_THROW(std::ignore = make_op(OutputVector{query, + key, + value, + state, + gate, + beta, + subsequence_begins, + la_block_indices, + la_block_indices_begins, + processed_tokens, + cache_interval}), + NodeValidationFailure, + testing::HasSubstr("input_shapes[0].rank().compatible(3)")); +} + +TEST_F(TypePropPagedGatedDeltaNetTest, query_incompatible_type) { + const auto query = std::make_shared(element::i32, Shape{10, 4, 8}); + const auto key = std::make_shared(element::i32, Shape{10, 4, 8}); + const auto value = std::make_shared(element::i32, Shape{10, 4, 16}); + const auto state = std::make_shared(element::i32, Shape{5, 4, 16, 8}); + const auto gate = std::make_shared(element::i32, Shape{10, 4}); + const auto beta = std::make_shared(element::i32, Shape{10, 4}); + const auto subsequence_begins = std::make_shared(element::i32, Shape{3}); + const auto la_block_indices = std::make_shared(element::i32, Shape{5}); + const auto la_block_indices_begins = std::make_shared(element::i32, Shape{3}); + const auto processed_tokens = std::make_shared(element::i32, Shape{2}); + const auto cache_interval = std::make_shared(element::i32, Shape{2}); + + OV_EXPECT_THROW(std::ignore = make_op(OutputVector{query, + key, + value, + state, + gate, + beta, + subsequence_begins, + la_block_indices, + la_block_indices_begins, + processed_tokens, + cache_interval}), + NodeValidationFailure, + testing::HasSubstr("Float inputs must have f32, f16, or bf16 element type")); +} + +TEST_F(TypePropPagedGatedDeltaNetTest, key_incompatible_head_num) { + const auto query = std::make_shared(element::f32, Shape{10, 4, 8}); + const auto key = std::make_shared(element::f32, Shape{10, 6, 8}); // num_heads 6 != 4 + const auto value = std::make_shared(element::f32, Shape{10, 4, 16}); + const auto state = std::make_shared(element::f32, Shape{5, 4, 16, 8}); + const auto gate = std::make_shared(element::f32, Shape{10, 4}); + const auto beta = std::make_shared(element::f32, Shape{10, 4}); + const auto subsequence_begins = std::make_shared(element::i32, Shape{3}); + const auto la_block_indices = std::make_shared(element::i32, Shape{5}); + const auto la_block_indices_begins = std::make_shared(element::i32, Shape{3}); + const auto processed_tokens = std::make_shared(element::i32, Shape{2}); + const auto cache_interval = std::make_shared(element::i32, Shape{2}); + + OV_EXPECT_THROW(std::ignore = make_op(OutputVector{query, + key, + value, + state, + gate, + beta, + subsequence_begins, + la_block_indices, + la_block_indices_begins, + processed_tokens, + cache_interval}), + NodeValidationFailure, + testing::HasSubstr("The number of heads in query and key inputs must be equal")); +} + +TEST_F(TypePropPagedGatedDeltaNetTest, key_incompatible_head_num_partial) { + // Non-overlapping dimension ranges: query num_heads {2,4}, key num_heads {6,10} + const auto query = std::make_shared(element::f32, PartialShape{-1, {2, 4}, 8}); + const auto key = std::make_shared(element::f32, PartialShape{-1, {6, 10}, 8}); + const auto value = std::make_shared(element::f32, PartialShape{-1, {6, 10}, 16}); + const auto state = std::make_shared(element::f32, PartialShape{-1, {6, 10}, 16, 8}); + const auto gate = std::make_shared(element::f32, PartialShape{-1, {6, 10}}); + const auto beta = std::make_shared(element::f32, PartialShape{-1, {6, 10}}); + const auto subsequence_begins = std::make_shared(element::i32, PartialShape{-1}); + const auto la_block_indices = std::make_shared(element::i32, PartialShape{-1}); + const auto la_block_indices_begins = std::make_shared(element::i32, PartialShape{-1}); + const auto processed_tokens = std::make_shared(element::i32, PartialShape{-1}); + const auto cache_interval = std::make_shared(element::i32, PartialShape{-1}); + + OV_EXPECT_THROW(std::ignore = make_op(OutputVector{query, + key, + value, + state, + gate, + beta, + subsequence_begins, + la_block_indices, + la_block_indices_begins, + processed_tokens, + cache_interval}), + NodeValidationFailure, + testing::HasSubstr("The number of heads in query and key inputs must be equal")); +} + +TEST_F(TypePropPagedGatedDeltaNetTest, key_incompatible_head_size) { + const auto query = std::make_shared(element::f32, Shape{10, 4, 8}); + const auto key = std::make_shared(element::f32, Shape{10, 4, 32}); // key_head_dim 32 != 8 + const auto value = std::make_shared(element::f32, Shape{10, 4, 16}); + const auto state = std::make_shared(element::f32, Shape{5, 4, 16, 32}); + const auto gate = std::make_shared(element::f32, Shape{10, 4}); + const auto beta = std::make_shared(element::f32, Shape{10, 4}); + const auto subsequence_begins = std::make_shared(element::i32, Shape{3}); + const auto la_block_indices = std::make_shared(element::i32, Shape{5}); + const auto la_block_indices_begins = std::make_shared(element::i32, Shape{3}); + const auto processed_tokens = std::make_shared(element::i32, Shape{2}); + const auto cache_interval = std::make_shared(element::i32, Shape{2}); + + OV_EXPECT_THROW(std::ignore = make_op(OutputVector{query, + key, + value, + state, + gate, + beta, + subsequence_begins, + la_block_indices, + la_block_indices_begins, + processed_tokens, + cache_interval}), + NodeValidationFailure, + testing::HasSubstr("The head size of query and key inputs must be equal")); +} + +TEST_F(TypePropPagedGatedDeltaNetTest, gate_incompatible_head_num) { + const auto query = std::make_shared(element::f32, Shape{10, 4, 8}); + const auto key = std::make_shared(element::f32, Shape{10, 4, 8}); + const auto value = std::make_shared(element::f32, Shape{10, 4, 16}); + const auto state = std::make_shared(element::f32, Shape{5, 4, 16, 8}); + const auto gate = std::make_shared(element::f32, Shape{10, 6}); // gate heads 6 != 4 + const auto beta = std::make_shared(element::f32, Shape{10, 4}); + const auto subsequence_begins = std::make_shared(element::i32, Shape{3}); + const auto la_block_indices = std::make_shared(element::i32, Shape{5}); + const auto la_block_indices_begins = std::make_shared(element::i32, Shape{3}); + const auto processed_tokens = std::make_shared(element::i32, Shape{2}); + const auto cache_interval = std::make_shared(element::i32, Shape{2}); + + OV_EXPECT_THROW(std::ignore = make_op(OutputVector{query, + key, + value, + state, + gate, + beta, + subsequence_begins, + la_block_indices, + la_block_indices_begins, + processed_tokens, + cache_interval}), + NodeValidationFailure, + testing::HasSubstr("The number of heads in gate, beta, and value inputs must be equal")); +} + +TEST_F(TypePropPagedGatedDeltaNetTest, state_incompatible_value_dim) { + const auto query = std::make_shared(element::f32, Shape{10, 4, 8}); + const auto key = std::make_shared(element::f32, Shape{10, 4, 8}); + const auto value = std::make_shared(element::f32, Shape{10, 4, 16}); + const auto state = + std::make_shared(element::f32, Shape{5, 4, 32, 8}); // value_head_dim 32 != 16 + const auto gate = std::make_shared(element::f32, Shape{10, 4}); + const auto beta = std::make_shared(element::f32, Shape{10, 4}); + const auto subsequence_begins = std::make_shared(element::i32, Shape{3}); + const auto la_block_indices = std::make_shared(element::i32, Shape{5}); + const auto la_block_indices_begins = std::make_shared(element::i32, Shape{3}); + const auto processed_tokens = std::make_shared(element::i32, Shape{2}); + const auto cache_interval = std::make_shared(element::i32, Shape{2}); + + OV_EXPECT_THROW(std::ignore = make_op(OutputVector{query, + key, + value, + state, + gate, + beta, + subsequence_begins, + la_block_indices, + la_block_indices_begins, + processed_tokens, + cache_interval}), + NodeValidationFailure, + testing::HasSubstr( + "The value dimension of recurrent_state_table and the head size of value input must be equal")); +} + +TEST_F(TypePropPagedGatedDeltaNetTest, state_incompatible_num_heads) { + const auto query = std::make_shared(element::f32, Shape{10, 4, 8}); + const auto key = std::make_shared(element::f32, Shape{10, 4, 8}); + const auto value = std::make_shared(element::f32, Shape{10, 4, 16}); + const auto state = std::make_shared(element::f32, Shape{5, 6, 16, 8}); // num_heads 6 != 4 + const auto gate = std::make_shared(element::f32, Shape{10, 4}); + const auto beta = std::make_shared(element::f32, Shape{10, 4}); + const auto subsequence_begins = std::make_shared(element::i32, Shape{3}); + const auto la_block_indices = std::make_shared(element::i32, Shape{5}); + const auto la_block_indices_begins = std::make_shared(element::i32, Shape{3}); + const auto processed_tokens = std::make_shared(element::i32, Shape{2}); + const auto cache_interval = std::make_shared(element::i32, Shape{2}); + + OV_EXPECT_THROW(std::ignore = make_op(OutputVector{query, + key, + value, + state, + gate, + beta, + subsequence_begins, + la_block_indices, + la_block_indices_begins, + processed_tokens, + cache_interval}), + NodeValidationFailure, + testing::HasSubstr("The number of heads in recurrent_state_table and value inputs must be equal.")); +} + +TEST_F(TypePropPagedGatedDeltaNetTest, state_incompatible_key_dim) { + const auto query = std::make_shared(element::f32, Shape{10, 4, 8}); + const auto key = std::make_shared(element::f32, Shape{10, 4, 8}); + const auto value = std::make_shared(element::f32, Shape{10, 4, 16}); + const auto state = std::make_shared(element::f32, Shape{5, 4, 16, 16}); // key_head_dim 16 != 8 + const auto gate = std::make_shared(element::f32, Shape{10, 4}); + const auto beta = std::make_shared(element::f32, Shape{10, 4}); + const auto subsequence_begins = std::make_shared(element::i32, Shape{3}); + const auto la_block_indices = std::make_shared(element::i32, Shape{5}); + const auto la_block_indices_begins = std::make_shared(element::i32, Shape{3}); + const auto processed_tokens = std::make_shared(element::i32, Shape{2}); + const auto cache_interval = std::make_shared(element::i32, Shape{2}); + + OV_EXPECT_THROW( + std::ignore = make_op(OutputVector{query, + key, + value, + state, + gate, + beta, + subsequence_begins, + la_block_indices, + la_block_indices_begins, + processed_tokens, + cache_interval}), + NodeValidationFailure, + testing::HasSubstr("The key dimension of recurrent_state_table and the head size of key input must be equal.")); +} + +TEST_F(TypePropPagedGatedDeltaNetTest, state_incompatible_value_dim_partial) { + // Non-overlapping dimension ranges: value value_head_dim {8,16}, state value_head_dim {32,64} + const auto query = std::make_shared(element::f32, PartialShape{-1, 4, 8}); + const auto key = std::make_shared(element::f32, PartialShape{-1, 4, 8}); + const auto value = std::make_shared(element::f32, PartialShape{-1, 4, {8, 16}}); + const auto state = std::make_shared(element::f32, PartialShape{-1, 4, {32, 64}, 8}); + const auto gate = std::make_shared(element::f32, PartialShape{-1, 4}); + const auto beta = std::make_shared(element::f32, PartialShape{-1, 4}); + const auto subsequence_begins = std::make_shared(element::i32, PartialShape{-1}); + const auto la_block_indices = std::make_shared(element::i32, PartialShape{-1}); + const auto la_block_indices_begins = std::make_shared(element::i32, PartialShape{-1}); + const auto processed_tokens = std::make_shared(element::i32, PartialShape{-1}); + const auto cache_interval = std::make_shared(element::i32, PartialShape{-1}); + + OV_EXPECT_THROW(std::ignore = make_op(OutputVector{query, + key, + value, + state, + gate, + beta, + subsequence_begins, + la_block_indices, + la_block_indices_begins, + processed_tokens, + cache_interval}), + NodeValidationFailure, + testing::HasSubstr( + "The value dimension of recurrent_state_table and the head size of value input must be equal")); +} + +TEST_F(TypePropPagedGatedDeltaNetTest, incompatible_inputs_count) { + const auto p = std::make_shared(element::f32, Shape{10, 4, 8}); + OV_EXPECT_THROW(std::ignore = make_op(OutputVector{p, p, p}), + NodeValidationFailure, + testing::HasSubstr("get_input_size() == 11")); +} + +TEST_F(TypePropPagedGatedDeltaNetTest, dynamic_rank) { + // Dynamic rank inputs should be accepted during model import / shape propagation + const auto query = std::make_shared(element::f32, PartialShape::dynamic()); + const auto key = std::make_shared(element::f32, PartialShape::dynamic()); + const auto value = std::make_shared(element::f32, PartialShape::dynamic()); + const auto state = std::make_shared(element::f32, PartialShape::dynamic()); + const auto gate = std::make_shared(element::f32, PartialShape::dynamic()); + const auto beta = std::make_shared(element::f32, PartialShape::dynamic()); + const auto subsequence_begins = std::make_shared(element::i32, PartialShape{-1}); + const auto la_block_indices = std::make_shared(element::i32, PartialShape{-1}); + const auto la_block_indices_begins = std::make_shared(element::i32, PartialShape{-1}); + const auto processed_tokens = std::make_shared(element::i32, PartialShape{-1}); + const auto cache_interval = std::make_shared(element::i32, PartialShape{-1}); + + const auto op = make_op(OutputVector{query, + key, + value, + state, + gate, + beta, + subsequence_begins, + la_block_indices, + la_block_indices_begins, + processed_tokens, + cache_interval}); + + EXPECT_EQ(op->get_output_size(), 1); + EXPECT_EQ(op->get_output_partial_shape(0), PartialShape::dynamic()); +} + +TEST_F(TypePropPagedGatedDeltaNetTest, dynamic_type) { + // Dynamic element type inputs should be accepted during model import / shape propagation + const auto query = std::make_shared(element::dynamic, Shape{10, 4, 8}); + const auto key = std::make_shared(element::dynamic, Shape{10, 4, 8}); + const auto value = std::make_shared(element::dynamic, Shape{10, 4, 16}); + const auto state = std::make_shared(element::dynamic, Shape{5, 4, 16, 8}); + const auto gate = std::make_shared(element::dynamic, Shape{10, 4}); + const auto beta = std::make_shared(element::dynamic, Shape{10, 4}); + const auto subsequence_begins = std::make_shared(element::i32, Shape{3}); + const auto la_block_indices = std::make_shared(element::i32, Shape{5}); + const auto la_block_indices_begins = std::make_shared(element::i32, Shape{3}); + const auto processed_tokens = std::make_shared(element::i32, Shape{2}); + const auto cache_interval = std::make_shared(element::i32, Shape{2}); + + const auto op = make_op(OutputVector{query, + key, + value, + state, + gate, + beta, + subsequence_begins, + la_block_indices, + la_block_indices_begins, + processed_tokens, + cache_interval}); + + EXPECT_EQ(op->get_output_size(), 1); + EXPECT_EQ(op->get_output_element_type(0), element::dynamic); + EXPECT_EQ(op->get_output_partial_shape(0), (PartialShape{10, 4, 16})); +} + +TEST_F(TypePropPagedGatedDeltaNetTest, dynamic_and_concrete_float_type_merged) { + const auto query = std::make_shared(element::dynamic, Shape{10, 4, 8}); + const auto key = std::make_shared(element::f32, Shape{10, 4, 8}); + const auto value = std::make_shared(element::f32, Shape{10, 4, 16}); + const auto state = std::make_shared(element::f32, Shape{5, 4, 16, 8}); + const auto gate = std::make_shared(element::f32, Shape{10, 4}); + const auto beta = std::make_shared(element::f32, Shape{10, 4}); + const auto subsequence_begins = std::make_shared(element::i32, Shape{3}); + const auto la_block_indices = std::make_shared(element::i32, Shape{5}); + const auto la_block_indices_begins = std::make_shared(element::i32, Shape{3}); + const auto processed_tokens = std::make_shared(element::i32, Shape{2}); + const auto cache_interval = std::make_shared(element::i32, Shape{2}); + + const auto op = make_op(OutputVector{query, + key, + value, + state, + gate, + beta, + subsequence_begins, + la_block_indices, + la_block_indices_begins, + processed_tokens, + cache_interval}); + + EXPECT_EQ(op->get_output_size(), 1); + EXPECT_EQ(op->get_output_element_type(0), element::f32); + EXPECT_EQ(op->get_output_partial_shape(0), (PartialShape{10, 4, 16})); +} + +TEST_F(TypePropPagedGatedDeltaNetTest, overlapping_dim_ranges_compatible) { + // Overlapping (not identical) ranges are compatible: query num_heads {2,6}, key num_heads {4,8} + const auto query = std::make_shared(element::f32, PartialShape{-1, {2, 6}, 8}); + const auto key = std::make_shared(element::f32, PartialShape{-1, {4, 8}, 8}); + const auto value = std::make_shared(element::f32, PartialShape{-1, {4, 6}, {16, 32}}); + const auto state = std::make_shared(element::f32, PartialShape{-1, {4, 6}, {16, 32}, 8}); + const auto gate = std::make_shared(element::f32, PartialShape{-1, {4, 6}}); + const auto beta = std::make_shared(element::f32, PartialShape{-1, {4, 6}}); + const auto subsequence_begins = std::make_shared(element::i32, PartialShape{-1}); + const auto la_block_indices = std::make_shared(element::i32, PartialShape{-1}); + const auto la_block_indices_begins = std::make_shared(element::i32, PartialShape{-1}); + const auto processed_tokens = std::make_shared(element::i32, PartialShape{-1}); + const auto cache_interval = std::make_shared(element::i32, PartialShape{-1}); + + const auto op = make_op(OutputVector{query, + key, + value, + state, + gate, + beta, + subsequence_begins, + la_block_indices, + la_block_indices_begins, + processed_tokens, + cache_interval}); + + EXPECT_EQ(op->get_output_element_type(0), element::f32); + EXPECT_EQ(op->get_output_partial_shape(0), (PartialShape{-1, {4, 6}, {16, 32}})); +} + +TEST_F(TypePropPagedGatedDeltaNetTest, state_float_type_independent_from_main_inputs) { + const auto query = std::make_shared(element::f32, Shape{10, 4, 8}); + const auto key = std::make_shared(element::f32, Shape{10, 4, 8}); + const auto value = std::make_shared(element::f32, Shape{10, 4, 16}); + const auto state = std::make_shared(element::f16, Shape{5, 4, 16, 8}); + const auto gate = std::make_shared(element::f32, Shape{10, 4}); + const auto beta = std::make_shared(element::f32, Shape{10, 4}); + const auto subsequence_begins = std::make_shared(element::i32, Shape{3}); + const auto la_block_indices = std::make_shared(element::i32, Shape{5}); + const auto la_block_indices_begins = std::make_shared(element::i32, Shape{3}); + const auto processed_tokens = std::make_shared(element::i32, Shape{2}); + const auto cache_interval = std::make_shared(element::i32, Shape{2}); + + const auto op = make_op(OutputVector{query, + key, + value, + state, + gate, + beta, + subsequence_begins, + la_block_indices, + la_block_indices_begins, + processed_tokens, + cache_interval}); + + EXPECT_EQ(op->get_output_size(), 1); + EXPECT_EQ(op->get_output_element_type(0), element::f32); + EXPECT_EQ(op->get_output_partial_shape(0), PartialShape(Shape{10, 4, 16})); +} + +TEST_F(TypePropPagedGatedDeltaNetTest, float_type_mismatch_among_query_key_value_gate_beta) { + const auto query = std::make_shared(element::f32, Shape{10, 4, 8}); + const auto key = std::make_shared(element::f16, Shape{10, 4, 8}); + const auto value = std::make_shared(element::f32, Shape{10, 4, 16}); + const auto state = std::make_shared(element::f32, Shape{5, 4, 16, 8}); + const auto gate = std::make_shared(element::f32, Shape{10, 4}); + const auto beta = std::make_shared(element::f32, Shape{10, 4}); + const auto subsequence_begins = std::make_shared(element::i32, Shape{3}); + const auto la_block_indices = std::make_shared(element::i32, Shape{5}); + const auto la_block_indices_begins = std::make_shared(element::i32, Shape{3}); + const auto processed_tokens = std::make_shared(element::i32, Shape{2}); + const auto cache_interval = std::make_shared(element::i32, Shape{2}); + + OV_EXPECT_THROW(std::ignore = make_op(OutputVector{query, + key, + value, + state, + gate, + beta, + subsequence_begins, + la_block_indices, + la_block_indices_begins, + processed_tokens, + cache_interval}), + NodeValidationFailure, + testing::HasSubstr("PagedGatedDeltaNet expects query, key, value, gate, and beta to have the " + "same element type.")); +} + +TEST_F(TypePropPagedGatedDeltaNetTest, i64_integer_inputs) { + const auto query = std::make_shared(element::f32, Shape{10, 4, 8}); + const auto key = std::make_shared(element::f32, Shape{10, 4, 8}); + const auto value = std::make_shared(element::f32, Shape{10, 4, 16}); + const auto state = std::make_shared(element::f32, Shape{5, 4, 16, 8}); + const auto gate = std::make_shared(element::f32, Shape{10, 4}); + const auto beta = std::make_shared(element::f32, Shape{10, 4}); + const auto subsequence_begins = std::make_shared(element::i64, Shape{3}); + const auto la_block_indices = std::make_shared(element::i64, Shape{5}); + const auto la_block_indices_begins = std::make_shared(element::i64, Shape{3}); + const auto processed_tokens = std::make_shared(element::i64, Shape{2}); + const auto cache_interval = std::make_shared(element::i64, Shape{2}); + + const auto op = make_op(OutputVector{query, + key, + value, + state, + gate, + beta, + subsequence_begins, + la_block_indices, + la_block_indices_begins, + processed_tokens, + cache_interval}); + + EXPECT_EQ(op->get_output_size(), 1); + EXPECT_EQ(op->get_output_element_type(0), element::f32); + EXPECT_EQ(op->get_output_partial_shape(0), PartialShape(Shape{10, 4, 16})); +} + +TEST_F(TypePropPagedGatedDeltaNetTest, subsequence_begins_incompatible_rank) { + const auto query = std::make_shared(element::f32, Shape{10, 4, 8}); + const auto key = std::make_shared(element::f32, Shape{10, 4, 8}); + const auto value = std::make_shared(element::f32, Shape{10, 4, 16}); + const auto state = std::make_shared(element::f32, Shape{5, 4, 16, 8}); + const auto gate = std::make_shared(element::f32, Shape{10, 4}); + const auto beta = std::make_shared(element::f32, Shape{10, 4}); + const auto subsequence_begins = std::make_shared(element::i32, Shape{2, 2}); // rank 2 - invalid + const auto la_block_indices = std::make_shared(element::i32, Shape{5}); + const auto la_block_indices_begins = std::make_shared(element::i32, Shape{3}); + const auto processed_tokens = std::make_shared(element::i32, Shape{2}); + const auto cache_interval = std::make_shared(element::i32, Shape{2}); + + OV_EXPECT_THROW(std::ignore = make_op(OutputVector{query, + key, + value, + state, + gate, + beta, + subsequence_begins, + la_block_indices, + la_block_indices_begins, + processed_tokens, + cache_interval}), + NodeValidationFailure, + testing::HasSubstr("input_shapes[6].rank().compatible(1)")); +} + +TEST_F(TypePropPagedGatedDeltaNetTest, subsequence_begins_incompatible_type) { + const auto query = std::make_shared(element::f32, Shape{10, 4, 8}); + const auto key = std::make_shared(element::f32, Shape{10, 4, 8}); + const auto value = std::make_shared(element::f32, Shape{10, 4, 16}); + const auto state = std::make_shared(element::f32, Shape{5, 4, 16, 8}); + const auto gate = std::make_shared(element::f32, Shape{10, 4}); + const auto beta = std::make_shared(element::f32, Shape{10, 4}); + const auto subsequence_begins = std::make_shared(element::i16, Shape{3}); // i16 - invalid + const auto la_block_indices = std::make_shared(element::i32, Shape{5}); + const auto la_block_indices_begins = std::make_shared(element::i32, Shape{3}); + const auto processed_tokens = std::make_shared(element::i32, Shape{2}); + const auto cache_interval = std::make_shared(element::i32, Shape{2}); + + OV_EXPECT_THROW(std::ignore = make_op(OutputVector{query, + key, + value, + state, + gate, + beta, + subsequence_begins, + la_block_indices, + la_block_indices_begins, + processed_tokens, + cache_interval}), + NodeValidationFailure, + testing::HasSubstr("Integer inputs must have i32 or i64 element type")); +} + +TEST_F(TypePropPagedGatedDeltaNetTest, q_l2norm_eps_incompatible_negative) { + const auto query = std::make_shared(element::f32, Shape{10, 4, 8}); + const auto key = std::make_shared(element::f32, Shape{10, 4, 8}); + const auto value = std::make_shared(element::f32, Shape{10, 4, 16}); + const auto state = std::make_shared(element::f32, Shape{5, 4, 16, 8}); + const auto gate = std::make_shared(element::f32, Shape{10, 4}); + const auto beta = std::make_shared(element::f32, Shape{10, 4}); + const auto subsequence_begins = std::make_shared(element::i32, Shape{3}); + const auto la_block_indices = std::make_shared(element::i32, Shape{5}); + const auto la_block_indices_begins = std::make_shared(element::i32, Shape{3}); + const auto processed_tokens = std::make_shared(element::i32, Shape{2}); + const auto cache_interval = std::make_shared(element::i32, Shape{2}); + + OV_EXPECT_THROW(std::ignore = make_op(OutputVector{query, + key, + value, + state, + gate, + beta, + subsequence_begins, + la_block_indices, + la_block_indices_begins, + processed_tokens, + cache_interval}, + false, + -1.0f, + 1e-6f), + NodeValidationFailure, + testing::HasSubstr("Attribute 'q_l2_norm_eps' must be a positive floating-point number")); +} + +TEST_F(TypePropPagedGatedDeltaNetTest, q_l2norm_eps_incompatible_zero) { + const auto query = std::make_shared(element::f32, Shape{10, 4, 8}); + const auto key = std::make_shared(element::f32, Shape{10, 4, 8}); + const auto value = std::make_shared(element::f32, Shape{10, 4, 16}); + const auto state = std::make_shared(element::f32, Shape{5, 4, 16, 8}); + const auto gate = std::make_shared(element::f32, Shape{10, 4}); + const auto beta = std::make_shared(element::f32, Shape{10, 4}); + const auto subsequence_begins = std::make_shared(element::i32, Shape{3}); + const auto la_block_indices = std::make_shared(element::i32, Shape{5}); + const auto la_block_indices_begins = std::make_shared(element::i32, Shape{3}); + const auto processed_tokens = std::make_shared(element::i32, Shape{2}); + const auto cache_interval = std::make_shared(element::i32, Shape{2}); + + OV_EXPECT_THROW(std::ignore = make_op(OutputVector{query, + key, + value, + state, + gate, + beta, + subsequence_begins, + la_block_indices, + la_block_indices_begins, + processed_tokens, + cache_interval}, + false, + 0.0f, + 1e-6f), + NodeValidationFailure, + testing::HasSubstr("Attribute 'q_l2_norm_eps' must be a positive floating-point number")); +} + +TEST_F(TypePropPagedGatedDeltaNetTest, k_l2norm_eps_incompatible_zero) { + const auto query = std::make_shared(element::f32, Shape{10, 4, 8}); + const auto key = std::make_shared(element::f32, Shape{10, 4, 8}); + const auto value = std::make_shared(element::f32, Shape{10, 4, 16}); + const auto state = std::make_shared(element::f32, Shape{5, 4, 16, 8}); + const auto gate = std::make_shared(element::f32, Shape{10, 4}); + const auto beta = std::make_shared(element::f32, Shape{10, 4}); + const auto subsequence_begins = std::make_shared(element::i32, Shape{3}); + const auto la_block_indices = std::make_shared(element::i32, Shape{5}); + const auto la_block_indices_begins = std::make_shared(element::i32, Shape{3}); + const auto processed_tokens = std::make_shared(element::i32, Shape{2}); + const auto cache_interval = std::make_shared(element::i32, Shape{2}); + + OV_EXPECT_THROW(std::ignore = make_op(OutputVector{query, + key, + value, + state, + gate, + beta, + subsequence_begins, + la_block_indices, + la_block_indices_begins, + processed_tokens, + cache_interval}, + false, + 1e-6f, + 0.0f), + NodeValidationFailure, + testing::HasSubstr("Attribute 'k_l2_norm_eps' must be a positive floating-point number")); +} + +TEST_F(TypePropPagedGatedDeltaNetTest, gqa_static_shapes) { + // GQA: 2 q/k heads, 8 v heads (4 groups) + const auto query = std::make_shared(element::f32, Shape{10, 2, 64}); + const auto key = std::make_shared(element::f32, Shape{10, 2, 64}); + const auto value = std::make_shared(element::f32, Shape{10, 8, 32}); + const auto state = std::make_shared(element::f32, Shape{5, 8, 32, 64}); + const auto gate = std::make_shared(element::f32, Shape{10, 8}); + const auto beta = std::make_shared(element::f32, Shape{10, 8}); + const auto subsequence_begins = std::make_shared(element::i32, Shape{3}); + const auto la_block_indices = std::make_shared(element::i32, Shape{5}); + const auto la_block_indices_begins = std::make_shared(element::i32, Shape{3}); + const auto processed_tokens = std::make_shared(element::i32, Shape{2}); + const auto cache_interval = std::make_shared(element::i32, Shape{2}); + + const auto op = make_op(OutputVector{query, + key, + value, + state, + gate, + beta, + subsequence_begins, + la_block_indices, + la_block_indices_begins, + processed_tokens, + cache_interval}); + + EXPECT_EQ(op->get_output_size(), 1); + EXPECT_EQ(op->get_output_element_type(0), element::f32); + EXPECT_EQ(op->get_output_partial_shape(0), PartialShape(Shape{10, 8, 32})); +} + +TEST_F(TypePropPagedGatedDeltaNetTest, gqa_partial_shapes) { + // GQA with partial shapes: q/k heads {1,4}, v heads {4,16} + const auto query = std::make_shared(element::f32, PartialShape{-1, {1, 4}, 64}); + const auto key = std::make_shared(element::f32, PartialShape{-1, {1, 4}, 64}); + const auto value = std::make_shared(element::f32, PartialShape{-1, {4, 16}, {32, 128}}); + const auto state = std::make_shared(element::f32, PartialShape{-1, {4, 16}, {32, 128}, 64}); + const auto gate = std::make_shared(element::f32, PartialShape{-1, {4, 16}}); + const auto beta = std::make_shared(element::f32, PartialShape{-1, {4, 16}}); + const auto subsequence_begins = std::make_shared(element::i32, PartialShape{-1}); + const auto la_block_indices = std::make_shared(element::i32, PartialShape{-1}); + const auto la_block_indices_begins = std::make_shared(element::i32, PartialShape{-1}); + const auto processed_tokens = std::make_shared(element::i32, PartialShape{-1}); + const auto cache_interval = std::make_shared(element::i32, PartialShape{-1}); + + const auto op = make_op(OutputVector{query, + key, + value, + state, + gate, + beta, + subsequence_begins, + la_block_indices, + la_block_indices_begins, + processed_tokens, + cache_interval}); + + EXPECT_EQ(op->get_output_size(), 1); + EXPECT_EQ(op->get_output_element_type(0), element::f32); + EXPECT_EQ(op->get_output_partial_shape(0), (PartialShape{-1, {4, 16}, {32, 128}})); +} +} // namespace ov::test diff --git a/src/core/tests/type_prop/reshape.cpp b/src/core/tests/type_prop/reshape.cpp index fed1dcda7204..6e0b0956501a 100644 --- a/src/core/tests/type_prop/reshape.cpp +++ b/src/core/tests/type_prop/reshape.cpp @@ -14,17 +14,17 @@ #include "openvino/op/gather.hpp" #include "openvino/op/multiply.hpp" #include "openvino/op/reduce_prod.hpp" +#include "openvino/op/reduce_sum.hpp" #include "openvino/op/shape_of.hpp" +#include "openvino/op/softmax.hpp" #include "openvino/op/squeeze.hpp" #include "openvino/op/subtract.hpp" #include "openvino/op/unsqueeze.hpp" -using namespace ov; -using std::ignore; -using std::make_shared; -using testing::Each; -using testing::ElementsAre; -using testing::HasSubstr; +namespace ov::test { +using ov::op::v0::Constant, ov::op::v0::Parameter; +using std::ignore, std::make_shared; +using testing::Each, testing::ElementsAre, testing::HasSubstr; TEST(type_prop, static_value_propagation) { auto param = make_shared(element::f32, Shape{1, 2, 3}); @@ -1353,4 +1353,65 @@ TEST(type_prop, reshape_symbol_deducing) { EXPECT_EQ(reshape->get_output_partial_shape(0), ov::PartialShape({-1, -1, 12, 64})); EXPECT_TRUE(ov::symbol::are_equal(B, C)); -} \ No newline at end of file +} + +// Simulate ONNX softmax converted model as a flatten->Softmax->unflatten(ShapeOf) sequence. +// When this is followed by a Reshape back to 4D (using dims extracted from Shape(x)) and then two consecutive +// ReduceSum(axis=-1, keepdims=false) ops, the final Reshape to [N, C, 1, 1] must infer the correct shape +// without raising an error. +TEST(type_prop, reshape_after_softmax_flatten_double_reduce_sum) { + using ov::op::v0::Unsqueeze, op::v1::Gather; + + // Input: [1, 2, 4, 4] (batch=1, channels=2, H=4, W=4) + const auto input = std::make_shared(element::f32, PartialShape{1, 2, 4, 4}); + + // Dynamically extract each spatial dimension via ShapeOf + Gather + const auto shape_of = std::make_shared(input); + const auto gather_axis = Constant::create(element::i64, {}, {0}); + const auto dim_0 = std::make_shared(shape_of, Constant::create(element::i64, {}, {0}), gather_axis); + const auto dim_1 = std::make_shared(shape_of, Constant::create(element::i64, {}, {1}), gather_axis); + const auto dim_2 = std::make_shared(shape_of, Constant::create(element::i64, {}, {2}), gather_axis); + const auto dim_3 = std::make_shared(shape_of, Constant::create(element::i64, {}, {3}), gather_axis); + + // Compute H*W for the flat reshape + const auto hw = std::make_shared(dim_2, dim_3); + + // Unsqueeze scalars to 1-element vectors for Concat + const auto unsq_axis = Constant::create(element::i64, {1}, {0}); + const auto dim_0_1d = std::make_shared(dim_0, unsq_axis); + const auto dim_1_1d = std::make_shared(dim_1, unsq_axis); + const auto dim_2_1d = std::make_shared(dim_2, unsq_axis); + const auto dim_3_1d = std::make_shared(dim_3, unsq_axis); + const auto hw_1d = std::make_shared(hw, unsq_axis); + + // Reshape: [1, 2, 4, 4] -> [1, 2, H*W] (flat for Softmax) + const auto flat_shape = std::make_shared(OutputVector{dim_0_1d, dim_1_1d, hw_1d}, 0); + const auto flat = std::make_shared(input, flat_shape, false); + + // ONNX opset9 Softmax(axis=2): OV frontend converts this to: + // flatten(flat, axis=2) -> Softmax -> Reshape(result, ShapeOf(flat)) + // For shape-inference purposes the ShapeOf captures the pre-Softmax shape. + const auto shape_of_flat = std::make_shared(flat); + const auto sm = std::make_shared(flat, 2); + const auto sm_restored = std::make_shared(sm, shape_of_flat, false); + + // restore from [1, 2, H*W] back to [1, 2, H, W] + const auto orig_shape = std::make_shared(OutputVector{dim_0_1d, dim_1_1d, dim_2_1d, dim_3_1d}, 0); + const auto sm_4d = std::make_shared(sm_restored, orig_shape, false); + + // Two consecutive ReduceSum(axis=-1, keepdims=false): + // [1, 2, 4, 4] -> [1, 2, 4] -> [1, 2] + const auto reduce_axes1 = Constant::create(element::i64, {1}, {-1}); + const auto reduce_axes2 = Constant::create(element::i64, {1}, {-1}); + const auto reduce1 = std::make_shared(sm_4d, reduce_axes1, false); + const auto reduce2 = std::make_shared(reduce1, reduce_axes2, false); + + // Final Reshape: [1, 2] -> [1, 2, 1, 1] using shape built from ShapeOf(input) + const auto const_1 = Constant::create(element::i64, {1}, {1}); + const auto out_shape = std::make_shared(OutputVector{dim_0_1d, dim_1_1d, const_1, const_1}, 0); + const auto result = std::make_shared(reduce2, out_shape, false); + + ASSERT_EQ(result->get_element_type(), element::f32); + ASSERT_EQ(result->get_output_partial_shape(0), (PartialShape{1, 2, 1, 1})); +} +} // namespace ov::test diff --git a/src/core/tests/type_prop/transpose.cpp b/src/core/tests/type_prop/transpose.cpp index 93e004d90dd2..2a7113704a43 100644 --- a/src/core/tests/type_prop/transpose.cpp +++ b/src/core/tests/type_prop/transpose.cpp @@ -290,7 +290,7 @@ TEST(type_prop, transpose_order_as_parameter_shape) { EXPECT_EQ(r->get_output_partial_shape(v1::Transpose::ARG_T), PartialShape({Dimension(4, 16), 6, Dimension(2, 8)})); } -/** \brief Transpose with order as paramater shape dimensions after multiple transformations. */ +/** \brief Transpose with order as parameter shape dimensions after multiple transformations. */ TEST(type_prop, transpose_order_as_parameter_shape_after_transformation) { const auto arg = make_shared(element::f32, PartialShape{Dimension(2, 8), Dimension(4, 16), 6}); diff --git a/src/core/tests/visitors/op/paged_causal_conv1d.cpp b/src/core/tests/visitors/op/paged_causal_conv1d.cpp new file mode 100644 index 000000000000..309e7504cec5 --- /dev/null +++ b/src/core/tests/visitors/op/paged_causal_conv1d.cpp @@ -0,0 +1,55 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "openvino/op/paged_causal_conv1d.hpp" + +#include + +#include "visitors/visitors.hpp" + +namespace ov::test { + +TEST(attributes, paged_causal_conv1d_roundtrip) { + NodeBuilder::opset().insert(); + + const auto input_embeds = std::make_shared(element::f32, PartialShape{4, 8}); + const auto conv_state_table = std::make_shared(element::f32, PartialShape{2, 8, 4}); + const auto conv_weight = std::make_shared(element::f32, PartialShape{8, 1, 4}); + const auto conv_bias = std::make_shared(element::f32, PartialShape{8}); + const auto subsequence_begins = std::make_shared(element::i32, PartialShape{2}); + const auto la_block_indices = std::make_shared(element::i32, PartialShape{2}); + const auto la_block_indices_begins = std::make_shared(element::i32, PartialShape{2}); + const auto processed_tokens = std::make_shared(element::i32, PartialShape{1}); + const auto cache_interval = std::make_shared(element::i32, PartialShape{1}); + + const auto op = std::make_shared(input_embeds, + conv_state_table, + conv_weight, + conv_bias, + subsequence_begins, + la_block_indices, + la_block_indices_begins, + processed_tokens, + cache_interval); + + NodeBuilder builder(op, + {input_embeds, + conv_state_table, + conv_weight, + conv_bias, + subsequence_begins, + la_block_indices, + la_block_indices_begins, + processed_tokens, + cache_interval}); + auto g_op = as_type_ptr(builder.create()); + + constexpr auto expected_attr_count = 0; + EXPECT_EQ(builder.get_value_map_size(), expected_attr_count); + + EXPECT_EQ(g_op->get_output_element_type(0), op->get_output_element_type(0)); + EXPECT_EQ(g_op->get_output_partial_shape(0), op->get_output_partial_shape(0)); +} + +} // namespace ov::test diff --git a/src/core/tests/visitors/op/paged_gated_delta_net.cpp b/src/core/tests/visitors/op/paged_gated_delta_net.cpp new file mode 100644 index 000000000000..6b252fa65c13 --- /dev/null +++ b/src/core/tests/visitors/op/paged_gated_delta_net.cpp @@ -0,0 +1,120 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "openvino/op/paged_gated_delta_net.hpp" + +#include + +#include "visitors/visitors.hpp" + +namespace ov::test { + +TEST(attributes, paged_gated_delta_net_default_attrs) { + NodeBuilder::opset().insert(); + const auto query = std::make_shared(element::f32, PartialShape{-1, 4, 8}); + const auto key = std::make_shared(element::f32, PartialShape{-1, 4, 8}); + const auto value = std::make_shared(element::f32, PartialShape{-1, 4, 16}); + const auto state = std::make_shared(element::f32, PartialShape{-1, 4, 16, 8}); + const auto gate = std::make_shared(element::f32, PartialShape{-1, 4}); + const auto beta = std::make_shared(element::f32, PartialShape{-1, 4}); + const auto subsequence_begins = std::make_shared(element::i32, PartialShape{-1}); + const auto la_block_indices = std::make_shared(element::i32, PartialShape{-1}); + const auto la_block_indices_begins = std::make_shared(element::i32, PartialShape{-1}); + const auto processed_tokens = std::make_shared(element::i32, PartialShape{-1}); + const auto cache_interval = std::make_shared(element::i32, PartialShape{-1}); + + const auto op = std::make_shared(OutputVector{query, + key, + value, + state, + gate, + beta, + subsequence_begins, + la_block_indices, + la_block_indices_begins, + processed_tokens, + cache_interval}); + + NodeBuilder builder(op, + {query, + key, + value, + state, + gate, + beta, + subsequence_begins, + la_block_indices, + la_block_indices_begins, + processed_tokens, + cache_interval}); + auto g_op = as_type_ptr(builder.create()); + + constexpr auto expected_attr_count = 3; + EXPECT_EQ(builder.get_value_map_size(), expected_attr_count); + + EXPECT_FALSE(op->get_use_qk_l2norm()); + EXPECT_FLOAT_EQ(op->get_q_l2_norm_eps(), 1e-6f); + EXPECT_FLOAT_EQ(op->get_k_l2_norm_eps(), 1e-6f); + + EXPECT_EQ(g_op->get_use_qk_l2norm(), op->get_use_qk_l2norm()); + EXPECT_FLOAT_EQ(g_op->get_q_l2_norm_eps(), op->get_q_l2_norm_eps()); + EXPECT_FLOAT_EQ(g_op->get_k_l2_norm_eps(), op->get_k_l2_norm_eps()); +} + +TEST(attributes, paged_gated_delta_net_non_default_attrs) { + NodeBuilder::opset().insert(); + const auto query = std::make_shared(element::f32, PartialShape{-1, 4, 8}); + const auto key = std::make_shared(element::f32, PartialShape{-1, 4, 8}); + const auto value = std::make_shared(element::f32, PartialShape{-1, 4, 16}); + const auto state = std::make_shared(element::f32, PartialShape{-1, 4, 16, 8}); + const auto gate = std::make_shared(element::f32, PartialShape{-1, 4}); + const auto beta = std::make_shared(element::f32, PartialShape{-1, 4}); + const auto subsequence_begins = std::make_shared(element::i32, PartialShape{-1}); + const auto la_block_indices = std::make_shared(element::i32, PartialShape{-1}); + const auto la_block_indices_begins = std::make_shared(element::i32, PartialShape{-1}); + const auto processed_tokens = std::make_shared(element::i32, PartialShape{-1}); + const auto cache_interval = std::make_shared(element::i32, PartialShape{-1}); + + const auto op = std::make_shared(OutputVector{query, + key, + value, + state, + gate, + beta, + subsequence_begins, + la_block_indices, + la_block_indices_begins, + processed_tokens, + cache_interval}, + true, // use_qk_l2norm + 1e-3f, // q_l2_norm_eps + 2e-3f); // k_l2_norm_eps + + NodeBuilder builder(op, + {query, + key, + value, + state, + gate, + beta, + subsequence_begins, + la_block_indices, + la_block_indices_begins, + processed_tokens, + cache_interval}); + auto g_op = as_type_ptr(builder.create()); + + constexpr auto expected_attr_count = 3; + EXPECT_EQ(builder.get_value_map_size(), expected_attr_count); + + EXPECT_TRUE(op->get_use_qk_l2norm()); + EXPECT_FLOAT_EQ(op->get_q_l2_norm_eps(), 1e-3f); + EXPECT_FLOAT_EQ(op->get_k_l2_norm_eps(), 2e-3f); + + EXPECT_EQ(g_op->get_use_qk_l2norm(), op->get_use_qk_l2norm()); + EXPECT_FLOAT_EQ(g_op->get_q_l2_norm_eps(), op->get_q_l2_norm_eps()); + EXPECT_FLOAT_EQ(g_op->get_k_l2_norm_eps(), op->get_k_l2_norm_eps()); +} + +} // namespace ov::test diff --git a/src/core/tests/weight_sharing_util_test.cpp b/src/core/tests/weight_sharing_util_test.cpp index deaf456195ca..008aed8ae873 100644 --- a/src/core/tests/weight_sharing_util_test.cpp +++ b/src/core/tests/weight_sharing_util_test.cpp @@ -238,15 +238,15 @@ TEST_F(WeightShareExtensionTest, set_constant_buffer_with_id) { auto buffer = std::make_shared(4000); auto wt_buffer = std::make_shared>>( buffer->get_ptr() + 100, - buffer->size(), + buffer->size() - 100, buffer, ov::create_base_descriptor(12, 0, buffer)); - auto c = Constant(element::f32, Shape{1000}, wt_buffer); + auto c = Constant(element::f32, Shape{975}, wt_buffer); ASSERT_TRUE(weight_sharing::set_constant(shared_ctx, c)); const auto& [const_offset, const_size, const_type] = shared_ctx.m_weight_registry[12][100]; EXPECT_EQ(const_offset, 100); - EXPECT_EQ(const_size, 4000); + EXPECT_EQ(const_size, 4000 - 100); EXPECT_EQ(const_type, element::f32); } diff --git a/src/core/tests/xml_util/custom_ir.cpp b/src/core/tests/xml_util/custom_ir.cpp index e1ab70f75fc1..cffc4da46a0a 100644 --- a/src/core/tests/xml_util/custom_ir.cpp +++ b/src/core/tests/xml_util/custom_ir.cpp @@ -423,4 +423,24 @@ TEST_F(CustomIRTest, modified_serialization_deserialization) { const auto& [is_valid, error_msg] = model_comparator().compare(ov_model, drv_model); EXPECT_TRUE(is_valid) << error_msg; } + +TEST(StrToContainer, FloatVectorWithInfAndNan) { + { + std::vector result; + ov::util::str_to_container("1.5,inf,-inf,nan,3.0", result); + ASSERT_EQ(result.size(), 5); + EXPECT_FLOAT_EQ(result[0], 1.5f); + EXPECT_TRUE(std::isinf(result[1]) && result[1] > 0); + EXPECT_TRUE(std::isinf(result[2]) && result[2] < 0); + EXPECT_TRUE(std::isnan(result[3])); + EXPECT_FLOAT_EQ(result[4], 3.0f); + } + { + std::vector result; + ov::util::str_to_container("-inf,inf", result); + ASSERT_EQ(result.size(), 2); + EXPECT_TRUE(std::isinf(result[0]) && result[0] < 0); + EXPECT_TRUE(std::isinf(result[1]) && result[1] > 0); + } +} } // namespace ov::test diff --git a/src/core/xml_util/CMakeLists.txt b/src/core/xml_util/CMakeLists.txt index a4bf6a3362bb..1995ffe2b8f2 100644 --- a/src/core/xml_util/CMakeLists.txt +++ b/src/core/xml_util/CMakeLists.txt @@ -11,7 +11,6 @@ file(GLOB_RECURSE PUBLIC_HEADERS ${TARGET_INCLUDE_DIR}/*.hpp) # Create named folders for the sources within the .vcproj # Empty name lists them directly under the .vcproj - source_group("src" FILES ${LIBRARY_SRC}) source_group("include" FILES ${PUBLIC_HEADERS}) @@ -27,6 +26,7 @@ target_include_directories(${TARGET_NAME} PUBLIC ov_add_clang_format_target(${TARGET_NAME}_clang FOR_TARGETS ${TARGET_NAME}) target_link_libraries(${TARGET_NAME} PRIVATE openvino::runtime) + # LTO set_target_properties(${TARGET_NAME} PROPERTIES INTERPROCEDURAL_OPTIMIZATION_RELEASE ${ENABLE_LTO}) @@ -37,8 +37,7 @@ endif() ov_build_target_faster(${TARGET_NAME} PCH) # developer package - ov_install_static_lib(${TARGET_NAME} ${OV_CPACK_COMP_CORE}) ov_developer_package_export_targets(TARGET ${TARGET_NAME} - INSTALL_INCLUDE_DIRECTORIES "${TARGET_INCLUDE_DIR}/") + INSTALL_INCLUDE_DIRECTORIES "${TARGET_INCLUDE_DIR}/") diff --git a/src/core/xml_util/include/openvino/xml_util/xml_deserialize_util.hpp b/src/core/xml_util/include/openvino/xml_util/xml_deserialize_util.hpp index 7b841176c087..72bbf9a3f9f6 100644 --- a/src/core/xml_util/include/openvino/xml_util/xml_deserialize_util.hpp +++ b/src/core/xml_util/include/openvino/xml_util/xml_deserialize_util.hpp @@ -17,6 +17,7 @@ #include "openvino/op/util/multi_subgraph_base.hpp" #include "openvino/opsets/opset.hpp" #include "openvino/runtime/aligned_buffer.hpp" +#include "openvino/util/common_util.hpp" namespace ov::util { struct GenericLayerParams; @@ -28,9 +29,15 @@ void str_to_container(const std::string& value, T& res) { while (getline(ss, field, ',')) { if (field.empty()) OPENVINO_THROW("Cannot get vector of parameters! \"", value, "\" is incorrect"); - std::stringstream fs(field); typename T::value_type val; - fs >> val; + if constexpr (std::is_arithmetic_v) { + auto parsed = ov::util::view_to_number(ov::util::trim(field)); + OPENVINO_ASSERT(parsed.has_value(), "Cannot parse '", field, "' in \"", value, "\""); + val = *parsed; + } else { + std::stringstream fs(field); + fs >> val; + } res.insert(res.end(), val); } } diff --git a/src/core/xml_util/src/xml_deserialize_util.cpp b/src/core/xml_util/src/xml_deserialize_util.cpp index 474e395c6063..facfe5c3005b 100644 --- a/src/core/xml_util/src/xml_deserialize_util.cpp +++ b/src/core/xml_util/src/xml_deserialize_util.cpp @@ -65,12 +65,9 @@ bool getParameters(const pugi::xml_node& node, const std::string& name, std::vec template T stringToType(const std::string& valStr) { - T ret{0}; - std::istringstream ss(valStr); - if (!ss.eof()) { - ss >> ret; - } - return ret; + auto result = ov::util::view_to_number(ov::util::trim(valStr)); + OPENVINO_ASSERT(result.has_value(), "Cannot parse '", valStr, "' to number"); + return *result; } bool get_partial_shape_from_attribute(const pugi::xml_node& node, const std::string& name, PartialShape& value) { @@ -81,14 +78,6 @@ bool get_partial_shape_from_attribute(const pugi::xml_node& node, const std::str return true; } -bool get_dimension_from_attribute(const pugi::xml_node& node, const std::string& name, Dimension& value) { - std::string param; - if (!getStrAttribute(node, name, param)) - return false; - value = Dimension(param); - return true; -} - void str_to_set_of_strings(const std::string& value, std::set& res) { std::stringstream ss(value); std::string field; @@ -750,10 +739,9 @@ void XmlDeserializer::on_adapter(const std::string& name, ov::ValueAccessorset(shape); } else if (auto a = ov::as_type>(&adapter)) { - Dimension dim; - if (!get_dimension_from_attribute(m_node.child("data"), name, dim)) - return; - a->set(dim); + if (const auto dim_str = util::pugixml::get_attribute_view(m_node.child("data"), name); dim_str.has_value()) { + a->set(Dimension(*dim_str)); + } } else if (auto a = ov::as_type>(&adapter)) { std::vector shape; if (!getParameters(m_node.child("data"), name, shape)) diff --git a/src/frontends/common/include/openvino/frontend/sequence_at.hpp b/src/frontends/common/include/openvino/frontend/sequence_at.hpp new file mode 100644 index 000000000000..9a20d4f4e37e --- /dev/null +++ b/src/frontends/common/include/openvino/frontend/sequence_at.hpp @@ -0,0 +1,43 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#pragma once + +#include "openvino/frontend/visibility.hpp" +#include "openvino/op/util/framework_node.hpp" + +namespace ov { +namespace frontend { + +/// \brief SequenceAt is a helper operation that represents extracting a single element +/// from a sequence at a given position. It is emitted by frontends when the sequence +/// input is not yet resolvable to a SequenceMark (e.g., comes from an If/Loop output) +/// and is later resolved by SequenceIfReplacer or similar passes. +/// +/// Inputs: +/// - input_sequence: The input sequence (typically the output of an If/Loop whose +/// body produces a SequenceMark/SequenceInsert chain). +/// - position: The position (scalar tensor) of the element to extract. +/// +/// Output: +/// - The tensor at the requested position. +class FRONTEND_API SequenceAt : public ov::op::util::FrameworkNode { +public: + OPENVINO_OP("SequenceAt", "util", ov::op::util::FrameworkNode); + + SequenceAt(const Output& input_sequence, const Output& position); + + std::shared_ptr clone_with_new_inputs(const OutputVector& inputs) const override; + + Output get_input_sequence() const { + return input_value(0); + } + + Output get_position() const { + return input_value(1); + } +}; + +} // namespace frontend +} // namespace ov diff --git a/src/frontends/common/include/openvino/frontend/sequence_erase.hpp b/src/frontends/common/include/openvino/frontend/sequence_erase.hpp new file mode 100644 index 000000000000..d0e5e244d2f8 --- /dev/null +++ b/src/frontends/common/include/openvino/frontend/sequence_erase.hpp @@ -0,0 +1,48 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#pragma once + +#include "openvino/frontend/visibility.hpp" +#include "openvino/op/util/framework_node.hpp" + +namespace ov { +namespace frontend { + +/// \brief SequenceErase is a helper operation that represents removing one element +/// from a sequence. Emitted by frontends when the sequence input is not yet +/// resolvable to a SequenceMark. +/// +/// Inputs: +/// - input_sequence: The input sequence. +/// - position (optional): The position of the element to remove. When omitted, +/// the last element is removed (per the ONNX SequenceErase spec). +/// +/// Output: +/// - The sequence with the element removed. +class FRONTEND_API SequenceErase : public ov::op::util::FrameworkNode { +public: + OPENVINO_OP("SequenceErase", "util", ov::op::util::FrameworkNode); + + explicit SequenceErase(const Output& input_sequence); + SequenceErase(const Output& input_sequence, const Output& position); + + std::shared_ptr clone_with_new_inputs(const OutputVector& inputs) const override; + + Output get_input_sequence() const { + return input_value(0); + } + + bool has_position() const { + return get_input_size() > 1; + } + + Output get_position() const { + OPENVINO_ASSERT(has_position(), "SequenceErase: no position input; check has_position() first"); + return input_value(1); + } +}; + +} // namespace frontend +} // namespace ov diff --git a/src/frontends/common/include/openvino/frontend/sequence_length.hpp b/src/frontends/common/include/openvino/frontend/sequence_length.hpp new file mode 100644 index 000000000000..beb50f53b77a --- /dev/null +++ b/src/frontends/common/include/openvino/frontend/sequence_length.hpp @@ -0,0 +1,37 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#pragma once + +#include "openvino/frontend/visibility.hpp" +#include "openvino/op/util/framework_node.hpp" + +namespace ov { +namespace frontend { + +/// \brief SequenceLength is a helper operation that represents the length of a sequence. +/// Emitted by frontends when the sequence input is not yet resolvable to a SequenceMark. +/// +/// Inputs: +/// - input_sequence: The input sequence. +/// +/// Output: +/// - Scalar int64 tensor with the number of elements in the sequence. +class FRONTEND_API SequenceLength : public ov::op::util::FrameworkNode { +public: + OPENVINO_OP("SequenceLength", "util", ov::op::util::FrameworkNode); + + explicit SequenceLength(const Output& input_sequence); + + void validate_and_infer_types() override; + + std::shared_ptr clone_with_new_inputs(const OutputVector& inputs) const override; + + Output get_input_sequence() const { + return input_value(0); + } +}; + +} // namespace frontend +} // namespace ov diff --git a/src/frontends/common/src/random_normal_helper.cpp b/src/frontends/common/src/random_normal_helper.cpp index 450eff9f4438..c904fb71beb2 100644 --- a/src/frontends/common/src/random_normal_helper.cpp +++ b/src/frontends/common/src/random_normal_helper.cpp @@ -11,7 +11,7 @@ #include "openvino/op/constant.hpp" #include "openvino/opsets/opset12.hpp" #include "openvino/pass/graph_rewrite.hpp" -#include "transformations/rt_info/disable_fp16_compression.hpp" +#include "transformations/rt_info/disable_precision_conversion.hpp" namespace ov { namespace frontend { @@ -32,7 +32,7 @@ OutputVector make_random_normal(pass::NodeRegistry& registry, // uint32 zero value). // Float -0 value will be interpreted as a valid uint32 value. const void* seed_ptr = &seed; // To prevent strict-aliasing error - const uint64_t op_seed = static_cast(*static_cast(seed_ptr)); + const uint64_t op_seed = static_cast(*static_cast(seed_ptr)); // We need to use two op_seeds to make sure we get different results for two RandomUniform series // But we also have to keep original logic and pass "0" (auto-generated seed) to RandomUniform @@ -64,8 +64,8 @@ OutputVector make_random_normal(pass::NodeRegistry& registry, auto sum = registry.make(product, mean); // if we don't disable down-casting then log(float32_min) gives -inf - disable_fp16_compression(uniform_1); - disable_fp16_compression(log); + ov::disable_conversion(uniform_1, element::f16); + ov::disable_conversion(log, element::f16); return {sum}; } diff --git a/src/frontends/common/src/sequence_at.cpp b/src/frontends/common/src/sequence_at.cpp new file mode 100644 index 000000000000..d98163adb343 --- /dev/null +++ b/src/frontends/common/src/sequence_at.cpp @@ -0,0 +1,19 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "openvino/frontend/sequence_at.hpp" + +namespace ov { +namespace frontend { + +SequenceAt::SequenceAt(const Output& input_sequence, const Output& position) + : FrameworkNode({input_sequence, position}, 1) {} + +std::shared_ptr SequenceAt::clone_with_new_inputs(const OutputVector& inputs) const { + OPENVINO_ASSERT(inputs.size() == 2, "SequenceAt requires 2 inputs"); + return std::make_shared(inputs[0], inputs[1]); +} + +} // namespace frontend +} // namespace ov diff --git a/src/frontends/common/src/sequence_erase.cpp b/src/frontends/common/src/sequence_erase.cpp new file mode 100644 index 000000000000..962b1b329025 --- /dev/null +++ b/src/frontends/common/src/sequence_erase.cpp @@ -0,0 +1,25 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "openvino/frontend/sequence_erase.hpp" + +namespace ov { +namespace frontend { + +SequenceErase::SequenceErase(const Output& input_sequence) : FrameworkNode({input_sequence}, 1) {} + +SequenceErase::SequenceErase(const Output& input_sequence, const Output& position) + : FrameworkNode({input_sequence, position}, 1) {} + +std::shared_ptr SequenceErase::clone_with_new_inputs(const OutputVector& inputs) const { + if (inputs.size() == 1) { + return std::make_shared(inputs[0]); + } else if (inputs.size() == 2) { + return std::make_shared(inputs[0], inputs[1]); + } + OPENVINO_THROW("SequenceErase requires 1 or 2 inputs"); +} + +} // namespace frontend +} // namespace ov diff --git a/src/frontends/common/src/sequence_length.cpp b/src/frontends/common/src/sequence_length.cpp new file mode 100644 index 000000000000..014999ee7187 --- /dev/null +++ b/src/frontends/common/src/sequence_length.cpp @@ -0,0 +1,26 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "openvino/frontend/sequence_length.hpp" + +#include "openvino/core/type/element_type.hpp" + +namespace ov { +namespace frontend { + +SequenceLength::SequenceLength(const Output& input_sequence) : FrameworkNode({input_sequence}, 1) { + validate_and_infer_types(); +} + +void SequenceLength::validate_and_infer_types() { + set_output_type(0, ov::element::i64, ov::Shape{}); +} + +std::shared_ptr SequenceLength::clone_with_new_inputs(const OutputVector& inputs) const { + OPENVINO_ASSERT(inputs.size() == 1, "SequenceLength requires 1 input"); + return std::make_shared(inputs[0]); +} + +} // namespace frontend +} // namespace ov diff --git a/src/frontends/common_translators/src/unconverted_ops_report.cpp b/src/frontends/common_translators/src/unconverted_ops_report.cpp index 821dcb95c83c..64b998573599 100644 --- a/src/frontends/common_translators/src/unconverted_ops_report.cpp +++ b/src/frontends/common_translators/src/unconverted_ops_report.cpp @@ -34,14 +34,24 @@ UnconvertedOpsReport collect_unconverted_ops(const std::shared_ptr& m if (!model) { return report; } - for (const auto& node : model->get_ordered_ops()) { - // Try framework-specific extractor first + // Try framework-specific extractor first; fall back to the universal + // FrameworkNode handler for any node the extractor did not claim. if (auto result = extractor(node)) { report.add(result->first, result->second); + } else if (ov::is_type(node)) { + // Any unclaimed FrameworkNode left at the end of conversion — + // typically a frontend helper like SequenceMark / SequenceAt that + // no transformation managed to lower — is an unconverted op too. + const auto& ti = node->get_type_info(); + std::string op_name = ti.version_id ? std::string(ti.version_id) + "::" + ti.name : std::string(ti.name); + report.add(op_name, std::string{}); } - // Handle MultiSubGraphOp (parent of Loop, If, etc.) - common for all frontends + // Handle MultiSubGraphOp instances (Loop / If / TensorIterator, and FrameworkNode + // subclasses such as PtFrameworkNode that hold real body subgraphs) — recurse into + // their body subgraphs. Helper FrameworkNodes without bodies (SequenceAt, SequenceErase, + // etc.) have get_internal_subgraphs_size() == 0 and the loop simply does not execute. if (const auto& subgraph_op = ov::as_type_ptr(node)) { for (size_t i = 0; i < subgraph_op->get_internal_subgraphs_size(); ++i) { report.merge(collect_unconverted_ops(subgraph_op->get_function(i), extractor)); diff --git a/src/frontends/ir/src/frontend.cpp b/src/frontends/ir/src/frontend.cpp index 5337bed0210b..7d9c8c143bb4 100644 --- a/src/frontends/ir/src/frontend.cpp +++ b/src/frontends/ir/src/frontend.cpp @@ -18,6 +18,7 @@ #include "openvino/util/file_util.hpp" #include "openvino/util/mmap_object.hpp" #include "openvino/util/xml_parse_utils.hpp" +#include "transformations/fp16_compression/convert_legacy_precision_attribute.hpp" #include "transformations/resolve_names_collisions.hpp" #include "utils.hpp" @@ -261,6 +262,7 @@ std::string FrontEnd::get_name() const { void FrontEnd::normalize(const std::shared_ptr& model) const { ov::pass::Manager manager("Frontend:IR:normalize"); manager.register_pass(); + manager.register_pass(); manager.run_passes(model); } diff --git a/src/frontends/ir/src/utils.cpp b/src/frontends/ir/src/utils.cpp index 6e740da6df7b..bec1c547f258 100644 --- a/src/frontends/ir/src/utils.cpp +++ b/src/frontends/ir/src/utils.cpp @@ -10,7 +10,8 @@ namespace ov { void operator>>(const std::stringstream& in, ov::element::Type& type) { - type = ov::element::Type(ov::util::trim(in.str())); + const std::string type_name{util::trim(in.str())}; + type = element::Type(type_name); } } // namespace ov diff --git a/src/frontends/onnx/docs/check_supported_ops.py b/src/frontends/onnx/docs/check_supported_ops.py index 2681cbbaedf7..a85ee7421ba9 100644 --- a/src/frontends/onnx/docs/check_supported_ops.py +++ b/src/frontends/onnx/docs/check_supported_ops.py @@ -51,7 +51,8 @@ def run_in_ci(): "OPENVINO_ONNX_DOMAIN":"org.openvinotoolkit", "MICROSOFT_DOMAIN":"com.microsoft", "PYTORCH_ATEN_DOMAIN":"org.pytorch.aten", - "MMDEPLOY_DOMAIN":"mmdeploy" + "MMDEPLOY_DOMAIN":"mmdeploy", + "AIONNX_ML_DOMAIN":"ai.onnx.ml" } hdr = "" diff --git a/src/frontends/onnx/docs/supported_ops.md b/src/frontends/onnx/docs/supported_ops.md index 279a6e722340..7ebd331a0c71 100644 --- a/src/frontends/onnx/docs/supported_ops.md +++ b/src/frontends/onnx/docs/supported_ops.md @@ -36,7 +36,7 @@ OpenVINO provides support for operations of Default Opset (empty in table below) | |Celu |12 |12 | | | |CenterCropPad |18 |18 | | | |Clip |11, 1 |13, 12, 11, 6, 1 | | -| |Col2Im | |18 | | +| |Col2Im |18 |18 | | | |Compress |9 |11, 9 | | | |Concat |1 |13, 11, 4, 1 | | | |ConcatFromSequence |11 |11 |Supported only in certain patterns| @@ -134,6 +134,7 @@ OpenVINO provides support for operations of Default Opset (empty in table below) | |QLinearConv |10 |10 | | | |QLinearMatMul |10 |21, 10 | | | |QuantizeLinear |21, 13, 10 |21, 19, 13, 10 | | +| |RMSNormalization |23 |23 | | | |RNN |1 |22, 14, 7, 1 | | | |RandomNormal |1 |22, 1 | | | |RandomNormalLike |1 |22, 1 | | @@ -143,7 +144,7 @@ OpenVINO provides support for operations of Default Opset (empty in table below) | |Reciprocal |1 |13, 6, 1 | | | |ReduceL1 |18, 13, 1 |18, 13, 11, 1 | | | |ReduceL2 |18, 13, 1 |18, 13, 11, 1 | | -| |ReduceLogSum |18, 1 |18, 13, 11, 1 | | +| |ReduceLogSum |18, 13, 1 |18, 13, 11, 1 | | | |ReduceLogSumExp |18, 13, 1 |18, 13, 11, 1 | | | |ReduceMax |20, 18, 13, 1 |20, 18, 13, 12, 11, 1 | | | |ReduceMean |18, 13, 1 |18, 13, 11, 1 | | @@ -157,19 +158,20 @@ OpenVINO provides support for operations of Default Opset (empty in table below) | |Resize |11, 10 |19, 18, 13, 11, 10 | | | |ReverseSequence |10 |10 | | | |RoiAlign |16, 10 |22, 16, 10 | | +| |RotaryEmbedding |23 |23 | | | |Round |11 |22, 11 | | | |STFT |17 |17 | | | |Scan |9, 8 |21, 19, 16, 11, 9, 8 | | | |Scatter |9 |11, 9 | | | |ScatterElements |11 |18, 16, 13, 11 | | -| |ScatterND |11 |18, 16, 13, 11 | | +| |ScatterND |16, 11 |18, 16, 13, 11 | | | |Selu |1 |22, 6, 1 | | | |SequenceAt |11 |11 |Supported only in certain patterns| | |SequenceConstruct |11 |11 |Supported only in certain patterns| | |SequenceEmpty |11 |11 |Supported only in certain patterns| -| |SequenceErase | |11 | | +| |SequenceErase |11 |11 |Supported only in certain patterns| | |SequenceInsert |11 |11 |Supported only in certain patterns| -| |SequenceLength | |11 | | +| |SequenceLength |11 |11 |Supported only in certain patterns| | |SequenceMap | |17 | | | |Shape |15, 1 |21, 19, 15, 13, 1 | | | |Shrink |9 |9 | | @@ -215,7 +217,7 @@ OpenVINO provides support for operations of Default Opset (empty in table below) |com.microsoft |BiasGelu |1 |1 | | |com.microsoft |BiasSoftmax | |1 | | |com.microsoft |BiasSplitGelu | |1 | | -|com.microsoft |BifurcationDetector | |1 | | +|com.microsoft |BifurcationDetector |1 |1 | | |com.microsoft |BitmaskBiasDropout | |1 | | |com.microsoft |BitmaskDropout | |1 | | |com.microsoft |CDist | |1 | | @@ -261,7 +263,7 @@ OpenVINO provides support for operations of Default Opset (empty in table below) |com.microsoft |MaxpoolWithMask | |1 | | |com.microsoft |MoE | |1 | | |com.microsoft |MulInteger | |1 | | -|com.microsoft |MultiHeadAttention | |1 | | +|com.microsoft |MultiHeadAttention |1 |1 | | |com.microsoft |MurmurHash3 | |1 | | |com.microsoft |NGramRepeatBlock | |1 | | |com.microsoft |NhwcConv | |1 | | @@ -335,3 +337,4 @@ OpenVINO provides support for operations of Default Opset (empty in table below) |org.pytorch.aten |adaptive_avg_pool2d |1 |1 | | |mmdeploy |MMCVRoIAlignRotated |1 |1 | | |mmdeploy |NMSRotated |1 |1 | | +|ai.onnx.ml |Normalizer |1 |1 | | diff --git a/src/frontends/onnx/frontend/src/core/attribute.hpp b/src/frontends/onnx/frontend/src/core/attribute.hpp index fe69b53b7d15..155c8a601a2d 100644 --- a/src/frontends/onnx/frontend/src/core/attribute.hpp +++ b/src/frontends/onnx/frontend/src/core/attribute.hpp @@ -6,6 +6,8 @@ #include +#include + #include "core/sparse_tensor.hpp" #include "core/tensor.hpp" #include "openvino/core/except.hpp" @@ -185,7 +187,7 @@ class Attribute { Attribute() = delete; Attribute(const AttributeProto& attribute_proto, - const std::string& model_dir, + const std::filesystem::path& model_dir, detail::MappedMemoryHandles mmap_cache) : m_attribute_proto{&attribute_proto}, m_model_dir{model_dir}, @@ -339,7 +341,7 @@ class Attribute { private: const AttributeProto* m_attribute_proto; - std::string m_model_dir; + std::filesystem::path m_model_dir; detail::MappedMemoryHandles m_mmap_cache; }; diff --git a/src/frontends/onnx/frontend/src/core/graph.cpp b/src/frontends/onnx/frontend/src/core/graph.cpp index 6e19ff9f4315..2007ebf59fca 100644 --- a/src/frontends/onnx/frontend/src/core/graph.cpp +++ b/src/frontends/onnx/frontend/src/core/graph.cpp @@ -90,13 +90,13 @@ ov::frontend::ExtensionHolder subgraph_required_extensions( } } // namespace detail -Graph::Graph(const std::string& model_dir, +Graph::Graph(const std::filesystem::path& model_dir, const std::shared_ptr& model_proto, detail::MappedMemoryHandles mmap_cache, ov::frontend::ExtensionHolder extensions) : Graph(model_dir, model_proto, common::make_unique(), mmap_cache, std::move(extensions)) {} -Graph::Graph(const std::string& model_dir, +Graph::Graph(const std::filesystem::path& model_dir, const std::shared_ptr& model_proto, std::unique_ptr&& cache, detail::MappedMemoryHandles mmap_cache, diff --git a/src/frontends/onnx/frontend/src/core/graph.hpp b/src/frontends/onnx/frontend/src/core/graph.hpp index a564d439f719..69b7873d3ecb 100644 --- a/src/frontends/onnx/frontend/src/core/graph.hpp +++ b/src/frontends/onnx/frontend/src/core/graph.hpp @@ -6,6 +6,7 @@ #include +#include #include #include #include @@ -23,7 +24,7 @@ namespace frontend { namespace onnx { class Graph : public std::enable_shared_from_this { public: - Graph(const std::string& model_dir, + Graph(const std::filesystem::path& model_dir, const std::shared_ptr& model_proto, detail::MappedMemoryHandles mmap_cache, ov::frontend::ExtensionHolder extensions = {}); @@ -40,7 +41,7 @@ class Graph : public std::enable_shared_from_this { const std::string& get_name() const { return m_model->get_graph().name(); } - const std::string& model_dir() const { + const std::filesystem::path& model_dir() const { return m_model_dir; } detail::MappedMemoryHandles get_mmap_cache() const { @@ -67,7 +68,7 @@ class Graph : public std::enable_shared_from_this { } protected: - Graph(const std::string& model_dir, + Graph(const std::filesystem::path& model_dir, const std::shared_ptr& model, std::unique_ptr&& cache, detail::MappedMemoryHandles mmap_cache, @@ -92,7 +93,7 @@ class Graph : public std::enable_shared_from_this { private: std::vector m_nodes; - std::string m_model_dir; + std::filesystem::path m_model_dir; detail::MappedMemoryHandles m_mmap_cache; OperatorsBridge m_ops_bridge; }; diff --git a/src/frontends/onnx/frontend/src/core/graph_iterator_proto.cpp b/src/frontends/onnx/frontend/src/core/graph_iterator_proto.cpp index 62ab25b48700..67f4e6b44f38 100644 --- a/src/frontends/onnx/frontend/src/core/graph_iterator_proto.cpp +++ b/src/frontends/onnx/frontend/src/core/graph_iterator_proto.cpp @@ -21,8 +21,8 @@ #include "openvino/frontend/graph_iterator.hpp" #include "openvino/frontend/onnx/graph_iterator.hpp" #include "openvino/util/file_util.hpp" -#include "openvino/util/wstring_convert_util.hpp" #include "transform.hpp" +#include "utils/tensor_external_data.hpp" namespace { // THis is copied from utils/common.hpp @@ -104,6 +104,54 @@ void fixup_legacy_nodes(::ONNX_NAMESPACE::ModelProto& model_proto) { } } +/// \brief Collect all external references from a subgraph, recursing into nested subgraphs. +/// Returns all tensor names referenced by the subgraph (directly or via nested subgraphs) +/// that are not defined within the subgraph itself. +void collect_subgraph_external_refs(const GraphProto& subgraph, std::unordered_set& deps) { + // Build set of names defined within this subgraph + std::unordered_set subgraph_defined; + for (const auto& input : subgraph.input()) { + subgraph_defined.insert(input.name()); + } + for (const auto& init : subgraph.initializer()) { + subgraph_defined.insert(init.name()); + } + for (const auto& sub_node : subgraph.node()) { + for (const auto& out : sub_node.output()) { + subgraph_defined.insert(out); + } + } + // Collect direct references to outer graph tensors + for (const auto& sub_node : subgraph.node()) { + for (const auto& inp : sub_node.input()) { + if (!inp.empty() && subgraph_defined.count(inp) == 0) { + deps.insert(inp); + } + } + // Recurse into nested subgraphs + for (const auto& attr : sub_node.attribute()) { + if (attr.has_g()) { + std::unordered_set nested_deps; + collect_subgraph_external_refs(attr.g(), nested_deps); + // Nested external refs that are also external to this subgraph + for (const auto& dep : nested_deps) { + if (subgraph_defined.count(dep) == 0) { + deps.insert(dep); + } + } + } + } + } + // Subgraph outputs may directly reference outer-scope tensors when the body + // has no node producing them (e.g., an If branch returning a parent value). + for (const auto& output : subgraph.output()) { + const auto& name = output.name(); + if (!name.empty() && subgraph_defined.count(name) == 0) { + deps.insert(name); + } + } +} + /// \brief Collect all dependencies for a node (direct inputs + subgraph external references). void collect_node_dependencies(const NodeProto& node, std::unordered_set& deps) { // Direct inputs @@ -117,28 +165,7 @@ void collect_node_dependencies(const NodeProto& node, std::unordered_set subgraph_defined; - for (const auto& input : subgraph.input()) { - subgraph_defined.insert(input.name()); - } - for (const auto& init : subgraph.initializer()) { - subgraph_defined.insert(init.name()); - } - for (const auto& sub_node : subgraph.node()) { - for (const auto& out : sub_node.output()) { - subgraph_defined.insert(out); - } - } - // Find references to outer graph tensors - for (const auto& sub_node : subgraph.node()) { - for (const auto& inp : sub_node.input()) { - if (!inp.empty() && subgraph_defined.count(inp) == 0) { - deps.insert(inp); - } - } - } + collect_subgraph_external_refs(attr.g(), deps); } } @@ -255,7 +282,7 @@ bool extract_tensor_external_data(ov::frontend::onnx::TensorMetaInfo& tensor_met std::string m_sha1_digest{}; // for future use for (const auto& entry : tensor_info->external_data()) { if (entry.key() == "location") { - ext_location = ov::util::sanitize_path(entry.value()); + ext_location = entry.value(); } else if (entry.key() == "offset") { ext_data_offset = std::stoull(entry.value()); } else if (entry.key() == "length") { @@ -264,8 +291,15 @@ bool extract_tensor_external_data(ov::frontend::onnx::TensorMetaInfo& tensor_met m_sha1_digest = entry.value(); } } - const auto full_path = - ov::util::get_absolute_file_path(ov::util::path_join({graph_iterator->get_model_dir(), ext_location})); + if (ext_location == detail::ORT_MEM_ADDR) { + // Specific ONNX Runtime Case when it passes a model with self-managed data + tensor_meta_info.m_is_raw = true; + tensor_meta_info.m_tensor_data = reinterpret_cast(ext_data_offset); + tensor_meta_info.m_tensor_data_size = ext_data_length; + tensor_meta_info.m_external_location = std::make_shared(ext_location); + return true; + } + const auto full_path = ov::util::sanitize_path(graph_iterator->get_model_dir(), ov::util::make_path(ext_location)); const int64_t file_size = ov::util::file_size(full_path); if ((file_size <= 0 && ext_data_length > 0) || ext_data_length > static_cast(file_size) || ext_data_offset > static_cast(file_size) - ext_data_length) { @@ -279,24 +313,15 @@ bool extract_tensor_external_data(ov::frontend::onnx::TensorMetaInfo& tensor_met ? static_cast(ext_data_length) : static_cast(file_size) - static_cast(ext_data_offset); auto memory_mode = graph_iterator->get_memory_management_mode(); - // Remove when cache map will use path instead string. - const auto full_path_str = ov::util::path_to_string(full_path); - if (ext_location == "*/_ORT_MEM_ADDR_/*") { - // Specific ONNX Runtime Case when it passes a model with self-managed data - tensor_meta_info.m_is_raw = true; - tensor_meta_info.m_tensor_data = reinterpret_cast(ext_data_offset); - tensor_meta_info.m_tensor_data_size = ext_data_length; - tensor_meta_info.m_external_location = std::make_shared(ext_location); - return true; - } else if (memory_mode == External_MMAP) { + if (memory_mode == External_MMAP) { auto cache = graph_iterator->get_mmap_cache(); - auto cached_mapped_memory = cache->find(full_path_str); + auto cached_mapped_memory = cache->find(full_path); std::shared_ptr mapped_memory; if (cached_mapped_memory != cache->end()) { mapped_memory = cached_mapped_memory->second; } else { mapped_memory = ov::load_mmap_object(full_path); - (*cache)[full_path_str] = mapped_memory; + (*cache)[full_path] = mapped_memory; } tensor_meta_info.m_is_raw = true; tensor_meta_info.m_tensor_data = @@ -306,7 +331,7 @@ bool extract_tensor_external_data(ov::frontend::onnx::TensorMetaInfo& tensor_met } else if (memory_mode == External_Stream) { auto cache = graph_iterator->get_stream_cache(); FRONT_END_GENERAL_CHECK(cache, "Stream cache is not initialized for external stream mode"); - auto cached_stream = cache->find(full_path_str); + auto cached_stream = cache->find(full_path); std::shared_ptr external_data_stream; if (cached_stream != cache->end()) { external_data_stream = cached_stream->second; @@ -316,7 +341,7 @@ bool extract_tensor_external_data(ov::frontend::onnx::TensorMetaInfo& tensor_met p->close(); delete p; }}; - (*cache)[full_path_str] = external_data_stream; + (*cache)[full_path] = external_data_stream; } if (external_data_stream->fail() || !external_data_stream->good()) { @@ -335,7 +360,7 @@ bool extract_tensor_external_data(ov::frontend::onnx::TensorMetaInfo& tensor_met tensor_meta_info.m_tensor_data_size); return true; } else if (memory_mode == Internal_MMAP || memory_mode == Internal_Stream) { - tensor_meta_info.m_external_location = std::make_shared(full_path_str); + tensor_meta_info.m_external_location = std::make_shared(ov::util::path_to_string(full_path)); tensor_meta_info.m_tensor_data = reinterpret_cast(ext_data_offset); tensor_meta_info.m_tensor_data_size = ext_data_length; return true; @@ -457,10 +482,12 @@ GraphIteratorProto::GraphIteratorProto(const GraphIteratorProtoMemoryManagementM : m_graph(nullptr), m_parent(nullptr), m_mode(mode), - m_mmap_cache{mode == External_MMAP ? std::make_shared>>() - : nullptr}, - m_stream_cache{mode == External_Stream ? std::make_shared>>() - : nullptr}, + m_mmap_cache{mode == External_MMAP + ? std::make_shared>>() + : nullptr}, + m_stream_cache{mode == External_Stream + ? std::make_shared>>() + : nullptr}, m_data_holder{mode == External_Stream ? std::make_shared>>() : nullptr} {} GraphIteratorProto::GraphIteratorProto(GraphIteratorProto* parent, const GraphProto* graph_def) { @@ -476,10 +503,9 @@ GraphIteratorProto::GraphIteratorProto(GraphIteratorProto* parent, const GraphPr void GraphIteratorProto::initialize(const std::filesystem::path& path) { m_model_dir = ov::util::get_directory(path); - const auto path_string = ov::util::path_to_string(path); try { std::ifstream model_file(path, std::ios::binary | std::ios::in); - FRONT_END_GENERAL_CHECK(model_file && model_file.is_open(), "Could not open the file: \"", path_string, "\""); + FRONT_END_GENERAL_CHECK(model_file && model_file.is_open(), "Could not open the file: ", path); m_model = std::make_shared(); FRONT_END_GENERAL_CHECK(m_model->ParseFromIstream(&model_file), "Model can't be parsed"); @@ -501,6 +527,18 @@ void GraphIteratorProto::initialize(const std::filesystem::path& path) { throw; } } + +void GraphIteratorProto::initialize(std::shared_ptr model) { + m_model = std::move(model); + if (m_model && m_model->has_graph()) { + fixup_legacy_nodes(*m_model); + topological_sort_graph(m_model->mutable_graph()); + m_graph = &m_model->graph(); + } else { + m_graph = nullptr; + } +} + std::shared_ptr GraphIteratorProto::get_tensor(const std::string& name, GraphIteratorProto** owner) { if (m_tensors.count(name) == 0) { diff --git a/src/frontends/onnx/frontend/src/core/graph_iterator_proto.hpp b/src/frontends/onnx/frontend/src/core/graph_iterator_proto.hpp index 3cea90ad6300..4518ff247076 100644 --- a/src/frontends/onnx/frontend/src/core/graph_iterator_proto.hpp +++ b/src/frontends/onnx/frontend/src/core/graph_iterator_proto.hpp @@ -11,7 +11,6 @@ #include "openvino/frontend/onnx/graph_iterator.hpp" #include "openvino/util/file_util.hpp" #include "openvino/util/mmap_object.hpp" -#include "openvino/util/wstring_convert_util.hpp" using ::ONNX_NAMESPACE::AttributeProto_AttributeType; using ::ONNX_NAMESPACE::GraphProto; @@ -29,9 +28,9 @@ namespace frontend { namespace onnx { class DecoderProtoTensor; -using MappedMemoryHandles = std::shared_ptr>>; +using MappedMemoryHandles = std::shared_ptr>>; using LocalMemoryHandles = std::shared_ptr>>; -using LocalStreamHandles = std::shared_ptr>>; +using LocalStreamHandles = std::shared_ptr>>; enum GraphIteratorProtoMemoryManagementMode : int { Undefined = 0, @@ -67,14 +66,12 @@ class GraphIteratorProto : public ov::frontend::onnx::GraphIterator { ~GraphIteratorProto() = default; void initialize(const std::filesystem::path& path); + void initialize(std::shared_ptr model); /// Verifies file is supported template static bool is_supported(const std::basic_string& path) { - FRONT_END_GENERAL_CHECK(ov::util::file_exists(path), - "Could not open the file: \"", - ov::util::path_to_string(path), - '"'); + FRONT_END_GENERAL_CHECK(ov::util::file_exists(path), "Could not open the file: ", path); try { std::streamsize file_size = ov::util::file_size(path); // Skip files which less than size of file identifier diff --git a/src/frontends/onnx/frontend/src/core/node.cpp b/src/frontends/onnx/frontend/src/core/node.cpp index d1e39f2c2281..5f176d04701d 100644 --- a/src/frontends/onnx/frontend/src/core/node.cpp +++ b/src/frontends/onnx/frontend/src/core/node.cpp @@ -354,12 +354,12 @@ Node::Node(const NodeProto& node_proto, Graph* graph) }}, m_decoder(nullptr), m_translate_session(nullptr) {} -Node::Node(const DecoderBaseOperation& decoder, TranslateSession* translate_session) +Node::Node(std::shared_ptr decoder, TranslateSession* translate_session) : m_pimpl{nullptr, [](Impl* impl) { }}, - m_decoder(&decoder), + m_decoder(std::move(decoder)), m_translate_session(translate_session) {} Node::Node(Node&& other) noexcept diff --git a/src/frontends/onnx/frontend/src/core/node.hpp b/src/frontends/onnx/frontend/src/core/node.hpp index b2267dca4aa0..7c4d0be6ec51 100644 --- a/src/frontends/onnx/frontend/src/core/node.hpp +++ b/src/frontends/onnx/frontend/src/core/node.hpp @@ -48,7 +48,7 @@ class Node { Node() = delete; // TODO: hide this ctor since it uses protobufs generated structures Node(const NodeProto& node_proto, Graph* graph); - Node(const DecoderBaseOperation& decoder, TranslateSession* translate_session); + Node(std::shared_ptr decoder, TranslateSession* translate_session); Node(Node&&) noexcept; Node(const Node&); @@ -124,7 +124,7 @@ class Node { // compilation fails; the compiler is unable to parameterize an allocator's // default deleter due to incomple type. std::unique_ptr m_pimpl; - const DecoderBaseOperation* m_decoder; + std::shared_ptr m_decoder; TranslateSession* m_translate_session; }; diff --git a/src/frontends/onnx/frontend/src/core/operator_set.hpp b/src/frontends/onnx/frontend/src/core/operator_set.hpp index 18f0f00b610c..55f246d06f3a 100644 --- a/src/frontends/onnx/frontend/src/core/operator_set.hpp +++ b/src/frontends/onnx/frontend/src/core/operator_set.hpp @@ -29,6 +29,7 @@ extern const char* OPENVINO_ONNX_DOMAIN; extern const char* MICROSOFT_DOMAIN; extern const char* PYTORCH_ATEN_DOMAIN; extern const char* MMDEPLOY_DOMAIN; +extern const char* AIONNX_ML_DOMAIN; /// \brief Registering a versions range of translator in global map of translators (preferred to use) extern bool register_translator(const std::string name, diff --git a/src/frontends/onnx/frontend/src/core/sparse_tensor.hpp b/src/frontends/onnx/frontend/src/core/sparse_tensor.hpp index a50d4e672395..b24b556144b4 100644 --- a/src/frontends/onnx/frontend/src/core/sparse_tensor.hpp +++ b/src/frontends/onnx/frontend/src/core/sparse_tensor.hpp @@ -21,7 +21,7 @@ class SparseTensor { public: SparseTensor() = delete; SparseTensor(const SparseTensorProto& sparse_tensor, - const std::string& model_dir, + const std::filesystem::path& model_dir, detail::MappedMemoryHandles mmap_cache) : m_values{sparse_tensor.values(), model_dir, mmap_cache}, m_indices{sparse_tensor.indices(), model_dir, mmap_cache}, diff --git a/src/frontends/onnx/frontend/src/core/tensor.cpp b/src/frontends/onnx/frontend/src/core/tensor.cpp index 2d0b36ce755c..8d02203a2cf2 100644 --- a/src/frontends/onnx/frontend/src/core/tensor.cpp +++ b/src/frontends/onnx/frontend/src/core/tensor.cpp @@ -455,9 +455,9 @@ std::shared_ptr Tensor::get_ov_constant() const { if (ext_data.data_location() == detail::ORT_MEM_ADDR) { constant_buffer = ext_data.load_external_mem_data(); } else if (m_mmap_cache) { - constant_buffer = ext_data.load_external_mmap_data(m_model_dir.string(), m_mmap_cache); + constant_buffer = ext_data.load_external_mmap_data(m_model_dir, m_mmap_cache); } else { - constant_buffer = ext_data.load_external_data(m_model_dir.string()); + constant_buffer = ext_data.load_external_data(m_model_dir); } if (element_count == 0 && constant_buffer) { element_count = constant_buffer->size() * 8 / ov_type.bitwidth(); @@ -536,56 +536,64 @@ std::shared_ptr Tensor::get_ov_constant() const { "UINT4, UINT8, UINT16, UINT32, UINT64, STRING"); } } else if (m_tensor_place != nullptr) { - switch (m_tensor_place->get_element_type()) { - case ov::element::f32: - case ov::element::f64: - case ov::element::i32: - case ov::element::i64: - case ov::element::u32: - case ov::element::u64: - constant = std::make_shared(ov_type, m_shape, get_data_ptr()); - break; - case ov::element::i4: - constant = std::make_shared(ov_type, m_shape, get_data().data()); - break; - case ov::element::i8: - constant = std::make_shared(ov_type, m_shape, get_data().data()); - break; - case ov::element::i16: - constant = std::make_shared(ov_type, m_shape, get_data().data()); - break; - case ov::element::u4: - constant = std::make_shared(ov_type, m_shape, get_data().data()); - break; - case ov::element::u8: - constant = std::make_shared(ov_type, m_shape, get_data().data()); - break; - case ov::element::u16: - constant = std::make_shared(ov_type, m_shape, get_data().data()); - break; - case ov::element::boolean: - constant = std::make_shared(ov_type, m_shape, get_data().data()); - break; - case ov::element::bf16: - constant = std::make_shared(ov_type, m_shape, get_data().data()); - break; - case ov::element::f16: - constant = std::make_shared(ov_type, m_shape, get_data().data()); - break; - case ov::element::f8e4m3: - constant = std::make_shared(ov_type, m_shape, get_data().data()); - break; - case ov::element::f8e5m2: - constant = std::make_shared(ov_type, m_shape, get_data().data()); - break; - case ov::element::string: - constant = std::make_shared(ov_type, m_shape, get_data().data()); - break; - default: - ONNX_UNSUPPORTED_DATA_TYPE(m_tensor_proto->data_type(), - "BOOL, BFLOAT16, FLOAT8E4M3FN, FLOAT8E5M2, FLOAT, FLOAT16, DOUBLE, INT4, " - "INT8, INT16, INT32, INT64, " - "UINT4, UINT8, UINT16, UINT32, UINT64, STRING"); + auto elemnt_type = m_tensor_place->get_element_type(); + + if (!m_tensor_place->is_const_data_reusable() || elemnt_type == ov::element::string) { + switch (elemnt_type) { + case ov::element::f32: + case ov::element::f64: + case ov::element::i32: + case ov::element::i64: + case ov::element::u32: + case ov::element::u64: + constant = std::make_shared(ov_type, m_shape, get_data_ptr()); + break; + case ov::element::i4: + constant = std::make_shared(ov_type, m_shape, get_data().data()); + break; + case ov::element::i8: + constant = std::make_shared(ov_type, m_shape, get_data().data()); + break; + case ov::element::i16: + constant = std::make_shared(ov_type, m_shape, get_data().data()); + break; + case ov::element::u4: + constant = std::make_shared(ov_type, m_shape, get_data().data()); + break; + case ov::element::u8: + constant = std::make_shared(ov_type, m_shape, get_data().data()); + break; + case ov::element::u16: + constant = std::make_shared(ov_type, m_shape, get_data().data()); + break; + case ov::element::boolean: + constant = std::make_shared(ov_type, m_shape, get_data().data()); + break; + case ov::element::bf16: + constant = std::make_shared(ov_type, m_shape, get_data().data()); + break; + case ov::element::f16: + constant = std::make_shared(ov_type, m_shape, get_data().data()); + break; + case ov::element::f8e4m3: + constant = std::make_shared(ov_type, m_shape, get_data().data()); + break; + case ov::element::f8e5m2: + constant = std::make_shared(ov_type, m_shape, get_data().data()); + break; + case ov::element::string: + constant = std::make_shared(ov_type, m_shape, get_data().data()); + break; + default: + ONNX_UNSUPPORTED_DATA_TYPE(m_tensor_proto->data_type(), + "BOOL, BFLOAT16, FLOAT8E4M3FN, FLOAT8E5M2, FLOAT, FLOAT16, DOUBLE, INT4, " + "INT8, INT16, INT32, INT64, " + "UINT4, UINT8, UINT16, UINT32, UINT64, STRING"); + } + } else { + const void* data_ptr = m_tensor_place->get_data(); + auto constant_buffer = std::make_shared(data_ptr); + constant = std::make_shared(ov_type, m_shape, data_ptr, constant_buffer); } } else { FRONT_END_THROW("Tensor shape doesn't match data size"); diff --git a/src/frontends/onnx/frontend/src/core/tensor.hpp b/src/frontends/onnx/frontend/src/core/tensor.hpp index 1a13c18462b6..adbbf7c8845e 100644 --- a/src/frontends/onnx/frontend/src/core/tensor.hpp +++ b/src/frontends/onnx/frontend/src/core/tensor.hpp @@ -75,8 +75,8 @@ inline std::vector __get_raw_data(const std::string& raw_data, int onnx_data_ } // namespace } // namespace detail -using MappedMemoryHandles = std::shared_ptr>>; -using LocalStreamHandles = std::shared_ptr>>; +using MappedMemoryHandles = std::shared_ptr>>; +using LocalStreamHandles = std::shared_ptr>>; class TensorONNXPlace : public ov::frontend::onnx::TensorPlace { public: @@ -88,14 +88,16 @@ class TensorONNXPlace : public ov::frontend::onnx::TensorPlace { const size_t data_size, const ov::Any& data_any, std::shared_ptr data_location, - const bool is_raw) + const bool is_raw, + const bool reuse_const_data = false) : ov::frontend::onnx::TensorPlace(input_model, pshape, type, names), m_input_model(input_model), m_data(data), m_data_any(data_any), m_data_size(data_size), m_data_location(data_location), - m_is_raw(is_raw) {}; + m_is_raw(is_raw), + m_reuse_const_data(reuse_const_data) {}; void translate(ov::Output& output); @@ -140,6 +142,10 @@ class TensorONNXPlace : public ov::frontend::onnx::TensorPlace { return m_is_raw; } + bool is_const_data_reusable() const { + return m_reuse_const_data; + } + detail::MappedMemoryHandles get_mmap_cache(); detail::LocalStreamHandles get_stream_cache(); std::filesystem::path get_model_dir() const; @@ -152,6 +158,7 @@ class TensorONNXPlace : public ov::frontend::onnx::TensorPlace { size_t m_data_size; std::shared_ptr m_data_location; bool m_is_raw; + bool m_reuse_const_data; }; class Tensor { @@ -317,9 +324,9 @@ class Tensor { if (ext_data.data_location() == detail::ORT_MEM_ADDR) { buffer = ext_data.load_external_mem_data(); } else if (m_mmap_cache) { - buffer = ext_data.load_external_mmap_data(m_model_dir.string(), m_mmap_cache); + buffer = ext_data.load_external_mmap_data(m_model_dir, m_mmap_cache); } else { - buffer = ext_data.load_external_data(m_model_dir.string()); + buffer = ext_data.load_external_data(m_model_dir); } return std::vector(buffer->get_ptr(), buffer->get_ptr() + (buffer->size() / sizeof(T))); } diff --git a/src/frontends/onnx/frontend/src/editor.cpp b/src/frontends/onnx/frontend/src/editor.cpp index 627b4c64575b..ef53ed9f9333 100644 --- a/src/frontends/onnx/frontend/src/editor.cpp +++ b/src/frontends/onnx/frontend/src/editor.cpp @@ -313,45 +313,28 @@ struct ONNXModelEditor::Impl { graph_topological_sort(m_model_proto->mutable_graph()); } - Impl(const std::string& model_path) : Impl(std::make_shared(parse_from_file(model_path))) {} + Impl(const std::filesystem::path& model_path) : Impl(std::make_shared(parse_from_file(model_path))) {} Impl(std::istream& model_stream) : Impl(std::make_shared(parse_from_istream(model_stream))) {} - -#if defined(OPENVINO_ENABLE_UNICODE_PATH_SUPPORT) && defined(_WIN32) - Impl(const std::wstring& model_path) : Impl(std::make_shared(parse_from_file(model_path))) {} -#endif }; -ONNXModelEditor::ONNXModelEditor(const std::string& model_path, +ONNXModelEditor::ONNXModelEditor(const std::filesystem::path& model_path, const bool enable_mmap, frontend::ExtensionHolder extensions) : m_model_path{model_path}, - m_mmap_cache{enable_mmap ? std::make_shared>>() + m_mmap_cache{enable_mmap ? std::make_shared>>() : nullptr}, m_extensions{std::move(extensions)}, m_pimpl{new ONNXModelEditor::Impl{model_path}, [](Impl* impl) { delete impl; }} {} -#if defined(OPENVINO_ENABLE_UNICODE_PATH_SUPPORT) && defined(_WIN32) -ONNXModelEditor::ONNXModelEditor(const std::wstring& model_path, - const bool enable_mmap, - frontend::ExtensionHolder extensions) - : m_extensions{std::move(extensions)}, - m_model_path{ov::util::wstring_to_string(model_path)}, - m_mmap_cache{enable_mmap ? std::make_shared>>() - : nullptr}, - m_pimpl{new ONNXModelEditor::Impl{model_path}, [](Impl* impl) { - delete impl; - }} {} -#endif - ONNXModelEditor::ONNXModelEditor(std::istream& model_stream, - const std::string& model_path, + const std::filesystem::path& model_path, const bool enable_mmap, frontend::ExtensionHolder extensions) : m_model_path{model_path}, - m_mmap_cache{enable_mmap ? std::make_shared>>() + m_mmap_cache{enable_mmap ? std::make_shared>>() : nullptr}, m_extensions{std::move(extensions)}, m_pimpl{new ONNXModelEditor::Impl{model_stream}, [](Impl* impl) { @@ -366,11 +349,11 @@ ONNXModelEditor::ONNXModelEditor(std::shared_ptr model_proto, fronte delete impl; }} {} -const std::string& ONNXModelEditor::model_path() const { +const std::filesystem::path& ONNXModelEditor::model_path() const { return m_model_path; } -void ONNXModelEditor::serialize(const std::string& out_file_path) const { +void ONNXModelEditor::serialize(const std::filesystem::path& out_file_path) const { std::ofstream out_file{out_file_path, std::ios::out | std::ios::binary}; OPENVINO_ASSERT(out_file.is_open(), "Could not open the file: ", out_file_path); diff --git a/src/frontends/onnx/frontend/src/editor.hpp b/src/frontends/onnx/frontend/src/editor.hpp index eccc2cb6cb4d..d17dc0bb8d7e 100644 --- a/src/frontends/onnx/frontend/src/editor.hpp +++ b/src/frontends/onnx/frontend/src/editor.hpp @@ -4,6 +4,7 @@ #pragma once +#include #include #include #include @@ -34,14 +35,9 @@ class ONNXModelEditor final { /// \param model_path Path to the file containing the model. /// \param enable_mmap Enable mapping files with external weights instead of reading. /// \param extensions Holder for custom extensions (like custom ops). - explicit ONNXModelEditor(const std::string& model_path, + explicit ONNXModelEditor(const std::filesystem::path& model_path, const bool enable_mmap = false, frontend::ExtensionHolder extensions = {}); -#if defined(OPENVINO_ENABLE_UNICODE_PATH_SUPPORT) && defined(_WIN32) - ONNXModelEditor(const std::wstring& model_path, - const bool enable_mmap = false, - frontend::ExtensionHolder extensions = {}); -#endif /// \brief Creates an editor from a model stream. The stream is parsed and loaded /// into the m_model_proto member variable. @@ -52,7 +48,7 @@ class ONNXModelEditor final { /// \param enable_mmap Enable mapping files with external weights instead of reading. /// \param extensions Holder for custom extensions (like custom ops). explicit ONNXModelEditor(std::istream& model_stream, - const std::string& path = {}, + const std::filesystem::path& path = {}, const bool enable_mmap = false, frontend::ExtensionHolder extensions = {}); @@ -195,13 +191,13 @@ class ONNXModelEditor final { bool is_output(const OutputEdge& edge) const; /// \brief Returns the path to the original model file - const std::string& model_path() const; + const std::filesystem::path& model_path() const; /// \brief Saves the possibly modified model held by this class to a file. /// Serializes in binary mode. /// /// \param out_file_path A path to the file where the modified model should be dumped. - void serialize(const std::string& out_file_path) const; + void serialize(const std::filesystem::path& out_file_path) const; /// \brief Returns the InputEdge based on a node (node name or output name) /// and an input (input name or input index). @@ -313,7 +309,7 @@ class ONNXModelEditor final { private: void update_mapper_if_needed() const; - const std::string m_model_path; + const std::filesystem::path m_model_path; ov::frontend::onnx::detail::MappedMemoryHandles m_mmap_cache; frontend::ExtensionHolder m_extensions; diff --git a/src/frontends/onnx/frontend/src/frontend.cpp b/src/frontends/onnx/frontend/src/frontend.cpp index afec5715d093..0f77da433eb7 100644 --- a/src/frontends/onnx/frontend/src/frontend.cpp +++ b/src/frontends/onnx/frontend/src/frontend.cpp @@ -172,8 +172,12 @@ ov::frontend::InputModel::Ptr FrontEnd::load_impl(const std::vector& va if (variants.empty()) { return nullptr; } + // enable mmap by default - const bool enable_mmap = variants[variants.size() - 1].is() ? variants[variants.size() - 1].as() : true; + bool enable_mmap = true; + + if (variants.size() > 1 && variants[1].is()) + enable_mmap = variants[1].as(); const auto create_iterator_model = [&](const std::filesystem::path& model_path) { OPENVINO_DEBUG("[ONNX Frontend] Enabled an experimental GraphIteratorProto interface!!!"); @@ -186,7 +190,7 @@ ov::frontend::InputModel::Ptr FrontEnd::load_impl(const std::vector& va if (const auto path = get_path_from_any(variants[0])) { if (!gi_enabled) { - return std::make_shared(path.value().native(), enable_mmap, m_extensions); + return std::make_shared(path.value(), enable_mmap, m_extensions); } return create_iterator_model(path.value()); } @@ -194,7 +198,7 @@ ov::frontend::InputModel::Ptr FrontEnd::load_impl(const std::vector& va const auto stream = variants[0].as(); if (variants.size() > 1) if (const auto path = get_path_from_any(variants[1])) { - return std::make_shared(*stream, path.value().native(), enable_mmap, m_extensions); + return std::make_shared(*stream, path.value(), enable_mmap, m_extensions); } return std::make_shared(*stream, enable_mmap, m_extensions); } @@ -209,10 +213,19 @@ ov::frontend::InputModel::Ptr FrontEnd::load_impl(const std::vector& va return std::make_shared(std::make_shared(*model_proto_ptr), m_extensions); } // !!! End of Experimental feature + if (variants[0].is()) { auto graph_iterator = variants[0].as(); + bool reuse_const_data = false; + + if (variants.size() > 2 && variants[2].is()) + reuse_const_data = variants[2].as(); + // enable_mmap is a hint for a fallback in case external GraphIterator cannot work with external data - return std::make_shared(graph_iterator, enable_mmap, m_extensions.telemetry); + auto inputModel = + std::make_shared(graph_iterator, enable_mmap, m_extensions.telemetry, reuse_const_data); + + return inputModel; } return nullptr; } @@ -261,6 +274,39 @@ void FrontEnd::normalize(const std::shared_ptr& model) const { manager.run_passes(model); } +ov::frontend::AdditionalErrorCallback make_onnx_tokenizer_callback() { + return [](const std::set& unsupported_ops) -> std::string { + std::stringstream additional_info; + additional_info << "To facilitate the conversion of unsupported operations, refer to Frontend Extension " + "documentation: " + "https://docs.openvino.ai/latest/openvino_docs_Extensibility_UG_Frontend_Extensions.html \n"; + + // Check for tokenizer operations + const auto& all_tokenizer_ops = ov::frontend::onnx::get_supported_ops_via_tokenizers(); + std::string unsupported_ops_from_tokenizers; + size_t tokenizer_counter = 0; + for (const auto& unsupported_operation : unsupported_ops) { + if (std::find(all_tokenizer_ops.begin(), all_tokenizer_ops.end(), unsupported_operation) != + all_tokenizer_ops.end()) { + if (tokenizer_counter > 0) { + unsupported_ops_from_tokenizers += ", "; + } + unsupported_ops_from_tokenizers += unsupported_operation; + ++tokenizer_counter; + } + } + + if (!unsupported_ops_from_tokenizers.empty()) { + additional_info << "\nEncountered unconverted operation(s) for which openvino-tokenizers package " + "provides conversion extension(s): " + << unsupported_ops_from_tokenizers + << ". Install OpenVINO Tokenizers, refer to the documentation: " + "https://docs.openvino.ai/2026/openvino-workflow-generative/ov-tokenizers.html \n"; + } + return additional_info.str(); + }; +} + std::shared_ptr FrontEnd::convert(const InputModel::Ptr& input_model) const { auto unify_model = std::dynamic_pointer_cast(input_model); if (unify_model != nullptr) { @@ -287,7 +333,12 @@ std::shared_ptr FrontEnd::convert(const InputModel::Ptr& input_model) normalize(converted_model); const auto report = ov::frontend::collect_unconverted_ops(converted_model, make_onnx_extractor()); - ov::frontend::check_unconverted_ops(report, m_extensions.telemetry, "onnx", "[ONNX Frontend] "); + ov::frontend::check_unconverted_ops(report, + m_extensions.telemetry, + "onnx", + "[ONNX Frontend] ", + "", + make_onnx_tokenizer_callback()); return converted_model; } diff --git a/src/frontends/onnx/frontend/src/input_model.cpp b/src/frontends/onnx/frontend/src/input_model.cpp index 21f9a4c60591..81c277959159 100644 --- a/src/frontends/onnx/frontend/src/input_model.cpp +++ b/src/frontends/onnx/frontend/src/input_model.cpp @@ -16,31 +16,18 @@ using namespace ov; using namespace ov::frontend::onnx; -InputModel::InputModel(const std::string& path, const bool enable_mmap, frontend::ExtensionHolder extensions) +InputModel::InputModel(const std::filesystem::path& path, const bool enable_mmap, frontend::ExtensionHolder extensions) : m_editor{std::make_shared(path, enable_mmap, std::move(extensions))} {} -#if defined(OPENVINO_ENABLE_UNICODE_PATH_SUPPORT) && defined(_WIN32) -InputModel::InputModel(const std::wstring& path, const bool enable_mmap, frontend::ExtensionHolder extensions) - : m_editor{std::make_shared(path, enable_mmap, std::move(extensions))} {} -#endif - InputModel::InputModel(std::istream& model_stream, const bool enable_mmap, frontend::ExtensionHolder extensions) : m_editor{std::make_shared(model_stream, "", enable_mmap, std::move(extensions))} {} InputModel::InputModel(std::istream& model_stream, - const std::string& path, + const std::filesystem::path& path, const bool enable_mmap, frontend::ExtensionHolder extensions) : m_editor{std::make_shared(model_stream, path, enable_mmap, std::move(extensions))} {} -#ifdef OPENVINO_ENABLE_UNICODE_PATH_SUPPORT -InputModel::InputModel(std::istream& model_stream, - const std::wstring& path, - const bool enable_mmap, - frontend::ExtensionHolder extensions) - : InputModel(model_stream, ov::util::wstring_to_string(path), enable_mmap, std::move(extensions)) {} -#endif - InputModel::InputModel(std::shared_ptr model_proto, frontend::ExtensionHolder extensions) : m_editor{std::make_shared(model_proto, std::move(extensions))} {} @@ -571,7 +558,9 @@ class InputModel::InputModelONNXImpl { InputModelONNXImpl(const GraphIterator::Ptr& graph_iterator, const ov::frontend::InputModel& input_model, const std::shared_ptr& telemetry, - const bool enable_mmap); + const bool enable_mmap, + const bool reuse_const_data); + InputModelONNXImpl(const GraphIterator::Ptr& graph_iterator, const ov::frontend::InputModel& input_model, unify::InputModel::Ptr parent_model); @@ -646,6 +635,7 @@ class InputModel::InputModelONNXImpl { std::map m_metadata; std::shared_ptr m_telemetry; bool m_enable_mmap; + bool m_reuse_const_data; // This is used for keeping MMAP cache handles detail::MappedMemoryHandles m_mmap_cache; @@ -664,7 +654,8 @@ class InputModel::InputModelONNXImpl { namespace { std::shared_ptr decode_tensor_place( const ov::frontend::onnx::TensorMetaInfo& tensor_meta_info, - const ov::frontend::InputModel& model) { + const ov::frontend::InputModel& model, + const bool reuse_const_data) { auto tensor_place = std::make_shared(model, tensor_meta_info.m_partial_shape, @@ -674,7 +665,8 @@ std::shared_ptr decode_tensor_place( tensor_meta_info.m_tensor_data_size, tensor_meta_info.m_tensor_data_any, tensor_meta_info.m_external_location, - tensor_meta_info.m_is_raw); + tensor_meta_info.m_is_raw, + reuse_const_data); return tensor_place; } @@ -691,23 +683,26 @@ void InputModel::InputModelONNXImpl::load_model() { const auto& decoder = m_graph_iterator->get_decoder(); if (auto tensor_decoder = std::dynamic_pointer_cast(decoder)) { - auto tensor_place = decode_tensor_place(tensor_decoder->get_tensor_info(), m_input_model); + auto tensor_place = + decode_tensor_place(tensor_decoder->get_tensor_info(), m_input_model, m_reuse_const_data); + const auto output_idx = tensor_decoder->get_output_idx(); tensor_place->set_input_index(tensor_decoder->get_input_idx()); - tensor_place->set_output_index(tensor_decoder->get_output_idx()); + tensor_place->set_output_index(output_idx); - // Constant with data has been found - if (tensor_place->get_data() != nullptr) + const bool has_data = tensor_place->get_data() != nullptr || tensor_place->get_data_location() != nullptr; + // Skip constants that are not graph outputs — they don't contribute to the model graph. + if (has_data && output_idx < 0) continue; auto tensor_place_registered = register_tensor_place(tensor_place); if (!tensor_place_registered) continue; - if (tensor_place_registered->is_input()) + if (!has_data && tensor_place_registered->is_input()) m_inputs.push_back(tensor_place_registered); - if (tensor_decoder->get_output_idx() >= 0) { + if (output_idx >= 0) { m_outputs.push_back(tensor_place_registered); - output_indices.push_back(tensor_decoder->get_output_idx()); + output_indices.push_back(output_idx); } } else { auto op_place = std::make_shared(m_input_model, decoder); @@ -810,7 +805,7 @@ std::shared_ptr InputModel::InputModelONNXImpl::ensure_tensor_p if (auto existing = find_tensor_place(tensor_meta_info)) { return existing; } - return register_tensor_place(decode_tensor_place(tensor_meta_info, m_input_model)); + return register_tensor_place(decode_tensor_place(tensor_meta_info, m_input_model, m_reuse_const_data)); } void InputModel::InputModelONNXImpl::connect_inputs(const std::shared_ptr& op_place, @@ -855,21 +850,23 @@ void InputModel::InputModelONNXImpl::connect_outputs(const std::shared_ptr& telemetry, - const bool enable_mmap) + const bool enable_mmap, + const bool reuse_const_data) : m_graph_iterator(graph_iterator), m_input_model(input_model), m_telemetry(telemetry), - m_enable_mmap(enable_mmap) { + m_enable_mmap(enable_mmap), + m_reuse_const_data(reuse_const_data) { FRONT_END_GENERAL_CHECK(m_graph_iterator, "Null pointer specified for GraphIterator"); if (const auto graph_iterator = std::dynamic_pointer_cast(m_graph_iterator)) { m_model_dir = graph_iterator->get_model_dir(); } if (m_enable_mmap) { - m_mmap_cache = std::make_shared>>(); + m_mmap_cache = std::make_shared>>(); m_stream_cache = nullptr; } else { m_mmap_cache = nullptr; - m_stream_cache = std::make_shared>>(); + m_stream_cache = std::make_shared>>(); } load_model(); } @@ -881,6 +878,7 @@ InputModel::InputModelONNXImpl::InputModelONNXImpl(const GraphIterator::Ptr& gra m_input_model(input_model), m_telemetry(parent_model->get_telemetry_extension()), m_enable_mmap(parent_model->is_enabled_mmap()), + m_reuse_const_data(false), m_mmap_cache(parent_model->_impl->m_mmap_cache), m_stream_cache(parent_model->_impl->m_stream_cache) { FRONT_END_GENERAL_CHECK(m_graph_iterator, "Null pointer specified for GraphIterator"); @@ -1009,8 +1007,9 @@ void InputModel::InputModelONNXImpl::clean_up() {} InputModel::InputModel(const GraphIterator::Ptr& graph_iterator, const bool enable_mmap, - const std::shared_ptr& telemetry) - : _impl{std::make_shared(graph_iterator, *this, telemetry, enable_mmap)} {} + const std::shared_ptr& telemetry, + const bool reuse_const_data) + : _impl{std::make_shared(graph_iterator, *this, telemetry, enable_mmap, reuse_const_data)} {} InputModel::InputModel(const GraphIterator::Ptr& graph_iterator, ov::frontend::onnx::unify::InputModel::Ptr parent_model) diff --git a/src/frontends/onnx/frontend/src/input_model.hpp b/src/frontends/onnx/frontend/src/input_model.hpp index f62f72679475..20ebcc7cee06 100644 --- a/src/frontends/onnx/frontend/src/input_model.hpp +++ b/src/frontends/onnx/frontend/src/input_model.hpp @@ -23,23 +23,14 @@ namespace onnx { class InputModel : public ov::frontend::InputModel { public: - InputModel(const std::string& path, const bool enable_mmap = false, ExtensionHolder extensions = {}); -#if defined(OPENVINO_ENABLE_UNICODE_PATH_SUPPORT) && defined(_WIN32) - InputModel(const std::wstring& path, const bool enable_mmap = false, ExtensionHolder extensions = {}); -#endif + InputModel(const std::filesystem::path& path, const bool enable_mmap = false, ExtensionHolder extensions = {}); InputModel(std::istream& model_stream, const bool enable_mmap = false, ExtensionHolder extensions = {}); // The path can be required even if the model is passed as a stream because it is necessary // for ONNX external data feature InputModel(std::istream& model_stream, - const std::string& path, + const std::filesystem::path& path, const bool enable_mmap = false, ExtensionHolder extensions = {}); -#ifdef OPENVINO_ENABLE_UNICODE_PATH_SUPPORT - InputModel(std::istream& model_stream, - const std::wstring& path, - const bool enable_mmap = false, - ExtensionHolder extensions = {}); -#endif InputModel(std::shared_ptr model_proto, ExtensionHolder extensions = {}); std::vector get_inputs() const override; @@ -120,7 +111,9 @@ class InputModel : public ov::frontend::InputModel { explicit InputModel(const ov::frontend::onnx::GraphIterator::Ptr& graph_iterator, const bool enable_mmap, - const std::shared_ptr& telemetry = {}); + const std::shared_ptr& telemetry = {}, + const bool reuse_const_data = false); + explicit InputModel(const ov::frontend::onnx::GraphIterator::Ptr& graph_iterator, unify::InputModel::Ptr parent_model); diff --git a/src/frontends/onnx/frontend/src/op/ai.onnx.ml/normalizer.cpp b/src/frontends/onnx/frontend/src/op/ai.onnx.ml/normalizer.cpp new file mode 100644 index 000000000000..2e44d70cddc8 --- /dev/null +++ b/src/frontends/onnx/frontend/src/op/ai.onnx.ml/normalizer.cpp @@ -0,0 +1,81 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// +#include +#include + +#include "core/operator_set.hpp" +#include "exceptions.hpp" +#include "openvino/frontend/exception.hpp" +#include "openvino/op/abs.hpp" +#include "openvino/op/constant.hpp" +#include "openvino/op/convert.hpp" +#include "openvino/op/divide.hpp" +#include "openvino/op/equal.hpp" +#include "openvino/op/multiply.hpp" +#include "openvino/op/power.hpp" +#include "openvino/op/reduce_max.hpp" +#include "openvino/op/reduce_sum.hpp" +#include "openvino/op/select.hpp" +#include "openvino/op/sqrt.hpp" +#include "utils/common.hpp" + +using namespace ov::op; + +namespace ov { +namespace frontend { +namespace onnx { +namespace ai_onnx_ml { +namespace opset_1 { + +const ov::Output check_zero_divisor(const ov::Output& val) { + const auto zero = ov::op::v0::Constant::create(val.get_element_type(), ov::Shape{}, {0.0f}); + const auto one_val = ov::op::v0::Constant::create(val.get_element_type(), ov::Shape{}, {1.0f}); + const auto is_zero = std::make_shared(val, zero); + return std::make_shared(is_zero, one_val, val); +} + +ov::OutputVector normalizer(const ov::frontend::onnx::Node& node) { + // Step 1: Get input tensor + const auto input = node.get_ov_inputs()[0]; + + CHECK_VALID_NODE(node, + input.get_element_type() == ov::element::f64 || input.get_element_type() == ov::element::f32 || + input.get_element_type() == ov::element::i64 || input.get_element_type() == ov::element::i32, + "Unsupported input type, accepted FP32, FP64, I64, I32 got: ", + input.get_element_type()); + + const auto normalization_type = node.get_attribute_value("norm", "MAX"); + + CHECK_VALID_NODE(node, + normalization_type == "MAX" || normalization_type == "L1" || normalization_type == "L2", + "Normalization Mode should be either MAX, L1 or L2, got ", + normalization_type); + + auto float_input = std::make_shared(input, ov::element::f32); + const auto x = std::make_shared(float_input); + const auto axes = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{1}, {-1}); // e.g., last axis + if (normalization_type == "L1") { + const auto sum_x = std::make_shared(x, axes, true); + const auto safe_x = check_zero_divisor(sum_x); + return {std::make_shared(float_input, safe_x)}; + } else if (normalization_type == "L2") { + const auto x_squared = std::make_shared(x, x); + const auto sum_sqrdx = std::make_shared(x_squared, axes, true); + const auto sqrt_sum_sqrdx = std::make_shared(sum_sqrdx); + const auto safe_x = check_zero_divisor(sqrt_sum_sqrdx); + const auto result = std::make_shared(float_input, safe_x); + return {result}; + } else { // Must be max + const auto max_x = std::make_shared(x, axes, /*keep_dims=*/true); + const auto safe_x = check_zero_divisor(max_x); + return {std::make_shared(float_input, safe_x)}; + } +} + +ONNX_OP("Normalizer", OPSET_SINCE(1), ai_onnx_ml::opset_1::normalizer, AIONNX_ML_DOMAIN); +} // namespace opset_1 +} // namespace ai_onnx_ml +} // namespace onnx +} // namespace frontend +} // namespace ov diff --git a/src/frontends/onnx/frontend/src/op/attention.cpp b/src/frontends/onnx/frontend/src/op/attention.cpp index db75a444f1b6..28b858766afc 100644 --- a/src/frontends/onnx/frontend/src/op/attention.cpp +++ b/src/frontends/onnx/frontend/src/op/attention.cpp @@ -10,27 +10,15 @@ #include "openvino/op/add.hpp" #include "openvino/op/concat.hpp" #include "openvino/op/constant.hpp" -#include "openvino/op/convert.hpp" #include "openvino/op/divide.hpp" -#include "openvino/op/greater_eq.hpp" -#include "openvino/op/matmul.hpp" -#include "openvino/op/multiply.hpp" -#include "openvino/op/range.hpp" #include "openvino/op/reshape.hpp" -#include "openvino/op/scaled_dot_product_attention.hpp" -#include "openvino/op/select.hpp" #include "openvino/op/shape_of.hpp" -#include "openvino/op/softmax.hpp" -#include "openvino/op/sqrt.hpp" -#include "openvino/op/squeeze.hpp" -#include "openvino/op/subtract.hpp" -#include "openvino/op/tanh.hpp" #include "openvino/op/tile.hpp" #include "openvino/op/transpose.hpp" -#include "openvino/op/unsqueeze.hpp" #include "utils/common.hpp" using namespace ov::op; +using namespace ov::frontend::onnx::attention; namespace ov { namespace frontend { @@ -39,8 +27,6 @@ namespace ai_onnx { namespace detail { namespace { -using ov::frontend::onnx::attention::get_dimensions; - // Reshape 3D input (batch, seq, num_heads * head_size) to 4D (batch, num_heads, seq, head_size) // Uses direct reshape matching the ONNX Attention spec (no transpose): // the hidden dimension is split as [num_heads, seq, head_size] in row-major order. @@ -77,161 +63,6 @@ ov::Output repeat_kv(const ov::Output& input, int64_t n_rep) return std::make_shared(input, repeats); } -// Convert boolean mask to float additive mask: true -> 0.0, false -> -10000.0 -ov::Output convert_boolean_mask(const ov::Output& mask, const ov::element::Type& type) { - auto zero = v0::Constant::create(type, ov::Shape{}, {0.0f}); - auto neg_large = v0::Constant::create(type, ov::Shape{}, {-10000.0f}); - return std::make_shared(mask, zero, neg_large); -} - -// Build additive causal mask of shape (seq_q, seq_kv): 0 for allowed, -10000 for masked. -// When use_offset=true, accounts for KV cache offset so that query position i attends to -// key positions j where j <= i + (seq_kv - seq_q). Use this for KV cache scenarios. -// When use_offset=false, builds a simple lower-triangular mask matching np.tril(k=0), -// where query position i attends to key positions j where j <= i. -ov::Output build_causal_mask(const ov::Output& Q, const ov::Output& K, bool use_offset) { - auto q_shape = std::make_shared(Q); - auto k_shape = std::make_shared(K); - // Q is 4D: (B, heads, seq_q, head_size), K is 4D: (B, heads, seq_kv, head_size) - auto seq_q = get_dimensions(q_shape, {2}); - auto seq_kv = get_dimensions(k_shape, {2}); - - auto zero = v0::Constant::create(ov::element::i64, ov::Shape{}, {0}); - auto one = v0::Constant::create(ov::element::i64, ov::Shape{}, {1}); - - auto seq_q_scalar = std::make_shared(seq_q, zero); - auto seq_kv_scalar = std::make_shared(seq_kv, zero); - - // Column indices: [0, 1, ..., seq_kv-1] - auto col_indices = std::make_shared(zero, seq_kv_scalar, one, ov::element::i64); - - std::shared_ptr row_indices; - if (use_offset) { - // Row indices adjusted for KV cache offset: [offset, offset+1, ..., offset+seq_q-1] - auto offset = std::make_shared(seq_kv_scalar, seq_q_scalar); - auto end = std::make_shared(offset, seq_q_scalar); - row_indices = std::make_shared(offset, end, one, ov::element::i64); - } else { - // Row indices without offset: [0, 1, ..., seq_q-1] (matches np.tril(k=0)) - row_indices = std::make_shared(zero, seq_q_scalar, one, ov::element::i64); - } - - // Unsqueeze rows to (seq_q, 1) for broadcasting with (seq_kv,) - auto axis = v0::Constant::create(ov::element::i64, ov::Shape{1}, {1}); - auto rows_2d = std::make_shared(row_indices, axis); - - // Lower-triangular: position (i, j) allowed when adjusted_row[i] >= col[j] - auto is_allowed = std::make_shared(rows_2d, col_indices); - - // Convert boolean to additive float mask: true -> 0.0, false -> -10000.0 - return convert_boolean_mask(is_allowed, Q.get_element_type()); -} - -// Build SDPA-based attention (primary fast path) -ov::Output build_sdpa(const ov::Output& Q, - const ov::Output& K, - const ov::Output& V, - bool has_mask, - const ov::Output& attn_mask, - float scale_attr, - bool is_causal) { - ov::OutputVector inputs{Q, K, V}; - if (has_mask) { - inputs.push_back(attn_mask); - } - if (scale_attr != 0.0f) { - if (!has_mask) { - // SDPA interprets inputs positionally (index 3 = mask, index 4 = scale), - // so a zero mask placeholder is needed when only scale is provided - inputs.push_back(v0::Constant::create(Q.get_element_type(), ov::Shape{}, {0.0f})); - } - inputs.push_back(v0::Constant::create(Q.get_element_type(), ov::Shape{}, {scale_attr})); - } - return std::make_shared(inputs, is_causal)->output(0); -} - -// Build manual attention decomposition (for softcap or qk_matmul_output) -// Returns {Y, qk_matmul_output_or_null} -ov::OutputVector build_manual_attention(const ov::Output& Q, - const ov::Output& K, - const ov::Output& V, - bool has_mask, - const ov::Output& attn_mask, - float scale_attr, - float softcap, - bool is_causal, - int64_t qk_matmul_output_mode, - bool needs_qk_output) { - // 1. Q @ K^T - auto qk = std::make_shared(Q, K, false, true); - - // 2. Apply scale - std::shared_ptr scaled_qk; - if (scale_attr != 0.0f) { - auto scale_node = v0::Constant::create(Q.get_element_type(), ov::Shape{}, {scale_attr}); - scaled_qk = std::make_shared(qk, scale_node); - } else { - // Default scale: 1/sqrt(head_size). Q is always 4D here: (B, heads, seq, head_size) - auto q_shape = std::make_shared(Q); - auto head_size = get_dimensions(q_shape, {3}); - auto head_size_f = std::make_shared(head_size, Q.get_element_type()); - auto sqrt_head = std::make_shared(head_size_f); - scaled_qk = std::make_shared(qk, sqrt_head); - } - - // 3. Apply attention mask and causal mask - std::shared_ptr masked = scaled_qk; - if (has_mask) { - masked = std::make_shared(scaled_qk, attn_mask); - } - if (is_causal) { - auto causal_mask = build_causal_mask(Q, K, false); - masked = std::make_shared(masked, causal_mask); - } - - // Capture qk_matmul_output at mode 0 (raw QK) or mode 1 (after mask) - ov::Output qk_debug_output; - if (needs_qk_output && qk_matmul_output_mode == 0) { - qk_debug_output = scaled_qk->output(0); - } else if (needs_qk_output && qk_matmul_output_mode == 1) { - qk_debug_output = masked->output(0); - } - - // 4. Apply softcap: softcap * tanh(scores / softcap) - std::shared_ptr capped = masked; - if (softcap > 0.0f) { - auto cap = v0::Constant::create(Q.get_element_type(), ov::Shape{}, {softcap}); - auto divided = std::make_shared(masked, cap); - auto tanh_out = std::make_shared(divided); - capped = std::make_shared(tanh_out, cap); - } - - // Capture at mode 2 (after softcap) - if (needs_qk_output && qk_matmul_output_mode == 2) { - qk_debug_output = capped->output(0); - } - - // 5. Softmax - auto softmax_out = std::make_shared(capped, -1); - - // Capture at mode 3 (after softmax) - if (needs_qk_output && qk_matmul_output_mode == 3) { - qk_debug_output = softmax_out->output(0); - } - - // 6. softmax @ V - auto output = std::make_shared(softmax_out, V); - - ov::OutputVector results; - results.push_back(output->output(0)); - if (needs_qk_output && qk_debug_output.get_node()) { - results.push_back(qk_debug_output); - } else { - results.push_back(ov::Output{}); - } - return results; -} - } // namespace } // namespace detail @@ -365,7 +196,7 @@ ov::OutputVector attention(const ov::frontend::onnx::Node& node) { if (attn_mask.get_element_type() == ov::element::boolean) { // For manual path, convert boolean to float; for SDPA path, SDPA handles boolean natively if (softcap > 0.0f || needs_qk_output) { - attn_mask = detail::convert_boolean_mask(attn_mask, Q.get_element_type()); + attn_mask = convert_boolean_mask(attn_mask, Q.get_element_type()); } } } @@ -375,7 +206,7 @@ ov::OutputVector attention(const ov::frontend::onnx::Node& node) { // which doesn't match the ONNX spec's np.tril(k=0) for non-square attention matrices // (seq_q != seq_kv). For KV cache scenarios, use offset to account for past sequence. if (is_causal) { - auto causal_mask = detail::build_causal_mask(Q, K, has_past_key); + auto causal_mask = build_causal_mask(Q, K, has_past_key); if (has_attn_mask) { attn_mask = std::make_shared(attn_mask, causal_mask); } else { @@ -391,23 +222,23 @@ ov::OutputVector attention(const ov::frontend::onnx::Node& node) { if (softcap > 0.0f || needs_qk_output) { // Manual decomposition path (softcap or debug output) - auto results = detail::build_manual_attention(Q, - K, - V, - has_attn_mask, - attn_mask, - scale_attr, - softcap, - is_causal, - qk_matmul_output_mode, - needs_qk_output); + auto results = build_manual_attention(Q, + K, + V, + has_attn_mask, + attn_mask, + scale_attr, + softcap, + is_causal, + qk_matmul_output_mode, + needs_qk_output); Y = results[0]; if (needs_qk_output && results.size() > 1 && results[1].get_node()) { qk_debug_output = results[1]; } } else { // SDPA path (primary fast path) - Y = detail::build_sdpa(Q, K, V, has_attn_mask, attn_mask, scale_attr, is_causal); + Y = build_sdpa(Q, K, V, has_attn_mask, attn_mask, scale_attr, is_causal); } // Reshape output back to 3D if Q was 3D diff --git a/src/frontends/onnx/frontend/src/op/col2im.cpp b/src/frontends/onnx/frontend/src/op/col2im.cpp new file mode 100644 index 000000000000..154226c5f219 --- /dev/null +++ b/src/frontends/onnx/frontend/src/op/col2im.cpp @@ -0,0 +1,69 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "openvino/op/col2im.hpp" + +#include "core/operator_set.hpp" +#include "exceptions.hpp" +#include "utils/common.hpp" + +namespace ov { +namespace frontend { +namespace onnx { +namespace ai_onnx { +namespace opset_18 { +ov::OutputVector col2im(const ov::frontend::onnx::Node& node) { + // 1. get inputs + common::default_op_checks(node, 3); + const auto inputs = node.get_ov_inputs(); + const auto& data = inputs[0]; // input + const auto& output_size = inputs[1]; // image_shape + const auto& kernel_size = inputs[2]; // block_shape + + // 2. get attributes + const size_t spatial_rank = 2; + + std::vector default_attr_vals(spatial_rank, 1); + auto dilations = node.get_attribute_value>("dilations", default_attr_vals); + auto strides = node.get_attribute_value>("strides", default_attr_vals); + + std::vector default_pads(spatial_rank * 2, 0); + auto pads = node.get_attribute_value>("pads", default_pads); + std::vector pads_begin; + std::vector pads_end; + + // ov::op::v15::Col2Im only supports 2D spatial dimensions + CHECK_VALID_NODE(node, + dilations.size() == spatial_rank, + "Col2Im 'dilations' attribute must have size [2]. Got: ", + dilations.size()); + CHECK_VALID_NODE(node, + strides.size() == spatial_rank, + "Col2Im 'strides' attribute must have size [2]. Got: ", + strides.size()); + CHECK_VALID_NODE(node, + pads.size() == spatial_rank * 2, + "Col2Im 'pads' attribute must have size [4]. Got: ", + pads.size()); + + // spatial_rank == pads.size() / 2 + pads_begin.assign(pads.begin(), pads.begin() + spatial_rank); + pads_end.assign(pads.begin() + spatial_rank, pads.end()); + + // 3. return Col2Im + return {std::make_shared(data, + output_size, + kernel_size, + strides, + dilations, + pads_begin, + pads_end)}; +} + +ONNX_OP("Col2Im", OPSET_SINCE(1), ai_onnx::opset_18::col2im); +} // namespace opset_18 +} // namespace ai_onnx +} // namespace onnx +} // namespace frontend +} // namespace ov diff --git a/src/frontends/onnx/frontend/src/op/com.microsoft/bifurcation_detector.cpp b/src/frontends/onnx/frontend/src/op/com.microsoft/bifurcation_detector.cpp new file mode 100644 index 000000000000..5b582c3037da --- /dev/null +++ b/src/frontends/onnx/frontend/src/op/com.microsoft/bifurcation_detector.cpp @@ -0,0 +1,294 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include + +#include "core/operator_set.hpp" +#include "exceptions.hpp" +#include "openvino/op/add.hpp" +#include "openvino/op/concat.hpp" +#include "openvino/op/constant.hpp" +#include "openvino/op/convert.hpp" +#include "openvino/op/cum_sum.hpp" +#include "openvino/op/equal.hpp" +#include "openvino/op/gather.hpp" +#include "openvino/op/greater.hpp" +#include "openvino/op/greater_eq.hpp" +#include "openvino/op/logical_and.hpp" +#include "openvino/op/logical_not.hpp" +#include "openvino/op/logical_or.hpp" +#include "openvino/op/maximum.hpp" +#include "openvino/op/minimum.hpp" +#include "openvino/op/range.hpp" +#include "openvino/op/reduce_logical_and.hpp" +#include "openvino/op/reduce_sum.hpp" +#include "openvino/op/reshape.hpp" +#include "openvino/op/select.hpp" +#include "openvino/op/shape_of.hpp" +#include "openvino/op/slice.hpp" +#include "openvino/op/squeeze.hpp" +#include "openvino/op/subtract.hpp" +#include "openvino/op/unsqueeze.hpp" +#include "utils/common.hpp" + +using namespace ov::op; + +namespace ov { +namespace frontend { +namespace onnx { +namespace com_microsoft { +namespace opset_1 { + +// Translator for com.microsoft.BifurcationDetector. +// +// Reference implementation (ORT): +// onnxruntime/contrib_ops/cpu/bert/bifurcation_detector.h +// +// Semantics: +// +// Inputs: src_tokens [S] int64, +// cur_tokens [C] int64, +// prev_suffix_match_idx (scalar) int64, +// pred_tokens [P] int64 (optional, P == S + 1 - prev_idx) +// Attrs: min_ngram_size (int, default 1, >= 1) +// max_ngram_size (int, default 1, >= min_ngram_size) +// Outputs: tokens [variable] int64, +// suffix_match_idx (same shape as prev_suffix_match_idx) int64 +// +// Phase 1 (bifurcation merge): +// - If pred is absent: tokens = cur_tokens. +// - Else: scan pred_tokens vs src_tokens[prev_idx:] for first mismatch +// at index `pred_bifur_idx in [0, S - prev_idx]`. If all pred[0..n-1] +// match (where n = S - prev_idx), pred_bifur_idx = n. The output +// is `cur ++ pred[0 : pred_bifur_idx + 1]`. Note the +1 means at +// least one pred token is always appended (the bifurcating token, +// or the trailing token when fully matching). +// +// Phase 2 (suffix match): +// For n in [min_ngram_size, max_ngram_size]: +// - If n > tokens_len: break. +// - Search the suffix tokens[-n:] in src_tokens as a substring. +// Let first_pos be the first occurrence (or absent). +// - If absent: break (keep current suffix_idx). +// - candidate = first_pos + n. +// - If candidate >= src_len: suffix_idx = candidate, break. +// - If a second occurrence exists in src[first_pos + 1 :], i.e. +// count(matches) >= 2: suffix_idx = -1, continue. +// - Else: suffix_idx = candidate, continue. +// Return suffix_idx (initial value -1). +ov::OutputVector bifurcation_detector(const ov::frontend::onnx::Node& node) { + common::default_op_checks(node, 3, 4); + + const auto inputs = node.get_ov_inputs(); + const auto& src = inputs[0]; + const auto& cur = inputs[1]; + const auto& prev_idx = inputs[2]; + const bool has_pred = common::is_input_valid(node, 3); + + const int64_t min_ngram = node.get_attribute_value("min_ngram_size", 1); + const int64_t max_ngram = node.get_attribute_value("max_ngram_size", 1); + CHECK_VALID_NODE(node, min_ngram >= 1, "min_ngram_size must be >= 1"); + CHECK_VALID_NODE(node, max_ngram >= min_ngram, "max_ngram_size must be >= min_ngram_size"); + + // ---- common scalar constants ---- + auto i64_scalar = [](int64_t v) { + return v0::Constant::create(element::i64, Shape{}, {v}); + }; + auto i64_1d = [](int64_t v) { + return v0::Constant::create(element::i64, Shape{1}, {v}); + }; + auto i64_axis0 = i64_1d(0); + auto i64_axis0_scalar = i64_scalar(0); + auto zero_scalar = i64_scalar(0); + auto one_scalar = i64_scalar(1); + auto neg_one_scalar = i64_scalar(-1); + auto zero_1d = i64_1d(0); + auto one_1d = i64_1d(1); + + // Tokens are int64 throughout. + auto shape_of_i64 = [&](const ov::Output& t) { + return std::make_shared(t, element::i64); + }; + auto squeeze_to_scalar = [&](const ov::Output& t) -> ov::Output { + return std::make_shared(t); + }; + + auto src_shape = shape_of_i64(src); + auto src_len_scalar = squeeze_to_scalar(src_shape); + + // ============================================================ + // Phase 1: bifurcation merge. + // ============================================================ + ov::Output tokens_out; + if (!has_pred) { + tokens_out = cur; + } else { + const auto& pred = inputs[3]; + auto prev_idx_raw = squeeze_to_scalar(prev_idx); + // Clamp prev_suffix_match_idx into [0, src_len]. A negative value (e.g. the + // -1 sentinel) would otherwise be interpreted by v8::Slice as an index from + // the end, producing wrong windows or out-of-bounds slicing. + auto prev_idx_clamped_low = std::make_shared(prev_idx_raw, zero_scalar); + auto prev_idx_scalar = std::make_shared(prev_idx_clamped_low, src_len_scalar); + auto pred_shape = shape_of_i64(pred); + auto pred_len_scalar = squeeze_to_scalar(pred_shape); + // n = min(P, S - prev_idx); clamp to >= 0 for safety. + // We compare only up to min(P, S - prev_idx) positions because both + // pred[0:k] and src[prev_idx:prev_idx+k] must be in-bounds. + auto n_raw = std::make_shared(src_len_scalar, prev_idx_scalar); + auto n_clipped_to_src = std::make_shared(n_raw, zero_scalar); + auto n_scalar = std::make_shared(n_clipped_to_src, pred_len_scalar); + + auto n_1d = std::make_shared(n_scalar, i64_axis0); + auto prev_idx_1d = std::make_shared(prev_idx_scalar, i64_axis0); + // src_window_end = prev_idx + n + auto src_window_end_scalar = std::make_shared(prev_idx_scalar, n_scalar); + auto src_window_end_1d = std::make_shared(src_window_end_scalar, i64_axis0); + + // pred_head = pred[0:n], src_window = src[prev_idx:prev_idx+n] + auto pred_head = std::make_shared(pred, zero_1d, n_1d, one_1d, zero_1d); + auto src_window = std::make_shared(src, prev_idx_1d, src_window_end_1d, one_1d, zero_1d); + + // First-mismatch index via cumulative-sum trick: + // mismatch_i64[i] = (pred[i] != src_window[i]) ? 1 : 0 + // cum_mismatch[i] = number of mismatches in [0..i] + // leading_match[i] = (cum_mismatch[i] == 0) + // bifur = sum(leading_match) + // For full match: cum is all-zero, leading_match all-true, bifur == n. + // For first mismatch at k: leading_match true on [0..k-1], bifur == k. + // For n == 0 (empty slice): ReduceSum over empty -> 0, bifur == 0. + auto eq_bifur = std::make_shared(pred_head, src_window); + auto mismatch_i64 = std::make_shared(std::make_shared(eq_bifur), element::i64); + auto cum_mismatch = std::make_shared(mismatch_i64, i64_axis0_scalar); + auto leading_match = std::make_shared(cum_mismatch, zero_scalar); + auto bifur = + std::make_shared(std::make_shared(leading_match, element::i64), i64_axis0); + + // take = bifur + 1; pred_kept = pred[0:take]; tokens = concat(cur, pred_kept) + auto take = std::make_shared(bifur, one_scalar); + auto take_1d = std::make_shared(take, i64_axis0); + auto pred_kept = std::make_shared(pred, zero_1d, take_1d, one_1d, zero_1d); + tokens_out = std::make_shared(OutputVector{cur, pred_kept}, 0); + } + + // ============================================================ + // Phase 2: suffix match. + // ============================================================ + auto tokens_shape = shape_of_i64(tokens_out); + auto tokens_len_scalar = squeeze_to_scalar(tokens_shape); + + // Append a sentinel so the Gather source is never empty (src_len == 0) and any + // clamped index stays in-bounds. src_safe = src ++ [-1], length src_len + 1. + auto src_safe = std::make_shared(OutputVector{src, i64_1d(-1)}, 0); + // Same sentinel trick for tokens, used by the fixed-length-n suffix gather so a + // length-n suffix stays in-bounds even when tokens_len < n or tokens_len == 0. + auto tokens_safe = std::make_shared(OutputVector{tokens_out, i64_1d(-1)}, 0); + + // State variables, threaded across unrolled n iterations. + ov::Output suffix_idx = neg_one_scalar; + ov::Output stopped = v0::Constant::create(element::boolean, Shape{}, {false}); + + for (int64_t n = min_ngram; n <= max_ngram; ++n) { + auto n_const = i64_scalar(n); + // Static [0, 1, ..., n-1] offset vector. n is a compile-time constant of the + // unrolled loop, so this (and everything keyed only on n) is a plain constant + // rather than a dynamic Range, keeping the n-axis statically shaped. + std::vector j_values(static_cast(n)); + std::iota(j_values.begin(), j_values.end(), int64_t{0}); + auto j_const = v0::Constant::create(element::i64, Shape{static_cast(n)}, j_values); + + // n_too_large = (n > tokens_len) + auto n_too_large = std::make_shared(n_const, tokens_len_scalar); + + // ---- Fixed-length-n suffix via clamped gather over tokens_safe ---- + // suffix[j] = tokens_out[(tokens_len - n) + j], j in [0, n). For tokens_len >= n + // these are the true last-n tokens; for tokens_len < n the clamped indices yield + // a bogus suffix that is masked out by n_too_large below. Statically [n]-shaped. + auto suffix_start = std::make_shared(tokens_len_scalar, n_const); + auto suffix_idx_raw = std::make_shared(suffix_start, j_const); + auto suffix_idx_low = std::make_shared(suffix_idx_raw, zero_scalar); + auto suffix_idx_clamped = std::make_shared(suffix_idx_low, tokens_len_scalar); + auto suffix_padded = std::make_shared(tokens_safe, suffix_idx_clamped, i64_axis0_scalar); + // suffix_padded has static length n + + // ---- Build sliding windows over src: windows[i,j] = src[i+j]. + // We need num_windows >= 1 to keep the Range/Gather valid even when + // src_len < n. The result for that degenerate case is masked out by + // n_too_large below. The j (column) axis is the static j_const, so only the + // num_windows (row) axis stays dynamic. + auto num_windows_raw = + std::make_shared(std::make_shared(src_len_scalar, n_const), one_scalar); + auto num_windows_safe = std::make_shared(num_windows_raw, one_scalar); + auto i_range = std::make_shared(zero_scalar, num_windows_safe, one_scalar, element::i64); + auto i_col = std::make_shared(i_range, i64_1d(1)); + auto j_row = std::make_shared(j_const, i64_1d(0)); + auto idx = std::make_shared(i_col, j_row); + // Clamp into [0, src_len] (valid indices of src_safe). For src_len >= n the + // real indices are all < src_len and are untouched; for shorter src the + // out-of-range entries fold onto the sentinel and are masked out below. + auto idx_clamped = std::make_shared(idx, src_len_scalar); + auto windows = std::make_shared(src_safe, idx_clamped, i64_axis0_scalar); + + // ---- Per-window match: ReduceLogicalAnd over the n-axis ---- + auto suffix_broadcast = std::make_shared(suffix_padded, i64_1d(0)); + auto eq = std::make_shared(windows, suffix_broadcast); + auto match_per_win = std::make_shared(eq, i64_1d(1), false); + auto match_i64 = std::make_shared(match_per_win, element::i64); + auto count_n = std::make_shared(match_i64, i64_axis0, false); + + // first_idx via cumulative-sum trick: count of leading positions with + // cum_match == 0 equals the index of the first True in match_per_win. + // When match_per_win is all-false, first_idx == num_windows; that result + // is masked out by `no_match` below (skip_update covers it). + auto cum_match = std::make_shared(match_i64, i64_axis0_scalar); + auto leading_nonmatch = std::make_shared(cum_match, zero_scalar); + auto first_idx = std::make_shared(std::make_shared(leading_nonmatch, element::i64), + i64_axis0, + false); + + auto candidate = std::make_shared(first_idx, n_const); + + // Decision flags + // A length-n substring cannot exist when src_len < n, so force no_match in + // that case regardless of the (masked, sentinel-filled) window comparison. + auto src_too_short = std::make_shared(n_const, src_len_scalar); + auto no_match = + std::make_shared(std::make_shared(count_n, zero_scalar), src_too_short); + auto count_ge_2 = std::make_shared(count_n, one_scalar); + auto out_of_range = std::make_shared(candidate, src_len_scalar); + + // Updated value when we do update: + // if (count >= 2 AND NOT out_of_range): -1 + // else: candidate + auto neg1_branch = std::make_shared(count_ge_2, std::make_shared(out_of_range)); + auto new_val_if_updating = std::make_shared(neg1_branch, neg_one_scalar, candidate); + + // skip_update = stopped OR n_too_large OR no_match + auto skip_update = + std::make_shared(stopped, std::make_shared(n_too_large, no_match)); + auto new_suffix_idx = std::make_shared(skip_update, suffix_idx, new_val_if_updating); + + // new_stopped = stopped OR n_too_large OR no_match OR out_of_range + auto stop_now = + std::make_shared(n_too_large, std::make_shared(no_match, out_of_range)); + auto new_stopped = std::make_shared(stopped, stop_now); + + suffix_idx = new_suffix_idx; + stopped = new_stopped; + } + + // Restore suffix_match_idx output shape to match prev_suffix_match_idx. + auto prev_idx_shape = shape_of_i64(prev_idx); + auto suffix_idx_out = std::make_shared(suffix_idx, prev_idx_shape, false); + + return {tokens_out, suffix_idx_out}; +} + +ONNX_OP("BifurcationDetector", OPSET_SINCE(1), com_microsoft::opset_1::bifurcation_detector, MICROSOFT_DOMAIN); + +} // namespace opset_1 +} // namespace com_microsoft +} // namespace onnx +} // namespace frontend +} // namespace ov diff --git a/src/frontends/onnx/frontend/src/op/com.microsoft/dynamic_quantize_matmul.cpp b/src/frontends/onnx/frontend/src/op/com.microsoft/dynamic_quantize_matmul.cpp index d904d955b652..bc519b9fed9b 100644 --- a/src/frontends/onnx/frontend/src/op/com.microsoft/dynamic_quantize_matmul.cpp +++ b/src/frontends/onnx/frontend/src/op/com.microsoft/dynamic_quantize_matmul.cpp @@ -4,12 +4,10 @@ #include "core/operator_set.hpp" #include "exceptions.hpp" +#include "openvino/decompositions/low_precision_dequantize.hpp" #include "openvino/frontend/exception.hpp" #include "openvino/op/add.hpp" -#include "openvino/op/convert.hpp" #include "openvino/op/matmul.hpp" -#include "openvino/op/multiply.hpp" -#include "openvino/op/subtract.hpp" #include "utils/common.hpp" using namespace ov::op; @@ -75,12 +73,7 @@ ov::OutputVector dynamic_quantize_matmul(const ov::frontend::onnx::Node& node) { // here https://tomwildenhain-microsoft.github.io/onnxruntime/docs/performance/quantization.html B_dequantized = (B // - b_zero_point) * b_scale - ov::Output B_dequantized = std::make_shared(B, b_scale.get_element_type()); - if (b_zero_point.get_node_shared_ptr()) { - b_zero_point = std::make_shared(b_zero_point, b_scale.get_element_type()); - B_dequantized = std::make_shared(B_dequantized, b_zero_point); - } - B_dequantized = std::make_shared(B_dequantized, b_scale); + ov::Output B_dequantized = ov::decomposition::low_precision_dequantize(B, b_scale, b_zero_point); // A, B are N-dimensional matrices. According to example ONNX models for this operator, the suboperations pass input // A/B such that B's shape is already transposed. E.g. diff --git a/src/frontends/onnx/frontend/src/op/com.microsoft/matmul_integer_to_float.cpp b/src/frontends/onnx/frontend/src/op/com.microsoft/matmul_integer_to_float.cpp index e18b319fe20e..644fe78d122e 100644 --- a/src/frontends/onnx/frontend/src/op/com.microsoft/matmul_integer_to_float.cpp +++ b/src/frontends/onnx/frontend/src/op/com.microsoft/matmul_integer_to_float.cpp @@ -5,12 +5,9 @@ #include "core/null_node.hpp" #include "core/operator_set.hpp" #include "exceptions.hpp" +#include "openvino/decompositions/low_precision_dequantize.hpp" #include "openvino/op/add.hpp" -#include "openvino/op/constant.hpp" -#include "openvino/op/convert.hpp" #include "openvino/op/matmul.hpp" -#include "openvino/op/multiply.hpp" -#include "openvino/op/subtract.hpp" #include "utils/common.hpp" using namespace ov::op; @@ -31,14 +28,14 @@ ov::OutputVector matmulintegertofloat(const ov::frontend::onnx::Node& node) { const auto& a_scale = inputs[2]; const auto& b_scale = inputs[3]; - ov::Output a_zero_point = - common::is_input_valid(node, 4) - ? inputs[4] - : std::make_shared(a_int.get_element_type(), ov::Shape{}, std::vector{0}); - ov::Output b_zero_point = - common::is_input_valid(node, 5) - ? inputs[5] - : std::make_shared(b_int.get_element_type(), ov::Shape{}, std::vector{0}); + ov::Output a_zero_point; + if (common::is_input_valid(node, 4)) { + a_zero_point = inputs[4]; + } + ov::Output b_zero_point; + if (common::is_input_valid(node, 5)) { + b_zero_point = inputs[5]; + } CHECK_VALID_NODE(node, a_int.get_element_type() == ov::element::i8 || a_int.get_element_type() == ov::element::u8, @@ -50,16 +47,8 @@ ov::OutputVector matmulintegertofloat(const ov::frontend::onnx::Node& node) { "Unsupported input B type. Expected int8 or uint8, got: ", b_int.get_element_type()); - const auto a_dequantized = std::make_shared(a_int, a_zero_point); - const auto b_dequantized = std::make_shared(b_int, b_zero_point); - - const auto a_dequantized_converted = - std::make_shared(a_dequantized, a_scale.get_element_type()); - const auto b_dequantized_converted = - std::make_shared(b_dequantized, b_scale.get_element_type()); - - const auto a_scaled = std::make_shared(a_dequantized_converted, a_scale); - const auto b_scaled = std::make_shared(b_dequantized_converted, b_scale); + auto a_scaled = ov::decomposition::low_precision_dequantize(a_int, a_scale, a_zero_point); + auto b_scaled = ov::decomposition::low_precision_dequantize(b_int, b_scale, b_zero_point); const auto matmul_result = std::make_shared(a_scaled, b_scaled); diff --git a/src/frontends/onnx/frontend/src/op/com.microsoft/matmulnbits.cpp b/src/frontends/onnx/frontend/src/op/com.microsoft/matmulnbits.cpp index 22b10612099d..22e4750bac35 100644 --- a/src/frontends/onnx/frontend/src/op/com.microsoft/matmulnbits.cpp +++ b/src/frontends/onnx/frontend/src/op/com.microsoft/matmulnbits.cpp @@ -8,6 +8,7 @@ #include "core/null_node.hpp" #include "core/operator_set.hpp" #include "exceptions.hpp" +#include "openvino/decompositions/low_precision_dequantize.hpp" #include "openvino/frontend/exception.hpp" #include "openvino/op/add.hpp" #include "openvino/op/constant.hpp" @@ -254,15 +255,12 @@ ov::OutputVector matmulnbits(const ov::frontend::onnx::Node& node) { // use fp16 for compute - // convert b to fp16 - auto converted_b = std::make_shared(casted_b, a.get_element_type()); - - // sub and scale - const auto sub_b = std::make_shared(converted_b, converted_zero_points); + // sub and scale via the shared low-precision dequantization helper const auto scales_fp16 = std::make_shared(scales, a.get_element_type()); const auto scales_reshaped = op::util::reshape(scales_fp16, ov::Shape{static_cast(N), static_cast(n_blocks_per_col), 1}); - const auto scaled_b = std::make_shared(sub_b, scales_reshaped); + + auto scaled_b = ov::decomposition::low_precision_dequantize(casted_b, scales_reshaped, converted_zero_points); // reshape b to [N, K] auto shape_b = v0::Constant::create(ov::element::i32, ov::Shape{2}, {0, -1}); diff --git a/src/frontends/onnx/frontend/src/op/com.microsoft/multi_head_attention.cpp b/src/frontends/onnx/frontend/src/op/com.microsoft/multi_head_attention.cpp new file mode 100644 index 000000000000..ee4dcf9c24ea --- /dev/null +++ b/src/frontends/onnx/frontend/src/op/com.microsoft/multi_head_attention.cpp @@ -0,0 +1,463 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include + +#include "core/null_node.hpp" +#include "core/operator_set.hpp" +#include "exceptions.hpp" +#include "openvino/frontend/exception.hpp" +#include "openvino/op/add.hpp" +#include "openvino/op/broadcast.hpp" +#include "openvino/op/concat.hpp" +#include "openvino/op/constant.hpp" +#include "openvino/op/convert.hpp" +#include "openvino/op/gather_elements.hpp" +#include "openvino/op/greater.hpp" +#include "openvino/op/multiply.hpp" +#include "openvino/op/range.hpp" +#include "openvino/op/reshape.hpp" +#include "openvino/op/scatter_update.hpp" +#include "openvino/op/shape_of.hpp" +#include "openvino/op/slice.hpp" +#include "openvino/op/squeeze.hpp" +#include "openvino/op/transpose.hpp" +#include "openvino/op/unsqueeze.hpp" +#include "openvino/op/variadic_split.hpp" +#include "utils/attention.hpp" +#include "utils/common.hpp" +#include "utils/split.hpp" + +using namespace ov::op; +using namespace ov::frontend::onnx::attention; + +namespace ov { +namespace frontend { +namespace onnx { +namespace com_microsoft { +namespace detail { +namespace { + +// Reshape 3D input (batch, seq, num_heads * head_size) to 4D (batch, num_heads, seq, head_size) +ov::Output reshape_3d_to_4d(const ov::Output& input, int64_t num_heads) { + // Reshape to (batch, seq, num_heads, head_size) + auto reshape_pattern = v0::Constant::create(ov::element::i64, ov::Shape{4}, {0, 0, num_heads, -1}); + auto reshaped = std::make_shared(input, reshape_pattern, true); + // Transpose to (batch, num_heads, seq, head_size) + auto perm = v0::Constant::create(ov::element::i64, ov::Shape{4}, {0, 2, 1, 3}); + return std::make_shared(reshaped, perm); +} + +// Reshape 4D output (batch, num_heads, seq, head_size) back to 3D (batch, seq, num_heads * head_size) +ov::Output reshape_4d_to_3d(const ov::Output& output) { + // Transpose from (batch, num_heads, seq, head_size) to (batch, seq, num_heads, head_size) + auto perm = v0::Constant::create(ov::element::i64, ov::Shape{4}, {0, 2, 1, 3}); + auto transposed = std::make_shared(output, perm); + // Reshape to (batch, seq, num_heads * head_size) + auto reshape_pattern = v0::Constant::create(ov::element::i64, ov::Shape{3}, {0, 0, -1}); + return std::make_shared(transposed, reshape_pattern, true); +} + +// Split bias into Q, K, V parts and reshape each to (1, num_heads, 1, head_size) for broadcasting. +OutputVector split_bias(const Output& bias, + const Output& Q, + const Output& K, + const Output& V, + int64_t num_heads) { + auto Q_shape = std::make_shared(Q); + auto K_shape = std::make_shared(K); + auto V_shape = std::make_shared(V); + auto Q_head_depth = get_dimensions(Q_shape, {3}); + auto K_head_depth = get_dimensions(K_shape, {3}); + auto V_head_depth = get_dimensions(V_shape, {3}); + auto num_heads_node = ov::op::v0::Constant::create(ov::element::i64, Shape{1}, {num_heads}); + auto Q_hidden_dim = std::make_shared(Q_head_depth, num_heads_node); + auto K_hidden_dim = std::make_shared(K_head_depth, num_heads_node); + auto V_hidden_dim = std::make_shared(V_head_depth, num_heads_node); + + auto axis_node = v0::Constant::create(ov::element::i64, Shape{}, {0}); + auto split_lengths_node = std::make_shared(ov::NodeVector{Q_hidden_dim, K_hidden_dim, V_hidden_dim}, 0); + auto variadic_split = std::make_shared(bias, axis_node, split_lengths_node); + + auto outputs = variadic_split->outputs(); + auto reshape_pattern = v0::Constant::create(ov::element::i64, ov::Shape{4}, {1, num_heads, 1, -1}); + outputs[0] = std::make_shared(outputs[0], reshape_pattern, false); + outputs[1] = std::make_shared(outputs[1], reshape_pattern, false); + outputs[2] = std::make_shared(outputs[2], reshape_pattern, false); + return outputs; +} + +// Build 4D attention mask from key_padding_mask input, which can have rank 1, 2 or 3. +ov::Output build_mask(const ov::Output& key_padding_mask, + const ov::Output& Q, + const ov::Output& K, + float mask_filter_value) { + auto q_shape = std::make_shared(Q); + auto k_shape = std::make_shared(K); + // Q is 4D: (B, heads, seq_q, head_size), K is 4D: (B, heads, seq_kv, head_size) + auto seq_q = get_dimensions(q_shape, {2}); + auto seq_kv = get_dimensions(k_shape, {2}); + auto batch = get_dimensions(q_shape, {0}); + + auto zero = v0::Constant::create(ov::element::i64, ov::Shape{}, {0}); + auto zero_1d = v0::Constant::create(ov::element::i64, ov::Shape{1}, {0}); + auto one = v0::Constant::create(ov::element::i64, ov::Shape{}, {1}); + auto one_1d = v0::Constant::create(ov::element::i64, ov::Shape{1}, {1}); + + auto seq_q_scalar = std::make_shared(seq_q, zero); + auto seq_kv_scalar = std::make_shared(seq_kv, zero); + + auto key_padding_rank = key_padding_mask.get_partial_shape().rank(); + FRONT_END_GENERAL_CHECK(key_padding_rank.is_static(), "'key_padding_mask' rank must be static"); + ov::Output output_mask_3d; + if (key_padding_rank.get_length() == 1) { + // Column indices: [0, 1, ..., seq_kv-1] + auto col_indices = std::make_shared(zero, seq_kv_scalar, one, ov::element::i64); + + // Take only first B elements, in case of packed 3*B+2 mask, still only first B needed + auto sliced = std::make_shared(key_padding_mask, zero_1d, batch, one_1d, zero_1d); + // Unsqueeze mask to (B, 1) + auto converted = std::make_shared(sliced, ov::element::i64); + auto batch_indices = std::make_shared(converted, one_1d); + + auto is_allowed = std::make_shared(batch_indices, col_indices); + + auto mask_3d = std::make_shared(is_allowed, one_1d); // (B, 1, seq_kv) + + auto broadcast_shape = std::make_shared(ov::NodeVector{batch, seq_q, seq_kv}, 0); + output_mask_3d = std::make_shared(mask_3d, broadcast_shape); + } else if (key_padding_rank.get_length() == 2) { + auto mask_3d = std::make_shared(key_padding_mask, one_1d); // (B, 1, seq_kv) + auto converted = std::make_shared(mask_3d, ov::element::boolean); + auto broadcast_shape = std::make_shared(ov::NodeVector{batch, seq_q, seq_kv}, 0); + output_mask_3d = std::make_shared(converted, broadcast_shape); + } else { + FRONT_END_GENERAL_CHECK(key_padding_rank.get_length() == 3, + "Expected 'key_padding_mask' to have a rank of 1, 2 or 3, got: ", + key_padding_rank.get_length()); + output_mask_3d = std::make_shared(key_padding_mask, ov::element::boolean); + } + // Convert boolean to additive float mask: true -> 0.0, false -> -10000.0 + auto output_mask_4d = std::make_shared(output_mask_3d, one_1d); // (B, 1, seq_q, seq_kv) + return convert_boolean_mask(output_mask_4d, Q.get_element_type(), mask_filter_value); +} + +// Handle different input formats for Q, K, V: separate vs packed. Returns {Q, K, V} in (batch, num_heads, seq_len, +// head_size) and is_regular_QKV flag. +std::tuple prepare_qkv(const ov::frontend::onnx::Node& node, + const ov::OutputVector& inputs, + int64_t num_heads) { + ov::Output Q, K, V; + bool is_packed_QKV = !common::is_input_valid(node, 1) && !common::is_input_valid(node, 2); + bool is_packed_KV = !common::is_input_valid(node, 2) && !is_packed_QKV; + bool is_cross_attn = false; + + if (is_packed_QKV) { + // Packed QKV: split input 0 into Q, K, V + // In this case input[0] should have shape (batch_size, kv_sequence_length, num_heads, 3, head_size) + auto split = ov::op::util::make_split(inputs[0], 3, 3); + auto reshape_pattern = v0::Constant::create(ov::element::i64, ov::Shape{4}, {0, 0, 0, -1}); + auto reshaped_Q = std::make_shared(split[0], reshape_pattern, true); + auto reshaped_K = std::make_shared(split[1], reshape_pattern, true); + auto reshaped_V = std::make_shared(split[2], reshape_pattern, true); + auto perm = v0::Constant::create(ov::element::i64, ov::Shape{4}, {0, 2, 1, 3}); + Q = std::make_shared(reshaped_Q, perm); + K = std::make_shared(reshaped_K, perm); + V = std::make_shared(reshaped_V, perm); + } else if (is_packed_KV) { + // Packed KV: input 1 is K/V concatenated; split into K and V + // Q should be 3D in this case + Q = reshape_3d_to_4d(inputs[0], num_heads); + // In this case input[1] should have shape (batch_size, kv_sequence_length, num_heads, 2, head_size) + auto split = ov::op::util::make_split(inputs[1], 2, 3); + auto reshape_pattern = v0::Constant::create(ov::element::i64, ov::Shape{4}, {0, 0, 0, -1}); + auto reshaped_K = std::make_shared(split[0], reshape_pattern, true); + auto reshaped_V = std::make_shared(split[1], reshape_pattern, true); + auto perm = v0::Constant::create(ov::element::i64, ov::Shape{4}, {0, 2, 1, 3}); + K = std::make_shared(reshaped_K, perm); + V = std::make_shared(reshaped_V, perm); + } else { + Q = reshape_3d_to_4d(inputs[0], num_heads); + + K = inputs[1]; + V = inputs[2]; + // Check if this is cross-attention: K and V are 4D in this case + auto k_rank = K.get_partial_shape().rank(); + auto v_rank = V.get_partial_shape().rank(); + if (k_rank.is_static() && v_rank.is_static()) { + bool kv_is_3d = (k_rank.get_length() == 3) && (v_rank.get_length() == 3); + is_cross_attn = (k_rank.get_length() == 4) && (v_rank.get_length() == 4); + CHECK_VALID_NODE(node, + kv_is_3d || is_cross_attn, + "KV input rank must be 3 or 4, got: ", + k_rank.get_length(), + v_rank.get_length()); + } + if (!is_cross_attn) { + // For self-attention, reshape K and V to 4D + K = reshape_3d_to_4d(K, num_heads); + V = reshape_3d_to_4d(V, num_heads); + } + } + bool is_regular_QKV = !(is_packed_QKV || is_packed_KV || is_cross_attn); + return {{Q, K, V}, is_regular_QKV}; +} + +// Apply beam search cache indirection to reorder cached K/V by beam indices. +ov::OutputVector apply_cache_indirection(const ov::Output& cache_K, + const ov::Output& cache_V, + const ov::Output& indirection_input, + const ov::Output& past_seq_len_1d) { + auto zero_1d = v0::Constant::create(ov::element::i64, ov::Shape{1}, {0}); + auto one_1d = v0::Constant::create(ov::element::i64, ov::Shape{1}, {1}); + auto axis_2 = v0::Constant::create(ov::element::i64, ov::Shape{1}, {2}); + + ov::Output cache_indirection = + std::make_shared(indirection_input, zero_1d, past_seq_len_1d, one_1d, axis_2); + auto indirection_shape = std::make_shared(cache_indirection); + auto cache_shape = std::make_shared(cache_K); + auto num_beams = get_dimensions(indirection_shape, {1}); + auto indirection_batch = get_dimensions(indirection_shape, {0}); + auto cache_num_heads = get_dimensions(cache_shape, {1}); + auto cache_head_size = get_dimensions(cache_shape, {3}); + auto reshape_pattern = std::make_shared( + ov::OutputVector{indirection_batch, num_beams, cache_num_heads, past_seq_len_1d, cache_head_size}, + 0); + ov::Output cache_K_out = std::make_shared(cache_K, reshape_pattern, false); + ov::Output cache_V_out = std::make_shared(cache_V, reshape_pattern, false); + + auto indirection_reshape_pattern = + std::make_shared(ov::OutputVector{indirection_batch, num_beams, one_1d, past_seq_len_1d, one_1d}, + 0); + cache_indirection = std::make_shared(cache_indirection, indirection_reshape_pattern, false); + cache_indirection = std::make_shared(cache_indirection, reshape_pattern); + + // Gather with indirection: (B * beam_num, seq_kv) + cache_K_out = std::make_shared(cache_K_out, cache_indirection, 1); + cache_V_out = std::make_shared(cache_V_out, cache_indirection, 1); + + cache_K_out = std::make_shared(cache_K_out, cache_shape, false); + cache_V_out = std::make_shared(cache_V_out, cache_shape, false); + return {cache_K_out, cache_V_out}; +} + +// Handle KV cache: buffer sharing, cache indirection, and concatenation with current K/V. +// Modifies K, V by prepending cached values. Returns {present_key, present_value}. +ov::OutputVector apply_kv_cache(const ov::Output& K, + const ov::Output& V, + const ov::OutputVector& inputs, + bool has_buffer_sharing, + bool has_cache_indirection) { + ov::Output present_key, present_value; + ov::Output cache_K = inputs[6]; + ov::Output cache_V = inputs[7]; + if (has_buffer_sharing) { + auto past_seq_len_converted = std::make_shared(inputs[8], ov::element::i64); + auto new_shape = v0::Constant::create(ov::element::i64, ov::Shape{1}, {1}); + auto past_seq_len_1d = std::make_shared(past_seq_len_converted, new_shape, false); + + auto k_shape = std::make_shared(K); + auto seq_kv = get_dimensions(k_shape, {2}); + + // First, update the present values + auto start = std::make_shared(past_seq_len_1d); + auto end = std::make_shared(std::make_shared(past_seq_len_1d, seq_kv)); + auto step = v0::Constant::create(ov::element::i64, ov::Shape{}, {1}); + auto axis_2 = v0::Constant::create(ov::element::i64, ov::Shape{1}, {2}); + auto indices = std::make_shared(start, end, step, ov::element::i64); + present_key = std::make_shared(cache_K, indices, K, axis_2); + present_value = std::make_shared(cache_V, indices, V, axis_2); + + // Then slice the past values + auto zero_1d = v0::Constant::create(ov::element::i64, ov::Shape{1}, {0}); + auto one_1d = v0::Constant::create(ov::element::i64, ov::Shape{1}, {1}); + cache_K = std::make_shared(cache_K, zero_1d, past_seq_len_1d, one_1d, axis_2); + cache_V = std::make_shared(cache_V, zero_1d, past_seq_len_1d, one_1d, axis_2); + if (has_cache_indirection) { + auto kv_cache_with_indirection = apply_cache_indirection(cache_K, cache_V, inputs[9], past_seq_len_1d); + cache_K = kv_cache_with_indirection[0]; + cache_V = kv_cache_with_indirection[1]; + } + } + + auto K_out = std::make_shared(ov::OutputVector{cache_K, K}, 2); + auto V_out = std::make_shared(ov::OutputVector{cache_V, V}, 2); + // If buffer sharing is disabled, present_key and present_value are K and V after concatenation + if (!has_buffer_sharing) { + present_key = K_out; + present_value = V_out; + } + return {K_out, V_out, present_key, present_value}; +} + +} // namespace +} // namespace detail + +namespace opset_1 { +ov::OutputVector multi_head_attention(const ov::frontend::onnx::Node& node) { + auto inputs = node.get_ov_inputs(); + const auto num_inputs = inputs.size(); + CHECK_VALID_NODE(node, num_inputs >= 1, "MultiHeadAttention expects at least 1 input, got: ", num_inputs); + + // Attributes + int64_t num_heads = node.get_attribute_value("num_heads"); + float mask_filter_value = node.get_attribute_value("mask_filter_value", -10000.0f); + bool unidirectional = static_cast(node.get_attribute_value("unidirectional", 0)); + float scale_attr = node.get_attribute_value("scale", 0); + + // Required inputs + CHECK_VALID_NODE(node, num_heads > 0, "num_heads attribute should be > 0"); + auto [qkv, is_regular_QKV] = detail::prepare_qkv(node, inputs, num_heads); + auto Q = qkv[0], K = qkv[1], V = qkv[2]; + + // Optional inputs + bool has_bias = common::is_input_valid(node, 3); + bool has_key_padding_mask = common::is_input_valid(node, 4); + bool has_attention_bias = common::is_input_valid(node, 5); + bool has_past_key = common::is_input_valid(node, 6); + bool has_past_value = common::is_input_valid(node, 7); + bool has_buffer_sharing = common::is_input_valid(node, 8); + bool has_cache_indirection = common::is_input_valid(node, 9); + + CHECK_VALID_NODE(node, + !has_cache_indirection || has_buffer_sharing, + "cache_indirection is only supported in buffer_sharing mode"); + + CHECK_VALID_NODE(node, + !has_buffer_sharing || has_past_key, + "buffer_sharing is supported when past key/value are present"); + + CHECK_VALID_NODE(node, + has_past_key == has_past_value, + "past_key and past_value must be both present or both absent"); + CHECK_VALID_NODE(node, + !has_past_key || is_regular_QKV, + "past_key and past_value are only supported in unpacked 3D case"); + + if (has_bias) { + auto bias_splits = detail::split_bias(inputs[3], Q, K, V, num_heads); + Q = std::make_shared(Q, bias_splits[0]); + K = std::make_shared(K, bias_splits[1]); + V = std::make_shared(V, bias_splits[2]); + } + + // Handle KV cache + ov::Output present_key = K; + ov::Output present_value = V; + if (has_past_key) { + auto kv_cache_result = detail::apply_kv_cache(K, V, inputs, has_buffer_sharing, has_cache_indirection); + K = kv_cache_result[0]; + V = kv_cache_result[1]; + present_key = kv_cache_result[2]; + present_value = kv_cache_result[3]; + } + + // Prepare attention mask + ov::Output attn_mask; + if (has_key_padding_mask) { + attn_mask = inputs[4]; + auto attn_rank = attn_mask.get_partial_shape().rank(); + CHECK_VALID_NODE(node, attn_rank.is_static(), "Attention rank must be static, got rank: ", attn_rank); + + attn_mask = detail::build_mask(attn_mask, Q, K, mask_filter_value); + } + + // Build an explicit unidirectional mask instead of relying on SDPA's internal is_causal flag. + // SDPA's internal mask uses offset-based semantics (ncausal = kv_len - q_len + m + 1) + // which doesn't match the ONNX spec's np.tril(k=0) for non-square attention matrices + // (seq_q != seq_kv). For KV cache scenarios, use offset to account for past sequence. + if (unidirectional) { + auto unidirectional_mask = build_causal_mask(Q, K, has_past_key); + if (has_key_padding_mask) { + attn_mask = std::make_shared(attn_mask, unidirectional_mask); + } else { + attn_mask = unidirectional_mask; + } + has_key_padding_mask = true; + } + + if (has_attention_bias) { + auto attention_bias = inputs[5]; + auto bias_rank = attention_bias.get_partial_shape().rank(); + CHECK_VALID_NODE(node, + bias_rank.is_static() && bias_rank.get_length() == 4, + "attention_bias must have rank 4, got: ", + bias_rank); + if (has_key_padding_mask) { + attn_mask = std::make_shared(attn_mask, attention_bias); + } else { + attn_mask = attention_bias; + } + has_key_padding_mask = true; + } + + // Choose execution path + ov::Output Y; + ov::Output qk_debug_output; + + // Determine number of requested outputs + size_t num_outputs = node.get_outputs_size(); + const auto& output_names = node.get_output_names(); + bool needs_qk_output = output_names.size() > 3 && !output_names[3].get().empty(); + + if (needs_qk_output) { + // Manual decomposition path (softcap or debug output) + auto results = build_manual_attention(Q, + K, + V, + has_key_padding_mask, + attn_mask, + scale_attr, + 0.0f /*softcap*/, + false, + 0, + true); + Y = results[0]; + qk_debug_output = results[1]; + } else { + // SDPA path (primary fast path) + Y = build_sdpa(Q, K, V, has_key_padding_mask, attn_mask, scale_attr, false); + } + + Y = detail::reshape_4d_to_3d(Y); + + // Build output vector. + // Output names from the ONNX graph determine which outputs are actually requested. + // Empty names indicate unused optional outputs — push NullNode for those to avoid + // creating shared input/output parameters that confuse port resolution. + ov::OutputVector results; + results.push_back(Y); + + if (num_outputs > 1) { + if (!output_names[1].get().empty()) { + results.push_back(present_key); + } else { + results.push_back(std::make_shared()->output(0)); + } + } + if (num_outputs > 2) { + if (!output_names[2].get().empty()) { + results.push_back(present_value); + } else { + results.push_back(std::make_shared()->output(0)); + } + } + if (num_outputs > 3) { + if (qk_debug_output.get_node() && !output_names[3].get().empty()) { + results.push_back(qk_debug_output); + } else { + results.push_back(std::make_shared()->output(0)); + } + } + + return results; +} + +ONNX_OP("MultiHeadAttention", OPSET_SINCE(1), com_microsoft::opset_1::multi_head_attention, MICROSOFT_DOMAIN); +} // namespace opset_1 + +} // namespace com_microsoft +} // namespace onnx +} // namespace frontend +} // namespace ov diff --git a/src/frontends/onnx/frontend/src/op/com.microsoft/qlinear_activation.cpp b/src/frontends/onnx/frontend/src/op/com.microsoft/qlinear_activation.cpp index ed01fda79477..bd9ad77006e8 100644 --- a/src/frontends/onnx/frontend/src/op/com.microsoft/qlinear_activation.cpp +++ b/src/frontends/onnx/frontend/src/op/com.microsoft/qlinear_activation.cpp @@ -5,6 +5,7 @@ #include "core/null_node.hpp" #include "core/operator_set.hpp" #include "exceptions.hpp" +#include "openvino/decompositions/low_precision_dequantize.hpp" #include "openvino/frontend/exception.hpp" #include "openvino/op/add.hpp" #include "openvino/op/avg_pool.hpp" @@ -49,12 +50,9 @@ ov::OutputVector qlinear_activation(const ov::frontend::onnx::Node& node, const "Input tensor must be either int8 or uint8. Got: ", input_tensor.get_element_type()); - auto input_subtracted = std::make_shared(input_tensor, input_zero_point); - auto input_dequantized = - std::make_shared(std::make_shared(input_subtracted, input_scale.get_element_type()), - input_scale); + auto input_dequantized = ov::decomposition::low_precision_dequantize(input_tensor, input_scale, input_zero_point); - auto activation_result = activation_fn(input_dequantized); + auto activation_result = activation_fn(input_dequantized.get_node_shared_ptr()); auto scaled_result_float = std::make_shared(activation_result, output_scale); auto quantized_result = diff --git a/src/frontends/onnx/frontend/src/op/com.microsoft/qlinear_concat.cpp b/src/frontends/onnx/frontend/src/op/com.microsoft/qlinear_concat.cpp index 984a36262e85..e2ac3654e3da 100644 --- a/src/frontends/onnx/frontend/src/op/com.microsoft/qlinear_concat.cpp +++ b/src/frontends/onnx/frontend/src/op/com.microsoft/qlinear_concat.cpp @@ -1,16 +1,16 @@ // Copyright (C) 2018-2026 Intel Corporation // SPDX-License-Identifier: Apache-2.0 +// #include "core/null_node.hpp" #include "core/operator_set.hpp" #include "exceptions.hpp" +#include "openvino/decompositions/low_precision_dequantize.hpp" #include "openvino/frontend/exception.hpp" #include "openvino/op/add.hpp" #include "openvino/op/concat.hpp" #include "openvino/op/convert.hpp" #include "openvino/op/divide.hpp" -#include "openvino/op/multiply.hpp" -#include "openvino/op/subtract.hpp" #include "openvino/op/util/op_types.hpp" #include "utils/common.hpp" @@ -33,23 +33,18 @@ ov::OutputVector qlinear_concat(const ov::frontend::onnx::Node& node) { auto Y_zero_point = ov::op::util::is_null(inputs[1]) ? v0::Constant::create(Y_scale.get_element_type(), {}, {0}) : inputs[1]; - std::vector> dequantized_inputs; + ov::OutputVector dequantized_inputs; for (size_t i = 2; i < inputs.size(); i += 3) { auto X = inputs[i]; auto X_scale = inputs[i + 1]; auto X_zero_point = ov::op::util::is_null(inputs[i + 2]) ? v0::Constant::create(X.get_element_type(), {}, {0}) : inputs[i + 2]; - auto X_minus_zero_point = std::make_shared(X, X_zero_point); - auto X_minus_zero_point_float = std::make_shared(X_minus_zero_point, X_scale.get_element_type()); - auto dequantized_X = std::make_shared(X_scale, X_minus_zero_point_float); - - dequantized_inputs.push_back(dequantized_X); + dequantized_inputs.push_back(ov::decomposition::low_precision_dequantize(X, X_scale, X_zero_point)); } auto axis = node.get_attribute_value("axis"); - auto concatenated = - std::make_shared(ov::OutputVector(dequantized_inputs.begin(), dequantized_inputs.end()), axis); + auto concatenated = std::make_shared(dequantized_inputs, axis); auto requantized = std::make_shared(concatenated, Y_scale); auto Y_zero_point_float = std::make_shared(Y_zero_point, Y_scale.get_element_type()); diff --git a/src/frontends/onnx/frontend/src/op/com.microsoft/qlinear_ops.cpp b/src/frontends/onnx/frontend/src/op/com.microsoft/qlinear_ops.cpp index 5998b65f78d5..ed3b62a1f7ee 100644 --- a/src/frontends/onnx/frontend/src/op/com.microsoft/qlinear_ops.cpp +++ b/src/frontends/onnx/frontend/src/op/com.microsoft/qlinear_ops.cpp @@ -5,14 +5,13 @@ #include "core/null_node.hpp" #include "core/operator_set.hpp" #include "exceptions.hpp" +#include "openvino/decompositions/low_precision_dequantize.hpp" #include "openvino/frontend/exception.hpp" #include "openvino/op/add.hpp" #include "openvino/op/constant.hpp" #include "openvino/op/convert.hpp" #include "openvino/op/divide.hpp" #include "openvino/op/multiply.hpp" -#include "openvino/op/reshape.hpp" -#include "openvino/op/subtract.hpp" #include "openvino/op/util/op_types.hpp" #include "utils/common.hpp" @@ -59,14 +58,8 @@ ov::OutputVector qlinear_op(const ov::frontend::onnx::Node& node, BinaryOp binar ", B_zero_point: ", B_zero_point.get_element_type()); - auto A_minus_zero_point = std::make_shared(A, A_zero_point); - auto B_minus_zero_point = std::make_shared(B, B_zero_point); - - auto A_minus_zero_point_float = std::make_shared(A_minus_zero_point, A_scale.get_element_type()); - auto B_minus_zero_point_float = std::make_shared(B_minus_zero_point, B_scale.get_element_type()); - - auto A_scaled = std::make_shared(A_scale, A_minus_zero_point_float); - auto B_scaled = std::make_shared(B_scale, B_minus_zero_point_float); + auto A_scaled = ov::decomposition::low_precision_dequantize(A, A_scale, A_zero_point); + auto B_scaled = ov::decomposition::low_precision_dequantize(B, B_scale, B_zero_point); auto result_scaled = binary_op(A_scaled, B_scaled); diff --git a/src/frontends/onnx/frontend/src/op/com.microsoft/qlinear_where.cpp b/src/frontends/onnx/frontend/src/op/com.microsoft/qlinear_where.cpp index 744e57292927..411ec4131f21 100644 --- a/src/frontends/onnx/frontend/src/op/com.microsoft/qlinear_where.cpp +++ b/src/frontends/onnx/frontend/src/op/com.microsoft/qlinear_where.cpp @@ -1,15 +1,15 @@ // Copyright (C) 2018-2026 Intel Corporation // SPDX-License-Identifier: Apache-2.0 +// #include "core/operator_set.hpp" #include "exceptions.hpp" +#include "openvino/decompositions/low_precision_dequantize.hpp" #include "openvino/frontend/exception.hpp" #include "openvino/op/add.hpp" #include "openvino/op/convert.hpp" #include "openvino/op/divide.hpp" -#include "openvino/op/multiply.hpp" #include "openvino/op/select.hpp" -#include "openvino/op/subtract.hpp" #include "utils/common.hpp" using namespace ov::op; @@ -33,14 +33,8 @@ ov::OutputVector qlinear_where(const ov::frontend::onnx::Node& node) { auto z_scale = node.get_ov_inputs().at(7); auto z_zero_point = node.get_ov_inputs().at(8); - auto x_minus_zero_point = std::make_shared(x, x_zero_point); - auto y_minus_zero_point = std::make_shared(y, y_zero_point); - - auto x_minus_zero_point_float = std::make_shared(x_minus_zero_point, x_scale.get_element_type()); - auto y_minus_zero_point_float = std::make_shared(y_minus_zero_point, y_scale.get_element_type()); - - auto x_dequant = std::make_shared(x_scale, x_minus_zero_point_float); - auto y_dequant = std::make_shared(y_scale, y_minus_zero_point_float); + auto x_dequant = ov::decomposition::low_precision_dequantize(x, x_scale, x_zero_point); + auto y_dequant = ov::decomposition::low_precision_dequantize(y, y_scale, y_zero_point); auto selected = std::make_shared(condition, x_dequant, y_dequant); diff --git a/src/frontends/onnx/frontend/src/op/com.microsoft/rotary_embedding.cpp b/src/frontends/onnx/frontend/src/op/com.microsoft/rotary_embedding.cpp index 6c8a4aa74342..da05c7e7f220 100644 --- a/src/frontends/onnx/frontend/src/op/com.microsoft/rotary_embedding.cpp +++ b/src/frontends/onnx/frontend/src/op/com.microsoft/rotary_embedding.cpp @@ -8,27 +8,19 @@ #include "exceptions.hpp" #include "openvino/core/graph_util.hpp" #include "openvino/core/rt_info.hpp" +#include "openvino/decompositions/rope.hpp" #include "openvino/frontend/exception.hpp" #include "openvino/op/add.hpp" -#include "openvino/op/broadcast.hpp" #include "openvino/op/concat.hpp" #include "openvino/op/constant.hpp" -#include "openvino/op/convert.hpp" #include "openvino/op/gather.hpp" -#include "openvino/op/greater.hpp" -#include "openvino/op/greater_eq.hpp" #include "openvino/op/multiply.hpp" -#include "openvino/op/range.hpp" #include "openvino/op/reshape.hpp" -#include "openvino/op/scaled_dot_product_attention.hpp" -#include "openvino/op/select.hpp" #include "openvino/op/shape_of.hpp" -#include "openvino/op/slice.hpp" #include "openvino/op/split.hpp" -#include "openvino/op/squeeze.hpp" -#include "openvino/op/subtract.hpp" #include "openvino/op/transpose.hpp" #include "openvino/op/unsqueeze.hpp" +#include "openvino/op/variadic_split.hpp" #include "openvino/pass/pattern/op/wrap_type.hpp" #include "utils/common.hpp" #include "utils/reshape.hpp" @@ -41,14 +33,6 @@ namespace onnx { namespace com_microsoft { namespace opset_1 { -ov::OutputVector make_split(const ov::Output& value, int64_t num_splits, int64_t axis) { - using namespace ov::op; - const auto axis_node = v0::Constant::create(ov::element::i64, ov::Shape{}, {axis}); - const auto split = std::make_shared(value, axis_node, num_splits); - - return split->outputs(); -} - std::shared_ptr get_dimensions(const std::shared_ptr& shape, const std::vector& dims) { static const auto zero = v0::Constant::create(ov::element::i32, ov::Shape{}, {0}); const auto dims_const = v0::Constant::create(ov::element::i32, ov::Shape{dims.size()}, dims); @@ -60,7 +44,7 @@ ov::OutputVector rotary_embedding(const ov::frontend::onnx::Node& node) { // Original documentation: // https://github.com/microsoft/onnxruntime/blob/main/docs/ContribOperators.md#com.microsoft.RotaryEmbedding const auto inputs = node.get_ov_inputs(); - const auto& input = inputs[0]; // [bs,seqlen,hidden] or [bs,num_heads,seqlen,headsize] + const auto& input = inputs[0]; // [bs,seqlen,hidden] or [bs,num_heads,seqlen,head_size] const auto& position_ids = inputs[1]; // [seqlen] or [bs, seqlen] const auto& cos_cache = inputs[2]; // [max_seqlen, head_size/2] const auto& sin_cache = inputs[3]; // [max_seqlen, head_size/2] @@ -68,7 +52,6 @@ ov::OutputVector rotary_embedding(const ov::frontend::onnx::Node& node) { const auto interleaved = node.get_attribute_value("interleaved"); // required const auto minus_one = v0::Constant::create(ov::element::i64, ov::Shape{1}, {-1}); const auto zero = v0::Constant::create(ov::element::i64, ov::Shape{1}, {0}); - const auto one = v0::Constant::create(ov::element::i64, ov::Shape{1}, {1}); const auto two = v0::Constant::create(ov::element::i64, ov::Shape{1}, {2}); const auto cos = std::make_shared(cos_cache, @@ -89,7 +72,8 @@ ov::OutputVector rotary_embedding(const ov::frontend::onnx::Node& node) { last_dim.is_static(), "cos_cache last dimension must be static to derive head size, got: ", cos_cache_shape); - const auto headsize_val = static_cast(last_dim.get_length() * 2); + const auto half_head_size_val = static_cast(last_dim.get_length()); + const auto head_size_val = half_head_size_val * 2; const auto input_shape = std::make_shared(input); const auto input_rank = input.get_partial_shape().rank(); @@ -98,46 +82,67 @@ ov::OutputVector rotary_embedding(const ov::frontend::onnx::Node& node) { ov::Output input_4d = input; if (input_is_3d) { - const auto headsize = v0::Constant::create(ov::element::i64, ov::Shape{1}, {headsize_val}); + const auto headsize = v0::Constant::create(ov::element::i64, ov::Shape{1}, {head_size_val}); const auto input_shape_prev_2 = get_dimensions(input_shape, {0, 1}); auto new_input_shape = std::make_shared(ov::NodeVector{input_shape_prev_2, minus_one, headsize}, 0); auto input_reshaped = - std::make_shared(input, new_input_shape, false); // [bs,seqlen,num_heads,headsize] - input_4d = std::make_shared(input_reshaped, perm); // [bs,num_heads,seqlen,headsize] + std::make_shared(input, new_input_shape, false); // [bs,seqlen,num_heads,head_size] + input_4d = std::make_shared(input_reshaped, perm); // [bs,num_heads,seqlen,head_size] } - ov::Output output; - if (interleaved) { - auto input_4d_shape = std::make_shared(input_4d); - auto dim_bns = get_dimensions(input_4d_shape, {0, 1, 2}); - auto half_head_size = v0::Constant::create(ov::element::i64, ov::Shape{1}, {last_dim.get_length()}); - auto split_input_shape = std::make_shared(ov::NodeVector{dim_bns, half_head_size, two}, 0); - auto reshaped_input = std::make_shared(input_4d, split_input_shape, false); - - auto in_split = make_split(reshaped_input, 2, -1); - split_input_shape = std::make_shared(ov::NodeVector{dim_bns, half_head_size}, 0); - auto in_split_0 = std::make_shared(in_split[0], split_input_shape, false); - auto in_split_1 = std::make_shared(in_split[1], split_input_shape, false); - - auto res_0 = std::make_shared(std::make_shared(in_split_0, cos), - std::make_shared(in_split_1, sin)); - auto res_1 = std::make_shared(std::make_shared(in_split_0, sin), - std::make_shared(in_split_1, cos)); - - split_input_shape = std::make_shared(ov::NodeVector{dim_bns, half_head_size, one}, 0); - auto res_0_5d = std::make_shared(res_0, split_input_shape, false); - auto res_1_5d = std::make_shared(res_1, split_input_shape, false); - - auto concat_ret = std::make_shared(ov::NodeVector{res_0_5d, res_1_5d}, -1); - output = std::make_shared(concat_ret, input_4d_shape, - false); // [bs,num_heads,seqlen,headsize] + // Unsqueeze cos/sin to 4D [?, 1, ?, head_size/2] to match RoPE fusion pattern + ov::Output cos_4d, sin_4d; + const auto cos_out_rank = cos->get_output_partial_shape(0).rank(); + if (cos_out_rank.is_static() && cos_out_rank.get_length() == 2) { + // cos is [seqlen, head_size/2] → [1, 1, seqlen, head_size/2] + auto axes = v0::Constant::create(ov::element::i64, ov::Shape{2}, {0, 1}); + cos_4d = std::make_shared(cos, axes); + sin_4d = std::make_shared(sin, axes); } else { - auto in_split = make_split(input_4d, 2, -1); // [bs,num_heads,seqlen,headsize/2] - auto res_0 = std::make_shared(std::make_shared(in_split[0], cos), - std::make_shared(in_split[1], sin)); - auto res_1 = std::make_shared(std::make_shared(in_split[0], sin), - std::make_shared(in_split[1], cos)); - output = std::make_shared(ov::NodeVector{res_0, res_1}, -1); // [bs,num_heads,seqlen,headsize] + // cos is [bs, seqlen, head_size/2] → [bs, 1, seqlen, head_size/2] + auto axes = v0::Constant::create(ov::element::i64, ov::Shape{1}, {1}); + cos_4d = std::make_shared(cos, axes); + sin_4d = std::make_shared(sin, axes); + } + + // For interleaved mode, deinterleave first so the core RoPE formula is identical + ov::Output rope_input = input_4d; + std::shared_ptr input_4d_shape; + std::shared_ptr dim_bns; + std::shared_ptr half_head_size; + std::shared_ptr perm_5d; + if (interleaved) { + input_4d_shape = std::make_shared(input_4d); + dim_bns = get_dimensions(input_4d_shape, {0, 1, 2}); + half_head_size = v0::Constant::create(ov::element::i64, ov::Shape{1}, {half_head_size_val}); + perm_5d = v0::Constant::create(ov::element::i64, ov::Shape{5}, {0, 1, 2, 4, 3}); + + // Deinterleave: [bs,num_heads,seqlen,head_size] + // → reshape [bs,num_heads,seqlen,head_size/2,2] + // → transpose [bs,num_heads,seqlen,2,head_size/2] + // → reshape [bs,num_heads,seqlen,head_size] (now [first_half, second_half]) + auto deinterleave_5d = std::make_shared(ov::NodeVector{dim_bns, half_head_size, two}, 0); + auto reshaped_5d = std::make_shared(input_4d, deinterleave_5d, false); + auto transposed_5d = std::make_shared(reshaped_5d, perm_5d); + rope_input = std::make_shared(transposed_5d, input_4d_shape, false); + } + + // Core RoPE formula via shared decomposition helper. + ov::pass::NodeRegistry rope_reg; + ov::Output output = ov::decomposition::rope(rope_reg, rope_input, cos_4d, sin_4d, half_head_size_val); + + // For interleaved mode, re-interleave the result + if (interleaved) { + // Re-interleave: [bs,num_heads,seqlen,head_size] + // → reshape [bs,num_heads,seqlen,2,head_size/2] + // → transpose [bs,num_heads,seqlen,head_size/2,2] + // → reshape [bs,num_heads,seqlen,head_size] + auto reinterleave_5d = std::make_shared(ov::NodeVector{dim_bns, two, half_head_size}, 0); + auto result_5d = std::make_shared(output, reinterleave_5d, false); + auto result_transposed = std::make_shared(result_5d, perm_5d); + output = std::make_shared(result_transposed, + input_4d_shape, + false); // [bs,num_heads,seqlen,head_size] } if (input_is_3d) { diff --git a/src/frontends/onnx/frontend/src/op/dequantize_linear.cpp b/src/frontends/onnx/frontend/src/op/dequantize_linear.cpp index 3d472c2d4f05..5efcf0898397 100644 --- a/src/frontends/onnx/frontend/src/op/dequantize_linear.cpp +++ b/src/frontends/onnx/frontend/src/op/dequantize_linear.cpp @@ -8,6 +8,7 @@ #include "core/null_node.hpp" #include "core/operator_set.hpp" #include "openvino/core/validation_util.hpp" +#include "openvino/decompositions/low_precision_dequantize.hpp" #include "openvino/frontend/exception.hpp" #include "openvino/op/constant.hpp" #include "openvino/op/convert.hpp" @@ -18,7 +19,6 @@ #include "openvino/op/subtract.hpp" #include "openvino/op/transpose.hpp" #include "openvino/op/unsqueeze.hpp" -#include "transformations/rt_info/disable_constant_folding.hpp" #include "utils/common.hpp" #include "utils/reshape.hpp" using namespace ov::op; @@ -58,14 +58,12 @@ ov::OutputVector dequantize_linear(const ov::frontend::onnx::Node& node, int64_t common::validate_scalar_input("Dequantization scale", scale.get_node_shared_ptr(), valid_types); - const auto converted_x = std::make_shared(x, scale.get_element_type()); - if (zero_point) { common::validate_scalar_input("Zero point", zero_point); - return {std::make_shared(std::make_shared(converted_x, zero_point), scale)}; - } else { - return {std::make_shared(converted_x, scale)}; } + + auto result = ov::decomposition::low_precision_dequantize(x, scale, zero_point); + return {result}; } } // namespace detail @@ -165,16 +163,15 @@ ov::OutputVector dequantize_linear(const ov::Output& x, validate_scale(scale, x, axis); const auto scale_reshaped = reshape_input(scale, axis, x_shape); - const auto converted_x = std::make_shared(x, scale.get_element_type()); + ov::Output zp; if (zero_point) { validate_zero_point(zero_point, x, axis); - return {std::make_shared( - std::make_shared(converted_x, reshape_input(zero_point, axis, x_shape)), - scale_reshaped)}; - } else { - return {std::make_shared(converted_x, scale_reshaped)}; + zp = reshape_input(zero_point, axis, x_shape); } + + auto result = ov::decomposition::low_precision_dequantize(x, scale_reshaped, zp); + return {result}; } } // namespace detail @@ -252,13 +249,10 @@ ov::OutputVector dequantize_linear(const ov::frontend::onnx::Node& node) { // Check if this is channel-wise quantization (block_size equals dimension size at axis) bool is_cw_quantize = (src_x.get_shape()[axis] == block_size); if (is_cw_quantize) { - ov::Output converted_x = std::make_shared(src_x, scale.get_element_type()); if (inputs.size() > 2) { zp = inputs[2]; - zp = std::make_shared(zp, scale.get_element_type()); - converted_x = std::make_shared(converted_x, zp); } - auto scaled_x = std::make_shared(converted_x, scale); + auto scaled_x = ov::decomposition::low_precision_dequantize(src_x, scale, zp); return {scaled_x}; } @@ -286,34 +280,18 @@ ov::OutputVector dequantize_linear(const ov::frontend::onnx::Node& node) { const auto& unsqueezed_axes = std::make_shared(ov::element::i64, Shape{1}, std::vector{unsqueeze_axis}); - const auto scale_type = scale.get_element_type(); if (inputs.size() > 2) { - zp = inputs[2]; - zp = std::make_shared(zp, unsqueezed_axes); - if (zp.get_element_type() != scale.get_element_type()) { - zp = std::make_shared(zp, scale_type); - } + zp = std::make_shared(inputs[2], unsqueezed_axes); } - const auto& x = src_x.get_element_type() == scale_type ? broadcastable_x - : std::make_shared(broadcastable_x, scale_type); - // Adding additional dimension for broadcasting scale = std::make_shared(scale, unsqueezed_axes); - if (zp.get_node_shared_ptr()) { - broadcastable_x = std::make_shared(x, zp); - } else { - broadcastable_x = x; - } - - const auto& scaled_x = std::make_shared(broadcastable_x, scale); + auto out_shape = std::make_shared(src_x); - // Returning back a shape - const auto& reshaped_scaled_x = - std::make_shared(scaled_x, std::make_shared(src_x), false); + auto reshaped_scaled_x = ov::decomposition::low_precision_dequantize(broadcastable_x, scale, zp, out_shape); - reshaped_scaled_x->set_friendly_name(node.get_name()); + reshaped_scaled_x.get_node_shared_ptr()->set_friendly_name(node.get_name()); return {reshaped_scaled_x}; } diff --git a/src/frontends/onnx/frontend/src/op/expand.cpp b/src/frontends/onnx/frontend/src/op/expand.cpp index c93b42dcefa0..420c6988552d 100644 --- a/src/frontends/onnx/frontend/src/op/expand.cpp +++ b/src/frontends/onnx/frontend/src/op/expand.cpp @@ -18,10 +18,11 @@ ov::OutputVector expand(const ov::frontend::onnx::Node& node) { const ov::Output data{node.get_ov_inputs().at(0)}; const ov::Output shape{node.get_ov_inputs().at(1)}; - if (common::is_failsafe_node(shape.get_node_shared_ptr())) { - // in case the "shape" input is connected to a failsafe node created in place of an invalid initializer - // the target shape should be ignored and this Expand operation should not modify its input tensor - // the Broadcast created below should be eliminated later on by an appropriate optimization pass + if (common::is_failsafe_node(shape.get_node_shared_ptr()) || + common::is_constant_empty_node(shape.get_node_shared_ptr())) { + // Ignore an unusable target shape, such as a failsafe node created for an invalid + // initializer or an empty constant. Use an identity broadcast so Expand preserves + // the input tensor, and let a later optimization pass eliminate this Broadcast. const auto identity_broadcast = v0::Constant::create(ov::element::i64, ov::Shape{1}, {1}); return {std::make_shared(data, identity_broadcast, ov::op::BroadcastType::BIDIRECTIONAL)}; } else { diff --git a/src/frontends/onnx/frontend/src/op/if.cpp b/src/frontends/onnx/frontend/src/op/if.cpp index f38f6c2412fb..4854f904e816 100644 --- a/src/frontends/onnx/frontend/src/op/if.cpp +++ b/src/frontends/onnx/frontend/src/op/if.cpp @@ -8,6 +8,8 @@ #include "core/operator_set.hpp" #include "openvino/core/model.hpp" #include "openvino/frontend/exception.hpp" +#include "openvino/op/constant.hpp" +#include "openvino/op/parameter.hpp" #include "translate_session.hpp" using namespace ov::op; @@ -123,6 +125,31 @@ ov::OutputVector if_op(const ov::frontend::onnx::Node& node) { auto then_results = then_branch->get_results(); auto else_results = else_branch->get_results(); + + // The InputModel for subgraphs may register parent-scope tensors (that are + // also outputs of the parent graph) as additional subgraph outputs. Trim + // branches to the ONNX node's declared output count, but only trim results + // that are parent-scope pass-throughs (Parameter → Result chains), not + // genuine computation outputs. + const size_t expected_outputs = node.get_outputs_size(); + auto trim_parent_scope_results = [](ov::ResultVector& branch_results, size_t target) { + while (branch_results.size() > target) { + auto last = branch_results.back(); + auto src = last->input_value(0).get_node_shared_ptr(); + // Accept: Parameter, Constant, or single-input node wrapping Parameter/Constant + bool is_passthrough = ov::is_type(src) || ov::is_type(src); + if (!is_passthrough && src->get_input_size() == 1 && src->get_output_size() == 1) { + auto inner = src->input_value(0).get_node_shared_ptr(); + is_passthrough = ov::is_type(inner) || ov::is_type(inner); + } + if (!is_passthrough) + break; + branch_results.pop_back(); + } + }; + trim_parent_scope_results(then_results, expected_outputs); + trim_parent_scope_results(else_results, expected_outputs); + FRONT_END_GENERAL_CHECK(then_results.size() == else_results.size(), "'then' and 'else' branches have to have the same number of outputs"); int output_size = static_cast(then_results.size()); diff --git a/src/frontends/onnx/frontend/src/op/loop.cpp b/src/frontends/onnx/frontend/src/op/loop.cpp index 31570e6c8c16..0de92ceeb109 100644 --- a/src/frontends/onnx/frontend/src/op/loop.cpp +++ b/src/frontends/onnx/frontend/src/op/loop.cpp @@ -100,7 +100,14 @@ namespace detail { ov::OutputVector loop_legacy(const ov::frontend::onnx::Node& node) { const auto& ng_inputs = node.get_ov_inputs(); - const ov::OutputVector loop_carried_dependencies{std::next(ng_inputs.begin(), 2), ng_inputs.end()}; + constexpr size_t control_inputs_count = 2; + + FRONT_END_GENERAL_CHECK( + ng_inputs.size() >= control_inputs_count, + "Expecting at least two canonical inputs for Loop op: trip count and termination condition"); + + const ov::OutputVector loop_carried_dependencies{std::next(ng_inputs.begin(), control_inputs_count), + ng_inputs.end()}; const auto& subgraphs = node.get_subgraphs(); auto body_graph_it = subgraphs.find("body"); @@ -109,10 +116,32 @@ ov::OutputVector loop_legacy(const ov::frontend::onnx::Node& node) { auto body_outputs = body_graph->get_ov_outputs(); const auto& body_inputs = body_graph->get_ng_parameters(); + // NOTE: We're relaxing the check on body_inputs size here to allow extra parameters that are not loop-carried + // dependencies and are not control inputs (iteration number and termination condition). These extra parameters are + // expected to be invariant inputs that are wired from the parent graph via Loop::set_invariant_input, and their + // presence does not violate ONNX spec as long as the loop body graph is well-formed and can be executed by ONNX + // Runtime. + CHECK_VALID_NODE(node, + body_inputs.size() >= control_inputs_count && + body_inputs.size() - control_inputs_count >= loop_carried_dependencies.size(), + "The provided loop body graph canonical inputs size (", + body_inputs.size(), + "), does not match the sum of loop carried dependencies " + "and two mandatory inputs (", + loop_carried_dependencies.size() + control_inputs_count, + ")"); + + CHECK_VALID_NODE(node, + body_outputs.size() >= 1 && body_outputs.size() - 1 >= loop_carried_dependencies.size(), + "The provided loop body graph outputs size (", + body_outputs.size(), + ") is smaller than number of outputs. Required at least: ", + loop_carried_dependencies.size() + 1); + // Infer loop body inputs' element type based on carried dependencies for (size_t i = 0; i < loop_carried_dependencies.size(); i++) { - body_inputs[i + 2]->set_element_type(loop_carried_dependencies[i].get_element_type()); - body_inputs[i + 2]->set_partial_shape(loop_carried_dependencies[i].get_partial_shape()); + body_inputs[i + control_inputs_count]->set_element_type(loop_carried_dependencies[i].get_element_type()); + body_inputs[i + control_inputs_count]->set_partial_shape(loop_carried_dependencies[i].get_partial_shape()); } // optional inputs @@ -165,30 +194,13 @@ ov::OutputVector loop_legacy(const ov::frontend::onnx::Node& node) { body_outputs[0] = v0::Constant::create(ov::element::boolean, {1}, {true}); // Construct body_params without the condition parameter (body_inputs[1]) body_params = ov::ParameterVector{body_inputs[0]}; - body_params.insert(body_params.end(), body_inputs.begin() + 2, body_inputs.end()); + body_params.insert(body_params.end(), body_inputs.begin() + control_inputs_count, body_inputs.end()); needs_condition_param = false; } else { // Construct body_params with all body_inputs body_params = ov::ParameterVector(body_inputs.begin(), body_inputs.end()); } - CHECK_VALID_NODE(node, - body_inputs.size() >= loop_carried_dependencies.size() + 2, - "The provided loop body graph inputs size (", - body_inputs.size(), - "), is not greater than the sum of loop carried dependencies " - "and two mandatory" - " inputs (", - loop_carried_dependencies.size() + 2, - ")"); - - CHECK_VALID_NODE(node, - body_outputs.size() >= loop_carried_dependencies.size() + 1, - "The provided loop body graph outputs size (", - body_outputs.size(), - ") is not greater than number of outputs. Required at least: ", - loop_carried_dependencies.size() + 1); - const auto body = std::make_shared(body_outputs, body_params); auto loop = std::make_shared(trip_count, termination_cond); v5::Loop::SpecialBodyPorts spec_ports{0, 0}; @@ -200,7 +212,7 @@ ov::OutputVector loop_legacy(const ov::frontend::onnx::Node& node) { } // Setting up other Loop body inputs. // body_inputs[0] is iteration number, body_inputs[1] is termination condition - auto body_inputs_it = std::next(body_inputs.begin(), 2); + auto body_inputs_it = std::next(body_inputs.begin(), control_inputs_count); // body_outputs[0] is termination condition output auto body_outputs_it = std::next(body_outputs.begin(), 1); @@ -246,7 +258,14 @@ ov::OutputVector loop_legacy(const ov::frontend::onnx::Node& node) { ov::OutputVector loop(const ov::frontend::onnx::Node& node) { const auto& ng_inputs = node.get_ov_inputs(); - const ov::OutputVector loop_carried_dependencies{std::next(ng_inputs.begin(), 2), ng_inputs.end()}; + constexpr size_t control_inputs_count = 2; + + FRONT_END_GENERAL_CHECK( + ng_inputs.size() >= control_inputs_count, + "Expecting at least two canonical inputs for Loop op: trip count and termination condition"); + + const ov::OutputVector loop_carried_dependencies{std::next(ng_inputs.begin(), control_inputs_count), + ng_inputs.end()}; auto body_graph = node.get_attribute_value>("body"); const auto& body_results = body_graph->get_results(); @@ -276,15 +295,31 @@ ov::OutputVector loop(const ov::frontend::onnx::Node& node) { } body_outputs = std::move(filtered_body_outputs); + // The InputModel for subgraphs may register parent-scope constants that are + // also outputs of the parent graph as additional body outputs (initializer + // declared as a body graph output without being consumed by any node). + // The Loop body must have exactly 1 (termination cond) + node.get_outputs_size() + // outputs, so trim trailing pass-through Constants that are not real loop + // outputs. + const size_t expected_body_outputs = 1 + node.get_outputs_size(); + while (body_outputs.size() > expected_body_outputs && + ov::is_type(body_outputs.back().get_node_shared_ptr())) { + body_outputs.pop_back(); + } + CHECK_VALID_NODE(node, - canonical_inputs.size() >= 2, - "The provided loop body graph inputs size (", + canonical_inputs.size() >= control_inputs_count && + canonical_inputs.size() - control_inputs_count == loop_carried_dependencies.size(), + "The provided loop body graph canonical inputs size (", canonical_inputs.size(), - ") is not greater than the mandatory iteration and condition inputs (2)"); + "), does not match the sum of loop carried dependencies " + "and two mandatory inputs (", + loop_carried_dependencies.size() + control_inputs_count, + ")"); auto iteration_param = canonical_inputs[0]; auto condition_param = canonical_inputs[1]; - ov::ParameterVector state_parameters(canonical_inputs.begin() + 2, canonical_inputs.end()); + ov::ParameterVector state_parameters(canonical_inputs.begin() + control_inputs_count, canonical_inputs.end()); const auto default_trip_count = v0::Constant::create(ov::element::i64, {1}, {-1}); const auto true_condition = v0::Constant::create(ov::element::boolean, {1}, {true}); @@ -338,14 +373,6 @@ ov::OutputVector loop(const ov::frontend::onnx::Node& node) { } } - CHECK_VALID_NODE(node, - state_parameters.size() == loop_carried_dependencies.size(), - "The provided loop body state parameters size (", - state_parameters.size(), - ") must match the number of loop carried dependencies (", - loop_carried_dependencies.size(), - ")"); - const auto mapped = loop_carried_dependencies.size(); // Infer loop body inputs' element type based on carried dependencies @@ -355,11 +382,11 @@ ov::OutputVector loop(const ov::frontend::onnx::Node& node) { } CHECK_VALID_NODE(node, - body_outputs.size() >= state_parameters.size() + 1, + body_outputs.size() >= 1 && body_outputs.size() - 1 >= loop_carried_dependencies.size(), "The provided loop body graph outputs size (", body_outputs.size(), - ") is not greater than number of outputs. Required at least: ", - state_parameters.size() + 1); + ") is smaller than number of outputs. Required at least: ", + loop_carried_dependencies.size() + 1); ov::ParameterVector body_params; body_params.push_back(iteration_param); diff --git a/src/frontends/onnx/frontend/src/op/reduce.cpp b/src/frontends/onnx/frontend/src/op/reduce.cpp index 92acbe1754ae..a80cebcde393 100644 --- a/src/frontends/onnx/frontend/src/op/reduce.cpp +++ b/src/frontends/onnx/frontend/src/op/reduce.cpp @@ -5,6 +5,7 @@ #include "core/operator_set.hpp" #include "exceptions.hpp" #include "openvino/frontend/exception.hpp" +#include "openvino/op/add.hpp" #include "openvino/op/constant.hpp" #include "openvino/op/convert.hpp" #include "openvino/op/exp.hpp" @@ -21,6 +22,7 @@ #include "openvino/op/reduce_sum.hpp" #include "openvino/op/shape_of.hpp" #include "openvino/op/squeeze.hpp" +#include "openvino/op/subtract.hpp" #include "utils/common.hpp" using namespace ov::op; @@ -149,19 +151,61 @@ std::shared_ptr onnx_reduce_sum_square(const ov::frontend::onnx::Node& const auto square_node = std::make_shared(input, input); return make_ov_reduction_op(node, square_node, supported_types, axes_as_attr); } + +ov::OutputVector onnx_reduce_log_sum_exp_stable(const ov::frontend::onnx::Node& node, bool axes_as_attr = true) { + auto input = node.get_ov_inputs().at(0); + auto keepdims = static_cast(node.get_attribute_value("keepdims", 1)); + + auto reduction_axes = axes_as_attr ? get_reduction_axes_from_attr(node) : get_reduction_axes_from_input(node); + + if (reduction_axes == nullptr) { + return {std::make_shared(input)}; + } + + // Numerically stable LogSumExp: k + log(sum(exp(x - k))) where k = max(x) + // For numerical stability and avoiding exponent explosion: + // ln(e^x1 + ... + e^xn) = ln(e^k * (e^(x1-k) + ... + e^(xn-k))) + // = k + ln(e^(x1-k) + ... + e^(xn-k)) + // where k = max(x1, ..., xn), so all exponent degrees <= 0. + // + // Always compute k with keepdims=true so that (input - k) broadcasts correctly + // regardless of which axes are being reduced. Then squeeze k at the end if the + // original keepdims was false. This follows the pattern from the PyTorch frontend + // (src/frontends/pytorch/src/op/log.cpp). + std::shared_ptr k = std::make_shared(input, reduction_axes, true); + auto input_minus_k = std::make_shared(input, k); + auto exp_node = std::make_shared(input_minus_k); + auto sum_node = std::make_shared(exp_node, reduction_axes, keepdims); + auto log_node = std::make_shared(sum_node); + + if (!keepdims) { + k = std::make_shared(k, reduction_axes); + } + + auto out = std::make_shared(k, log_node); + return {out}; +} } // namespace namespace opset_1 { ov::OutputVector reduce_log_sum(const ov::frontend::onnx::Node& node) { - const ov::Output sum_node = - make_ov_reduction_op(node, node.get_ov_inputs().at(0), supported_types_v2); - return {std::make_shared(sum_node)}; + auto input = node.get_ov_inputs().at(0); + auto keepdims = static_cast(node.get_attribute_value("keepdims", 1)); + + auto reduction_axes = + (node.get_ov_inputs().size() > 1) ? get_reduction_axes_from_input(node) : get_reduction_axes_from_attr(node); + + if (reduction_axes == nullptr) { + return {std::make_shared(input)}; + } + + auto sum_node = std::make_shared(input, reduction_axes, keepdims); + auto log_node = std::make_shared(sum_node); + return {log_node}; } ov::OutputVector reduce_log_sum_exp(const ov::frontend::onnx::Node& node) { - const auto exp_node = std::make_shared(node.get_ov_inputs().at(0)); - const ov::Output sum_node = make_ov_reduction_op(node, exp_node, supported_types_v1); - return {std::make_shared(sum_node)}; + return onnx_reduce_log_sum_exp_stable(node, true); } ov::OutputVector reduce_l1(const ov::frontend::onnx::Node& node) { @@ -233,10 +277,23 @@ ov::OutputVector reduce_l2(const Node& node) { return {make_ov_reduction_op(node, node.get_ov_inputs().at(0), supported_types_v2)}; } +ov::OutputVector reduce_log_sum(const ov::frontend::onnx::Node& node) { + auto input = node.get_ov_inputs().at(0); + auto keepdims = static_cast(node.get_attribute_value("keepdims", 1)); + + auto reduction_axes = get_reduction_axes_from_input(node); + + if (reduction_axes == nullptr) { + return {std::make_shared(input)}; + } + + auto sum_node = std::make_shared(input, reduction_axes, keepdims); + auto log_node = std::make_shared(sum_node); + return {log_node}; +} + ov::OutputVector reduce_log_sum_exp(const ov::frontend::onnx::Node& node) { - const auto exp_node = std::make_shared(node.get_ov_inputs().at(0)); - const ov::Output sum_node = make_ov_reduction_op(node, exp_node, supported_types_v2); - return {std::make_shared(sum_node)}; + return onnx_reduce_log_sum_exp_stable(node, false); } ov::OutputVector reduce_max(const ov::frontend::onnx::Node& node) { @@ -262,6 +319,7 @@ ov::OutputVector reduce_sum_square(const ov::frontend::onnx::Node& node) { static bool register_multiple_translators(void) { ONNX_OP_M("ReduceL1", OPSET_RANGE(13, 17), ai_onnx::opset_13::reduce_l1); ONNX_OP_M("ReduceL2", OPSET_RANGE(13, 17), ai_onnx::opset_13::reduce_l2); + ONNX_OP_M("ReduceLogSum", OPSET_RANGE(13, 17), ai_onnx::opset_13::reduce_log_sum); ONNX_OP_M("ReduceLogSumExp", OPSET_RANGE(13, 17), ai_onnx::opset_13::reduce_log_sum_exp); ONNX_OP_M("ReduceMax", OPSET_RANGE(13, 17), ai_onnx::opset_13::reduce_max); ONNX_OP_M("ReduceMean", OPSET_RANGE(13, 17), ai_onnx::opset_13::reduce_mean); @@ -284,13 +342,6 @@ ov::OutputVector reduce_l1(const Node& node) { return {make_ov_reduction_op(node, node.get_ov_inputs().at(0), supported_types_v2, false)}; } -ov::OutputVector reduce_log_sum_exp(const ov::frontend::onnx::Node& node) { - const auto exp_node = std::make_shared(node.get_ov_inputs().at(0)); - const ov::Output sum_node = - make_ov_reduction_op(node, exp_node, supported_types_v3, false); - return {std::make_shared(sum_node)}; -} - ov::OutputVector reduce_max(const ov::frontend::onnx::Node& node) { return {make_ov_reduction_op(node, node.get_ov_inputs().at(0), supported_types_v3, false)}; } @@ -304,9 +355,22 @@ ov::OutputVector reduce_min(const ov::frontend::onnx::Node& node) { } ov::OutputVector reduce_log_sum(const ov::frontend::onnx::Node& node) { - const ov::Output sum_node = - make_ov_reduction_op(node, node.get_ov_inputs().at(0), supported_types_v2, false); - return {std::make_shared(sum_node)}; + auto input = node.get_ov_inputs().at(0); + auto keepdims = static_cast(node.get_attribute_value("keepdims", 1)); + + auto reduction_axes = get_reduction_axes_from_input(node); + + if (reduction_axes == nullptr) { + return {std::make_shared(input)}; + } + + auto sum_node = std::make_shared(input, reduction_axes, keepdims); + auto log_node = std::make_shared(sum_node); + return {log_node}; +} + +ov::OutputVector reduce_log_sum_exp(const ov::frontend::onnx::Node& node) { + return onnx_reduce_log_sum_exp_stable(node, false); } ov::OutputVector reduce_prod(const ov::frontend::onnx::Node& node) { diff --git a/src/frontends/onnx/frontend/src/op/reshape.cpp b/src/frontends/onnx/frontend/src/op/reshape.cpp index 3975b02559b7..c04f5c374db9 100644 --- a/src/frontends/onnx/frontend/src/op/reshape.cpp +++ b/src/frontends/onnx/frontend/src/op/reshape.cpp @@ -6,6 +6,7 @@ #include "core/operator_set.hpp" #include "exceptions.hpp" +#include "openvino/op/constant.hpp" #include "utils/common.hpp" #include "utils/reshape.hpp" using namespace ov::op; @@ -20,7 +21,8 @@ ov::OutputVector reshape(const ov::frontend::onnx::Node& node) { const auto data = ov_inputs.at(0); ov::Output pattern; - bool special_zero = true; + // ONNX `allowzero` is the inverse of OpenVINO's `special_zero`. + const bool special_zero = !node.get_attribute_value("allowzero", 0); // Since opset 5 the target shape is provided as input if (ov_inputs.size() == 2) { pattern = ov_inputs.at(1); @@ -31,9 +33,6 @@ ov::OutputVector reshape(const ov::frontend::onnx::Node& node) { pattern = v0::Constant::create(ov::element::i64, {0}, {}); } } else { - // Added in onnx reshape version 14 - special_zero = !node.get_attribute_value("allowzero", 0); - pattern = node.get_attribute_as_constant>("shape", {}); } diff --git a/src/frontends/onnx/frontend/src/op/rms_normalization.cpp b/src/frontends/onnx/frontend/src/op/rms_normalization.cpp new file mode 100644 index 000000000000..c4f3e1ee642a --- /dev/null +++ b/src/frontends/onnx/frontend/src/op/rms_normalization.cpp @@ -0,0 +1,98 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "core/operator_set.hpp" +#include "exceptions.hpp" +#include "openvino/decompositions/rms_norm.hpp" +#include "openvino/op/constant.hpp" +#include "openvino/op/convert.hpp" +#include "openvino/op/convert_like.hpp" +#include "openvino/op/multiply.hpp" +#include "openvino/op/range.hpp" +#include "openvino/op/shape_of.hpp" +#include "openvino/op/squeeze.hpp" +#include "utils/common.hpp" + +using namespace ov::op; +using ::ONNX_NAMESPACE::TensorProto_DataType; + +namespace ov { +namespace frontend { +namespace onnx { +namespace ai_onnx { +namespace opset_23 { + +ov::OutputVector rms_normalization(const ov::frontend::onnx::Node& node) { + // Operator definition: https://onnx.ai/onnx/operators/onnx__RMSNormalization.html + // Y = Mul(Cast(X / Sqrt(ReduceMean(X*X, [axis,..,rank-1]) + epsilon), T), scale) + // The inner inv-rms graph is built via the shared decomposition helper so that + // ov::pass::RMSFusion can fuse it back into ov::op::internal::RMS in plugins. + const auto inputs = node.get_ov_inputs(); + CHECK_VALID_NODE(node, + inputs.size() == 2, + "RMSNormalization expects 2 input tensors (X, scale). Got: ", + inputs.size()); + + const int64_t axis = node.get_attribute_value("axis", -1); + const float epsilon = node.get_attribute_value("epsilon", 1e-5f); + const int64_t stash_type_i = + node.get_attribute_value("stash_type", + static_cast(TensorProto_DataType::TensorProto_DataType_FLOAT)); + const ov::element::Type stash_type = common::get_ov_element_type(stash_type_i); + + ov::Output x = inputs[0]; + const ov::Output& scale = inputs[1]; + const ov::element::Type original_type = x.get_element_type(); + const bool needs_cast = stash_type != original_type; + + if (needs_cast) { + x = std::make_shared(x, stash_type); + } + + // normalized_axes = [axis, axis+1, ..., rank(X)-1] + // For axis < 0, equivalent to Range(axis, 0, 1) which yields exactly the + // negative tail (e.g. axis=-2 → [-2, -1]). + const auto axes_start = v0::Constant::create(ov::element::i64, ov::Shape{}, {axis}); + const auto axes_step = v0::Constant::create(ov::element::i64, ov::Shape{}, {1}); + ov::Output axes_end; + if (axis < 0) { + axes_end = v0::Constant::create(ov::element::i64, ov::Shape{}, {0}); + } else { + // rank(x) = Squeeze(ShapeOf(ShapeOf(x))) + auto x_shape = std::make_shared(x, ov::element::i64); + auto x_rank_1d = std::make_shared(x_shape, ov::element::i64); + axes_end = std::make_shared(x_rank_1d); + } + auto axes = std::make_shared(axes_start, axes_end, axes_step, ov::element::i64); + + // Epsilon constant in the same precision as `x` (the helper's Add(ReduceMean, eps) + // requires matching dtypes; RMSFusion expects raw Constant, not ConvertLike). + const auto x_et = x.get_element_type(); + ov::Output eps; + if (x_et.is_static()) { + eps = v0::Constant::create(x_et, ov::Shape{}, {epsilon}); + } else { + const auto eps_f32 = v0::Constant::create(ov::element::f32, ov::Shape{}, {epsilon}); + eps = std::make_shared(eps_f32, x); + } + + // Build inv-rms via the shared helper (no scale here — scale is applied after the + // cast back to the original type, per ONNX spec). + ov::pass::NodeRegistry reg; + ov::Output normalized = ov::decomposition::rms_norm(reg, x, axes, eps); + + if (needs_cast) { + normalized = std::make_shared(normalized, inputs[0]); + } + + return {std::make_shared(normalized, scale)}; +} + +ONNX_OP("RMSNormalization", OPSET_SINCE(1), ai_onnx::opset_23::rms_normalization); + +} // namespace opset_23 +} // namespace ai_onnx +} // namespace onnx +} // namespace frontend +} // namespace ov diff --git a/src/frontends/onnx/frontend/src/op/rotary_embedding.cpp b/src/frontends/onnx/frontend/src/op/rotary_embedding.cpp new file mode 100644 index 000000000000..604050258715 --- /dev/null +++ b/src/frontends/onnx/frontend/src/op/rotary_embedding.cpp @@ -0,0 +1,230 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "core/null_node.hpp" +#include "core/operator_set.hpp" +#include "exceptions.hpp" +#include "openvino/decompositions/rope.hpp" +#include "openvino/op/concat.hpp" +#include "openvino/op/constant.hpp" +#include "openvino/op/gather.hpp" +#include "openvino/op/reshape.hpp" +#include "openvino/op/shape_of.hpp" +#include "openvino/op/slice.hpp" +#include "openvino/op/transpose.hpp" +#include "openvino/op/unsqueeze.hpp" +#include "openvino/op/variadic_split.hpp" +#include "utils/common.hpp" + +using namespace ov::op; + +namespace ov { +namespace frontend { +namespace onnx { +namespace ai_onnx { +namespace opset_23 { + +namespace { + +// Build a 1-D i64 Constant from a list of values. We use std::vector explicitly so +// that mixed `int64_t` / `int` literals don't break Constant::create template deduction. +std::shared_ptr i64_const(const std::vector& values) { + return v0::Constant::create(ov::element::i64, ov::Shape{values.size()}, values); +} + +// Reshape with `special_zero=true` so a literal 0 in `target` means "keep the +// corresponding input dimension" — saves us from gathering shape parts dynamically. +std::shared_ptr reshape_keep(const ov::Output& x, const std::vector& target) { + return std::make_shared(x, i64_const(target), /*special_zero=*/true); +} + +} // namespace + +ov::OutputVector rotary_embedding(const ov::frontend::onnx::Node& node) { + // Operator definition: https://onnx.ai/onnx/operators/onnx__RotaryEmbedding.html + // + // Strategy: normalize layout to [bs, num_heads, seq, head_size], optionally peel off + // the no-rotate tail of the head dim, optionally de-interleave for the interleaved + // mode, delegate the core formula to ov::decomposition::rope (recognised by + // ov::pass::RoPEFusion), then undo the wrappers. + // + // Note: we never need to know `head_size` statically — the rope helper's split is + // sized from `half_rotary_dim` (derived from `rotary_embedding_dim` or cos_cache), + // and partial-rotation tail length uses VariadicSplit's `-1` (take-the-rest) form. + const auto inputs = node.get_ov_inputs(); + CHECK_VALID_NODE(node, + inputs.size() == 3 || inputs.size() == 4, + "RotaryEmbedding expects 3 or 4 inputs (X, cos_cache, sin_cache, [position_ids]). Got: ", + inputs.size()); + + const auto& input = inputs[0]; + const auto& cos_cache = inputs[1]; + const auto& sin_cache = inputs[2]; + const bool has_position_ids = inputs.size() == 4 && !ov::op::util::is_null(inputs[3]); + + const int64_t interleaved = node.get_attribute_value("interleaved", 0); + const int64_t rotary_dim = node.get_attribute_value("rotary_embedding_dim", 0); + const int64_t num_heads = node.get_attribute_value("num_heads", 0); + const bool partial = rotary_dim > 0; + if (partial) { + CHECK_VALID_NODE(node, rotary_dim % 2 == 0, "rotary_embedding_dim must be even, got: ", rotary_dim); + } + + // The shared rope helper requires a static `half_rotary_dim` to build a VariadicSplit. + const auto& cos_pshape = cos_cache.get_partial_shape(); + CHECK_VALID_NODE(node, + cos_pshape.rank().is_static() && cos_pshape.rank().get_length() >= 2 && + cos_pshape[cos_pshape.rank().get_length() - 1].is_static(), + "cos_cache must have static rank >= 2 and a static last dimension, got: ", + cos_pshape); + const int64_t cos_last_dim = cos_pshape[cos_pshape.rank().get_length() - 1].get_length(); + const int64_t half_rotary_dim = partial ? rotary_dim / 2 : cos_last_dim; + + // The cos/sin cache must be wide enough to feed half_rotary_dim columns into + // the rope helper; otherwise the downstream Slice/VariadicSplit would build + // an invalid graph that only fails much later at shape inference. + CHECK_VALID_NODE(node, + half_rotary_dim <= cos_last_dim, + "cos_cache last dimension (", + cos_last_dim, + ") must be >= rotary_embedding_dim/2 (", + half_rotary_dim, + ")."); + + const auto& input_pshape = input.get_partial_shape(); + const auto input_rank = input_pshape.rank(); + CHECK_VALID_NODE(node, + input_rank.is_static() && (input_rank.get_length() == 3 || input_rank.get_length() == 4), + "RotaryEmbedding input must have static rank 3 or 4."); + const bool input_is_3d = input_rank.get_length() == 3; + if (input_is_3d) { + CHECK_VALID_NODE(node, num_heads > 0, "num_heads attribute is required for 3D input."); + } + + // When the head dimension is statically known, validate that rotary_dim fits + // into it. For 4D input that is the last dim directly; for 3D input the head + // dim is hidden/num_heads, where hidden is the last input dim. + if (partial) { + const auto& last_input_dim = input_pshape[input_rank.get_length() - 1]; + if (last_input_dim.is_static()) { + const int64_t last_input_dim_val = last_input_dim.get_length(); + if (input_is_3d) { + CHECK_VALID_NODE(node, + last_input_dim_val % num_heads == 0, + "Hidden size (", + last_input_dim_val, + ") must be divisible by num_heads (", + num_heads, + ")."); + const int64_t head_size = last_input_dim_val / num_heads; + CHECK_VALID_NODE(node, + rotary_dim <= head_size, + "rotary_embedding_dim (", + rotary_dim, + ") must be <= head_size (", + head_size, + ")."); + } else { + CHECK_VALID_NODE(node, + rotary_dim <= last_input_dim_val, + "rotary_embedding_dim (", + rotary_dim, + ") must be <= head_size (", + last_input_dim_val, + ")."); + } + } + } + + // ---- Step 1. Normalize layout to [bs, num_heads, seq, head_size] ---- + const auto perm_bnsh = i64_const({0, 2, 1, 3}); + const auto input_shape = input_is_3d ? std::make_shared(input, ov::element::i64) : nullptr; + + ov::Output x = input; + if (input_is_3d) { + // [bs, seq, hidden] -> [bs, seq, num_heads, head_size] -> [bs, num_heads, seq, head_size] + x = reshape_keep(input, {0, 0, num_heads, -1}); + x = std::make_shared(x, perm_bnsh); + } + + // ---- Step 2. For partial rotation, peel off the no-rotate tail of the head dim ---- + ov::Output x_rotate = x; + ov::Output x_no_rotate; + if (partial) { + const auto axis_neg1_scalar = v0::Constant::create(ov::element::i64, ov::Shape{}, {int64_t{-1}}); + // {rotary_dim, -1}: -1 = "the rest". When rotary_dim == head_size, the second piece is empty. + const auto lengths = i64_const({rotary_dim, -1}); + auto split = std::make_shared(x, axis_neg1_scalar, lengths); + x_rotate = split->output(0); + x_no_rotate = split->output(1); + } + + // ---- Step 3. Build cos/sin in shape [..., 1, ..., half_rotary_dim] ---- + ov::Output cos = cos_cache; + ov::Output sin = sin_cache; + if (has_position_ids) { + const auto axis0 = v0::Constant::create(ov::element::i64, ov::Shape{}, {int64_t{0}}); + cos = std::make_shared(cos_cache, inputs[3], axis0); + sin = std::make_shared(sin_cache, inputs[3], axis0); + } + // For partial rotation, the cache may hold the full head_size/2; slice it down. + if (cos_last_dim > half_rotary_dim) { + cos = std::make_shared(cos, + i64_const({0}), + i64_const({half_rotary_dim}), + i64_const({1}), + i64_const({-1})); + sin = std::make_shared(sin, + i64_const({0}), + i64_const({half_rotary_dim}), + i64_const({1}), + i64_const({-1})); + } + // Insert axis=1 broadcast dim → [bs, 1, seq, half_rotary_dim]. + const auto axis_1_1d = i64_const({1}); + cos = std::make_shared(cos, axis_1_1d); + sin = std::make_shared(sin, axis_1_1d); + + // ---- Step 4. Optionally de-interleave so the split-half formula in `rope` applies ---- + // [.., rotary_dim] -reshape-> [.., half, 2] -transpose [0,1,2,4,3]-> [.., 2, half] -reshape-> [.., rotary_dim] + const auto perm_5d = i64_const({0, 1, 2, 4, 3}); + ov::Output rope_input = x_rotate; + if (interleaved) { + rope_input = reshape_keep(x_rotate, {0, 0, 0, half_rotary_dim, 2}); + rope_input = std::make_shared(rope_input, perm_5d); + rope_input = reshape_keep(rope_input, {0, 0, 0, -1}); + } + + // ---- Step 5. Core RoPE formula via the shared decomposition helper ---- + ov::pass::NodeRegistry rope_reg; + ov::Output rotated = ov::decomposition::rope(rope_reg, rope_input, cos, sin, half_rotary_dim); + + // ---- Step 6. Re-interleave if needed (mirror of step 4) ---- + if (interleaved) { + rotated = reshape_keep(rotated, {0, 0, 0, 2, half_rotary_dim}); + rotated = std::make_shared(rotated, perm_5d); + rotated = reshape_keep(rotated, {0, 0, 0, -1}); + } + + // ---- Step 7. Concat rotated and no-rotate parts back along the head dim ---- + ov::Output output = + partial ? std::make_shared(ov::OutputVector{rotated, x_no_rotate}, -1) : rotated; + + // ---- Step 8. Restore original 3D layout if needed ---- + if (input_is_3d) { + // [bs, num_heads, seq, head_size] -> [bs, seq, num_heads, head_size] -> [bs, seq, hidden] + output = std::make_shared(output, perm_bnsh); + output = std::make_shared(output, input_shape, /*special_zero=*/false); + } + + return {output}; +} + +ONNX_OP("RotaryEmbedding", OPSET_SINCE(1), ai_onnx::opset_23::rotary_embedding); + +} // namespace opset_23 +} // namespace ai_onnx +} // namespace onnx +} // namespace frontend +} // namespace ov diff --git a/src/frontends/onnx/frontend/src/op/scan.cpp b/src/frontends/onnx/frontend/src/op/scan.cpp index 552a0374f7d5..635be82b5988 100644 --- a/src/frontends/onnx/frontend/src/op/scan.cpp +++ b/src/frontends/onnx/frontend/src/op/scan.cpp @@ -6,6 +6,8 @@ #include "core/null_node.hpp" #include "core/operator_set.hpp" #include "exceptions.hpp" +#include "openvino/core/type.hpp" +#include "openvino/frontend/exception.hpp" #include "openvino/op/constant.hpp" #include "openvino/op/squeeze.hpp" #include "openvino/op/tensor_iterator.hpp" @@ -24,13 +26,22 @@ namespace { ov::OutputVector scan_to_tensor_iterator(const ov::OutputVector& node_inputs, ov::ParameterVector& body_inputs, ov::OutputVector& body_outputs, - int64_t num_scan_inputs, + size_t num_scan_inputs, const std::vector& scan_input_axes, const std::vector& scan_input_directions, const std::vector& scan_output_axes, const std::vector& scan_output_directions, - int64_t in_offset = 0, + size_t in_offset = 0, const std::string& node_description = "") { + FRONT_END_OP_CONVERSION_CHECK( + node_inputs.size() >= in_offset && node_inputs.size() - in_offset >= body_inputs.size(), + node_description, + " Number of Scan node inputs (", + node_inputs.size(), + ") is less than required (", + in_offset + body_inputs.size(), + ") based on body graph parameters and in_offset"); + const size_t num_initial_values = body_inputs.size() - num_scan_inputs; const size_t num_scan_outputs = body_outputs.size() - num_initial_values; @@ -38,10 +49,10 @@ ov::OutputVector scan_to_tensor_iterator(const ov::OutputVector& node_inputs, if (r.is_static()) { axis = common::normalize_axis(node_description, axis, r); } else { - FRONT_END_GENERAL_CHECK(axis >= 0, - node_description, - " Rank must be static in order to normalize negative axis=", - axis); + FRONT_END_OP_CONVERSION_CHECK(axis >= 0, + node_description, + " Rank must be static in order to normalize negative axis=", + axis); } return axis; }; @@ -56,7 +67,7 @@ ov::OutputVector scan_to_tensor_iterator(const ov::OutputVector& node_inputs, // but in ONNX Scan the slice of input can has one dimension less, // so the parameter needs to have aligned rank with 1 at sliced axis, // and then squeezed to restore original shape. - for (int64_t i = 0; i < num_scan_inputs; ++i) { + for (size_t i = 0; i < num_scan_inputs; ++i) { const auto in_idx = num_initial_values + i; auto axis = scan_input_axes[i]; const auto axis_node = v0::Constant::create(ov::element::i64, ov::Shape{1}, {axis}); @@ -65,10 +76,10 @@ ov::OutputVector scan_to_tensor_iterator(const ov::OutputVector& node_inputs, axis = try_normalize_axis_for_static_rank(axis, shape.rank()); shape[axis] = 1; } else { - FRONT_END_GENERAL_CHECK(axis >= 0, - node_description, - " Rank must be static in order to normalize negative axis=", - axis); + FRONT_END_OP_CONVERSION_CHECK(axis >= 0, + node_description, + " Rank must be static in order to normalize negative axis=", + axis); } body_inputs[in_idx]->set_partial_shape(shape); body_inputs[in_idx]->validate_and_infer_types(); @@ -93,7 +104,7 @@ ov::OutputVector scan_to_tensor_iterator(const ov::OutputVector& node_inputs, tensor_iterator->set_function(ti_body); // Set slicing for Scan (TensorIterator) inputs - for (int64_t i = 0; i < num_scan_inputs; ++i) { + for (size_t i = 0; i < num_scan_inputs; ++i) { const auto in_idx = num_initial_values + i; const auto axis = try_normalize_axis_for_static_rank(scan_input_axes[i], @@ -127,8 +138,8 @@ ov::OutputVector scan_to_tensor_iterator(const ov::OutputVector& node_inputs, } ov::OutputVector import_onnx_scan(const ov::frontend::onnx::Node& node, - int64_t default_axis, - int64_t in_offset, + size_t default_axis, + size_t in_offset, std::string&& in_directions_attr_name) { const auto& node_inputs = node.get_ov_inputs(); @@ -146,23 +157,88 @@ ov::OutputVector import_onnx_scan(const ov::frontend::onnx::Node& node, body_outputs.push_back(res->get_input_source_output(0)); } body_inputs = body_graph->get_parameters(); + + // The InputModel for subgraphs may register parent-scope constants that + // are also declared as body graph outputs (initializer-as-output) as + // additional body outputs. Trim trailing pass-through Constants beyond + // the Scan node's declared output count so num_scan_outputs is computed + // correctly below. + const size_t expected_body_outputs = node.get_outputs_size(); + while (body_outputs.size() > expected_body_outputs && + ov::is_type(body_outputs.back().get_node_shared_ptr())) { + body_outputs.pop_back(); + } } - const int64_t num_scan_inputs = node.get_attribute_value("num_scan_inputs"); + + const int64_t num_scan_inputs_signed = node.get_attribute_value("num_scan_inputs"); + FRONT_END_OP_CONVERSION_CHECK(num_scan_inputs_signed >= 0, + node.get_description(), + " num_scan_inputs attribute is negative: ", + num_scan_inputs_signed); + const size_t num_scan_inputs = static_cast(num_scan_inputs_signed); + + FRONT_END_OP_CONVERSION_CHECK(body_inputs.size() >= num_scan_inputs, + node.get_description(), + " num_scan_inputs (", + num_scan_inputs, + ") negative or exceeds the number of body graph inputs (", + body_inputs.size(), + ")"); + const size_t num_initial_values = body_inputs.size() - num_scan_inputs; + FRONT_END_OP_CONVERSION_CHECK(body_outputs.size() >= num_initial_values, + node.get_description(), + " num_scan_outputs can't be negative: body outputs (", + body_outputs.size(), + ") is less than num_initial_values (", + num_initial_values, + ")"); + const size_t num_scan_outputs = body_outputs.size() - num_initial_values; std::vector scan_input_axes = node.get_attribute_value>("scan_input_axes", std::vector(num_scan_inputs, default_axis)); + FRONT_END_OP_CONVERSION_CHECK(scan_input_axes.size() >= num_scan_inputs, + node.get_description(), + " num_scan_inputs (", + num_scan_inputs, + ") exceeds the number of scan_input_axes (", + scan_input_axes.size(), + ")"); + std::vector scan_input_directions = node.get_attribute_value>(in_directions_attr_name, std::vector(num_scan_inputs, 0)); + FRONT_END_OP_CONVERSION_CHECK(scan_input_directions.size() >= num_scan_inputs, + node.get_description(), + " num_scan_inputs (", + num_scan_inputs, + ") exceeds the number of scan_input_directions (", + scan_input_directions.size(), + ")"); + std::vector scan_output_axes = node.get_attribute_value>("scan_output_axes", std::vector(num_scan_outputs, default_axis)); + FRONT_END_OP_CONVERSION_CHECK(scan_output_axes.size() >= num_scan_outputs, + node.get_description(), + " num_scan_outputs (", + num_scan_outputs, + ") exceeds the number of scan_output_axes (", + scan_output_axes.size(), + ")"); + std::vector scan_output_directions = node.get_attribute_value>("scan_output_directions", std::vector(num_scan_outputs, 0)); + FRONT_END_OP_CONVERSION_CHECK(scan_output_directions.size() >= num_scan_outputs, + node.get_description(), + " num_scan_outputs (", + num_scan_outputs, + ") exceeds the number of scan_output_directions (", + scan_output_directions.size(), + ")"); return scan_to_tensor_iterator(node_inputs, body_inputs, diff --git a/src/frontends/onnx/frontend/src/op/scatter_nd.cpp b/src/frontends/onnx/frontend/src/op/scatter_nd.cpp index e70aafc15cc4..86ba4e4ba712 100644 --- a/src/frontends/onnx/frontend/src/op/scatter_nd.cpp +++ b/src/frontends/onnx/frontend/src/op/scatter_nd.cpp @@ -2,9 +2,6 @@ // SPDX-License-Identifier: Apache-2.0 // -// Disabled in CMakeList -// Update to higher opset required - #include "core/operator_set.hpp" #include "exceptions.hpp" #include "openvino/op/scatter_nd_update.hpp" @@ -31,8 +28,43 @@ ov::OutputVector scatter_nd(const ov::frontend::onnx::Node& node) { return {std::make_shared(data, indices, updates)}; } -ONNX_OP("ScatterND", OPSET_SINCE(1), ai_onnx::opset_1::scatter_nd); +ONNX_OP("ScatterND", OPSET_RANGE(1, 15), ai_onnx::opset_1::scatter_nd); } // namespace opset_1 + +namespace opset_16 { +// ScatterND-16 introduces the `reduction` attribute with values: none, add, mul. +// ScatterND-18 extends `reduction` with: min, max. +ov::OutputVector scatter_nd(const ov::frontend::onnx::Node& node) { + ov::OutputVector ov_inputs{node.get_ov_inputs()}; + auto data = ov_inputs.at(0); + auto indices = ov_inputs.at(1); + auto updates = ov_inputs.at(2); + + const auto reduction_onnx = node.get_attribute_value("reduction", "none"); + v15::ScatterNDUpdate::Reduction reduction_ov = v15::ScatterNDUpdate::Reduction::NONE; + if (reduction_onnx == "none") { + reduction_ov = v15::ScatterNDUpdate::Reduction::NONE; + } else if (reduction_onnx == "add") { + reduction_ov = v15::ScatterNDUpdate::Reduction::SUM; + } else if (reduction_onnx == "mul") { + reduction_ov = v15::ScatterNDUpdate::Reduction::PROD; + } else if (reduction_onnx == "min") { + reduction_ov = v15::ScatterNDUpdate::Reduction::MIN; + } else if (reduction_onnx == "max") { + reduction_ov = v15::ScatterNDUpdate::Reduction::MAX; + } else { + CHECK_VALID_NODE(node, + false, + "Unsupported value of attribute: `reduction`. " + "Supported modes: `none`, `add`, `mul`, `min`, `max`, got: ", + reduction_onnx); + } + + return {std::make_shared(data, indices, updates, reduction_ov)}; +} + +ONNX_OP("ScatterND", OPSET_SINCE(16), ai_onnx::opset_16::scatter_nd); +} // namespace opset_16 } // namespace ai_onnx } // namespace onnx } // namespace frontend diff --git a/src/frontends/onnx/frontend/src/op/sequence_at.cpp b/src/frontends/onnx/frontend/src/op/sequence_at.cpp index 5fc7af3117dd..ba7cbd008247 100644 --- a/src/frontends/onnx/frontend/src/op/sequence_at.cpp +++ b/src/frontends/onnx/frontend/src/op/sequence_at.cpp @@ -2,6 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 // +#include "openvino/frontend/sequence_at.hpp" + #include #include "core/operator_set.hpp" @@ -31,24 +33,27 @@ ov::OutputVector sequence_at(const ov::frontend::onnx::Node& node) { const auto& inputs = node.get_ov_inputs(); - const auto& input_sequence = as_type_ptr(inputs[0].get_node_shared_ptr()); - OPENVINO_ASSERT(input_sequence, "SequenceAt: 'input' must be a sequence"); - auto position = inputs[1]; OPENVINO_ASSERT(position.get_partial_shape().rank().compatible(0), "SequenceAt: 'position' input must be a scalar"); - const auto position_const = ov::util::get_constant_from_source(position); - OPENVINO_ASSERT(position_const, "SequenceAt: 'position' input must be constant"); - - const auto position_value = position_const->cast_vector()[0]; - - const auto input_sequence_length = static_cast(input_sequence->get_sequence().size()); - - const auto position_value_normalized = position_value < 0 ? position_value + input_sequence_length : position_value; - OPENVINO_ASSERT(position_value_normalized >= 0 && position_value_normalized < input_sequence_length, - "SequenceAt: 'position' is out of bounds"); - - return {input_sequence->get_sequence().at(position_value_normalized)}; + // Fast path: input is a SequenceMark chain - resolve directly. + if (const auto input_sequence = as_type_ptr(inputs[0].get_node_shared_ptr())) { + const auto position_const = ov::util::get_constant_from_source(position); + if (position_const) { + const auto position_value = position_const->cast_vector()[0]; + const auto seq = input_sequence->get_sequence(); + const auto input_sequence_length = static_cast(seq.size()); + const auto position_value_normalized = + position_value < 0 ? position_value + input_sequence_length : position_value; + OPENVINO_ASSERT(position_value_normalized >= 0 && position_value_normalized < input_sequence_length, + "SequenceAt: 'position' is out of bounds"); + return {seq.at(position_value_normalized)}; + } + } + + // Deferred path: the sequence is not directly accessible (e.g., it comes from an + // If/Loop output). Emit a helper op that will be resolved by a later transformation. + return {std::make_shared(inputs[0], inputs[1])}; } /// @brief Registers the SequenceAt operator implementation in the ONNX frontend diff --git a/src/frontends/onnx/frontend/src/op/sequence_erase.cpp b/src/frontends/onnx/frontend/src/op/sequence_erase.cpp new file mode 100644 index 000000000000..9480177beb13 --- /dev/null +++ b/src/frontends/onnx/frontend/src/op/sequence_erase.cpp @@ -0,0 +1,71 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "openvino/frontend/sequence_erase.hpp" + +#include + +#include "core/operator_set.hpp" +#include "exceptions.hpp" +#include "openvino/core/validation_util.hpp" +#include "openvino/frontend/sequence_mark.hpp" +#include "openvino/op/constant.hpp" +#include "utils/common.hpp" + +namespace ov { +namespace frontend { +namespace onnx { +namespace ai_onnx { +namespace opset_11 { + +/// @brief Implements the SequenceErase operator. +/// @param node Input ONNX node. Inputs: sequence and optional position. +/// When position is omitted, the last element is removed. +/// @return A new sequence with the requested element removed. +ov::OutputVector sequence_erase(const ov::frontend::onnx::Node& node) { + constexpr auto min_inputs = 1; + constexpr auto max_inputs = 2; + common::default_op_checks(node, min_inputs, max_inputs); + + const auto& inputs = node.get_ov_inputs(); + + if (inputs.size() == 2) { + OPENVINO_ASSERT(inputs[1].get_partial_shape().rank().compatible(0), + "SequenceErase: 'position' input must be a scalar"); + } + + // Fast path: input is a SequenceMark with a constant (or omitted) position. + if (const auto input_sequence = as_type_ptr(inputs[0].get_node_shared_ptr())) { + auto seq = input_sequence->get_sequence(); + const auto length = static_cast(seq.size()); + std::int64_t position = length - 1; + if (inputs.size() == 2) { + const auto position_const = ov::util::get_constant_from_source(inputs[1]); + if (!position_const) { + // Position is dynamic - defer. + return {std::make_shared(inputs[0], inputs[1])}; + } + position = position_const->cast_vector()[0]; + } + const auto position_normalized = position < 0 ? position + length : position; + OPENVINO_ASSERT(position_normalized >= 0 && position_normalized < length, + "SequenceErase: 'position' is out of bounds"); + seq.erase(seq.begin() + position_normalized); + return {std::make_shared(seq)}; + } + + // Deferred path: emit a helper op resolved by a later transformation. + if (inputs.size() == 2) { + return {std::make_shared(inputs[0], inputs[1])}; + } + return {std::make_shared(inputs[0])}; +} + +ONNX_OP("SequenceErase", OPSET_SINCE(1), ai_onnx::opset_11::sequence_erase); + +} // namespace opset_11 +} // namespace ai_onnx +} // namespace onnx +} // namespace frontend +} // namespace ov diff --git a/src/frontends/onnx/frontend/src/op/sequence_insert.cpp b/src/frontends/onnx/frontend/src/op/sequence_insert.cpp index 902fcde7216c..6301159d77bf 100644 --- a/src/frontends/onnx/frontend/src/op/sequence_insert.cpp +++ b/src/frontends/onnx/frontend/src/op/sequence_insert.cpp @@ -28,11 +28,24 @@ ov::OutputVector sequence_insert(const ov::frontend::onnx::Node& node) { const auto& sequence_input = inputs[0]; const auto& to_insert = inputs[1]; - // Create a SequenceInsert helper op that will be resolved by a transformation pass + // Create a SequenceInsert helper op that will be resolved by a transformation pass. + // Wrap the result in a SequenceMark so that downstream consumers (e.g. ConcatFromSequence + // or SequenceAt) can use SequenceMark::get_sequence() to walk through a chain of + // SequenceInsert nodes and collect all inserted elements. This mirrors what the PyTorch + // frontend does for aten::append. Without the wrapping SequenceMark, a chain of + // SequenceInsert nodes feeding into ConcatFromSequence (as produced for models like + // ConvNeXt/Swin) cannot be matched by the SequenceConcatReplacer pattern. + std::shared_ptr seq_insert; if (inputs.size() == 3) { - return {std::make_shared(sequence_input, to_insert, inputs[2])}; + seq_insert = std::make_shared(sequence_input, to_insert, inputs[2]); + } else { + seq_insert = std::make_shared(sequence_input, to_insert); } - return {std::make_shared(sequence_input, to_insert)}; + auto sequence = std::make_shared(ov::OutputVector{seq_insert}); + ov::op::util::FrameworkNodeAttrs attrs; + attrs.set_type_name("SequenceInsert"); + sequence->set_attrs(attrs); + return {sequence}; } ONNX_OP("SequenceInsert", OPSET_SINCE(1), ai_onnx::opset_11::sequence_insert); diff --git a/src/frontends/onnx/frontend/src/op/sequence_length.cpp b/src/frontends/onnx/frontend/src/op/sequence_length.cpp new file mode 100644 index 000000000000..7215e8f958b2 --- /dev/null +++ b/src/frontends/onnx/frontend/src/op/sequence_length.cpp @@ -0,0 +1,45 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "openvino/frontend/sequence_length.hpp" + +#include + +#include "core/operator_set.hpp" +#include "exceptions.hpp" +#include "openvino/core/type.hpp" +#include "openvino/frontend/sequence_mark.hpp" +#include "openvino/op/constant.hpp" +#include "utils/common.hpp" + +namespace ov { +namespace frontend { +namespace onnx { +namespace ai_onnx { +namespace opset_11 { + +/// @brief Implements the SequenceLength operator +/// @param node Input ONNX node with a single sequence input. +/// @return A scalar int64 tensor with the number of elements in the sequence. +ov::OutputVector sequence_length(const ov::frontend::onnx::Node& node) { + common::default_op_checks(node, 1, 1); + const auto& inputs = node.get_ov_inputs(); + + // Fast path: input is a SequenceMark chain - resolve directly to a constant. + if (const auto input_sequence = as_type_ptr(inputs[0].get_node_shared_ptr())) { + const auto length = static_cast(input_sequence->get_sequence().size()); + return {ov::op::v0::Constant::create(ov::element::i64, ov::Shape{}, {length})}; + } + + // Deferred path: emit a helper op that will be resolved by a later transformation. + return {std::make_shared(inputs[0])}; +} + +ONNX_OP("SequenceLength", OPSET_SINCE(1), ai_onnx::opset_11::sequence_length); + +} // namespace opset_11 +} // namespace ai_onnx +} // namespace onnx +} // namespace frontend +} // namespace ov diff --git a/src/frontends/onnx/frontend/src/ops_bridge.cpp b/src/frontends/onnx/frontend/src/ops_bridge.cpp index 5cbc4bd232eb..ab00472f3ed7 100644 --- a/src/frontends/onnx/frontend/src/ops_bridge.cpp +++ b/src/frontends/onnx/frontend/src/ops_bridge.cpp @@ -44,6 +44,7 @@ const char* OPENVINO_ONNX_DOMAIN = "org.openvinotoolkit"; const char* MICROSOFT_DOMAIN = "com.microsoft"; const char* PYTORCH_ATEN_DOMAIN = "org.pytorch.aten"; const char* MMDEPLOY_DOMAIN = "mmdeploy"; +const char* AIONNX_ML_DOMAIN = "ai.onnx.ml"; // Central storage of supported translators for operations typedef std::unordered_map SupportedOps; @@ -264,6 +265,9 @@ OperatorsBridge::OperatorsBridge() { // custom ops } +const std::vector get_supported_ops_via_tokenizers() { + return {"StringNormalizer", "LabelEncoder", "Tokenizer", "TfIdfVectorizer"}; +} #undef REGISTER_OPERATOR #undef REGISTER_OPERATOR_WITH_DOMAIN } // namespace onnx diff --git a/src/frontends/onnx/frontend/src/ops_bridge.hpp b/src/frontends/onnx/frontend/src/ops_bridge.hpp index 90eb610b0439..952aacaf724f 100644 --- a/src/frontends/onnx/frontend/src/ops_bridge.hpp +++ b/src/frontends/onnx/frontend/src/ops_bridge.hpp @@ -89,6 +89,8 @@ class OperatorsBridge { std::unordered_map m_map; }; +const std::vector get_supported_ops_via_tokenizers(); + } // namespace onnx } // namespace frontend } // namespace ov diff --git a/src/frontends/onnx/frontend/src/translate_session.cpp b/src/frontends/onnx/frontend/src/translate_session.cpp index 99584f7191e1..3c015c034f98 100644 --- a/src/frontends/onnx/frontend/src/translate_session.cpp +++ b/src/frontends/onnx/frontend/src/translate_session.cpp @@ -126,7 +126,7 @@ void TranslateSession::translate_graph(const ov::frontend::InputModel::Ptr& inpu ov::OutputVector ov_outputs(out_size); const Operator* translator = m_translator_map->get_operator(decoder->get_domain(), decoder->get_op_type(), decoder->get_op_set()); - ov::frontend::onnx::Node node_context(*decoder, this); + ov::frontend::onnx::Node node_context(decoder, this); std::string error_message{}; try { if (translator == nullptr) { @@ -194,15 +194,37 @@ void TranslateSession::translate_graph(const ov::frontend::InputModel::Ptr& inpu } // outputs + // Materialize any output tensors that are direct constants (initializers used as graph + // outputs without being consumed by any op). These are not created during the inputs or + // operations loops because they have data but are not referenced as op inputs. + // Also handle subgraph outputs that reference parent scope tensors — lookup_tensor() + // will create a Parameter for any parent-scope non-constant value. ResultVector results; results.reserve(model_onnx->get_outputs().size()); for (const auto& output : model_onnx->get_outputs()) { const auto tensor = std::dynamic_pointer_cast(output); FRONT_END_GENERAL_CHECK(tensor != nullptr, - "Inputs of ov::frontend::onnx::InputModel must be TensorONNXPlace instances"); + "Outputs of ov::frontend::onnx::InputModel must be TensorONNXPlace instances"); const auto name = tensor->get_names()[0]; if (!m_tensor_values.count(name)) { - continue; + auto place_it = all_tensor_places.find(name); + if (place_it != all_tensor_places.end() && + (place_it->second->get_data() != nullptr || place_it->second->get_data_location() != nullptr)) { + create_const_or_param(name, place_it->second); + } else if (auto parent_value = lookup_tensor(name); parent_value.get_node() != nullptr) { + // lookup_tensor() resolved the name from a parent scope. For non-constant + // parent values it already cached a Parameter in m_tensor_values; for + // parent-scope Constants it returns the Constant directly without caching, + // so insert it here to make the subsequent m_tensor_values[name] lookup + // safe. + m_tensor_values.emplace(name, parent_value); + } else { + FRONT_END_GENERAL_CHECK(false, + "Output tensor \"", + name, + "\" was declared as a graph output but is not produced by any operation, " + "is not an initializer, and cannot be resolved from a parent scope."); + } } const auto& output_value = m_tensor_values[name]; const auto result = std::make_shared(output_value); diff --git a/src/frontends/onnx/frontend/src/utils/attention.cpp b/src/frontends/onnx/frontend/src/utils/attention.cpp index 4164c1f77142..419d964d58bd 100644 --- a/src/frontends/onnx/frontend/src/utils/attention.cpp +++ b/src/frontends/onnx/frontend/src/utils/attention.cpp @@ -4,8 +4,24 @@ #include "utils/attention.hpp" +#include "openvino/op/add.hpp" #include "openvino/op/constant.hpp" +#include "openvino/op/convert.hpp" +#include "openvino/op/divide.hpp" #include "openvino/op/gather.hpp" +#include "openvino/op/greater_eq.hpp" +#include "openvino/op/matmul.hpp" +#include "openvino/op/multiply.hpp" +#include "openvino/op/range.hpp" +#include "openvino/op/scaled_dot_product_attention.hpp" +#include "openvino/op/select.hpp" +#include "openvino/op/shape_of.hpp" +#include "openvino/op/softmax.hpp" +#include "openvino/op/sqrt.hpp" +#include "openvino/op/squeeze.hpp" +#include "openvino/op/subtract.hpp" +#include "openvino/op/tanh.hpp" +#include "openvino/op/unsqueeze.hpp" using namespace ov::op; @@ -24,6 +40,163 @@ std::shared_ptr get_dimensions(const std::shared_ptr& node, return get_dimensions(std::make_shared(node), dims); } +// Convert boolean mask to float additive mask: true -> 0.0, false -> -10000.0 +ov::Output convert_boolean_mask(const ov::Output& mask, + const ov::element::Type& type, + float mask_filter_value) { + auto zero = v0::Constant::create(type, ov::Shape{}, {0.0f}); + auto neg_large = v0::Constant::create(type, ov::Shape{}, {mask_filter_value}); + return std::make_shared(mask, zero, neg_large); +} + +// Build additive causal mask of shape (seq_q, seq_kv): 0 for allowed, -10000 for masked. +// When use_offset=true, accounts for KV cache offset so that query position i attends to +// key positions j where j <= i + (seq_kv - seq_q). Use this for KV cache scenarios. +// When use_offset=false, builds a simple lower-triangular mask matching np.tril(k=0), +// where query position i attends to key positions j where j <= i. +ov::Output build_causal_mask(const ov::Output& Q, const ov::Output& K, bool use_offset) { + auto q_shape = std::make_shared(Q); + auto k_shape = std::make_shared(K); + // Q is 4D: (B, heads, seq_q, head_size), K is 4D: (B, heads, seq_kv, head_size) + auto seq_q = get_dimensions(q_shape, {2}); + auto seq_kv = get_dimensions(k_shape, {2}); + + auto zero = v0::Constant::create(ov::element::i64, ov::Shape{}, {0}); + auto one = v0::Constant::create(ov::element::i64, ov::Shape{}, {1}); + + auto seq_q_scalar = std::make_shared(seq_q, zero); + auto seq_kv_scalar = std::make_shared(seq_kv, zero); + + // Column indices: [0, 1, ..., seq_kv-1] + auto col_indices = std::make_shared(zero, seq_kv_scalar, one, ov::element::i64); + + std::shared_ptr row_indices; + if (use_offset) { + // Row indices adjusted for KV cache offset: [offset, offset+1, ..., offset+seq_q-1] + auto offset = std::make_shared(seq_kv_scalar, seq_q_scalar); + auto end = std::make_shared(offset, seq_q_scalar); + row_indices = std::make_shared(offset, end, one, ov::element::i64); + } else { + // Row indices without offset: [0, 1, ..., seq_q-1] (matches np.tril(k=0)) + row_indices = std::make_shared(zero, seq_q_scalar, one, ov::element::i64); + } + + // Unsqueeze rows to (seq_q, 1) for broadcasting with (seq_kv,) + auto axis = v0::Constant::create(ov::element::i64, ov::Shape{1}, {1}); + auto rows_2d = std::make_shared(row_indices, axis); + + // Lower-triangular: position (i, j) allowed when adjusted_row[i] >= col[j] + auto is_allowed = std::make_shared(rows_2d, col_indices); + + // Convert boolean to additive float mask: true -> 0.0, false -> -10000.0 + return convert_boolean_mask(is_allowed, Q.get_element_type()); +} + +// Build SDPA-based attention (primary fast path) +ov::Output build_sdpa(const ov::Output& Q, + const ov::Output& K, + const ov::Output& V, + bool has_mask, + const ov::Output& attn_mask, + float scale_attr, + bool is_causal) { + ov::OutputVector inputs{Q, K, V}; + if (has_mask) { + inputs.push_back(attn_mask); + } + if (scale_attr != 0.0f) { + if (!has_mask) { + // SDPA interprets inputs positionally (index 3 = mask, index 4 = scale), + // so a zero mask placeholder is needed when only scale is provided + inputs.push_back(v0::Constant::create(Q.get_element_type(), ov::Shape{}, {0.0f})); + } + inputs.push_back(v0::Constant::create(Q.get_element_type(), ov::Shape{}, {scale_attr})); + } + return std::make_shared(inputs, is_causal)->output(0); +} + +// Build manual attention decomposition (for softcap or qk_matmul_output) +// Returns {Y, qk_matmul_output_or_null} +ov::OutputVector build_manual_attention(const ov::Output& Q, + const ov::Output& K, + const ov::Output& V, + bool has_mask, + const ov::Output& attn_mask, + float scale_attr, + float softcap, + bool is_causal, + int64_t qk_matmul_output_mode, + bool needs_qk_output) { + // 1. Q @ K^T + auto qk = std::make_shared(Q, K, false, true); + + // 2. Apply scale + std::shared_ptr scaled_qk; + if (scale_attr != 0.0f) { + auto scale_node = v0::Constant::create(Q.get_element_type(), ov::Shape{}, {scale_attr}); + scaled_qk = std::make_shared(qk, scale_node); + } else { + // Default scale: 1/sqrt(head_size). Q is always 4D here: (B, heads, seq, head_size) + auto q_shape = std::make_shared(Q); + auto head_size = get_dimensions(q_shape, {3}); + auto head_size_f = std::make_shared(head_size, Q.get_element_type()); + auto sqrt_head = std::make_shared(head_size_f); + scaled_qk = std::make_shared(qk, sqrt_head); + } + + // 3. Apply attention mask and causal mask + std::shared_ptr masked = scaled_qk; + if (has_mask) { + masked = std::make_shared(scaled_qk, attn_mask); + } + if (is_causal) { + auto causal_mask = build_causal_mask(Q, K, false); + masked = std::make_shared(masked, causal_mask); + } + + // Capture qk_matmul_output at mode 0 (raw QK) or mode 1 (after mask) + ov::Output qk_debug_output; + if (needs_qk_output && qk_matmul_output_mode == 0) { + qk_debug_output = scaled_qk->output(0); + } else if (needs_qk_output && qk_matmul_output_mode == 1) { + qk_debug_output = masked->output(0); + } + + // 4. Apply softcap: softcap * tanh(scores / softcap) + std::shared_ptr capped = masked; + if (softcap > 0.0f) { + auto cap = v0::Constant::create(Q.get_element_type(), ov::Shape{}, {softcap}); + auto divided = std::make_shared(masked, cap); + auto tanh_out = std::make_shared(divided); + capped = std::make_shared(tanh_out, cap); + } + + // Capture at mode 2 (after softcap) + if (needs_qk_output && qk_matmul_output_mode == 2) { + qk_debug_output = capped->output(0); + } + + // 5. Softmax + auto softmax_out = std::make_shared(capped, -1); + + // Capture at mode 3 (after softmax) + if (needs_qk_output && qk_matmul_output_mode == 3) { + qk_debug_output = softmax_out->output(0); + } + + // 6. softmax @ V + auto output = std::make_shared(softmax_out, V); + + ov::OutputVector results; + results.push_back(output->output(0)); + if (needs_qk_output && qk_debug_output.get_node()) { + results.push_back(qk_debug_output); + } else { + results.push_back(ov::Output{}); + } + return results; +} + } // namespace attention } // namespace onnx } // namespace frontend diff --git a/src/frontends/onnx/frontend/src/utils/attention.hpp b/src/frontends/onnx/frontend/src/utils/attention.hpp index 32d9dca25093..42cec2371301 100644 --- a/src/frontends/onnx/frontend/src/utils/attention.hpp +++ b/src/frontends/onnx/frontend/src/utils/attention.hpp @@ -25,6 +25,69 @@ std::shared_ptr get_dimensions(const std::shared_ptr get_dimensions(const std::shared_ptr& node, const std::vector& dims); +/// \brief Convert boolean mask to float additive mask: true -> 0.0, false -> mask_filter_value. +/// +/// \param mask Boolean tensor where true means "attend" and false means "mask out". +/// \param type Target floating-point element type for the output mask. +/// \param mask_filter_value Value assigned to masked (false) positions (default: -10000.0). +/// \return A Select node producing an additive float mask of the same shape as the input. +ov::Output convert_boolean_mask(const ov::Output& mask, + const ov::element::Type& type, + float mask_filter_value = -10000.0f); + +/// \brief Build additive causal mask of shape (seq_q, seq_kv): 0 for allowed, -10000 for masked. +/// +/// \param Q Query tensor of shape (B, heads, seq_q, head_size). +/// \param K Key tensor of shape (B, heads, seq_kv, head_size). +/// \param use_offset When true, adjusts row indices for KV cache offset so that query position i +/// attends to key positions j where j <= i + (seq_kv - seq_q). When false, builds +/// a simple lower-triangular mask (np.tril with k=0). +/// \return A float additive mask broadcastable over the attention scores. +ov::Output build_causal_mask(const ov::Output& Q, const ov::Output& K, bool use_offset); + +/// \brief Build SDPA-based attention (primary fast path). +/// +/// \param Q Query tensor of shape (B, heads, seq_q, head_size). +/// \param K Key tensor of shape (B, heads, seq_kv, head_size). +/// \param V Value tensor of shape (B, heads, seq_kv, head_size). +/// \param has_mask Whether an attention mask is provided. +/// \param attn_mask Additive attention mask (used only when has_mask is true). +/// \param scale_attr Explicit scale factor; when 0.0 the SDPA op uses the default 1/sqrt(head_size). +/// \param is_causal When true, the SDPA op applies internal causal masking. +/// \return The output of ScaledDotProductAttention. +ov::Output build_sdpa(const ov::Output& Q, + const ov::Output& K, + const ov::Output& V, + bool has_mask, + const ov::Output& attn_mask, + float scale_attr, + bool is_causal); + +/// \brief Build manual attention decomposition (for softcap or qk_matmul_output). +/// +/// \param Q Query tensor of shape (B, heads, seq_q, head_size). +/// \param K Key tensor of shape (B, heads, seq_kv, head_size). +/// \param V Value tensor of shape (B, heads, seq_kv, head_size). +/// \param has_mask Whether an attention mask is provided. +/// \param attn_mask Additive attention mask (used only when has_mask is true). +/// \param scale_attr Explicit scale factor; when 0.0, uses default 1/sqrt(head_size). +/// \param softcap Soft-capping value; when > 0, applies softcap * tanh(scores / softcap). +/// \param is_causal When true, adds an additive causal mask to attention scores. +/// \param qk_matmul_output_mode Selects at which stage the intermediate QK tensor is captured: +/// 0 = after scale, 1 = after mask, 2 = after softcap, 3 = after softmax. +/// \param needs_qk_output When true, the intermediate QK tensor is returned as the second output. +/// \return A two-element OutputVector: {attention_output, qk_intermediate_or_empty}. +ov::OutputVector build_manual_attention(const ov::Output& Q, + const ov::Output& K, + const ov::Output& V, + bool has_mask, + const ov::Output& attn_mask, + float scale_attr, + float softcap, + bool is_causal, + int64_t qk_matmul_output_mode, + bool needs_qk_output); + } // namespace attention } // namespace onnx } // namespace frontend diff --git a/src/frontends/onnx/frontend/src/utils/common.cpp b/src/frontends/onnx/frontend/src/utils/common.cpp index 06676be96d81..8050012fd42d 100644 --- a/src/frontends/onnx/frontend/src/utils/common.cpp +++ b/src/frontends/onnx/frontend/src/utils/common.cpp @@ -202,6 +202,13 @@ bool is_failsafe_node(const std::shared_ptr& node) { return rt_info.find(FAILSAFE_NODE) != rt_info.end(); } +bool is_constant_empty_node(const std::shared_ptr& node) { + if (auto constant_node = ov::as_type_ptr(node)) { + return shape_size(constant_node->get_shape()) == 0; + } + return false; +} + const std::string OPTIMIZED_OUT_NODE = "OPTIMIZED_OUT_NODE"; void mark_as_optimized_out(ov::Output& node_output) { diff --git a/src/frontends/onnx/frontend/src/utils/common.hpp b/src/frontends/onnx/frontend/src/utils/common.hpp index 0c6314f944fb..31b747be3773 100644 --- a/src/frontends/onnx/frontend/src/utils/common.hpp +++ b/src/frontends/onnx/frontend/src/utils/common.hpp @@ -198,6 +198,12 @@ inline uint32_t convert_float_seed(const float seed) { /// \param rank Rank used for axis normalization. /// \return Normalized axis value. int64_t normalize_axis(const std::string& description, const int64_t axis, const Rank& rank); + +/// \brief Checks if a given node is a Constant with an empty data. +/// \param node The node to check. +/// \return `true` if the node is a Constant with an empty data, else `false`. +bool is_constant_empty_node(const std::shared_ptr& node); + } // namespace common } // namespace onnx } // namespace frontend diff --git a/src/frontends/onnx/frontend/src/utils/onnx_internal.cpp b/src/frontends/onnx/frontend/src/utils/onnx_internal.cpp index c2f9f975f6b2..8de419110d28 100644 --- a/src/frontends/onnx/frontend/src/utils/onnx_internal.cpp +++ b/src/frontends/onnx/frontend/src/utils/onnx_internal.cpp @@ -88,21 +88,21 @@ void convert_decoded_model(std::shared_ptr model) { } std::shared_ptr import_onnx_model(std::shared_ptr model_proto, - const std::string& model_path, + const std::filesystem::path& model_path, detail::MappedMemoryHandles mmap_cache, ov::frontend::ExtensionHolder extensions) { apply_transformations(*model_proto); - Graph graph{ov::util::get_directory(model_path).string(), model_proto, mmap_cache, std::move(extensions)}; + Graph graph{ov::util::get_directory(model_path), model_proto, mmap_cache, std::move(extensions)}; return graph.convert(); } std::shared_ptr decode_to_framework_nodes(std::shared_ptr model_proto, - const std::string& model_path, + const std::filesystem::path& model_path, detail::MappedMemoryHandles mmap_cache, ov::frontend::ExtensionHolder extensions) { apply_transformations(*model_proto); auto graph = - std::make_shared(ov::util::get_directory(model_path).string(), model_proto, mmap_cache, extensions); + std::make_shared(ov::util::get_directory(model_path), model_proto, mmap_cache, std::move(extensions)); return graph->decode(); } } // namespace ov::frontend::onnx::detail diff --git a/src/frontends/onnx/frontend/src/utils/onnx_internal.hpp b/src/frontends/onnx/frontend/src/utils/onnx_internal.hpp index 88add1d3422a..b9eddbe7b0ab 100644 --- a/src/frontends/onnx/frontend/src/utils/onnx_internal.hpp +++ b/src/frontends/onnx/frontend/src/utils/onnx_internal.hpp @@ -4,6 +4,7 @@ #pragma once +#include #include #include @@ -38,7 +39,7 @@ using ::ONNX_NAMESPACE::ModelProto; /// \return An ov::Model that represents a single output from the created /// graph. std::shared_ptr import_onnx_model(std::shared_ptr model_proto, - const std::string& model_path, + const std::filesystem::path& model_path, detail::MappedMemoryHandles mmap_cache, ov::frontend::ExtensionHolder extensions = {}); @@ -51,7 +52,7 @@ std::shared_ptr import_onnx_model(std::shared_ptr model_p /// \param extensions An object containing a collection of frontend extensions to use during the import process /// \return A ov::Model with ONNXFrameworkNodes std::shared_ptr decode_to_framework_nodes(std::shared_ptr model_proto, - const std::string& model_path, + const std::filesystem::path& model_path, detail::MappedMemoryHandles mmap_cache, ov::frontend::ExtensionHolder extensions = {}); diff --git a/src/frontends/onnx/frontend/src/utils/tensor_external_data.cpp b/src/frontends/onnx/frontend/src/utils/tensor_external_data.cpp index f715d9f4558a..2146fe68ebbc 100644 --- a/src/frontends/onnx/frontend/src/utils/tensor_external_data.cpp +++ b/src/frontends/onnx/frontend/src/utils/tensor_external_data.cpp @@ -8,6 +8,7 @@ #include #include "exceptions.hpp" +#include "openvino/runtime/lazy_buffer.hpp" #include "openvino/util/file_util.hpp" #include "openvino/util/log.hpp" @@ -18,7 +19,7 @@ namespace detail { TensorExternalData::TensorExternalData(const TensorProto& tensor) { for (const auto& entry : tensor.external_data()) { if (entry.key() == "location") { - m_data_location = ov::util::sanitize_path(entry.value()); + m_data_location = entry.value(); } else if (entry.key() == "offset") { m_offset = std::stoull(entry.value()); } else if (entry.key() == "length") { @@ -39,22 +40,27 @@ TensorExternalData::TensorExternalData(const std::string& location, size_t offse m_data_length = size; } -Buffer TensorExternalData::load_external_mmap_data(const std::string& model_dir, +Buffer TensorExternalData::load_external_mmap_data(const std::filesystem::path& model_dir, MappedMemoryHandles cache) const { - const auto full_path = model_dir.empty() ? ov::util::make_path(m_data_location) - : ov::util::make_path(ov::util::path_join({model_dir, m_data_location})); + std::filesystem::path full_path; + try { + full_path = ov::util::sanitize_path(model_dir, ov::util::make_path(m_data_location)); + } catch (const std::runtime_error& e) { + throw error::invalid_external_data{e.what()}; + } + const int64_t file_size = ov::util::file_size(full_path); if (file_size <= 0 || m_data_length > static_cast(file_size) || m_offset > static_cast(file_size) - m_data_length) { throw error::invalid_external_data{*this}; } - auto cached_mapped_memory = cache->find(ov::util::path_to_string(full_path)); + auto cached_mapped_memory = cache->find(full_path); std::shared_ptr mapped_memory; if (cached_mapped_memory != cache->end()) { mapped_memory = cached_mapped_memory->second; } else { mapped_memory = ov::load_mmap_object(full_path); - (*cache)[ov::util::path_to_string(full_path)] = mapped_memory; + (*cache)[full_path] = mapped_memory; } if (m_data_length > mapped_memory->size() || mapped_memory->size() == 0) { throw error::invalid_external_data{*this}; @@ -65,34 +71,46 @@ Buffer TensorExternalData::load_external_mmap_data(const std:: mapped_memory); } -Buffer TensorExternalData::load_external_data(const std::string& model_dir) const { - const auto full_path = model_dir.empty() ? ov::util::make_path(m_data_location) - : std::filesystem::absolute(std::filesystem::weakly_canonical( - ov::util::path_join({model_dir, m_data_location}))); - std::ifstream external_data_stream(full_path, std::ios::binary | std::ios::in | std::ios::ate); - - if (external_data_stream.fail()) { - throw error::invalid_external_data{*this}; +Buffer TensorExternalData::load_external_data(const std::filesystem::path& model_dir) const { + std::filesystem::path full_path; + try { + full_path = ov::util::sanitize_path(model_dir, ov::util::make_path(m_data_location)); + } catch (const std::runtime_error& e) { + throw error::invalid_external_data{e.what()}; } - const uint64_t file_size = static_cast(external_data_stream.tellg()); - if (m_data_length > file_size || m_offset > file_size - m_data_length) { + + const auto file_size = util::file_size(full_path); + if (file_size < 0 || m_data_length > static_cast(file_size) || + m_offset > static_cast(file_size) - m_data_length) { throw error::invalid_external_data{*this}; } uint64_t read_data_length = m_data_length > 0 ? m_data_length : static_cast(file_size) - m_offset; + const auto get_now_buffer = [&]() { + std::ifstream external_data_stream(full_path, std::ios::binary | std::ios::in | std::ios::ate); + if (external_data_stream.fail()) { + throw error::invalid_external_data{*this}; + } + // default value of m_offset is 0 + external_data_stream.seekg(m_offset, std::ios::beg); - // default value of m_offset is 0 - external_data_stream.seekg(m_offset, std::ios::beg); - - auto read_data = std::make_shared(read_data_length); - external_data_stream.read(read_data->get_ptr(), read_data_length); - external_data_stream.close(); - - auto buffer = std::make_shared>>(read_data->get_ptr(), - read_data->size(), - read_data); + auto read_data = std::make_shared(read_data_length); + external_data_stream.read(read_data->get_ptr(), read_data_length); + external_data_stream.close(); + return std::make_shared>>(read_data->get_ptr(), + read_data->size(), + read_data); + }; + const auto get_lazy_buffer = [&]() { + const auto lazy = std::make_shared(full_path, m_offset, read_data_length); + return std::make_shared>>( + static_cast(lazy->get_reserved_ptr()), + lazy->size(), + lazy); + }; - return buffer; + constexpr size_t lazy_loading_threshold = 0x100000; // 1MB + return read_data_length >= lazy_loading_threshold ? get_lazy_buffer() : get_now_buffer(); } Buffer TensorExternalData::load_external_mem_data() const { diff --git a/src/frontends/onnx/frontend/src/utils/tensor_external_data.hpp b/src/frontends/onnx/frontend/src/utils/tensor_external_data.hpp index b1682f6fd426..f02395037137 100644 --- a/src/frontends/onnx/frontend/src/utils/tensor_external_data.hpp +++ b/src/frontends/onnx/frontend/src/utils/tensor_external_data.hpp @@ -6,6 +6,8 @@ #include +#include + #include "openvino/runtime/aligned_buffer.hpp" #include "openvino/runtime/shared_buffer.hpp" #include "openvino/util/mmap_object.hpp" @@ -17,8 +19,8 @@ namespace detail { using ::ONNX_NAMESPACE::TensorProto; template using Buffer = std::shared_ptr>>; -using MappedMemoryHandles = std::shared_ptr>>; -using LocalStreamHandles = std::shared_ptr>>; +using MappedMemoryHandles = std::shared_ptr>>; +using LocalStreamHandles = std::shared_ptr>>; /// \brief Helper class used to load tensor data from external files class TensorExternalData { public: @@ -32,7 +34,7 @@ class TensorExternalData { /// the invalid_external_data exception is thrown. /// /// \return External binary data loaded into the SharedBuffer - Buffer load_external_data(const std::string& model_dir) const; + Buffer load_external_data(const std::filesystem::path& model_dir) const; /// \brief Map (mmap for lin, MapViewOfFile for win) external data from tensor passed to constructor /// @@ -41,7 +43,8 @@ class TensorExternalData { /// the invalid_external_data exception is thrown. /// /// \return External binary data loaded into the SharedBuffer - Buffer load_external_mmap_data(const std::string& model_dir, MappedMemoryHandles cache) const; + Buffer load_external_mmap_data(const std::filesystem::path& model_dir, + MappedMemoryHandles cache) const; /// \brief Load external data from existing shared memory when m_data_location is ORT_MEM_ADDR /// diff --git a/src/frontends/onnx/onnx_common/include/onnx_common/parser.hpp b/src/frontends/onnx/onnx_common/include/onnx_common/parser.hpp index ac1422086215..25e1c6fce7e9 100644 --- a/src/frontends/onnx/onnx_common/include/onnx_common/parser.hpp +++ b/src/frontends/onnx/onnx_common/include/onnx_common/parser.hpp @@ -5,6 +5,7 @@ #pragma once #include +#include #include #include @@ -18,10 +19,7 @@ using namespace ::ONNX_NAMESPACE; /// \param file_path Path to the file containing an ONNX model. /// /// \return The parsed in-memory representation of the ONNX model -ModelProto parse_from_file(const std::string& file_path); -#if defined(OPENVINO_ENABLE_UNICODE_PATH_SUPPORT) && defined(_WIN32) -ModelProto parse_from_file(const std::wstring& file_path); -#endif +ModelProto parse_from_file(const std::filesystem::path& file_path); /// \brief Parses an ONNX model from a stream (representing for example a file) /// diff --git a/src/frontends/onnx/onnx_common/src/parser.cpp b/src/frontends/onnx/onnx_common/src/parser.cpp index b7a83a0b99ff..0d665b1670c8 100644 --- a/src/frontends/onnx/onnx_common/src/parser.cpp +++ b/src/frontends/onnx/onnx_common/src/parser.cpp @@ -17,32 +17,16 @@ namespace ov { namespace frontend { namespace onnx { namespace common { -ModelProto parse_from_file(const std::string& file_path) { - std::ifstream file_stream{file_path.c_str(), std::ios::in | std::ios::binary}; +ModelProto parse_from_file(const std::filesystem::path& file_path) { + std::ifstream file_stream{file_path, std::ios::binary}; - if (!file_stream.is_open()) { - OPENVINO_THROW("Could not open the file: \"" + file_path, '"'); - }; + OPENVINO_ASSERT(file_stream.is_open(), "Could not open the file: ", file_path); auto model_proto = parse_from_istream(file_stream); file_stream.close(); return model_proto; } -#if defined(OPENVINO_ENABLE_UNICODE_PATH_SUPPORT) && defined(_WIN32) -ModelProto parse_from_file(const std::wstring& file_path) { - std::ifstream file_stream{file_path.c_str(), std::ios::in | std::ios::binary}; - - if (!file_stream.is_open()) { - OPENVINO_THROW("Could not open the file: \"", ov::util::wstring_to_string(file_path), '"'); - }; - - auto model_proto = parse_from_istream(file_stream); - file_stream.close(); - return model_proto; -} -#endif - ModelProto parse_from_istream(std::istream& model_stream) { if (!model_stream.good()) { model_stream.clear(); diff --git a/src/frontends/onnx/tests/CMakeLists.txt b/src/frontends/onnx/tests/CMakeLists.txt index 5a2e00d075f6..ff240c5cb97d 100644 --- a/src/frontends/onnx/tests/CMakeLists.txt +++ b/src/frontends/onnx/tests/CMakeLists.txt @@ -51,6 +51,7 @@ endif() set(MULTI_TEST_SRC onnx_import.in.cpp onnx_import_com_microsoft.in.cpp + onnx_import_ml_normalizer.in.cpp onnx_import_controlflow.in.cpp onnx_import_const_folding.in.cpp onnx_import_convpool.in.cpp diff --git a/src/frontends/onnx/tests/__init__.py b/src/frontends/onnx/tests/__init__.py index 8309e2f0614d..25aeb8191c8b 100644 --- a/src/frontends/onnx/tests/__init__.py +++ b/src/frontends/onnx/tests/__init__.py @@ -52,8 +52,7 @@ def xfail_test(reason="Mark the test as expected to fail", strict=True): "Axes input must be constant") skip_bitwise_ui64 = pytest.mark.skip(reason="AssertionError: Not equal to tolerance rtol=0.001, atol=1e-07") xfail_issue_99950 = xfail_test(reason="CenterCropPad func is not supported") -xfail_issue_99952 = xfail_test(reason="Col2Im operator is not supported") -xfail_issue_99954 = xfail_test(reason="Constant Pad - RuntimeError: Shape inference of Reference node with name y failed") +xfail_issue_99952 = xfail_test(reason="Col2Im operator is not supported 5 dimensions") xfail_issue_99955 = xfail_test(reason="GroupNorm is not supported") xfail_issue_99957 = xfail_test(reason="LayerNorm - RuntimeError: While validating node ''") xfail_issue_99960 = xfail_test(reason="MVN - Results mismatch") @@ -67,7 +66,6 @@ def xfail_test(reason="Mark the test as expected to fail", strict=True): xfail_issue_99970 = xfail_test(reason="Scatter and ScatterND - RuntimeError: Check '(reduction == none)' failed at " "src/frontends/onnx/frontend/src/op/scatter_elements.cpp OR at " "src/frontends/onnx/frontend/src/op/scatter_nd") -xfail_issue_38710 = xfail_test(reason="RuntimeError: data has zero dimension which is not allowed") xfail_issue_38713 = xfail_test(reason="RuntimeError: OV does not support the following ONNX operations: " "ai.onnx.preview.training.Momentum") xfail_issue_38724 = xfail_test(reason="RuntimeError: While validating ONNX node '': " @@ -178,7 +176,5 @@ def xfail_test(reason="Mark the test as expected to fail", strict=True): # ONNX 1.18 xfail_issue_171767 = pytest.mark.skip(reason="Unsupported element type: FLOAT4E2M1") -xfail_issue_171768 = pytest.mark.skip(reason="Unsupported feature: RMSNormalization") -xfail_issue_171770 = pytest.mark.skip(reason="Unsupported feature: RotaryEmbedding") xfail_issue_171771 = pytest.mark.skip(reason="Mismatches in tests: Top K values") xfail_issue_171772 = pytest.mark.skip(reason="Mismatches in tests: AveragePool") diff --git a/src/frontends/onnx/tests/convert_tests.cpp b/src/frontends/onnx/tests/convert_tests.cpp index b7af058a9341..e6762f6dca52 100644 --- a/src/frontends/onnx/tests/convert_tests.cpp +++ b/src/frontends/onnx/tests/convert_tests.cpp @@ -9,6 +9,7 @@ #include "common_test_utils/file_utils.hpp" #include "common_test_utils/test_assertions.hpp" #include "onnx_utils.hpp" +#include "openvino/frontend/exception.hpp" #include "openvino/frontend/manager.hpp" using namespace ov::frontend::onnx::tests; @@ -76,3 +77,21 @@ TEST(ONNXFeConvertException, exception_if_qlinear_concat_invalid_x_input_triplet ov::AssertFailure, testing::AllOf(testing::HasSubstr("expected 2 + 3*N inputs"), testing::HasSubstr(" got: 6"))); } + +TEST(ONNXFeConvertException, exception_if_scan_num_scan_inputs_exceeds_body_inputs) { + OV_EXPECT_THROW(convert_model("scan15_num_scan_inputs_exceeds_body_inputs.onnx"), + ov::frontend::OpConversionFailure, + testing::HasSubstr("num_scan_inputs (10) negative or exceeds the number of body graph inputs (2)")); +} + +TEST(ONNXFeConvertException, exception_if_scan_negative_num_scan_inputs) { + OV_EXPECT_THROW(convert_model("scan15_negative_num_scan_inputs.onnx"), + ov::frontend::OpConversionFailure, + testing::HasSubstr("num_scan_inputs attribute is negative: -5")); +} + +TEST(ONNXFeConvertException, exception_if_scan_body_fewer_outputs_than_initial_values) { + OV_EXPECT_THROW(convert_model("scan15_body_fewer_outputs_than_initial_values.onnx"), + ov::frontend::OpConversionFailure, + testing::HasSubstr("num_scan_outputs can't be negative")); +} diff --git a/src/frontends/onnx/tests/graph_iterator.cpp b/src/frontends/onnx/tests/graph_iterator.cpp index c3524199f937..77fb38df93b1 100644 --- a/src/frontends/onnx/tests/graph_iterator.cpp +++ b/src/frontends/onnx/tests/graph_iterator.cpp @@ -508,3 +508,83 @@ TEST(FrontEndGraphIteratorTest, override_all_outputs_empty_throws) { EXPECT_THROW(input_model->override_all_outputs({}), ov::frontend::GeneralFailure); } + +// +// graph { +// node { input: "inputA" input: "shape" output: "expandOutput_0" op_type: "Expand" } +// initializer { dims: 0 data_type: INT64 name: "shape" } // no data +// input { name: "inputA" type { tensor_type { elem_type: FLOAT shape {} } } } +// output { name: "expandOutput_0" type { tensor_type { elem_type: FLOAT shape {} } } } +// } +TEST(FrontEndGraphIteratorTest, loads_programmatic_empty_shape_initializer_graph) { + auto model_proto = std::make_shared(); + model_proto->set_ir_version(13); + model_proto->set_producer_name("OpenVINO ONNX Frontend"); + + auto* opset = model_proto->add_opset_import(); + opset->set_version(25); + + auto* graph = model_proto->mutable_graph(); + graph->set_name(""); + + // Expand node: Expand("inputA", "shape") -> "expandOutput_0" + auto* node = graph->add_node(); + node->set_name("_0"); + node->set_op_type("Expand"); + node->add_input("inputA"); + node->add_input("shape"); + node->add_output("expandOutput_0"); + + // Initializer: "shape" — 1-D INT64 tensor with 0 elements and NO data payload. + // When decoded: m_data=nullptr, m_data_location=nullptr, partial_shape={0}. + // This triggers the `else if (get_partial_shape() == PartialShape{0})` branch in + // translate_session.cpp (line 87), creating an empty Constant rather than reading data. + auto* init = graph->add_initializer(); + init->set_name("shape"); + init->set_data_type(7); // INT64 + init->add_dims(0); // shape [0]: 1-D tensor with zero elements + + // Input: "inputA" — float32 scalar (empty shape) + auto* input = graph->add_input(); + input->set_name("inputA"); + auto* input_type = input->mutable_type()->mutable_tensor_type(); + input_type->set_elem_type(1); // FLOAT + input_type->mutable_shape(); // empty shape → scalar + + // Output: "expandOutput_0" — float32 scalar (empty shape) + auto* output = graph->add_output(); + output->set_name("expandOutput_0"); + auto* output_type = output->mutable_type()->mutable_tensor_type(); + output_type->set_elem_type(1); // FLOAT + output_type->mutable_shape(); // empty shape → scalar + + auto iter = std::make_shared( + ov::frontend::onnx::GraphIteratorProtoMemoryManagementMode::External_Stream); + iter->initialize(model_proto); + iter->reset(); + + auto graph_iter = std::dynamic_pointer_cast(iter); + auto frontend = ov::frontend::FrontEndManager().load_by_framework("onnx"); + ASSERT_NE(frontend, nullptr); + ASSERT_TRUE(frontend->supported(graph_iter)); + + auto input_model = frontend->load(graph_iter); + ASSERT_NE(input_model, nullptr); + + std::shared_ptr model; + ASSERT_NO_THROW(model = frontend->convert(input_model)); + ASSERT_NE(model, nullptr); + + ASSERT_EQ(iter->get_mmap_cache(), nullptr); + ASSERT_NE(iter->get_stream_cache(), nullptr); + ASSERT_EQ(iter->get_stream_cache()->size(), 0); // no external files opened + ASSERT_EQ(model->get_ordered_ops().size(), 4); // Parameter, Constant(shape), Expand, Result + for (const auto& op : model->get_ordered_ops()) { + if (ov::is_type(op)) { + auto const_node = ov::as_type_ptr(op); + EXPECT_EQ(const_node->get_element_type(), ov::element::i64); + EXPECT_EQ(const_node->get_shape(), ov::Shape{1}); + EXPECT_EQ(const_node->get_vector(), std::vector{1}); + } + } +} diff --git a/src/frontends/onnx/tests/load_from.cpp b/src/frontends/onnx/tests/load_from.cpp index a3e62e5a8446..b8704eb1c8ad 100644 --- a/src/frontends/onnx/tests/load_from.cpp +++ b/src/frontends/onnx/tests/load_from.cpp @@ -31,10 +31,8 @@ static LoadFromFEParam getTestData() { } TEST_P(FrontEndLoadFromTest, testLoadFromStreamAndPassPath) { - const auto path = - ov::util::path_join( - {ov::test::utils::getExecutableDirectory(), TEST_ONNX_MODELS_DIRNAME, "external_data/external_data.onnx"}) - .string(); + const auto path = ov::util::path_join( + {ov::test::utils::getExecutableDirectory(), TEST_ONNX_MODELS_DIRNAME, "external_data/external_data.onnx"}); std::ifstream ifs(path, std::ios::in | std::ios::binary); ASSERT_TRUE(ifs.is_open()) << "Could not open an ifstream for the model path: " << path; std::istream* is = &ifs; @@ -67,8 +65,7 @@ TEST_P(FrontEndLoadFromTest, load_model_not_exists_at_path) { TEST_P(FrontEndLoadFromTest, load_model_and_apply_ppp) { auto model_file_path = - ov::util::path_join({ov::test::utils::getExecutableDirectory(), TEST_ONNX_MODELS_DIRNAME, m_param.m_stream}) - .string(); + ov::util::path_join({ov::test::utils::getExecutableDirectory(), TEST_ONNX_MODELS_DIRNAME, m_param.m_stream}); m_frontEnd = m_fem.load_by_model(model_file_path); const auto fe_model = m_frontEnd->load(model_file_path); @@ -102,7 +99,7 @@ using ::ONNX_NAMESPACE::Version; TEST_P(FrontEndLoadFromTest, testLoadFromModelProtoUint64) { const auto path = - ov::util::path_join({ov::test::utils::getExecutableDirectory(), TEST_ONNX_MODELS_DIRNAME, "abs.onnx"}).string(); + ov::util::path_join({ov::test::utils::getExecutableDirectory(), TEST_ONNX_MODELS_DIRNAME, "abs.onnx"}); std::ifstream ifs(path, std::ios::in | std::ios::binary); ASSERT_TRUE(ifs.is_open()) << "Could not open an ifstream for the model path: " << path; std::vector frontends; @@ -130,7 +127,7 @@ TEST_P(FrontEndLoadFromTest, testLoadFromModelProtoUint64) { TEST_P(FrontEndLoadFromTest, testLoadFromModelProtoUint64_Negative) { const auto path = - ov::util::path_join({ov::test::utils::getExecutableDirectory(), TEST_ONNX_MODELS_DIRNAME, "abs.onnx"}).string(); + ov::util::path_join({ov::test::utils::getExecutableDirectory(), TEST_ONNX_MODELS_DIRNAME, "abs.onnx"}); std::ifstream ifs(path, std::ios::in | std::ios::binary); ASSERT_TRUE(ifs.is_open()) << "Could not open an ifstream for the model path: " << path; std::vector frontends; diff --git a/src/frontends/onnx/tests/models/ai.onnx.ml/normalizer_l1.prototxt b/src/frontends/onnx/tests/models/ai.onnx.ml/normalizer_l1.prototxt new file mode 100644 index 000000000000..4e35e4e7eed2 --- /dev/null +++ b/src/frontends/onnx/tests/models/ai.onnx.ml/normalizer_l1.prototxt @@ -0,0 +1,56 @@ +ir_version: 7 +producer_name: "OpenVINO ONNX Frontend" +graph { + node { + input: "X" + output: "Y" + name: "Normalizer_L1" + op_type: "Normalizer" + attribute { + name: "norm" + s: "L1" + type: STRING + } + domain: "ai.onnx.ml" + } + name: "normalizer_l1_graph" + input { + name: "X" + type { + tensor_type { + elem_type: 1 + shape { + dim { + dim_value: 2 + } + dim { + dim_value: 3 + } + } + } + } + } + output { + name: "Y" + type { + tensor_type { + elem_type: 1 + shape { + dim { + dim_value: 2 + } + dim { + dim_value: 3 + } + } + } + } + } +} +opset_import { + version: 15 +} +opset_import { + domain: "ai.onnx.ml" + version: 3 +} diff --git a/src/frontends/onnx/tests/models/ai.onnx.ml/normalizer_l2.prototxt b/src/frontends/onnx/tests/models/ai.onnx.ml/normalizer_l2.prototxt new file mode 100644 index 000000000000..47a7a0dc464f --- /dev/null +++ b/src/frontends/onnx/tests/models/ai.onnx.ml/normalizer_l2.prototxt @@ -0,0 +1,56 @@ +ir_version: 7 +producer_name: "OpenVINO ONNX Frontend" +graph { + node { + input: "X" + output: "Y" + name: "Normalizer_L2" + op_type: "Normalizer" + attribute { + name: "norm" + s: "L2" + type: STRING + } + domain: "ai.onnx.ml" + } + name: "normalizer_l2_graph" + input { + name: "X" + type { + tensor_type { + elem_type: 1 + shape { + dim { + dim_value: 3 + } + dim { + dim_value: 4 + } + } + } + } + } + output { + name: "Y" + type { + tensor_type { + elem_type: 1 + shape { + dim { + dim_value: 3 + } + dim { + dim_value: 4 + } + } + } + } + } +} +opset_import { + version: 15 +} +opset_import { + domain: "ai.onnx.ml" + version: 3 +} diff --git a/src/frontends/onnx/tests/models/ai.onnx.ml/normalizer_max.prototxt b/src/frontends/onnx/tests/models/ai.onnx.ml/normalizer_max.prototxt new file mode 100644 index 000000000000..979c5c973935 --- /dev/null +++ b/src/frontends/onnx/tests/models/ai.onnx.ml/normalizer_max.prototxt @@ -0,0 +1,56 @@ +ir_version: 7 +producer_name: "OpenVINO ONNX Frontend" +graph { + node { + input: "X" + output: "Y" + name: "Normalizer_MAX" + op_type: "Normalizer" + attribute { + name: "norm" + s: "MAX" + type: STRING + } + domain: "ai.onnx.ml" + } + name: "normalizer_max_graph" + input { + name: "X" + type { + tensor_type { + elem_type: 1 + shape { + dim { + dim_value: 2 + } + dim { + dim_value: 5 + } + } + } + } + } + output { + name: "Y" + type { + tensor_type { + elem_type: 1 + shape { + dim { + dim_value: 2 + } + dim { + dim_value: 5 + } + } + } + } + } +} +opset_import { + version: 15 +} +opset_import { + domain: "ai.onnx.ml" + version: 3 +} diff --git a/src/frontends/onnx/tests/models/col2im_2D_batch_opset_18.prototxt b/src/frontends/onnx/tests/models/col2im_2D_batch_opset_18.prototxt new file mode 100644 index 000000000000..c049208acd40 --- /dev/null +++ b/src/frontends/onnx/tests/models/col2im_2D_batch_opset_18.prototxt @@ -0,0 +1,103 @@ +ir_version: 8 +producer_name: "OpenVINO ONNX Frontend" +graph { + node { + input: "input" + input: "image_shape" + input: "block_shape" + output: "output" + name: "col2im_2D_batch_opset_18" + op_type: "Col2Im" + attribute { + name: "dilations" + ints: 1 + ints: 1 + type: INTS + } + attribute { + name: "pads" + ints: 0 + ints: 0 + ints: 0 + ints: 0 + type: INTS + } + attribute { + name: "strides" + ints: 1 + ints: 1 + type: INTS + } + } + name: "col2im_2D_batch_opset_18" + input { + name: "input" + type { + tensor_type { + elem_type: 1 + shape { + dim { + dim_value: 2 + } + dim { + dim_value: 4 + } + dim { + dim_value: 4 + } + } + } + } + } + input { + name: "image_shape" + type { + tensor_type { + elem_type: 7 + shape { + dim { + dim_value: 2 + } + } + } + } + } + input { + name: "block_shape" + type { + tensor_type { + elem_type: 7 + shape { + dim { + dim_value: 2 + } + } + } + } + } + output { + name: "output" + type { + tensor_type { + elem_type: 1 + shape { + dim { + dim_value: 2 + } + dim { + dim_value: 1 + } + dim { + dim_value: 3 + } + dim { + dim_value: 3 + } + } + } + } + } +} +opset_import { + version: 18 +} diff --git a/src/frontends/onnx/tests/models/col2im_2D_channel_opset_18.prototxt b/src/frontends/onnx/tests/models/col2im_2D_channel_opset_18.prototxt new file mode 100644 index 000000000000..56d7b2a7fe8c --- /dev/null +++ b/src/frontends/onnx/tests/models/col2im_2D_channel_opset_18.prototxt @@ -0,0 +1,103 @@ +ir_version: 8 +producer_name: "OpenVINO ONNX Frontend" +graph { + node { + input: "input" + input: "image_shape" + input: "block_shape" + output: "output" + name: "col2im_2D_channel_opset_18" + op_type: "Col2Im" + attribute { + name: "dilations" + ints: 1 + ints: 1 + type: INTS + } + attribute { + name: "pads" + ints: 0 + ints: 0 + ints: 0 + ints: 0 + type: INTS + } + attribute { + name: "strides" + ints: 1 + ints: 1 + type: INTS + } + } + name: "col2im_2D_channel_opset_18" + input { + name: "input" + type { + tensor_type { + elem_type: 1 + shape { + dim { + dim_value: 1 + } + dim { + dim_value: 12 + } + dim { + dim_value: 4 + } + } + } + } + } + input { + name: "image_shape" + type { + tensor_type { + elem_type: 7 + shape { + dim { + dim_value: 2 + } + } + } + } + } + input { + name: "block_shape" + type { + tensor_type { + elem_type: 7 + shape { + dim { + dim_value: 2 + } + } + } + } + } + output { + name: "output" + type { + tensor_type { + elem_type: 1 + shape { + dim { + dim_value: 1 + } + dim { + dim_value: 3 + } + dim { + dim_value: 3 + } + dim { + dim_value: 3 + } + } + } + } + } +} +opset_import { + version: 18 +} diff --git a/src/frontends/onnx/tests/models/col2im_2D_default_opset_18.prototxt b/src/frontends/onnx/tests/models/col2im_2D_default_opset_18.prototxt new file mode 100644 index 000000000000..0a66c36ea853 --- /dev/null +++ b/src/frontends/onnx/tests/models/col2im_2D_default_opset_18.prototxt @@ -0,0 +1,103 @@ +ir_version: 8 +producer_name: "OpenVINO ONNX Frontend" +graph { + node { + input: "input" + input: "image_shape" + input: "block_shape" + output: "output" + name: "col2im_2D_default_opset_18" + op_type: "Col2Im" + attribute { + name: "dilations" + ints: 1 + ints: 1 + type: INTS + } + attribute { + name: "pads" + ints: 0 + ints: 0 + ints: 0 + ints: 0 + type: INTS + } + attribute { + name: "strides" + ints: 1 + ints: 1 + type: INTS + } + } + name: "col2im_2D_default_opset_18" + input { + name: "input" + type { + tensor_type { + elem_type: 1 + shape { + dim { + dim_value: 1 + } + dim { + dim_value: 4 + } + dim { + dim_value: 4 + } + } + } + } + } + input { + name: "image_shape" + type { + tensor_type { + elem_type: 7 + shape { + dim { + dim_value: 2 + } + } + } + } + } + input { + name: "block_shape" + type { + tensor_type { + elem_type: 7 + shape { + dim { + dim_value: 2 + } + } + } + } + } + output { + name: "output" + type { + tensor_type { + elem_type: 1 + shape { + dim { + dim_value: 1 + } + dim { + dim_value: 1 + } + dim { + dim_value: 3 + } + dim { + dim_value: 3 + } + } + } + } + } +} +opset_import { + version: 18 +} diff --git a/src/frontends/onnx/tests/models/col2im_2D_dilations_opset_18.prototxt b/src/frontends/onnx/tests/models/col2im_2D_dilations_opset_18.prototxt new file mode 100644 index 000000000000..f64779e9db31 --- /dev/null +++ b/src/frontends/onnx/tests/models/col2im_2D_dilations_opset_18.prototxt @@ -0,0 +1,103 @@ +ir_version: 8 +producer_name: "OpenVINO ONNX Frontend" +graph { + node { + input: "input" + input: "image_shape" + input: "block_shape" + output: "output" + name: "col2im_2D_dilations_opset_18" + op_type: "Col2Im" + attribute { + name: "dilations" + ints: 2 + ints: 2 + type: INTS + } + attribute { + name: "pads" + ints: 0 + ints: 0 + ints: 0 + ints: 0 + type: INTS + } + attribute { + name: "strides" + ints: 1 + ints: 1 + type: INTS + } + } + name: "col2im_2D_dilations_opset_18" + input { + name: "input" + type { + tensor_type { + elem_type: 1 + shape { + dim { + dim_value: 1 + } + dim { + dim_value: 4 + } + dim { + dim_value: 9 + } + } + } + } + } + input { + name: "image_shape" + type { + tensor_type { + elem_type: 7 + shape { + dim { + dim_value: 2 + } + } + } + } + } + input { + name: "block_shape" + type { + tensor_type { + elem_type: 7 + shape { + dim { + dim_value: 2 + } + } + } + } + } + output { + name: "output" + type { + tensor_type { + elem_type: 1 + shape { + dim { + dim_value: 1 + } + dim { + dim_value: 1 + } + dim { + dim_value: 5 + } + dim { + dim_value: 5 + } + } + } + } + } +} +opset_import { + version: 18 +} diff --git a/src/frontends/onnx/tests/models/col2im_2D_pads_opset_18.prototxt b/src/frontends/onnx/tests/models/col2im_2D_pads_opset_18.prototxt new file mode 100644 index 000000000000..ae624ef4059c --- /dev/null +++ b/src/frontends/onnx/tests/models/col2im_2D_pads_opset_18.prototxt @@ -0,0 +1,103 @@ +ir_version: 8 +producer_name: "OpenVINO ONNX Frontend" +graph { + node { + input: "input" + input: "image_shape" + input: "block_shape" + output: "output" + name: "col2im_2D_pads_opset_18" + op_type: "Col2Im" + attribute { + name: "dilations" + ints: 1 + ints: 1 + type: INTS + } + attribute { + name: "pads" + ints: 1 + ints: 1 + ints: 1 + ints: 1 + type: INTS + } + attribute { + name: "strides" + ints: 1 + ints: 1 + type: INTS + } + } + name: "col2im_2D_pads_opset_18" + input { + name: "input" + type { + tensor_type { + elem_type: 1 + shape { + dim { + dim_value: 1 + } + dim { + dim_value: 4 + } + dim { + dim_value: 9 + } + } + } + } + } + input { + name: "image_shape" + type { + tensor_type { + elem_type: 7 + shape { + dim { + dim_value: 2 + } + } + } + } + } + input { + name: "block_shape" + type { + tensor_type { + elem_type: 7 + shape { + dim { + dim_value: 2 + } + } + } + } + } + output { + name: "output" + type { + tensor_type { + elem_type: 1 + shape { + dim { + dim_value: 1 + } + dim { + dim_value: 1 + } + dim { + dim_value: 2 + } + dim { + dim_value: 2 + } + } + } + } + } +} +opset_import { + version: 18 +} diff --git a/src/frontends/onnx/tests/models/col2im_2D_strides_opset_18.prototxt b/src/frontends/onnx/tests/models/col2im_2D_strides_opset_18.prototxt new file mode 100644 index 000000000000..82e70a1b8fb2 --- /dev/null +++ b/src/frontends/onnx/tests/models/col2im_2D_strides_opset_18.prototxt @@ -0,0 +1,103 @@ +ir_version: 8 +producer_name: "OpenVINO ONNX Frontend" +graph { + node { + input: "input" + input: "image_shape" + input: "block_shape" + output: "output" + name: "col2im_2D_strides_opset_18" + op_type: "Col2Im" + attribute { + name: "dilations" + ints: 1 + ints: 1 + type: INTS + } + attribute { + name: "pads" + ints: 0 + ints: 0 + ints: 0 + ints: 0 + type: INTS + } + attribute { + name: "strides" + ints: 2 + ints: 2 + type: INTS + } + } + name: "col2im_2D_strides_opset_18" + input { + name: "input" + type { + tensor_type { + elem_type: 1 + shape { + dim { + dim_value: 1 + } + dim { + dim_value: 4 + } + dim { + dim_value: 4 + } + } + } + } + } + input { + name: "image_shape" + type { + tensor_type { + elem_type: 7 + shape { + dim { + dim_value: 2 + } + } + } + } + } + input { + name: "block_shape" + type { + tensor_type { + elem_type: 7 + shape { + dim { + dim_value: 2 + } + } + } + } + } + output { + name: "output" + type { + tensor_type { + elem_type: 1 + shape { + dim { + dim_value: 1 + } + dim { + dim_value: 1 + } + dim { + dim_value: 4 + } + dim { + dim_value: 4 + } + } + } + } + } +} +opset_import { + version: 18 +} diff --git a/src/frontends/onnx/tests/models/com.microsoft/bifurcation_detector_no_pred.prototxt b/src/frontends/onnx/tests/models/com.microsoft/bifurcation_detector_no_pred.prototxt new file mode 100644 index 000000000000..0a16b52c8a5b --- /dev/null +++ b/src/frontends/onnx/tests/models/com.microsoft/bifurcation_detector_no_pred.prototxt @@ -0,0 +1,52 @@ +ir_version: 7 +producer_name: "OpenVINO ONNX Frontend" +graph { + node { + input: "src_tokens" + input: "cur_tokens" + input: "prev_suffix_match_idx" + output: "tokens" + output: "suffix_match_idx" + name: "BifurcationDetector_1" + op_type: "BifurcationDetector" + attribute { + name: "min_ngram_size" + i: 1 + type: INT + } + attribute { + name: "max_ngram_size" + i: 2 + type: INT + } + domain: "com.microsoft" + } + name: "test_graph" + input { + name: "src_tokens" + type { tensor_type { elem_type: 7 shape { dim { dim_param: "S" } } } } + } + input { + name: "cur_tokens" + type { tensor_type { elem_type: 7 shape { dim { dim_param: "C" } } } } + } + input { + name: "prev_suffix_match_idx" + type { tensor_type { elem_type: 7 shape { } } } + } + output { + name: "tokens" + type { tensor_type { elem_type: 7 shape { dim { dim_param: "T" } } } } + } + output { + name: "suffix_match_idx" + type { tensor_type { elem_type: 7 shape { } } } + } +} +opset_import { + version: 13 +} +opset_import { + domain: "com.microsoft" + version: 1 +} diff --git a/src/frontends/onnx/tests/models/com.microsoft/bifurcation_detector_no_pred_ngram_5_7.prototxt b/src/frontends/onnx/tests/models/com.microsoft/bifurcation_detector_no_pred_ngram_5_7.prototxt new file mode 100644 index 000000000000..dcabaeeec190 --- /dev/null +++ b/src/frontends/onnx/tests/models/com.microsoft/bifurcation_detector_no_pred_ngram_5_7.prototxt @@ -0,0 +1,52 @@ +ir_version: 7 +producer_name: "OpenVINO ONNX Frontend" +graph { + node { + input: "src_tokens" + input: "cur_tokens" + input: "prev_suffix_match_idx" + output: "tokens" + output: "suffix_match_idx" + name: "BifurcationDetector_1" + op_type: "BifurcationDetector" + attribute { + name: "min_ngram_size" + i: 5 + type: INT + } + attribute { + name: "max_ngram_size" + i: 7 + type: INT + } + domain: "com.microsoft" + } + name: "test_graph" + input { + name: "src_tokens" + type { tensor_type { elem_type: 7 shape { dim { dim_param: "S" } } } } + } + input { + name: "cur_tokens" + type { tensor_type { elem_type: 7 shape { dim { dim_param: "C" } } } } + } + input { + name: "prev_suffix_match_idx" + type { tensor_type { elem_type: 7 shape { } } } + } + output { + name: "tokens" + type { tensor_type { elem_type: 7 shape { dim { dim_param: "T" } } } } + } + output { + name: "suffix_match_idx" + type { tensor_type { elem_type: 7 shape { } } } + } +} +opset_import { + version: 13 +} +opset_import { + domain: "com.microsoft" + version: 1 +} diff --git a/src/frontends/onnx/tests/models/com.microsoft/bifurcation_detector_with_pred.prototxt b/src/frontends/onnx/tests/models/com.microsoft/bifurcation_detector_with_pred.prototxt new file mode 100644 index 000000000000..6e0d40cd821a --- /dev/null +++ b/src/frontends/onnx/tests/models/com.microsoft/bifurcation_detector_with_pred.prototxt @@ -0,0 +1,57 @@ +ir_version: 7 +producer_name: "OpenVINO ONNX Frontend" +graph { + node { + input: "src_tokens" + input: "cur_tokens" + input: "prev_suffix_match_idx" + input: "pred_tokens" + output: "tokens" + output: "suffix_match_idx" + name: "BifurcationDetector_1" + op_type: "BifurcationDetector" + attribute { + name: "min_ngram_size" + i: 1 + type: INT + } + attribute { + name: "max_ngram_size" + i: 1 + type: INT + } + domain: "com.microsoft" + } + name: "test_graph" + input { + name: "src_tokens" + type { tensor_type { elem_type: 7 shape { dim { dim_param: "S" } } } } + } + input { + name: "cur_tokens" + type { tensor_type { elem_type: 7 shape { dim { dim_param: "C" } } } } + } + input { + name: "prev_suffix_match_idx" + type { tensor_type { elem_type: 7 shape { } } } + } + input { + name: "pred_tokens" + type { tensor_type { elem_type: 7 shape { dim { dim_param: "P" } } } } + } + output { + name: "tokens" + type { tensor_type { elem_type: 7 shape { dim { dim_param: "T" } } } } + } + output { + name: "suffix_match_idx" + type { tensor_type { elem_type: 7 shape { } } } + } +} +opset_import { + version: 13 +} +opset_import { + domain: "com.microsoft" + version: 1 +} diff --git a/src/frontends/onnx/tests/models/com.microsoft/gqa_static_rotary.prototxt b/src/frontends/onnx/tests/models/com.microsoft/gqa_static_rotary.prototxt new file mode 100644 index 000000000000..fea3e38e50ac --- /dev/null +++ b/src/frontends/onnx/tests/models/com.microsoft/gqa_static_rotary.prototxt @@ -0,0 +1,191 @@ +ir_version: 10 +graph { + node { + input: "query" + input: "" + input: "" + input: "past_key" + input: "past_value" + input: "seqlens_k" + input: "total_sequence_length" + input: "cos_cache" + input: "sin_cache" + output: "output" + output: "present_key" + output: "present_value" + name: "GroupQueryAttention_0" + op_type: "GroupQueryAttention" + attribute { + name: "do_rotary" + i: 1 + type: INT + } + attribute { + name: "kv_num_heads" + i: 1 + type: INT + } + attribute { + name: "local_window_size" + i: -1 + type: INT + } + attribute { + name: "num_heads" + i: 2 + type: INT + } + attribute { + name: "rotary_interleaved" + i: 0 + type: INT + } + attribute { + name: "smooth_softmax" + i: 0 + type: INT + } + attribute { + name: "softcap" + f: 0 + type: FLOAT + } + domain: "com.microsoft" + } + name: "GroupQueryAttention_Static_Graph" + input { + name: "query" + type { + tensor_type { + elem_type: 1 + shape { + dim { dim_value: 1 } + dim { dim_value: 1 } + dim { dim_value: 64 } + } + } + } + } + input { + name: "past_key" + type { + tensor_type { + elem_type: 1 + shape { + dim { dim_value: 1 } + dim { dim_value: 1 } + dim { dim_value: 8 } + dim { dim_value: 16 } + } + } + } + } + input { + name: "past_value" + type { + tensor_type { + elem_type: 1 + shape { + dim { dim_value: 1 } + dim { dim_value: 1 } + dim { dim_value: 8 } + dim { dim_value: 16 } + } + } + } + } + input { + name: "seqlens_k" + type { + tensor_type { + elem_type: 6 + shape { + dim { dim_value: 1 } + dim { dim_value: 1 } + } + } + } + } + input { + name: "total_sequence_length" + type { + tensor_type { + elem_type: 6 + shape { + } + } + } + } + input { + name: "cos_cache" + type { + tensor_type { + elem_type: 1 + shape { + dim { dim_value: 8 } + dim { dim_value: 8 } + } + } + } + } + input { + name: "sin_cache" + type { + tensor_type { + elem_type: 1 + shape { + dim { dim_value: 8 } + dim { dim_value: 8 } + } + } + } + } + output { + name: "output" + type { + tensor_type { + elem_type: 1 + shape { + dim { dim_value: 1 } + dim { dim_value: 1 } + dim { dim_value: 32 } + } + } + } + } + output { + name: "present_key" + type { + tensor_type { + elem_type: 1 + shape { + dim { dim_value: 1 } + dim { dim_value: 1 } + dim { dim_value: 8 } + dim { dim_value: 16 } + } + } + } + } + output { + name: "present_value" + type { + tensor_type { + elem_type: 1 + shape { + dim { dim_value: 1 } + dim { dim_value: 1 } + dim { dim_value: 8 } + dim { dim_value: 16 } + } + } + } + } +} +opset_import { + version: 11 +} +opset_import { + domain: "com.microsoft" + version: 1 +} diff --git a/src/frontends/onnx/tests/models/com.microsoft/multi_head_attention_01_packed_qkv.prototxt b/src/frontends/onnx/tests/models/com.microsoft/multi_head_attention_01_packed_qkv.prototxt new file mode 100644 index 000000000000..03dc890e8906 --- /dev/null +++ b/src/frontends/onnx/tests/models/com.microsoft/multi_head_attention_01_packed_qkv.prototxt @@ -0,0 +1,38 @@ +ir_version: 10 +producer_name: "OpenVINO ONNX Frontend" +graph { + node { + input: "query" + output: "output" + name: "MultiHeadAttention_01_packed_qkv" + op_type: "MultiHeadAttention" + attribute { + name: "num_heads" + i: 2 + type: INT + } + domain: "com.microsoft" + } + name: "multi_head_attention_01_packed_qkv" + input { + name: "query" + type { tensor_type { elem_type: 1 + shape { + dim { dim_value: 1 } + dim { dim_value: 4 } + dim { dim_value: 2 } + dim { dim_value: 3 } + dim { dim_value: 3 } + } } } + } + output { + name: "output" + type { tensor_type { elem_type: 1 + shape { dim { dim_value: 1 } dim { dim_value: 4 } dim { dim_value: 6 } } } } + } +} +opset_import { version: 11 } +opset_import { + domain: "com.microsoft" + version: 1 +} \ No newline at end of file diff --git a/src/frontends/onnx/tests/models/com.microsoft/multi_head_attention_02_query_packed_kv.prototxt b/src/frontends/onnx/tests/models/com.microsoft/multi_head_attention_02_query_packed_kv.prototxt new file mode 100644 index 000000000000..cef02b86f741 --- /dev/null +++ b/src/frontends/onnx/tests/models/com.microsoft/multi_head_attention_02_query_packed_kv.prototxt @@ -0,0 +1,44 @@ +ir_version: 10 +producer_name: "OpenVINO ONNX Frontend" +graph { + node { + input: "query" + input: "key" + output: "output" + name: "MultiHeadAttention_02_query_packed_kv" + op_type: "MultiHeadAttention" + attribute { + name: "num_heads" + i: 2 + type: INT + } + domain: "com.microsoft" + } + name: "multi_head_attention_02_query_packed_kv" + input { + name: "query" + type { tensor_type { elem_type: 1 + shape { dim { dim_value: 1 } dim { dim_value: 4 } dim { dim_value: 6 } } } } + } + input { + name: "key" + type { tensor_type { elem_type: 1 + shape { + dim { dim_value: 1 } + dim { dim_value: 4 } + dim { dim_value: 2 } + dim { dim_value: 2 } + dim { dim_value: 3 } + } } } + } + output { + name: "output" + type { tensor_type { elem_type: 1 + shape { dim { dim_value: 1 } dim { dim_value: 4 } dim { dim_value: 6 } } } } + } +} +opset_import { version: 11 } +opset_import { + domain: "com.microsoft" + version: 1 +} \ No newline at end of file diff --git a/src/frontends/onnx/tests/models/com.microsoft/multi_head_attention_03_qkv_regular.prototxt b/src/frontends/onnx/tests/models/com.microsoft/multi_head_attention_03_qkv_regular.prototxt new file mode 100644 index 000000000000..b40fcf7b458f --- /dev/null +++ b/src/frontends/onnx/tests/models/com.microsoft/multi_head_attention_03_qkv_regular.prototxt @@ -0,0 +1,44 @@ +ir_version: 10 +producer_name: "OpenVINO ONNX Frontend" +graph { + node { + input: "query" + input: "key" + input: "value" + output: "output" + name: "MultiHeadAttention_03_qkv_regular" + op_type: "MultiHeadAttention" + attribute { + name: "num_heads" + i: 2 + type: INT + } + domain: "com.microsoft" + } + name: "multi_head_attention_03_qkv_regular" + input { + name: "query" + type { tensor_type { elem_type: 1 + shape { dim { dim_value: 1 } dim { dim_value: 4 } dim { dim_value: 6 } } } } + } + input { + name: "key" + type { tensor_type { elem_type: 1 + shape { dim { dim_value: 1 } dim { dim_value: 4 } dim { dim_value: 6 } } } } + } + input { + name: "value" + type { tensor_type { elem_type: 1 + shape { dim { dim_value: 1 } dim { dim_value: 4 } dim { dim_value: 6 } } } } + } + output { + name: "output" + type { tensor_type { elem_type: 1 + shape { dim { dim_value: 1 } dim { dim_value: 4 } dim { dim_value: 6 } } } } + } +} +opset_import { version: 11 } +opset_import { + domain: "com.microsoft" + version: 1 +} \ No newline at end of file diff --git a/src/frontends/onnx/tests/models/com.microsoft/multi_head_attention_04_cross_attn.prototxt b/src/frontends/onnx/tests/models/com.microsoft/multi_head_attention_04_cross_attn.prototxt new file mode 100644 index 000000000000..b661d40017b0 --- /dev/null +++ b/src/frontends/onnx/tests/models/com.microsoft/multi_head_attention_04_cross_attn.prototxt @@ -0,0 +1,44 @@ +ir_version: 10 +producer_name: "OpenVINO ONNX Frontend" +graph { + node { + input: "query" + input: "key" + input: "value" + output: "output" + name: "MultiHeadAttention_04_cross_attn" + op_type: "MultiHeadAttention" + attribute { + name: "num_heads" + i: 2 + type: INT + } + domain: "com.microsoft" + } + name: "multi_head_attention_04_cross_attn" + input { + name: "query" + type { tensor_type { elem_type: 1 + shape { dim { dim_value: 1 } dim { dim_value: 4 } dim { dim_value: 6 } } } } + } + input { + name: "key" + type { tensor_type { elem_type: 1 + shape { dim { dim_value: 1 } dim { dim_value: 2 } dim { dim_value: 4 } dim { dim_value: 3 } } } } + } + input { + name: "value" + type { tensor_type { elem_type: 1 + shape { dim { dim_value: 1 } dim { dim_value: 2 } dim { dim_value: 4 } dim { dim_value: 3 } } } } + } + output { + name: "output" + type { tensor_type { elem_type: 1 + shape { dim { dim_value: 1 } dim { dim_value: 4 } dim { dim_value: 6 } } } } + } +} +opset_import { version: 11 } +opset_import { + domain: "com.microsoft" + version: 1 +} \ No newline at end of file diff --git a/src/frontends/onnx/tests/models/com.microsoft/multi_head_attention_05_qkv_with_bias.prototxt b/src/frontends/onnx/tests/models/com.microsoft/multi_head_attention_05_qkv_with_bias.prototxt new file mode 100644 index 000000000000..1e20555c4368 --- /dev/null +++ b/src/frontends/onnx/tests/models/com.microsoft/multi_head_attention_05_qkv_with_bias.prototxt @@ -0,0 +1,50 @@ +ir_version: 10 +producer_name: "OpenVINO ONNX Frontend" +graph { + node { + input: "query" + input: "key" + input: "value" + input: "bias" + output: "output" + name: "MultiHeadAttention_05_qkv_with_bias" + op_type: "MultiHeadAttention" + attribute { + name: "num_heads" + i: 2 + type: INT + } + domain: "com.microsoft" + } + name: "multi_head_attention_05_qkv_with_bias" + input { + name: "query" + type { tensor_type { elem_type: 1 + shape { dim { dim_value: 1 } dim { dim_value: 4 } dim { dim_value: 6 } } } } + } + input { + name: "key" + type { tensor_type { elem_type: 1 + shape { dim { dim_value: 1 } dim { dim_value: 4 } dim { dim_value: 6 } } } } + } + input { + name: "value" + type { tensor_type { elem_type: 1 + shape { dim { dim_value: 1 } dim { dim_value: 4 } dim { dim_value: 6 } } } } + } + input { + name: "bias" + type { tensor_type { elem_type: 1 + shape { dim { dim_value: 18 } } } } + } + output { + name: "output" + type { tensor_type { elem_type: 1 + shape { dim { dim_value: 1 } dim { dim_value: 4 } dim { dim_value: 6 } } } } + } +} +opset_import { version: 11 } +opset_import { + domain: "com.microsoft" + version: 1 +} \ No newline at end of file diff --git a/src/frontends/onnx/tests/models/com.microsoft/multi_head_attention_06_mask_batch.prototxt b/src/frontends/onnx/tests/models/com.microsoft/multi_head_attention_06_mask_batch.prototxt new file mode 100644 index 000000000000..19cc57a0bec3 --- /dev/null +++ b/src/frontends/onnx/tests/models/com.microsoft/multi_head_attention_06_mask_batch.prototxt @@ -0,0 +1,51 @@ +ir_version: 10 +producer_name: "OpenVINO ONNX Frontend" +graph { + node { + input: "query" + input: "key" + input: "value" + input: "" + input: "key_padding_mask" + output: "output" + name: "MultiHeadAttention_06_mask_batch" + op_type: "MultiHeadAttention" + attribute { + name: "num_heads" + i: 2 + type: INT + } + domain: "com.microsoft" + } + name: "multi_head_attention_06_mask_batch" + input { + name: "query" + type { tensor_type { elem_type: 1 + shape { dim { dim_value: 2 } dim { dim_value: 4 } dim { dim_value: 6 } } } } + } + input { + name: "key" + type { tensor_type { elem_type: 1 + shape { dim { dim_value: 2 } dim { dim_value: 4 } dim { dim_value: 6 } } } } + } + input { + name: "value" + type { tensor_type { elem_type: 1 + shape { dim { dim_value: 2 } dim { dim_value: 4 } dim { dim_value: 6 } } } } + } + input { + name: "key_padding_mask" + type { tensor_type { elem_type: 6 + shape { dim { dim_value: 2 } } } } + } + output { + name: "output" + type { tensor_type { elem_type: 1 + shape { dim { dim_value: 2 } dim { dim_value: 4 } dim { dim_value: 6 } } } } + } +} +opset_import { version: 11 } +opset_import { + domain: "com.microsoft" + version: 1 +} \ No newline at end of file diff --git a/src/frontends/onnx/tests/models/com.microsoft/multi_head_attention_07_mask_packed_metadata.prototxt b/src/frontends/onnx/tests/models/com.microsoft/multi_head_attention_07_mask_packed_metadata.prototxt new file mode 100644 index 000000000000..1b69769a5164 --- /dev/null +++ b/src/frontends/onnx/tests/models/com.microsoft/multi_head_attention_07_mask_packed_metadata.prototxt @@ -0,0 +1,51 @@ +ir_version: 10 +producer_name: "OpenVINO ONNX Frontend" +graph { + node { + input: "query" + input: "key" + input: "value" + input: "" + input: "key_padding_mask" + output: "output" + name: "MultiHeadAttention_07_mask_packed_metadata" + op_type: "MultiHeadAttention" + attribute { + name: "num_heads" + i: 2 + type: INT + } + domain: "com.microsoft" + } + name: "multi_head_attention_07_mask_packed_metadata" + input { + name: "query" + type { tensor_type { elem_type: 1 + shape { dim { dim_value: 1 } dim { dim_value: 4 } dim { dim_value: 6 } } } } + } + input { + name: "key" + type { tensor_type { elem_type: 1 + shape { dim { dim_value: 1 } dim { dim_value: 4 } dim { dim_value: 6 } } } } + } + input { + name: "value" + type { tensor_type { elem_type: 1 + shape { dim { dim_value: 1 } dim { dim_value: 4 } dim { dim_value: 6 } } } } + } + input { + name: "key_padding_mask" + type { tensor_type { elem_type: 6 + shape { dim { dim_value: 5 } } } } + } + output { + name: "output" + type { tensor_type { elem_type: 1 + shape { dim { dim_value: 1 } dim { dim_value: 4 } dim { dim_value: 6 } } } } + } +} +opset_import { version: 11 } +opset_import { + domain: "com.microsoft" + version: 1 +} \ No newline at end of file diff --git a/src/frontends/onnx/tests/models/com.microsoft/multi_head_attention_08_mask_2D.prototxt b/src/frontends/onnx/tests/models/com.microsoft/multi_head_attention_08_mask_2D.prototxt new file mode 100644 index 000000000000..ec508c069740 --- /dev/null +++ b/src/frontends/onnx/tests/models/com.microsoft/multi_head_attention_08_mask_2D.prototxt @@ -0,0 +1,51 @@ +ir_version: 10 +producer_name: "OpenVINO ONNX Frontend" +graph { + node { + input: "query" + input: "key" + input: "value" + input: "" + input: "key_padding_mask" + output: "output" + name: "MultiHeadAttention_08_mask_2D" + op_type: "MultiHeadAttention" + attribute { + name: "num_heads" + i: 2 + type: INT + } + domain: "com.microsoft" + } + name: "multi_head_attention_08_mask_2D" + input { + name: "query" + type { tensor_type { elem_type: 1 + shape { dim { dim_value: 2 } dim { dim_value: 4 } dim { dim_value: 6 } } } } + } + input { + name: "key" + type { tensor_type { elem_type: 1 + shape { dim { dim_value: 2 } dim { dim_value: 4 } dim { dim_value: 6 } } } } + } + input { + name: "value" + type { tensor_type { elem_type: 1 + shape { dim { dim_value: 2 } dim { dim_value: 4 } dim { dim_value: 6 } } } } + } + input { + name: "key_padding_mask" + type { tensor_type { elem_type: 6 + shape { dim { dim_value: 2 } dim { dim_value: 4 } } } } + } + output { + name: "output" + type { tensor_type { elem_type: 1 + shape { dim { dim_value: 2 } dim { dim_value: 4 } dim { dim_value: 6 } } } } + } +} +opset_import { version: 11 } +opset_import { + domain: "com.microsoft" + version: 1 +} \ No newline at end of file diff --git a/src/frontends/onnx/tests/models/com.microsoft/multi_head_attention_09_mask_3D.prototxt b/src/frontends/onnx/tests/models/com.microsoft/multi_head_attention_09_mask_3D.prototxt new file mode 100644 index 000000000000..3c2eb311efdf --- /dev/null +++ b/src/frontends/onnx/tests/models/com.microsoft/multi_head_attention_09_mask_3D.prototxt @@ -0,0 +1,51 @@ +ir_version: 10 +producer_name: "OpenVINO ONNX Frontend" +graph { + node { + input: "query" + input: "key" + input: "value" + input: "" + input: "key_padding_mask" + output: "output" + name: "MultiHeadAttention_09_mask_3D" + op_type: "MultiHeadAttention" + attribute { + name: "num_heads" + i: 2 + type: INT + } + domain: "com.microsoft" + } + name: "multi_head_attention_09_mask_3D" + input { + name: "query" + type { tensor_type { elem_type: 1 + shape { dim { dim_value: 2 } dim { dim_value: 4 } dim { dim_value: 6 } } } } + } + input { + name: "key" + type { tensor_type { elem_type: 1 + shape { dim { dim_value: 2 } dim { dim_value: 4 } dim { dim_value: 6 } } } } + } + input { + name: "value" + type { tensor_type { elem_type: 1 + shape { dim { dim_value: 2 } dim { dim_value: 4 } dim { dim_value: 6 } } } } + } + input { + name: "key_padding_mask" + type { tensor_type { elem_type: 6 + shape { dim { dim_value: 2 } dim { dim_value: 4 } dim { dim_value: 4 } } } } + } + output { + name: "output" + type { tensor_type { elem_type: 1 + shape { dim { dim_value: 2 } dim { dim_value: 4 } dim { dim_value: 6 } } } } + } +} +opset_import { version: 11 } +opset_import { + domain: "com.microsoft" + version: 1 +} \ No newline at end of file diff --git a/src/frontends/onnx/tests/models/com.microsoft/multi_head_attention_10_attention_bias.prototxt b/src/frontends/onnx/tests/models/com.microsoft/multi_head_attention_10_attention_bias.prototxt new file mode 100644 index 000000000000..39b2b3a6f54c --- /dev/null +++ b/src/frontends/onnx/tests/models/com.microsoft/multi_head_attention_10_attention_bias.prototxt @@ -0,0 +1,52 @@ +ir_version: 10 +producer_name: "OpenVINO ONNX Frontend" +graph { + node { + input: "query" + input: "key" + input: "value" + input: "" + input: "" + input: "attention_bias" + output: "output" + name: "MultiHeadAttention_10_attention_bias" + op_type: "MultiHeadAttention" + attribute { + name: "num_heads" + i: 2 + type: INT + } + domain: "com.microsoft" + } + name: "multi_head_attention_10_attention_bias" + input { + name: "query" + type { tensor_type { elem_type: 1 + shape { dim { dim_value: 2 } dim { dim_value: 4 } dim { dim_value: 6 } } } } + } + input { + name: "key" + type { tensor_type { elem_type: 1 + shape { dim { dim_value: 2 } dim { dim_value: 4 } dim { dim_value: 6 } } } } + } + input { + name: "value" + type { tensor_type { elem_type: 1 + shape { dim { dim_value: 2 } dim { dim_value: 4 } dim { dim_value: 6 } } } } + } + input { + name: "attention_bias" + type { tensor_type { elem_type: 1 + shape { dim { dim_value: 2 } dim { dim_value: 1 } dim { dim_value: 4 } dim { dim_value: 4 } } } } + } + output { + name: "output" + type { tensor_type { elem_type: 1 + shape { dim { dim_value: 2 } dim { dim_value: 4 } dim { dim_value: 6 } } } } + } +} +opset_import { version: 11 } +opset_import { + domain: "com.microsoft" + version: 1 +} \ No newline at end of file diff --git a/src/frontends/onnx/tests/models/com.microsoft/multi_head_attention_11_custom_scale.prototxt b/src/frontends/onnx/tests/models/com.microsoft/multi_head_attention_11_custom_scale.prototxt new file mode 100644 index 000000000000..3ff2890ce6cd --- /dev/null +++ b/src/frontends/onnx/tests/models/com.microsoft/multi_head_attention_11_custom_scale.prototxt @@ -0,0 +1,49 @@ +ir_version: 10 +producer_name: "OpenVINO ONNX Frontend" +graph { + node { + input: "query" + input: "key" + input: "value" + output: "output" + name: "MultiHeadAttention_11_custom_scale" + op_type: "MultiHeadAttention" + attribute { + name: "num_heads" + i: 2 + type: INT + } + attribute { + name: "scale" + f: 0.5 + type: FLOAT + } + domain: "com.microsoft" + } + name: "multi_head_attention_11_custom_scale" + input { + name: "query" + type { tensor_type { elem_type: 1 + shape { dim { dim_value: 1 } dim { dim_value: 4 } dim { dim_value: 6 } } } } + } + input { + name: "key" + type { tensor_type { elem_type: 1 + shape { dim { dim_value: 1 } dim { dim_value: 4 } dim { dim_value: 6 } } } } + } + input { + name: "value" + type { tensor_type { elem_type: 1 + shape { dim { dim_value: 1 } dim { dim_value: 4 } dim { dim_value: 6 } } } } + } + output { + name: "output" + type { tensor_type { elem_type: 1 + shape { dim { dim_value: 1 } dim { dim_value: 4 } dim { dim_value: 6 } } } } + } +} +opset_import { version: 11 } +opset_import { + domain: "com.microsoft" + version: 1 +} \ No newline at end of file diff --git a/src/frontends/onnx/tests/models/com.microsoft/multi_head_attention_12_unidirectional.prototxt b/src/frontends/onnx/tests/models/com.microsoft/multi_head_attention_12_unidirectional.prototxt new file mode 100644 index 000000000000..d43e689e03a8 --- /dev/null +++ b/src/frontends/onnx/tests/models/com.microsoft/multi_head_attention_12_unidirectional.prototxt @@ -0,0 +1,49 @@ +ir_version: 10 +producer_name: "OpenVINO ONNX Frontend" +graph { + node { + input: "query" + input: "key" + input: "value" + output: "output" + name: "MultiHeadAttention_12_unidirectional" + op_type: "MultiHeadAttention" + attribute { + name: "num_heads" + i: 2 + type: INT + } + attribute { + name: "unidirectional" + i: 1 + type: INT + } + domain: "com.microsoft" + } + name: "multi_head_attention_12_unidirectional" + input { + name: "query" + type { tensor_type { elem_type: 1 + shape { dim { dim_value: 1 } dim { dim_value: 4 } dim { dim_value: 6 } } } } + } + input { + name: "key" + type { tensor_type { elem_type: 1 + shape { dim { dim_value: 1 } dim { dim_value: 4 } dim { dim_value: 6 } } } } + } + input { + name: "value" + type { tensor_type { elem_type: 1 + shape { dim { dim_value: 1 } dim { dim_value: 4 } dim { dim_value: 6 } } } } + } + output { + name: "output" + type { tensor_type { elem_type: 1 + shape { dim { dim_value: 1 } dim { dim_value: 4 } dim { dim_value: 6 } } } } + } +} +opset_import { version: 11 } +opset_import { + domain: "com.microsoft" + version: 1 +} \ No newline at end of file diff --git a/src/frontends/onnx/tests/models/com.microsoft/multi_head_attention_13_mask_filter_value.prototxt b/src/frontends/onnx/tests/models/com.microsoft/multi_head_attention_13_mask_filter_value.prototxt new file mode 100644 index 000000000000..b67ca3cea082 --- /dev/null +++ b/src/frontends/onnx/tests/models/com.microsoft/multi_head_attention_13_mask_filter_value.prototxt @@ -0,0 +1,56 @@ +ir_version: 10 +producer_name: "OpenVINO ONNX Frontend" +graph { + node { + input: "query" + input: "key" + input: "value" + input: "" + input: "key_padding_mask" + output: "output" + name: "MultiHeadAttention_13_mask_filter_value" + op_type: "MultiHeadAttention" + attribute { + name: "num_heads" + i: 2 + type: INT + } + attribute { + name: "mask_filter_value" + f: -1.0 + type: FLOAT + } + domain: "com.microsoft" + } + name: "multi_head_attention_13_mask_filter_value" + input { + name: "query" + type { tensor_type { elem_type: 1 + shape { dim { dim_value: 1 } dim { dim_value: 4 } dim { dim_value: 6 } } } } + } + input { + name: "key" + type { tensor_type { elem_type: 1 + shape { dim { dim_value: 1 } dim { dim_value: 4 } dim { dim_value: 6 } } } } + } + input { + name: "value" + type { tensor_type { elem_type: 1 + shape { dim { dim_value: 1 } dim { dim_value: 4 } dim { dim_value: 6 } } } } + } + input { + name: "key_padding_mask" + type { tensor_type { elem_type: 6 + shape { dim { dim_value: 1 } } } } + } + output { + name: "output" + type { tensor_type { elem_type: 1 + shape { dim { dim_value: 1 } dim { dim_value: 4 } dim { dim_value: 6 } } } } + } +} +opset_import { version: 11 } +opset_import { + domain: "com.microsoft" + version: 1 +} \ No newline at end of file diff --git a/src/frontends/onnx/tests/models/com.microsoft/multi_head_attention_14_cache_standard.prototxt b/src/frontends/onnx/tests/models/com.microsoft/multi_head_attention_14_cache_standard.prototxt new file mode 100644 index 000000000000..211abbc9f5be --- /dev/null +++ b/src/frontends/onnx/tests/models/com.microsoft/multi_head_attention_14_cache_standard.prototxt @@ -0,0 +1,71 @@ +ir_version: 10 +producer_name: "OpenVINO ONNX Frontend" +graph { + node { + input: "query" + input: "key" + input: "value" + input: "" + input: "" + input: "" + input: "past_key" + input: "past_value" + output: "output" + output: "present_key" + output: "present_value" + name: "MultiHeadAttention_14_cache_standard" + op_type: "MultiHeadAttention" + attribute { + name: "num_heads" + i: 2 + type: INT + } + domain: "com.microsoft" + } + name: "multi_head_attention_14_cache_standard" + input { + name: "query" + type { tensor_type { elem_type: 1 + shape { dim { dim_value: 1 } dim { dim_value: 2 } dim { dim_value: 6 } } } } + } + input { + name: "key" + type { tensor_type { elem_type: 1 + shape { dim { dim_value: 1 } dim { dim_value: 2 } dim { dim_value: 6 } } } } + } + input { + name: "value" + type { tensor_type { elem_type: 1 + shape { dim { dim_value: 1 } dim { dim_value: 2 } dim { dim_value: 6 } } } } + } + input { + name: "past_key" + type { tensor_type { elem_type: 1 + shape { dim { dim_value: 1 } dim { dim_value: 2 } dim { dim_value: 3 } dim { dim_value: 3 } } } } + } + input { + name: "past_value" + type { tensor_type { elem_type: 1 + shape { dim { dim_value: 1 } dim { dim_value: 2 } dim { dim_value: 3 } dim { dim_value: 3 } } } } + } + output { + name: "output" + type { tensor_type { elem_type: 1 + shape { dim { dim_value: 1 } dim { dim_value: 2 } dim { dim_value: 6 } } } } + } + output { + name: "present_key" + type { tensor_type { elem_type: 1 + shape { dim { dim_value: 1 } dim { dim_value: 2 } dim { dim_value: 5 } dim { dim_value: 3 } } } } + } + output { + name: "present_value" + type { tensor_type { elem_type: 1 + shape { dim { dim_value: 1 } dim { dim_value: 2 } dim { dim_value: 5 } dim { dim_value: 3 } } } } + } +} +opset_import { version: 11 } +opset_import { + domain: "com.microsoft" + version: 1 +} \ No newline at end of file diff --git a/src/frontends/onnx/tests/models/com.microsoft/multi_head_attention_15_cache_buffer_sharing.prototxt b/src/frontends/onnx/tests/models/com.microsoft/multi_head_attention_15_cache_buffer_sharing.prototxt new file mode 100644 index 000000000000..6b057a50563b --- /dev/null +++ b/src/frontends/onnx/tests/models/com.microsoft/multi_head_attention_15_cache_buffer_sharing.prototxt @@ -0,0 +1,77 @@ +ir_version: 10 +producer_name: "OpenVINO ONNX Frontend" +graph { + node { + input: "query" + input: "key" + input: "value" + input: "" + input: "" + input: "" + input: "past_key" + input: "past_value" + input: "past_sequence_length" + output: "output" + output: "present_key" + output: "present_value" + name: "MultiHeadAttention_15_cache_buffer_sharing" + op_type: "MultiHeadAttention" + attribute { + name: "num_heads" + i: 2 + type: INT + } + domain: "com.microsoft" + } + name: "multi_head_attention_15_cache_buffer_sharing" + input { + name: "query" + type { tensor_type { elem_type: 1 + shape { dim { dim_value: 1 } dim { dim_value: 1 } dim { dim_value: 6 } } } } + } + input { + name: "key" + type { tensor_type { elem_type: 1 + shape { dim { dim_value: 1 } dim { dim_value: 1 } dim { dim_value: 6 } } } } + } + input { + name: "value" + type { tensor_type { elem_type: 1 + shape { dim { dim_value: 1 } dim { dim_value: 1 } dim { dim_value: 6 } } } } + } + input { + name: "past_key" + type { tensor_type { elem_type: 1 + shape { dim { dim_value: 1 } dim { dim_value: 2 } dim { dim_value: 8 } dim { dim_value: 3 } } } } + } + input { + name: "past_value" + type { tensor_type { elem_type: 1 + shape { dim { dim_value: 1 } dim { dim_value: 2 } dim { dim_value: 8 } dim { dim_value: 3 } } } } + } + input { + name: "past_sequence_length" + type { tensor_type { elem_type: 6 + shape { dim { dim_value: 1 } } } } + } + output { + name: "output" + type { tensor_type { elem_type: 1 + shape { dim { dim_value: 1 } dim { dim_value: 1 } dim { dim_value: 6 } } } } + } + output { + name: "present_key" + type { tensor_type { elem_type: 1 + shape { dim { dim_value: 1 } dim { dim_value: 2 } dim { dim_value: 8 } dim { dim_value: 3 } } } } + } + output { + name: "present_value" + type { tensor_type { elem_type: 1 + shape { dim { dim_value: 1 } dim { dim_value: 2 } dim { dim_value: 8 } dim { dim_value: 3 } } } } + } +} +opset_import { version: 11 } +opset_import { + domain: "com.microsoft" + version: 1 +} \ No newline at end of file diff --git a/src/frontends/onnx/tests/models/com.microsoft/multi_head_attention_16_cache_beam_search.prototxt b/src/frontends/onnx/tests/models/com.microsoft/multi_head_attention_16_cache_beam_search.prototxt new file mode 100644 index 000000000000..a5c083d8d33b --- /dev/null +++ b/src/frontends/onnx/tests/models/com.microsoft/multi_head_attention_16_cache_beam_search.prototxt @@ -0,0 +1,83 @@ +ir_version: 10 +producer_name: "OpenVINO ONNX Frontend" +graph { + node { + input: "query" + input: "key" + input: "value" + input: "" + input: "" + input: "" + input: "past_key" + input: "past_value" + input: "past_sequence_length" + input: "cache_indirection" + output: "output" + output: "present_key" + output: "present_value" + name: "MultiHeadAttention_16_cache_beam_search" + op_type: "MultiHeadAttention" + attribute { + name: "num_heads" + i: 2 + type: INT + } + domain: "com.microsoft" + } + name: "multi_head_attention_16_cache_beam_search" + input { + name: "query" + type { tensor_type { elem_type: 1 + shape { dim { dim_value: 2 } dim { dim_value: 1 } dim { dim_value: 6 } } } } + } + input { + name: "key" + type { tensor_type { elem_type: 1 + shape { dim { dim_value: 2 } dim { dim_value: 1 } dim { dim_value: 6 } } } } + } + input { + name: "value" + type { tensor_type { elem_type: 1 + shape { dim { dim_value: 2 } dim { dim_value: 1 } dim { dim_value: 6 } } } } + } + input { + name: "past_key" + type { tensor_type { elem_type: 1 + shape { dim { dim_value: 2 } dim { dim_value: 2 } dim { dim_value: 4 } dim { dim_value: 3 } } } } + } + input { + name: "past_value" + type { tensor_type { elem_type: 1 + shape { dim { dim_value: 2 } dim { dim_value: 2 } dim { dim_value: 4 } dim { dim_value: 3 } } } } + } + input { + name: "past_sequence_length" + type { tensor_type { elem_type: 6 + shape { } } } + } + input { + name: "cache_indirection" + type { tensor_type { elem_type: 6 + shape { dim { dim_value: 1 } dim { dim_value: 2 } dim { dim_value: 4 } } } } + } + output { + name: "output" + type { tensor_type { elem_type: 1 + shape { dim { dim_value: 2 } dim { dim_value: 1 } dim { dim_value: 6 } } } } + } + output { + name: "present_key" + type { tensor_type { elem_type: 1 + shape { dim { dim_value: 2 } dim { dim_value: 2 } dim { dim_value: 4 } dim { dim_value: 3 } } } } + } + output { + name: "present_value" + type { tensor_type { elem_type: 1 + shape { dim { dim_value: 2 } dim { dim_value: 2 } dim { dim_value: 4 } dim { dim_value: 3 } } } } + } +} +opset_import { version: 11 } +opset_import { + domain: "com.microsoft" + version: 1 +} \ No newline at end of file diff --git a/src/frontends/onnx/tests/models/com.microsoft/multi_head_attention_17_output_qk.prototxt b/src/frontends/onnx/tests/models/com.microsoft/multi_head_attention_17_output_qk.prototxt new file mode 100644 index 000000000000..50578ef7f44e --- /dev/null +++ b/src/frontends/onnx/tests/models/com.microsoft/multi_head_attention_17_output_qk.prototxt @@ -0,0 +1,57 @@ +ir_version: 10 +producer_name: "OpenVINO ONNX Frontend" +graph { + node { + input: "query" + input: "key" + input: "value" + input: "" + input: "" + input: "" + input: "" + input: "" + output: "output" + output: "" + output: "" + output: "qk" + name: "MultiHeadAttention_17_output_qk" + op_type: "MultiHeadAttention" + attribute { + name: "num_heads" + i: 2 + type: INT + } + domain: "com.microsoft" + } + name: "multi_head_attention_17_output_qk" + input { + name: "query" + type { tensor_type { elem_type: 1 + shape { dim { dim_value: 1 } dim { dim_value: 3 } dim { dim_value: 6 } } } } + } + input { + name: "key" + type { tensor_type { elem_type: 1 + shape { dim { dim_value: 1 } dim { dim_value: 4 } dim { dim_value: 6 } } } } + } + input { + name: "value" + type { tensor_type { elem_type: 1 + shape { dim { dim_value: 1 } dim { dim_value: 4 } dim { dim_value: 6 } } } } + } + output { + name: "output" + type { tensor_type { elem_type: 1 + shape { dim { dim_value: 1 } dim { dim_value: 3 } dim { dim_value: 6 } } } } + } + output { + name: "qk" + type { tensor_type { elem_type: 1 + shape { dim { dim_value: 1 } dim { dim_value: 2 } dim { dim_value: 3 } dim { dim_value: 4 } } } } + } +} +opset_import { version: 11 } +opset_import { + domain: "com.microsoft" + version: 1 +} \ No newline at end of file diff --git a/src/frontends/onnx/tests/models/com.microsoft/multi_head_attention_18_cache_unidirectional.prototxt b/src/frontends/onnx/tests/models/com.microsoft/multi_head_attention_18_cache_unidirectional.prototxt new file mode 100644 index 000000000000..00750b555628 --- /dev/null +++ b/src/frontends/onnx/tests/models/com.microsoft/multi_head_attention_18_cache_unidirectional.prototxt @@ -0,0 +1,76 @@ +ir_version: 10 +producer_name: "OpenVINO ONNX Frontend" +graph { + node { + input: "query" + input: "key" + input: "value" + input: "" + input: "" + input: "" + input: "past_key" + input: "past_value" + output: "output" + output: "present_key" + output: "present_value" + name: "MultiHeadAttention_18_cache_unidirectional" + op_type: "MultiHeadAttention" + attribute { + name: "num_heads" + i: 2 + type: INT + } + attribute { + name: "unidirectional" + i: 1 + type: INT + } + domain: "com.microsoft" + } + name: "multi_head_attention_18_cache_unidirectional" + input { + name: "query" + type { tensor_type { elem_type: 1 + shape { dim { dim_value: 1 } dim { dim_value: 2 } dim { dim_value: 6 } } } } + } + input { + name: "key" + type { tensor_type { elem_type: 1 + shape { dim { dim_value: 1 } dim { dim_value: 2 } dim { dim_value: 6 } } } } + } + input { + name: "value" + type { tensor_type { elem_type: 1 + shape { dim { dim_value: 1 } dim { dim_value: 2 } dim { dim_value: 6 } } } } + } + input { + name: "past_key" + type { tensor_type { elem_type: 1 + shape { dim { dim_value: 1 } dim { dim_value: 2 } dim { dim_value: 3 } dim { dim_value: 3 } } } } + } + input { + name: "past_value" + type { tensor_type { elem_type: 1 + shape { dim { dim_value: 1 } dim { dim_value: 2 } dim { dim_value: 3 } dim { dim_value: 3 } } } } + } + output { + name: "output" + type { tensor_type { elem_type: 1 + shape { dim { dim_value: 1 } dim { dim_value: 2 } dim { dim_value: 6 } } } } + } + output { + name: "present_key" + type { tensor_type { elem_type: 1 + shape { dim { dim_value: 1 } dim { dim_value: 2 } dim { dim_value: 5 } dim { dim_value: 3 } } } } + } + output { + name: "present_value" + type { tensor_type { elem_type: 1 + shape { dim { dim_value: 1 } dim { dim_value: 2 } dim { dim_value: 5 } dim { dim_value: 3 } } } } + } +} +opset_import { version: 11 } +opset_import { + domain: "com.microsoft" + version: 1 +} \ No newline at end of file diff --git a/src/frontends/onnx/tests/models/com.microsoft/multi_head_attention_19_cache_bias.prototxt b/src/frontends/onnx/tests/models/com.microsoft/multi_head_attention_19_cache_bias.prototxt new file mode 100644 index 000000000000..f664d90c9042 --- /dev/null +++ b/src/frontends/onnx/tests/models/com.microsoft/multi_head_attention_19_cache_bias.prototxt @@ -0,0 +1,76 @@ +ir_version: 10 +producer_name: "OpenVINO ONNX Frontend" +graph { + node { + input: "query" + input: "key" + input: "value" + input: "bias" + input: "" + input: "" + input: "past_key" + input: "past_value" + output: "output" + output: "present_key" + output: "present_value" + name: "MultiHeadAttention_19_cache_bias" + op_type: "MultiHeadAttention" + attribute { + name: "num_heads" + i: 2 + type: INT + } + domain: "com.microsoft" + } + name: "multi_head_attention_19_cache_bias" + input { + name: "query" + type { tensor_type { elem_type: 1 + shape { dim { dim_value: 1 } dim { dim_value: 2 } dim { dim_value: 6 } } } } + } + input { + name: "key" + type { tensor_type { elem_type: 1 + shape { dim { dim_value: 1 } dim { dim_value: 2 } dim { dim_value: 6 } } } } + } + input { + name: "value" + type { tensor_type { elem_type: 1 + shape { dim { dim_value: 1 } dim { dim_value: 2 } dim { dim_value: 6 } } } } + } + input { + name: "bias" + type { tensor_type { elem_type: 1 + shape { dim { dim_value: 18 } } } } + } + input { + name: "past_key" + type { tensor_type { elem_type: 1 + shape { dim { dim_value: 1 } dim { dim_value: 2 } dim { dim_value: 3 } dim { dim_value: 3 } } } } + } + input { + name: "past_value" + type { tensor_type { elem_type: 1 + shape { dim { dim_value: 1 } dim { dim_value: 2 } dim { dim_value: 3 } dim { dim_value: 3 } } } } + } + output { + name: "output" + type { tensor_type { elem_type: 1 + shape { dim { dim_value: 1 } dim { dim_value: 2 } dim { dim_value: 6 } } } } + } + output { + name: "present_key" + type { tensor_type { elem_type: 1 + shape { dim { dim_value: 1 } dim { dim_value: 2 } dim { dim_value: 5 } dim { dim_value: 3 } } } } + } + output { + name: "present_value" + type { tensor_type { elem_type: 1 + shape { dim { dim_value: 1 } dim { dim_value: 2 } dim { dim_value: 5 } dim { dim_value: 3 } } } } + } +} +opset_import { version: 11 } +opset_import { + domain: "com.microsoft" + version: 1 +} \ No newline at end of file diff --git a/src/frontends/onnx/tests/models/controlflow/if_with_initializer_as_output.prototxt b/src/frontends/onnx/tests/models/controlflow/if_with_initializer_as_output.prototxt new file mode 100644 index 000000000000..b931a8c08e1e --- /dev/null +++ b/src/frontends/onnx/tests/models/controlflow/if_with_initializer_as_output.prototxt @@ -0,0 +1,104 @@ +ir_version: 7 +producer_name: "OpenVINO ONNX Frontend" +graph { + name: "if_with_initializer_output" + node { + input: "condition" + output: "result" + name: "if_node" + op_type: "If" + attribute { + name: "then_branch" + type: GRAPH + g { + name: "then_branch" + node { + input: "x" + output: "then_out" + name: "then_identity" + op_type: "Identity" + } + output { + name: "then_out" + type { + tensor_type { + elem_type: 1 + shape { + dim { + dim_value: 3 + } + } + } + } + } + } + } + attribute { + name: "else_branch" + type: GRAPH + g { + name: "else_branch" + initializer { + dims: 3 + data_type: 1 + float_data: 0.0 + float_data: 0.0 + float_data: 0.0 + name: "else_const" + } + output { + name: "else_const" + type { + tensor_type { + elem_type: 1 + shape { + dim { + dim_value: 3 + } + } + } + } + } + } + } + } + input { + name: "condition" + type { + tensor_type { + elem_type: 9 + shape { + } + } + } + } + input { + name: "x" + type { + tensor_type { + elem_type: 1 + shape { + dim { + dim_value: 3 + } + } + } + } + } + output { + name: "result" + type { + tensor_type { + elem_type: 1 + shape { + dim { + dim_value: 3 + } + } + } + } + } +} +opset_import { + version: 14 +} diff --git a/src/frontends/onnx/tests/models/controlflow/loop_too_few_body_inputs.prototxt b/src/frontends/onnx/tests/models/controlflow/loop_too_few_body_inputs.prototxt new file mode 100644 index 000000000000..7be0c77bf954 --- /dev/null +++ b/src/frontends/onnx/tests/models/controlflow/loop_too_few_body_inputs.prototxt @@ -0,0 +1,138 @@ +ir_version: 6 +producer_name: "OpenVINO ONNX Frontend" +doc_string: "Malformed Loop: 3 loop-carried deps but body only has 2 params (iter + cond, missing state params)" +graph { + name: "loop_body_inputs_oob" + node { + input: "trip_count" + input: "" + input: "dep1" + input: "dep2" + input: "dep3" + output: "res1" + output: "res2" + output: "res3" + op_type: "Loop" + attribute { + name: "body" + g { + node { + input: "cond_in" + output: "cond_out" + name: "cond_identity" + op_type: "Identity" + } + name: "loop body with too few inputs" + input { + name: "i" + type { + tensor_type { + elem_type: 7 + shape { + } + } + } + } + input { + name: "cond_in" + type { + tensor_type { + elem_type: 9 + shape { + } + } + } + } + output { + name: "cond_out" + type { + tensor_type { + elem_type: 9 + shape { + } + } + } + } + } + type: GRAPH + } + } + initializer { + dims: 1 + data_type: 7 + int64_data: 3 + name: "trip_count" + } + input { + name: "dep1" + type { + tensor_type { + elem_type: 1 + shape { + dim { + dim_value: 1 + } + } + } + } + } + input { + name: "dep2" + type { + tensor_type { + elem_type: 1 + shape { + dim { + dim_value: 1 + } + } + } + } + } + input { + name: "dep3" + type { + tensor_type { + elem_type: 1 + shape { + dim { + dim_value: 1 + } + } + } + } + } + output { + name: "res1" + type { + tensor_type { + elem_type: 1 + shape { + } + } + } + } + output { + name: "res2" + type { + tensor_type { + elem_type: 1 + shape { + } + } + } + } + output { + name: "res3" + type { + tensor_type { + elem_type: 1 + shape { + } + } + } + } +} +opset_import { + version: 11 +} diff --git a/src/frontends/onnx/tests/models/controlflow/loop_too_few_body_outputs.prototxt b/src/frontends/onnx/tests/models/controlflow/loop_too_few_body_outputs.prototxt new file mode 100644 index 000000000000..01fd168345c2 --- /dev/null +++ b/src/frontends/onnx/tests/models/controlflow/loop_too_few_body_outputs.prototxt @@ -0,0 +1,112 @@ +ir_version: 6 +producer_name: "OpenVINO ONNX Frontend" +doc_string: "Malformed Loop: has loop-carried deps but body has too few outputs (only cond_out, missing state outputs)" +graph { + name: "loop_body_outputs_oob" + node { + input: "trip_count" + input: "" + input: "a_init" + output: "a_final" + output: "a_values" + op_type: "Loop" + attribute { + name: "body" + g { + node { + input: "cond_in" + output: "cond_out" + name: "cond_identity" + op_type: "Identity" + } + name: "loop body with too few outputs" + input { + name: "i" + type { + tensor_type { + elem_type: 7 + shape { + } + } + } + } + input { + name: "cond_in" + type { + tensor_type { + elem_type: 9 + shape { + } + } + } + } + input { + name: "a_in" + type { + tensor_type { + elem_type: 1 + shape { + dim { + dim_value: 1 + } + } + } + } + } + output { + name: "cond_out" + type { + tensor_type { + elem_type: 9 + shape { + } + } + } + } + } + type: GRAPH + } + } + initializer { + dims: 1 + data_type: 7 + int64_data: 3 + name: "trip_count" + } + input { + name: "a_init" + type { + tensor_type { + elem_type: 1 + shape { + dim { + dim_value: 1 + } + } + } + } + } + output { + name: "a_final" + type { + tensor_type { + elem_type: 1 + shape { + } + } + } + } + output { + name: "a_values" + type { + tensor_type { + elem_type: 1 + shape { + } + } + } + } +} +opset_import { + version: 11 +} diff --git a/src/frontends/onnx/tests/models/controlflow/loop_with_initializer_as_output.prototxt b/src/frontends/onnx/tests/models/controlflow/loop_with_initializer_as_output.prototxt new file mode 100644 index 000000000000..22d26cdae35b --- /dev/null +++ b/src/frontends/onnx/tests/models/controlflow/loop_with_initializer_as_output.prototxt @@ -0,0 +1,157 @@ +ir_version: 7 +producer_name: "OpenVINO ONNX Frontend" +graph { + name: "loop_with_initializer_output" + node { + input: "trip_count" + input: "cond_in" + input: "x_init" + output: "x_final" + name: "loop_node" + op_type: "Loop" + attribute { + name: "body" + type: GRAPH + g { + name: "loop_body" + initializer { + dims: 1 + data_type: 1 + float_data: 1.0 + name: "step" + } + node { + input: "x_in" + input: "step" + output: "x_out" + name: "add_step" + op_type: "Add" + } + node { + input: "cond_body_in" + output: "cond_body_out" + name: "cond_passthrough" + op_type: "Identity" + } + input { + name: "iter" + type { + tensor_type { + elem_type: 7 + shape { + } + } + } + } + input { + name: "cond_body_in" + type { + tensor_type { + elem_type: 9 + shape { + } + } + } + } + input { + name: "x_in" + type { + tensor_type { + elem_type: 1 + shape { + dim { + dim_value: 1 + } + } + } + } + } + output { + name: "cond_body_out" + type { + tensor_type { + elem_type: 9 + shape { + } + } + } + } + output { + name: "x_out" + type { + tensor_type { + elem_type: 1 + shape { + dim { + dim_value: 1 + } + } + } + } + } + output { + name: "step" + type { + tensor_type { + elem_type: 1 + shape { + dim { + dim_value: 1 + } + } + } + } + } + } + } + } + input { + name: "trip_count" + type { + tensor_type { + elem_type: 7 + shape { + } + } + } + } + input { + name: "cond_in" + type { + tensor_type { + elem_type: 9 + shape { + } + } + } + } + input { + name: "x_init" + type { + tensor_type { + elem_type: 1 + shape { + dim { + dim_value: 1 + } + } + } + } + } + output { + name: "x_final" + type { + tensor_type { + elem_type: 1 + shape { + dim { + dim_value: 1 + } + } + } + } + } +} +opset_import { + version: 14 +} diff --git a/src/frontends/onnx/tests/models/controlflow/scan_with_initializer_as_output.prototxt b/src/frontends/onnx/tests/models/controlflow/scan_with_initializer_as_output.prototxt new file mode 100644 index 000000000000..48d88d2e1282 --- /dev/null +++ b/src/frontends/onnx/tests/models/controlflow/scan_with_initializer_as_output.prototxt @@ -0,0 +1,170 @@ +ir_version: 7 +producer_name: "OpenVINO ONNX Frontend" +graph { + name: "scan_with_initializer_output" + node { + input: "init_state" + input: "seq" + output: "final_state" + output: "scan_out" + name: "scan_node" + op_type: "Scan" + attribute { + name: "num_scan_inputs" + type: INT + i: 1 + } + attribute { + name: "body" + type: GRAPH + g { + name: "scan_body" + initializer { + dims: 1 + data_type: 1 + float_data: 7.0 + name: "leak" + } + node { + input: "state_in" + input: "seq_in" + output: "state_next" + name: "add" + op_type: "Add" + } + node { + input: "state_next" + output: "state_copy" + name: "ident" + op_type: "Identity" + } + input { + name: "state_in" + type { + tensor_type { + elem_type: 1 + shape { + dim { + dim_value: 1 + } + } + } + } + } + input { + name: "seq_in" + type { + tensor_type { + elem_type: 1 + shape { + dim { + dim_value: 1 + } + } + } + } + } + output { + name: "state_next" + type { + tensor_type { + elem_type: 1 + shape { + dim { + dim_value: 1 + } + } + } + } + } + output { + name: "state_copy" + type { + tensor_type { + elem_type: 1 + shape { + dim { + dim_value: 1 + } + } + } + } + } + output { + name: "leak" + type { + tensor_type { + elem_type: 1 + shape { + dim { + dim_value: 1 + } + } + } + } + } + } + } + } + input { + name: "init_state" + type { + tensor_type { + elem_type: 1 + shape { + dim { + dim_value: 1 + } + } + } + } + } + input { + name: "seq" + type { + tensor_type { + elem_type: 1 + shape { + dim { + dim_value: 3 + } + dim { + dim_value: 1 + } + } + } + } + } + output { + name: "final_state" + type { + tensor_type { + elem_type: 1 + shape { + dim { + dim_value: 1 + } + } + } + } + } + output { + name: "scan_out" + type { + tensor_type { + elem_type: 1 + shape { + dim { + dim_value: 3 + } + dim { + dim_value: 1 + } + } + } + } + } +} +opset_import { + version: 11 +} diff --git a/src/frontends/onnx/tests/models/controlflow/sequence_insert_chain_concat.prototxt b/src/frontends/onnx/tests/models/controlflow/sequence_insert_chain_concat.prototxt new file mode 100644 index 000000000000..912f7d7a9e41 --- /dev/null +++ b/src/frontends/onnx/tests/models/controlflow/sequence_insert_chain_concat.prototxt @@ -0,0 +1,108 @@ +ir_version: 7 +producer_name: "OpenVINO ONNX Frontend" +graph { + name: "sequence_insert_chain_concat" + node { + input: "input_a" + output: "seq0" + op_type: "SequenceConstruct" + } + node { + input: "seq0" + input: "input_b" + output: "seq1" + op_type: "SequenceInsert" + } + node { + input: "seq1" + input: "input_c" + output: "seq2" + op_type: "SequenceInsert" + } + node { + input: "seq2" + input: "input_d" + output: "seq3" + op_type: "SequenceInsert" + } + node { + input: "seq3" + output: "output" + op_type: "ConcatFromSequence" + attribute { + name: "axis" + i: 0 + type: INT + } + attribute { + name: "new_axis" + i: 0 + type: INT + } + } + input { + name: "input_a" + type { + tensor_type { + elem_type: 1 + shape { + dim { + dim_value: 2 + } + } + } + } + } + input { + name: "input_b" + type { + tensor_type { + elem_type: 1 + shape { + dim { + dim_value: 2 + } + } + } + } + } + input { + name: "input_c" + type { + tensor_type { + elem_type: 1 + shape { + dim { + dim_value: 2 + } + } + } + } + } + input { + name: "input_d" + type { + tensor_type { + elem_type: 1 + shape { + dim { + dim_value: 2 + } + } + } + } + } + output { + name: "output" + type { + tensor_type { + elem_type: 1 + shape { + } + } + } + } +} +opset_import { + version: 14 +} diff --git a/src/frontends/onnx/tests/models/controlflow/sequence_insert_chain_new_axis.prototxt b/src/frontends/onnx/tests/models/controlflow/sequence_insert_chain_new_axis.prototxt new file mode 100644 index 000000000000..17fb1b66b9a1 --- /dev/null +++ b/src/frontends/onnx/tests/models/controlflow/sequence_insert_chain_new_axis.prototxt @@ -0,0 +1,94 @@ +ir_version: 7 +producer_name: "OpenVINO ONNX Frontend" +graph { + name: "sequence_insert_chain_new_axis" + node { + output: "seq0" + op_type: "SequenceEmpty" + } + node { + input: "seq0" + input: "input_a" + output: "seq1" + op_type: "SequenceInsert" + } + node { + input: "seq1" + input: "input_b" + output: "seq2" + op_type: "SequenceInsert" + } + node { + input: "seq2" + input: "input_c" + output: "seq3" + op_type: "SequenceInsert" + } + node { + input: "seq3" + output: "output" + op_type: "ConcatFromSequence" + attribute { + name: "axis" + i: 0 + type: INT + } + attribute { + name: "new_axis" + i: 1 + type: INT + } + } + input { + name: "input_a" + type { + tensor_type { + elem_type: 1 + shape { + dim { + dim_value: 2 + } + } + } + } + } + input { + name: "input_b" + type { + tensor_type { + elem_type: 1 + shape { + dim { + dim_value: 2 + } + } + } + } + } + input { + name: "input_c" + type { + tensor_type { + elem_type: 1 + shape { + dim { + dim_value: 2 + } + } + } + } + } + output { + name: "output" + type { + tensor_type { + elem_type: 1 + shape { + } + } + } + } +} +opset_import { + version: 14 +} diff --git a/src/frontends/onnx/tests/models/external_data/inner_scope/external_data_file_in_up_dir.prototxt b/src/frontends/onnx/tests/models/external_data/inner_scope/external_data_file_in_up_dir.prototxt index dcc4c33d0c6b..c2d2a958f80e 100644 --- a/src/frontends/onnx/tests/models/external_data/inner_scope/external_data_file_in_up_dir.prototxt +++ b/src/frontends/onnx/tests/models/external_data/inner_scope/external_data_file_in_up_dir.prototxt @@ -18,14 +18,6 @@ graph { key: "location", value: "../tensors_data/tensor.data" } - external_data { - key: "offset", - value: "4096" - } - external_data { - key: "length", - value: "16" - } data_location: 1 } input { diff --git a/src/frontends/onnx/tests/models/reshape_allowzero.prototxt b/src/frontends/onnx/tests/models/reshape_allowzero.prototxt new file mode 100644 index 000000000000..3afc66fc03b4 --- /dev/null +++ b/src/frontends/onnx/tests/models/reshape_allowzero.prototxt @@ -0,0 +1,64 @@ +ir_version: 7 +producer_name: "OpenVINO ONNX Frontend" +graph { + node { + output: "shape_const" + op_type: "Constant" + attribute { + name: "value" + t { + dims: 3 + data_type: 7 + int64_data: 3 + int64_data: 4 + int64_data: 0 + name: "shape_tensor" + } + type: TENSOR + } + } + node { + input: "data" + input: "shape_const" + output: "reshaped" + op_type: "Reshape" + attribute { + name: "allowzero" + i: 1 + type: INT + } + } + name: "test_reshape_allowzero" + input { + name: "data" + type { + tensor_type { + elem_type: 1 + shape { + dim { + dim_value: 0 + } + dim { + dim_value: 3 + } + dim { + dim_value: 4 + } + } + } + } + } + output { + name: "reshaped" + type { + tensor_type { + elem_type: 1 + shape { + } + } + } + } +} +opset_import { + version: 14 +} diff --git a/src/frontends/onnx/tests/models/resize10_down_scales_const_nearest_f64.prototxt b/src/frontends/onnx/tests/models/resize10_down_scales_const_nearest_f64.prototxt new file mode 100644 index 000000000000..5c67e49163a9 --- /dev/null +++ b/src/frontends/onnx/tests/models/resize10_down_scales_const_nearest_f64.prototxt @@ -0,0 +1,81 @@ +ir_version: 7 +producer_name: "OpenVINO ONNX Frontend" +graph { + node { + output: "scales" + op_type: "Constant" + attribute { + name: "value" + t { + dims: 4 + data_type: 1 + float_data: 1.0 + float_data: 1.0 + float_data: 0.6000000238418579 + float_data: 0.6000000238418579 + name: "const_tensor" + } + type: TENSOR + } + } + node { + input: "X" + input: "scales" + output: "Y" + op_type: "Resize" + attribute { + name: "mode" + s: "nearest" + type: STRING + } + } + name: "test-model-f64" + input { + name: "X" + type { + tensor_type { + elem_type: 11 + shape { + dim { + dim_value: 1 + } + dim { + dim_value: 1 + } + dim { + dim_value: 2 + } + dim { + dim_value: 4 + } + } + } + } + } + output { + name: "Y" + type { + tensor_type { + elem_type: 11 + shape { + dim { + dim_value: 1 + } + dim { + dim_value: 1 + } + dim { + dim_value: 1 + } + dim { + dim_value: 2 + } + } + } + } + } +} +opset_import { + domain: "" + version: 10 +} diff --git a/src/frontends/onnx/tests/models/rms_normalization.prototxt b/src/frontends/onnx/tests/models/rms_normalization.prototxt new file mode 100644 index 000000000000..1168f858842d --- /dev/null +++ b/src/frontends/onnx/tests/models/rms_normalization.prototxt @@ -0,0 +1,63 @@ +ir_version: 11 +producer_name: "OpenVINO ONNX Frontend" +graph { + node { + input: "X" + input: "scale" + output: "Y" + op_type: "RMSNormalization" + attribute { + name: "axis" + i: -1 + type: INT + } + attribute { + name: "epsilon" + f: 1e-05 + type: FLOAT + } + } + name: "rms_norm_graph" + initializer { + dims: 4 + data_type: 1 + name: "scale" + raw_data: "\000\000\200?\000\000\200?\000\000\200?\000\000\200?" + } + input { + name: "X" + type { + tensor_type { + elem_type: 1 + shape { + dim { + dim_value: 2 + } + dim { + dim_value: 4 + } + } + } + } + } + output { + name: "Y" + type { + tensor_type { + elem_type: 1 + shape { + dim { + dim_value: 2 + } + dim { + dim_value: 4 + } + } + } + } + } +} +opset_import { + domain: "" + version: 23 +} diff --git a/src/frontends/onnx/tests/models/rotary_embedding.prototxt b/src/frontends/onnx/tests/models/rotary_embedding.prototxt new file mode 100644 index 000000000000..8d553a944442 --- /dev/null +++ b/src/frontends/onnx/tests/models/rotary_embedding.prototxt @@ -0,0 +1,81 @@ +ir_version: 11 +producer_name: "OpenVINO ONNX Frontend" +graph { + node { + input: "X" + input: "cos_cache" + input: "sin_cache" + output: "Y" + op_type: "RotaryEmbedding" + attribute { + name: "interleaved" + i: 0 + type: INT + } + } + name: "rotary_embedding_graph" + initializer { + dims: 1 + dims: 2 + dims: 2 + data_type: 1 + name: "cos_cache" + raw_data: "\000\000\200?\000\000\200?\000\000\200?\000\000\200?" + } + initializer { + dims: 1 + dims: 2 + dims: 2 + data_type: 1 + name: "sin_cache" + raw_data: "\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000" + } + input { + name: "X" + type { + tensor_type { + elem_type: 1 + shape { + dim { + dim_value: 1 + } + dim { + dim_value: 1 + } + dim { + dim_value: 2 + } + dim { + dim_value: 4 + } + } + } + } + } + output { + name: "Y" + type { + tensor_type { + elem_type: 1 + shape { + dim { + dim_value: 1 + } + dim { + dim_value: 1 + } + dim { + dim_value: 2 + } + dim { + dim_value: 4 + } + } + } + } + } +} +opset_import { + domain: "" + version: 23 +} diff --git a/src/frontends/onnx/tests/models/scan15_body_fewer_outputs_than_initial_values.prototxt b/src/frontends/onnx/tests/models/scan15_body_fewer_outputs_than_initial_values.prototxt new file mode 100644 index 000000000000..fd2f6803ed73 --- /dev/null +++ b/src/frontends/onnx/tests/models/scan15_body_fewer_outputs_than_initial_values.prototxt @@ -0,0 +1,116 @@ +ir_version: 8 +producer_name: "OpenVINO ONNX Frontend" +graph { + node { + input: "initial1" + input: "initial2" + input: "sequence" + output: "scan_end" + op_type: "Scan" + attribute { + name: "body" + g { + node { + input: "state1" + input: "seq_elem" + output: "sum" + op_type: "Add" + } + name: "body" + input { + name: "state1" + type { + tensor_type { + elem_type: 1 + shape { + } + } + } + } + input { + name: "state2" + type { + tensor_type { + elem_type: 1 + shape { + } + } + } + } + input { + name: "seq_elem" + type { + tensor_type { + elem_type: 1 + shape { + } + } + } + } + output { + name: "sum" + type { + tensor_type { + elem_type: 1 + shape { + } + } + } + } + } + type: GRAPH + } + attribute { + name: "num_scan_inputs" + i: 1 + type: INT + } + } + name: "test-model-scan-fewer-body-outputs" + input { + name: "initial1" + type { + tensor_type { + elem_type: 1 + shape { + } + } + } + } + input { + name: "initial2" + type { + tensor_type { + elem_type: 1 + shape { + } + } + } + } + input { + name: "sequence" + type { + tensor_type { + elem_type: 1 + shape { + dim { + dim_value: 5 + } + } + } + } + } + output { + name: "scan_end" + type { + tensor_type { + elem_type: 1 + shape { + } + } + } + } +} +opset_import { + version: 15 +} diff --git a/src/frontends/onnx/tests/models/scan15_negative_num_scan_inputs.prototxt b/src/frontends/onnx/tests/models/scan15_negative_num_scan_inputs.prototxt new file mode 100644 index 000000000000..6d940e8856c6 --- /dev/null +++ b/src/frontends/onnx/tests/models/scan15_negative_num_scan_inputs.prototxt @@ -0,0 +1,109 @@ +ir_version: 8 +producer_name: "OpenVINO ONNX Frontend" +graph { + node { + input: "initial" + input: "sequence" + output: "scan_end" + output: "scan_seq" + op_type: "Scan" + attribute { + name: "body" + g { + node { + input: "state" + input: "seq_elem" + output: "sum" + op_type: "Add" + } + name: "body" + input { + name: "state" + type { + tensor_type { + elem_type: 1 + shape { + } + } + } + } + input { + name: "seq_elem" + type { + tensor_type { + elem_type: 1 + shape { + } + } + } + } + output { + name: "sum" + type { + tensor_type { + elem_type: 1 + shape { + } + } + } + } + } + type: GRAPH + } + attribute { + name: "num_scan_inputs" + i: -5 + type: INT + } + } + name: "test-model-scan-neg" + input { + name: "initial" + type { + tensor_type { + elem_type: 1 + shape { + } + } + } + } + input { + name: "sequence" + type { + tensor_type { + elem_type: 1 + shape { + dim { + dim_value: 5 + } + } + } + } + } + output { + name: "scan_end" + type { + tensor_type { + elem_type: 1 + shape { + } + } + } + } + output { + name: "scan_seq" + type { + tensor_type { + elem_type: 1 + shape { + dim { + dim_value: 5 + } + } + } + } + } +} +opset_import { + version: 15 +} diff --git a/src/frontends/onnx/tests/models/scan15_num_scan_inputs_exceeds_body_inputs.prototxt b/src/frontends/onnx/tests/models/scan15_num_scan_inputs_exceeds_body_inputs.prototxt new file mode 100644 index 000000000000..5557f012dd96 --- /dev/null +++ b/src/frontends/onnx/tests/models/scan15_num_scan_inputs_exceeds_body_inputs.prototxt @@ -0,0 +1,109 @@ +ir_version: 8 +producer_name: "OpenVINO ONNX Frontend" +graph { + node { + input: "initial" + input: "sequence" + output: "scan_end" + output: "scan_seq" + op_type: "Scan" + attribute { + name: "body" + g { + node { + input: "state" + input: "seq_elem" + output: "sum" + op_type: "Add" + } + name: "body" + input { + name: "state" + type { + tensor_type { + elem_type: 1 + shape { + } + } + } + } + input { + name: "seq_elem" + type { + tensor_type { + elem_type: 1 + shape { + } + } + } + } + output { + name: "sum" + type { + tensor_type { + elem_type: 1 + shape { + } + } + } + } + } + type: GRAPH + } + attribute { + name: "num_scan_inputs" + i: 10 + type: INT + } + } + name: "test-model-scan-invalid" + input { + name: "initial" + type { + tensor_type { + elem_type: 1 + shape { + } + } + } + } + input { + name: "sequence" + type { + tensor_type { + elem_type: 1 + shape { + dim { + dim_value: 5 + } + } + } + } + } + output { + name: "scan_end" + type { + tensor_type { + elem_type: 1 + shape { + } + } + } + } + output { + name: "scan_seq" + type { + tensor_type { + elem_type: 1 + shape { + dim { + dim_value: 5 + } + } + } + } + } +} +opset_import { + version: 15 +} diff --git a/src/frontends/onnx/tests/models/sequence_erase_last.prototxt b/src/frontends/onnx/tests/models/sequence_erase_last.prototxt new file mode 100644 index 000000000000..4d419195cfa5 --- /dev/null +++ b/src/frontends/onnx/tests/models/sequence_erase_last.prototxt @@ -0,0 +1,82 @@ +ir_version: 10 +graph { + node { + input: "a" + input: "b" + input: "c" + output: "seq" + op_type: "SequenceConstruct" + } + node { + input: "seq" + output: "seq2" + op_type: "SequenceErase" + } + node { + input: "seq2" + input: "pos_at" + output: "output" + op_type: "SequenceAt" + } + name: "sequence_erase_last" + initializer { + data_type: 7 + int64_data: 1 + name: "pos_at" + } + input { + name: "a" + type { + tensor_type { + elem_type: 1 + shape { + dim { + dim_value: 2 + } + } + } + } + } + input { + name: "b" + type { + tensor_type { + elem_type: 1 + shape { + dim { + dim_value: 2 + } + } + } + } + } + input { + name: "c" + type { + tensor_type { + elem_type: 1 + shape { + dim { + dim_value: 2 + } + } + } + } + } + output { + name: "output" + type { + tensor_type { + elem_type: 1 + shape { + dim { + dim_value: 2 + } + } + } + } + } +} +opset_import { + version: 11 +} diff --git a/src/frontends/onnx/tests/models/sequence_erase_middle.prototxt b/src/frontends/onnx/tests/models/sequence_erase_middle.prototxt new file mode 100644 index 000000000000..21838c8d2ec3 --- /dev/null +++ b/src/frontends/onnx/tests/models/sequence_erase_middle.prototxt @@ -0,0 +1,88 @@ +ir_version: 10 +graph { + node { + input: "a" + input: "b" + input: "c" + output: "seq" + op_type: "SequenceConstruct" + } + node { + input: "seq" + input: "pos_erase" + output: "seq2" + op_type: "SequenceErase" + } + node { + input: "seq2" + input: "pos_at" + output: "output" + op_type: "SequenceAt" + } + name: "sequence_erase_middle" + initializer { + data_type: 7 + int64_data: 1 + name: "pos_erase" + } + initializer { + data_type: 7 + int64_data: 0 + name: "pos_at" + } + input { + name: "a" + type { + tensor_type { + elem_type: 1 + shape { + dim { + dim_value: 2 + } + } + } + } + } + input { + name: "b" + type { + tensor_type { + elem_type: 1 + shape { + dim { + dim_value: 2 + } + } + } + } + } + input { + name: "c" + type { + tensor_type { + elem_type: 1 + shape { + dim { + dim_value: 2 + } + } + } + } + } + output { + name: "output" + type { + tensor_type { + elem_type: 1 + shape { + dim { + dim_value: 2 + } + } + } + } + } +} +opset_import { + version: 11 +} diff --git a/src/frontends/onnx/tests/models/sequence_length_3_elements.prototxt b/src/frontends/onnx/tests/models/sequence_length_3_elements.prototxt new file mode 100644 index 000000000000..ce5b8f89d662 --- /dev/null +++ b/src/frontends/onnx/tests/models/sequence_length_3_elements.prototxt @@ -0,0 +1,50 @@ +ir_version: 10 +graph { + node { + input: "a" + input: "b" + input: "c" + output: "seq" + op_type: "SequenceConstruct" + } + node { + input: "seq" + output: "length" + op_type: "SequenceLength" + } + name: "sequence_length_3_elements" + initializer { + dims: 2 + data_type: 1 + float_data: 1.0 + float_data: 2.0 + name: "a" + } + initializer { + dims: 2 + data_type: 1 + float_data: 3.0 + float_data: 4.0 + name: "b" + } + initializer { + dims: 2 + data_type: 1 + float_data: 5.0 + float_data: 6.0 + name: "c" + } + output { + name: "length" + type { + tensor_type { + elem_type: 7 + shape { + } + } + } + } +} +opset_import { + version: 11 +} diff --git a/src/frontends/onnx/tests/onnx_import.in.cpp b/src/frontends/onnx/tests/onnx_import.in.cpp index f065f30c6f1e..526e89871e90 100644 --- a/src/frontends/onnx/tests/onnx_import.in.cpp +++ b/src/frontends/onnx/tests/onnx_import.in.cpp @@ -320,6 +320,131 @@ OPENVINO_TEST(${BACKEND_NAME}, onnx_expand_model_with_initializers) { test_case.run(); } +OPENVINO_TEST(${BACKEND_NAME}, onnx_col2im_default) { + const auto model = convert_model("col2im_2D_default_opset_18.onnx"); + auto test_case = ov::test::TestCase(model, s_device); + + // Input: [1, 4, 4] + std::vector input_data = + {1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f, 9.0f, 10.0f, 11.0f, 12.0f, 13.0f, 14.0f, 15.0f, 16.0f}; + test_case.add_input(ov::Shape{1, 4, 4}, input_data); + + // image_shape: [3, 3] + test_case.add_input(ov::Shape{2}, {3, 3}); + // block_shape: [2, 2] + test_case.add_input(ov::Shape{2}, {2, 2}); + + // Expected Output: [1, 1, 3, 3] + std::vector expected_output = {1.0f, 7.0f, 6.0f, 12.0f, 34.0f, 22.0f, 11.0f, 27.0f, 16.0f}; + test_case.add_expected_output(ov::Shape{1, 1, 3, 3}, expected_output); + test_case.run(); +} + +OPENVINO_TEST(${BACKEND_NAME}, onnx_col2im_dilations) { + const auto model = convert_model("col2im_2D_dilations_opset_18.onnx"); + auto test_case = ov::test::TestCase(model, s_device); + + // Input: [1, 4, 9] + std::vector input_data(36, 1.0f); + test_case.add_input(ov::Shape{1, 4, 9}, input_data); + + // image_shape: [5, 5] + test_case.add_input(ov::Shape{2}, {5, 5}); + // block_shape: [2, 2] + test_case.add_input(ov::Shape{2}, {2, 2}); + + // Expected Output: [1, 1, 5, 5] + std::vector expected_output = {1.0f, 1.0f, 2.0f, 1.0f, 1.0f, 1.0f, 1.0f, 2.0f, 1.0f, 1.0f, 2.0f, 2.0f, 4.0f, + 2.0f, 2.0f, 1.0f, 1.0f, 2.0f, 1.0f, 1.0f, 1.0f, 1.0f, 2.0f, 1.0f, 1.0f}; + test_case.add_expected_output(ov::Shape{1, 1, 5, 5}, expected_output); + test_case.run(); +} + +OPENVINO_TEST(${BACKEND_NAME}, onnx_col2im_pads) { + const auto model = convert_model("col2im_2D_pads_opset_18.onnx"); + auto test_case = ov::test::TestCase(model, s_device); + + // Input: [1, 4, 9] + std::vector input_data(36, 1.0f); + test_case.add_input(ov::Shape{1, 4, 9}, input_data); + + // image_shape: [2, 2] + test_case.add_input(ov::Shape{2}, {2, 2}); + // block_shape: [2, 2] + test_case.add_input(ov::Shape{2}, {2, 2}); + + // Expected Output: [1, 1, 2, 2] + std::vector expected_output = {4.0f, 4.0f, 4.0f, 4.0f}; + test_case.add_expected_output(ov::Shape{1, 1, 2, 2}, expected_output); + test_case.run(); +} + +OPENVINO_TEST(${BACKEND_NAME}, onnx_col2im_strides) { + const auto model = convert_model("col2im_2D_strides_opset_18.onnx"); + auto test_case = ov::test::TestCase(model, s_device); + + // Input: [1, 4, 4] + std::vector input_data(16, 1.0f); + test_case.add_input(ov::Shape{1, 4, 4}, input_data); + + // image_shape: [4, 4] + test_case.add_input(ov::Shape{2}, {4, 4}); + // block_shape: [2, 2] + test_case.add_input(ov::Shape{2}, {2, 2}); + + // Expected Output: [1, 1, 4, 4] + std::vector expected_output(16, 1.0f); + test_case.add_expected_output(ov::Shape{1, 1, 4, 4}, expected_output); + test_case.run(); +} + +OPENVINO_TEST(${BACKEND_NAME}, onnx_col2im_batch) { + const auto model = convert_model("col2im_2D_batch_opset_18.onnx"); + auto test_case = ov::test::TestCase(model, s_device); + + // Input: [2, 4, 4] + std::vector input_data(32, 1.0f); + test_case.add_input(ov::Shape{2, 4, 4}, input_data); + + // image_shape: [3, 3] + test_case.add_input(ov::Shape{2}, {3, 3}); + // block_shape: [2, 2] + test_case.add_input(ov::Shape{2}, {2, 2}); + + // Expected Output: [2, 1, 3, 3] + std::vector single_batch_output = {1.0f, 2.0f, 1.0f, 2.0f, 4.0f, 2.0f, 1.0f, 2.0f, 1.0f}; + std::vector expected_output; + expected_output.insert(expected_output.end(), single_batch_output.begin(), single_batch_output.end()); + expected_output.insert(expected_output.end(), single_batch_output.begin(), single_batch_output.end()); + + test_case.add_expected_output(ov::Shape{2, 1, 3, 3}, expected_output); + test_case.run(); +} + +OPENVINO_TEST(${BACKEND_NAME}, onnx_col2im_channel) { + const auto model = convert_model("col2im_2D_channel_opset_18.onnx"); + auto test_case = ov::test::TestCase(model, s_device); + + // Input: [1, 12, 4] + std::vector input_data(48, 1.0f); + test_case.add_input(ov::Shape{1, 12, 4}, input_data); + + // image_shape: [3, 3] + test_case.add_input(ov::Shape{2}, {3, 3}); + // block_shape: [2, 2] + test_case.add_input(ov::Shape{2}, {2, 2}); + + // Expected Output: [1, 3, 3, 3] + std::vector single_channel_output = {1.0f, 2.0f, 1.0f, 2.0f, 4.0f, 2.0f, 1.0f, 2.0f, 1.0f}; + std::vector expected_output; + for (int i = 0; i < 3; ++i) { + expected_output.insert(expected_output.end(), single_channel_output.begin(), single_channel_output.end()); + } + + test_case.add_expected_output(ov::Shape{1, 3, 3, 3}, expected_output); + test_case.run(); +} + // ############################################################################ OPERATOR TESTS OPENVINO_TEST(${BACKEND_NAME}, onnx_model_addmul_abc) { auto model = convert_model("addmul_abc.onnx"); @@ -1106,6 +1231,49 @@ OPENVINO_TEST(${BACKEND_NAME}, onnx_model_reduce_log_sum_exp_18) { test_case.run_with_tolerance_as_fp(); } +// Numerical stability tests for ReduceLogSumExp (issue #32839). +// The old naive implementation log(sum(exp(x))) overflows to Inf for inputs >= 89. +// The stable implementation k + log(sum(exp(x - k))) should produce correct finite results. + +OPENVINO_TEST(${BACKEND_NAME}, onnx_model_reduce_log_sum_exp_large_values) { + // Uses opset 1-12 model with keepdims=true, reducing all axes + auto model = convert_model("reduce_log_sum_exp.onnx"); + + // input data shape (1, 1, 4, 4) - all values 100.0, well above overflow threshold (~88.7) + Inputs inputs{ov::test::NDArray( + {{{{100, 100, 100, 100}, {100, 100, 100, 100}, {100, 100, 100, 100}, {100, 100, 100, 100}}}}) + .get_vector()}; + + // Expected: 100 + log(16) = 100 + 2.77258872... = 102.77258872 + // Old naive implementation would produce Inf here + auto expected_output = ov::test::NDArray({{{{102.77259f}}}}).get_vector(); + + auto test_case = ov::test::TestCase(model, s_device); + test_case.add_multiple_inputs(inputs); + test_case.add_expected_output(expected_output); + test_case.run_with_tolerance_as_fp(); +} + +OPENVINO_TEST(${BACKEND_NAME}, onnx_model_reduce_log_sum_exp_18_large_values) { + // Uses opset 18 model with keepdims=false, reducing non-trailing axes + // This also tests the broadcasting fix for keepdims=false + auto model = convert_model("reduce_log_sum_exp_18.onnx"); + + // input data shape (1, 1, 4, 4) - all values 100.0 + Inputs inputs{ov::test::NDArray( + {{{{100, 100, 100, 100}, {100, 100, 100, 100}, {100, 100, 100, 100}, {100, 100, 100, 100}}}}) + .get_vector()}; + + // output data shape (4) - each element reduces 4 values along axis 2 + // Expected: 100 + log(4) = 100 + 1.38629... = 101.38629 + auto expected_output = ov::test::NDArray({101.38629f, 101.38629f, 101.38629f, 101.38629f}).get_vector(); + + auto test_case = ov::test::TestCase(model, s_device); + test_case.add_multiple_inputs(inputs); + test_case.add_expected_output(expected_output); + test_case.run_with_tolerance_as_fp(); +} + OPENVINO_TEST(${BACKEND_NAME}, onnx_model_reduce_l1) { auto model = convert_model("reduce_l1.onnx"); @@ -1664,6 +1832,21 @@ OPENVINO_TEST(${BACKEND_NAME}, onnx_resize10_down_scales_const_nearest) { test_case.run(); } +OPENVINO_TEST(${BACKEND_NAME}, onnx_resize10_down_scales_const_nearest_f64) { + const auto model = convert_model("resize10_down_scales_const_nearest_f64.onnx"); + + // Input data shape (1, 1, 2, 4) + // Input const scales values {1.0, 1.0, 0.6, 0.6} + // mode: nearest + // elem_type: 11 (double / f64) + + Shape expected_output_shape{1, 1, 1, 2}; + auto test_case = ov::test::TestCase(model, s_device); + test_case.add_input({1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0}); + test_case.add_expected_output(expected_output_shape, {1.0, 3.0}); + test_case.run(); +} + OPENVINO_TEST(${BACKEND_NAME}, onnx_resize10_up_scales_const_linear) { const auto model = convert_model("resize10_up_scales_const_linear.onnx"); @@ -3443,8 +3626,17 @@ OPENVINO_TEST(${BACKEND_NAME}, onnx_model_scatterND_opset16_reduction_none) { } OPENVINO_TEST(${BACKEND_NAME}, onnx_model_scatterND_opset16_reduction_add) { - EXPECT_THROW(convert_model("scatter_nd_opset16_reduction_add.onnx"), ov::Exception) - << "Unsupported type of attribute: `reduction`. Only `none` is supported"; + const auto model = convert_model("scatter_nd_opset16_reduction_add.onnx"); + auto test_case = ov::test::TestCase(model, s_device); + + // Duplicate indices (1 appears twice) exercise the `add` reduction: + // updates 10 and 12 both target index 1, so result[1] = 2 + 10 + 12 = 24. + test_case.add_input({1.f, 2.f, 3.f, 4.f, 5.f, 6.f, 7.f, 8.f}); + test_case.add_input({4, 1, 1, 7}); + test_case.add_input({9.f, 10.f, 12.f, 11.f}); + test_case.add_expected_output(Shape{8}, {1.f, 24.f, 3.f, 4.f, 14.f, 6.f, 7.f, 19.f}); + + test_case.run(); } OPENVINO_TEST(${BACKEND_NAME}, onnx_model_gather_float_1D) { @@ -7436,6 +7628,48 @@ OPENVINO_TEST(${BACKEND_NAME}, onnx_split_to_sequence_explicit_split_1d) { test_case.run(); } +/// @brief Testing ONNX SequenceLength operator via SequenceConstruct fast path. +/// All inputs are graph initializers (constants), so SequenceLength is constant-folded +/// at conversion time to the scalar value 3. +OPENVINO_TEST(${BACKEND_NAME}, onnx_sequence_length_constant_sequence) { + const auto model = convert_model("sequence_length_3_elements.onnx"); + auto test_case = test::TestCase(model, s_device); + + test_case.add_expected_output(Shape{}, {3}); + + test_case.run(); +} + +/// @brief Testing ONNX SequenceErase operator: fast path, erase middle element. +/// Graph: SequenceConstruct(a, b, c) -> SequenceErase(pos=1) -> SequenceAt(pos=0) -> a. +/// After erasing element at index 1 (b), the sequence is [a, c]; SequenceAt(0) returns a. +OPENVINO_TEST(${BACKEND_NAME}, onnx_sequence_erase_middle_element) { + const auto model = convert_model("sequence_erase_middle.onnx"); + auto test_case = test::TestCase(model, s_device); + + test_case.add_input(Shape{2}, {10., 20.}); // a + test_case.add_input(Shape{2}, {30., 40.}); // b (erased) + test_case.add_input(Shape{2}, {50., 60.}); // c + test_case.add_expected_output(Shape{2}, {10., 20.}); + + test_case.run(); +} + +/// @brief Testing ONNX SequenceErase operator: fast path, erase last element (no position input). +/// Graph: SequenceConstruct(a, b, c) -> SequenceErase() -> SequenceAt(pos=1) -> b. +/// After erasing the last element (c), the sequence is [a, b]; SequenceAt(1) returns b. +OPENVINO_TEST(${BACKEND_NAME}, onnx_sequence_erase_last_element) { + const auto model = convert_model("sequence_erase_last.onnx"); + auto test_case = test::TestCase(model, s_device); + + test_case.add_input(Shape{2}, {10., 20.}); // a + test_case.add_input(Shape{2}, {30., 40.}); // b + test_case.add_input(Shape{2}, {50., 60.}); // c (erased) + test_case.add_expected_output(Shape{2}, {30., 40.}); + + test_case.run(); +} + /// @brief Testing ONNX BitShift with an Y output of uint32 type /// The input model was taken from a bug report, where parsing the type of the Y input /// failed. @@ -7485,6 +7719,39 @@ OPENVINO_TEST(${BACKEND_NAME}, onnx_layer_normalization_biased) { test_case.run_with_tolerance_as_fp(0.0001f); } +// Minimal smoke test for the standard ONNX opset-23 RMSNormalization translator. +// Verifies that the decomposition built via ov::decomposition::rms_norm produces +// the expected RMS-normalized output. Comprehensive coverage lives in the ONNX +// node conformance suite (test_rms_normalization_*). +OPENVINO_TEST(${BACKEND_NAME}, onnx_rms_normalization) { + const auto model = convert_model("rms_normalization.onnx"); + auto test_case = test::TestCase(model, s_device); + + test_case.add_input(Shape{2, 4}, {1.0f, 2.0f, 3.0f, 4.0f, 1.0f, 1.0f, 1.0f, 1.0f}); + // RMS over last axis: sqrt(mean(x^2) + 1e-5) + // Row 0: sqrt(7.5 + 1e-5) ~ 2.738614 + // Row 1: sqrt(1.0 + 1e-5) ~ 1.0000050 + test_case.add_expected_output( + Shape{2, 4}, + {0.36514688f, 0.73029375f, 1.09544063f, 1.46058750f, 0.99999499f, 0.99999499f, 0.99999499f, 0.99999499f}); + test_case.run_with_tolerance_as_fp(1e-4f); +} + +// Minimal smoke test for the standard ONNX opset-23 RotaryEmbedding translator. +// Uses cos=1, sin=0 so the rotation is identity — the test verifies that the +// reshape/transpose/decomposition pipeline plumbs the input through unchanged. +// Comprehensive correctness coverage lives in the ONNX node conformance suite +// (test_rotary_embedding_*). +OPENVINO_TEST(${BACKEND_NAME}, onnx_rotary_embedding_identity) { + const auto model = convert_model("rotary_embedding.onnx"); + auto test_case = test::TestCase(model, s_device); + + const std::vector input = {0.1f, 0.2f, 0.3f, 0.4f, 0.5f, 0.6f, 0.7f, 0.8f}; + test_case.add_input(Shape{1, 1, 2, 4}, input); + test_case.add_expected_output(Shape{1, 1, 2, 4}, input); + test_case.run_with_tolerance_as_fp(1e-5f); +} + OPENVINO_TEST(${BACKEND_NAME}, onnx_model_attention_mha_4d) { auto model = convert_model("attention_mha_4d.onnx"); auto test_case = test::TestCase(model, s_device); diff --git a/src/frontends/onnx/tests/onnx_import_com_microsoft.in.cpp b/src/frontends/onnx/tests/onnx_import_com_microsoft.in.cpp index 31ee0a0801d8..7917ca360092 100644 --- a/src/frontends/onnx/tests/onnx_import_com_microsoft.in.cpp +++ b/src/frontends/onnx/tests/onnx_import_com_microsoft.in.cpp @@ -1110,6 +1110,985 @@ OPENVINO_TEST(${BACKEND_NAME}, onnx_model_attention_dynamic_shapes) { test_case.run_with_tolerance_as_fp(1e-6f); } +OPENVINO_TEST(${BACKEND_NAME}, onnx_model_multi_head_attention_01_packed_qkv) { + auto model = convert_model("com.microsoft/multi_head_attention_01_packed_qkv.onnx"); + auto test_case = test::TestCase(model, s_device); + // QKV: (1, 4, 2, 3, 3) + test_case.add_input( + {-0.035826f, 1.564644f, -2.619745f, 0.296120f, 0.261055f, 0.005113f, 0.791032f, -0.909387f, 1.402794f, + -0.808494f, -0.501757f, 0.915402f, 0.257550f, -0.074446f, -1.918771f, -1.062304f, 0.473592f, -0.919424f, + 0.821903f, 0.087047f, -0.299007f, -0.234587f, -1.415371f, -0.420645f, -1.401851f, 0.586857f, 2.190456f, + 0.328751f, -0.529760f, 0.513267f, -0.026514f, 0.060230f, 2.463242f, 1.549934f, -0.783253f, -0.322062f, + 0.091761f, -1.987569f, -0.219672f, -0.342715f, -0.802277f, -0.161286f, -0.990536f, -0.566298f, 0.099651f, + 0.097078f, 0.968645f, -0.702053f, -0.192361f, 0.301547f, -0.034712f, 0.813517f, -1.230864f, 0.227460f, + 0.357113f, 1.477894f, -0.518270f, 0.404051f, 1.886186f, 0.174578f, -0.503476f, -1.550663f, 0.068563f, + -0.327662f, -0.392108f, -1.463515f, -1.168678f, 1.142823f, 0.751933f, 1.307143f, -1.607483f, 0.184634f}); + test_case.add_expected_output( + Shape{1, 4, 6}, + {-0.376205f, -1.164383f, 0.494832f, 1.251880f, -1.010314f, -0.134396f, -0.433258f, -0.718601f, + 0.898995f, 0.979683f, -0.832299f, -0.209895f, -1.080272f, 0.093997f, 1.487117f, 0.203439f, + -0.572602f, -0.311992f, -0.340959f, -1.273095f, 0.406927f, -0.541346f, 0.026989f, -0.646756f}); + test_case.run_with_tolerance_as_fp(1e-4f); +} + +OPENVINO_TEST(${BACKEND_NAME}, onnx_model_multi_head_attention_02_query_packed_kv) { + auto model = convert_model("com.microsoft/multi_head_attention_02_query_packed_kv.onnx"); + auto test_case = test::TestCase(model, s_device); + // Q: (1, 4, 6) + test_case.add_input({-0.035826f, 1.564644f, -2.619745f, -0.808494f, -0.501757f, 0.915402f, + 0.821903f, 0.087047f, -0.299007f, 0.328751f, -0.529760f, 0.513267f, + 0.091761f, -1.987569f, -0.219672f, 0.097078f, 0.968645f, -0.702053f, + 0.357113f, 1.477894f, -0.518270f, -0.327662f, -0.392108f, -1.463515f}); + // KV: (1, 4, 2, 2, 3) + test_case.add_input({0.296120f, 0.261055f, 0.005113f, 0.791032f, -0.909387f, 1.402794f, 0.257550f, + -0.074446f, -1.918771f, -1.062304f, 0.473592f, -0.919424f, -0.234587f, -1.415371f, + -0.420645f, -1.401851f, 0.586857f, 2.190456f, -0.026514f, 0.060230f, 2.463242f, + 1.549934f, -0.783253f, -0.322062f, -0.342715f, -0.802277f, -0.161286f, -0.990536f, + -0.566298f, 0.099651f, -0.192361f, 0.301547f, -0.034712f, 0.813517f, -1.230864f, + 0.227460f, 0.404051f, 1.886186f, 0.174578f, -0.503476f, -1.550663f, 0.068563f, + -1.168678f, 1.142823f, 0.751933f, 1.307143f, -1.607483f, 0.184634f}); + test_case.add_expected_output( + Shape{1, 4, 6}, + {-0.376205f, -1.164383f, 0.494832f, 1.251880f, -1.010314f, -0.134396f, -0.433258f, -0.718601f, + 0.898995f, 0.979683f, -0.832299f, -0.209895f, -1.080272f, 0.093997f, 1.487117f, 0.203439f, + -0.572602f, -0.311992f, -0.340959f, -1.273095f, 0.406927f, -0.541346f, 0.026989f, -0.646756f}); + test_case.run_with_tolerance_as_fp(1e-4f); +} + +OPENVINO_TEST(${BACKEND_NAME}, onnx_model_multi_head_attention_03_qkv_regular) { + auto model = convert_model("com.microsoft/multi_head_attention_03_qkv_regular.onnx"); + auto test_case = test::TestCase(model, s_device); + // Q: (1, 4, 6) + test_case.add_input({-0.035826f, 1.564644f, -2.619745f, -0.808494f, -0.501757f, 0.915402f, + 0.821903f, 0.087047f, -0.299007f, 0.328751f, -0.529760f, 0.513267f, + 0.091761f, -1.987569f, -0.219672f, 0.097078f, 0.968645f, -0.702053f, + 0.357113f, 1.477894f, -0.518270f, -0.327662f, -0.392108f, -1.463515f}); + // K: (1, 4, 6) + test_case.add_input({0.296120f, 0.261055f, 0.005113f, 0.257550f, -0.074446f, -1.918771f, + -0.234587f, -1.415371f, -0.420645f, -0.026514f, 0.060230f, 2.463242f, + -0.342715f, -0.802277f, -0.161286f, -0.192361f, 0.301547f, -0.034712f, + 0.404051f, 1.886186f, 0.174578f, -1.168678f, 1.142823f, 0.751933f}); + // V: (1, 4, 6) + test_case.add_input({0.791032f, -0.909387f, 1.402794f, -1.062304f, 0.473592f, -0.919424f, + -1.401851f, 0.586857f, 2.190456f, 1.549934f, -0.783253f, -0.322062f, + -0.990536f, -0.566298f, 0.099651f, 0.813517f, -1.230864f, 0.227460f, + -0.503476f, -1.550663f, 0.068563f, 1.307143f, -1.607483f, 0.184634f}); + test_case.add_expected_output( + Shape{1, 4, 6}, + {-0.376205f, -1.164383f, 0.494832f, 1.251880f, -1.010314f, -0.134396f, -0.433258f, -0.718601f, + 0.898995f, 0.979683f, -0.832299f, -0.209895f, -1.080272f, 0.093997f, 1.487117f, 0.203439f, + -0.572602f, -0.311992f, -0.340959f, -1.273095f, 0.406927f, -0.541346f, 0.026989f, -0.646756f}); + test_case.run_with_tolerance_as_fp(1e-4f); +} + +OPENVINO_TEST(${BACKEND_NAME}, onnx_model_multi_head_attention_04_cross_attn) { + auto model = convert_model("com.microsoft/multi_head_attention_04_cross_attn.onnx"); + auto test_case = test::TestCase(model, s_device); + // Q: (1, 4, 6) + test_case.add_input({-0.035826f, 1.564644f, -2.619745f, -0.808494f, -0.501757f, 0.915402f, + 0.821903f, 0.087047f, -0.299007f, 0.328751f, -0.529760f, 0.513267f, + 0.091761f, -1.987569f, -0.219672f, 0.097078f, 0.968645f, -0.702053f, + 0.357113f, 1.477894f, -0.518270f, -0.327662f, -0.392108f, -1.463515f}); + // K: (1, 2, 4, 3) + test_case.add_input({0.296120f, 0.261055f, 0.005113f, -0.234587f, -1.415371f, -0.420645f, + -0.342715f, -0.802277f, -0.161286f, 0.404051f, 1.886186f, 0.174578f, + 0.257550f, -0.074446f, -1.918771f, -0.026514f, 0.060230f, 2.463242f, + -0.192361f, 0.301547f, -0.034712f, -1.168678f, 1.142823f, 0.751933f}); + // V: (1, 2, 4, 3) + test_case.add_input({0.791032f, -0.909387f, 1.402794f, -1.401851f, 0.586857f, 2.190456f, + -0.990536f, -0.566298f, 0.099651f, -0.503476f, -1.550663f, 0.068563f, + -1.062304f, 0.473592f, -0.919424f, 1.549934f, -0.783253f, -0.322062f, + 0.813517f, -1.230864f, 0.227460f, 1.307143f, -1.607483f, 0.184634f}); + test_case.add_expected_output( + Shape{1, 4, 6}, + {-0.376205f, -1.164383f, 0.494832f, 1.251880f, -1.010314f, -0.134396f, -0.433258f, -0.718601f, + 0.898995f, 0.979683f, -0.832299f, -0.209895f, -1.080272f, 0.093997f, 1.487117f, 0.203439f, + -0.572602f, -0.311992f, -0.340959f, -1.273095f, 0.406927f, -0.541346f, 0.026989f, -0.646756f}); + test_case.run_with_tolerance_as_fp(1e-4f); +} + +OPENVINO_TEST(${BACKEND_NAME}, onnx_model_multi_head_attention_05_qkv_with_bias) { + auto model = convert_model("com.microsoft/multi_head_attention_05_qkv_with_bias.onnx"); + auto test_case = test::TestCase(model, s_device); + // Q: (1, 4, 6) + test_case.add_input({0.500788f, -0.466532f, -0.072120f, 0.797042f, -0.196038f, -1.626126f, + 1.667177f, -1.467366f, -0.519668f, -0.834358f, 0.883516f, -0.423264f, + 0.414926f, -1.375037f, 0.590562f, 0.306573f, -0.956622f, 1.786636f, + -0.292153f, 0.079714f, -0.360660f, 1.460447f, 1.037711f, 0.655908f}); + // K: (1, 4, 6) + test_case.add_input({-0.159858f, -0.520155f, 1.281123f, 1.497393f, 0.327479f, -0.099673f, + -0.285721f, -0.526933f, 0.836597f, 0.974657f, -1.115275f, -1.066058f, + -0.129775f, -1.613878f, -0.328692f, -0.190567f, 1.685015f, 0.405967f, + -0.187556f, 0.874089f, 0.691861f, -1.465640f, 1.912281f, -1.244010f}); + // V: (1, 4, 6) + test_case.add_input({-1.047010f, 0.303499f, -1.294304f, 0.855919f, -1.039776f, -0.735715f, + -0.043205f, -0.137544f, 0.279007f, 1.171143f, -0.879711f, 0.284893f, + 0.827591f, 0.073086f, 1.101309f, 0.321106f, 1.278001f, 0.094257f, + -0.738374f, 0.852652f, 0.877477f, -0.748606f, 0.451661f, 0.959747f}); + // Bias: (18) + test_case.add_input({-1.584188f, + -0.921880f, + -1.485478f, + 0.448456f, + 0.559887f, + -1.520577f, + 1.551526f, + 0.704305f, + 0.177998f, + 0.591930f, + 0.842607f, + 1.271139f, + -0.742549f, + 0.999647f, + -0.208352f, + 0.588757f, + -0.206641f, + 1.173998f}); + test_case.add_expected_output( + Shape{1, 4, 6}, + {-0.230642f, 1.097652f, 0.601209f, 1.207533f, -0.727536f, 1.429972f, -0.032768f, 1.075902f, + 0.773524f, 0.043782f, 0.222059f, 1.984603f, -0.195165f, 1.081651f, 0.599461f, 1.380906f, + -0.736803f, 1.064204f, -0.301282f, 1.119363f, 0.574715f, 1.128514f, -0.502941f, 0.996273f}); + test_case.run_with_tolerance_as_fp(1e-4f); +} + +OPENVINO_TEST(${BACKEND_NAME}, onnx_model_multi_head_attention_06_mask_batch) { + auto model = convert_model("com.microsoft/multi_head_attention_06_mask_batch.onnx"); + auto test_case = test::TestCase(model, s_device); + // Q: (2, 4, 6) + test_case.add_input({2.414269f, 1.493686f, -0.248355f, -0.882597f, -0.934036f, 1.496378f, 0.931530f, + 0.121790f, 0.952951f, 0.496913f, 1.046078f, 0.006713f, 0.278629f, -1.290837f, + 0.878857f, -0.687077f, -0.232260f, 0.194475f, 0.111104f, -1.542101f, -0.512412f, + -0.481877f, 0.705976f, 0.388797f, 0.659943f, -0.620150f, -1.462235f, -0.275975f, + -2.143318f, 0.096746f, -0.229285f, 0.448629f, 0.894851f, -1.235702f, -0.461685f, + 0.368896f, 1.031920f, -1.116880f, 0.300853f, 0.169378f, -0.604415f, -2.676176f, + -0.167914f, 1.926949f, 0.624043f, -0.714218f, -0.416713f, -0.562544f}); + // K: (2, 4, 6) + test_case.add_input({-2.033886f, -0.940820f, -1.691066f, 0.867390f, 2.073566f, -1.431837f, -0.296306f, + -0.831233f, -0.876234f, -0.379968f, -0.967283f, -0.113957f, -0.073196f, -0.301250f, + 0.837002f, -0.357545f, 0.992237f, 0.251948f, -1.349071f, -1.487922f, 0.697364f, + -0.844215f, -0.678731f, -0.444069f, 0.742095f, 0.642506f, 1.343380f, -2.115452f, + -2.063730f, 0.868498f, -0.569475f, 0.806037f, -0.563011f, -0.822407f, -0.228678f, + -0.716711f, 0.811572f, 0.658422f, -1.091227f, -0.519456f, 0.145228f, -1.719544f, + -0.241919f, -0.698864f, -0.268680f, 1.593978f, -0.569250f, 0.405264f}); + // V: (2, 4, 6) + test_case.add_input({0.199364f, -1.095764f, -1.328098f, 0.790207f, 0.223132f, -0.336433f, 0.276191f, + 1.415912f, -1.880674f, -0.503161f, 0.111360f, -1.038897f, -0.183301f, -0.813844f, + 0.288287f, 0.593714f, 0.967929f, -0.627702f, 1.973337f, -0.133892f, 1.728640f, + 1.146520f, -0.398141f, 2.302237f, -1.639317f, 0.924433f, -0.570678f, -0.567836f, + 0.895974f, 1.214792f, -1.006176f, -1.000671f, 0.620023f, 1.871847f, 2.304910f, + -0.181579f, 2.192029f, 1.863850f, -0.473984f, 1.304245f, -0.562526f, 0.316287f, + 0.637470f, 1.157756f, -0.478634f, -0.037194f, -0.792742f, 1.001240f}); + // Mask: (2) + test_case.add_input({2, 3}); + test_case.add_expected_output( + Shape{2, 4, 6}, + {0.269799f, 1.206941f, -1.834700f, -0.461953f, 0.114921f, -1.016516f, 0.260877f, 0.915272f, + -1.770532f, 0.659943f, 0.211875f, -0.407183f, 0.249166f, 0.532398f, -1.686298f, -0.168022f, + 0.140322f, -0.856874f, 0.233433f, 0.018039f, -1.573138f, 0.330898f, 0.183439f, -0.585897f, + 1.095134f, 1.091988f, -0.217685f, -0.327546f, 0.935886f, 1.083994f, -0.850754f, 0.565629f, + -0.238988f, -0.085193f, 0.970903f, 0.952430f, -0.230991f, 0.957618f, -0.358885f, 1.359290f, + 0.007306f, 0.248131f, -0.668151f, 0.485498f, -0.163849f, 0.656470f, 0.836396f, 0.566272f}); + test_case.run_with_tolerance_as_fp(1e-4f); +} + +OPENVINO_TEST(${BACKEND_NAME}, onnx_model_multi_head_attention_07_mask_packed_metadata) { + auto model = convert_model("com.microsoft/multi_head_attention_07_mask_packed_metadata.onnx"); + auto test_case = test::TestCase(model, s_device); + // Q: (1, 4, 6) + test_case.add_input({-0.035826f, 1.564644f, -2.619745f, -0.808494f, -0.501757f, 0.915402f, + 0.821903f, 0.087047f, -0.299007f, 0.328751f, -0.529760f, 0.513267f, + 0.091761f, -1.987569f, -0.219672f, 0.097078f, 0.968645f, -0.702053f, + 0.357113f, 1.477894f, -0.518270f, -0.327662f, -0.392108f, -1.463515f}); + // K: (1, 4, 6) + test_case.add_input({0.296120f, 0.261055f, 0.005113f, 0.257550f, -0.074446f, -1.918771f, + -0.234587f, -1.415371f, -0.420645f, -0.026514f, 0.060230f, 2.463242f, + -0.342715f, -0.802277f, -0.161286f, -0.192361f, 0.301547f, -0.034712f, + 0.404051f, 1.886186f, 0.174578f, -1.168678f, 1.142823f, 0.751933f}); + // V: (1, 4, 6) + test_case.add_input({0.791032f, -0.909387f, 1.402794f, -1.062304f, 0.473592f, -0.919424f, + -1.401851f, 0.586857f, 2.190456f, 1.549934f, -0.783253f, -0.322062f, + -0.990536f, -0.566298f, 0.099651f, 0.813517f, -1.230864f, 0.227460f, + -0.503476f, -1.550663f, 0.068563f, 1.307143f, -1.607483f, 0.184634f}); + // Mask: (5) + test_case.add_input({2, 0, 4, 0, 4}); + test_case.add_expected_output( + Shape{1, 4, 6}, + {0.138752f, -0.464324f, 1.637087f, 1.334554f, -0.679626f, -0.371315f, -0.162276f, -0.258928f, + 1.745213f, 0.94686f, -0.493092f, -0.459972f, -1.128629f, 0.400433f, 2.092317f, -0.664492f, + 0.28219f, -0.828453f, 0.361562f, -0.616352f, 1.557055f, -0.997998f, 0.442652f, -0.904719f}); + test_case.run_with_tolerance_as_fp(1e-4f); +} + +OPENVINO_TEST(${BACKEND_NAME}, onnx_model_multi_head_attention_08_mask_2D) { + auto model = convert_model("com.microsoft/multi_head_attention_08_mask_2D.onnx"); + auto test_case = test::TestCase(model, s_device); + // Q: (2, 4, 6) + test_case.add_input({1.178842f, -0.588332f, 0.411434f, -1.091373f, 1.698974f, -0.942831f, -0.912207f, + 0.298497f, -1.012579f, -0.583918f, 0.952963f, 0.478946f, 0.189541f, -0.312117f, + -0.081829f, -0.539691f, -2.017537f, 0.185154f, -0.149735f, -0.462661f, 1.931883f, + 0.168544f, -0.991800f, 0.485264f, -0.225168f, 1.449681f, -1.086470f, 1.098004f, + -1.012051f, -0.297929f, -0.177786f, 1.791980f, 0.099199f, -0.259967f, -0.235795f, + 1.181306f, 2.159240f, -0.625949f, 0.792943f, -0.300578f, 0.392830f, 0.800928f, + -0.393912f, 2.075703f, 0.787087f, 2.016828f, -1.149390f, 0.419710f}); + // K: (2, 4, 6) + test_case.add_input({-1.745823f, 0.832173f, -0.103363f, -0.312527f, 0.705116f, 0.052063f, -0.871936f, + 0.426491f, -0.813450f, 0.551936f, 1.369293f, 0.153079f, -0.469306f, 0.557184f, + 0.045078f, 1.612454f, -0.500493f, 0.599157f, 0.564862f, 1.462613f, -0.639713f, + -0.037957f, 1.651678f, -0.647739f, -1.673202f, -0.557986f, 1.723515f, 1.312842f, + 0.823060f, -0.167017f, -2.073166f, 0.485943f, -1.185025f, -0.656164f, 0.446398f, + -1.283233f, -0.048684f, -0.503312f, -0.988174f, -0.270661f, 0.070413f, -0.752441f, + -0.829736f, 1.631957f, -0.955723f, 0.525124f, 1.433940f, -0.155200f}); + // V: (2, 4, 6) + test_case.add_input({-0.744891f, -0.332752f, -0.645481f, -0.599071f, -0.372085f, 0.540749f, 1.217237f, + 0.960120f, 1.222193f, -0.039909f, 0.884915f, -1.485115f, 0.690202f, 0.536082f, + 0.457347f, 0.854271f, 0.290782f, 0.401909f, 1.806902f, 2.080657f, 0.582483f, + 0.342819f, 0.536176f, -0.322169f, 0.386478f, -0.098439f, -0.387852f, 0.647060f, + 0.562506f, 0.415310f, -1.020316f, -0.287768f, -0.360681f, -1.445551f, -0.535288f, + -0.310522f, -0.856826f, -0.839114f, -0.901571f, -0.946835f, 0.153810f, -0.281031f, + 0.056529f, -0.598709f, 1.605809f, 0.638936f, 0.615119f, -0.133803f}); + // Mask: (2, 4) + test_case.add_input({0, 1, 1, 0, 1, 0, 0, 1}); + test_case.add_expected_output( + Shape{2, 4, 6}, + {0.897471f, 0.702845f, 0.758140f, 0.014065f, 0.849052f, -1.371211f, 1.041358f, 0.818613f, + 0.966953f, 0.157232f, 0.753926f, -1.069080f, 0.956361f, 0.750227f, 0.843604f, 0.737445f, + 0.368406f, 0.155367f, 0.843860f, 0.659711f, 0.680339f, 0.662595f, 0.418140f, -0.002593f, + 0.067145f, -0.582613f, 1.541665f, 0.644642f, 0.578168f, 0.251846f, 0.094979f, -0.540412f, + 1.373484f, 0.642910f, 0.589380f, 0.134834f, 0.295551f, -0.236303f, 0.161558f, 0.642432f, + 0.592481f, 0.102469f, 0.132021f, -0.484248f, 1.149662f, 0.645347f, 0.573600f, 0.299524f}); + test_case.run_with_tolerance_as_fp(1e-4f); +} + +OPENVINO_TEST(${BACKEND_NAME}, onnx_model_multi_head_attention_09_mask_3D) { + auto model = convert_model("com.microsoft/multi_head_attention_09_mask_3D.onnx"); + auto test_case = test::TestCase(model, s_device); + // Q: (2, 4, 6) + test_case.add_input({0.171911f, 1.149517f, -1.411962f, 0.410496f, 0.409460f, -1.135476f, -0.194869f, + -0.272008f, 1.612752f, 1.204299f, -0.403567f, 0.034698f, -1.260308f, -0.489011f, + -0.239979f, 0.532567f, -0.630106f, -3.002482f, 1.721283f, 0.066135f, -0.008350f, + -0.302829f, 1.189350f, -0.127051f, -0.402091f, -0.609681f, -0.429554f, 0.117646f, + 1.824711f, 0.974382f, -0.522243f, -1.687855f, 1.724833f, 0.767940f, 0.112722f, + -0.211808f, -1.005934f, 0.488847f, 1.578735f, -1.036853f, -0.808379f, 0.650656f, + -0.463425f, 1.085516f, 0.471409f, -1.386118f, 0.281034f, -1.405665f}); + // K: (2, 4, 6) + test_case.add_input({-0.214302f, 1.139173f, 1.632261f, -0.048986f, 1.215915f, 0.138627f, 0.614674f, + -0.653751f, -0.878698f, 0.016974f, -1.530056f, -0.171601f, 0.165732f, -0.055068f, + -0.048665f, 0.089228f, 0.530092f, 1.210951f, 0.550441f, 1.109022f, 1.200449f, + 1.100326f, 0.879118f, -0.051082f, 0.270656f, -0.104863f, 1.593474f, -0.972787f, + 0.500993f, 0.122744f, 0.010271f, 0.791066f, -0.784976f, -0.918032f, -0.522943f, + -0.568718f, 0.442974f, 0.194005f, -0.886002f, -0.282961f, 0.202165f, -1.997038f, + 0.313970f, -0.069803f, 0.793222f, 2.214140f, -1.313767f, 0.849646f}); + // V: (2, 4, 6) + test_case.add_input({-0.319471f, 0.544227f, -0.710860f, 1.333414f, 0.232788f, 0.946570f, 1.471010f, + 0.544308f, 0.281784f, 0.588671f, 0.451751f, -0.662783f, 1.277066f, 0.167604f, + -0.170514f, -0.002224f, -1.843149f, 1.819181f, 0.879733f, 0.265978f, -1.158533f, + -0.499476f, 0.716312f, 0.407628f, -0.241591f, -0.806894f, 0.641374f, 1.580501f, + 0.000453f, 0.433148f, -0.658517f, -1.511967f, 1.421374f, 0.158469f, -1.537625f, + -0.286630f, -0.208616f, -0.420988f, -0.254890f, 0.931781f, -1.423789f, 2.363608f, + -0.061310f, 0.809641f, 0.177027f, -0.629823f, 0.016361f, -0.331419f}); + // Mask: (2, 4, 4) + test_case.add_input( + {1, 1, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 0, 1, 1, 1, 1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0, 1, 0}); + test_case.add_expected_output( + Shape{2, 4, 6}, + {0.967805f, 0.544285f, 0.002807f, 1.039917f, 0.319080f, 0.312337f, 0.118178f, 0.477229f, + -0.529566f, 0.580508f, -0.304798f, 0.475190f, 1.152318f, 0.198490f, -0.480716f, -0.457030f, + 0.497832f, 0.528121f, 1.201172f, 0.346924f, -0.370560f, -0.174293f, -0.361930f, 0.866541f, + -0.143535f, 0.072346f, 0.388814f, 1.102068f, 0.003896f, 0.267655f, -0.394515f, -0.871780f, + 0.437742f, 0.640447f, -1.466675f, 1.365169f, -0.219685f, 0.193967f, 0.507019f, -0.057731f, + -1.111422f, -0.298914f, -0.229401f, -0.664236f, 0.310052f, 1.091398f, -1.073355f, 1.888620f}); + test_case.run_with_tolerance_as_fp(1e-4f); +} + +OPENVINO_TEST(${BACKEND_NAME}, onnx_model_multi_head_attention_10_attention_bias) { + auto model = convert_model("com.microsoft/multi_head_attention_10_attention_bias.onnx"); + auto test_case = test::TestCase(model, s_device); + // Q: (2, 4, 6) + test_case.add_input({0.631754f, 0.669734f, 2.255635f, 0.160511f, -0.097022f, -1.058020f, -0.090150f, + -1.516458f, -0.191944f, 1.118815f, -2.206739f, -1.692282f, 0.441020f, 0.114214f, + 0.147258f, -0.586127f, 0.619637f, -0.292006f, -0.320732f, 0.446724f, -0.823775f, + 0.016548f, -0.428055f, 1.232233f, -0.155777f, -1.587976f, 0.427614f, -2.071625f, + 3.295498f, 0.220447f, 0.108732f, 1.229409f, -0.809486f, -0.359760f, -0.767645f, + 1.447128f, -2.079479f, 0.373836f, -0.149541f, 0.878389f, 0.304425f, -0.060318f, + 0.901337f, -1.003998f, 0.999944f, -1.844644f, -0.240135f, -0.230783f}); + // K: (2, 4, 6) + test_case.add_input({-2.088322f, -0.650248f, -0.292286f, -1.082512f, 0.916626f, 0.716934f, 0.906080f, + 0.028912f, -0.250721f, -0.740634f, 1.105669f, -0.030639f, -0.052854f, 0.388908f, + 0.596515f, -0.616387f, 1.060799f, 0.625264f, 0.617091f, 0.084425f, -0.082083f, + -0.439068f, 1.467175f, 0.845821f, -0.112836f, -1.008509f, -0.249840f, 0.266454f, + 1.701386f, -0.205294f, -1.337344f, 1.006911f, 1.298912f, 0.510521f, 0.246669f, + -0.831456f, -0.085988f, -1.642547f, -0.497313f, -0.097910f, 0.437478f, 1.890606f, + 1.256807f, -0.193415f, 0.835403f, 2.683102f, 0.201742f, 0.364024f}); + // V: (2, 4, 6) + test_case.add_input({1.546191f, 0.584335f, 0.849519f, 1.563010f, -1.045431f, -0.424912f, -1.397499f, + 1.080616f, -0.135972f, 0.942940f, 0.183162f, 0.326155f, -0.508379f, -0.533352f, + -1.639434f, -0.930212f, -0.459669f, 0.920094f, -0.182963f, -0.678488f, 0.555118f, + -0.322072f, -1.361877f, -0.090990f, 0.777217f, 0.184627f, -1.227485f, 1.100581f, + -0.552859f, -0.602199f, 0.434188f, 0.166850f, -0.197795f, 0.995602f, 1.554552f, + 1.540314f, 1.007041f, 1.083889f, 0.628102f, 0.256808f, 0.160061f, 1.222113f, + -0.138347f, -0.360813f, -0.418015f, 0.592153f, 2.731303f, -1.734940f}); + // Attention bias: (2, 4, 4) + test_case.add_input({-0.018581f, 1.473321f, 0.753584f, 1.403393f, -0.213004f, -1.000797f, 0.784008f, + 0.083793f, -0.796462f, 0.327761f, 0.917032f, 1.953662f, -0.428677f, 1.394534f, + 2.070687f, 1.696589f, 1.289813f, 1.208038f, -0.186171f, -0.554063f, 1.812312f, + -1.232174f, 1.511315f, 0.932466f, -0.870709f, 1.124674f, -0.681370f, -0.101360f, + -0.267328f, 0.771216f, -1.237868f, -0.964730f}); + test_case.add_expected_output( + Shape{2, 4, 6}, + {-0.637803f, -0.074269f, -0.436488f, 0.334656f, -0.427601f, 0.257946f, 0.253579f, -0.024499f, + -0.112622f, -0.154810f, -0.588941f, 0.465481f, -0.378858f, -0.389806f, 0.014392f, -0.198539f, + -0.950647f, 0.162309f, -0.499025f, -0.140439f, -0.468329f, -0.340393f, -0.697783f, 0.430090f, + 0.734679f, 0.356473f, -0.626391f, 1.063969f, -0.446041f, -0.453523f, 0.609446f, 0.284722f, + -0.529473f, 0.372587f, 0.299403f, 0.804409f, 0.449080f, 0.182920f, -0.203662f, 0.802314f, + 1.797163f, 0.061460f, 0.418494f, 0.123126f, -0.432803f, 0.931614f, 0.933841f, 0.990207f}); + test_case.run_with_tolerance_as_fp(1e-4f); +} + +OPENVINO_TEST(${BACKEND_NAME}, onnx_model_multi_head_attention_11_custom_scale) { + auto model = convert_model("com.microsoft/multi_head_attention_11_custom_scale.onnx"); + auto test_case = test::TestCase(model, s_device); + // Q: (1, 4, 6) + test_case.add_input({0.591468f, -1.255400f, -0.161877f, -0.427714f, 0.490554f, 0.471514f, + 0.033110f, -0.230670f, -1.325633f, -0.678606f, 1.637428f, -0.039956f, + 1.424739f, 1.477517f, 0.117651f, -0.496312f, 0.811690f, -0.378689f, + 1.108388f, -0.708602f, 0.205968f, -0.612794f, -1.079098f, 0.016927f}); + // K: (1, 4, 6) + test_case.add_input({-0.281602f, 0.834503f, 0.493743f, 1.438564f, -0.714108f, 0.507913f, + 0.874791f, -1.060169f, 0.991535f, 0.666758f, 0.334132f, -0.180667f, + 0.113462f, -1.028317f, 0.204918f, -1.713102f, 0.110518f, -1.096585f, + -0.074069f, -0.042640f, -0.195191f, -0.710781f, -0.596969f, -0.048746f}); + // V: (1, 4, 6) + test_case.add_input({0.356172f, 0.401375f, -0.413736f, -0.747005f, -0.519761f, 0.460136f, + 0.286267f, 0.444109f, -1.543422f, -0.908271f, 0.919309f, 0.078947f, + 0.324866f, 0.137249f, -0.360263f, 1.289033f, -0.039674f, -0.982759f, + -0.928877f, -0.452811f, 0.098747f, 0.861644f, 1.201556f, -1.256939f}); + test_case.add_expected_output( + Shape{1, 4, 6}, + {0.091921f, 0.180198f, -0.758029f, 0.265805f, 0.432730f, -0.530781f, -0.100481f, 0.042511f, + -0.423590f, 0.489783f, 0.395589f, -0.649330f, 0.035272f, 0.176660f, -0.549437f, 0.474474f, + 0.394462f, -0.642841f, 0.104952f, 0.209011f, -0.824280f, 0.443754f, 0.432535f, -0.653196f}); + test_case.run_with_tolerance_as_fp(1e-4f); +} + +OPENVINO_TEST(${BACKEND_NAME}, onnx_model_multi_head_attention_12_unidirectional) { + auto model = convert_model("com.microsoft/multi_head_attention_12_unidirectional.onnx"); + auto test_case = test::TestCase(model, s_device); + // Q: (1, 4, 6) + test_case.add_input({0.763157f, -1.736108f, -1.693602f, -0.130071f, -1.493915f, 0.536139f, + -1.695399f, 0.213582f, 0.779525f, -0.422040f, 0.442692f, 0.904750f, + -1.345052f, 1.181543f, -0.383095f, -1.129182f, -1.358498f, 0.181608f, + 1.467260f, -0.488720f, 0.067600f, 0.813261f, -0.410832f, -0.859603f}); + // K: (1, 4, 6) + test_case.add_input({0.320349f, 0.455102f, -0.201270f, -1.073210f, 0.131007f, 0.279952f, + 1.310442f, 0.439139f, 0.059728f, -0.544267f, 0.417130f, 1.576669f, + -0.988473f, -0.611304f, 0.043102f, -0.005837f, -0.772120f, 0.306943f, + -0.885798f, -0.185542f, 0.456907f, 0.100875f, 0.757357f, 0.318546f}); + // V: (1, 4, 6) + test_case.add_input({1.294832f, -0.045494f, 0.123401f, 1.788317f, 1.602329f, 0.723241f, + 0.776543f, -0.425794f, -1.787336f, -1.103704f, 1.669609f, 0.591973f, + 0.421913f, 0.106899f, 0.529449f, 0.275918f, -0.134726f, -2.559006f, + 0.054601f, 1.336137f, 0.305235f, 0.864290f, 0.870780f, 0.777970f}); + test_case.add_expected_output( + Shape{1, 4, 6}, + {1.294832f, -0.045494f, 0.123401f, 1.788317f, 1.602329f, 0.723241f, 1.140035f, -0.159078f, + -0.447276f, -0.093177f, 1.646100f, 0.637840f, 0.802408f, -0.035060f, 0.007217f, 0.507874f, + 0.956543f, -0.559896f, 0.785790f, -0.095690f, -0.872420f, 0.570673f, 0.715425f, -0.572088f}); + test_case.run_with_tolerance_as_fp(1e-4f); +} + +OPENVINO_TEST(${BACKEND_NAME}, onnx_model_multi_head_attention_13_mask_filter_value) { + auto model = convert_model("com.microsoft/multi_head_attention_13_mask_filter_value.onnx"); + auto test_case = test::TestCase(model, s_device); + // Q: (1, 4, 6) + test_case.add_input({-0.004815f, 0.315698f, -0.950926f, 1.736456f, -0.569141f, 3.417437f, + 0.092952f, 0.949537f, -0.312971f, -0.445572f, 0.012943f, 0.145773f, + 0.793050f, -1.220664f, 0.058867f, 0.037140f, 1.069238f, -0.331442f, + 0.743906f, -1.078334f, 0.011862f, 2.430786f, -1.919814f, -1.248909f}); + // K: (1, 4, 6) + test_case.add_input({0.436345f, 0.443550f, 0.178174f, 0.821855f, -0.419325f, 0.530089f, + -0.451802f, 1.333365f, 0.910245f, -0.854425f, 0.077223f, -1.458590f, + 1.535575f, 1.271915f, -0.273165f, -0.751594f, -0.903711f, 1.265961f, + -1.450006f, -0.389976f, 0.259376f, 0.782885f, -1.362092f, -1.661010f}); + // V: (1, 4, 6) + test_case.add_input({-0.651325f, 0.499091f, 0.394836f, -0.836832f, -1.138136f, -0.060758f, + 0.553349f, 0.272738f, 0.074907f, -1.566197f, 2.381995f, 3.666111f, + 0.886348f, -1.190575f, -0.703657f, -0.138615f, -1.165533f, -0.190768f, + -0.754842f, -0.861068f, -1.140825f, 0.191213f, -0.103034f, -0.623577f}); + // Mask: (1) + test_case.add_input({2}); + test_case.add_expected_output( + Shape{1, 4, 6}, + {0.015217f, -0.075032f, -0.105048f, -0.642630f, -1.132953f, -0.090547f, 0.165671f, -0.031624f, + -0.064637f, -0.908601f, 0.394500f, 1.384287f, -0.184221f, -0.020413f, -0.066868f, -1.102100f, + 0.905021f, 1.945917f, -0.163094f, -0.023861f, -0.067551f, -0.046673f, -0.186338f, -0.365650f}); + test_case.run_with_tolerance_as_fp(1e-4f); +} + +OPENVINO_TEST(${BACKEND_NAME}, onnx_model_multi_head_attention_14_cache_standard) { + auto model = convert_model("com.microsoft/multi_head_attention_14_cache_standard.onnx"); + auto test_case = test::TestCase(model, s_device); + // Q: (1, 2, 6) + test_case.add_input({0.545757f, + -0.112433f, + 1.860055f, + -0.902486f, + 0.048880f, + -0.611522f, + -0.133876f, + -0.510272f, + 0.937600f, + 1.344088f, + 0.173445f, + -0.768379f}); + // K: (1, 2, 6) + test_case.add_input({0.912625f, + -0.001525f, + 0.701053f, + 1.307534f, + 0.131957f, + -0.757927f, + 0.012941f, + -1.291529f, + -0.619094f, + -0.053724f, + -0.629695f, + 1.326786f}); + // V: (1, 2, 6) + test_case.add_input({1.175690f, + -0.776358f, + -0.897604f, + 1.889715f, + 0.692087f, + 0.801224f, + 2.171347f, + -0.775571f, + 1.237221f, + 0.503803f, + -0.494952f, + -0.776084f}); + // past_key: (1, 2, 3, 3) + test_case.add_input({0.007796f, + -1.399294f, + -0.810345f, + -0.790828f, + -1.212683f, + 0.933362f, + -0.843760f, + 0.849301f, + -0.305789f, + 1.279238f, + -0.750740f, + 0.855745f, + -1.495647f, + 0.042313f, + -1.011973f, + -1.202155f, + -0.739261f, + 0.526944f}); + // past_value: (1, 2, 3, 3) + test_case.add_input({0.234069f, + 0.635683f, + 1.871725f, + -0.247566f, + 0.535697f, + -0.174310f, + 0.880957f, + -0.307055f, + 0.489298f, + -1.554317f, + 0.724578f, + 0.447344f, + -0.086974f, + 0.765638f, + 1.224396f, + -0.958257f, + -0.750733f, + -0.695065f}); + // Output: (1, 2, 6) + test_case.add_expected_output(Shape{1, 2, 6}, + {0.681416f, + -0.190417f, + -0.169402f, + -0.116328f, + 0.263488f, + 0.470749f, + 0.620325f, + -0.017722f, + 0.272212f, + 0.675188f, + 0.559061f, + 0.566867f}); + // present_key: (1, 2, 5, 3) + test_case.add_expected_output( + Shape{1, 2, 5, 3}, + {0.007796f, -1.399294f, -0.810345f, -0.790828f, -1.212683f, 0.933362f, -0.843760f, 0.849301f, + -0.305789f, 0.912625f, -0.001525f, 0.701053f, 0.012941f, -1.291529f, -0.619094f, 1.279238f, + -0.750740f, 0.855745f, -1.495647f, 0.042313f, -1.011973f, -1.202155f, -0.739261f, 0.526944f, + 1.307534f, 0.131957f, -0.757927f, -0.053724f, -0.629695f, 1.326786f}); + // present_value: (1, 2, 5, 3) + test_case.add_expected_output( + Shape{1, 2, 5, 3}, + {0.234069f, 0.635683f, 1.871725f, -0.247566f, 0.535697f, -0.174310f, 0.880957f, -0.307055f, + 0.489298f, 1.175690f, -0.776358f, -0.897604f, 2.171347f, -0.775571f, 1.237221f, -1.554317f, + 0.724578f, 0.447344f, -0.086974f, 0.765638f, 1.224396f, -0.958257f, -0.750733f, -0.695065f, + 1.889715f, 0.692087f, 0.801224f, 0.503803f, -0.494952f, -0.776084f}); + + test_case.run_with_tolerance_as_fp(1e-4f); +} + +OPENVINO_TEST(${BACKEND_NAME}, onnx_model_multi_head_attention_15_cache_buffer_sharing) { + auto model = convert_model("com.microsoft/multi_head_attention_15_cache_buffer_sharing.onnx"); + auto test_case = test::TestCase(model, s_device); + // Q: (1, 1, 6) + test_case.add_input({-0.502763f, -0.268339f, -0.627002f, -0.026298f, -0.477923f, -0.959982f}); + // K: (1, 1, 6) + test_case.add_input({-1.522568f, -0.516134f, -0.797255f, 0.956753f, 0.386596f, 0.501619f}); + // V: (1, 1, 6) + test_case.add_input({-0.061915f, -0.069701f, 0.272578f, 0.507838f, -0.291678f, -1.688897f}); + // past_key: (1, 2, 8, 3) + test_case.add_input({-1.633144f, 0.574429f, 0.180382f, -2.065948f, 2.251946f, -2.474243f, -0.095624f, + -0.475753f, -0.283201f, -0.906520f, -0.161607f, 0.439711f, 0.060502f, 1.313367f, + -0.219802f, 0.459024f, 0.545309f, 0.562743f, -0.208949f, 0.036794f, 2.255347f, + 0.719079f, -0.648445f, 0.094805f, -0.660082f, -0.284994f, 1.612288f, -0.163533f, + -0.512006f, -0.556645f, -1.343872f, 0.942692f, -1.384635f, 1.417410f, 0.588609f, + -0.012637f, -0.395745f, 1.228617f, 0.738330f, -0.893525f, -0.904549f, 0.072122f, + 1.918728f, -0.093395f, 2.004566f, -0.474692f, 0.144446f, -0.880570f}); + // past_value: (1, 2, 8, 3) + test_case.add_input({1.624742f, 1.589109f, 0.223456f, 0.341966f, -1.576143f, 2.364328f, 0.117859f, + 0.885522f, -0.390474f, -0.817975f, 0.185934f, -0.158277f, -1.156958f, -0.256053f, + 0.640194f, -1.933494f, 1.459255f, 0.924655f, 0.019855f, 0.311210f, 0.732947f, + -0.491835f, 1.382449f, 0.153854f, -0.418824f, -0.789253f, 0.140109f, 0.096971f, + -0.637027f, -0.991046f, -0.278132f, 0.238912f, -0.166286f, 1.644490f, -0.756377f, + 1.568445f, -0.601715f, -0.862884f, -1.157213f, 0.815486f, 2.053719f, 0.026404f, + -0.720157f, -0.195971f, 0.281561f, -1.334606f, 0.029407f, 0.999880f}); + // past_sequence_length: (1) + test_case.add_input({3}); + // Output: (1, 1, 6) + test_case.add_expected_output(Shape{1, 1, 6}, + {0.414841f, -0.230426f, 0.985419f, -0.037767f, -0.261106f, -0.663645f}); + // present_key: (1, 2, 8, 3) + test_case.add_expected_output( + Shape{1, 2, 8, 3}, + {-1.633144f, 0.574429f, 0.180382f, -2.065948f, 2.251946f, -2.474243f, -0.095624f, -0.475753f, + -0.283201f, -1.522568f, -0.516134f, -0.797255f, 0.060502f, 1.313367f, -0.219802f, 0.459024f, + 0.545309f, 0.562743f, -0.208949f, 0.036794f, 2.255347f, 0.719079f, -0.648445f, 0.094805f, + -0.660082f, -0.284994f, 1.612288f, -0.163533f, -0.512006f, -0.556645f, -1.343872f, 0.942692f, + -1.384635f, 0.956753f, 0.386596f, 0.501619f, -0.395745f, 1.228617f, 0.738330f, -0.893525f, + -0.904549f, 0.072122f, 1.918728f, -0.093395f, 2.004566f, -0.474692f, 0.144446f, -0.880570f}); + // present_value: (1, 2, 8, 3) + test_case.add_expected_output( + Shape{1, 2, 8, 3}, + {1.624742f, 1.589109f, 0.223456f, 0.341966f, -1.576143f, 2.364328f, 0.117859f, 0.885522f, + -0.390474f, -0.061915f, -0.069701f, 0.272578f, -1.156958f, -0.256053f, 0.640194f, -1.933494f, + 1.459255f, 0.924655f, 0.019855f, 0.311210f, 0.732947f, -0.491835f, 1.382449f, 0.153854f, + -0.418824f, -0.789253f, 0.140109f, 0.096971f, -0.637027f, -0.991046f, -0.278132f, 0.238912f, + -0.166286f, 0.507838f, -0.291678f, -1.688897f, -0.601715f, -0.862884f, -1.157213f, 0.815486f, + 2.053719f, 0.026404f, -0.720157f, -0.195971f, 0.281561f, -1.334606f, 0.029407f, 0.999880f}); + + test_case.run_with_tolerance_as_fp(1e-4f); +} + +OPENVINO_TEST(${BACKEND_NAME}, onnx_model_multi_head_attention_16_cache_beam_search) { + auto model = convert_model("com.microsoft/multi_head_attention_16_cache_beam_search.onnx"); + auto test_case = test::TestCase(model, s_device); + // Q: (2, 1, 6) + test_case.add_input({0.014633f, + 2.050712f, + 0.219489f, + -0.758997f, + 0.532654f, + 0.233358f, + -1.221251f, + -0.758219f, + -1.441508f, + 0.281829f, + 0.176174f, + 0.052142f}); + // K: (2, 1, 6) + test_case.add_input({-0.708303f, + -0.651082f, + -0.966571f, + 2.566096f, + 1.036422f, + 1.169582f, + -1.467058f, + 0.566868f, + 0.300726f, + 0.163399f, + 1.761693f, + 1.046739f}); + // V: (2, 1, 6) + test_case.add_input({-0.261049f, + -0.510067f, + -0.164336f, + 2.401257f, + 0.038037f, + -0.907690f, + -1.401583f, + 0.475986f, + 1.020456f, + 0.141976f, + -0.242386f, + -0.229653f}); + // past_key: (2, 2, 4, 3) + test_case.add_input({-1.179903f, -0.820847f, 0.147255f, -0.806921f, -0.541269f, -3.832336f, 1.263634f, + -0.013534f, -2.544328f, -0.451034f, -1.318139f, -1.049004f, -0.600362f, -2.259691f, + -0.968099f, -0.473152f, -0.157991f, 0.246555f, 0.184235f, -2.002737f, -0.956401f, + 0.397505f, -1.326733f, 0.002806f, -0.753141f, -0.227997f, -0.595219f, -2.109120f, + -0.323724f, 0.044683f, 0.587469f, -0.054632f, 1.351582f, -1.290819f, 0.657286f, + -1.984299f, -1.012252f, -0.647734f, -0.720376f, -1.461135f, 0.683642f, -0.069769f, + -0.746679f, -0.572421f, -0.793770f, 0.092700f, 0.198125f, -0.802795f}); + // past_value: (2, 2, 4, 3) + test_case.add_input({0.098929f, 0.397491f, 0.480757f, 0.093701f, -0.474389f, 0.398639f, 1.665058f, + 0.975667f, 0.237879f, -1.127842f, 0.765723f, 1.999417f, 1.193337f, 0.827391f, + 1.474067f, -1.358840f, 1.459235f, -0.447496f, -2.640829f, 0.224341f, -0.238580f, + -0.752533f, 0.008406f, 0.354385f, -1.521480f, -1.131067f, 0.140327f, -0.246926f, + -0.799155f, -0.498223f, 0.798299f, -0.514823f, -0.028915f, 0.419435f, 0.038079f, + 0.910346f, -0.993182f, -1.103120f, -0.344755f, 1.704449f, -0.041656f, 0.687651f, + -1.323473f, 1.099172f, -0.646563f, -2.083734f, 0.930031f, 0.866925f}); + // past_sequence_length: (1) + test_case.add_input({3}); + // cache_indirection: (1, 2, 4) + test_case.add_input({0, 1, 1, 0, 1, 1, 0, 0}); + // Output: (2, 1, 6) + test_case.add_expected_output(Shape{2, 1, 6}, + {0.254965f, + -0.453316f, + -0.097362f, + 1.010793f, + 0.346349f, + 0.288128f, + -0.197175f, + -0.247718f, + 0.041533f, + -0.353823f, + -0.274332f, + -0.046942f}); + // present_key: (2, 2, 4, 3) + test_case.add_expected_output( + Shape{2, 2, 4, 3}, + {-1.179903f, -0.820847f, 0.147255f, -0.806921f, -0.541269f, -3.832336f, 1.263634f, -0.013534f, + -2.544328f, -0.708303f, -0.651082f, -0.966571f, -0.600362f, -2.259691f, -0.968099f, -0.473152f, + -0.157991f, 0.246555f, 0.184235f, -2.002737f, -0.956401f, 2.566096f, 1.036422f, 1.169582f, + -0.753141f, -0.227997f, -0.595219f, -2.109120f, -0.323724f, 0.044683f, 0.587469f, -0.054632f, + 1.351582f, -1.467058f, 0.566868f, 0.300726f, -1.012252f, -0.647734f, -0.720376f, -1.461135f, + 0.683642f, -0.069769f, -0.746679f, -0.572421f, -0.793770f, 0.163399f, 1.761693f, 1.046739f}); + // present_value: (2, 2, 4, 3) + test_case.add_expected_output( + Shape{2, 2, 4, 3}, + {0.098929f, 0.397491f, 0.480757f, 0.093701f, -0.474389f, 0.398639f, 1.665058f, 0.975667f, + 0.237879f, -0.261049f, -0.510067f, -0.164336f, 1.193337f, 0.827391f, 1.474067f, -1.358840f, + 1.459235f, -0.447496f, -2.640829f, 0.224341f, -0.238580f, 2.401257f, 0.038037f, -0.907690f, + -1.521480f, -1.131067f, 0.140327f, -0.246926f, -0.799155f, -0.498223f, 0.798299f, -0.514823f, + -0.028915f, -1.401583f, 0.475986f, 1.020456f, -0.993182f, -1.103120f, -0.344755f, 1.704449f, + -0.041656f, 0.687651f, -1.323473f, 1.099172f, -0.646563f, 0.141976f, -0.242386f, -0.229653f}); + + test_case.run_with_tolerance_as_fp(1e-4f); +} + +OPENVINO_TEST(${BACKEND_NAME}, onnx_model_multi_head_attention_17_output_qk) { + auto model = convert_model("com.microsoft/multi_head_attention_17_output_qk.onnx"); + auto test_case = test::TestCase(model, s_device); + // Q: (1, 4, 6) + test_case.add_input({1.051868f, + 0.625783f, + 0.409239f, + -0.709610f, + 1.814253f, + 0.535333f, + 0.456102f, + -0.496785f, + 0.645828f, + -0.992543f, + 0.600572f, + -0.907871f, + -0.360408f, + 0.011880f, + -0.644068f, + 1.302161f, + 1.409975f, + 2.692361f}); + // K: (1, 4, 6) + test_case.add_input({-0.667510f, -1.561886f, -0.916755f, -0.400884f, 0.676683f, 0.501052f, + -0.432432f, -0.965812f, -1.167684f, -1.032523f, 2.662656f, -0.046547f, + 1.023166f, 0.435278f, -0.722526f, 0.134017f, -1.191387f, 0.166177f, + 0.566148f, 0.516441f, -0.385748f, 0.223202f, -0.529323f, 0.440974f}); + // V: (1, 4, 6) + test_case.add_input({0.351677f, -0.281540f, -0.582924f, -0.987096f, -0.557608f, -0.322645f, + 2.578884f, 0.533502f, 0.430136f, 0.620238f, -0.775554f, 0.141197f, + 1.106112f, 0.258746f, -0.448094f, 0.824640f, -1.355070f, 0.688569f, + 0.138647f, 0.742778f, -0.490102f, 0.700676f, 1.003421f, 0.434979f}); + test_case.add_expected_output(Shape{1, 3, 6}, + {0.832044f, + 0.429053f, + -0.385924f, + 0.464469f, + -0.721851f, + 0.106980f, + 0.983984f, + 0.295017f, + -0.300954f, + 0.363394f, + -0.636786f, + 0.128481f, + 1.156259f, + 0.276583f, + -0.221725f, + 0.125018f, + -0.425868f, + 0.077078f}); + test_case.add_expected_output( + Shape{1, 2, 3, 4}, + {-1.186285f, -0.887452f, 0.607915f, 0.439265f, -0.069627f, -0.272253f, -0.124823f, -0.142874f, + 0.469082f, 0.517563f, 0.058757f, 0.029179f, 1.027900f, 3.197656f, -1.251474f, -0.509595f, + 0.201727f, 1.539331f, -0.577003f, -0.542583f, 1.028321f, 1.318925f, -0.610782f, 0.422374f}); + test_case.run_with_tolerance_as_fp(1e-4f); +} + +OPENVINO_TEST(${BACKEND_NAME}, onnx_model_multi_head_attention_18_cache_unidirectional) { + auto model = convert_model("com.microsoft/multi_head_attention_18_cache_unidirectional.onnx"); + auto test_case = test::TestCase(model, s_device); + // Q: (1, 2, 6) + test_case.add_input({1.123173f, + 1.560471f, + -0.982731f, + 0.547975f, + -0.461464f, + -0.488095f, + 0.823373f, + 0.885939f, + -0.486904f, + -0.051340f, + 0.378610f, + 0.122047f}); + // K: (1, 2, 6) + test_case.add_input({-1.509428f, + -0.268259f, + 1.906879f, + 0.167194f, + -0.542977f, + -0.340443f, + -0.197825f, + -1.105133f, + -1.770980f, + 0.047243f, + 0.305181f, + 0.145112f}); + // V: (1, 2, 6) + test_case.add_input({0.253975f, + -0.832144f, + 0.151920f, + -0.008827f, + 1.886953f, + -0.562337f, + 0.977240f, + -1.360663f, + -0.819639f, + 1.336034f, + 0.113086f, + -0.018064f}); + // past_key: (1, 2, 3, 3) + test_case.add_input({0.579000f, + 0.723763f, + -1.860608f, + -0.344961f, + 0.228242f, + -1.000478f, + 0.084259f, + -0.019806f, + -1.482682f, + 0.037463f, + -0.686512f, + -0.311796f, + 0.093783f, + 0.688912f, + -0.159462f, + -1.397634f, + -0.137557f, + -0.338529f}); + // past_value: (1, 2, 3, 3) + test_case.add_input({0.037679f, + -0.045304f, + -0.488814f, + 1.094324f, + 0.850733f, + -0.892390f, + -1.093348f, + -0.089354f, + 0.707041f, + -0.529374f, + 0.797083f, + -1.046826f, + 1.921977f, + -0.794896f, + 1.044066f, + 1.725781f, + -0.258335f, + -0.714704f}); + // Output: (1, 2, 6) + test_case.add_expected_output(Shape{1, 2, 6}, + {-0.033162f, + 0.066176f, + -0.306049f, + 0.529358f, + 0.624898f, + -0.402690f, + 0.096545f, + -0.080529f, + -0.315609f, + 0.995826f, + 0.255064f, + -0.181851f}); + // present_key: (1, 2, 6, 3) + test_case.add_expected_output( + Shape{1, 2, 5, 3}, + {0.579000f, 0.723763f, -1.860608f, -0.344961f, 0.228242f, -1.000478f, 0.084259f, -0.019806f, + -1.482682f, -1.509428f, -0.268259f, 1.906879f, -0.197825f, -1.105133f, -1.770980f, 0.037463f, + -0.686512f, -0.311796f, 0.093783f, 0.688912f, -0.159462f, -1.397634f, -0.137557f, -0.338529f, + 0.167194f, -0.542977f, -0.340443f, 0.047243f, 0.305181f, 0.145112f}); + // present_value: (1, 2, 6, 3) + test_case.add_expected_output( + Shape{1, 2, 5, 3}, + {0.037679f, -0.045304f, -0.488814f, 1.094324f, 0.850733f, -0.892390f, -1.093348f, -0.089354f, + 0.707041f, 0.253975f, -0.832144f, 0.151920f, 0.977240f, -1.360663f, -0.819639f, -0.529374f, + 0.797083f, -1.046826f, 1.921977f, -0.794896f, 1.044066f, 1.725781f, -0.258335f, -0.714704f, + -0.008827f, 1.886953f, -0.562337f, 1.336034f, 0.113086f, -0.018064f}); + + test_case.run_with_tolerance_as_fp(1e-4f); +} + +OPENVINO_TEST(${BACKEND_NAME}, onnx_model_multi_head_attention_19_cache_bias) { + auto model = convert_model("com.microsoft/multi_head_attention_19_cache_bias.onnx"); + auto test_case = test::TestCase(model, s_device); + // Q: (1, 2, 6) + test_case.add_input({2.279423f, + -0.652962f, + 0.500308f, + 1.043512f, + 0.386979f, + 0.545369f, + -0.538741f, + -0.960648f, + 0.931486f, + 1.067415f, + -0.337619f, + 0.041636f}); + // K: (1, 2, 6) + test_case.add_input({0.680873f, + -0.413977f, + -0.555961f, + -0.912894f, + -0.909848f, + 0.168466f, + 0.584217f, + -1.205842f, + 1.238899f, + 1.448333f, + -0.218776f, + 1.595606f}); + // V: (1, 2, 6) + test_case.add_input({0.502159f, + -0.775111f, + 0.925219f, + 0.635485f, + -0.137744f, + -0.343956f, + -2.171855f, + -1.056835f, + -0.492180f, + -0.626209f, + 1.531523f, + 0.145174f}); + // bias: (18) + test_case.add_input({-0.981659f, + 1.382180f, + 0.142673f, + 0.935466f, + 0.081688f, + 0.211567f, + 0.729370f, + 0.307383f, + 0.737141f, + 1.001979f, + -0.077435f, + -0.255901f, + -1.147294f, + 0.600177f, + 1.774534f, + 1.259823f, + -0.078648f, + -1.016208f}); + // past_key: (1, 2, 3, 3) + test_case.add_input({-0.369884f, + 0.829115f, + -0.750030f, + 0.413610f, + -0.101967f, + -0.340023f, + -0.380541f, + 0.637319f, + -0.576284f, + 1.086806f, + 0.564889f, + 0.002937f, + -0.785653f, + -0.713155f, + -0.168350f, + -0.397433f, + -0.467106f, + 0.534076f}); + // past_value: (1, 2, 3, 3) + test_case.add_input({-0.056342f, + -0.628234f, + -1.537388f, + 1.151579f, + -1.671898f, + 2.328456f, + 1.445433f, + -1.268296f, + -0.570315f, + 0.989535f, + 0.197333f, + -0.550965f, + -1.947071f, + -0.335638f, + -1.325481f, + -0.174132f, + -0.659972f, + -0.141652f}); + // Output: (1, 2, 6) + test_case.add_expected_output(Shape{1, 2, 6}, + {-1.275576f, + -0.598931f, + 1.451448f, + 0.666530f, + 1.196724f, + -0.832774f, + -0.215988f, + -0.885536f, + 0.242943f, + 0.671371f, + 1.141197f, + -0.843512f}); + // present_key: (1, 2, 5, 3) + test_case.add_expected_output( + Shape{1, 2, 5, 3}, + {-0.369884f, 0.829115f, -0.750030f, 0.413610f, -0.101967f, -0.340023f, -0.380541f, 0.637319f, + -0.576284f, 1.410243f, -0.106594f, 0.181180f, 1.313587f, -0.898459f, 1.976040f, 1.086806f, + 0.564889f, 0.002937f, -0.785653f, -0.713155f, -0.168350f, -0.397433f, -0.467106f, 0.534076f, + 0.089085f, -0.987283f, -0.087435f, 2.450312f, -0.296211f, 1.339705f}); + // present_value: (1, 2, 5, 3) + test_case.add_expected_output( + Shape{1, 2, 5, 3}, + {-0.056342f, -0.628234f, -1.537388f, 1.151579f, -1.671898f, 2.328456f, 1.445433f, -1.268296f, + -0.570315f, -0.645135f, -0.174934f, 2.699753f, -3.319149f, -0.456658f, 1.282354f, 0.989535f, + 0.197333f, -0.550965f, -1.947071f, -0.335638f, -1.325481f, -0.174132f, -0.659972f, -0.141652f, + 1.895308f, -0.216392f, -1.360164f, 0.633614f, 1.452875f, -0.871034f}); + + test_case.run_with_tolerance_as_fp(1e-4f); +} + OPENVINO_TEST(${BACKEND_NAME}, onnx_model_fusedgemm_abc) { const auto model = convert_model("com.microsoft/fusedgemm.onnx"); auto test_case = ov::test::TestCase(model, s_device); @@ -1959,6 +2938,63 @@ OPENVINO_TEST(${BACKEND_NAME}, onnx_com_microsoft_rotary_embedding) { test_case.run_with_tolerance_as_fp(0.01f); } +OPENVINO_TEST(${BACKEND_NAME}, onnx_com_microsoft_rotary_embedding_fp16) { + // Load the existing rotary_embedding model and change input types to float16 + // to verify that neg_one constant type matches input element type + ov::frontend::FrontEnd::Ptr front_end; + auto input_model = load_model("com.microsoft/rotary_embedding.onnx", &front_end); + input_model->set_element_type(input_model->get_place_by_tensor_name("input"), ov::element::f16); + input_model->set_element_type(input_model->get_place_by_tensor_name("cos_cache"), ov::element::f16); + input_model->set_element_type(input_model->get_place_by_tensor_name("sin_cache"), ov::element::f16); + const auto model = front_end->convert(input_model); + + using f16 = ov::float16; + std::vector input = { + f16(-1.1258f), f16(-1.1524f), f16(-0.2506f), f16(-0.4339f), f16(0.8487f), f16(0.6920f), f16(-0.3160f), + f16(-2.1152f), f16(0.3223f), f16(-1.2633f), f16(0.3500f), f16(0.3081f), f16(0.1198f), f16(1.2377f), + f16(1.1168f), f16(-0.2473f), f16(-1.3527f), f16(-1.6959f), f16(0.5667f), f16(0.7935f), f16(0.5988f), + f16(-1.5551f), f16(-0.3414f), f16(1.8530f), f16(0.7502f), f16(-0.5855f), f16(-0.1734f), f16(0.1835f), + f16(1.3894f), f16(1.5863f), f16(0.9463f), f16(-0.8437f), + }; + std::vector position_ids = {0, 1}; + std::vector cos_cache = { + f16(0.8437f), + f16(-0.7849f), + f16(-0.7829f), + f16(0.4581f), + f16(-0.9870f), + f16(0.6273f), + f16(-0.9483f), + f16(-0.9962f), + }; + std::vector sin_cache = { + f16(0.5368f), + f16(0.6196f), + f16(-0.6222f), + f16(0.8889f), + f16(0.1605f), + f16(-0.7788f), + f16(0.3174f), + f16(-0.0872f), + }; + + std::vector expected_output = { + f16(-1.4054f), f16(0.4758f), f16(-0.0004f), f16(1.6814f), f16(0.1117f), f16(-1.2572f), f16(0.4033f), + f16(-1.3547f), f16(0.2076f), f16(0.2247f), f16(0.4209f), f16(0.361f), f16(0.2741f), f16(-1.7542f), + f16(-1.0921f), f16(0.1606f), f16(1.239f), f16(-2.275f), f16(-0.429f), f16(-0.6289f), f16(-0.8081f), + f16(0.3453f), f16(0.5036f), f16(-1.9152f), f16(-0.9634f), f16(0.8681f), f16(-0.1359f), f16(-0.2564f), + f16(-1.2509f), f16(1.4511f), f16(-0.9524f), f16(0.8245f), + }; + + auto test_case = ov::test::TestCase(model, s_device); + test_case.add_input(Shape{1, 2, 16}, input); + test_case.add_input(Shape{1, 2}, position_ids); + test_case.add_input(Shape{2, 4}, cos_cache); + test_case.add_input(Shape{2, 4}, sin_cache); + test_case.add_expected_output(Shape{1, 2, 16}, expected_output); + test_case.run_with_tolerance_as_fp(0.01f); +} + OPENVINO_TEST(${BACKEND_NAME}, onnx_model_gqa_past_0_input_1_rotary) { const auto model = convert_model("com.microsoft/gqa_rotary.onnx"); @@ -2048,6 +3084,116 @@ OPENVINO_TEST(${BACKEND_NAME}, onnx_model_gqa_past_0_input_1_rotary) { test_case.run_with_tolerance_as_fp(); } +OPENVINO_TEST(${BACKEND_NAME}, onnx_model_gqa_past_0_input_1_rotary_fp16) { + // Load the existing GQA rotary model and change float inputs to float16 + // to verify that neg_one constant type matches input element type in GQA decomposition + ov::frontend::FrontEnd::Ptr front_end; + auto input_model = load_model("com.microsoft/gqa_rotary.onnx", &front_end); + input_model->set_element_type(input_model->get_place_by_tensor_name("query"), ov::element::f16); + input_model->set_element_type(input_model->get_place_by_tensor_name("past_key"), ov::element::f16); + input_model->set_element_type(input_model->get_place_by_tensor_name("past_value"), ov::element::f16); + input_model->set_element_type(input_model->get_place_by_tensor_name("cos_cache"), ov::element::f16); + input_model->set_element_type(input_model->get_place_by_tensor_name("sin_cache"), ov::element::f16); + const auto model = front_end->convert(input_model); + + using f16 = ov::float16; + std::vector query = { + f16(-1.1258f), f16(-1.1524f), f16(-0.2506f), f16(-0.4339f), f16(0.8487f), f16(0.6920f), f16(-0.3160f), + f16(-2.1152f), f16(0.3223f), f16(-1.2633f), f16(0.3500f), f16(0.3081f), f16(0.1198f), f16(1.2377f), + f16(1.1168f), f16(-0.2473f), f16(-1.3527f), f16(-1.6959f), f16(0.5667f), f16(0.7935f), f16(0.5988f), + f16(-1.5551f), f16(-0.3414f), f16(1.8530f), f16(0.7502f), f16(-0.5855f), f16(-0.1734f), f16(0.1835f), + f16(1.3894f), f16(1.5863f), f16(0.9463f), f16(-0.8437f), f16(1.6459f), f16(-1.3602f), f16(0.3446f), + f16(0.5199f), f16(-2.6133f), f16(-1.6965f), f16(-0.2282f), f16(0.2800f), f16(0.2469f), f16(0.0769f), + f16(0.3380f), f16(0.4544f), f16(0.4569f), f16(-0.8654f), f16(0.7813f), f16(-0.9268f), f16(-0.2188f), + f16(-2.4351f), f16(-0.0729f), f16(-0.0340f), f16(0.9625f), f16(0.3492f), f16(-0.9215f), f16(-0.0562f), + f16(-0.6227f), f16(-0.4637f), f16(1.9218f), f16(-0.4025f), f16(0.1239f), f16(1.1648f), f16(0.9234f), + f16(1.3873f), + }; + std::vector past_key = {}; + std::vector past_value = {}; + std::vector seqlens_k = {0}; + std::vector total_sequence_length = {1}; + std::vector cos_cache = { + f16(0.8437f), + f16(-0.7849f), + f16(-0.7829f), + f16(0.4581f), + f16(-0.9870f), + f16(0.6273f), + f16(-0.9483f), + f16(-0.9962f), + }; + std::vector sin_cache = { + f16(0.5368f), + f16(0.6196f), + f16(-0.6222f), + f16(0.8889f), + f16(0.1605f), + f16(-0.7788f), + f16(0.3174f), + f16(-0.0872f), + }; + + std::vector expected_output = { + f16(-0.2188f), f16(-2.4351f), f16(-0.0729f), f16(-0.034f), f16(0.9625f), f16(0.3492f), f16(-0.9215f), + f16(-0.0562f), f16(-0.6227f), f16(-0.4637f), f16(1.9218f), f16(-0.4025f), f16(0.1239f), f16(1.1648f), + f16(0.9234f), f16(1.3873f), f16(-0.2188f), f16(-2.4351f), f16(-0.0729f), f16(-0.034f), f16(0.9625f), + f16(0.3492f), f16(-0.9215f), f16(-0.0562f), f16(-0.6227f), f16(-0.4637f), f16(1.9218f), f16(-0.4025f), + f16(0.1239f), f16(1.1648f), f16(0.9234f), f16(1.3873f), + }; + + std::vector expected_present_key = { + f16(1.2561098f), + f16(1.0199738f), + f16(-0.05948371f), + f16(-0.16574995f), + f16(2.5059946f), + f16(-1.738188f), + f16(-0.03158256f), + f16(-0.35975295f), + f16(1.0918287f), + f16(-0.90313876f), + f16(-0.4790303f), + f16(0.67029977f), + f16(-0.87039495f), + f16(0.7783688f), + f16(-0.81333745f), + f16(0.89886224f), + }; + + std::vector expected_present_value = { + f16(-0.2188f), + f16(-2.4351f), + f16(-0.0729f), + f16(-0.034f), + f16(0.9625f), + f16(0.3492f), + f16(-0.9215f), + f16(-0.0562f), + f16(-0.6227f), + f16(-0.4637f), + f16(1.9218f), + f16(-0.4025f), + f16(0.1239f), + f16(1.1648f), + f16(0.9234f), + f16(1.3873f), + }; + + auto test_case = ov::test::TestCase(model, s_device); + test_case.add_input(Shape{1, 1, 64}, query); + test_case.add_input(Shape{1, 1, 0, 16}, past_key); + test_case.add_input(Shape{1, 1, 0, 16}, past_value); + test_case.add_input(Shape{1, 1}, seqlens_k); + test_case.add_input(Shape{}, total_sequence_length); + test_case.add_input(Shape{1, 8}, cos_cache); + test_case.add_input(Shape{1, 8}, sin_cache); + test_case.add_expected_output(Shape{1, 1, 32}, expected_output); + test_case.add_expected_output(Shape{1, 1, 1, 16}, expected_present_key); + test_case.add_expected_output(Shape{1, 1, 1, 16}, expected_present_value); + test_case.run_with_tolerance_as_fp(0.01f); +} + OPENVINO_TEST(${BACKEND_NAME}, onnx_model_gqa_past_0_input_1_rotary_posid) { const auto model = convert_model("com.microsoft/gqa_rotary_posid.onnx"); @@ -2941,6 +4087,129 @@ OPENVINO_TEST(${BACKEND_NAME}, onnx_model_gqa_minimum_inputs_passes) { test_case.run_with_tolerance_as_fp(); } +OPENVINO_TEST(${BACKEND_NAME}, onnx_model_gqa_static_rotary) { + const auto model = convert_model("com.microsoft/gqa_static_rotary.onnx"); + + // Packed QKV input [1, 1, 64]: Q(2*16) + K(1*16) + V(1*16) + std::vector query = { + -1.1258f, -1.1524f, -0.2506f, -0.4339f, 0.8487f, 0.6920f, -0.3160f, -2.1152f, 0.3223f, -1.2633f, 0.3500f, + 0.3081f, 0.1198f, 1.2377f, 1.1168f, -0.2473f, -1.3527f, -1.6959f, 0.5667f, 0.7935f, 0.5988f, -1.5551f, + -0.3414f, 1.8530f, 0.7502f, -0.5855f, -0.1734f, 0.1835f, 1.3894f, 1.5863f, 0.9463f, -0.8437f, 1.6459f, + -1.3602f, 0.3446f, 0.5199f, -2.6133f, -1.6965f, -0.2282f, 0.2800f, 0.2469f, 0.0769f, 0.3380f, 0.4544f, + 0.4569f, -0.8654f, 0.7813f, -0.9268f, -0.2188f, -2.4351f, -0.0729f, -0.0340f, 0.9625f, 0.3492f, -0.9215f, + -0.0562f, -0.6227f, -0.4637f, 1.9218f, -0.4025f, 0.1239f, 1.1648f, 0.9234f, 1.3873f, + }; + + // Past KV [1, 1, 8, 16]: left-aligned, first 4 seqlen valid, last 4 zeros + std::vector past_key = { + -0.6136f, 0.0316f, -0.4927f, 0.2484f, 0.4397f, 0.1124f, 0.6408f, 0.4412f, -0.1023f, 0.7924f, -0.2897f, + 0.0525f, 0.5229f, 2.3022f, -1.4689f, -1.5867f, 0.2300f, 0.5100f, -0.3200f, 0.1100f, 0.7800f, -0.4500f, + 0.6700f, 0.3300f, -0.1200f, 0.8900f, -0.5600f, 0.4400f, 0.2100f, -0.7800f, 0.9300f, -0.1500f, 0.4100f, + -0.2300f, 0.8500f, -0.6100f, 0.1900f, 0.7200f, -0.3800f, 0.5500f, -0.4700f, 0.6300f, -0.1800f, 0.9100f, + -0.2700f, 0.3600f, -0.8100f, 0.4800f, 0.3500f, -0.6500f, 0.7100f, 0.2200f, -0.8900f, 0.4300f, -0.1600f, + 0.5800f, -0.7300f, 0.2800f, 0.6400f, -0.5200f, 0.1400f, -0.3300f, 0.8700f, -0.4100f, 0.0000f, 0.0000f, + 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, + 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, + 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, + 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, + 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, + 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, + }; + std::vector past_value = { + -0.5692f, 0.9200f, 1.1108f, 1.2899f, -1.4782f, 2.5672f, -0.4731f, 0.3356f, -1.6293f, -0.5497f, -0.4798f, + -0.4997f, -1.0670f, 1.1149f, -0.1407f, 0.8058f, 0.1500f, -0.3200f, 0.7800f, 0.4500f, -0.6100f, 0.2800f, + 0.9300f, -0.1200f, 0.5400f, -0.8700f, 0.3100f, 0.6600f, -0.2300f, 0.4100f, -0.7500f, 0.1800f, 0.6200f, + -0.4100f, 0.2700f, -0.8300f, 0.5500f, 0.1300f, -0.6800f, 0.9100f, -0.3500f, 0.7400f, -0.1600f, 0.4800f, + -0.9200f, 0.2100f, 0.6300f, -0.5700f, 0.8400f, -0.2600f, 0.3900f, 0.7100f, -0.1500f, 0.5200f, -0.8800f, + 0.3300f, -0.4300f, 0.6700f, -0.5100f, 0.2400f, -0.7600f, 0.1100f, 0.8500f, -0.3700f, 0.0000f, 0.0000f, + 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, + 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, + 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, + 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, + 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, + 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, + }; + + // seqlens_k = 4 means 4 valid past tokens (positions 0-3) + std::vector seqlens_k = {4}; + std::vector total_sequence_length = {8}; + + // cos/sin cache [8, 8] (max_seq_len=8, head_size/2=8) + std::vector cos_cache = { + -0.2509198f, 0.9014286f, 0.4639879f, 0.1973170f, -0.6879627f, -0.6880109f, -0.8838328f, 0.7323523f, + 0.2022300f, 0.4161451f, -0.9588310f, 0.9398197f, 0.6648853f, -0.5753218f, -0.6363501f, -0.6331910f, + -0.3915155f, 0.0495129f, -0.1361100f, -0.4175417f, 0.2237058f, -0.7210123f, -0.4157107f, -0.2672763f, + -0.0878600f, 0.5703519f, -0.6006525f, 0.0284689f, 0.1848291f, -0.9070992f, 0.2150897f, -0.6589518f, + -0.8698968f, 0.8977711f, 0.9312640f, 0.6167947f, -0.3907725f, -0.8046558f, 0.3684660f, -0.1196950f, + -0.7559235f, -0.0096462f, -0.9312230f, 0.8186408f, -0.4824400f, 0.3250446f, -0.3765779f, 0.0401360f, + 0.0934206f, -0.6302911f, 0.9391692f, 0.5502657f, 0.8789979f, 0.7896547f, 0.1958000f, 0.8437485f, + -0.8230150f, -0.6080343f, -0.9095454f, -0.3493393f, -0.2226454f, -0.4573019f, 0.6574750f, -0.2864934f, + }; + std::vector sin_cache = { + -0.4381310f, 0.0853922f, -0.7181516f, 0.6043940f, -0.8508987f, 0.9737739f, 0.5444896f, -0.6025686f, + -0.9889557f, 0.6309229f, 0.4137147f, 0.4580143f, 0.5425407f, -0.8519107f, -0.2830685f, -0.7682619f, + 0.7262068f, 0.2465962f, -0.3382039f, -0.8728833f, -0.3780354f, -0.3496334f, 0.4592124f, 0.2751150f, + 0.7744255f, -0.0555701f, -0.7608115f, 0.4264896f, 0.5215701f, 0.1225544f, 0.5419344f, -0.0124088f, + 0.0454657f, -0.1449180f, -0.9491618f, -0.7842171f, -0.9371417f, 0.2728208f, -0.3712880f, 0.0171414f, + 0.8151330f, -0.5014156f, -0.1792341f, 0.5111023f, -0.5424037f, -0.8460402f, -0.4204971f, -0.6775574f, + 0.8593953f, 0.6162407f, 0.2668075f, 0.7429212f, 0.6073442f, -0.6268599f, 0.7851180f, 0.0786845f, + 0.6148803f, 0.7921826f, -0.3639930f, -0.7798961f, -0.5441297f, -0.1457844f, 0.6360295f, 0.7214612f, + }; + + // Expected output [1, 1, 32] + std::vector expected_output = { + 0.4162956f, -0.4182589f, 0.4727696f, 0.2936685f, -0.1128318f, 0.5199768f, -0.3651099f, 0.3073604f, + -0.2850454f, 0.1067962f, -0.0051489f, 0.2774143f, -0.6051727f, 0.3807620f, 0.3404019f, -0.0230611f, + 0.3233508f, -0.7579141f, 0.3853467f, 0.1999806f, 0.0748156f, 0.4330993f, -0.3911937f, 0.2431847f, + -0.2730540f, -0.0074681f, 0.3233708f, 0.2087932f, -0.4696053f, 0.4899607f, 0.3912313f, 0.1830239f, + }; + + // Expected present_key [1, 1, 8, 16]: past seqlen 0-3 unchanged, seqlen 4 = K_rotary, seqlen 5-7 = 0 + std::vector expected_present_key = { + -0.6136f, 0.0316f, -0.4927f, 0.2484f, 0.4397f, 0.1124f, 0.6408f, 0.4412f, -0.1023f, 0.7924f, -0.2897f, + 0.0525f, 0.5229f, 2.3022f, -1.4689f, -1.5867f, 0.2300f, 0.5100f, -0.3200f, 0.1100f, 0.7800f, -0.4500f, + 0.6700f, 0.3300f, -0.1200f, 0.8900f, -0.5600f, 0.4400f, 0.2100f, -0.7800f, 0.9300f, -0.1500f, 0.4100f, + -0.2300f, 0.8500f, -0.6100f, 0.1900f, 0.7200f, -0.3800f, 0.5500f, -0.4700f, 0.6300f, -0.1800f, 0.9100f, + -0.2700f, 0.3600f, -0.8100f, 0.4800f, 0.3500f, -0.6500f, 0.7100f, 0.2200f, -0.8900f, 0.4300f, -0.1600f, + 0.5800f, -0.7300f, 0.2800f, 0.6400f, -0.5200f, 0.1400f, -0.3300f, 0.8700f, -0.4100f, -1.4430f, -1.2100f, + 0.6417f, 0.6770f, 1.4494f, 1.6012f, 0.2060f, -0.0176f, -0.1399f, 0.2662f, -0.0123f, -0.1274f, 2.2705f, + 0.2335f, 0.3726f, 0.1157f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, + 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, + 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, + 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, + 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, + }; + + // Expected present_value [1, 1, 8, 16]: past seqlen 0-3 unchanged, seqlen 4 = V, seqlen 5-7 = 0 + std::vector expected_present_value = { + -0.5692f, 0.9200f, 1.1108f, 1.2899f, -1.4782f, 2.5672f, -0.4731f, 0.3356f, -1.6293f, -0.5497f, -0.4798f, + -0.4997f, -1.0670f, 1.1149f, -0.1407f, 0.8058f, 0.1500f, -0.3200f, 0.7800f, 0.4500f, -0.6100f, 0.2800f, + 0.9300f, -0.1200f, 0.5400f, -0.8700f, 0.3100f, 0.6600f, -0.2300f, 0.4100f, -0.7500f, 0.1800f, 0.6200f, + -0.4100f, 0.2700f, -0.8300f, 0.5500f, 0.1300f, -0.6800f, 0.9100f, -0.3500f, 0.7400f, -0.1600f, 0.4800f, + -0.9200f, 0.2100f, 0.6300f, -0.5700f, 0.8400f, -0.2600f, 0.3900f, 0.7100f, -0.1500f, 0.5200f, -0.8800f, + 0.3300f, -0.4300f, 0.6700f, -0.5100f, 0.2400f, -0.7600f, 0.1100f, 0.8500f, -0.3700f, -0.2188f, -2.4351f, + -0.0729f, -0.0340f, 0.9625f, 0.3492f, -0.9215f, -0.0562f, -0.6227f, -0.4637f, 1.9218f, -0.4025f, 0.1239f, + 1.1648f, 0.9234f, 1.3873f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, + 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, + 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, + 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, + 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, + }; + + auto test_case = ov::test::TestCase(model, s_device); + test_case.add_input(Shape{1, 1, 64}, query); + test_case.add_input(Shape{1, 1, 8, 16}, past_key); + test_case.add_input(Shape{1, 1, 8, 16}, past_value); + test_case.add_input(Shape{1, 1}, seqlens_k); + test_case.add_input(Shape{}, total_sequence_length); + test_case.add_input(Shape{8, 8}, cos_cache); + test_case.add_input(Shape{8, 8}, sin_cache); + test_case.add_expected_output(Shape{1, 1, 32}, expected_output); + test_case.add_expected_output(Shape{1, 1, 8, 16}, expected_present_key); + test_case.add_expected_output(Shape{1, 1, 8, 16}, expected_present_value); + test_case.run_with_tolerance_as_fp(1e-4f); +} + OPENVINO_TEST(${BACKEND_NAME}, onnx_model_gqa_insufficient_inputs_throws) { try { convert_model("com.microsoft/gqa_oob.onnx"); @@ -3271,3 +4540,195 @@ OPENVINO_TEST(${BACKEND_NAME}, onnx_model_group_norm_channels_last) { test_case.add_expected_output(shape, output); test_case.run(); } + +OPENVINO_TEST(${BACKEND_NAME}, onnx_model_bifurcation_detector_phase1_bifurcates) { + // src=[10,20,30,40,50], cur=[99], prev_idx=0, pred=[10,20,99] + // Phase 1: pred matches src at k=0,1; mismatch at k=2 -> bifur_idx=2 + // tokens = cur ++ pred[0:3] = [99,10,20,99] + // Phase 2 (n=1): suffix=[99] not in src -> suffix_idx stays -1 + const auto model = convert_model("com.microsoft/bifurcation_detector_with_pred.onnx"); + auto test_case = ov::test::TestCase(model, s_device); + test_case.add_input(Shape{5}, {10, 20, 30, 40, 50}); + test_case.add_input(Shape{1}, {99}); + test_case.add_input(Shape{}, {0}); + test_case.add_input(Shape{3}, {10, 20, 99}); + test_case.add_expected_output(Shape{4}, {99, 10, 20, 99}); + test_case.add_expected_output(Shape{}, {-1}); + test_case.run(); +} + +OPENVINO_TEST(${BACKEND_NAME}, onnx_model_bifurcation_detector_no_pred_unique_suffix) { + // src=[1,2,3,4,5], cur=[1,2], prev_idx=-1, no pred + // Phase 1: tokens = cur = [1,2] + // Phase 2 (n=1): suffix=[2] found uniquely at idx=1; candidate=2; count==1 -> suffix_idx=2 + // (n=2): suffix=[1,2] found uniquely at idx=0; candidate=2; count==1 -> suffix_idx=2 + const auto model = convert_model("com.microsoft/bifurcation_detector_no_pred.onnx"); + auto test_case = ov::test::TestCase(model, s_device); + test_case.add_input(Shape{5}, {1, 2, 3, 4, 5}); + test_case.add_input(Shape{2}, {1, 2}); + test_case.add_input(Shape{}, {-1}); + test_case.add_expected_output(Shape{2}, {1, 2}); + test_case.add_expected_output(Shape{}, {2}); + test_case.run(); +} + +OPENVINO_TEST(${BACKEND_NAME}, onnx_model_bifurcation_detector_no_pred_ambiguous_suffix) { + // src=[1,2,3,1,2], cur=[9,1,2], prev_idx=-1, no pred + // Phase 1: tokens = cur = [9,1,2] + // Phase 2 (n=1): suffix=[2] found at idx 1 and 4 -> count>=2 -> suffix_idx=-1 + // (n=2): suffix=[1,2] found at idx 0 and 3 -> count>=2 -> suffix_idx=-1 + const auto model = convert_model("com.microsoft/bifurcation_detector_no_pred.onnx"); + auto test_case = ov::test::TestCase(model, s_device); + test_case.add_input(Shape{5}, {1, 2, 3, 1, 2}); + test_case.add_input(Shape{3}, {9, 1, 2}); + test_case.add_input(Shape{}, {-1}); + test_case.add_expected_output(Shape{3}, {9, 1, 2}); + test_case.add_expected_output(Shape{}, {-1}); + test_case.run(); +} + +OPENVINO_TEST(${BACKEND_NAME}, onnx_model_bifurcation_detector_bifurcation_at_first_token) { + // src=[1,2,3], cur=[10,20], prev_idx=0, pred=[99,2,3,0] + // Phase 1: pred[0]=99 != src[0]=1 -> bifur_idx=0, take=1, pred_kept=[99] + // tokens = cur ++ [99] = [10,20,99] + // Phase 2 (n=1): suffix=[99] not in src -> suffix_idx stays -1 + const auto model = convert_model("com.microsoft/bifurcation_detector_with_pred.onnx"); + auto test_case = ov::test::TestCase(model, s_device); + test_case.add_input(Shape{3}, {1, 2, 3}); + test_case.add_input(Shape{2}, {10, 20}); + test_case.add_input(Shape{}, {0}); + test_case.add_input(Shape{4}, {99, 2, 3, 0}); + test_case.add_expected_output(Shape{3}, {10, 20, 99}); + test_case.add_expected_output(Shape{}, {-1}); + test_case.run(); +} + +OPENVINO_TEST(${BACKEND_NAME}, onnx_model_bifurcation_detector_full_match_no_bifurcation) { + // src=[10,20,30], cur=[5], prev_idx=0, pred=[10,20,30,99] + // Phase 1: n=min(4,3)=3. pred[0..2] all match src[0..2] -> bifur_idx=3 (full match) + // take=4, pred_kept=[10,20,30,99], tokens=[5,10,20,30,99] + // Phase 2 (n=1): suffix=[99] not in src -> suffix_idx stays -1 + const auto model = convert_model("com.microsoft/bifurcation_detector_with_pred.onnx"); + auto test_case = ov::test::TestCase(model, s_device); + test_case.add_input(Shape{3}, {10, 20, 30}); + test_case.add_input(Shape{1}, {5}); + test_case.add_input(Shape{}, {0}); + test_case.add_input(Shape{4}, {10, 20, 30, 99}); + test_case.add_expected_output(Shape{5}, {5, 10, 20, 30, 99}); + test_case.add_expected_output(Shape{}, {-1}); + test_case.run(); +} + +OPENVINO_TEST(${BACKEND_NAME}, onnx_model_bifurcation_detector_prev_idx_at_boundary) { + // src=[1,5,3,4], cur=[10,20], prev_idx=4 (==src_len), pred=[99] + // Phase 1: n=min(1, 4-4)=0 -> bifur_idx=0, take=1, pred_kept=pred[0:1]=[99] + // tokens = cur ++ [99] = [10,20,99] + // Phase 2 (n=1): suffix=[99] not in src -> suffix_idx stays -1 + const auto model = convert_model("com.microsoft/bifurcation_detector_with_pred.onnx"); + auto test_case = ov::test::TestCase(model, s_device); + test_case.add_input(Shape{4}, {1, 5, 3, 4}); + test_case.add_input(Shape{2}, {10, 20}); + test_case.add_input(Shape{}, {4}); + test_case.add_input(Shape{1}, {99}); + test_case.add_expected_output(Shape{3}, {10, 20, 99}); + test_case.add_expected_output(Shape{}, {-1}); + test_case.run(); +} + +OPENVINO_TEST(${BACKEND_NAME}, onnx_model_bifurcation_detector_bifurcation_and_suffix_match_combined) { + // src=[10,20,30,40,50,60], cur=[5,8], prev_idx=3, pred=[40,50,99,0] + // Phase 1: n=min(4, 6-3)=3. pred[0..2]=[40,50,99] vs src[3..5]=[40,50,60] + // match at k=0,1; mismatch at k=2 -> bifur_idx=2, take=3 + // pred_kept=[40,50,99], tokens=[5,8,40,50,99] + // Phase 2 (n=1): suffix=[99] not in src -> suffix_idx stays -1 + const auto model = convert_model("com.microsoft/bifurcation_detector_with_pred.onnx"); + auto test_case = ov::test::TestCase(model, s_device); + test_case.add_input(Shape{6}, {10, 20, 30, 40, 50, 60}); + test_case.add_input(Shape{2}, {5, 8}); + test_case.add_input(Shape{}, {3}); + test_case.add_input(Shape{4}, {40, 50, 99, 0}); + test_case.add_expected_output(Shape{5}, {5, 8, 40, 50, 99}); + test_case.add_expected_output(Shape{}, {-1}); + test_case.run(); +} + +OPENVINO_TEST(${BACKEND_NAME}, onnx_model_bifurcation_detector_suffix_match_at_end_of_src) { + // src=[1,2,3], cur=[5,3], prev_idx=0, no pred + // Phase 1: tokens = cur = [5,3] + // Phase 2 (n=1): suffix=[3] unique at src[2]; candidate=3 == src_len -> out_of_range, + // assign suffix_idx=3 then stop. + // (n=2): stopped -> suffix_idx stays 3 + const auto model = convert_model("com.microsoft/bifurcation_detector_no_pred.onnx"); + auto test_case = ov::test::TestCase(model, s_device); + test_case.add_input(Shape{3}, {1, 2, 3}); + test_case.add_input(Shape{2}, {5, 3}); + test_case.add_input(Shape{}, {0}); + test_case.add_expected_output(Shape{2}, {5, 3}); + test_case.add_expected_output(Shape{}, {3}); + test_case.run(); +} + +OPENVINO_TEST(${BACKEND_NAME}, onnx_model_bifurcation_detector_ngram_exceeds_tokens_len) { + // src=[1..10], cur=[5,3], prev_idx=0, no pred, min_ngram=5, max_ngram=7 + // Phase 1: tokens = cur = [5,3] (len 2) + // Phase 2: n=5 > tokens_len=2 -> n_too_large -> skip, stop. Same for n=6,7. + // suffix_idx stays at initial -1. + const auto model = convert_model("com.microsoft/bifurcation_detector_no_pred_ngram_5_7.onnx"); + auto test_case = ov::test::TestCase(model, s_device); + test_case.add_input(Shape{10}, {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}); + test_case.add_input(Shape{2}, {5, 3}); + test_case.add_input(Shape{}, {0}); + test_case.add_expected_output(Shape{2}, {5, 3}); + test_case.add_expected_output(Shape{}, {-1}); + test_case.run(); +} + +OPENVINO_TEST(${BACKEND_NAME}, onnx_model_bifurcation_detector_with_pred_negative_prev_idx) { + // src=[10,20,30,40,50], cur=[99], prev_idx=-1, pred=[10,20,99] + // prev_suffix_match_idx=-1 must be clamped to 0 before slicing (a raw -1 would be + // interpreted by Slice as an index from the end and corrupt Phase 1). + // Phase 1 (prev_idx->0): pred matches src at k=0,1; mismatch at k=2 -> bifur_idx=2 + // tokens = cur ++ pred[0:3] = [99,10,20,99] + // Phase 2 (n=1): suffix=[99] not in src -> suffix_idx stays -1 + const auto model = convert_model("com.microsoft/bifurcation_detector_with_pred.onnx"); + auto test_case = ov::test::TestCase(model, s_device); + test_case.add_input(Shape{5}, {10, 20, 30, 40, 50}); + test_case.add_input(Shape{1}, {99}); + test_case.add_input(Shape{}, {-1}); + test_case.add_input(Shape{3}, {10, 20, 99}); + test_case.add_expected_output(Shape{4}, {99, 10, 20, 99}); + test_case.add_expected_output(Shape{}, {-1}); + test_case.run(); +} + +OPENVINO_TEST(${BACKEND_NAME}, onnx_model_bifurcation_detector_empty_src) { + // src=[] (empty), cur=[1,2], prev_idx=0, no pred + // Phase 1: tokens = cur = [1,2] + // Phase 2 (n=1,2): src_len=0 < n -> no_match for every n; the Gather over src must + // stay in-bounds (regression: empty src previously produced an invalid + // Gather index). suffix_idx stays at initial -1. + const auto model = convert_model("com.microsoft/bifurcation_detector_no_pred.onnx"); + auto test_case = ov::test::TestCase(model, s_device); + test_case.add_input(Shape{0}, {}); + test_case.add_input(Shape{2}, {1, 2}); + test_case.add_input(Shape{}, {0}); + test_case.add_expected_output(Shape{2}, {1, 2}); + test_case.add_expected_output(Shape{}, {-1}); + test_case.run(); +} + +OPENVINO_TEST(${BACKEND_NAME}, onnx_model_bifurcation_detector_src_shorter_than_ngram) { + // src=[1,2,3], cur=[1,2,3,4,5,6], prev_idx=0, no pred, min_ngram=5, max_ngram=7 + // Phase 1: tokens = cur = [1,2,3,4,5,6] (len 6) + // Phase 2: n=5 <= tokens_len=6 but n > src_len=3, so a length-n substring cannot + // exist in src -> no_match (regression: clamped windows must not be + // reported as a false match). Same for n=6,7. suffix_idx stays -1. + const auto model = convert_model("com.microsoft/bifurcation_detector_no_pred_ngram_5_7.onnx"); + auto test_case = ov::test::TestCase(model, s_device); + test_case.add_input(Shape{3}, {1, 2, 3}); + test_case.add_input(Shape{6}, {1, 2, 3, 4, 5, 6}); + test_case.add_input(Shape{}, {0}); + test_case.add_expected_output(Shape{6}, {1, 2, 3, 4, 5, 6}); + test_case.add_expected_output(Shape{}, {-1}); + test_case.run(); +} diff --git a/src/frontends/onnx/tests/onnx_import_controlflow.in.cpp b/src/frontends/onnx/tests/onnx_import_controlflow.in.cpp index e8c2c7521115..6cf3ac872437 100644 --- a/src/frontends/onnx/tests/onnx_import_controlflow.in.cpp +++ b/src/frontends/onnx/tests/onnx_import_controlflow.in.cpp @@ -9,6 +9,7 @@ #include "common_test_utils/type_prop.hpp" #include "gtest/gtest.h" #include "onnx_utils.hpp" +#include "openvino/frontend/exception.hpp" #include "openvino/op/loop.hpp" #include "openvino/op/multiply.hpp" @@ -237,6 +238,23 @@ OPENVINO_TEST(${BACKEND_NAME}, onnx_controlflow_loop_add_value_access_to_body_sc } } +// Regression test: Loop body with too few inputs relative to loop-carried +// dependencies must throw instead of accessing body_inputs out of bounds. +OPENVINO_TEST(${BACKEND_NAME}, onnx_controlflow_loop_too_few_body_inputs_exception) { + OV_EXPECT_THROW(convert_model("controlflow/loop_too_few_body_inputs.onnx"), + ov::AssertFailure, + testing::AllOf(testing::HasSubstr("loop body graph canonical inputs size"), + testing::HasSubstr("does not match the sum of loop carried dependencies"))); +} + +// Regression test: Loop body with too few outputs relative to loop-carried +// dependencies must throw instead of accessing body_outputs out of bounds. +OPENVINO_TEST(${BACKEND_NAME}, onnx_controlflow_loop_too_few_body_outputs_exception) { + OV_EXPECT_THROW(convert_model("controlflow/loop_too_few_body_outputs.onnx"), + ov::AssertFailure, + testing::HasSubstr("loop body graph outputs size")); +} + OPENVINO_TEST(${BACKEND_NAME}, onnx_controlflow_loop_add_value_the_same_node_from_parent_and_subgraph) { const auto model = convert_model("controlflow/loop_2d_add_the_same_name.onnx"); @@ -758,6 +776,69 @@ OPENVINO_TEST(${BACKEND_NAME}, onnx_if_negative_missing_branches) { } } +OPENVINO_TEST(${BACKEND_NAME}, onnx_if_with_initializer_as_output) { + /* + if (condition) { + return identity(x) + } else { + return initializer constant [0, 0, 0] + } + Tests that an If branch whose output is directly an initializer + (no compute nodes, no graph inputs) converts correctly. + */ + const auto model = convert_model("controlflow/if_with_initializer_as_output.onnx"); + + auto test_case = ov::test::TestCase(model, s_device); + + // condition == true => return x + test_case.add_input({true}); + test_case.add_input({1.0f, 2.0f, 3.0f}); + test_case.add_expected_output({1.0f, 2.0f, 3.0f}); + test_case.run(); + + // condition == false => return initializer [0, 0, 0] + test_case.add_input({false}); + test_case.add_input({1.0f, 2.0f, 3.0f}); + test_case.add_expected_output({0.0f, 0.0f, 0.0f}); + test_case.run(); +} + +OPENVINO_TEST(${BACKEND_NAME}, onnx_loop_with_initializer_as_output) { + /* + Loop body declares an initializer ("step") as one of its graph outputs + in addition to the canonical [cond_out, loop_carried_out]. + The body output count therefore exceeds 1 + node.get_outputs_size(); the + trailing Constant pass-through must be trimmed so num_scan_outputs is 0 + and the Loop converts correctly. + */ + const auto model = convert_model("controlflow/loop_with_initializer_as_output.onnx"); + + auto test_case = ov::test::TestCase(model, s_device); + test_case.add_input({3}); // trip_count + test_case.add_input({true}); // cond_in + test_case.add_input({0.0f}); // x_init + test_case.add_expected_output(Shape{1}, {3.0f}); + test_case.run(); +} + +OPENVINO_TEST(${BACKEND_NAME}, onnx_scan_with_initializer_as_output) { + /* + Scan body declares an initializer ("leak") as a trailing graph output. + The body output count would otherwise be miscounted as an extra scan + output; the trailing Constant pass-through must be trimmed so the + Scan converts with num_scan_outputs equal to node.get_outputs_size() - + num_initial_values. + */ + const auto model = convert_model("controlflow/scan_with_initializer_as_output.onnx"); + + auto test_case = ov::test::TestCase(model, s_device); + test_case.add_input(Shape{1}, {0.0f}); // init_state + test_case.add_input(Shape{3, 1}, {1.0f, 2.0f, 3.0f}); // seq + test_case.add_expected_output(Shape{1}, {6.0f}); // final_state = 0+1+2+3 + test_case.add_expected_output(Shape{3, 1}, {1.0f, 3.0f, 6.0f}); // scan outputs + test_case.run(); +} + OPENVINO_TEST(${BACKEND_NAME}, onnx_if_negative_mismatch_between_branches_output) { try { const auto model = convert_model("controlflow/if_negative_mismatch_between_branches_output.onnx"); @@ -825,3 +906,38 @@ OPENVINO_TEST(${BACKEND_NAME}, onnx_sequence_construct_concat) { test_case.add_expected_output(Shape{3, 2}, {1.f, 2.f, 3.f, 4.f, 5.f, 6.f}); test_case.run(); } + +/// @brief Test SequenceConstruct -> SequenceInsert chain -> ConcatFromSequence pattern +/// Reproduces the pattern produced by exporters for vision transformer style models +/// (ConvNeXt, Swin family) where a sequence is built up via repeated SequenceInsert calls +/// outside any Loop and then collapsed by ConcatFromSequence. Verifies that +/// SequenceMark::get_sequence() correctly walks the chain of SequenceInsert nodes. +OPENVINO_TEST(${BACKEND_NAME}, onnx_sequence_insert_chain_concat) { + const auto model = convert_model("controlflow/sequence_insert_chain_concat.onnx"); + + auto test_case = ov::test::TestCase(model, s_device); + // Four input tensors of shape [2] + test_case.add_input({1.f, 2.f}); // input_a + test_case.add_input({3.f, 4.f}); // input_b + test_case.add_input({5.f, 6.f}); // input_c + test_case.add_input({7.f, 8.f}); // input_d + + // axis=0, new_axis=0: concatenate four [2] tensors -> [8] + test_case.add_expected_output(Shape{8}, {1.f, 2.f, 3.f, 4.f, 5.f, 6.f, 7.f, 8.f}); + test_case.run(); +} + +/// @brief Test SequenceEmpty -> SequenceInsert chain -> ConcatFromSequence with new_axis=1 +/// Exercises the same chain pattern starting from an empty sequence and stacking along a new axis. +OPENVINO_TEST(${BACKEND_NAME}, onnx_sequence_insert_chain_new_axis) { + const auto model = convert_model("controlflow/sequence_insert_chain_new_axis.onnx"); + + auto test_case = ov::test::TestCase(model, s_device); + test_case.add_input({1.f, 2.f}); // input_a + test_case.add_input({3.f, 4.f}); // input_b + test_case.add_input({5.f, 6.f}); // input_c + + // axis=0, new_axis=1: stack three [2] tensors -> [3, 2] + test_case.add_expected_output(Shape{3, 2}, {1.f, 2.f, 3.f, 4.f, 5.f, 6.f}); + test_case.run(); +} diff --git a/src/frontends/onnx/tests/onnx_import_ml_normalizer.in.cpp b/src/frontends/onnx/tests/onnx_import_ml_normalizer.in.cpp new file mode 100644 index 000000000000..0c17affa5f30 --- /dev/null +++ b/src/frontends/onnx/tests/onnx_import_ml_normalizer.in.cpp @@ -0,0 +1,129 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +// clang-format off +#ifdef ${BACKEND_NAME}_FLOAT_TOLERANCE_BITS +# define DEFAULT_FLOAT_TOLERANCE_BITS ${BACKEND_NAME}_FLOAT_TOLERANCE_BITS +#endif +#ifdef ${BACKEND_NAME}_DOUBLE_TOLERANCE_BITS +# define DEFAULT_DOUBLE_TOLERANCE_BITS ${BACKEND_NAME}_DOUBLE_TOLERANCE_BITS +#endif +// clang-format on + +#include + +#include "common_test_utils/file_utils.hpp" +#include "common_test_utils/test_case.hpp" +#include "common_test_utils/test_control.hpp" +#include "gtest/gtest.h" +#include "onnx_utils.hpp" + +using namespace ov::test; +using namespace ov; +using namespace ov::frontend::onnx::tests; + +static std::string s_manifest = "${MANIFEST}"; +static std::string s_device = backend_name_to_device("${BACKEND_NAME}"); + +// ----------------------------------------------------------------------- +// ai.onnx.ml.Normalizer – norm="L2" +// +// Input X shape: [3, 4] +// Each row is divided by its L2 norm (sqrt of sum of squares). +// ----------------------------------------------------------------------- +OPENVINO_TEST(${BACKEND_NAME}, onnx_ai_onnx_ml_normalizer_l2) { + const auto model = convert_model("ai.onnx.ml/normalizer_l2.onnx"); + auto test_case = ov::test::TestCase(model, s_device); + + // clang-format off + test_case.add_input({ + 1.0f, 2.0f, 3.0f, 4.0f, + 5.0f, 6.0f, 7.0f, 8.0f, + 9.0f, 10.0f, 11.0f, 12.0f + }); + // clang-format on + + // Expected: divide each row by its L2 norm + const float n0 = std::sqrt(1.0f * 1.0f + 2.0f * 2.0f + 3.0f * 3.0f + 4.0f * 4.0f); // sqrt(30) + const float n1 = std::sqrt(5.0f * 5.0f + 6.0f * 6.0f + 7.0f * 7.0f + 8.0f * 8.0f); // sqrt(174) + const float n2 = std::sqrt(9.0f * 9.0f + 10.0f * 10.0f + 11.0f * 11.0f + 12.0f * 12.0f); // sqrt(446) + + // clang-format off + test_case.add_expected_output( + ov::Shape{3, 4}, + { + 1.0f / n0, 2.0f / n0, 3.0f / n0, 4.0f / n0, + 5.0f / n1, 6.0f / n1, 7.0f / n1, 8.0f / n1, + 9.0f / n2, 10.0f / n2, 11.0f / n2, 12.0f / n2 + }); + // clang-format on + + test_case.run(); +} + +// ----------------------------------------------------------------------- +// ai.onnx.ml.Normalizer – norm="L1" +// +// Input X shape: [2, 3] +// Each row is divided by its L1 norm (sum of absolute values). +// ----------------------------------------------------------------------- +OPENVINO_TEST(${BACKEND_NAME}, onnx_ai_onnx_ml_normalizer_l1) { + const auto model = convert_model("ai.onnx.ml/normalizer_l1.onnx"); + auto test_case = ov::test::TestCase(model, s_device); + + // clang-format off + test_case.add_input({ + 1.0f, 2.0f, 3.0f, + 4.0f, 5.0f, 6.0f + }); + // clang-format on + + // Expected: divide each row by its L1 norm (sum of abs values) + const float n0 = 1.0f + 2.0f + 3.0f; // 6.0 + const float n1 = 4.0f + 5.0f + 6.0f; // 15.0 + + // clang-format off + test_case.add_expected_output( + ov::Shape{2, 3}, + { + 1.0f / n0, 2.0f / n0, 3.0f / n0, + 4.0f / n1, 5.0f / n1, 6.0f / n1 + }); + // clang-format on + + test_case.run(); +} + +// ----------------------------------------------------------------------- +// ai.onnx.ml.Normalizer – norm="MAX" +// +// Input X shape: [2, 5] +// Each row is divided by its MAX value (maximum of absolute values). +// ----------------------------------------------------------------------- +OPENVINO_TEST(${BACKEND_NAME}, onnx_ai_onnx_ml_normalizer_max) { + const auto model = convert_model("ai.onnx.ml/normalizer_max.onnx"); + auto test_case = ov::test::TestCase(model, s_device); + + // clang-format off + test_case.add_input({ + 1.0f, 2.0f, 3.0f, 4.0f, 5.0f, + 6.0f, 2.0f, 8.0f, 1.0f, 4.0f + }); + // clang-format on + + // Expected: divide each row by its max element + const float m0 = 5.0f; // max of row 0 + const float m1 = 8.0f; // max of row 1 + + // clang-format off + test_case.add_expected_output( + ov::Shape{2, 5}, + { + 1.0f / m0, 2.0f / m0, 3.0f / m0, 4.0f / m0, 5.0f / m0, + 6.0f / m1, 2.0f / m1, 8.0f / m1, 1.0f / m1, 4.0f / m1 + }); + // clang-format on + + test_case.run(); +} diff --git a/src/frontends/onnx/tests/onnx_import_reshape.in.cpp b/src/frontends/onnx/tests/onnx_import_reshape.in.cpp index bea93f087c08..1206895f0dfb 100644 --- a/src/frontends/onnx/tests/onnx_import_reshape.in.cpp +++ b/src/frontends/onnx/tests/onnx_import_reshape.in.cpp @@ -183,6 +183,25 @@ OPENVINO_TEST(${BACKEND_NAME}, onnx_model_reshape_output_shape_as_input) { test_case.run(); } +// Regression test: with the `allowzero=1` attribute (opset 14+), a `0` in the +// target shape must be taken literally instead of being mapped to OpenVINO's +// `special_zero=true` (copy-dim) semantics. Previously, when the shape was +// provided as an input (opset 5+ form), `allowzero` was ignored and `0` was +// always interpreted as "copy from input". +OPENVINO_TEST(${BACKEND_NAME}, onnx_model_reshape_allowzero) { + auto model = convert_model("reshape_allowzero.onnx"); + + // input shape (0, 3, 4), zero-element tensor + std::vector input; + // target shape literally [3, 4, 0] (must NOT be reinterpreted as [3, 4, 4]) + std::vector expected_output; + + auto test_case = ov::test::TestCase(model, s_device); + test_case.add_input(Shape{0, 3, 4}, input); + test_case.add_expected_output(Shape{3, 4, 0}, expected_output); + test_case.run(); +} + OPENVINO_TEST(${BACKEND_NAME}, onnx_model_depth_to_space) { auto model = convert_model("depth_to_space.onnx"); diff --git a/src/frontends/onnx/tests/onnx_importer_test.cpp b/src/frontends/onnx/tests/onnx_importer_test.cpp index 1e1bb53e9738..b52f4ca04201 100644 --- a/src/frontends/onnx/tests/onnx_importer_test.cpp +++ b/src/frontends/onnx/tests/onnx_importer_test.cpp @@ -114,7 +114,7 @@ TEST(ONNX_Importer_Tests, ImportModelWhenFileDoesNotExist) { TEST(ONNX_Importer_Tests, ImportModelFromStream) { auto model_file_path = - test::utils::getModelFromTestModelZoo(util::path_join({TEST_ONNX_MODELS_DIRNAME, "addmul_abc.onnx"}).string()); + test::utils::getModelFromTestModelZoo(util::path_join({TEST_ONNX_MODELS_DIRNAME, "addmul_abc.onnx"})); std::ifstream model_file_stream(model_file_path, std::ifstream::binary); ASSERT_TRUE(model_file_stream.is_open()); int count_adds = 0; @@ -135,15 +135,15 @@ TEST(ONNX_Importer_Tests, ImportModelFromStream) { TEST(ONNX_Importer_Tests, ImportModelWithoutMetadata) { Core core; - auto model = core.read_model(test::utils::getModelFromTestModelZoo( - util::path_join({TEST_ONNX_MODELS_DIRNAME, "priorbox_clustered.onnx"}).string())); + auto model = core.read_model( + test::utils::getModelFromTestModelZoo(util::path_join({TEST_ONNX_MODELS_DIRNAME, "priorbox_clustered.onnx"}))); ASSERT_FALSE(model->has_rt_info("framework")); } TEST(ONNX_Importer_Tests, ImportModelWithMetadata) { Core core; - auto model = core.read_model(test::utils::getModelFromTestModelZoo( - util::path_join({TEST_ONNX_MODELS_DIRNAME, "model_with_metadata.onnx"}).string())); + auto model = core.read_model( + test::utils::getModelFromTestModelZoo(util::path_join({TEST_ONNX_MODELS_DIRNAME, "model_with_metadata.onnx"}))); ASSERT_TRUE(model->has_rt_info("framework")); const auto rtinfo = model->get_rt_info(); diff --git a/src/frontends/onnx/tests/onnx_reader_external_data.cpp b/src/frontends/onnx/tests/onnx_reader_external_data.cpp index 624cf46d3548..f5449fdb5389 100644 --- a/src/frontends/onnx/tests/onnx_reader_external_data.cpp +++ b/src/frontends/onnx/tests/onnx_reader_external_data.cpp @@ -18,6 +18,7 @@ #include "openvino/core/rt_info/weightless_caching_attributes.hpp" #include "openvino/frontend/manager.hpp" #include "openvino/op/constant.hpp" +#include "utils/tensor_external_data.hpp" using namespace std; using namespace ov; @@ -253,10 +254,7 @@ TEST_P(OnnxFeMmapFixture, onnx_external_invalid_up_dir_path) { core.read_model(path); FAIL() << "Incorrect path to external data not detected"; } catch (const Exception& ex) { - EXPECT_PRED_FORMAT2(testing::IsSubstring, - string("tensor.data, offset: 4096, " - "data_length: 16)"), - ex.what()); + EXPECT_PRED_FORMAT2(testing::IsSubstring, string("which is outside the base directory"), ex.what()); } catch (...) { FAIL() << "Importing onnx model failed for unexpected reason"; } diff --git a/src/frontends/onnx/tests/op_extension.cpp b/src/frontends/onnx/tests/op_extension.cpp index 675c1d4eabf1..4f2a5e625030 100644 --- a/src/frontends/onnx/tests/op_extension.cpp +++ b/src/frontends/onnx/tests/op_extension.cpp @@ -155,7 +155,7 @@ TEST(ONNXOpExtensionViaCommonConstructor, onnx_op_extension_via_template_arg_wit fe->add_extension(ext); const auto input_model = fe->load(ov::test::utils::getModelFromTestModelZoo( - ov::util::path_join({TEST_ONNX_MODELS_DIRNAME, "relu_custom_domain.onnx"}).string())); + ov::util::path_join({TEST_ONNX_MODELS_DIRNAME, "relu_custom_domain.onnx"}))); std::shared_ptr model; EXPECT_NO_THROW(fe->convert(input_model)); @@ -169,7 +169,7 @@ TEST(ONNXOpExtensionViaCommonConstructor, onnx_op_extension_via_ov_type_name_wit fe->add_extension(ext); const auto input_model = fe->load(ov::test::utils::getModelFromTestModelZoo( - ov::util::path_join({TEST_ONNX_MODELS_DIRNAME, "relu_custom_domain.onnx"}).string())); + ov::util::path_join({TEST_ONNX_MODELS_DIRNAME, "relu_custom_domain.onnx"}))); std::shared_ptr model; EXPECT_NO_THROW(fe->convert(input_model)); diff --git a/src/frontends/onnx/tests/tests_python/test_backend.py b/src/frontends/onnx/tests/tests_python/test_backend.py index f942f4d1843e..2837365d3afa 100644 --- a/src/frontends/onnx/tests/tests_python/test_backend.py +++ b/src/frontends/onnx/tests/tests_python/test_backend.py @@ -17,7 +17,6 @@ xfail_issue_38699, xfail_issue_38701, xfail_issue_38706, - xfail_issue_38710, xfail_issue_38713, xfail_issue_38724, xfail_issue_38734, @@ -35,7 +34,6 @@ xfail_issue_63137, xfail_issue_69444, skip_segfault, - xfail_issue_82038, xfail_issue_82039, xfail_issue_90649, skip_bitwise_ui64, @@ -44,7 +42,6 @@ xfail_issue_99961, xfail_issue_99968, xfail_issue_99969, - xfail_issue_99970, xfail_issue_101965, xfail_issue_113506, skip_dynamic_model, @@ -72,8 +69,6 @@ xfail_issue_139937, xfail_issue_139938, xfail_issue_171767, - xfail_issue_171768, - xfail_issue_171770, xfail_issue_171771, xfail_issue_171772, ) @@ -155,9 +150,6 @@ def expect_fail(test_case_path, xfail): # type: (str) -> None xfail_issue_33596, "OnnxBackendSimpleModelTest.test_sequence_model1_cpu", "OnnxBackendSimpleModelTest.test_sequence_model3_cpu", - "OnnxBackendSimpleModelTest.test_sequence_model6_cpu", - "OnnxBackendSimpleModelTest.test_sequence_model8_cpu", - "OnnxBackendSimpleModelTest.test_sequence_model2_cpu", "OnnxBackendNodeModelTest.test_identity_sequence_cpu", "OnnxBackendNodeModelTest.test_if_seq_cpu", "OnnxBackendNodeModelTest.test_if_opt_cpu", # Optional, SequenceConstruct @@ -289,10 +281,6 @@ def expect_fail(test_case_path, xfail): # type: (str) -> None "OnnxBackendNodeModelTest.test_lstm_batchwise_cpu", "OnnxBackendNodeModelTest.test_simple_rnn_batchwise_cpu", ), - ( - xfail_issue_38710, - "OnnxBackendNodeModelTest.test_reshape_allowzero_reordered_cpu", - ), ( skip_dynamic_model, "OnnxBackendNodeModelTest.test_triu_one_row_cpu", @@ -347,11 +335,6 @@ def expect_fail(test_case_path, xfail): # type: (str) -> None "OnnxBackendNodeModelTest.test_layer_normalization_4d_axis_negative_4_cpu", # ticket: 90649 "OnnxBackendNodeModelTest.test_layer_normalization_default_axis_cpu", # ticket: 90649 ), - ( - xfail_issue_82038, - "OnnxBackendNodeModelTest.test_scatternd_add_cpu", - "OnnxBackendNodeModelTest.test_scatternd_multiply_cpu", - ), ( xfail_issue_82039, "OnnxBackendNodeModelTest.test_identity_opt_cpu", @@ -381,10 +364,6 @@ def expect_fail(test_case_path, xfail): # type: (str) -> None ( xfail_issue_99952, "OnnxBackendNodeModelTest.test_col2im_5d_cpu", - "OnnxBackendNodeModelTest.test_col2im_cpu", - "OnnxBackendNodeModelTest.test_col2im_dilations_cpu", - "OnnxBackendNodeModelTest.test_col2im_pads_cpu", - "OnnxBackendNodeModelTest.test_col2im_strides_cpu", ), ( xfail_issue_99961, @@ -419,11 +398,6 @@ def expect_fail(test_case_path, xfail): # type: (str) -> None "OnnxBackendNodeModelTest.test_resize_upsample_sizes_nearest_not_larger_cpu", "OnnxBackendNodeModelTest.test_resize_upsample_sizes_nearest_not_smaller_cpu", ), - ( - xfail_issue_99970, - "OnnxBackendNodeModelTest.test_scatternd_max_cpu", - "OnnxBackendNodeModelTest.test_scatternd_min_cpu", - ), ( xfail_issue_101965, "OnnxBackendNodeModelTest.test_dft_axis_cpu", @@ -622,7 +596,6 @@ def expect_fail(test_case_path, xfail): # type: (str) -> None ( xfail_issue_139938, "OnnxBackendNodeModelTest.test_qlinearmatmul_2D_int8_float32_cpu", - "OnnxBackendNodeModelTest.test_qlinearmatmul_2D_uint8_float16_cpu", ), ( xfail_issue_171767, @@ -633,39 +606,6 @@ def expect_fail(test_case_path, xfail): # type: (str) -> None "OnnxBackendNodeModelTest.test_dequantizelinear_float4e2m1_cpu", "OnnxBackendNodeModelTest.test_quantizelinear_float4e2m1_cpu", ), - ( - xfail_issue_171768, - "OnnxBackendNodeModelTest.test_rms_normalization_2d_axis0_cpu", - "OnnxBackendNodeModelTest.test_rms_normalization_2d_axis1_cpu", - "OnnxBackendNodeModelTest.test_rms_normalization_2d_axis_negative_1_cpu", - "OnnxBackendNodeModelTest.test_rms_normalization_2d_axis_negative_2_cpu", - "OnnxBackendNodeModelTest.test_rms_normalization_3d_axis0_epsilon_cpu", - "OnnxBackendNodeModelTest.test_rms_normalization_3d_axis1_epsilon_cpu", - "OnnxBackendNodeModelTest.test_rms_normalization_3d_axis2_epsilon_cpu", - "OnnxBackendNodeModelTest.test_rms_normalization_3d_axis_negative_1_epsilon_cpu", - "OnnxBackendNodeModelTest.test_rms_normalization_3d_axis_negative_2_epsilon_cpu", - "OnnxBackendNodeModelTest.test_rms_normalization_3d_axis_negative_3_epsilon_cpu", - "OnnxBackendNodeModelTest.test_rms_normalization_4d_axis0_cpu", - "OnnxBackendNodeModelTest.test_rms_normalization_4d_axis1_cpu", - "OnnxBackendNodeModelTest.test_rms_normalization_4d_axis2_cpu", - "OnnxBackendNodeModelTest.test_rms_normalization_4d_axis3_cpu", - "OnnxBackendNodeModelTest.test_rms_normalization_4d_axis_negative_1_cpu", - "OnnxBackendNodeModelTest.test_rms_normalization_4d_axis_negative_2_cpu", - "OnnxBackendNodeModelTest.test_rms_normalization_4d_axis_negative_3_cpu", - "OnnxBackendNodeModelTest.test_rms_normalization_4d_axis_negative_4_cpu", - "OnnxBackendNodeModelTest.test_rms_normalization_default_axis_cpu", - ), - ( - xfail_issue_171770, - "OnnxBackendNodeModelTest.test_rotary_embedding_3d_input_cpu", - "OnnxBackendNodeModelTest.test_rotary_embedding_cpu", - "OnnxBackendNodeModelTest.test_rotary_embedding_interleaved_cpu", - "OnnxBackendNodeModelTest.test_rotary_embedding_no_position_ids_cpu", - "OnnxBackendNodeModelTest.test_rotary_embedding_no_position_ids_interleaved_cpu", - "OnnxBackendNodeModelTest.test_rotary_embedding_no_position_ids_rotary_dim_cpu", - "OnnxBackendNodeModelTest.test_rotary_embedding_with_interleaved_rotary_dim_cpu", - "OnnxBackendNodeModelTest.test_rotary_embedding_with_rotary_dim_cpu", - ), ( xfail_issue_171771, "OnnxBackendNodeModelTest.test_top_k_same_values_2d_cpu", diff --git a/src/frontends/paddle/src/frontend.cpp b/src/frontends/paddle/src/frontend.cpp index 47bca7c00678..8e343943f9b3 100644 --- a/src/frontends/paddle/src/frontend.cpp +++ b/src/frontends/paddle/src/frontend.cpp @@ -151,7 +151,7 @@ std::istream* variant_to_stream_ptr(const ov::Any& variant, std::fstream& fs, st FRONT_END_INITIALIZATION_CHECK(ss && ss.good(), "Cannot open ov::tensor."); return &ss; } else if (const auto path = ov::frontend::get_path_from_any(variant)) { - fs.open(path.value(), std::ios::in | std::ifstream::binary); + fs.open(*path, std::ios::in | std::ifstream::binary); FRONT_END_INITIALIZATION_CHECK(fs && fs.is_open(), "Cannot open model file."); return &fs; } @@ -372,8 +372,8 @@ bool FrontEnd::supported_impl(const std::vector& variants) const { return false; // Validating first path, it must contain a model - if (const auto path = ov::frontend::get_path_from_any(variants[0])) { - std::filesystem::path model_path = path.value(); + if (auto path = ov::frontend::get_path_from_any(variants[0])) { + std::filesystem::path model_path = std::move(*path); if (ov::util::directory_exists(model_path)) { model_path /= "__model__"; FRONT_END_GENERAL_CHECK(util::file_exists(model_path), "Could not open the file: ", model_path); @@ -389,18 +389,25 @@ bool FrontEnd::supported_impl(const std::vector& variants) const { // but it will complicate the check, while it should be as quick as possible return model_str && model_str.is_open(); } else if (variants[0].is()) { - // Validating first stream, it must contain a model - // step 1: - // PDPD API ParseFromIstream always deconstructs the context in model stream. - // So, make a copy for variants[0] to avoid breaking the context in variants[0]. + // Do not parse stream content in supported_impl. Fuzzed / malformed data can + // trigger undefined behavior in protobuf internals while probing support. + // Keep this check lightweight and non-destructive, similar to path-based probing. const auto p_model_stream = variants[0].as(); - std::istream copy_model_stream(p_model_stream->rdbuf()); - ::paddle::framework::proto::ProgramDesc fw; - auto ret = fw.ParseFromIstream(©_model_stream); - // step 2: - // reset the stream position to the beginning. - p_model_stream->seekg(0, p_model_stream->beg); - return ret; + FRONT_END_GENERAL_CHECK(p_model_stream != nullptr, "Cannot use null model stream."); + + auto old_state = p_model_stream->rdstate(); + auto old_pos = p_model_stream->tellg(); + + p_model_stream->clear(); + auto first = p_model_stream->peek(); + + p_model_stream->clear(); + if (old_pos != std::istream::pos_type(-1)) { + p_model_stream->seekg(old_pos); + } + p_model_stream->clear(old_state); + + return first != std::char_traits::eof() && ((static_cast(first) & 0x07u) == 2u); } return false; } @@ -411,7 +418,7 @@ InputModel::Ptr FrontEnd::load_impl(const std::vector& variants) const if (variants.size() == 1 + extra_variants_num) { // The case when folder with __model__ and weight files is provided or .pdmodel file if (const auto path = ov::frontend::get_path_from_any(variants[0])) { - return std::make_shared(path.value().native(), m_telemetry); + return std::make_shared(*path, m_telemetry); } // The case with only model stream provided and no weights. This means model has // no learnable weights diff --git a/src/frontends/paddle/src/input_model.cpp b/src/frontends/paddle/src/input_model.cpp index dddbbacc6907..a0c33ecdd196 100644 --- a/src/frontends/paddle/src/input_model.cpp +++ b/src/frontends/paddle/src/input_model.cpp @@ -4,10 +4,8 @@ #include "input_model.hpp" +#include #include -#if defined(__MINGW32__) || defined(__MINGW64__) -# include -#endif #include #include @@ -31,8 +29,7 @@ using namespace ::paddle::framework::proto; class InputModel::InputModelImpl { public: - template - InputModelImpl(const std::basic_string& path, + InputModelImpl(const std::filesystem::path& path, const InputModel& input_model, const std::shared_ptr& telemetry); InputModelImpl(const std::vector& streams, @@ -63,8 +60,7 @@ class InputModel::InputModelImpl { private: void load_places(); - template - void load_consts(const std::basic_string& folder_with_weights); + void load_consts(const std::filesystem::path& folder_with_weights); void load_consts(std::istream* weight_stream); void create_temp_consts(); std::vector> determine_cut_nodes() const; @@ -187,67 +183,26 @@ ov::Shape make_shape_checked(const DimsT& dims) { return shape; } -template -std::basic_string get_const_path(const std::basic_string& folder_with_weights, const std::string& name) { - return folder_with_weights + paddle::get_path_sep() + name; +std::filesystem::path get_const_path(const std::filesystem::path& folder_with_weights, const std::string& name) { + return folder_with_weights / ov::util::make_path(name); } -#if defined(OPENVINO_ENABLE_UNICODE_PATH_SUPPORT) && defined(_WIN32) -template <> -std::basic_string get_const_path(const std::basic_string& folder, const std::string& name) { - return folder + paddle::get_path_sep() + ov::util::string_to_wstring(name); +bool is_pdmodel(const std::filesystem::path& path) { + return path.extension() == ".pdmodel"; } -#endif -template -bool is_pdmodel(const std::basic_string& path) { - std::string ext = ".pdmodel"; - return ov::util::ends_with(path, ext); -} - -#if defined(OPENVINO_ENABLE_UNICODE_PATH_SUPPORT) && defined(_WIN32) -template <> -bool is_pdmodel(const std::basic_string& path) { - std::wstring ext = L".pdmodel"; - return ov::util::ends_with(path, ext); -} -#endif - -template -std::basic_string get_model_path(const std::basic_string& path, std::ifstream* weights_stream) { - std::string model_file{path}; - std::string ext = ".pdmodel"; - if (ov::util::ends_with(model_file, ext)) { - std::string params_ext = ".pdiparams"; - std::string weights_file{path}; - weights_file.replace(weights_file.size() - ext.size(), ext.size(), params_ext); +std::filesystem::path get_model_path(std::filesystem::path model_file, std::ifstream* weights_stream) { + if (is_pdmodel(model_file)) { + auto weights_file = model_file; + weights_file.replace_extension(".pdiparams"); weights_stream->open(weights_file, std::ios::binary); // Don't throw error if file isn't opened // It may mean that model don't have constants } else { - model_file += paddle::get_path_sep() + "__model__"; + model_file = model_file / "__model__"; } return model_file; } - -#if defined(OPENVINO_ENABLE_UNICODE_PATH_SUPPORT) && defined(_WIN32) -template <> -std::basic_string get_model_path(const std::basic_string& path, std::ifstream* weights_stream) { - std::wstring model_file{path}; - std::wstring ext = L".pdmodel"; - if (ov::util::ends_with(model_file, ext)) { - std::wstring params_ext = L".pdiparams"; - std::wstring weights_file{path}; - weights_file.replace(weights_file.size() - ext.size(), ext.size(), params_ext); - weights_stream->open(weights_file.c_str(), std::ios::binary); - // Don't throw error if file isn't opened - // It may mean that model don't have constants - } else { - model_file += paddle::get_path_sep() + L"__model__"; - } - return model_file; -} -#endif } // namespace std::vector> InputModel::InputModelImpl::get_op_places(const int32_t blck_idx) const { @@ -299,8 +254,7 @@ std::vector> InputModel::InputModelImpl::determine_cut_ } // load_consts with folder is compatible with old PaddlePaddle API. -template -void InputModel::InputModelImpl::load_consts(const std::basic_string& folder_with_weights) { +void InputModel::InputModelImpl::load_consts(const std::filesystem::path& folder_with_weights) { for (const auto& item : m_var_places) { const auto& var_desc = item.second->get_desc(); const auto& name = item.first; @@ -319,12 +273,7 @@ void InputModel::InputModelImpl::load_consts(const std::basic_string& folder_ bool read_succeed = false; if (!folder_with_weights.empty()) { -#if defined(__MINGW32__) || defined(__MINGW64__) - std::ifstream is(std::filesystem::path(get_const_path(folder_with_weights, name)), - std::ios::in | std::ifstream::binary); -#else std::ifstream is(get_const_path(folder_with_weights, name), std::ios::in | std::ifstream::binary); -#endif FRONT_END_GENERAL_CHECK(is && is.is_open(), "Cannot open file for constant value."); const size_t header_size = 16; std::vector header(header_size); @@ -438,20 +387,16 @@ void InputModel::InputModelImpl::load_consts(std::istream* weight_stream) { read *.pdmodel as model stream. read *.pdiparam as weight stream. */ -template -InputModel::InputModelImpl::InputModelImpl(const std::basic_string& path, +InputModel::InputModelImpl::InputModelImpl(const std::filesystem::path& path, const InputModel& input_model, const std::shared_ptr& telemetry) : m_fw_ptr{std::make_shared()}, m_input_model(input_model), m_telemetry(telemetry) { std::ifstream weights_stream; - std::ifstream pb_stream(get_model_path(path, &weights_stream).c_str(), std::ios::in | std::ifstream::binary); + std::ifstream pb_stream(get_model_path(path, &weights_stream), std::ios::in | std::ifstream::binary); - FRONT_END_GENERAL_CHECK(pb_stream && pb_stream.is_open(), - "Could not open the file: \"", - util::path_to_string(path), - '"'); + FRONT_END_GENERAL_CHECK(pb_stream && pb_stream.is_open(), "Could not open the file: ", path); FRONT_END_GENERAL_CHECK(m_fw_ptr->ParseFromIstream(&pb_stream), "Model can't be parsed"); // According to Paddle, the saved model has the framework version // For example Paddle 2.1.0 is encoded as 2001000. 0 means the latest framework. @@ -625,13 +570,8 @@ void InputModel::InputModelImpl::set_tensor_value(Place::Ptr place, const void* m_tensor_values[name] = constant; } -InputModel::InputModel(const std::string& path, const std::shared_ptr& telemetry) - : _impl{std::make_shared(path, *this, telemetry)} {} - -#if defined(OPENVINO_ENABLE_UNICODE_PATH_SUPPORT) && defined(_WIN32) -InputModel::InputModel(const std::wstring& path, const std::shared_ptr& telemetry) +InputModel::InputModel(const std::filesystem::path& path, const std::shared_ptr& telemetry) : _impl{std::make_shared(path, *this, telemetry)} {} -#endif InputModel::InputModel(const std::vector& streams, const std::shared_ptr& telemetry) : _impl{std::make_shared(streams, *this, telemetry)} {} diff --git a/src/frontends/paddle/src/input_model.hpp b/src/frontends/paddle/src/input_model.hpp index 4bdd6c58c68e..b1fdacf36c30 100644 --- a/src/frontends/paddle/src/input_model.hpp +++ b/src/frontends/paddle/src/input_model.hpp @@ -4,6 +4,8 @@ #pragma once +#include + #include "openvino/frontend/extension/telemetry.hpp" #include "openvino/frontend/paddle/frontend.hpp" @@ -16,10 +18,7 @@ class TensorPlace; class InputModel : public ov::frontend::InputModel { public: - explicit InputModel(const std::string& path, const std::shared_ptr& telemetry = {}); -#if defined(OPENVINO_ENABLE_UNICODE_PATH_SUPPORT) && defined(_WIN32) - explicit InputModel(const std::wstring& path, const std::shared_ptr& telemetry = {}); -#endif + explicit InputModel(const std::filesystem::path& path, const std::shared_ptr& telemetry = {}); explicit InputModel(const std::vector& streams, const std::shared_ptr& telemetry = {}); std::vector get_inputs() const override; diff --git a/src/frontends/paddle/src/internal/op/conditional_block.hpp b/src/frontends/paddle/src/internal/op/conditional_block.hpp index b4e26043b720..99dbf61dc724 100644 --- a/src/frontends/paddle/src/internal/op/conditional_block.hpp +++ b/src/frontends/paddle/src/internal/op/conditional_block.hpp @@ -34,7 +34,7 @@ class ConditionalBlock : public Op { /// \return A vector containing the values for each input except "cond". const OutputVector get_inputs_from_parent() const; - const int32_t get_subblock_index() const { + int32_t get_subblock_index() const { return m_sub_block_index; } diff --git a/src/frontends/paddle/src/internal/op/while.hpp b/src/frontends/paddle/src/internal/op/while.hpp index f6e96e4abada..2a12d34850cc 100644 --- a/src/frontends/paddle/src/internal/op/while.hpp +++ b/src/frontends/paddle/src/internal/op/while.hpp @@ -25,7 +25,7 @@ class While : public Op { std::shared_ptr clone_with_new_inputs(const OutputVector& new_args) const override; - const int32_t get_subblock_index() const { + int32_t get_subblock_index() const { return m_sub_block; } diff --git a/src/frontends/paddle/src/op/dequantize_linear.cpp b/src/frontends/paddle/src/op/dequantize_linear.cpp index 65c67c9f2fdc..334569ac9c5d 100644 --- a/src/frontends/paddle/src/op/dequantize_linear.cpp +++ b/src/frontends/paddle/src/op/dequantize_linear.cpp @@ -3,6 +3,7 @@ // #include "default_opset.hpp" +#include "openvino/decompositions/low_precision_dequantize.hpp" #include "openvino/frontend/paddle/node_context.hpp" namespace ov { @@ -35,9 +36,8 @@ NamedOutputs dequantize_linear(const NodeContext& node) { const auto bit_length = node.get_attribute("bit_length"); const auto range = (1 << (bit_length - 1)) - 1; const auto range_node = std::make_shared(element::f32, Shape{1}, (1.0 / range)); - const auto real_scale = std::make_shared(scale, range_node); + ov::Output real_scale = std::make_shared(scale, range_node); - auto q_node = std::make_shared(x, element::f32); // extract the ATTRIBUTES and explanation for quant_axis: // / [-1] --- per-tensor, scale is always 1-D // quant_axis - [0 or 1] --- per-channel, scale may be 1-D or 2-D, needing to reshape for input shape. @@ -51,13 +51,8 @@ NamedOutputs dequantize_linear(const NodeContext& node) { return quant_axis == value; }), "dequantize_linear quant_axis is NOT in the range of [-1, 0, 1]."); - if (quant_axis == -1) { - const auto zp_node = std::make_shared(zero_point, element::f32); - const auto out_node = - std::make_shared(std::make_shared(q_node, zp_node), - real_scale); - return node.default_single_output_mapping({out_node}, {"Y"}); - } else { + ov::Output zp_input = zero_point; + if (quant_axis != -1) { // But for per-channel scenario, the shape of scale is NOT stable. // Sometimes scale is 1-D and sometimes scale is 2-D. But the last dim(e.g. s[len-1]) really makes sense. // Let's prepare a pattern to reshape operation according to the scale shape. @@ -65,15 +60,11 @@ NamedOutputs dequantize_linear(const NodeContext& node) { reshape_pattern.at(quant_axis) = scale_shape[scale_shape_length - 1].get_length(); const auto reshape_node = std::make_shared(element::i32, Shape{reshape_pattern.size()}, reshape_pattern); - const auto reshape_scale = std::make_shared(real_scale, reshape_node, true); - const auto zp_node = std::make_shared( - std::make_shared(zero_point, reshape_node, true), - element::f32); - const auto out_node = - std::make_shared(std::make_shared(q_node, zp_node), - reshape_scale); - return node.default_single_output_mapping({out_node}, {"Y"}); + real_scale = std::make_shared(real_scale, reshape_node, true); + zp_input = std::make_shared(zero_point, reshape_node, true); } + auto out_node = ov::decomposition::low_precision_dequantize(x, real_scale, zp_input); + return node.default_single_output_mapping({out_node.get_node_shared_ptr()}, {"Y"}); } } // namespace op diff --git a/src/frontends/paddle/src/paddle_utils.hpp b/src/frontends/paddle/src/paddle_utils.hpp index 6ef7966668aa..5c8c8120b7da 100644 --- a/src/frontends/paddle/src/paddle_utils.hpp +++ b/src/frontends/paddle/src/paddle_utils.hpp @@ -10,26 +10,6 @@ namespace ov { namespace frontend { namespace paddle { -#ifdef _WIN32 -const char PATH_SEPARATOR = '\\'; -# if defined(OPENVINO_ENABLE_UNICODE_PATH_SUPPORT) -const wchar_t WPATH_SEPARATOR = L'\\'; -# endif -#else -const char PATH_SEPARATOR = '/'; -#endif - -template -inline std::basic_string get_path_sep() { - return std::basic_string{PATH_SEPARATOR}; -} - -#if defined(OPENVINO_ENABLE_UNICODE_PATH_SUPPORT) && defined(_WIN32) -template <> -inline std::basic_string get_path_sep() { - return std::basic_string{WPATH_SEPARATOR}; -} -#endif std::shared_ptr reorder_axes(const Output& value, std::vector axes_order); } // namespace paddle diff --git a/src/frontends/pytorch/src/op/full.cpp b/src/frontends/pytorch/src/op/full.cpp index baba649a0845..2884d5569110 100644 --- a/src/frontends/pytorch/src/op/full.cpp +++ b/src/frontends/pytorch/src/op/full.cpp @@ -424,6 +424,21 @@ OutputVector translate_fill_diagonal(const NodeContext& context) { filled_tensor = context.mark_node(std::make_shared(filled_tensor, input_shape, false)); return {filled_tensor}; } + +OutputVector translate_empty_fx(const NodeContext& context) { + // aten.empty.memory_format(SymInt[] size, *, ScalarType? dtype=None, ...) + // In the FX graph dtype is passed as a node attribute, not as a positional input. + // In OV uninitialized data is not supported, so we fill with zeros. + num_inputs_check(context, 1, 1); + auto sizes = context.get_input(0); + auto value = context.mark_node(v0::Constant::create(element::f32, Shape{}, {0})); + auto filled_tensor = base_translate_full(context, sizes, value); + if (context.has_attribute("dtype")) { + auto dtype = context.get_attribute("dtype"); + filled_tensor = context.mark_node(std::make_shared(filled_tensor, dtype)); + } + return {filled_tensor}; +}; } // namespace op } // namespace pytorch } // namespace frontend diff --git a/src/frontends/pytorch/src/op/linear.cpp b/src/frontends/pytorch/src/op/linear.cpp index 81370973a03b..73bdbe51b527 100644 --- a/src/frontends/pytorch/src/op/linear.cpp +++ b/src/frontends/pytorch/src/op/linear.cpp @@ -2,13 +2,12 @@ // SPDX-License-Identifier: Apache-2.0 // +#include "openvino/decompositions/low_precision_dequantize.hpp" #include "openvino/frontend/pytorch/node_context.hpp" #include "openvino/op/add.hpp" #include "openvino/op/matmul.hpp" -#include "openvino/op/multiply.hpp" #include "openvino/op/reshape.hpp" #include "openvino/op/shape_of.hpp" -#include "openvino/op/subtract.hpp" #include "utils.hpp" namespace ov { @@ -80,16 +79,10 @@ Output low_precision_subgraph(const NodeContext& context, const Output& zero_points, const Output& scales, const Output& out_shape) { - auto new_qweight = context.mark_node(std::make_shared(weights, scales.get_element_type())); - auto new_qzeros = context.mark_node(std::make_shared(zero_points, scales.get_element_type())); - - auto w_s = context.mark_node(std::make_shared(new_qweight, new_qzeros)); - auto weight = context.mark_node(std::make_shared(w_s, scales)); - auto weight_shape = weights.get_shape(); - if (out_shape.get_node() != nullptr) { - weight = context.mark_node(std::make_shared(weight, out_shape, false)); - } - weight = context.mark_node(std::make_shared(weight, x)); + ov::pass::NodeRegistry reg; + auto weight = ov::decomposition::low_precision_dequantize(reg, weights, scales, zero_points, out_shape); + weight = reg.make(weight, x); + context.mark_nodes(reg.get()); return weight; } diff --git a/src/frontends/pytorch/src/op/norm.cpp b/src/frontends/pytorch/src/op/norm.cpp index 28bbfddf06b9..a019e53697be 100644 --- a/src/frontends/pytorch/src/op/norm.cpp +++ b/src/frontends/pytorch/src/op/norm.cpp @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 // +#include "openvino/decompositions/rms_norm.hpp" #include "openvino/frontend/pytorch/node_context.hpp" #include "openvino/op/abs.hpp" #include "openvino/op/add.hpp" @@ -369,21 +370,14 @@ OutputVector translate_rms_norm(const NodeContext& context) { auto axes_range = context.mark_node(std::make_shared(num_axes, zero, minus_one, element::i32)); auto axes = context.mark_node(std::make_shared(axes_range, minus_one)); - // decomposition of RMSNorm to be fused by plugins - auto power_const = context.mark_node(v0::Constant::create(ov::element::f32, {}, {2.f})); - power_const = context.mark_node(std::make_shared(power_const, x)); - auto power = context.mark_node(std::make_shared(x, power_const)); - auto mean = context.mark_node(std::make_shared(power, axes, true)); - auto add_eps = context.mark_node(std::make_shared(mean, eps)); - auto sqrt = context.mark_node(std::make_shared(add_eps)); - auto div_const = context.mark_node(v0::Constant::create(ov::element::f32, {}, {-1})); - div_const = context.mark_node(std::make_shared(div_const, x)); - auto div = context.mark_node(std::make_shared(sqrt, div_const)); - auto result = context.mark_node(std::make_shared(x, div)); - + // Build the RMSNorm decomposition via the shared helper. + ov::Output scale; if (!context.input_is_none(2)) { - result = context.mark_node(std::make_shared(context.get_input(2), result)); + scale = context.get_input(2); } + ov::pass::NodeRegistry reg; + auto result = ov::decomposition::rms_norm(reg, x, axes, eps, scale); + context.mark_nodes(reg.get()); return {result}; } diff --git a/src/frontends/pytorch/src/op/rand.cpp b/src/frontends/pytorch/src/op/rand.cpp index b7d1b6adfdef..b5570b994c20 100644 --- a/src/frontends/pytorch/src/op/rand.cpp +++ b/src/frontends/pytorch/src/op/rand.cpp @@ -16,7 +16,6 @@ #include "openvino/op/shape_of.hpp" #include "openvino/op/sqrt.hpp" #include "pt_framework_node.hpp" -#include "transformations/rt_info/disable_fp16_compression.hpp" #include "utils.hpp" namespace ov { diff --git a/src/frontends/pytorch/src/op/scalar_tensor.cpp b/src/frontends/pytorch/src/op/scalar_tensor.cpp index a958ab013a5f..02ed5385bf65 100644 --- a/src/frontends/pytorch/src/op/scalar_tensor.cpp +++ b/src/frontends/pytorch/src/op/scalar_tensor.cpp @@ -18,11 +18,15 @@ using namespace ov::op; OutputVector translate_scalar_tensor_fx(const NodeContext& context) { // aten.scalar_tensor.default(-100.0, dtype = torch.float32, layout = torch.strided, device = device(type='cpu')) - num_inputs_check(context, 1, 1); + // TorchScript: aten::scalar_tensor(Scalar s, *, ScalarType? dtype, Layout? layout, Device? device, bool? + // pin_memory) + num_inputs_check(context, 1, 5); auto data = context.get_input(0); if (context.has_attribute("dtype")) { auto dtype = context.get_attribute("dtype"); data = context.mark_node(std::make_shared(context.get_input(0), dtype)); + } else if (context.get_input_size() > 1 && !context.input_is_none(1)) { + data = apply_dtype(context, 1, data); } // layout and device can be ignored return {data}; diff --git a/src/frontends/pytorch/src/op/scatter.cpp b/src/frontends/pytorch/src/op/scatter.cpp index e8daa4d7246b..6e75ba123423 100644 --- a/src/frontends/pytorch/src/op/scatter.cpp +++ b/src/frontends/pytorch/src/op/scatter.cpp @@ -48,7 +48,7 @@ Output prepare_source(const NodeContext& context, return src_input_dtype; } -const v12::ScatterElementsUpdate::Reduction get_reduction_mode(const std::string& pt_reduce_mode) { +v12::ScatterElementsUpdate::Reduction get_reduction_mode(const std::string& pt_reduce_mode) { static const std::unordered_map TORCH_REDUCTION_TO_OV{ {"add", v12::ScatterElementsUpdate::Reduction::SUM}, {"multiply", v12::ScatterElementsUpdate::Reduction::PROD}, diff --git a/src/frontends/pytorch/src/op_table.cpp b/src/frontends/pytorch/src/op_table.cpp index 40c033b5d7d2..d98261aa2c2c 100644 --- a/src/frontends/pytorch/src/op_table.cpp +++ b/src/frontends/pytorch/src/op_table.cpp @@ -87,6 +87,7 @@ OP_CONVERTER(translate_elu); OP_CONVERTER(translate_embedding); OP_CONVERTER(translate_embedding_bag); OP_CONVERTER(translate_empty); +OP_CONVERTER(translate_empty_fx); OP_CONVERTER(translate_empty_like); OP_CONVERTER(translate_erf); OP_CONVERTER(translate_erfc); @@ -432,6 +433,7 @@ const std::unordered_map get_supported_ops_ts() { {"aten::argmin", op::translate_argmin}, {"aten::argsort", op::translate_argsort}, {"aten::as_strided", op::translate_as_strided}, + {"aten::as_strided_copy", op::translate_as_strided}, {"aten::as_tensor", op::translate_as_tensor}, {"aten::asin", op::optional_out, 1>}, {"aten::asin_", op::inplace_op>}, @@ -518,6 +520,7 @@ const std::unordered_map get_supported_ops_ts() { {"aten::exp", op::optional_out}, {"aten::exp_", op::inplace_op}, {"aten::expand", op::translate_expand}, + {"aten::expand_copy", op::translate_expand}, {"aten::expand_as", op::translate_expand_as}, {"aten::expm1", op::translate_expm1}, {"aten::eye", op::translate_eye}, @@ -666,6 +669,7 @@ const std::unordered_map get_supported_ops_ts() { {"aten::pad", op::translate_pad}, {"aten::pairwise_distance", op::translate_pairwise_distance}, {"aten::permute", op::translate_permute}, + {"aten::permute_copy", op::translate_permute}, {"aten::pixel_shuffle", op::translate_pixel_shuffle}, {"aten::pixel_unshuffle", op::translate_pixel_unshuffle}, {"aten::prelu", op::translate_1to1_match_2_inputs}, @@ -715,11 +719,13 @@ const std::unordered_map get_supported_ops_ts() { {"aten::rsub", op::translate_rsub}, {"aten::searchsorted", op::translate_search_sorted}, {"aten::ScalarImplicit", op::skip_node}, + {"aten::scalar_tensor", op::translate_scalar_tensor_fx}, {"aten::scaled_dot_product_attention", op::translate_scaled_dot_product_attention}, {"aten::scatter", op::translate_scatter}, {"aten::scatter_add", op::translate_scatter_add}, {"aten::scatter_reduce", op::translate_scatter_reduce}, {"aten::select", op::quantizable_op}, + {"aten::select_copy", op::quantizable_op}, {"aten::selu", op::translate_selu}, {"aten::sigmoid", op::optional_out, 1>}, @@ -739,10 +745,12 @@ const std::unordered_map get_supported_ops_ts() { op::optional_out, 1>}, // aten::split - Supported in limited set of patterns {"aten::split_with_sizes", op::translate_split_with_sizes}, + {"aten::split_with_sizes_copy", op::translate_split_with_sizes}, {"aten::sqrt", op::optional_out, 1>}, {"aten::sqrt_", op::inplace_op>}, {"aten::square", op::translate_square}, {"aten::squeeze", op::quantizable_op}, + {"aten::squeeze_copy", op::quantizable_op}, {"aten::stack", op::translate_stack}, {"aten::std", op::translate_std}, {"aten::std_mean", op::translate_std_mean}, @@ -771,6 +779,7 @@ const std::unordered_map get_supported_ops_ts() { {"aten::unfold", op::translate_unfold}, // aten::unsafe_chunk - Supported in limited set of patterns {"aten::unsqueeze", common_translators::translate_unsqueeze}, + {"aten::unsqueeze_copy", common_translators::translate_unsqueeze}, {"aten::upsample_bicubic2d", op::translate_upsample_bicubic2d}, {"aten::upsample_bilinear2d", op::translate_upsample_bilinear2d}, {"aten::upsample_linear1d", op::translate_upsample_linear1d}, @@ -781,6 +790,7 @@ const std::unordered_map get_supported_ops_ts() { {"aten::var", op::translate_var}, {"aten::var_mean", op::translate_var_mean}, {"aten::view", op::quantizable_op}, + {"aten::view_copy", op::quantizable_op}, {"aten::view_as", op::translate_reshape_as}, {"aten::view_as_complex", op::translate_view_as_complex}, {"aten::view_as_real", op::translate_view_as_real}, @@ -938,7 +948,7 @@ const std::unordered_map get_supported_ops_fx() { {"aten.elu.default", op::translate_elu}, {"aten.elu_.default", op::inplace_op}, {"aten.embedding.default", op::translate_embedding}, - {"aten.empty.memory_format", op::translate_empty}, + {"aten.empty.memory_format", op::translate_empty_fx}, {"aten.eq.Scalar", op::translate_1to1_match_2_inputs_align_types}, {"aten.eq.Tensor", op::translate_1to1_match_2_inputs_align_types}, {"aten.erf.default", op::translate_erf}, diff --git a/src/frontends/pytorch/src/transforms/append_list_unpack_replacer.cpp b/src/frontends/pytorch/src/transforms/append_list_unpack_replacer.cpp index 7bfec85159e4..2b5e69bf0734 100644 --- a/src/frontends/pytorch/src/transforms/append_list_unpack_replacer.cpp +++ b/src/frontends/pytorch/src/transforms/append_list_unpack_replacer.cpp @@ -60,8 +60,16 @@ AppendListUnpackReplacer::AppendListUnpackReplacer() { return false; } auto index = index_val[0]; - if (index_val[0] < 0) { - index = inputs.size() + index; + auto num_inputs = static_cast(inputs.size()); + if (index < -num_inputs || index >= num_inputs) { + add_exception_to_fw_node(list_unpack, + "prim::ListUnpack: aten::__getitem__ index " + std::to_string(index_val[0]) + + " is out of bounds for input list with " + std::to_string(num_inputs) + + " elements."); + return false; + } + if (index < 0) { + index += num_inputs; } auto axis_0 = v0::Constant::create(element::i32, Shape{}, {0}); auto split = std::make_shared(inputs[index], axis_0, list_unpack->get_output_size()); diff --git a/src/frontends/pytorch/src/transforms/aten_getitem_replacer.cpp b/src/frontends/pytorch/src/transforms/aten_getitem_replacer.cpp index 489e9e9459b2..4ddfca54aefd 100644 --- a/src/frontends/pytorch/src/transforms/aten_getitem_replacer.cpp +++ b/src/frontends/pytorch/src/transforms/aten_getitem_replacer.cpp @@ -72,6 +72,49 @@ AtenGetItemReplacer::AtenGetItemReplacer() { auto axis_1d = rg.make(axis, const_0); auto getitem_idx = getitem->input(1).get_source_output(); + // Static OOB check when both index and input shape are known at conversion time. + auto getitem_idx_const = ov::util::get_constant_from_source(getitem_idx); + auto input_pshape = input.get_partial_shape(); + auto axis_const = ov::util::get_constant_from_source(axis); + auto split_size_const = ov::util::get_constant_from_source(split_size); + if (getitem_idx_const && axis_const && split_size_const && input_pshape.rank().is_static()) { + auto axis_const_1d = axis_const->cast_vector(); + if (axis_const_1d.size() != 1) { + add_exception_to_fw_node(getitem, "aten::__getitem__ axis is not scalar."); + return false; + } + auto axis_val = axis_const_1d[0]; + auto input_rank = static_cast(input_pshape.rank().get_length()); + if (input_rank < 0) { // NOTE: Very unlikely, but staying on the safe side + add_exception_to_fw_node(getitem, "aten::__getitem__ input_rank overflow."); + return false; + } + if (axis_val < 0) + axis_val += input_rank; + if (axis_val >= 0 && axis_val < input_rank && input_pshape[axis_val].is_static()) { + auto dim_size = input_pshape[axis_val].get_length(); + auto split_size_const_1d = split_size_const->cast_vector(); + if (split_size_const_1d.size() != 1) { + add_exception_to_fw_node(getitem, "aten::__getitem__ split_size is not scalar."); + return false; + } + auto split_size = split_size_const_1d[0]; + if (split_size <= 0) { + add_exception_to_fw_node(getitem, "aten::__getitem__ split_size must be positive."); + return false; + } + auto num_splits = (dim_size + split_size - 1) / split_size; + auto idx_val = getitem_idx_const->cast_vector()[0]; + if (idx_val < -num_splits || idx_val >= num_splits) { + add_exception_to_fw_node(getitem, + "aten::__getitem__: index " + std::to_string(idx_val) + + " is out of bounds for split with " + + std::to_string(num_splits) + " outputs."); + return false; + } + } + } + // Calculate number of splits based on input shape and split_size. auto shape = rg.make(input, element::i32); auto len_to_split = rg.make(shape, axis, const_0); @@ -108,8 +151,16 @@ AtenGetItemReplacer::AtenGetItemReplacer() { return false; } auto index = index_val[0]; + auto num_outputs = static_cast(split->outputs().size()); + if (index < -num_outputs || index >= num_outputs) { + add_exception_to_fw_node(getitem, + "aten::__getitem__: index " + std::to_string(index_val[0]) + + " is out of bounds for split with " + std::to_string(num_outputs) + + " outputs."); + return false; + } if (index < 0) { - index = split->outputs().size() + index; + index += num_outputs; } getitem->output(0).replace(split->outputs()[index]); } @@ -119,7 +170,24 @@ AtenGetItemReplacer::AtenGetItemReplacer() { auto getitem_idx_const = ov::util::get_constant_from_source(getitem_idx); if (getitem_idx_const) { auto idx = getitem_idx_const->cast_vector(); - getitem->output(0).replace(seq_mark->input_value(idx[0])); + if (idx.size() != 1) { + add_exception_to_fw_node(getitem, "aten::__getitem__ index is not scalar."); + return false; + } + auto idx_val = idx[0]; + auto inputs = seq_mark->get_sequence(); + auto num_inputs = static_cast(inputs.size()); + if (idx_val < -num_inputs || idx_val >= num_inputs) { + add_exception_to_fw_node(getitem, + "aten::__getitem__: index " + std::to_string(idx[0]) + + " is out of bounds for sequence with " + std::to_string(num_inputs) + + " inputs."); + return false; + } + if (idx_val < 0) { + idx_val += num_inputs; + } + getitem->output(0).replace(inputs[idx_val]); } else { auto input_concat = concat_list_construct(seq_mark); auto zero = v0::Constant::create(element::i32, Shape{}, {0}); diff --git a/src/frontends/pytorch/src/utils_quantize.hpp b/src/frontends/pytorch/src/utils_quantize.hpp index 855c4ce6f7ef..5f59e1278791 100644 --- a/src/frontends/pytorch/src/utils_quantize.hpp +++ b/src/frontends/pytorch/src/utils_quantize.hpp @@ -100,7 +100,7 @@ class QuantizedPtNode : public PtFrameworkNode { } return input_value(3); } - const QuantizedPtNodeType get_type() const { + QuantizedPtNodeType get_type() const { return type; } const element::Type get_dtype() const { diff --git a/src/frontends/tensorflow/docs/supported_ops.md b/src/frontends/tensorflow/docs/supported_ops.md index acab089a6083..7d16c5a6f961 100644 --- a/src/frontends/tensorflow/docs/supported_ops.md +++ b/src/frontends/tensorflow/docs/supported_ops.md @@ -1368,7 +1368,7 @@ A "supported operation" is one that TensorFlow Frontend can convert to the OpenV | Unpack | YES | | | UnravelIndex | YES | | | UnsortedSegmentJoin | NO | | -| UnsortedSegmentMax | NO | | +| UnsortedSegmentMax | YES | | | UnsortedSegmentMin | NO | | | UnsortedSegmentProd | NO | | | UnsortedSegmentSum | YES | | diff --git a/src/frontends/tensorflow/src/checkpoint_v1_reader.cpp b/src/frontends/tensorflow/src/checkpoint_v1_reader.cpp index d8ac5892f650..fe1e8704d765 100644 --- a/src/frontends/tensorflow/src/checkpoint_v1_reader.cpp +++ b/src/frontends/tensorflow/src/checkpoint_v1_reader.cpp @@ -17,13 +17,13 @@ using namespace ov::frontend::tensorflow; namespace { -std::vector list_files_in_dir(const std::string& directory_path) { - std::vector res; +std::vector list_files_in_dir(const std::filesystem::path& directory_path) { + std::vector res; try { ov::util::iterate_files( - ov::util::make_path(directory_path), + directory_path, [&res](const std::filesystem::path& file_path) { - res.push_back(ov::util::path_to_string(file_path)); + res.push_back(file_path); }, true); } catch (...) { @@ -33,11 +33,11 @@ std::vector list_files_in_dir(const std::string& directory_path) { } } // namespace -CheckpointV1Reader::CheckpointV1Reader(const std::string& checkpoints) : m_checkpoints(checkpoints) {} +CheckpointV1Reader::CheckpointV1Reader(const std::filesystem::path& checkpoints) : m_checkpoints(checkpoints) {} void CheckpointV1Reader::initialize() { // figure out if the input is a file or a directory of checkpoints - std::vector checkpoints_paths; + std::vector checkpoints_paths; if (ov::util::directory_exists(m_checkpoints)) { checkpoints_paths = list_files_in_dir(m_checkpoints); } else if (ov::util::file_exists(m_checkpoints)) { @@ -48,18 +48,20 @@ void CheckpointV1Reader::initialize() { m_variables_info_map.clear(); - for (auto checkpoint_path : checkpoints_paths) { + for (const auto& checkpoint_path : checkpoints_paths) { // create ifstream for each shard std::shared_ptr shard_stream = std::make_shared(checkpoint_path, std::ifstream::in | std::ifstream::binary); - FRONT_END_GENERAL_CHECK( - shard_stream && shard_stream->is_open(), - "[TensorFlow Frontend] incorrect model: checkpoint file " + checkpoint_path + "does not exist"); + FRONT_END_GENERAL_CHECK(shard_stream && shard_stream->is_open(), + "[TensorFlow Frontend] incorrect model: checkpoint file ", + checkpoint_path, + " does not exist"); const int32_t shard_ind = static_cast(m_shards.size()); m_shards.push_back(shard_stream); - m_shard_names.push_back(checkpoint_path); + const std::string shard_name = ov::util::path_to_string(checkpoint_path); + m_shard_names.push_back(shard_name); std::string value; - find_entry(shard_stream, checkpoint_path, SAVED_TENSOR_SLICES_KEY, value); + find_entry(shard_stream, shard_name, SAVED_TENSOR_SLICES_KEY, value); // parse empty index block // This is only present at the first item of each checkpoint file and serves diff --git a/src/frontends/tensorflow/src/checkpoint_v1_reader.hpp b/src/frontends/tensorflow/src/checkpoint_v1_reader.hpp index 168185ab2996..70d2606135f9 100644 --- a/src/frontends/tensorflow/src/checkpoint_v1_reader.hpp +++ b/src/frontends/tensorflow/src/checkpoint_v1_reader.hpp @@ -6,6 +6,7 @@ #include +#include #include #include @@ -31,7 +32,7 @@ struct VariableInfo { // reads checkpoints of v1 version // it parses value, shape and type for Variable nodes class CheckpointV1Reader { - const std::string m_checkpoints; + const std::filesystem::path m_checkpoints; // a map from Variable name to its information std::unordered_map m_variables_info_map; // a vector of streams for shards, where shard is one checkpoint file @@ -41,8 +42,7 @@ class CheckpointV1Reader { public: /// \brief constructs CheckpointV1Reader for a given directory of checkpoint files - // CheckpointV1Reader(const std::string& checkpoints_dir); - CheckpointV1Reader(const std::string& checkpoints); + CheckpointV1Reader(const std::filesystem::path& checkpoints); /// \brief initialize Checkpoint V1 reader void initialize(); diff --git a/src/frontends/tensorflow/src/frontend.cpp b/src/frontends/tensorflow/src/frontend.cpp index 20762bd98575..65ebc0c0c46e 100644 --- a/src/frontends/tensorflow/src/frontend.cpp +++ b/src/frontends/tensorflow/src/frontend.cpp @@ -142,21 +142,20 @@ bool FrontEnd::supported_impl(const std::vector& variants) const { if (variants.size() != 1 + extra_variants_num) return false; - std::filesystem::path model_fs_path, checkpoints_dir; - if (const auto path = ov::frontend::get_path_from_any(variants[0])) { - model_fs_path = path.value(); - } else if (const auto paths = ov::frontend::get_path_vec_from_any(variants[0])) { + std::filesystem::path model_path, checkpoints_or_tags; + if (auto path = ov::frontend::get_path_from_any(variants[0])) { + model_path = std::move(*path); + } else if (auto paths = ov::frontend::get_path_vec_from_any(variants[0])) { if (paths->size() == 2) { - model_fs_path = paths.value()[0]; - checkpoints_dir = paths.value()[1]; + model_path = std::move((*paths)[0]); + checkpoints_or_tags = std::move((*paths)[1]); } } // to figure out if the model with v1 checkpoints is supported, // it is sufficient to check only the input model format // avoid parsing of checkpoints here - if (!model_fs_path.empty() && checkpoints_dir.empty()) { - auto model_path = model_fs_path.native(); + if (!model_path.empty() && checkpoints_or_tags.empty()) { if (GraphIteratorProto::is_supported(model_path)) { // handle binary protobuf format // for automatic deduction of the frontend to convert the model @@ -170,8 +169,7 @@ bool FrontEnd::supported_impl(const std::vector& variants) const { // handle text protobuf format return true; } - } else if (!model_fs_path.empty() && !checkpoints_dir.empty()) { - auto model_path = model_fs_path.native(); + } else if (!model_path.empty() && !checkpoints_or_tags.empty()) { // here, we assume to get the input model path and checkpoints directory if (GraphIteratorProto::is_supported(model_path)) { // binary protobuf format with checkpoints @@ -202,17 +200,16 @@ ov::frontend::InputModel::Ptr FrontEnd::load_impl(const std::vector& va "frozen formats (.pb and .pbtxt), SavedModel and MetaGraph (.meta) formats, and v1 checkpoints."; FRONT_END_GENERAL_CHECK(variants.size() == 1 + extra_variants_num, err_msg); - std::filesystem::path model_fs_path, checkpoints_fs_dir; - if (const auto path = ov::frontend::get_path_from_any(variants[0])) { - model_fs_path = path.value(); - } else if (const auto paths = ov::frontend::get_path_vec_from_any(variants[0])) { + std::filesystem::path model_path, checkpoints_or_tags; + if (auto path = ov::frontend::get_path_from_any(variants[0])) { + model_path = std::move(*path); + } else if (auto paths = ov::frontend::get_path_vec_from_any(variants[0])) { FRONT_END_GENERAL_CHECK(paths->size() == 2, err_msg); - model_fs_path = paths.value()[0]; - checkpoints_fs_dir = paths.value()[1]; + model_path = std::move((*paths)[0]); + checkpoints_or_tags = std::move((*paths)[1]); } - if (!model_fs_path.empty() && checkpoints_fs_dir.empty()) { - const auto model_path = model_fs_path.native(); + if (!model_path.empty() && checkpoints_or_tags.empty()) { if (GraphIteratorProto::is_supported(model_path)) { // handle binary protobuf format return std::make_shared(std::make_shared(model_path), m_telemetry); @@ -243,10 +240,9 @@ ov::frontend::InputModel::Ptr FrontEnd::load_impl(const std::vector& va // handle text protobuf format return std::make_shared(std::make_shared(model_path), m_telemetry); } - } else if (!model_fs_path.empty() && !checkpoints_fs_dir.empty()) { + } else if (!model_path.empty() && !checkpoints_or_tags.empty()) { // here, we assume to get the input model path and checkpoints directory - const auto model_path = ov::util::path_to_string(model_fs_path); - const auto checkpoints_dir = ov::util::path_to_string(checkpoints_fs_dir); + const auto checkpoints_dir = checkpoints_or_tags; if (GraphIteratorProto::is_supported(model_path)) { auto graph_iterator = std::make_shared(model_path, checkpoints_dir); // handle binary protobuf format with checkpoints @@ -272,7 +268,7 @@ ov::frontend::InputModel::Ptr FrontEnd::load_impl(const std::vector& va graph_iterator->get_checkpoint_v1_reader(), false); } else if (GraphIteratorSavedModel::is_supported(model_path)) { - auto saved_model_tags = checkpoints_dir; + const auto saved_model_tags = ov::util::path_to_string(checkpoints_or_tags); std::shared_ptr graph_iterator; graph_iterator = std::make_shared(model_path, saved_model_tags, mmap_enabled); return std::make_shared(graph_iterator, diff --git a/src/frontends/tensorflow/src/graph_iterator_meta.cpp b/src/frontends/tensorflow/src/graph_iterator_meta.cpp index 8f069c56376a..d71b2771af86 100644 --- a/src/frontends/tensorflow/src/graph_iterator_meta.cpp +++ b/src/frontends/tensorflow/src/graph_iterator_meta.cpp @@ -29,18 +29,10 @@ bool GraphIteratorMeta::is_valid_signature(const ::tensorflow::SignatureDef& sig return true; } -template <> -std::basic_string get_variables_index_name(const std::string name) { - return name + ".index"; +std::filesystem::path get_variables_index_name(const std::filesystem::path& name) { + return std::filesystem::path(name) += ".index"; } -#if defined(OPENVINO_ENABLE_UNICODE_PATH_SUPPORT) && defined(_WIN32) -template <> -std::basic_string get_variables_index_name(const std::wstring name) { - return name + L".index"; -} -#endif - } // namespace tensorflow } // namespace frontend } // namespace ov diff --git a/src/frontends/tensorflow/src/graph_iterator_meta.hpp b/src/frontends/tensorflow/src/graph_iterator_meta.hpp index 3c4e3174f08c..f409b009e848 100644 --- a/src/frontends/tensorflow/src/graph_iterator_meta.hpp +++ b/src/frontends/tensorflow/src/graph_iterator_meta.hpp @@ -4,6 +4,7 @@ #pragma once +#include #include #include "graph_iterator_proto.hpp" @@ -14,16 +15,7 @@ namespace ov { namespace frontend { namespace tensorflow { -template -std::basic_string get_variables_index_name(const std::basic_string name) {} - -template <> -std::basic_string get_variables_index_name(const std::string name); - -#if defined(OPENVINO_ENABLE_UNICODE_PATH_SUPPORT) && defined(_WIN32) -template <> -std::basic_string get_variables_index_name(const std::wstring name); -#endif +std::filesystem::path get_variables_index_name(const std::filesystem::path& name); // Loads graph from Tensorflow MetaGraph file (*.meta) class GraphIteratorMeta : public GraphIteratorProto { @@ -36,21 +28,18 @@ class GraphIteratorMeta : public GraphIteratorProto { bool m_mmap_enabled; public: - template - GraphIteratorMeta(const std::basic_string& path, const bool mmap_enabled) + GraphIteratorMeta(const std::filesystem::path& path, const bool mmap_enabled) : m_metagraph_def(std::make_shared<::tensorflow::MetaGraphDef>()), m_mmap_enabled(mmap_enabled) { this->read_meta(path); } - template - static bool is_supported(const std::basic_string& path) { + static bool is_supported(const std::filesystem::path& path) { FRONT_END_GENERAL_CHECK(util::directory_exists(path) || util::file_exists(path), - "Could not open the file: \"", - util::path_to_string(path), - '"'); + "Could not open the file: ", + path); try { - std::ifstream mg_stream(path.c_str(), std::ios::in | std::ifstream::binary); + std::ifstream mg_stream(path, std::ios::in | std::ifstream::binary); auto metagraph_def = std::make_shared<::tensorflow::MetaGraphDef>(); return mg_stream && mg_stream.is_open() && metagraph_def->ParsePartialFromIstream(&mg_stream) && metagraph_def->has_graph_def() && metagraph_def->graph_def().node_size() > 0; @@ -82,17 +71,17 @@ class GraphIteratorMeta : public GraphIteratorProto { private: bool is_valid_signature(const ::tensorflow::SignatureDef& signature) const; - template - bool read_meta(const std::basic_string& path) { - std::basic_string model_path = path.substr(0, path.find_last_of('.')); + bool read_meta(const std::filesystem::path& path) { + auto model_path = path; + model_path.replace_extension(); - std::ifstream mg_stream{path.c_str(), std::ifstream::in | std::ifstream::binary}; + std::ifstream mg_stream{path, std::ifstream::in | std::ifstream::binary}; FRONT_END_GENERAL_CHECK(mg_stream && mg_stream.is_open(), "Model file does not exist"); - std::basic_string varIndexPath = get_variables_index_name(model_path); + auto varIndexPath = get_variables_index_name(model_path); if (ov::util::file_exists(varIndexPath)) { m_variables_index = std::make_shared(m_mmap_enabled); - std::ifstream vi_stream{varIndexPath.c_str(), std::ifstream::in | std::ifstream::binary}; + std::ifstream vi_stream{varIndexPath, std::ifstream::in | std::ifstream::binary}; FRONT_END_GENERAL_CHECK(vi_stream && vi_stream.is_open(), "MetaGraph's variable index file does not exist"); FRONT_END_GENERAL_CHECK(m_variables_index->read_variables(vi_stream, model_path, false), "MetaGraph's variable index file cannot be parsed"); diff --git a/src/frontends/tensorflow/src/graph_iterator_proto.hpp b/src/frontends/tensorflow/src/graph_iterator_proto.hpp index 966fe8e631a8..37ce202f095d 100644 --- a/src/frontends/tensorflow/src/graph_iterator_proto.hpp +++ b/src/frontends/tensorflow/src/graph_iterator_proto.hpp @@ -4,10 +4,8 @@ #pragma once +#include #include -#if defined(__MINGW32__) || defined(__MINGW64__) -# include -#endif #include #include "checkpoint_v1_reader.hpp" @@ -59,8 +57,7 @@ class GraphIteratorProto : public GraphIterator { } } - template - void initialize_v1_checkpoints(const std::basic_string& checkpoint_directory) { + void initialize_v1_checkpoints(const std::filesystem::path& checkpoint_directory) { m_checkpoint_v1_reader = std::make_shared(checkpoint_directory); m_checkpoint_v1_reader->initialize(); } @@ -107,16 +104,11 @@ class GraphIteratorProto : public GraphIterator { } /// \brief Construct GraphIterator for the frozen model without v1 checkpoints - template - GraphIteratorProto(const std::basic_string& model_path) + GraphIteratorProto(const std::filesystem::path& model_path) : m_graph_def(std::make_shared<::tensorflow::GraphDef>()), m_func_def(nullptr), m_checkpoint_v1_reader(nullptr) { -#if defined(__MINGW32__) || defined(__MINGW64__) - std::ifstream pb_stream(std::filesystem::path(model_path), std::ios::in | std::ifstream::binary); -#else std::ifstream pb_stream(model_path, std::ios::in | std::ifstream::binary); -#endif FRONT_END_GENERAL_CHECK(pb_stream && pb_stream.is_open(), "Model file does not exist"); FRONT_END_GENERAL_CHECK(m_graph_def->ParseFromIstream(&pb_stream), "Model cannot be parsed"); @@ -125,8 +117,7 @@ class GraphIteratorProto : public GraphIterator { } /// \brief Construct GraphIterator for the frozen model with v1 checkpoints - template - GraphIteratorProto(const std::basic_string& model_path, const std::basic_string& checkpoint_directory) + GraphIteratorProto(const std::filesystem::path& model_path, const std::filesystem::path& checkpoint_directory) : m_graph_def(std::make_shared<::tensorflow::GraphDef>()), m_func_def(nullptr), m_checkpoint_v1_reader(nullptr) { @@ -140,18 +131,12 @@ class GraphIteratorProto : public GraphIterator { } /// \brief Check if the input file is supported - template - static bool is_supported(const std::basic_string& path) { + static bool is_supported(const std::filesystem::path& path) { FRONT_END_GENERAL_CHECK(util::directory_exists(path) || util::file_exists(path), - "Could not open the file: \"", - util::path_to_string(path), - '"'); + "Could not open the file: ", + path); try { -#if defined(__MINGW32__) || defined(__MINGW64__) - std::ifstream pb_stream(std::filesystem::path(path), std::ios::in | std::ifstream::binary); -#else std::ifstream pb_stream(path, std::ios::in | std::ifstream::binary); -#endif auto graph_def = std::make_shared<::tensorflow::GraphDef>(); return pb_stream && pb_stream.is_open() && graph_def->ParsePartialFromIstream(&pb_stream) && graph_def->node_size() > 0; diff --git a/src/frontends/tensorflow/src/graph_iterator_proto_txt.hpp b/src/frontends/tensorflow/src/graph_iterator_proto_txt.hpp index 7922939116f5..7c65970d21b3 100644 --- a/src/frontends/tensorflow/src/graph_iterator_proto_txt.hpp +++ b/src/frontends/tensorflow/src/graph_iterator_proto_txt.hpp @@ -4,10 +4,8 @@ #pragma once +#include #include -#if defined(__MINGW32__) || defined(__MINGW64__) -# include -#endif #include "google/protobuf/io/zero_copy_stream_impl.h" #include "google/protobuf/text_format.h" @@ -22,13 +20,8 @@ namespace tensorflow { class GraphIteratorProtoTxt : public GraphIteratorProto { public: /// \brief Construct GraphIterator for the frozen model in text format without v1 checkpoints - template - GraphIteratorProtoTxt(const std::basic_string& path) : GraphIteratorProto() { -#if defined(__MINGW32__) || defined(__MINGW64__) - std::ifstream pbtxt_stream(std::filesystem::path(path), std::ios::in); -#else + GraphIteratorProtoTxt(const std::filesystem::path& path) : GraphIteratorProto() { std::ifstream pbtxt_stream(path, std::ios::in); -#endif FRONT_END_GENERAL_CHECK(pbtxt_stream && pbtxt_stream.is_open(), "Model file does not exist"); auto input_stream = std::make_shared<::google::protobuf::io::IstreamInputStream>(&pbtxt_stream); FRONT_END_GENERAL_CHECK(input_stream, "Model cannot be read"); @@ -41,8 +34,7 @@ class GraphIteratorProtoTxt : public GraphIteratorProto { } /// \brief Construct GraphIterator for the frozen model in text format with v1 checkpoints - template - GraphIteratorProtoTxt(const std::basic_string& path, const std::basic_string& checkpoint_directory) + GraphIteratorProtoTxt(const std::filesystem::path& path, const std::filesystem::path& checkpoint_directory) : GraphIteratorProto() { std::ifstream pbtxt_stream(path, std::ios::in); FRONT_END_GENERAL_CHECK(pbtxt_stream && pbtxt_stream.is_open(), "Model file does not exist"); @@ -58,14 +50,12 @@ class GraphIteratorProtoTxt : public GraphIteratorProto { } /// \brief Check if the input file is supported - template - static bool is_supported(const std::basic_string& path) { + static bool is_supported(const std::filesystem::path& path) { FRONT_END_GENERAL_CHECK(util::directory_exists(path) || util::file_exists(path), - "Could not open the file: \"", - util::path_to_string(path), - '"'); + "Could not open the file: ", + path); try { - std::ifstream pbtxt_stream(path.c_str(), std::ios::in); + std::ifstream pbtxt_stream(path, std::ios::in); bool model_exists = (pbtxt_stream && pbtxt_stream.is_open()); if (!model_exists) { return false; diff --git a/src/frontends/tensorflow/src/graph_iterator_saved_model.cpp b/src/frontends/tensorflow/src/graph_iterator_saved_model.cpp index 717ae5c5a029..37715b98a58c 100644 --- a/src/frontends/tensorflow/src/graph_iterator_saved_model.cpp +++ b/src/frontends/tensorflow/src/graph_iterator_saved_model.cpp @@ -29,43 +29,24 @@ bool GraphIteratorSavedModel::is_valid_signature(const ::tensorflow::SignatureDe return true; } -bool GraphIteratorSavedModel::is_supported(const std::string& path) { +bool GraphIteratorSavedModel::is_supported(const std::filesystem::path& path) { if (ov::util::directory_exists(path)) { - FRONT_END_GENERAL_CHECK(util::file_exists(ov::util::path_join({path, "saved_model.pb"})), - "Could not open the file: \"", - ov::util::path_join({path, "saved_model.pb"}), - '"'); + FRONT_END_GENERAL_CHECK(util::file_exists(path / "saved_model.pb"), + "Could not open the file: ", + path / "saved_model.pb"); return true; } else { return false; } } -#if defined(OPENVINO_ENABLE_UNICODE_PATH_SUPPORT) && defined(_WIN32) -bool GraphIteratorSavedModel::is_supported(const std::wstring& path) { - return ov::util::directory_exists(path) && ov::util::file_exists(ov::util::path_join_w({path, L"saved_model.pb"})); +std::filesystem::path get_saved_model_name() { + return "saved_model.pb"; } -#endif -template <> -std::basic_string get_saved_model_name() { - return "/saved_model.pb"; +std::filesystem::path get_variables_index_name() { + return std::filesystem::path("variables") / "variables.index"; } -template <> -std::basic_string get_variables_index_name() { - return "/variables/variables.index"; -} - -#if defined(OPENVINO_ENABLE_UNICODE_PATH_SUPPORT) && defined(_WIN32) -template <> -std::basic_string get_saved_model_name() { - return L"/saved_model.pb"; -} -template <> -std::basic_string get_variables_index_name() { - return L"/variables/variables.index"; -} -#endif std::vector GraphIteratorSavedModel::split_tags(const std::string tags) const { std::vector tag_list = {}; diff --git a/src/frontends/tensorflow/src/graph_iterator_saved_model.hpp b/src/frontends/tensorflow/src/graph_iterator_saved_model.hpp index 0a2ee5ac1a13..16a264a06f12 100644 --- a/src/frontends/tensorflow/src/graph_iterator_saved_model.hpp +++ b/src/frontends/tensorflow/src/graph_iterator_saved_model.hpp @@ -4,6 +4,7 @@ #pragma once +#include #include #include "graph_iterator_proto.hpp" @@ -15,22 +16,8 @@ namespace ov { namespace frontend { namespace tensorflow { -template -std::basic_string get_saved_model_name() {} -template -std::basic_string get_variables_index_name() {} - -template <> -std::basic_string get_saved_model_name(); -template <> -std::basic_string get_variables_index_name(); - -#if defined(OPENVINO_ENABLE_UNICODE_PATH_SUPPORT) && defined(_WIN32) -template <> -std::basic_string get_saved_model_name(); -template <> -std::basic_string get_variables_index_name(); -#endif +std::filesystem::path get_saved_model_name(); +std::filesystem::path get_variables_index_name(); // Loads graph from Tensorflow Saved Model file (saved_model.pb) class GraphIteratorSavedModel : public GraphIteratorProto { @@ -43,17 +30,13 @@ class GraphIteratorSavedModel : public GraphIteratorProto { bool m_mmap_enabled; public: - template - GraphIteratorSavedModel(const std::basic_string& path, const std::string& tags, const bool mmap_enabled) + GraphIteratorSavedModel(const std::filesystem::path& path, const std::string& tags, const bool mmap_enabled) : m_saved_model(std::make_shared<::tensorflow::SavedModel>()), m_mmap_enabled(mmap_enabled) { this->read_saved_model(path, tags); } - static bool is_supported(const std::string& path); -#if defined(OPENVINO_ENABLE_UNICODE_PATH_SUPPORT) && defined(_WIN32) - static bool is_supported(const std::wstring& path); -#endif + static bool is_supported(const std::filesystem::path& path); std::shared_ptr get_variables_index() { return m_variables_index; @@ -78,16 +61,15 @@ class GraphIteratorSavedModel : public GraphIteratorProto { private: bool is_valid_signature(const ::tensorflow::SignatureDef& signature) const; - template - bool read_saved_model(const std::basic_string& path, const std::string& tags) { - std::basic_string save_model_path = path + get_saved_model_name(); - std::ifstream sm_stream{save_model_path.c_str(), std::ifstream::in | std::ifstream::binary}; + bool read_saved_model(const std::filesystem::path& path, const std::string& tags) { + const auto save_model_path = path / get_saved_model_name(); + std::ifstream sm_stream{save_model_path, std::ifstream::in | std::ifstream::binary}; FRONT_END_GENERAL_CHECK(sm_stream && sm_stream.is_open(), "[TensorFlow Frontend] Model file does not exist"); - std::basic_string varIndexPath = path + get_variables_index_name(); + const auto varIndexPath = path / get_variables_index_name(); if (ov::util::file_exists(varIndexPath)) { m_variables_index = std::make_shared(m_mmap_enabled); - std::ifstream vi_stream{varIndexPath.c_str(), std::ifstream::in | std::ifstream::binary}; + std::ifstream vi_stream{varIndexPath, std::ifstream::in | std::ifstream::binary}; FRONT_END_GENERAL_CHECK(vi_stream && vi_stream.is_open(), "[TensorFlow Frontend] Saved Model's variable index file does not exist"); FRONT_END_GENERAL_CHECK(m_variables_index->read_variables(vi_stream, path), diff --git a/src/frontends/tensorflow/src/op_table.cpp b/src/frontends/tensorflow/src/op_table.cpp index 8ca7babc75f2..3268d0c8f896 100644 --- a/src/frontends/tensorflow/src/op_table.cpp +++ b/src/frontends/tensorflow/src/op_table.cpp @@ -430,6 +430,7 @@ const std::map get_supported_ops() { {"UniqueWithCounts", CreatorFunction(translate_unique_with_counts_op)}, {"Unpack", CreatorFunction(translate_unpack_op)}, {"UnravelIndex", CreatorFunction(translate_unravel_index_op)}, + {"UnsortedSegmentMax", CreatorFunction(translate_unsorted_segment_max_op)}, {"UnsortedSegmentSum", CreatorFunction(translate_unsorted_segment_sum_op)}, {"While", CreatorFunction(translate_while_op)}, {"Where", CreatorFunction(translate_where_op)}, diff --git a/src/frontends/tensorflow/src/parse_output_index.hpp b/src/frontends/tensorflow/src/parse_output_index.hpp new file mode 100644 index 000000000000..528c3017c19e --- /dev/null +++ b/src/frontends/tensorflow/src/parse_output_index.hpp @@ -0,0 +1,48 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace ov { +namespace frontend { +namespace tensorflow { + +// Strict integer parser for the output-index suffix of a TF node reference +// like "RestoreV2:3". Returns nullopt for empty / non-numeric / trailing +// garbage / overflow / out-of-int-range tokens; the caller decides how to +// react (e.g. throw a frontend exception). Uses strtoll + std::int64_t so +// the int-range check is meaningful on platforms where long aliases int. +inline std::optional parse_output_index(const std::string& token) { + if (token.empty()) { + return std::nullopt; + } + // strtoll otherwise silently skips leading whitespace; well-formed TF node + // references never contain it, so reject it as malformed up front. + if (std::isspace(static_cast(token.front()))) { + return std::nullopt; + } + char* end = nullptr; + errno = 0; + constexpr auto base = 10; + const std::int64_t v = std::strtoll(token.c_str(), &end, base); + if (end == token.c_str() || *end != '\0' || errno == ERANGE) { + return std::nullopt; + } + if (v < std::numeric_limits::min() || v > std::numeric_limits::max()) { + return std::nullopt; + } + return static_cast(v); +} + +} // namespace tensorflow +} // namespace frontend +} // namespace ov diff --git a/src/frontends/tensorflow/src/variables_index.cpp b/src/frontends/tensorflow/src/variables_index.cpp index d269b91ef907..604bc6f8df8f 100644 --- a/src/frontends/tensorflow/src/variables_index.cpp +++ b/src/frontends/tensorflow/src/variables_index.cpp @@ -2,8 +2,6 @@ // SPDX-License-Identifier: Apache-2.0 // -#include - #include #include @@ -14,6 +12,7 @@ #include "openvino/util/mmap_object.hpp" #include "ov_tensorflow/tensor_bundle.pb.h" #include "ov_tensorflow/trackable_object_graph.pb.h" +#include "parse_output_index.hpp" #include "tf_utils.hpp" #ifdef ENABLE_SNAPPY_COMPRESSION @@ -24,6 +23,13 @@ namespace ov { namespace frontend { namespace tensorflow { +namespace { +// RestoreV2 data inputs (per the TF Op definition): prefix(0), tensor_names(1), +// shape_and_slices(2). Control inputs ('^'-prefixed) live in the same input list +// and are filtered out by the data-port check below. +constexpr int tensor_names_index = 1; +} // namespace + void VariablesIndex::read_variables_index_block(std::ifstream& fs, const VIBlock& index, std::vector& data, @@ -206,7 +212,9 @@ void VariablesIndex::read_checkpointable_object_graph() { } } -bool VariablesIndex::read_variables(std::ifstream& vi_stream, const std::string& path, const bool is_saved_model) { +bool VariablesIndex::read_variables(std::ifstream& vi_stream, + const std::filesystem::path& path, + const bool is_saved_model) { m_variables_index.clear(); read_variables_index(vi_stream, m_variables_index); read_bundle_header(); @@ -216,9 +224,11 @@ bool VariablesIndex::read_variables(std::ifstream& vi_stream, const std::string& std::snprintf(suffix.data(), suffix.size(), "data-%05d-of-%05d", shard, m_total_shards); std::filesystem::path fullPath; if (is_saved_model) { - fullPath = ov::util::path_join({path, "variables", std::string("variables.") + suffix.data()}); + fullPath = path / "variables" / (std::string("variables.") + suffix.data()); } else { - fullPath = ov::util::make_path(path + "." + suffix.data()); + fullPath = path; + fullPath += "."; + fullPath += suffix.data(); } if (m_mmap_enabled) { m_data_files[shard].mmap = load_mmap_object(fullPath); @@ -234,36 +244,6 @@ bool VariablesIndex::read_variables(std::ifstream& vi_stream, const std::string& return true; } -#if defined(OPENVINO_ENABLE_UNICODE_PATH_SUPPORT) && defined(_WIN32) -bool VariablesIndex::read_variables(std::ifstream& vi_stream, const std::wstring& path, const bool is_saved_model) { - m_variables_index.clear(); - read_variables_index(vi_stream, m_variables_index); - read_bundle_header(); - - std::vector suffix(20); - for (int32_t shard = 0; shard < m_total_shards; ++shard) { - swprintf_s(suffix.data(), suffix.size(), L"data-%05d-of-%05d", shard, m_total_shards); - std::filesystem::path fullPath; - if (is_saved_model) { - fullPath = ov::util::path_join_w({path, L"variables", std::wstring(L"variables.") + suffix.data()}); - } else { - fullPath = path + L"." + suffix.data(); - } - if (m_mmap_enabled) { - m_data_files[shard].mmap = load_mmap_object(fullPath); - FRONT_END_GENERAL_CHECK(m_data_files[shard].mmap->data(), "Variable index data cannot be mapped"); - } else { - m_data_files[shard].stream = - std::shared_ptr(new std::ifstream(fullPath, std::ifstream::in | std::ifstream::binary)); - FRONT_END_GENERAL_CHECK(m_data_files[shard].stream->is_open(), "Variable index data file does not exist"); - } - } - - read_checkpointable_object_graph(); - return true; -} -#endif - struct PtrNode { using SharedPtrNode = std::shared_ptr; @@ -392,7 +372,19 @@ void VariablesIndex::map_assignvariable(const std::shared_ptr<::tensorflow::Grap std::map& variables_map, HashTableKeysValuesMap& hash_table_keys_map, HashTableKeysValuesMap& hash_table_values_map) { - std::map nodes; + using Nodes = std::map; + Nodes nodes; + + // Removing cross-links, otherwise memory leak will be caused by lost shared pointers + auto cross_link_deleter = [](Nodes* const nodes) { + for (auto& node : *nodes) { + node.second->inputs.clear(); + node.second->outputs.clear(); + } + nodes->clear(); + }; + + std::unique_ptr cross_link_guard(&nodes, cross_link_deleter); for (const auto& node : graph_def->node()) { auto shared_node = std::make_shared(node); @@ -404,6 +396,62 @@ void VariablesIndex::map_assignvariable(const std::shared_ptr<::tensorflow::Grap } } + const auto get_variable_name = [](const PtrNode::SharedPtrNode& rv2_node, int idx) -> std::string { + std::string tensor_name; + if (rv2_node->node->input().size() > tensor_names_index) { + const std::string& in = rv2_node->node->input(tensor_names_index); + if (!in.empty() && in[0] != '^') { + tensor_name = in; + } + } + FRONT_END_GENERAL_CHECK(!tensor_name.empty(), "RestoreV2 node is missing tensor_names input"); + std::vector parsed; + PtrNode::parse_node_name(tensor_name, parsed); + // Match by local node name on the rv2_node's already-linked inputs. Pointer-based: + // works in both top-level and StatefulPartitionedCall scopes (the global node dictionary + // uses prefixed keys for SPC-flattened nodes, but each PtrNode's NodeDef carries its + // local name in both cases). Also catches the case where associate_node compacted the + // inputs vector and inputs[1] silently refers to a non-tensor_names Const. + PtrNode::SharedPtrNode tn_ptr = nullptr; + for (const auto& input : rv2_node->inputs) { + if (input->node->name() == parsed[0]) { + tn_ptr = input; + break; + } + } + FRONT_END_GENERAL_CHECK(tn_ptr != nullptr, + "RestoreV2 tensor_names input not found among RestoreV2 inputs: ", + parsed[0]); + const auto* tn_node = tn_ptr->node; + const auto value_attr_it = tn_node->attr().find("value"); + FRONT_END_GENERAL_CHECK(tn_node->op() == "Const" && value_attr_it != tn_node->attr().end(), + "RestoreV2 tensor_names input is not a Const with 'value' attribute"); + const auto& tensor = value_attr_it->second.tensor(); + FRONT_END_GENERAL_CHECK(idx >= 0 && idx < tensor.string_val_size(), + "RestoreV2 output index out of range: ", + idx, + " (size=", + tensor.string_val_size(), + ")"); + return tensor.string_val(idx); + }; + + // Parse the integer suffix of a "RestoreV2:N" reference. The colon-less form + // (size < 2) implicitly refers to output 0 — preserved here as an explicit + // call-site default. Anything past the colon must be a strictly valid in-range + // integer; non-numeric, trailing garbage and overflow all throw via + // FRONT_END_GENERAL_CHECK below. + const auto resolve_output_index = [](const std::vector& restore_output) -> int { + if (restore_output.size() < 2) { + return 0; + } + const auto parsed = parse_output_index(restore_output.back()); + FRONT_END_GENERAL_CHECK(parsed.has_value(), + "RestoreV2 output index is not a valid integer: ", + restore_output.back()); + return *parsed; + }; + for (const auto& node : nodes) { if (node.second->op() == "AssignVariableOp") { // TODO: assets reading @@ -431,13 +479,10 @@ void VariablesIndex::map_assignvariable(const std::shared_ptr<::tensorflow::Grap FRONT_END_THROW("Unexpected topology near AssignVariableOp"); } - int output_index = std::atoi(restore_output[restore_output.size() - 1].c_str()); + const int output_index = resolve_output_index(restore_output); // Expected path is: Const(tensor_names) -(0)-(1)-> RestoreV2 - const auto& variable_name = - restorev2_nodes[0]->inputs[1]->node->attr().at("value").tensor().string_val(output_index); - - variables_map[varhandle_nodes[0]->node->name()] = variable_name; + variables_map[varhandle_nodes[0]->node->name()] = get_variable_name(restorev2_nodes[0], output_index); } } else if (node.second->op() == "Assign") { std::vector restorev2_nodes; @@ -459,13 +504,10 @@ void VariablesIndex::map_assignvariable(const std::shared_ptr<::tensorflow::Grap // Expected path is: RestoreV2 -(output_index)-(1)-> Assign PtrNode::parse_node_name(node.second->node->input(1), restore_output); - int output_index = std::atoi(restore_output[restore_output.size() - 1].c_str()); + const int output_index = resolve_output_index(restore_output); // Expected path is: Const(tensor_names) -(0)-(1)-> RestoreV2 - const auto& variable_name = - restorev2_nodes[0]->inputs[1]->node->attr().at("value").tensor().string_val(output_index); - - variables_map[variablev2_nodes[0]->node->name()] = variable_name; + variables_map[variablev2_nodes[0]->node->name()] = get_variable_name(restorev2_nodes[0], output_index); } } else if (node.second->op() == "LookupTableImportV2") { std::vector hash_tablev2_nodes; @@ -495,14 +537,6 @@ void VariablesIndex::map_assignvariable(const std::shared_ptr<::tensorflow::Grap hash_table_values_map[hash_tablev2_name] = values_const; } } - - // Removing cross-links, otherwise memory leak will be caused by lost shared pointers - for (auto node : nodes) { - node.second->inputs.clear(); - node.second->outputs.clear(); - } - - nodes.clear(); } } // namespace tensorflow diff --git a/src/frontends/tensorflow/src/variables_index.hpp b/src/frontends/tensorflow/src/variables_index.hpp index b120beba255c..a61c8b0a467c 100644 --- a/src/frontends/tensorflow/src/variables_index.hpp +++ b/src/frontends/tensorflow/src/variables_index.hpp @@ -4,6 +4,7 @@ #pragma once +#include #include #include "graph_iterator_proto.hpp" @@ -52,15 +53,7 @@ class VariablesIndex { /// \param path A path to file with variables data /// \param is_saved_model Flag shows variables index is a part of Saved Model format /// \returns Returns true in case of everything loads successfully, false otherwise - bool read_variables(std::ifstream& vi_stream, const std::string& path, const bool is_saved_model = true); -#if defined(OPENVINO_ENABLE_UNICODE_PATH_SUPPORT) && defined(_WIN32) - /// \brief Reads variables from opened variable index file. Can cause an asserts in case of issues. - /// \param vi_stream Opened stream file, file pointer doesn't matter, it will be rewind internally. - /// \param path A path to file with variables data - /// \param is_saved_model Flag shows variables index is a part of Saved Model format - /// \returns Returns true in case of everything loads successfully, false otherwise - bool read_variables(std::ifstream& vi_stream, const std::wstring& path, const bool is_saved_model = true); -#endif + bool read_variables(std::ifstream& vi_stream, const std::filesystem::path& path, const bool is_saved_model = true); /// \brief Returns data and size of data of stored variable /// \param name Name of variable diff --git a/src/frontends/tensorflow/tests/convert_saved_model.cpp b/src/frontends/tensorflow/tests/convert_saved_model.cpp index efa5b2891efa..af0e6e12a6a4 100644 --- a/src/frontends/tensorflow/tests/convert_saved_model.cpp +++ b/src/frontends/tensorflow/tests/convert_saved_model.cpp @@ -217,6 +217,122 @@ TEST(FrontEndConvertModelTest, SavedModelMaliciousOverflowOffset) { } } +// Crafted SavedModel where AssignVariableOp input references "save/RestoreV2:999" +// but the Const(tensor_names) has only 1 string_val entry → OOB positive index. +TEST(FrontEndConvertModelTest, SavedModelOobPositiveIndex) { + try { + convert_model("saved_model_oob_pos_index"); + FAIL() << "Expected exception for OOB positive RestoreV2 output index"; + } catch (const ov::Exception& e) { + EXPECT_TRUE(string(e.what()).find("out of range") != string::npos) << "Unexpected error message: " << e.what(); + } catch (const std::exception& e) { + FAIL() << "Unexpected std::exception: " << e.what(); + } catch (...) { + FAIL() << "Unexpected non-std exception"; + } +} + +// Crafted SavedModel where AssignVariableOp input references "save/RestoreV2:-1" → negative OOB. +TEST(FrontEndConvertModelTest, SavedModelOobNegativeIndex) { + try { + convert_model("saved_model_oob_neg_index"); + FAIL() << "Expected exception for negative RestoreV2 output index"; + } catch (const ov::Exception& e) { + EXPECT_TRUE(string(e.what()).find("out of range") != string::npos) << "Unexpected error message: " << e.what(); + } catch (const std::exception& e) { + FAIL() << "Unexpected std::exception: " << e.what(); + } catch (...) { + FAIL() << "Unexpected non-std exception"; + } +} + +// Crafted SavedModel where AssignVariableOp input references "save/RestoreV2:0" but +// Const(tensor_names) has zero string_val entries → upper-bound guard fires at index 0. +// This exercises the security check on the implicit-0 code path. +TEST(FrontEndConvertModelTest, SavedModelOobEmptyTensorNames) { + try { + convert_model("saved_model_oob_empty_names"); + FAIL() << "Expected exception for OOB index into empty tensor_names"; + } catch (const ov::Exception& e) { + EXPECT_TRUE(string(e.what()).find("out of range") != string::npos) << "Unexpected error message: " << e.what(); + } catch (const std::exception& e) { + FAIL() << "Unexpected std::exception: " << e.what(); + } catch (...) { + FAIL() << "Unexpected non-std exception"; + } +} + +// Crafted TF1-style SavedModel where Assign.input(1) = "save/RestoreV2:999" but +// Const(tensor_names) has only 1 string_val entry → exercises the Assign code path +// in map_assignvariable() (distinct from the AssignVariableOp path in other OOB tests). +TEST(FrontEndConvertModelTest, SavedModelOobAssignPath) { + try { + convert_model("saved_model_oob_assign_path"); + FAIL() << "Expected exception for OOB RestoreV2 output index in Assign path"; + } catch (const ov::Exception& e) { + EXPECT_TRUE(string(e.what()).find("out of range") != string::npos) << "Unexpected error message: " << e.what(); + } catch (const std::exception& e) { + FAIL() << "Unexpected std::exception: " << e.what(); + } catch (...) { + FAIL() << "Unexpected non-std exception"; + } +} + +TEST(FrontEndConvertModelTest, SavedModelOobRestoreV2ShortInputs) { + try { + convert_model("saved_model_oob_restorev2_short_inputs"); + FAIL() << "Expected exception when RestoreV2 tensor_names input is missing"; + } catch (const ov::Exception& e) { + EXPECT_TRUE(string(e.what()).find("missing tensor_names input") != string::npos) + << "Unexpected error message: " << e.what(); + } catch (const std::exception& e) { + FAIL() << "Unexpected std::exception: " << e.what(); + } catch (...) { + FAIL() << "Unexpected non-std exception"; + } +} + +// Crafted SavedModel where RestoreV2.input(1) names "tensor_names" but no such +// node exists in the GraphDef. shape_and_slices is a Const that occupies the +// inputs[1] slot in the compacted PtrNode::inputs vector — a buggy +// implementation that resolves tensor_names via inputs[1] would silently use +// shape_and_slices. Resolving by matching node->name() across rv2_node->inputs +// makes the missing input explicit. +TEST(FrontEndConvertModelTest, SavedModelOobRestoreV2WrongInputAtPort1) { + try { + convert_model("saved_model_oob_wrong_input_at_port1"); + FAIL() << "Expected exception when RestoreV2 tensor_names input node is absent"; + } catch (const ov::Exception& e) { + EXPECT_TRUE(string(e.what()).find("not found among RestoreV2 inputs") != string::npos) + << "Unexpected error message: " << e.what(); + } catch (const std::exception& e) { + FAIL() << "Unexpected std::exception: " << e.what(); + } catch (...) { + FAIL() << "Unexpected non-std exception"; + } +} + +// Crafted SavedModel where RestoreV2.input(1) is a control dep (`^bogus`) and +// `bogus` is a Const with non-empty `string_val`. parse_node_name strips the +// `^`, and associate_node has already linked `bogus` into rv2_node->inputs, so +// a buggy implementation that pulls the candidate from node->input(1) without +// checking for the `^` prefix would silently bind the variable to bogus's +// first string_val. The fix iterates node->input() and skips control inputs, +// finds only one data input ('prefix'), and throws. +TEST(FrontEndConvertModelTest, SavedModelOobRestoreV2ControlDepAtPort1) { + try { + convert_model("saved_model_oob_control_dep_at_port1"); + FAIL() << "Expected exception when RestoreV2.input(1) is a control dep"; + } catch (const ov::Exception& e) { + EXPECT_TRUE(string(e.what()).find("missing tensor_names input") != string::npos) + << "Unexpected error message: " << e.what(); + } catch (const std::exception& e) { + FAIL() << "Unexpected std::exception: " << e.what(); + } catch (...) { + FAIL() << "Unexpected non-std exception"; + } +} + // Same test with mmap disabled to verify the stream code path is also protected TEST(FrontEndConvertModelTest, SavedModelMaliciousOverflowOffsetNoMmap) { shared_ptr model = nullptr; diff --git a/src/frontends/tensorflow/tests/parse_output_index_tests.cpp b/src/frontends/tensorflow/tests/parse_output_index_tests.cpp new file mode 100644 index 000000000000..640deddd6541 --- /dev/null +++ b/src/frontends/tensorflow/tests/parse_output_index_tests.cpp @@ -0,0 +1,43 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include + +#include +#include +#include + +#include "../src/parse_output_index.hpp" + +using ov::frontend::tensorflow::parse_output_index; + +struct ParseOutputIndexCase { + std::string input; + std::optional expected; +}; + +class ParseOutputIndexTest : public ::testing::TestWithParam {}; + +TEST_P(ParseOutputIndexTest, ParsesValidAndRejectsInvalid) { + const auto& c = GetParam(); + EXPECT_EQ(parse_output_index(c.input), c.expected) << "input=\"" << c.input << "\""; +} + +INSTANTIATE_TEST_SUITE_P(Valid, + ParseOutputIndexTest, + ::testing::Values(ParseOutputIndexCase{"0", 0}, + ParseOutputIndexCase{"42", 42}, + ParseOutputIndexCase{"-7", -7}, + ParseOutputIndexCase{"2147483647", std::numeric_limits::max()}, + ParseOutputIndexCase{"-2147483648", std::numeric_limits::min()})); + +INSTANTIATE_TEST_SUITE_P(Invalid, + ParseOutputIndexTest, + ::testing::Values(ParseOutputIndexCase{"", std::nullopt}, + ParseOutputIndexCase{"abc", std::nullopt}, + ParseOutputIndexCase{"1junk", std::nullopt}, + ParseOutputIndexCase{" 42", std::nullopt}, + ParseOutputIndexCase{"99999999999999999999", std::nullopt}, + ParseOutputIndexCase{"2147483648", std::nullopt}, + ParseOutputIndexCase{"-2147483649", std::nullopt})); diff --git a/src/frontends/tensorflow/tests/test_models/gen_scripts/generate_saved_model_oob_index.py b/src/frontends/tensorflow/tests/test_models/gen_scripts/generate_saved_model_oob_index.py new file mode 100644 index 000000000000..f1e4dc34175f --- /dev/null +++ b/src/frontends/tensorflow/tests/test_models/gen_scripts/generate_saved_model_oob_index.py @@ -0,0 +1,775 @@ +# Copyright (C) 2018-2026 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +# What: Generates 7 malicious TF SavedModel directories that trigger security guards in +# VariablesIndex::map_assignvariable() (variables_index.cpp). +# No TF runtime needed — all protobuf and SSTable structures are built by hand. +# +# How: Each SavedModel has: +# saved_model.pb — GraphDef with nodes described per variant below. +# variables/variables.index — Minimal SSTable (bundle header only, num_shards=1). +# variables/variables.data-* — 64 zero bytes (needed to pass open-file checks). +# +# Seven variants: +# saved_model_oob_pos_index/ — TF2 (AssignVariableOp path): +# Identity.input = "save/RestoreV2:999", +# tensor_names has 1 entry → positive OOB index +# saved_model_oob_neg_index/ — TF2 (AssignVariableOp path): +# Identity.input = "save/RestoreV2:-1", +# tensor_names has 1 entry → negative OOB index +# saved_model_oob_empty_names/ — TF2 (AssignVariableOp path): +# Identity.input = "save/RestoreV2:0", +# tensor_names has 0 entries → OOB via implicit-0 +# path when string_val_size()==0 +# saved_model_oob_assign_path/ — TF1 (Assign path): +# Assign.input(1) = "save/RestoreV2:999", +# tensor_names has 1 entry → positive OOB index +# saved_model_oob_restorev2_short_inputs/ — TF2 (AssignVariableOp path): +# RestoreV2 declared with only 1 input in +# GraphDef → node->input_size()==1 → +# "missing tensor_names input" guard +# saved_model_oob_wrong_input_at_port1/ — TF2 (AssignVariableOp path): +# RestoreV2.input(1) names "tensor_names" but +# no such node exists; shape_and_slices is a +# Const present in the dict and would shift to +# inputs[1] in a buggy implementation +# saved_model_oob_control_dep_at_port1/ — TF2 (AssignVariableOp path): +# RestoreV2.input(1) is a control dep (^bogus), +# bogus is a Const with non-empty string_val +# already in the node dict; data-port scan must +# skip control inputs and refuse the silent +# variable mis-binding +# +# Why: SSTable builder is copied verbatim from generate_saved_model_malicious_overflow.py. +# Generator is run by the CMake build system. +# +# Usage: python3 generate_saved_model_oob_index.py +# Output dirs are created under /. + +import os +import struct +import sys + + +# --------------------------------------------------------------------------- +# Protobuf varint / field encoding (copied from generate_saved_model_malicious_overflow.py) +# --------------------------------------------------------------------------- + +def encode_varint(value): + result = bytearray() + while value > 0x7F: + result.append((value & 0x7F) | 0x80) + value >>= 7 + result.append(value & 0x7F) + return bytes(result) + + +def encode_signed_varint(value): + if value < 0: + value = (1 << 64) + value + return encode_varint(value) + + +def encode_field(field_number, wire_type, data): + tag = (field_number << 3) | wire_type + return encode_varint(tag) + data + + +def encode_varint_field(field_number, value): + return encode_field(field_number, 0, encode_varint(value)) + + +def encode_signed_varint_field(field_number, value): + return encode_field(field_number, 0, encode_signed_varint(value)) + + +def encode_bytes_field(field_number, data): + return encode_field(field_number, 2, encode_varint(len(data)) + data) + + +def encode_string_field(field_number, s): + return encode_bytes_field(field_number, s.encode('utf-8')) + + +def encode_fixed32_field(field_number, value): + return encode_field(field_number, 5, struct.pack('input_size()==1. Triggers the + 'RestoreV2 node is missing tensor_names input' guard in get_variable_name(). + """ + DT_FLOAT = 1 + DT_STRING = 7 + + dtype_attr = encode_varint_field(6, DT_FLOAT) + dim2 = encode_signed_varint_field(1, 2) + shape_proto = encode_bytes_field(2, dim2) + encode_bytes_field(2, dim2) + shape_attr = encode_bytes_field(7, shape_proto) + + varhandle_node = build_node_def( + "my_var", "VarHandleOp", + attrs={"dtype": dtype_attr, "shape": shape_attr, + "container": encode_string_field(2, ""), + "shared_name": encode_string_field(2, "my_var")}) + + scalar_shape = encode_bytes_field(4, b"") + prefix_tensor = (encode_varint_field(1, DT_STRING) + + scalar_shape + + encode_bytes_field(8, b"checkpoint")) + dtype_str_attr = encode_varint_field(6, DT_STRING) + prefix_attr = encode_bytes_field(8, prefix_tensor) + prefix_node = build_node_def( + "prefix", "Const", + attrs={"value": prefix_attr, "dtype": dtype_str_attr}) + + # RestoreV2 with only 1 declared input — no tensor_names, no shape_and_slices. + # node->input_size() == 1 in the GraphDef itself. + list_attr = encode_bytes_field(5, encode_varint_field(6, DT_FLOAT)) + restorev2_node = build_node_def( + "save/RestoreV2", "RestoreV2", + inputs=["prefix"], + attrs={"dtypes": list_attr}) + + t_attr = encode_varint_field(6, DT_FLOAT) + identity_node = build_node_def( + "save/identity", "Identity", + inputs=["save/RestoreV2:0"], + attrs={"T": t_attr}) + assign_node = build_node_def( + "save/Assign", "AssignVariableOp", + inputs=["my_var", "save/identity"], + attrs={"dtype": dtype_attr}) + read_node = build_node_def( + "read_var", "ReadVariableOp", + inputs=["my_var"], + attrs={"dtype": dtype_attr}) + output_identity = build_node_def( + "Identity", "Identity", + inputs=["read_var"], + attrs={"T": t_attr}) + + graph_def = (encode_bytes_field(1, varhandle_node) + + encode_bytes_field(1, prefix_node) + + encode_bytes_field(1, restorev2_node) + + encode_bytes_field(1, identity_node) + + encode_bytes_field(1, assign_node) + + encode_bytes_field(1, read_node) + + encode_bytes_field(1, output_identity)) + + meta_info_def = encode_string_field(1, "serve") + tensor_info = encode_string_field(1, "Identity:0") + encode_varint_field(2, DT_FLOAT) + sig_output_entry = encode_string_field(1, "output_0") + encode_bytes_field(2, tensor_info) + signature_def = encode_bytes_field(2, sig_output_entry) + sig_map_entry = encode_string_field(1, "serving_default") + encode_bytes_field(2, signature_def) + meta_graph_def = (encode_bytes_field(1, meta_info_def) + + encode_bytes_field(2, graph_def) + + encode_bytes_field(5, sig_map_entry)) + + return encode_signed_varint_field(1, 1) + encode_bytes_field(2, meta_graph_def) + + +def write_savedmodel_restorev2_short_inputs(output_dir): + os.makedirs(os.path.join(output_dir, "variables"), exist_ok=True) + with open(os.path.join(output_dir, "saved_model.pb"), 'wb') as f: + f.write(build_saved_model_pb_restorev2_short_inputs()) + with open(os.path.join(output_dir, "variables", "variables.index"), 'wb') as f: + f.write(build_minimal_variables_index()) + with open(os.path.join(output_dir, "variables", "variables.data-00000-of-00001"), 'wb') as f: + f.write(b'\x00' * 64) + + +def build_saved_model_pb_wrong_input_at_port1(): + """ + Build saved_model.pb where RestoreV2.input(1) refers to 'tensor_names' but no + node named 'tensor_names' exists in the GraphDef. shape_and_slices IS present + and is a Const with a 'value' attribute, so a buggy implementation that + consults the compacted PtrNode::inputs vector would pick up shape_and_slices + at index 1 and silently use the wrong Const. The correct implementation + resolves tensor_names by name through the nodes dictionary and throws. + """ + DT_FLOAT = 1 + DT_STRING = 7 + + dtype_attr = encode_varint_field(6, DT_FLOAT) + dim2 = encode_signed_varint_field(1, 2) + shape_proto = encode_bytes_field(2, dim2) + encode_bytes_field(2, dim2) + shape_attr = encode_bytes_field(7, shape_proto) + + varhandle_node = build_node_def( + "my_var", "VarHandleOp", + attrs={"dtype": dtype_attr, "shape": shape_attr, + "container": encode_string_field(2, ""), + "shared_name": encode_string_field(2, "my_var")}) + + dtype_str_attr = encode_varint_field(6, DT_STRING) + scalar_shape = encode_bytes_field(4, b"") + prefix_tensor = (encode_varint_field(1, DT_STRING) + + scalar_shape + + encode_bytes_field(8, b"checkpoint")) + prefix_attr = encode_bytes_field(8, prefix_tensor) + prefix_node = build_node_def( + "prefix", "Const", + attrs={"value": prefix_attr, "dtype": dtype_str_attr}) + + # shape_and_slices declared with a non-empty string_val so a buggy + # implementation using inputs[1] would silently succeed. + shape_tensor_shape = encode_bytes_field(2, encode_signed_varint_field(1, 1)) + shape_tensor = (encode_varint_field(1, DT_STRING) + + encode_bytes_field(4, shape_tensor_shape) + + encode_bytes_field(8, b"fake_var")) + shape_attr_const = encode_bytes_field(8, shape_tensor) + shape_slices_node = build_node_def( + "shape_and_slices", "Const", + attrs={"value": shape_attr_const, "dtype": dtype_str_attr}) + + # RestoreV2 references "tensor_names" at port 1, but the node is missing + # from the GraphDef entirely. prefix and shape_and_slices ARE in the dict + # at the time associate_node runs, so RestoreV2->inputs ends up as + # [prefix, shape_and_slices] (size==2) — buggy implementation indexes [1]. + list_attr = encode_bytes_field(5, encode_varint_field(6, DT_FLOAT)) + restorev2_node = build_node_def( + "save/RestoreV2", "RestoreV2", + inputs=["prefix", "tensor_names", "shape_and_slices"], + attrs={"dtypes": list_attr}) + + t_attr = encode_varint_field(6, DT_FLOAT) + identity_node = build_node_def( + "save/identity", "Identity", + inputs=["save/RestoreV2:0"], + attrs={"T": t_attr}) + assign_node = build_node_def( + "save/Assign", "AssignVariableOp", + inputs=["my_var", "save/identity"], + attrs={"dtype": dtype_attr}) + read_node = build_node_def( + "read_var", "ReadVariableOp", + inputs=["my_var"], + attrs={"dtype": dtype_attr}) + output_identity = build_node_def( + "Identity", "Identity", + inputs=["read_var"], + attrs={"T": t_attr}) + + # prefix and shape_and_slices declared BEFORE RestoreV2 so they get linked + # into RestoreV2->inputs by associate_node (which only links nodes already + # in the dictionary). + graph_def = (encode_bytes_field(1, varhandle_node) + + encode_bytes_field(1, prefix_node) + + encode_bytes_field(1, shape_slices_node) + + encode_bytes_field(1, restorev2_node) + + encode_bytes_field(1, identity_node) + + encode_bytes_field(1, assign_node) + + encode_bytes_field(1, read_node) + + encode_bytes_field(1, output_identity)) + + meta_info_def = encode_string_field(1, "serve") + tensor_info = encode_string_field(1, "Identity:0") + encode_varint_field(2, DT_FLOAT) + sig_output_entry = encode_string_field(1, "output_0") + encode_bytes_field(2, tensor_info) + signature_def = encode_bytes_field(2, sig_output_entry) + sig_map_entry = encode_string_field(1, "serving_default") + encode_bytes_field(2, signature_def) + meta_graph_def = (encode_bytes_field(1, meta_info_def) + + encode_bytes_field(2, graph_def) + + encode_bytes_field(5, sig_map_entry)) + + return encode_signed_varint_field(1, 1) + encode_bytes_field(2, meta_graph_def) + + +def write_savedmodel_wrong_input_at_port1(output_dir): + os.makedirs(os.path.join(output_dir, "variables"), exist_ok=True) + with open(os.path.join(output_dir, "saved_model.pb"), 'wb') as f: + f.write(build_saved_model_pb_wrong_input_at_port1()) + with open(os.path.join(output_dir, "variables", "variables.index"), 'wb') as f: + f.write(build_minimal_variables_index()) + with open(os.path.join(output_dir, "variables", "variables.data-00000-of-00001"), 'wb') as f: + f.write(b'\x00' * 64) + + +def build_saved_model_pb_control_dep_at_port1(): + """ + Build saved_model.pb where RestoreV2 is declared with inputs + ["prefix", "^bogus"]: only one data input plus a control dependency on + 'bogus'. 'bogus' is a Const with a non-empty string_val and is declared + before RestoreV2 so associate_node() links it into rv2_node->inputs. + + A buggy implementation that resolves tensor_names through node->input(1) + blindly would parse the control-dep token (parse_node_name strips '^'), + find 'bogus' among the linked inputs, observe it is a Const with 'value', + and silently bind the variable to bogus's first string_val — a security + regression (silent variable mis-mapping). The correct implementation + iterates node->input() and skips control inputs (those starting with '^'), + finds only one data input ('prefix'), and throws "missing tensor_names + input". + """ + DT_FLOAT = 1 + DT_STRING = 7 + + dtype_attr = encode_varint_field(6, DT_FLOAT) + dim2 = encode_signed_varint_field(1, 2) + shape_proto = encode_bytes_field(2, dim2) + encode_bytes_field(2, dim2) + shape_attr = encode_bytes_field(7, shape_proto) + + varhandle_node = build_node_def( + "my_var", "VarHandleOp", + attrs={"dtype": dtype_attr, "shape": shape_attr, + "container": encode_string_field(2, ""), + "shared_name": encode_string_field(2, "my_var")}) + + dtype_str_attr = encode_varint_field(6, DT_STRING) + scalar_shape = encode_bytes_field(4, b"") + prefix_tensor = (encode_varint_field(1, DT_STRING) + + scalar_shape + + encode_bytes_field(8, b"checkpoint")) + prefix_attr = encode_bytes_field(8, prefix_tensor) + prefix_node = build_node_def( + "prefix", "Const", + attrs={"value": prefix_attr, "dtype": dtype_str_attr}) + + # 'bogus' is a Const with a non-empty string_val; it would satisfy every + # current guard (Const + 'value' attr + idx 0 in range) if a buggy + # implementation accepted it as tensor_names. + bogus_tensor_shape = encode_bytes_field(2, encode_signed_varint_field(1, 1)) + bogus_tensor = (encode_varint_field(1, DT_STRING) + + encode_bytes_field(4, bogus_tensor_shape) + + encode_bytes_field(8, b"fake_var")) + bogus_attr = encode_bytes_field(8, bogus_tensor) + bogus_node = build_node_def( + "bogus", "Const", + attrs={"value": bogus_attr, "dtype": dtype_str_attr}) + + list_attr = encode_bytes_field(5, encode_varint_field(6, DT_FLOAT)) + restorev2_node = build_node_def( + "save/RestoreV2", "RestoreV2", + inputs=["prefix", "^bogus"], + attrs={"dtypes": list_attr}) + + t_attr = encode_varint_field(6, DT_FLOAT) + identity_node = build_node_def( + "save/identity", "Identity", + inputs=["save/RestoreV2:0"], + attrs={"T": t_attr}) + assign_node = build_node_def( + "save/Assign", "AssignVariableOp", + inputs=["my_var", "save/identity"], + attrs={"dtype": dtype_attr}) + read_node = build_node_def( + "read_var", "ReadVariableOp", + inputs=["my_var"], + attrs={"dtype": dtype_attr}) + output_identity = build_node_def( + "Identity", "Identity", + inputs=["read_var"], + attrs={"T": t_attr}) + + # 'prefix' and 'bogus' declared BEFORE RestoreV2 so associate_node() links + # them into rv2_node->inputs (associate_node strips '^' before lookup, so + # the control dep is reachable via rv2_node->inputs in a buggy lookup). + graph_def = (encode_bytes_field(1, varhandle_node) + + encode_bytes_field(1, prefix_node) + + encode_bytes_field(1, bogus_node) + + encode_bytes_field(1, restorev2_node) + + encode_bytes_field(1, identity_node) + + encode_bytes_field(1, assign_node) + + encode_bytes_field(1, read_node) + + encode_bytes_field(1, output_identity)) + + meta_info_def = encode_string_field(1, "serve") + tensor_info = encode_string_field(1, "Identity:0") + encode_varint_field(2, DT_FLOAT) + sig_output_entry = encode_string_field(1, "output_0") + encode_bytes_field(2, tensor_info) + signature_def = encode_bytes_field(2, sig_output_entry) + sig_map_entry = encode_string_field(1, "serving_default") + encode_bytes_field(2, signature_def) + meta_graph_def = (encode_bytes_field(1, meta_info_def) + + encode_bytes_field(2, graph_def) + + encode_bytes_field(5, sig_map_entry)) + + return encode_signed_varint_field(1, 1) + encode_bytes_field(2, meta_graph_def) + + +def write_savedmodel_control_dep_at_port1(output_dir): + os.makedirs(os.path.join(output_dir, "variables"), exist_ok=True) + with open(os.path.join(output_dir, "saved_model.pb"), 'wb') as f: + f.write(build_saved_model_pb_control_dep_at_port1()) + with open(os.path.join(output_dir, "variables", "variables.index"), 'wb') as f: + f.write(build_minimal_variables_index()) + with open(os.path.join(output_dir, "variables", "variables.data-00000-of-00001"), 'wb') as f: + f.write(b'\x00' * 64) + + +def main(): + if len(sys.argv) < 2: + print(f"Usage: {sys.argv[0]} ") + sys.exit(1) + + base = sys.argv[1] + + # Positive OOB: index 999 into a tensor with 1 string_val + write_savedmodel(os.path.join(base, "saved_model_oob_pos_index"), "save/RestoreV2:999") + + # Negative OOB: index -1 into a tensor with 1 string_val + write_savedmodel(os.path.join(base, "saved_model_oob_neg_index"), "save/RestoreV2:-1") + + # Empty tensor_names: index 0 into a tensor with 0 string_val entries; + # exercises the upper-bound guard on the implicit-0 code path + write_savedmodel(os.path.join(base, "saved_model_oob_empty_names"), "save/RestoreV2:0", + num_string_vals=0) + + # TF1-style Assign path: OOB positive index in the Assign branch of map_assignvariable() + write_savedmodel_tf1_assign(os.path.join(base, "saved_model_oob_assign_path"), + "save/RestoreV2:999") + + # RestoreV2 declared with only 1 input ('prefix') in the GraphDef itself — + # node->input_size()==1, exercises the "missing tensor_names input" guard. + write_savedmodel_restorev2_short_inputs( + os.path.join(base, "saved_model_oob_restorev2_short_inputs")) + + # RestoreV2 references tensor_names by name at port 1 but no node named + # tensor_names exists; shape_and_slices is present and would shift into + # the compacted inputs[1] slot in a buggy implementation. + write_savedmodel_wrong_input_at_port1( + os.path.join(base, "saved_model_oob_wrong_input_at_port1")) + + # RestoreV2 declared with inputs ["prefix", "^bogus"]: control dep at + # node->input(1). A buggy implementation would parse the control token, + # match 'bogus' (a Const with non-empty string_val) and silently bind the + # variable to the wrong name. Data-port iteration must skip control deps. + write_savedmodel_control_dep_at_port1( + os.path.join(base, "saved_model_oob_control_dep_at_port1")) + + +if __name__ == '__main__': + main() diff --git a/src/frontends/tensorflow_common/include/common_op_table.hpp b/src/frontends/tensorflow_common/include/common_op_table.hpp index ee39fcc0e02e..875812c7bee5 100644 --- a/src/frontends/tensorflow_common/include/common_op_table.hpp +++ b/src/frontends/tensorflow_common/include/common_op_table.hpp @@ -136,6 +136,7 @@ OP_CONVERTER(translate_random_uniform_op); OP_CONVERTER(translate_random_uniform_int_op); OP_CONVERTER(translate_real_imag_op); OP_CONVERTER(translate_relu_6_op); +OP_CONVERTER(translate_relu_0_to_1_op); OP_CONVERTER(translate_reciprocal_op); OP_CONVERTER(translate_reshape_op); OP_CONVERTER(translate_resource_gather_op); @@ -193,6 +194,7 @@ OP_CONVERTER(translate_truncate_mod_op); OP_CONVERTER(translate_unique_with_counts_op); OP_CONVERTER(translate_unpack_op); OP_CONVERTER(translate_unravel_index_op); +OP_CONVERTER(translate_unsorted_segment_max_op); OP_CONVERTER(translate_unsorted_segment_sum_op); OP_CONVERTER(translate_where_op); OP_CONVERTER(translate_x_div_y_op); diff --git a/src/frontends/tensorflow_common/src/op/relu_0_to_1.cpp b/src/frontends/tensorflow_common/src/op/relu_0_to_1.cpp new file mode 100644 index 000000000000..35f1d5a6b78c --- /dev/null +++ b/src/frontends/tensorflow_common/src/op/relu_0_to_1.cpp @@ -0,0 +1,23 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "common_op_table.hpp" +#include "openvino/op/clamp.hpp" + +namespace ov { +namespace frontend { +namespace tensorflow { +namespace op { +OutputVector translate_relu_0_to_1_op(const NodeContext& node) { + default_op_checks(node, 1, {}); + + auto data = node.get_input(0); + auto res = std::make_shared(data, 0.0, 1.0f); + set_node_name(node.get_name(), res); + return res->outputs(); +} +} // namespace op +} // namespace tensorflow +} // namespace frontend +} // namespace ov diff --git a/src/frontends/tensorflow_common/src/op/reverse_sequence.cpp b/src/frontends/tensorflow_common/src/op/reverse_sequence.cpp index bf29464daa9a..ab4c7300a670 100644 --- a/src/frontends/tensorflow_common/src/op/reverse_sequence.cpp +++ b/src/frontends/tensorflow_common/src/op/reverse_sequence.cpp @@ -5,6 +5,7 @@ #include "openvino/op/reverse_sequence.hpp" #include "common_op_table.hpp" +#include "helper_ops/complex_type_mark.hpp" using namespace std; using namespace ov::op; @@ -14,7 +15,7 @@ namespace frontend { namespace tensorflow { namespace op { OutputVector translate_reverse_sequence_op(const NodeContext& node) { - default_op_checks(node, 2, {"ReverseSequence", "REVERSE_SEQUENCE"}); + default_op_checks(node, 2, {"ReverseSequence", "REVERSE_SEQUENCE"}, true); auto input = node.get_input(0); auto seq_lengths = node.get_input(1); @@ -22,6 +23,17 @@ OutputVector translate_reverse_sequence_op(const NodeContext& node) { auto seq_dim = node.get_attribute("seq_dim"); auto batch_dim = node.get_attribute("batch_dim", 0); + auto complex_type_mark = as_type_ptr(input.get_node_shared_ptr()); + if (complex_type_mark) { + element::Type complex_part_type = complex_type_mark->get_complex_part_type(); + input = complex_type_mark->get_data(); + + auto reverse_sequence = make_shared(input, seq_lengths, batch_dim, seq_dim); + set_node_name(node.get_name(), reverse_sequence); + auto complex_reverse_sequence = make_shared(reverse_sequence, complex_part_type); + return {complex_reverse_sequence->output(0)}; + } + auto reverse_sequence = make_shared(input, seq_lengths, batch_dim, seq_dim); set_node_name(node.get_name(), reverse_sequence); return {reverse_sequence}; diff --git a/src/frontends/tensorflow_common/src/op/unsorted_segment_max.cpp b/src/frontends/tensorflow_common/src/op/unsorted_segment_max.cpp new file mode 100644 index 000000000000..1f5c23c8ff4c --- /dev/null +++ b/src/frontends/tensorflow_common/src/op/unsorted_segment_max.cpp @@ -0,0 +1,64 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "common_op_table.hpp" +#include "openvino/op/constant.hpp" +#include "openvino/op/convert.hpp" +#include "openvino/op/gather.hpp" +#include "openvino/op/reshape.hpp" +#include "openvino/op/segment_max.hpp" +#include "openvino/op/shape_of.hpp" +#include "openvino/op/squeeze.hpp" +#include "openvino/op/topk.hpp" +#include "utils.hpp" + +namespace ov { +namespace frontend { +namespace tensorflow { +namespace op { +OutputVector translate_unsorted_segment_max_op(const NodeContext& node) { + default_op_checks(node, 3, {"UnsortedSegmentMax"}); + auto data = node.get_input(0); + auto segment_ids = node.get_input(1); + auto num_segments = node.get_input(2); + + auto const_minus_one = + std::make_shared(ov::element::i64, ov::Shape{1}, std::vector{-1}); + auto flat_ids = std::make_shared(segment_ids, const_minus_one, false); + + auto squeeze_axis = std::make_shared(ov::element::i32, ov::Shape{1}, std::vector{0}); + auto ids_shape = std::make_shared(flat_ids, ov::element::i64); + auto num_indices = std::make_shared(ids_shape, squeeze_axis); + + auto topk = std::make_shared(flat_ids, + num_indices, + 0, + ov::op::v11::TopK::Mode::MIN, + ov::op::v11::TopK::SortType::SORT_VALUES, + ov::element::i32); + auto sorted_segment_ids = topk->output(0); + auto sort_permutation = topk->output(1); + + auto gather_axis = std::make_shared(ov::element::i32, ov::Shape{1}, std::vector{0}); + auto sorted_data = std::make_shared(data, sort_permutation, gather_axis); + + auto scalar_shape = std::make_shared(ov::element::i32, ov::Shape{0}, std::vector{}); + auto num_segments_scalar = + std::make_shared(std::make_shared(num_segments, ov::element::i64), + scalar_shape, + false); + + auto sorted_ids_i32 = std::make_shared(sorted_segment_ids, ov::element::i32); + + auto seg_max = std::make_shared(sorted_data, + sorted_ids_i32, + num_segments_scalar, + ov::op::FillMode::LOWEST); + set_node_name(node.get_name(), seg_max); + return {seg_max}; +} +} // namespace op +} // namespace tensorflow +} // namespace frontend +} // namespace ov diff --git a/src/frontends/tensorflow_common/src/utils.cpp b/src/frontends/tensorflow_common/src/utils.cpp index d753625e5c5a..c0d075f6bf9c 100644 --- a/src/frontends/tensorflow_common/src/utils.cpp +++ b/src/frontends/tensorflow_common/src/utils.cpp @@ -85,6 +85,7 @@ PadType convert_tf_padding(const frontend::NodeContext& node, const string& tf_p "AvgPool", "AvgPool3D", "CONV_2D", + "CONV_3D", "MAX_POOL_2D", "AVERAGE_POOL_2D", "TRANSPOSE_CONV", @@ -114,7 +115,7 @@ PadType convert_tf_padding(const frontend::NodeContext& node, const string& tf_p op_type == "MaxPool3D" || op_type == "MaxPoolWithArgmax" || op_type == "ExtractImagePatches" || op_type == "DepthwiseConv2dNative" || op_type == "AvgPool" || op_type == "AvgPool3D" || op_type == "CONV_2D" || op_type == "MAX_POOL_2D" || op_type == "AVERAGE_POOL_2D" || - op_type == "DEPTHWISE_CONV_2D") { + op_type == "DEPTHWISE_CONV_2D" || op_type == "CONV_3D") { if (tf_padding == "SAME") { // According to the formulas for calculating auto_pad values of the // Conv layer in the Operation specification, diff --git a/src/frontends/tensorflow_lite/include/openvino/frontend/tensorflow_lite/sparsity_info.hpp b/src/frontends/tensorflow_lite/include/openvino/frontend/tensorflow_lite/sparsity_info.hpp index 84f072d97344..ffddfaeda4b8 100644 --- a/src/frontends/tensorflow_lite/include/openvino/frontend/tensorflow_lite/sparsity_info.hpp +++ b/src/frontends/tensorflow_lite/include/openvino/frontend/tensorflow_lite/sparsity_info.hpp @@ -105,11 +105,7 @@ class TENSORFLOW_LITE_FRONTEND_API SparsityInfo : public ov::RuntimeAttribute { void disable() { m_disabled = true; } - void enable() { - // We dont count on data_desc in case other data is absent - m_disabled = (m_shape.size() == 0 || m_traversal_order.size() == 0 || m_block_map.size() == 0 || - m_dim_format.size() == 0); - } + void enable(); // Unpack sparse tensor and returns dense data void* dense_data() { if (m_disabled) diff --git a/src/frontends/tensorflow_lite/src/decoder_flatbuffer.cpp b/src/frontends/tensorflow_lite/src/decoder_flatbuffer.cpp index 4801d0e54633..85934233b8ba 100644 --- a/src/frontends/tensorflow_lite/src/decoder_flatbuffer.cpp +++ b/src/frontends/tensorflow_lite/src/decoder_flatbuffer.cpp @@ -17,6 +17,18 @@ namespace frontend { namespace tensorflow_lite { namespace { +void validate_tensor_name(const tflite::Tensor* tensor) { + FRONT_END_GENERAL_CHECK(tensor != nullptr, "TensorFlow Lite Frontend: tensor pointer is null."); + FRONT_END_GENERAL_CHECK(tensor->name() != nullptr, + "TensorFlow Lite Frontend: tensor has no 'name' field " + "(malformed flatbuffer: optional 'name' string is absent)."); +} + +std::string safe_tensor_name(const tflite::Tensor* tensor) { + validate_tensor_name(tensor); + return tensor->name()->str(); +} + TensorMetaInfo extract_tensor_meta_info(const TensorInfo& tensor_info) { TensorMetaInfo tensor_meta_info; const auto tensor = tensor_info.tensor; @@ -36,7 +48,7 @@ TensorMetaInfo extract_tensor_meta_info(const TensorInfo& tensor_info) { tensor_data_size); tensor_meta_info.m_tensor_data = tensor_data; tensor_meta_info.m_tensor_data_size = tensor_data_size; - tensor_meta_info.m_tensor_name = tensor->name()->str(); + tensor_meta_info.m_tensor_name = safe_tensor_name(tensor); return tensor_meta_info; } @@ -60,8 +72,7 @@ void DecoderFlatBuffer::get_input_node(size_t input_port_idx, inputs->size()); auto input_tensor_idx = (*inputs)[static_cast(input_port_idx)]; auto tensor = m_input_info.at(input_port_idx).tensor; - std::string name = (*tensor).name()->str(); - producer_name = name; + producer_name = safe_tensor_name(tensor); producer_output_port_index = input_tensor_idx; } @@ -79,7 +90,7 @@ size_t DecoderFlatBuffer::get_output_size() const { std::string DecoderFlatBuffer::get_input_tensor_name(size_t idx) const { FRONT_END_GENERAL_CHECK(idx < get_input_size(), "Requested input is out-of-range"); - return m_input_info.at(idx).tensor->name()->str(); + return safe_tensor_name(m_input_info.at(idx).tensor); } ov::element::Type DecoderFlatBuffer::get_input_tensor_type(size_t idx) const { @@ -89,7 +100,7 @@ ov::element::Type DecoderFlatBuffer::get_input_tensor_type(size_t idx) const { std::string DecoderFlatBuffer::get_output_tensor_name(size_t idx) const { FRONT_END_GENERAL_CHECK(idx < get_output_size(), "Requested output is out-of-range"); - return m_output_info.at(idx).tensor->name()->str(); + return safe_tensor_name(m_output_info.at(idx).tensor); } ov::element::Type DecoderFlatBuffer::get_output_tensor_type(size_t idx) const { @@ -115,7 +126,7 @@ std::shared_ptr DecoderFlatBuffe const ov::frontend::tensorflow_lite::TensorInfo& tensor_info, const ov::frontend::InputModel& model) const { const auto tensor = tensor_info.tensor; - std::vector names = {tensor->name()->str()}; + std::vector names = {safe_tensor_name(tensor)}; const uint8_t* tensor_data = (tensor_info.buffer && tensor_info.buffer->data() ? tensor_info.buffer->data()->data() : nullptr); const size_t tensor_data_size = @@ -270,6 +281,24 @@ ov::Any DecoderFlatBuffer::get_attribute(const std::string& name) const { return "NHWC"; } else if (name == "activation" && m_type == "CONV_2D") { return EnumNameActivationFunctionType(this->get_attribute(&tflite::Conv2DOptions::fused_activation_function)); + } else if (name == "strides" && m_type == "CONV_3D") { + return std::vector{1, + this->get_attribute(&tflite::Conv3DOptions::stride_d), + this->get_attribute(&tflite::Conv3DOptions::stride_h), + this->get_attribute(&tflite::Conv3DOptions::stride_w), + 1}; + } else if (name == "padding" && m_type == "CONV_3D") { + return std::string(EnumNamePadding(this->get_attribute(&tflite::Conv3DOptions::padding))); + } else if (name == "dilations" && m_type == "CONV_3D") { + return std::vector{1, + this->get_attribute(&tflite::Conv3DOptions::dilation_d_factor), + this->get_attribute(&tflite::Conv3DOptions::dilation_h_factor), + this->get_attribute(&tflite::Conv3DOptions::dilation_w_factor), + 1}; + } else if (name == "data_format" && m_type == "CONV_3D") { + return "NDHWC"; + } else if (name == "activation" && m_type == "CONV_3D") { + return EnumNameActivationFunctionType(this->get_attribute(&tflite::Conv3DOptions::fused_activation_function)); } else if (name == "strides" && m_type == "DEPTHWISE_CONV_2D") { return std::vector{1, this->get_attribute(&tflite::DepthwiseConv2DOptions::stride_h), diff --git a/src/frontends/tensorflow_lite/src/frontend.cpp b/src/frontends/tensorflow_lite/src/frontend.cpp index 8741f380f3ec..8108df4e5ffb 100644 --- a/src/frontends/tensorflow_lite/src/frontend.cpp +++ b/src/frontends/tensorflow_lite/src/frontend.cpp @@ -58,8 +58,7 @@ bool FrontEnd::supported_impl(const std::vector& variants) const { return false; if (const auto path = ov::frontend::get_path_from_any(variants[0])) { - const auto model_path = path.value().native(); - if (GraphIteratorFlatBuffer::is_supported(model_path)) { + if (GraphIteratorFlatBuffer::is_supported(*path)) { return true; } } else if (variants[0].is()) { @@ -73,11 +72,9 @@ ov::frontend::InputModel::Ptr FrontEnd::load_impl(const std::vector& va size_t extra_variants_num = variants.size() > 0 && variants[variants.size() - 1].is() ? 1 : 0; if (variants.size() == 1 + extra_variants_num) { if (const auto path = ov::frontend::get_path_from_any(variants[0])) { - const auto model_path = path.value().native(); - if (GraphIteratorFlatBuffer::is_supported(model_path)) { - return std::make_shared( - std::make_shared(model_path), - m_telemetry); + if (GraphIteratorFlatBuffer::is_supported(*path)) { + return std::make_shared(std::make_shared(*path), + m_telemetry); } } else if (variants[0].is()) { auto graph_iterator = variants[0].as(); diff --git a/src/frontends/tensorflow_lite/src/graph_iterator_flatbuffer.cpp b/src/frontends/tensorflow_lite/src/graph_iterator_flatbuffer.cpp index b92e8e480b92..e409ca7b95d7 100644 --- a/src/frontends/tensorflow_lite/src/graph_iterator_flatbuffer.cpp +++ b/src/frontends/tensorflow_lite/src/graph_iterator_flatbuffer.cpp @@ -10,14 +10,7 @@ using namespace ov::frontend::tensorflow_lite; -#ifdef OPENVINO_ENABLE_UNICODE_PATH_SUPPORT - -GraphIteratorFlatBuffer::GraphIteratorFlatBuffer(const std::wstring& path) - : GraphIteratorFlatBuffer(ov::util::wstring_to_string(path)) {} - -#endif // OPENVINO_ENABLE_UNICODE_PATH_SUPPORT - -GraphIteratorFlatBuffer::GraphIteratorFlatBuffer(const std::string& path) { +GraphIteratorFlatBuffer::GraphIteratorFlatBuffer(const std::filesystem::path& path) { std::ifstream model_file(path, std::ios::binary | std::ios::in); FRONT_END_GENERAL_CHECK(model_file && model_file.is_open(), "Model file does not exist: ", path); @@ -26,9 +19,9 @@ GraphIteratorFlatBuffer::GraphIteratorFlatBuffer(const std::string& path) { flatbuffers::Verifier verifier(m_data.data(), m_data.size()); FRONT_END_GENERAL_CHECK(tflite::VerifyModelBuffer(verifier), - "TensorFlow Lite Frontend: the model file \"", + "TensorFlow Lite Frontend: the model file ", path, - "\" is corrupted or malformed (FlatBuffer verification failed)."); + " is corrupted or malformed (FlatBuffer verification failed)."); m_model = tflite::GetModel(m_data.data()); FRONT_END_GENERAL_CHECK(m_model != nullptr, "Failed to parse TFLite model from file: ", path); @@ -176,15 +169,3 @@ std::shared_ptr GraphIteratorFlatBuffer::get_decoder() const { return std::make_shared(info, input_idx, output_idx); } } - -template <> -std::basic_string ov::frontend::tensorflow_lite::get_model_extension() { - return ::tflite::ModelExtension(); -} - -#if defined(OPENVINO_ENABLE_UNICODE_PATH_SUPPORT) && defined(_WIN32) -template <> -std::basic_string ov::frontend::tensorflow_lite::get_model_extension() { - return util::string_to_wstring(::tflite::ModelExtension()); -} -#endif diff --git a/src/frontends/tensorflow_lite/src/graph_iterator_flatbuffer.hpp b/src/frontends/tensorflow_lite/src/graph_iterator_flatbuffer.hpp index 591219a8bcf8..18996b02c721 100644 --- a/src/frontends/tensorflow_lite/src/graph_iterator_flatbuffer.hpp +++ b/src/frontends/tensorflow_lite/src/graph_iterator_flatbuffer.hpp @@ -4,16 +4,13 @@ #pragma once +#include #include -#if defined(__MINGW32__) || defined(__MINGW64__) -# include -#endif #include "openvino/core/any.hpp" #include "openvino/frontend/exception.hpp" #include "openvino/frontend/tensorflow_lite/decoder.hpp" #include "openvino/frontend/tensorflow_lite/graph_iterator.hpp" -#include "openvino/util/common_util.hpp" #include "openvino/util/file_util.hpp" #include "schema_generated.h" @@ -26,16 +23,6 @@ struct TensorInfo { const tflite::Buffer* buffer; }; -template -std::basic_string get_model_extension() {} -template <> -std::basic_string get_model_extension(); - -#if defined(OPENVINO_ENABLE_UNICODE_PATH_SUPPORT) && defined(_WIN32) -template <> -std::basic_string get_model_extension(); -#endif - class GraphIteratorFlatBuffer : public GraphIterator { size_t node_index = 0; std::vector m_data; @@ -46,25 +33,18 @@ class GraphIteratorFlatBuffer : public GraphIterator { public: GraphIteratorFlatBuffer() = default; - explicit GraphIteratorFlatBuffer(const std::string& path); - -#ifdef OPENVINO_ENABLE_UNICODE_PATH_SUPPORT - explicit GraphIteratorFlatBuffer(const std::wstring& path); -#endif + explicit GraphIteratorFlatBuffer(const std::filesystem::path& path); using Ptr = std::shared_ptr; ~GraphIteratorFlatBuffer() = default; /// Verifies file is supported - template - static bool is_supported(const std::basic_string& path) { - FRONT_END_GENERAL_CHECK(util::file_exists(path), - "Could not open the file: \"", - util::path_to_string(path), - '"'); + static bool is_supported(const std::filesystem::path& path) { + FRONT_END_GENERAL_CHECK(util::file_exists(path), "Could not open the file: ", path); + try { - if (!ov::util::ends_with(path, get_model_extension())) { + if (path.extension() != std::filesystem::path("." + std::string(::tflite::ModelExtension()))) { return false; } const std::streamsize offset_size = static_cast(sizeof(::flatbuffers::uoffset_t)); @@ -73,11 +53,7 @@ class GraphIteratorFlatBuffer : public GraphIterator { if (file_size < offset_size) { return false; } -#if defined(__MINGW32__) || defined(__MINGW64__) - std::ifstream tflite_stream(std::filesystem::path(path), std::ios::in | std::ifstream::binary); -#else std::ifstream tflite_stream(path, std::ios::in | std::ifstream::binary); -#endif char buf[offset_size * 2 + 1] = {}; // +1 is used to overcome gcc's -Wstringop-overread warning tflite_stream.read(buf, offset_size * 2); // If we have enough read bytes - try to detect prefixed identifier, else try without size prefix diff --git a/src/frontends/tensorflow_lite/src/op/conv3d.cpp b/src/frontends/tensorflow_lite/src/op/conv3d.cpp new file mode 100644 index 000000000000..0094aee412a6 --- /dev/null +++ b/src/frontends/tensorflow_lite/src/op/conv3d.cpp @@ -0,0 +1,32 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "common_op_table.hpp" +#include "op_translation_utils.hpp" +#include "utils.hpp" + +namespace ov { +namespace frontend { +namespace tensorflow_lite { +namespace op { + +OutputVector conv3d(const ov::frontend::tensorflow_lite::NodeContext& node) { + const auto& decoder = node.get_decoder(); + FRONT_END_GENERAL_CHECK(node.get_input_size() >= 2, + "Unexpected number of input in node of type=", + node.get_op_type(), + " name=", + node.get_name()); + OutputVector output; + get_conv(output, node, decoder, &ov::frontend::tensorflow::op::translate_conv_3d_op, {0, 1, 2, 3, 4}); + get_bias(output, node, decoder); + get_activation(output, decoder); + output[0].get_node_shared_ptr()->set_friendly_name(node.get_name()); + return output; +} + +} // namespace op +} // namespace tensorflow_lite +} // namespace frontend +} // namespace ov diff --git a/src/frontends/tensorflow_lite/src/op_table.cpp b/src/frontends/tensorflow_lite/src/op_table.cpp index f5cdcb05dcd1..679b9351bfef 100644 --- a/src/frontends/tensorflow_lite/src/op_table.cpp +++ b/src/frontends/tensorflow_lite/src/op_table.cpp @@ -58,7 +58,7 @@ std::map get_supported_ops() { // CONCAT_EMBEDDINGS {"CONCATENATION", DEQUANTIZE_INPUTS(concatenation)}, {"CONV_2D", DEQUANTIZE_INPUTS(conv2d)}, - // CONV_3D + {"CONV_3D", DEQUANTIZE_INPUTS(conv3d)}, // CONV_3D_TRANSPOSE {"COS", translate_unary}, {"CUMSUM", translate_cumsum_op}, @@ -141,7 +141,7 @@ std::map get_supported_ops() { {"REDUCE_MIN", translate_reduce_op}, {"REDUCE_PROD", translate_reduce_op}, {"RELU", translate_unary}, - // RELU_0_TO_1 + {"RELU_0_TO_1", DEQUANTIZE_INPUTS(translate_relu_0_to_1_op)}, // RELU_N1_TO_1 {"RELU6", DEQUANTIZE_INPUTS(translate_relu_6_op)}, {"RESHAPE", DEQUANTIZE_INPUTS(reshape)}, diff --git a/src/frontends/tensorflow_lite/src/op_table.hpp b/src/frontends/tensorflow_lite/src/op_table.hpp index 7ad208c8ed3f..fc7e681dcedc 100644 --- a/src/frontends/tensorflow_lite/src/op_table.hpp +++ b/src/frontends/tensorflow_lite/src/op_table.hpp @@ -29,6 +29,7 @@ TFL_OP_CONVERTER(avg_pool_2d); TFL_OP_CONVERTER(complex_abs); TFL_OP_CONVERTER(concatenation); TFL_OP_CONVERTER(conv2d); +TFL_OP_CONVERTER(conv3d); TFL_OP_CONVERTER(depthwise_conv2d); TFL_OP_CONVERTER(dequantize); TFL_OP_CONVERTER(embedding_lookup); diff --git a/src/frontends/tensorflow_lite/src/sparsity_info.cpp b/src/frontends/tensorflow_lite/src/sparsity_info.cpp index 5cd61e73f234..a7c1707106e2 100644 --- a/src/frontends/tensorflow_lite/src/sparsity_info.cpp +++ b/src/frontends/tensorflow_lite/src/sparsity_info.cpp @@ -12,6 +12,55 @@ bool ov::frontend::tensorflow_lite::SparsityInfo::is_copyable() const { return false; } +// Re-evaluates m_disabled against the actually-populated metadata. Disables +// the SparsityInfo whenever any precondition densify() depends on is missing +// or inconsistent, so that TensorLitePlace falls back to the raw constant +// buffer. The contract enforced here: +// +// 1. shape is non-empty; +// 2. traversal_order length is at least the tensor rank; +// 3. block_map size equals (traversal_order.size() - shape.size()), so for +// a standard CSR layout (traversal_order length == rank) block_map must +// be empty, and for a block-sparse layout it must list exactly the +// block dimensions appended after the rank-many leading dims (per +// schema.fbs:187-211); +// 4. dim_format size matches traversal_order size (DimensionMetadata is +// one entry per traversal-order position); +// 5. data_desc size matches dim_format size, and every SPARSE_CSR entry +// carries non-null array_segments + array_indices payloads (densify() +// dereferences these without a null check). +// +// Anything tighter (e.g. integer ranges in the segments/indices buffers) +// stays inside read_sparse_data() / densify(); that level of validation +// requires the actual flatbuffers vector type and is independent of the +// "is the metadata even shaped correctly" question handled here. +void ov::frontend::tensorflow_lite::SparsityInfo::enable() { + if (m_shape.size() == 0 || m_traversal_order.size() < m_shape.size()) { + m_disabled = true; + return; + } + if (m_block_map.size() != m_traversal_order.size() - m_shape.size()) { + m_disabled = true; + return; + } + if (m_dim_format.size() != m_traversal_order.size()) { + m_disabled = true; + return; + } + if (m_data_desc.size() != m_dim_format.size()) { + m_disabled = true; + return; + } + for (size_t i = 0; i < m_dim_format.size(); ++i) { + if (m_dim_format[i] == ::tflite::DimensionType_SPARSE_CSR && + (m_data_desc[i].segments == nullptr || m_data_desc[i].indices == nullptr)) { + m_disabled = true; + return; + } + } + m_disabled = false; +} + // Overflow-safe multiplication for size calculations. // Throws on overflow to prevent heap buffer allocation with a truncated size, // which would lead to out-of-bounds writes during sparse data deserialization. diff --git a/src/frontends/tensorflow_lite/src/tflite_transformations/tflite_quantize_resolver.cpp b/src/frontends/tensorflow_lite/src/tflite_transformations/tflite_quantize_resolver.cpp index 6304fceeaf9c..48b8ffbd9111 100644 --- a/src/frontends/tensorflow_lite/src/tflite_transformations/tflite_quantize_resolver.cpp +++ b/src/frontends/tensorflow_lite/src/tflite_transformations/tflite_quantize_resolver.cpp @@ -8,6 +8,7 @@ #include "openvino/core/rt_info.hpp" #include "openvino/core/validation_util.hpp" +#include "openvino/decompositions/low_precision_dequantize.hpp" #include "openvino/op/constant.hpp" #include "openvino/op/convert.hpp" #include "openvino/op/fake_quantize.hpp" @@ -20,7 +21,6 @@ #include "openvino/pass/pattern/op/optional.hpp" #include "openvino/pass/pattern/op/wrap_type.hpp" #include "tflite_ops/tflite_quantize.hpp" -#include "transformations/rt_info/disable_constant_folding.hpp" #include "utils.hpp" using namespace std; @@ -196,13 +196,13 @@ pass::TFLQuantizeReplacer::TFLQuantizeReplacer() { if (is_constant) { fuse_zp_to_weights(output, zp, zp_shape); - output = make_shared(output, element::f32); - disable_constant_folding(output.get_node_shared_ptr()); + ov::Output zp_input; if (std::any_of(zp.begin(), zp.end(), [](const int64_t& i) { return i != 0; - })) - output = std::make_shared(output, zp_node); - output = std::make_shared(output, scale_node); + })) { + zp_input = zp_node; + } + output = ov::decomposition::low_precision_dequantize(output, scale_node, zp_input); tfl_quantize->output(0).replace(output); return true; } diff --git a/src/frontends/tensorflow_lite/src/utils.cpp b/src/frontends/tensorflow_lite/src/utils.cpp index 05bc86faa0f1..d3be291516c2 100644 --- a/src/frontends/tensorflow_lite/src/utils.cpp +++ b/src/frontends/tensorflow_lite/src/utils.cpp @@ -64,6 +64,7 @@ std::shared_ptr ov::frontend::tenso sparsity->set_dim_format(dim_format); sparsity->set_data_desc(data_desc); } + sparsity->enable(); // Enable sparsity validation; disables if fields are incomplete return sparsity; } diff --git a/src/frontends/tensorflow_lite/tests/convert_sparse_incomplete.cpp b/src/frontends/tensorflow_lite/tests/convert_sparse_incomplete.cpp new file mode 100644 index 000000000000..262c3f3f9259 --- /dev/null +++ b/src/frontends/tensorflow_lite/tests/convert_sparse_incomplete.cpp @@ -0,0 +1,100 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "common_test_utils/ov_test_utils.hpp" +#include "common_test_utils/test_assertions.hpp" +#include "common_test_utils/test_case.hpp" +#include "common_test_utils/test_control.hpp" +#include "convert_model.hpp" +#include "tf_utils.hpp" +#include "utils.hpp" + +using namespace std; +using namespace ov; +using namespace ov::frontend; + +// End-to-end regression coverage for the get_sparsity() fix in +// src/frontends/tensorflow_lite/src/utils.cpp. Two test groups, matching the +// two outcomes that get_sparsity() / SparsityInfo::enable() must produce: +// +// * Fallback group (IncompleteSparsityLoadConvertTest) — the metadata is +// malformed (missing/empty traversal_order, dim_metadata, dim_format, or +// fully empty SparsityParameters). enable() must disable the tensor and +// TensorLitePlace must expose the raw constant buffer instead of trying +// to densify(). The unifying contract is that load() and convert() both +// succeed; without the fix dense_data() throws inside TensorLitePlace's +// ctor and convert() fails. +// +// * Densify group (IncompleteSparsityDensifyTest) — the standard-CSR +// corner case where block_map is legitimately absent. enable() must +// keep the tensor enabled and densify() must run. Pure load+convert is +// not enough here: a future regression that disabled the tensor would +// keep load+convert green by silently falling back to the raw buffer. +// The test asserts the densified all-zero constant by running inference +// on CPU and checking that ADD(input, const) == input. +class IncompleteSparsityLoadConvertTest : public ::testing::TestWithParam { +protected: + void SetUp() override { + FrontEndManager fem; + OV_ASSERT_NO_THROW(m_frontEnd = fem.load_by_framework(TF_LITE_FE)); + ASSERT_NE(m_frontEnd, nullptr); + } + FrontEnd::Ptr m_frontEnd; +}; + +TEST_P(IncompleteSparsityLoadConvertTest, load_and_convert_succeed) { + auto model_filename = FrontEndTestUtils::make_model_path(string(TEST_TENSORFLOW_LITE_MODELS_DIRNAME) + GetParam()); + + InputModel::Ptr inputModel; + OV_ASSERT_NO_THROW(inputModel = m_frontEnd->load(model_filename)); + ASSERT_NE(inputModel, nullptr); + + shared_ptr model; + OV_ASSERT_NO_THROW(model = m_frontEnd->convert(inputModel)); + ASSERT_NE(model, nullptr); + EXPECT_GT(model->get_ops().size(), 0u) + << "Converted model must contain at least one op (Parameter + ADD + Result for the simple " + "graphs, with a DEQUANTIZE inserted for the int8 variant)."; +} + +INSTANTIATE_TEST_SUITE_P(SparseIncomplete, + IncompleteSparsityLoadConvertTest, + ::testing::Values( + // Same shape as the HandsLandmarkFull regression: traversal_order + // and block_map populated, dim_metadata field omitted entirely. + "sparse_incomplete/missing_dim_metadata.tflite", + // dim_metadata present but vector length 0. + "sparse_incomplete/empty_dim_metadata.tflite", + // traversal_order omitted; block_map and dim_metadata present. + "sparse_incomplete/missing_traversal_order.tflite", + // SparsityParameters table referenced but all three fields omitted. + "sparse_incomplete/empty_sparsity.tflite", + // INT8 const with empty SparsityParameters consumed by DEQUANTIZE + // then ADD - mirrors the original failure context (DEQUANTIZE op + // on an incomplete-sparsity constant). + "sparse_incomplete/dequantize_incomplete_sparsity.tflite")); + +// missing_block_map.tflite carries a valid standard-CSR layout +// (traversal_order length == tensor rank, dim_metadata fully populated) and +// intentionally omits block_map. The generator emits segments=[0, 0, 0] and +// indices=[], which densifies the (2, 2) constant to all zeros, while the +// raw FLOAT32 buffer behind the same tensor is [1, 2, 3, 4]. The graph is +// ADD(input, const), so: +// +// * if densify() ran => output == input (asserted here) +// * if fallback fired => output == input + [1, 2, 3, 4] (would fail) +// +// Without that observable, a future change that disabled this tensor +// unconditionally and silently fell back to the raw buffer would keep +// load+convert green and the regression would slip past the suite. +static std::string s_manifest = ""; + +OPENVINO_TEST(IncompleteSparsityDensify, missing_block_map_runs_densify) { + auto model = ov::frontend::tensorflow_lite::tests::convert_model("sparse_incomplete/missing_block_map.tflite"); + + auto test_case = ov::test::TestCase(model, ov::test::utils::DEVICE_CPU); + test_case.add_input(Shape{2, 2}, {10, 20, 30, 40}); + test_case.add_expected_output(Shape{2, 2}, {10, 20, 30, 40}); + test_case.run(); +} diff --git a/src/frontends/tensorflow_lite/tests/convert_unsupported.cpp b/src/frontends/tensorflow_lite/tests/convert_unsupported.cpp index 2c643791d016..e2ffd7b92daa 100644 --- a/src/frontends/tensorflow_lite/tests/convert_unsupported.cpp +++ b/src/frontends/tensorflow_lite/tests/convert_unsupported.cpp @@ -85,3 +85,8 @@ INSTANTIATE_TEST_SUITE_P(BadBufferSize, MalformedModelLoadTest, ::testing::Values("bad_buffer_size/undersized_buffer.tflite", "bad_buffer_size/empty_buffer_nonempty_shape.tflite")); + +// Tensor with no `name` field set in its vtable: load() throws in safe_tensor_name() instead of segfault +INSTANTIATE_TEST_SUITE_P(MissingTensorName, + MalformedModelLoadTest, + ::testing::Values("malformed_tensor_name/missing_tensor_name.tflite")); diff --git a/src/frontends/tensorflow_lite/tests/sparsity_info_test.cpp b/src/frontends/tensorflow_lite/tests/sparsity_info_test.cpp new file mode 100644 index 000000000000..7f9b3321bc38 --- /dev/null +++ b/src/frontends/tensorflow_lite/tests/sparsity_info_test.cpp @@ -0,0 +1,237 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "openvino/frontend/tensorflow_lite/sparsity_info.hpp" + +#include + +#include "openvino/core/type/element_type.hpp" + +using ov::frontend::tensorflow_lite::SparsityInfo; + +// Direct unit tests for SparsityInfo::enable() and the disabled-state +// contract used by TensorLitePlace and the TFLite frontend's get_sparsity() +// factory. See src/frontends/tensorflow_lite/src/utils.cpp. + +namespace { + +constexpr uint8_t kDummyValues[16] = {0}; +constexpr uint8_t kSegmentsSentinel[1] = {0}; +constexpr uint8_t kIndicesSentinel[1] = {0}; + +// Standard-CSR data_desc for a (DENSE, SPARSE_CSR) dim_format. The DENSE +// entry's segments/indices are unused (densify() never indexes them); the +// SPARSE_CSR entry must carry non-null payloads for enable() to admit it. +inline std::vector make_dense_then_sparse_data_desc() { + return { + SparsityInfo::SparsityDataDesc{0, nullptr, 0, nullptr}, + SparsityInfo::SparsityDataDesc{0, kSegmentsSentinel, 0, kIndicesSentinel}, + }; +} + +} // namespace + +// enable() must disable a SparsityInfo whose required fields are all empty. +// The end-to-end coverage that get_sparsity() actually invokes enable() is in +// convert_sparse_incomplete.cpp; this test pins down the enable() semantics +// in isolation. +TEST(SparsityInfoTest, EnableOnEmptyObjectDisables) { + SparsityInfo s; + s.enable(); + EXPECT_TRUE(s.is_disabled()) << "enable() with all required fields empty must disable."; +} + +TEST(SparsityInfoTest, EnableDisablesWhenShapeEmpty) { + SparsityInfo s; + s.set_traversal_order({0, 1}); + s.set_block_map({0}); + s.set_dim_format({0, 1}); + // shape intentionally left empty + s.enable(); + EXPECT_TRUE(s.is_disabled()); +} + +TEST(SparsityInfoTest, EnableDisablesWhenTraversalOrderEmpty) { + SparsityInfo s; + s.set_shape({2, 2}); + s.set_block_map({0}); + s.set_dim_format({0, 1}); + s.enable(); + EXPECT_TRUE(s.is_disabled()); +} + +TEST(SparsityInfoTest, EnableDoesNotDisableWhenBlockMapEmpty) { + SparsityInfo s; + s.set_shape({2, 2}); + s.set_traversal_order({0, 1}); + s.set_dim_format({0, 1}); + s.set_data_desc(make_dense_then_sparse_data_desc()); + // block_map is intentionally absent — valid for standard CSR tensors + s.enable(); + EXPECT_FALSE(s.is_disabled()); +} + +// Block-sparse layouts (traversal_order longer than shape rank) require +// block_map. With block_map missing the metadata is malformed and enable() +// must disable the tensor so densify() is never called on it. +TEST(SparsityInfoTest, EnableDisablesWhenBlockSparseMissingBlockMap) { + SparsityInfo s; + s.set_shape({2, 2}); // rank = 2 + s.set_traversal_order({0, 1, 2, 3}); // length = 4 → block-sparse with k=2 + s.set_dim_format({0, 1, 1, 1}); + // block_map intentionally absent — invalid for block-sparse + s.enable(); + EXPECT_TRUE(s.is_disabled()); +} + +// traversal_order shorter than the tensor rank — densify() would later loop +// `for (dim = 0; dim < m_shape.size(); ++dim) m_dim_format[dim]` and read +// past the end of m_dim_format on untrusted model input. enable() must +// catch this size mismatch up front. +TEST(SparsityInfoTest, EnableDisablesWhenTraversalOrderShorterThanRank) { + SparsityInfo s; + s.set_shape({2, 2}); + s.set_traversal_order({0}); // length 1 < rank 2 + s.set_dim_format({0}); + s.set_data_desc({SparsityInfo::SparsityDataDesc{0, nullptr, 0, nullptr}}); + s.enable(); + EXPECT_TRUE(s.is_disabled()); +} + +// dim_format must carry exactly one DimensionMetadata entry per +// traversal-order position. A length mismatch is malformed; enable() must +// disable so the dispatch in densify() never indexes m_dim_format past its +// real end. +TEST(SparsityInfoTest, EnableDisablesWhenDimFormatLengthMismatchesTraversal) { + SparsityInfo s; + s.set_shape({2, 2}); + s.set_traversal_order({0, 1}); + s.set_dim_format({0}); // length 1 != traversal_order length 2 + s.set_data_desc({SparsityInfo::SparsityDataDesc{0, nullptr, 0, nullptr}}); + s.enable(); + EXPECT_TRUE(s.is_disabled()); +} + +// A SPARSE_CSR DimensionMetadata with absent array_segments or array_indices +// is recorded as a null pointer in m_data_desc. densify()'s dispatcher casts +// those pointers to flatbuffers vector types and calls ->values() without a +// null check, which would dereference null. enable() must disable the +// tensor in that case so the raw-buffer fallback runs instead. +TEST(SparsityInfoTest, EnableDisablesWhenSparseCsrDimMissesSegments) { + SparsityInfo s; + s.set_shape({2, 2}); + s.set_traversal_order({0, 1}); + s.set_dim_format({0, 1}); // dim 1 is SPARSE_CSR + s.set_data_desc({ + SparsityInfo::SparsityDataDesc{0, nullptr, 0, nullptr}, + SparsityInfo::SparsityDataDesc{0, nullptr, 0, kIndicesSentinel}, // segments absent + }); + s.enable(); + EXPECT_TRUE(s.is_disabled()); +} + +// HandsLandmarkFull-style: tf_sparsity is non-null but dim_metadata is empty, +// so dim_format ends up empty in get_sparsity(). +TEST(SparsityInfoTest, EnableDisablesWhenDimFormatEmpty) { + SparsityInfo s; + s.set_shape({2, 2}); + s.set_traversal_order({0, 1}); + // block_map left empty (consistent with non-block-sparse layout) + // dim_format intentionally left empty + s.enable(); + EXPECT_TRUE(s.is_disabled()); +} + +// Block-sparse positive: traversal_order length = rank + #block dims, +// block_map lists the block dims, dim_format and data_desc match. +TEST(SparsityInfoTest, EnableEnabledOnBlockSparseLayout) { + SparsityInfo s; + s.set_shape({2, 2}); // rank = 2 + s.set_traversal_order({0, 1, 2, 3}); // rank + 2 block dims + s.set_block_map({0, 1}); // 2 block dims appended after rank + s.set_dim_format({0, 0, 1, 1}); + s.set_data_desc({ + SparsityInfo::SparsityDataDesc{0, nullptr, 0, nullptr}, + SparsityInfo::SparsityDataDesc{0, nullptr, 0, nullptr}, + SparsityInfo::SparsityDataDesc{0, kSegmentsSentinel, 0, kIndicesSentinel}, + SparsityInfo::SparsityDataDesc{0, kSegmentsSentinel, 0, kIndicesSentinel}, + }); + s.enable(); + EXPECT_FALSE(s.is_disabled()); +} + +TEST(SparsityInfoTest, FullCtorAlreadyEnabled) { + SparsityInfo::SparsityDataDesc dense_desc{}; + SparsityInfo::SparsityDataDesc sparse_desc{0, kSegmentsSentinel, 0, kIndicesSentinel}; + SparsityInfo + s({2, 2}, {0, 1}, {}, {0, 1}, {dense_desc, sparse_desc}, ov::element::f32, kDummyValues, sizeof(kDummyValues)); + EXPECT_FALSE(s.is_disabled()) << "Full ctor must call enable() and find all fields populated."; +} + +// The full ctor calls enable() at the end (sparsity_info.hpp). With an +// empty dim_format the result must be disabled. +TEST(SparsityInfoTest, FullCtorWithEmptyDimFormatDisables) { + SparsityInfo s({2, 2}, + {0, 1}, + {}, // block_map empty (non-block-sparse) + {}, // dim_format empty + {}, + ov::element::f32, + kDummyValues, + sizeof(kDummyValues)); + EXPECT_TRUE(s.is_disabled()); +} + +// dense_data() must throw when m_disabled is true. This is the contract that +// TensorLitePlace::ctor relies on via the is_disabled() short-circuit. +TEST(SparsityInfoTest, DenseDataThrowsWhenDisabled) { + SparsityInfo s; + s.enable(); + ASSERT_TRUE(s.is_disabled()); + EXPECT_THROW(s.dense_data(), std::exception); +} + +// disable()/enable() round-trip: explicit disable() forces disabled; a +// subsequent enable() re-evaluates against the populated fields. Lock-down +// against future regressions that turn enable() into a one-way switch. +TEST(SparsityInfoTest, DisableSetsFlagAndEnableReevaluates) { + SparsityInfo s; + s.set_shape({2, 2}); + s.set_traversal_order({0, 1}); + s.set_dim_format({0, 1}); + s.set_data_desc(make_dense_then_sparse_data_desc()); + + s.disable(); + EXPECT_TRUE(s.is_disabled()); + + s.enable(); + EXPECT_FALSE(s.is_disabled()) << "enable() must re-evaluate from scratch, not stay disabled."; +} + +// is_copyable() is fixed to false for runtime attributes — sparsity is bound +// to the original tensor. Locks the contract used by RuntimeAttribute. +TEST(SparsityInfoTest, IsCopyableReturnsFalse) { + SparsityInfo s; + EXPECT_FALSE(s.is_copyable()); +} + +// Setters and getters round-trip values without side effects. Cheap sanity +// to make sure none of the setters accidentally re-disable the object. +TEST(SparsityInfoTest, SettersRoundTripPreserveValues) { + SparsityInfo s; + s.set_shape({4, 8}); + s.set_traversal_order({1, 0}); + s.set_block_map({0}); + s.set_dim_format({0, 1}); + s.set_target_type(ov::element::i8); + s.set_values(kDummyValues, sizeof(kDummyValues)); + + EXPECT_EQ(s.get_shape(), std::vector({4, 8})); + EXPECT_EQ(s.get_traversal_order(), std::vector({1, 0})); + EXPECT_EQ(s.get_block_map(), std::vector({0})); + EXPECT_EQ(s.get_dim_format(), std::vector({0, 1})); + EXPECT_EQ(s.get_target_type(), ov::element::i8); + EXPECT_EQ(s.get_values(), kDummyValues); + EXPECT_EQ(s.get_values_size(), sizeof(kDummyValues)); +} diff --git a/src/frontends/tensorflow_lite/tests/test_models/gen_scripts/generate_malformed_tensor_name.py b/src/frontends/tensorflow_lite/tests/test_models/gen_scripts/generate_malformed_tensor_name.py new file mode 100644 index 000000000000..3faa0e04486b --- /dev/null +++ b/src/frontends/tensorflow_lite/tests/test_models/gen_scripts/generate_malformed_tensor_name.py @@ -0,0 +1,177 @@ +# Copyright (C) 2018-2026 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +# Generate a crafted .tflite model whose Tensor.name field is absent. +# +# In the FlatBuffer schema for TensorFlow Lite, every table field is implicitly +# optional - the field is absent when its vtable slot offset is 0. The schema- +# generated `Tensor::name()` accessor returns nullptr for an absent name, and +# any caller that dereferences that pointer (e.g. `tensor->name()->str()`) +# crashes with a null-pointer dereference. +# +# This generator produces a structurally valid TFLite buffer +# (`tflite::VerifyModelBuffer` returns true) where one tensor has no `name` +# slot in its vtable. It is the regression input for the load-time null-name +# check in src/frontends/tensorflow_lite/src/decoder_flatbuffer.cpp. +# +# Models generated: +# malformed_tensor_name/missing_tensor_name.tflite +# - 2 tensors, both legal indices, but tensor 0 has no `name` field set. +# The frontend must throw a clear ov::Exception during load(), not +# segfault. + +import os +import sys + +import flatbuffers + + +def build_tflite_with_unnamed_tensor(unnamed_tensor_indices=()): + """Build a minimal valid TFLite FlatBuffer with selected tensors lacking a `name`. + + Default valid model structure: + - 2 tensors (index 0 = input, index 1 = output) + - 3 buffers (0 = empty sentinel, 1 for tensor0, 2 for tensor1) + - 1 operator code (ADD) + - 1 operator: inputs=[0], outputs=[1], opcode_index=0 + - SubGraph inputs=[0], outputs=[1] + + For each tensor index in `unnamed_tensor_indices`, the `name` slot is omitted + from the tensor's vtable so that `Tensor::name()` returns nullptr at load time. + """ + builder = flatbuffers.Builder(1024) + + num_tensors = 2 + num_buffers = 3 + num_opcodes = 1 + + # -- Buffers -- + buffer_offsets = [] + for _ in range(num_buffers): + builder.StartObject(3) # Buffer: data, offset, size + buf_offset = builder.EndObject() + buffer_offsets.append(buf_offset) + + builder.StartVector(4, len(buffer_offsets), 4) + for off in reversed(buffer_offsets): + builder.PrependUOffsetTRelative(off) + buffers_vec = builder.EndVector() + + # -- Operator Codes -- + opcode_offsets = [] + for _ in range(num_opcodes): + builder.StartObject(4) # OperatorCode: deprecated_builtin_code, custom_code, version, builtin_code + builder.PrependInt8Slot(0, 0, 0) # deprecated_builtin_code = ADD(0) + builder.PrependInt32Slot(3, 0, 0) # builtin_code = ADD(0) + builder.PrependInt32Slot(2, 1, 1) # version = 1 + oc_offset = builder.EndObject() + opcode_offsets.append(oc_offset) + + builder.StartVector(4, len(opcode_offsets), 4) + for off in reversed(opcode_offsets): + builder.PrependUOffsetTRelative(off) + opcodes_vec = builder.EndVector() + + # -- Tensors -- + # Pre-create name strings for the tensors that should be named. Strings are + # built up-front because builder offsets are LIFO; we cannot create strings + # while a table object is open. + tensor_names = {} + for i in range(num_tensors): + if i not in unnamed_tensor_indices: + tensor_names[i] = builder.CreateString(f"tensor_{i}") + + shape_vecs = [] + for _ in range(num_tensors): + builder.StartVector(4, 1, 4) + builder.PrependInt32(1) + shape_vecs.append(builder.EndVector()) + + tensor_offsets = [] + for i in range(num_tensors): + buf_idx = i + 1 if i + 1 < num_buffers else 0 + builder.StartObject(11) # Tensor: shape, type, buffer, name, ... + builder.PrependUOffsetTRelativeSlot(0, shape_vecs[i], 0) # shape + builder.PrependInt8Slot(1, 0, 0) # type = FLOAT32 + builder.PrependUint32Slot(2, buf_idx, 0) # buffer index + if i in tensor_names: + builder.PrependUOffsetTRelativeSlot(3, tensor_names[i], 0) # name + # else: deliberately omit slot 3 - this is the regression-under-test condition. + t_offset = builder.EndObject() + tensor_offsets.append(t_offset) + + builder.StartVector(4, len(tensor_offsets), 4) + for off in reversed(tensor_offsets): + builder.PrependUOffsetTRelative(off) + tensors_vec = builder.EndVector() + + # -- Operator -- + builder.StartVector(4, 1, 4) + builder.PrependInt32(0) + op_inputs_vec = builder.EndVector() + + builder.StartVector(4, 1, 4) + builder.PrependInt32(1) + op_outputs_vec = builder.EndVector() + + builder.StartObject(11) # Operator: opcode_index, inputs, outputs, ... + builder.PrependUint32Slot(0, 0, 0) + builder.PrependUOffsetTRelativeSlot(1, op_inputs_vec, 0) + builder.PrependUOffsetTRelativeSlot(2, op_outputs_vec, 0) + op_offset = builder.EndObject() + + builder.StartVector(4, 1, 4) + builder.PrependUOffsetTRelative(op_offset) + operators_vec = builder.EndVector() + + # -- SubGraph -- + builder.StartVector(4, 1, 4) + builder.PrependInt32(0) + sg_inputs_vec = builder.EndVector() + + builder.StartVector(4, 1, 4) + builder.PrependInt32(1) + sg_outputs_vec = builder.EndVector() + + sg_name = builder.CreateString("main") + + builder.StartObject(7) # SubGraph: tensors, inputs, outputs, operators, name, ... + builder.PrependUOffsetTRelativeSlot(0, tensors_vec, 0) + builder.PrependUOffsetTRelativeSlot(1, sg_inputs_vec, 0) + builder.PrependUOffsetTRelativeSlot(2, sg_outputs_vec, 0) + builder.PrependUOffsetTRelativeSlot(3, operators_vec, 0) + builder.PrependUOffsetTRelativeSlot(4, sg_name, 0) + sg_offset = builder.EndObject() + + builder.StartVector(4, 1, 4) + builder.PrependUOffsetTRelative(sg_offset) + subgraphs_vec = builder.EndVector() + + desc = builder.CreateString("missing_tensor_name_test_model") + + # -- Model -- + builder.StartObject(8) # Model: version, operator_codes, subgraphs, description, buffers, ... + builder.PrependUint32Slot(0, 3, 0) # version = 3 + builder.PrependUOffsetTRelativeSlot(1, opcodes_vec, 0) + builder.PrependUOffsetTRelativeSlot(2, subgraphs_vec, 0) + builder.PrependUOffsetTRelativeSlot(3, desc, 0) + builder.PrependUOffsetTRelativeSlot(4, buffers_vec, 0) + model_offset = builder.EndObject() + + builder.Finish(model_offset, b"TFL3") + return bytes(builder.Output()) + + +if __name__ == "__main__": + if len(sys.argv) < 2: + print(f"Usage: {sys.argv[0]} ", file=sys.stderr) + sys.exit(1) + + path_to_model_dir = os.path.join(sys.argv[1], "malformed_tensor_name") + os.makedirs(path_to_model_dir, exist_ok=True) + + # Tensor 0 (the graph input) has no `name` field in its vtable. The frontend + # must reject this with an ov::Exception during load(), not crash. + model = build_tflite_with_unnamed_tensor(unnamed_tensor_indices=(0,)) + with open(os.path.join(path_to_model_dir, "missing_tensor_name.tflite"), "wb") as f: + f.write(model) diff --git a/src/frontends/tensorflow_lite/tests/test_models/gen_scripts/generate_sparse_incomplete.py b/src/frontends/tensorflow_lite/tests/test_models/gen_scripts/generate_sparse_incomplete.py new file mode 100644 index 000000000000..c15ed6a04a1e --- /dev/null +++ b/src/frontends/tensorflow_lite/tests/test_models/gen_scripts/generate_sparse_incomplete.py @@ -0,0 +1,461 @@ +# Copyright (C) 2018-2026 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +# WHAT: +# Generates a family of small .tflite test models in which a constant +# tensor carries a non-null SparsityParameters table with sub-fields that +# are either incomplete (missing/empty traversal_order, dim_metadata, +# dim_format, or fully empty SparsityParameters) or that exercise the +# "valid standard CSR" corner case where block_map is absent. The unifying +# contract: every model produced here must both load() and convert() +# without throwing once the OpenVINO TFLite frontend's get_sparsity() +# factory invokes SparsityInfo::enable(). Two outcomes are expected: +# - Incomplete metadata (missing/empty traversal_order, dim_metadata, +# dim_format, or all-empty SparsityParameters) → tensor is recognized +# as "not really sparse" and falls back to the raw constant buffer. +# - Missing block_map on a non-block-sparse layout (traversal_order +# length == tensor rank) → tensor is treated as a valid standard CSR +# sparse tensor and is densified normally. block_map is required only +# for block-sparse layouts (schema.fbs:187-211). +# +# HOW: +# Uses the official Python `flatbuffers.Builder` (already a transitive +# dependency of the TensorFlow package required by tests/requirements.txt, +# so no new dep is introduced). Field offsets and table layouts follow +# src/frontends/tensorflow_lite/src/schema/schema.fbs verbatim. +# build_incomplete_sparsity_model() takes knobs for which of the three +# SparsityParameters sub-fields to include (or to include but leave empty) +# and which simple op graph to emit: +# - "add" : input + const_with_sparsity -> output (all FLOAT32) +# - "dequantize": DEQUANTIZE(const_with_sparsity) -> mid; +# ADD(input, mid) -> output (mirrors bug location) +# For both graphs the constant tensor's buffer holds exactly +# prod(shape) * elem_size bytes so that the buffer-size check in +# load_model() passes once enable() has correctly disabled the sparsity +# and TensorLitePlace consequently exposes the raw buffer. +# +# WHY: +# Regression coverage for src/frontends/tensorflow_lite/src/utils.cpp's +# get_sparsity() factory. Without sparsity->enable() at the end of that +# factory, an incomplete SparsityParameters keeps m_disabled = false, +# TensorLitePlace::ctor calls dense_data() -> densify() and the convert +# step throws "Sparse dimension isn't found for sparse tensor". With the +# fix, every model produced here loads + converts cleanly — either by +# falling back to the raw buffer (incomplete-metadata cases) or by +# densifying a valid standard CSR tensor (missing-block_map case). +# +# The missing_block_map model deliberately uses segments=[0, 0, 0] and +# indices=[] so that densify() produces an all-zero (2, 2) constant. +# Its raw FLOAT32 buffer is [1, 2, 3, 4]. The two outcomes are therefore +# observable on the converted model: ADD(input, const) == input proves +# densify() ran (asserted by IncompleteSparsityDensify in +# convert_sparse_incomplete.cpp), and a future regression that disabled +# this tensor would visibly produce input + [1, 2, 3, 4] instead. +# +# Real-world reproducer hand_landmark_full.tflite is 5.3 MB and cannot be +# committed, so the synthetic flatbuffers below stand in for it. +# +# Models produced (in subdirectory sparse_incomplete/): +# 1. missing_dim_metadata.tflite - traversal_order + block_map set; dim_metadata field omitted (HandsLandmarkFull shape) +# 2. empty_dim_metadata.tflite - traversal_order + block_map set; dim_metadata is an empty vector +# 3. missing_traversal_order.tflite - block_map + dim_metadata set; traversal_order omitted +# 4. missing_block_map.tflite - traversal_order + dim_metadata set; block_map omitted +# 5. empty_sparsity.tflite - SparsityParameters table present but all three fields omitted +# 6. dequantize_incomplete_sparsity.tflite - INT8 const with empty SparsityParameters consumed by DEQUANTIZE then ADD +# +# Generated tflite files are NOT committed to git; they are produced by the +# CMake target 'tensorflow_lite_test_models' at build time. + +import os +import struct +import sys + +import flatbuffers + +# tflite/schema.fbs enum values (mirrored from +# src/frontends/tensorflow_lite/src/schema/schema.fbs). +TT_FLOAT32 = 0 +TT_INT8 = 9 + +OP_ADD = 0 +OP_DEQUANTIZE = 6 + +DIM_DENSE = 0 +DIM_SPARSE_CSR = 1 + +# SparseIndexVector union tags +SPARSE_IDX_NONE = 0 +SPARSE_IDX_INT32 = 1 # Int32Vector + +# BuiltinOptions union tags (from schema.fbs:union BuiltinOptions order) +BO_NONE = 0 +BO_AddOptions = 11 + +# ActivationFunctionType +ACT_NONE = 0 + + +# ---------- Helpers building schema.fbs tables ---------- + +def _create_int_vector(b, values, prepend_fn=None): + """Create a Vector in the buffer, return offset.""" + prepend = prepend_fn or b.PrependInt32 + b.StartVector(4, len(values), 4) + for v in reversed(values): + prepend(v) + return b.EndVector() + + +def _create_uint_vector(b, values): + b.StartVector(4, len(values), 4) + for v in reversed(values): + b.PrependUint32(v) + return b.EndVector() + + +def _create_byte_vector(b, payload): + b.StartVector(1, len(payload), 1) + for byte in reversed(payload): + b.PrependUint8(byte) + return b.EndVector() + + +def _create_offset_vector(b, offsets): + b.StartVector(4, len(offsets), 4) + for off in reversed(offsets): + b.PrependUOffsetTRelative(off) + return b.EndVector() + + +def _build_int32_vector_table(b, values): + """schema.fbs:Int32Vector { values:[int]; } - field 0 only.""" + values_off = _create_int_vector(b, values) if values else _create_int_vector(b, []) + b.StartObject(1) + b.PrependUOffsetTRelativeSlot(0, values_off, 0) + return b.EndObject() + + +def _build_dim_metadata_dense(b, dense_size): + """schema.fbs:DimensionMetadata { format, dense_size, ... }.""" + b.StartObject(6) + b.PrependInt8Slot(0, DIM_DENSE, 0) + b.PrependInt32Slot(1, dense_size, 0) + return b.EndObject() + + +def _build_dim_metadata_sparse_csr(b, segments_off, indices_off): + """SPARSE_CSR DimensionMetadata referring to two Int32Vector tables.""" + b.StartObject(6) + b.PrependInt8Slot(0, DIM_SPARSE_CSR, 0) + b.PrependInt32Slot(1, 0, 0) # dense_size unused for sparse dims + b.PrependUint8Slot(2, SPARSE_IDX_INT32, 0) # array_segments_type + b.PrependUOffsetTRelativeSlot(3, segments_off, 0) # array_segments + b.PrependUint8Slot(4, SPARSE_IDX_INT32, 0) # array_indices_type + b.PrependUOffsetTRelativeSlot(5, indices_off, 0) # array_indices + return b.EndObject() + + +def _build_sparsity_params(b, *, include_traversal_order, include_block_map, + dim_metadata_mode): + """ + SparsityParameters { + traversal_order:[int]; // field 0 + block_map:[int]; // field 1 + dim_metadata:[DimensionMetadata]; // field 2 + } + + dim_metadata_mode: + "full" - 2 valid DimensionMetadata entries + "empty" - empty vector + "missing" - field absent + """ + if include_traversal_order: + traversal_order_off = _create_int_vector(b, [0, 1]) + else: + traversal_order_off = 0 + + if include_block_map: + block_map_off = _create_int_vector(b, [0]) + else: + block_map_off = 0 + + if dim_metadata_mode == "full": + # Build segments / indices Int32Vector tables BEFORE starting any + # other table (FlatBuffers nesting rule). + segments_table_off = _build_int32_vector_table(b, [0, 0, 0]) + indices_table_off = _build_int32_vector_table(b, []) + + dim0_off = _build_dim_metadata_dense(b, 2) + dim1_off = _build_dim_metadata_sparse_csr(b, segments_table_off, + indices_table_off) + dim_metadata_off = _create_offset_vector(b, [dim0_off, dim1_off]) + elif dim_metadata_mode == "empty": + dim_metadata_off = _create_offset_vector(b, []) + elif dim_metadata_mode == "missing": + dim_metadata_off = 0 + else: + raise ValueError("dim_metadata_mode=" + repr(dim_metadata_mode)) + + b.StartObject(3) + if traversal_order_off: + b.PrependUOffsetTRelativeSlot(0, traversal_order_off, 0) + if block_map_off: + b.PrependUOffsetTRelativeSlot(1, block_map_off, 0) + if dim_metadata_mode != "missing": + b.PrependUOffsetTRelativeSlot(2, dim_metadata_off, 0) + return b.EndObject() + + +def _build_buffer(b, payload): + """Buffer { data:[ubyte]; } - field 0.""" + if payload is None or len(payload) == 0: + b.StartObject(1) + return b.EndObject() + data_off = _create_byte_vector(b, payload) + b.StartObject(1) + b.PrependUOffsetTRelativeSlot(0, data_off, 0) + return b.EndObject() + + +def _build_tensor(b, *, shape, type_code, buffer_idx, name_off, + sparsity_off=0): + """ + Tensor { + shape:[int]; type:TensorType; buffer:uint; name:string; + quantization:QuantizationParameters; is_variable:bool=false; + sparsity:SparsityParameters; ... + } + """ + shape_off = _create_int_vector(b, shape) + b.StartObject(10) + b.PrependUOffsetTRelativeSlot(0, shape_off, 0) + b.PrependInt8Slot(1, type_code, 0) + b.PrependUint32Slot(2, buffer_idx, 0) + b.PrependUOffsetTRelativeSlot(3, name_off, 0) + if sparsity_off: + b.PrependUOffsetTRelativeSlot(6, sparsity_off, 0) + return b.EndObject() + + +def _build_opcode(b, builtin): + """OperatorCode { deprecated_builtin_code:byte; ...; version:int; builtin_code:int; }. + + For backward compatibility, BuiltinCode() readers fall back to the legacy + 8-bit deprecated_builtin_code (slot 0) whenever the 32-bit builtin_code + (slot 3) is below PLACEHOLDER_FOR_GREATER_OP_CODES (127). Both ADD and + DEQUANTIZE are below that threshold, so the value MUST go into slot 0. + """ + b.StartObject(4) + b.PrependInt8Slot(0, builtin, 0) + b.PrependInt32Slot(2, 1, 0) # version = 1 + return b.EndObject() + + +def _build_add_options(b): + """schema.fbs:AddOptions { fused_activation_function:ActivationFunctionType=NONE; pot_scale_int16:bool=true; } + All fields take their defaults; we still need a non-null table so that + decoder_flatbuffer.h:40's `opts != nullptr` check passes when the ADD + translator looks up fused_activation_function. + """ + b.StartObject(2) + # field 0 = fused_activation_function (default NONE = 0): omitted to keep + # default; field 1 = pot_scale_int16 (default true): omitted to keep default. + return b.EndObject() + + +def _build_operator(b, opcode_index, inputs, outputs, + builtin_options_type=BO_NONE, builtin_options_off=0): + inputs_off = _create_int_vector(b, inputs) + outputs_off = _create_int_vector(b, outputs) + b.StartObject(9) + b.PrependUint32Slot(0, opcode_index, 0) + b.PrependUOffsetTRelativeSlot(1, inputs_off, 0) + b.PrependUOffsetTRelativeSlot(2, outputs_off, 0) + if builtin_options_type != BO_NONE: + b.PrependUint8Slot(3, builtin_options_type, 0) + b.PrependUOffsetTRelativeSlot(4, builtin_options_off, 0) + return b.EndObject() + + +def _build_subgraph(b, *, tensors, inputs, outputs, operators, name_off): + tensors_off = _create_offset_vector(b, tensors) + inputs_off = _create_int_vector(b, inputs) + outputs_off = _create_int_vector(b, outputs) + operators_off = _create_offset_vector(b, operators) + b.StartObject(5) + b.PrependUOffsetTRelativeSlot(0, tensors_off, 0) + b.PrependUOffsetTRelativeSlot(1, inputs_off, 0) + b.PrependUOffsetTRelativeSlot(2, outputs_off, 0) + b.PrependUOffsetTRelativeSlot(3, operators_off, 0) + b.PrependUOffsetTRelativeSlot(4, name_off, 0) + return b.EndObject() + + +# ---------- Top-level builder ---------- + +def build_incomplete_sparsity_model(*, + include_traversal_order=True, + include_block_map=True, + dim_metadata_mode="full", + graph="add"): + """ + Build a tiny .tflite model whose only constant tensor carries a non-null + SparsityParameters with the requested completeness profile. + + graph: + "add" - a single ADD(input, const) op, all FLOAT32, shape (2, 2). + "dequantize" - DEQUANTIZE(const) -> mid; ADD(input, mid) -> output. + const is INT8 (2, 2) with 4-byte buffer; everything + else is FLOAT32 (2, 2). + """ + b = flatbuffers.Builder(1024) + + # -- Build leaf objects (strings, sparsity, buffers, tensors) bottom-up -- + + # Strings (must be created before tables that reference them.) + str_input = b.CreateString("input") + str_const = b.CreateString("sparse_const") + str_output = b.CreateString("output") + str_main = b.CreateString("main") + str_desc = b.CreateString("sparse_incomplete_test") + str_mid = b.CreateString("dequant_out") if graph == "dequantize" else 0 + + # Sparsity object referenced by the const tensor. + sparsity_off = _build_sparsity_params( + b, + include_traversal_order=include_traversal_order, + include_block_map=include_block_map, + dim_metadata_mode=dim_metadata_mode, + ) + + # Const tensor's raw buffer: prod(shape) * elem_size bytes. + if graph == "add": + const_dtype = TT_FLOAT32 + const_payload = struct.pack('<4f', 1.0, 2.0, 3.0, 4.0) # 16 B + else: + const_dtype = TT_INT8 + const_payload = struct.pack('<4b', 1, 2, 3, 4) # 4 B + + # Buffers: index 0 must exist and be empty (TFLite convention). + buffer0 = _build_buffer(b, b"") + buffer1 = _build_buffer(b, const_payload) # const tensor data + buffer2 = _build_buffer(b, b"") # input placeholder + buffer3 = _build_buffer(b, b"") # output placeholder + buffers = [buffer0, buffer1, buffer2, buffer3] + if graph == "dequantize": + buffer4 = _build_buffer(b, b"") # dequant intermediate + buffers.append(buffer4) + + # Tensors. + shape_22 = [2, 2] + tensor_input = _build_tensor(b, shape=shape_22, type_code=TT_FLOAT32, + buffer_idx=2, name_off=str_input) + tensor_const = _build_tensor(b, shape=shape_22, type_code=const_dtype, + buffer_idx=1, name_off=str_const, + sparsity_off=sparsity_off) + tensor_output = _build_tensor(b, shape=shape_22, type_code=TT_FLOAT32, + buffer_idx=3, name_off=str_output) + tensors = [tensor_input, tensor_const, tensor_output] + if graph == "dequantize": + tensor_mid = _build_tensor(b, shape=shape_22, type_code=TT_FLOAT32, + buffer_idx=4, name_off=str_mid) + tensors.append(tensor_mid) + + # Operator codes + Operators. + # AddOptions table is mandatory for ADD: the translator reads + # fused_activation_function via decoder_flatbuffer.h::get_attribute(), + # which dereferences builtin_options_as() and asserts non-null. + if graph == "add": + opcode_add = _build_opcode(b, OP_ADD) + opcodes = [opcode_add] + add_options_off = _build_add_options(b) + operator_add = _build_operator(b, opcode_index=0, + inputs=[0, 1], outputs=[2], + builtin_options_type=BO_AddOptions, + builtin_options_off=add_options_off) + operators = [operator_add] + else: + opcode_deq = _build_opcode(b, OP_DEQUANTIZE) + opcode_add = _build_opcode(b, OP_ADD) + opcodes = [opcode_deq, opcode_add] + add_options_off = _build_add_options(b) + # opcode_index 0 = DEQUANTIZE on the const, output to tensor 3 (mid) + operator_deq = _build_operator(b, opcode_index=0, + inputs=[1], outputs=[3]) + # opcode_index 1 = ADD(input, mid) -> output + operator_add = _build_operator(b, opcode_index=1, + inputs=[0, 3], outputs=[2], + builtin_options_type=BO_AddOptions, + builtin_options_off=add_options_off) + operators = [operator_deq, operator_add] + + # SubGraph. + subgraph0 = _build_subgraph(b, tensors=tensors, inputs=[0], outputs=[2], + operators=operators, name_off=str_main) + + # Top-level Model. + opcodes_off = _create_offset_vector(b, opcodes) + subgraphs_off = _create_offset_vector(b, [subgraph0]) + buffers_off = _create_offset_vector(b, buffers) + + b.StartObject(7) + b.PrependUint32Slot(0, 3, 0) # version + b.PrependUOffsetTRelativeSlot(1, opcodes_off, 0) # operator_codes + b.PrependUOffsetTRelativeSlot(2, subgraphs_off, 0) # subgraphs + b.PrependUOffsetTRelativeSlot(3, str_desc, 0) # description + b.PrependUOffsetTRelativeSlot(4, buffers_off, 0) # buffers + model = b.EndObject() + + b.Finish(model, file_identifier=b"TFL3") + return bytes(b.Output()) + + +def main(): + if len(sys.argv) != 2: + print("Usage: " + sys.argv[0] + " ", file=sys.stderr) + sys.exit(1) + + out_dir = os.path.join(sys.argv[1], "sparse_incomplete") + os.makedirs(out_dir, exist_ok=True) + + cases = [ + ("missing_dim_metadata.tflite", dict( + include_traversal_order=True, + include_block_map=True, + dim_metadata_mode="missing", + graph="add")), + ("empty_dim_metadata.tflite", dict( + include_traversal_order=True, + include_block_map=True, + dim_metadata_mode="empty", + graph="add")), + ("missing_traversal_order.tflite", dict( + include_traversal_order=False, + include_block_map=True, + dim_metadata_mode="full", + graph="add")), + ("missing_block_map.tflite", dict( + include_traversal_order=True, + include_block_map=False, + dim_metadata_mode="full", + graph="add")), + ("empty_sparsity.tflite", dict( + include_traversal_order=False, + include_block_map=False, + dim_metadata_mode="missing", + graph="add")), + ("dequantize_incomplete_sparsity.tflite", dict( + include_traversal_order=False, + include_block_map=False, + dim_metadata_mode="missing", + graph="dequantize")), + ] + + for filename, kwargs in cases: + data = build_incomplete_sparsity_model(**kwargs) + with open(os.path.join(out_dir, filename), "wb") as f: + f.write(data) + + +if __name__ == "__main__": + main() diff --git a/src/inference/dev_api/openvino/runtime/compilation_context.hpp b/src/inference/dev_api/openvino/runtime/compilation_context.hpp index 7ac65a342e65..c7fd8d904081 100644 --- a/src/inference/dev_api/openvino/runtime/compilation_context.hpp +++ b/src/inference/dev_api/openvino/runtime/compilation_context.hpp @@ -57,7 +57,7 @@ class CompiledBlobHeader final { return m_runtimeInfo; } - const uint32_t get_header_size_alignment() const { + uint32_t get_header_size_alignment() const { return m_headerSizeAlignment; } diff --git a/src/inference/dev_api/openvino/runtime/iasync_infer_request.hpp b/src/inference/dev_api/openvino/runtime/iasync_infer_request.hpp index 6d066cc95a7e..43cc1724cb69 100644 --- a/src/inference/dev_api/openvino/runtime/iasync_infer_request.hpp +++ b/src/inference/dev_api/openvino/runtime/iasync_infer_request.hpp @@ -214,7 +214,7 @@ class OPENVINO_RUNTIME_API IAsyncInferRequest : public IInferRequest { _this->m_callback = m_callback; } IAsyncInferRequest* _this = nullptr; - std::function m_callback; + std::shared_ptr> m_callback; }; void run_first_stage(const Pipeline::iterator itBeginStage, @@ -230,7 +230,7 @@ class OPENVINO_RUNTIME_API IAsyncInferRequest : public IInferRequest { check_tensors(); InferState state = InferState::IDLE; { - std::lock_guard lock{m_mutex}; + std::lock_guard lock{m_mutex}; state = m_state; switch (m_state) { case InferState::BUSY: @@ -277,7 +277,7 @@ class OPENVINO_RUNTIME_API IAsyncInferRequest : public IInferRequest { std::shared_ptr m_sync_callback_executor; //!< Used to run post inference callback in synchronous pipline mutable std::mutex m_mutex; - std::function m_callback; + std::shared_ptr> m_callback; }; } // namespace ov diff --git a/src/inference/dev_api/openvino/runtime/icache_manager.hpp b/src/inference/dev_api/openvino/runtime/icache_manager.hpp index 245ee574ef51..2fa0afa6013e 100644 --- a/src/inference/dev_api/openvino/runtime/icache_manager.hpp +++ b/src/inference/dev_api/openvino/runtime/icache_manager.hpp @@ -93,7 +93,6 @@ class IContextStore { /** * @brief Initializes the context store with the provided weight sharing context - * . * @param weight_sharing_context The weight sharing context to initialize the store with. */ virtual void initialize(std::shared_ptr weight_sharing_context) = 0; diff --git a/src/inference/dev_api/openvino/runtime/internal_properties.hpp b/src/inference/dev_api/openvino/runtime/internal_properties.hpp index 606bff44076f..a7a92bd869bc 100644 --- a/src/inference/dev_api/openvino/runtime/internal_properties.hpp +++ b/src/inference/dev_api/openvino/runtime/internal_properties.hpp @@ -174,6 +174,54 @@ static constexpr Property key_cache_quan static constexpr Property value_cache_quant_mode{"VALUE_CACHE_QUANT_MODE"}; +/** + * @brief KV cache quantization algorithm. + * Selects SCALAR vs TURBO for integer cache precision (u8/u4); defaults to SCALAR when unset. + * No effect for floating-point cache precision. + */ +enum class CacheQuantAlgorithm { + SCALAR = 0, // Per-group affine scale/zero-point (metadata in k_scale_zp / v_scale_zp) + TURBO = 1, // TurboQuant: random orthogonal rotation + Lloyd-Max codebook + per-head norm +}; + +/** @cond INTERNAL */ +inline std::ostream& operator<<(std::ostream& os, const CacheQuantAlgorithm& alg) { + switch (alg) { + case CacheQuantAlgorithm::SCALAR: + return os << "SCALAR"; + case CacheQuantAlgorithm::TURBO: + return os << "TURBO"; + default: + OPENVINO_THROW("Unsupported cache quant algorithm"); + } +} + +inline std::istream& operator>>(std::istream& is, CacheQuantAlgorithm& alg) { + std::string str; + is >> str; + if (str == "SCALAR") { + alg = CacheQuantAlgorithm::SCALAR; + } else if (str == "TURBO") { + alg = CacheQuantAlgorithm::TURBO; + } else { + OPENVINO_THROW("Unsupported cache quant algorithm: ", str); + } + return is; +} +/** @endcond */ + +/** + * @brief Define quantization algorithm for key cache. + * Set both key_cache_quant_alg and value_cache_quant_alg to the same value + * for symmetric K/V; differ them for asymmetric configs. + */ +static constexpr Property key_cache_quant_alg{"KEY_CACHE_QUANT_ALG"}; + +/** + * @brief Define quantization algorithm for value cache. See key_cache_quant_alg. + */ +static constexpr Property value_cache_quant_alg{"VALUE_CACHE_QUANT_ALG"}; + using WeightSharingCtxPtr = std::shared_ptr; /** diff --git a/src/inference/include/openvino/runtime/intel_gpu/ocl/ocl.hpp b/src/inference/include/openvino/runtime/intel_gpu/ocl/ocl.hpp index da8c296db76d..a7e743d76f8c 100644 --- a/src/inference/include/openvino/runtime/intel_gpu/ocl/ocl.hpp +++ b/src/inference/include/openvino/runtime/intel_gpu/ocl/ocl.hpp @@ -38,8 +38,19 @@ namespace ocl { * @brief Shortcut for defining a handle parameter * @ingroup ov_runtime_ocl_gpu_cpp_api */ + using gpu_handle_param = void*; +/** + * @brief Shortcut for defining a HANDLE on windows or file descriptor on linux + * @ingroup ov_runtime_ocl_gpu_cpp_api + */ +#ifdef __linux__ +using os_handle_param = int; +#else +using os_handle_param = void*; +#endif + /** * @brief This class represents an abstraction for GPU plugin remote tensor * which can be shared with user-supplied OpenCL buffer. @@ -58,6 +69,7 @@ class ClBufferTensor : public RemoteTensor { {{std::string(ov::intel_gpu::mem_handle.name()), {}}, {std::string(ov::intel_gpu::shared_mem_type.name()), {ov::Any(ov::intel_gpu::SharedMemType::OCL_BUFFER).as(), + ov::Any(ov::intel_gpu::SharedMemType::BUFFER_FROM_HANDLE).as(), ov::Any(ov::intel_gpu::SharedMemType::DX_BUFFER).as()}}}); } @@ -307,6 +319,30 @@ class ClContext : public RemoteContext { return create_tensor(type, shape, params).as(); } + /** + * @brief This function is used to obtain a remote tensor object from a user-supplied external memory handle + * The API mirrors the NPU pointer-based create_tensor form. + * @param type Tensor element type + * @param shape Tensor shape + * @param shared_buffer External memory handle from another API (DX12 shared NT handle on Windows passed as void*, + * DMA-BUF fd on Linux passed as int) + * @param memory_type Memory type to use; only MemType::SHARED_BUF is currently supported + * @return A remote tensor instance + */ + ClBufferTensor create_tensor(const element::Type type, + const Shape& shape, + os_handle_param shared_buffer, + const MemType memory_type) { +#ifndef __linux__ + OPENVINO_ASSERT(shared_buffer != nullptr, "shared_buffer must not be nullptr for SHARED_BUF memory type"); +#endif + OPENVINO_ASSERT(memory_type == MemType::SHARED_BUF, + "Only SHARED_BUF memory type is supported for raw buffer pointer or NT handle"); + AnyMap params = {{ov::intel_gpu::shared_mem_type.name(), ov::intel_gpu::SharedMemType::BUFFER_FROM_HANDLE}, + {ov::intel_gpu::os_handle.name(), shared_buffer}}; + return create_tensor(type, shape, params).as(); + } + /** * @brief This function is used to obtain remote tensor object from user-supplied USM pointer * @param type Tensor element type diff --git a/src/inference/include/openvino/runtime/intel_gpu/remote_properties.hpp b/src/inference/include/openvino/runtime/intel_gpu/remote_properties.hpp index c44c2d2f0d5f..977580aef39a 100644 --- a/src/inference/include/openvino/runtime/intel_gpu/remote_properties.hpp +++ b/src/inference/include/openvino/runtime/intel_gpu/remote_properties.hpp @@ -17,6 +17,12 @@ namespace intel_gpu { using gpu_handle_param = void*; +#ifdef __linux__ +using os_handle_param = int; +#else +using os_handle_param = void*; +#endif + /** * @brief Enum to define the type of the shared context * @ingroup ov_runtime_ocl_gpu_cpp_api @@ -103,13 +109,23 @@ static constexpr Property va_device{"VA_DEVICE"}; * @ingroup ov_runtime_ocl_gpu_cpp_api */ enum class SharedMemType { - OCL_BUFFER = 0, //!< Shared OpenCL buffer blob - OCL_IMAGE2D = 1, //!< Shared OpenCL 2D image blob - USM_USER_BUFFER = 2, //!< Shared USM pointer allocated by user - USM_HOST_BUFFER = 3, //!< Shared USM pointer type with host allocation type allocated by plugin - USM_DEVICE_BUFFER = 4, //!< Shared USM pointer type with device allocation type allocated by plugin - VA_SURFACE = 5, //!< Shared video decoder surface or D3D 2D texture blob - DX_BUFFER = 6 //!< Shared D3D buffer blob + OCL_BUFFER = 0, //!< Shared OpenCL buffer blob + OCL_IMAGE2D = 1, //!< Shared OpenCL 2D image blob + USM_USER_BUFFER = 2, //!< Shared USM pointer allocated by user + USM_HOST_BUFFER = 3, //!< Shared USM pointer type with host allocation type allocated by plugin + USM_DEVICE_BUFFER = 4, //!< Shared USM pointer type with device allocation type allocated by plugin + VA_SURFACE = 5, //!< Shared video decoder surface or D3D 2D texture blob + DX_BUFFER = 6, //!< Shared D3D buffer blob + BUFFER_FROM_HANDLE = 7, //!< OS-level external memory handle (e.g. DX12 NT handle on Windows, + //!< DMA-BUF fd on Linux) imported by the plugin into a cl_mem +}; + +/** + * @brief Enum to define memory type for pointer-based tensor sharing API. + * @ingroup ov_runtime_ocl_gpu_cpp_api + */ +enum class MemType { + SHARED_BUF = 0, //!< Shared OpenCL buffer handle passed as void* or int }; /** @cond INTERNAL */ @@ -129,6 +145,8 @@ inline std::ostream& operator<<(std::ostream& os, const SharedMemType& share_mem return os << "VA_SURFACE"; case SharedMemType::DX_BUFFER: return os << "DX_BUFFER"; + case SharedMemType::BUFFER_FROM_HANDLE: + return os << "BUFFER_FROM_HANDLE"; default: OPENVINO_THROW("Unsupported memory type"); } @@ -151,6 +169,8 @@ inline std::istream& operator>>(std::istream& is, SharedMemType& share_mem_type) share_mem_type = SharedMemType::VA_SURFACE; } else if (str == "DX_BUFFER") { share_mem_type = SharedMemType::DX_BUFFER; + } else if (str == "BUFFER_FROM_HANDLE") { + share_mem_type = SharedMemType::BUFFER_FROM_HANDLE; } else { OPENVINO_THROW("Unsupported memory type: ", str); } @@ -172,6 +192,12 @@ static constexpr Property shared_mem_type{"SHARED_MEM_TYPE"}; */ static constexpr Property mem_handle{"MEM_HANDLE"}; +/** + * @brief This key identifies system memory handle (fd on Linux, NT handle on Windows) + * @ingroup ov_runtime_ocl_gpu_cpp_api + */ +static constexpr Property os_handle{"OS_HANDLE"}; + /** * @brief This key identifies video decoder surface handle * in a shared memory blob parameter map diff --git a/src/inference/include/openvino/runtime/properties.hpp b/src/inference/include/openvino/runtime/properties.hpp index 7f7587ce6247..7467e350ff30 100644 --- a/src/inference/include/openvino/runtime/properties.hpp +++ b/src/inference/include/openvino/runtime/properties.hpp @@ -1438,4 +1438,93 @@ static constexpr Property key_cache_group_size * @ingroup ov_runtime_cpp_prop_api */ static constexpr Property value_cache_group_size{"VALUE_CACHE_GROUP_SIZE"}; + +/** + * @brief Enum to describe the compatibility of a compiled model blob with the current device environment. + * @ingroup ov_runtime_cpp_prop_api + * + * Returned by ov::compatibility_check when queried with an ov::runtime_requirements string argument. + */ +enum class CompatibilityCheck { + NOT_APPLICABLE = 0, //!< The device does not support this check, or no requirements string was provided. + SUPPORTED = 1, //!< Requirements are met; import is expected to succeed with optimal performance. + UNSUPPORTED = 2, //!< Requirements are not met; import will fail. +}; + +/** @cond INTERNAL */ +inline std::ostream& operator<<(std::ostream& os, const CompatibilityCheck& compatibility) { + switch (compatibility) { + case CompatibilityCheck::NOT_APPLICABLE: + return os << "NOT_APPLICABLE"; + case CompatibilityCheck::SUPPORTED: + return os << "SUPPORTED"; + case CompatibilityCheck::UNSUPPORTED: + return os << "UNSUPPORTED"; + default: + OPENVINO_THROW("Unsupported CompatibilityCheck value"); + } +} + +inline std::istream& operator>>(std::istream& is, CompatibilityCheck& compatibility) { + std::string str; + is >> str; + if (str == "NOT_APPLICABLE") { + compatibility = CompatibilityCheck::NOT_APPLICABLE; + } else if (str == "SUPPORTED") { + compatibility = CompatibilityCheck::SUPPORTED; + } else if (str == "UNSUPPORTED") { + compatibility = CompatibilityCheck::UNSUPPORTED; + } else { + OPENVINO_THROW("Unsupported CompatibilityCheck value: ", str); + } + return is; +} +/** @endcond */ + +/** + * @brief Read-only property carrying plugin-specific runtime requirements of a compiled model blob. + * @ingroup ov_runtime_cpp_prop_api + * + * The property value is a std::string encoding the device environment requirements at the time + * a model was compiled. The format and content are plugin-dependent and may encode information such as + * plugin version, required hardware capabilities, or driver version. + * + * **Reading** — query on a compiled model to obtain requirements to persist alongside the blob: + * @code + * ov::Core core; + * auto compiled_model = core.compile_model(model, "NPU"); + * std::string requirements = compiled_model.get_property(ov::runtime_requirements); + * @endcode + * + * **Passing as a hint** — use the property name to construct a hint pair when querying ov::compatibility_check: + * @code + * auto compat = core.get_property("NPU", ov::compatibility_check, {{ov::runtime_requirements.name(), requirements}}); + * @endcode + */ +inline constexpr Property runtime_requirements{"RUNTIME_REQUIREMENTS"}; + +/** + * @brief Read-only property to check whether a device satisfies the runtime requirements of a compiled model blob. + * @ingroup ov_runtime_cpp_prop_api + * + * Returns an ov::CompatibilityCheck value describing whether the current device environment is compatible + * with the requirements string passed as an argument via ov::runtime_requirements. + * The requirements string is obtained from a previously compiled model via ov::CompiledModel::get_property(). + * + * @note The property must be queried with an ov::runtime_requirements argument. + * Querying without arguments returns ov::CompatibilityCheck::NOT_APPLICABLE. + * + * **Check requirements before import** + * + * @code + * auto compiled_model = core.compile_model(model, "NPU"); + * auto requirements = compiled_model.get_property(ov::runtime_requirements); + * auto compat = core.get_property("NPU", ov::compatibility_check, {{ov::runtime_requirements.name(), requirements}}); + * if (compat == ov::CompatibilityCheck::OPTIMAL || + * compat == ov::CompatibilityCheck::PREFER_RECOMPILATION) { + * auto imported = core.import_model(blob_stream, "NPU"); + * } + * @endcode + */ +static constexpr Property compatibility_check{"COMPATIBILITY_CHECK"}; } // namespace ov diff --git a/src/inference/src/cache_manager.hpp b/src/inference/src/cache_manager.hpp index 199952486df6..ff4cdc5d9ae5 100644 --- a/src/inference/src/cache_manager.hpp +++ b/src/inference/src/cache_manager.hpp @@ -12,7 +12,6 @@ #include #include #include -#include #include #include @@ -21,6 +20,7 @@ #include "openvino/runtime/tensor.hpp" #include "openvino/util/file_util.hpp" #include "openvino/util/mmap_object.hpp" +#include "openvino/util/parallel_read_streambuf.hpp" namespace ov { @@ -51,7 +51,16 @@ class FileStorageCacheManager final : public ICacheManager { // Fix the bug caused by pugixml, which may return unexpected results if the locale is different from "C". ScopedLocale plocal_C(LC_ALL, "C"); const auto blob_path = get_blob_file(id); - std::ofstream stream(blob_path, std::ios_base::binary); + + if (ov::util::file_exists(blob_path)) { + std::filesystem::permissions(blob_path, + std::filesystem::perms::owner_write, + std::filesystem::perm_options::add); + } + + std::ofstream stream; + stream.exceptions(std::ios_base::failbit | std::ios_base::badbit); + stream.open(blob_path, std::ios_base::binary); writer(stream); stream.close(); std::filesystem::permissions(blob_path, @@ -62,12 +71,13 @@ class FileStorageCacheManager final : public ICacheManager { // Fix the bug caused by pugixml, which may return unexpected results if the locale is different from "C". ScopedLocale plocal_C(LC_ALL, "C"); const auto blob_path = get_blob_file(id); - if (std::filesystem::exists(blob_path)) { + if (ov::util::file_exists(blob_path)) { if (enable_mmap) { CompiledBlobVariant compiled_blob{std::in_place_index<0>, ov::read_tensor_data(blob_path)}; reader(compiled_blob); } else { - std::ifstream stream(blob_path, std::ios_base::binary); + ov::util::ParallelReadStreamBuf par_buf(blob_path); + std::istream stream(&par_buf); CompiledBlobVariant compiled_blob{std::in_place_index<1>, std::ref(stream)}; reader(compiled_blob); } @@ -76,8 +86,7 @@ class FileStorageCacheManager final : public ICacheManager { void remove_cache_entry(const std::string& id) override { const auto blob_path = get_blob_file(id); - - if (std::filesystem::exists(blob_path)) { + if (ov::util::file_exists(blob_path)) { std::ignore = std::filesystem::remove(blob_path); } } diff --git a/src/inference/src/dev/core_impl.cpp b/src/inference/src/dev/core_impl.cpp index 5c3af4886f4b..a280a0c60e29 100644 --- a/src/inference/src/dev/core_impl.cpp +++ b/src/inference/src/dev/core_impl.cpp @@ -1546,6 +1546,10 @@ ov::SoPtr ov::CoreImpl::compile_model_and_cache(ov::Plugin& header_size_alignment); compiled_model->export_model(stream); }); + } catch (const std::ios_base::failure&) { + cache_content.m_cache_manager->remove_cache_entry(cache_content.m_blob_id); + } catch (const ov::Exception&) { + cache_content.m_cache_manager->remove_cache_entry(cache_content.m_blob_id); } catch (...) { cache_content.m_cache_manager->remove_cache_entry(cache_content.m_blob_id); throw; diff --git a/src/inference/src/dev/iasync_infer_request.cpp b/src/inference/src/dev/iasync_infer_request.cpp index 323ebab688ca..683b865821b4 100644 --- a/src/inference/src/dev/iasync_infer_request.cpp +++ b/src/inference/src/dev/iasync_infer_request.cpp @@ -66,7 +66,7 @@ ov::IAsyncInferRequest::IAsyncInferRequest(const std::shared_ptr& void ov::IAsyncInferRequest::wait() { // Just use the last '_futures' member to wait pipeline completion auto future = [this] { - std::lock_guard lock{m_mutex}; + std::lock_guard lock{m_mutex}; return m_futures.empty() ? std::shared_future{} : m_futures.back(); }(); if (future.valid()) { @@ -79,7 +79,7 @@ bool ov::IAsyncInferRequest::wait_for(const std::chrono::milliseconds& timeout) // Just use the last '_futures' member to wait pipeline completion auto future = [this] { - std::lock_guard lock{m_mutex}; + std::lock_guard lock{m_mutex}; return m_futures.empty() ? std::shared_future{} : m_futures.back(); }(); @@ -98,7 +98,7 @@ bool ov::IAsyncInferRequest::wait_for(const std::chrono::milliseconds& timeout) } void ov::IAsyncInferRequest::cancel() { - std::lock_guard lock{m_mutex}; + std::lock_guard lock{m_mutex}; if (m_state == InferState::BUSY) { m_state = InferState::CANCELLED; } @@ -106,8 +106,8 @@ void ov::IAsyncInferRequest::cancel() { void ov::IAsyncInferRequest::set_callback(std::function callback) { check_state(); - std::lock_guard lock{m_mutex}; - m_callback = std::move(callback); + std::lock_guard lock{m_mutex}; + m_callback = std::make_shared>(std::move(callback)); } std::vector> ov::IAsyncInferRequest::query_state() const { @@ -160,23 +160,19 @@ ov::threading::Task ov::IAsyncInferRequest::make_next_stage_task( if ((itEndStage == itNextStage) || (nullptr != currentException)) { auto lastStageTask = [this, currentException]() mutable { std::promise promise; - std::function callback; + std::shared_ptr> callback; { - std::lock_guard lock{m_mutex}; + std::lock_guard lock{m_mutex}; m_state = InferState::IDLE; promise = std::move(m_promise); - std::swap(callback, m_callback); + callback = m_callback; } - if (callback) { + if (callback && *callback) { try { - callback(currentException); + (*callback)(currentException); } catch (...) { currentException = std::current_exception(); } - std::lock_guard lock{m_mutex}; - if (!m_callback) { - std::swap(callback, m_callback); - } } if (nullptr == currentException) { promise.set_value(); diff --git a/src/inference/src/shared_context_manager.cpp b/src/inference/src/shared_context_manager.cpp index eb56a19ac6a1..bcf6e961b1b1 100644 --- a/src/inference/src/shared_context_manager.cpp +++ b/src/inference/src/shared_context_manager.cpp @@ -5,6 +5,7 @@ #include "shared_context_manager.hpp" #include "openvino/runtime/icache_manager.hpp" +#include "openvino/util/common_util.hpp" namespace ov { @@ -34,12 +35,8 @@ void SharedContextManager::set_context(uint64_t id, std::weak_ptrsecond.expired()) { - it = m_weight_contexts.erase(it); - } else { - ++it; - } - } + util::erase_if(m_weight_contexts, [](const auto& ctx_entry) { + return ctx_entry.second.expired(); + }); } } // namespace ov diff --git a/src/inference/src/single_file_storage.cpp b/src/inference/src/single_file_storage.cpp index 6c846a055d20..e9171ac7c550 100644 --- a/src/inference/src/single_file_storage.cpp +++ b/src/inference/src/single_file_storage.cpp @@ -7,6 +7,7 @@ #include "openvino/runtime/aligned_buffer.hpp" #include "openvino/util/file_util.hpp" #include "openvino/util/mmap_object.hpp" +#include "openvino/util/parallel_read_streambuf.hpp" #include "openvino/util/variant_visitor.hpp" namespace ov::runtime { @@ -259,8 +260,9 @@ void SingleFileStorage::read_cache_entry(const std::string& blob_id, bool enable blob_pos)}; reader(compiled_blob); } else { - std::ifstream stream(m_file_path, std::ios::binary); - stream.seekg(blob_pos); + // Use parallel file I/O to saturate NVMe bandwidth instead of single-threaded ifstream. + ov::util::ParallelReadStreamBuf par_buf(m_file_path, static_cast(blob_pos)); + std::istream stream(&par_buf); CompiledBlobVariant compiled_blob{std::in_place_index<1>, std::ref(stream)}; reader(compiled_blob); } diff --git a/src/inference/tests/functional/caching_test.cpp b/src/inference/tests/functional/caching_test.cpp index 47747245ff95..4325abbc8ad8 100644 --- a/src/inference/tests/functional/caching_test.cpp +++ b/src/inference/tests/functional/caching_test.cpp @@ -1638,7 +1638,7 @@ TEST_P(CachingTest, TestNoCachingProperties) { } } -TEST_P(CachingTest, TestThrowOnExport) { +TEST_P(CachingTest, TestThrowOnExportFailure) { EXPECT_CALL(*mockPlugin, get_property(ov::supported_properties.name(), _)).Times(AnyNumber()); EXPECT_CALL(*mockPlugin, get_property(ov::device::capability::EXPORT_IMPORT, _)).Times(AnyNumber()); EXPECT_CALL(*mockPlugin, get_property(ov::device::architecture.name(), _)).Times(AnyNumber()); @@ -1657,6 +1657,66 @@ TEST_P(CachingTest, TestThrowOnExport) { testLoad([&](ov::Core& core) { core.set_property(ov::cache_dir(m_cacheDir)); EXPECT_ANY_THROW(m_testFunction(core)); + EXPECT_FALSE(std::filesystem::exists(ov::util::make_path(m_cacheDir)) && + std::filesystem::directory_iterator(ov::util::make_path(m_cacheDir)) != + std::filesystem::directory_iterator{}); + }); + } +} + +TEST_P(CachingTest, TestIgnoreOvExceptionExportFailure) { + EXPECT_CALL(*mockPlugin, get_property(ov::supported_properties.name(), _)).Times(AnyNumber()); + EXPECT_CALL(*mockPlugin, get_property(ov::device::capability::EXPORT_IMPORT, _)).Times(AnyNumber()); + EXPECT_CALL(*mockPlugin, get_property(ov::device::architecture.name(), _)).Times(AnyNumber()); + EXPECT_CALL(*mockPlugin, get_property(ov::internal::supported_properties.name(), _)).Times(AnyNumber()); + EXPECT_CALL(*mockPlugin, get_property(ov::internal::caching_properties.name(), _)).Times(AnyNumber()); + EXPECT_CALL(*mockPlugin, get_property(ov::device::capabilities.name(), _)).Times(AnyNumber()); + { + EXPECT_CALL(*mockPlugin, compile_model(_, _, _)).Times(m_remoteContext ? 1 : 0); + EXPECT_CALL(*mockPlugin, compile_model(A&>(), _)) + .Times(!m_remoteContext ? 1 : 0); + EXPECT_CALL(*mockPlugin, import_model(A(), _, _)).Times(0); + EXPECT_CALL(*mockPlugin, import_model(A(), _)).Times(0); + m_post_mock_net_callbacks.emplace_back([&](MockICompiledModelImpl& net) { + EXPECT_CALL(net, export_model(_)).Times(1).WillOnce(Invoke([&](std::ostream&) { + OPENVINO_THROW("export failed"); + })); + }); + testLoad([&](ov::Core& core) { + core.set_property(ov::cache_dir(m_cacheDir)); + OV_ASSERT_NO_THROW(m_testFunction(core)); + EXPECT_FALSE(std::filesystem::exists(ov::util::make_path(m_cacheDir)) && + std::filesystem::directory_iterator(ov::util::make_path(m_cacheDir)) != + std::filesystem::directory_iterator{}); + }); + } +} + +TEST_P(CachingTest, TestIgnoreStreamExportFailure) { + EXPECT_CALL(*mockPlugin, get_property(ov::supported_properties.name(), _)).Times(AnyNumber()); + EXPECT_CALL(*mockPlugin, get_property(ov::device::capability::EXPORT_IMPORT, _)).Times(AnyNumber()); + EXPECT_CALL(*mockPlugin, get_property(ov::device::architecture.name(), _)).Times(AnyNumber()); + EXPECT_CALL(*mockPlugin, get_property(ov::internal::supported_properties.name(), _)).Times(AnyNumber()); + EXPECT_CALL(*mockPlugin, get_property(ov::internal::caching_properties.name(), _)).Times(AnyNumber()); + EXPECT_CALL(*mockPlugin, get_property(ov::device::capabilities.name(), _)).Times(AnyNumber()); + { + EXPECT_CALL(*mockPlugin, compile_model(_, _, _)).Times(m_remoteContext ? 1 : 0); + EXPECT_CALL(*mockPlugin, compile_model(A&>(), _)) + .Times(!m_remoteContext ? 1 : 0); + EXPECT_CALL(*mockPlugin, import_model(A(), _, _)).Times(0); + EXPECT_CALL(*mockPlugin, import_model(A(), _)).Times(0); + m_post_mock_net_callbacks.emplace_back([&](MockICompiledModelImpl& net) { + EXPECT_CALL(net, export_model(_)).Times(1).WillOnce(Invoke([&](std::ostream& stream) { + stream << "partial"; + stream.setstate(std::ios_base::badbit); + })); + }); + testLoad([&](ov::Core& core) { + core.set_property(ov::cache_dir(m_cacheDir)); + OV_ASSERT_NO_THROW(m_testFunction(core)); + EXPECT_FALSE(std::filesystem::exists(ov::util::make_path(m_cacheDir)) && + std::filesystem::directory_iterator(ov::util::make_path(m_cacheDir)) != + std::filesystem::directory_iterator{}); }); } } diff --git a/src/inference/tests/functional/ov_core_test.cpp b/src/inference/tests/functional/ov_core_test.cpp index 18353eed8695..d8e6c4b6afa3 100644 --- a/src/inference/tests/functional/ov_core_test.cpp +++ b/src/inference/tests/functional/ov_core_test.cpp @@ -322,4 +322,13 @@ TEST_P(UnicodePathTest, read_compile_model) { std::filesystem::remove_all(prefix_dir); } +INSTANTIATE_TEST_SUITE_P(string_paths, UnicodePathTest, testing::Values("test_folder")); +INSTANTIATE_TEST_SUITE_P(u16_paths, UnicodePathTest, testing::Values(u"test_folder")); +INSTANTIATE_TEST_SUITE_P(u32_paths, UnicodePathTest, testing::Values(U"test_folder")); +INSTANTIATE_TEST_SUITE_P(wstring_paths, UnicodePathTest, testing::Values(L"test_folder")); + +#ifdef OPENVINO_ENABLE_UNICODE_PATH_SUPPORT +INSTANTIATE_TEST_SUITE_P(unicode_paths, UnicodePathTest, unicode_paths); +#endif + } // namespace ov::test diff --git a/src/inference/tests/unit/cache_manager.cpp b/src/inference/tests/unit/cache_manager.cpp new file mode 100644 index 000000000000..4d816ed9ba7b --- /dev/null +++ b/src/inference/tests/unit/cache_manager.cpp @@ -0,0 +1,124 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "cache_manager.hpp" + +#include + +#include +#include +#include +#include +#include +#include + +#include "common_test_utils/common_utils.hpp" + +namespace ov::test { +namespace { + +std::string read_blob(const std::filesystem::path& path) { + std::ifstream stream(path, std::ios::binary); + return {std::istreambuf_iterator(stream), std::istreambuf_iterator()}; +} + +class FlushFailingStreambuf final : public std::streambuf { +public: + explicit FlushFailingStreambuf(std::streambuf* delegate) : m_delegate(delegate) {} + +protected: + std::streamsize xsputn(const char* s, std::streamsize count) override { + return m_delegate->sputn(s, count); + } + + int_type overflow(int_type ch) override { + if (traits_type::eq_int_type(ch, traits_type::eof())) { + return traits_type::not_eof(ch); + } + return m_delegate->sputc(traits_type::to_char_type(ch)); + } + + int sync() override { + return -1; + } + +private: + std::streambuf* m_delegate; +}; + +class FileStorageCacheManagerTest : public ::testing::Test { +protected: + std::filesystem::path m_cache_dir; + std::unique_ptr m_cache_manager; + + void SetUp() override { + m_cache_dir = ov::test::utils::generateTestFilePrefix(); + m_cache_manager = std::make_unique(m_cache_dir); + } + + void TearDown() override { + m_cache_manager.reset(); + std::filesystem::remove_all(m_cache_dir); + } + + std::filesystem::path blob_path(const std::string& id) const { + return m_cache_dir / (id + ".blob"); + } +}; + +TEST_F(FileStorageCacheManagerTest, RemovesBlobAfterWriteFailureWhenCallerCleansUp) { + EXPECT_THROW(m_cache_manager->write_cache_entry("1", + [&](std::ostream& stream) { + stream << "partial"; + throw std::runtime_error("write failed"); + }), + std::runtime_error); + + EXPECT_NO_THROW(m_cache_manager->remove_cache_entry("1")); + EXPECT_FALSE(std::filesystem::exists(blob_path("1"))); +} + +TEST_F(FileStorageCacheManagerTest, RemovesBlobAfterStreamFailureWhenCallerCleansUp) { + EXPECT_THROW(m_cache_manager->write_cache_entry("2", + [&](std::ostream& stream) { + stream << "partial"; + stream.setstate(std::ios_base::badbit); + }), + std::ios_base::failure); + + EXPECT_NO_THROW(m_cache_manager->remove_cache_entry("2")); + EXPECT_FALSE(std::filesystem::exists(blob_path("2"))); +} + +TEST_F(FileStorageCacheManagerTest, OverwritesExistingBlobWhenEntryAlreadyExists) { + m_cache_manager->write_cache_entry("7", [&](std::ostream& stream) { + stream << "cached"; + }); + + bool writer_called = false; + EXPECT_NO_THROW(m_cache_manager->write_cache_entry("7", [&](std::ostream& stream) { + writer_called = true; + stream << "new"; + })); + + EXPECT_TRUE(writer_called); + EXPECT_EQ(read_blob(blob_path("7")), "new"); +} + +TEST_F(FileStorageCacheManagerTest, DoesNotLeaveAuxiliaryFilesInCacheDirectory) { + m_cache_manager->write_cache_entry("8", [&](std::ostream& stream) { + stream << "cached"; + }); + + std::vector entries; + for (const auto& entry : std::filesystem::directory_iterator(m_cache_dir)) { + entries.push_back(entry.path().filename()); + } + + ASSERT_EQ(entries.size(), 1); + EXPECT_EQ(entries.front(), std::filesystem::path{"8.blob"}); +} + +} // namespace +} // namespace ov::test diff --git a/src/inference/tests/unit/parallel_read_streambuf_test.cpp b/src/inference/tests/unit/parallel_read_streambuf_test.cpp new file mode 100644 index 000000000000..9483e5a287ad --- /dev/null +++ b/src/inference/tests/unit/parallel_read_streambuf_test.cpp @@ -0,0 +1,618 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "openvino/util/parallel_read_streambuf.hpp" + +#include + +#include +#include +#include +#include +#include + +#include "common_test_utils/common_utils.hpp" + +namespace ov::test { + +namespace { + +/** + * @brief Fill a vector with a deterministic pattern unique per byte position. + * + * byte[i] = (i % 251) -- 251 is prime so the period never aligns with any + * power-of-two chunk/page size. + */ +void fill_pattern(std::vector& buf, size_t start_index = 0) { + for (size_t i = 0; i < buf.size(); ++i) { + buf[i] = static_cast((start_index + i) % 251u); + } +} + +/** + * @brief Write data to a file, preceded by prefix_size bytes of 0xFF garbage + * so that non-zero-offset tests can verify the header bytes are never + * surfaced through the streambuf. + */ +// ASSERT_* macros expand to `return` (void), so they cannot be used directly +// in a non-void function. The canonical GTest pattern is to delegate to a +// void helper, then check HasFatalFailure() before continuing. +void write_temp_file_impl(const std::filesystem::path& path, const std::vector& data, size_t prefix_size) { + std::ofstream ofs(path, std::ios::binary | std::ios::trunc); + ASSERT_TRUE(ofs.is_open()) << "Cannot create temp file: " << path; + if (prefix_size > 0) { + std::vector prefix(prefix_size, static_cast(0xFFu)); + ofs.write(prefix.data(), static_cast(prefix_size)); + } + ofs.write(data.data(), static_cast(data.size())); +} + +} // namespace + +// Test fixture – creates a temporary file and removes it in TearDown +class ParallelReadStreamBufTest : public ::testing::Test { +protected: + std::filesystem::path m_tmp_path; + + void SetUp() override { + m_tmp_path = ov::test::utils::generateTestFilePrefix() + "_par_read.bin"; + } + + void setup_temp_file(const std::vector& data, size_t prefix_size = 0) { + ASSERT_FALSE(m_tmp_path.empty()); + write_temp_file_impl(m_tmp_path, data, prefix_size); + } + + void TearDown() override { + if (!m_tmp_path.empty() && std::filesystem::exists(m_tmp_path)) { + std::filesystem::remove(m_tmp_path); + } + } +}; + +// 1. Full sequential read – threshold=1 forces parallel_read() to be called; +// num_threads collapses to 1 for < 1 MB so single_read() is used, but the +// dispatch code path (chunk math, atomic success flag) is exercised. +TEST_F(ParallelReadStreamBufTest, FullReadSmallThreshold) { + constexpr size_t k_size = 16 * 1024; // 16 KB + std::vector expected(k_size); + fill_pattern(expected); + setup_temp_file(expected); + + util::ParallelReadStreamBuf buf(m_tmp_path, /*header_offset=*/0, /*threshold=*/1); + std::istream stream(&buf); + + std::vector got(k_size); + ASSERT_TRUE(stream.read(got.data(), static_cast(k_size))); + EXPECT_EQ(got, expected); +} + +// 2. Non-zero header_offset: the file starts with a "garbage" prefix that +// must never appear in reads made through the streambuf. +TEST_F(ParallelReadStreamBufTest, NonZeroHeaderOffsetSmallData) { + constexpr size_t k_prefix_size = 512; + constexpr size_t k_payload_size = 4 * 1024; + + std::vector payload(k_payload_size); + fill_pattern(payload); + setup_temp_file(payload, k_prefix_size); + + util::ParallelReadStreamBuf buf(m_tmp_path, + static_cast(k_prefix_size), + /*threshold=*/1); + std::istream stream(&buf); + + std::vector got(k_payload_size); + ASSERT_TRUE(stream.read(got.data(), static_cast(k_payload_size))); + EXPECT_EQ(got, payload); +} + +// 3. Multiple consecutive reads – each partial read must pick up exactly +// where the previous one left off. +TEST_F(ParallelReadStreamBufTest, ChunkedReads) { + constexpr size_t k_size = 8 * 1024; + std::vector expected(k_size); + fill_pattern(expected); + setup_temp_file(expected); + + util::ParallelReadStreamBuf buf(m_tmp_path, 0, /*threshold=*/1); + std::istream stream(&buf); + + std::vector got(k_size); + constexpr size_t k_chunk = 1000; // intentionally not a power-of-2 + size_t offset = 0; + while (offset < k_size) { + const size_t n = std::min(k_chunk, k_size - offset); + ASSERT_TRUE(stream.read(got.data() + offset, static_cast(n))); + offset += n; + } + EXPECT_EQ(got, expected); +} + +// 4. underflow() path: reading character-by-character exercises the internal +// 8 KB underflow buffer and the get-area bookkeeping. +TEST_F(ParallelReadStreamBufTest, CharByCharUnderflow) { + constexpr size_t k_size = 300; // small enough to fit in a single underflow fill + std::vector expected(k_size); + fill_pattern(expected); + setup_temp_file(expected); + + util::ParallelReadStreamBuf buf(m_tmp_path, 0, /*threshold=*/SIZE_MAX); // force all reads via underflow + std::istream stream(&buf); + + std::vector got; + got.reserve(k_size); + int ch; + while ((ch = stream.get()) != std::char_traits::eof()) { + got.push_back(static_cast(ch)); + } + ASSERT_EQ(got.size(), k_size); + EXPECT_EQ(got, expected); +} + +// 5. seekg(pos, beg): absolute seek then read must return bytes at that +// logical position (relative to the start exposed by the streambuf, i.e. +// after the header_offset). +TEST_F(ParallelReadStreamBufTest, SeekFromBeginning) { + constexpr size_t k_size = 2 * 1024; + std::vector expected(k_size); + fill_pattern(expected); + setup_temp_file(expected); + + util::ParallelReadStreamBuf buf(m_tmp_path, 0, /*threshold=*/1); + std::istream stream(&buf); + + // Seek to byte 500 and read 16 bytes + constexpr std::streamoff k_seek_pos = 500; + constexpr size_t k_read_len = 16; + stream.seekg(k_seek_pos, std::ios::beg); + ASSERT_TRUE(stream.good()); + + std::vector got(k_read_len); + ASSERT_TRUE(stream.read(got.data(), static_cast(k_read_len))); + + std::vector slice(expected.begin() + k_seek_pos, expected.begin() + k_seek_pos + k_read_len); + EXPECT_EQ(got, slice); +} + +// 6. seekg(off, cur): seek relative to current position. +TEST_F(ParallelReadStreamBufTest, SeekFromCurrent) { + constexpr size_t k_size = 2 * 1024; + std::vector expected(k_size); + fill_pattern(expected); + setup_temp_file(expected); + + util::ParallelReadStreamBuf buf(m_tmp_path, 0, /*threshold=*/1); + std::istream stream(&buf); + + // Read 100 bytes, skip 200 forward, read another 50 + constexpr size_t k_first_read = 100; + constexpr std::streamoff k_skip = 200; + constexpr size_t k_second_read = 50; + + std::vector first(k_first_read); + ASSERT_TRUE(stream.read(first.data(), static_cast(k_first_read))); + EXPECT_EQ(first, std::vector(expected.begin(), expected.begin() + k_first_read)); + + stream.seekg(k_skip, std::ios::cur); + ASSERT_TRUE(stream.good()); + + std::vector second(k_second_read); + ASSERT_TRUE(stream.read(second.data(), static_cast(k_second_read))); + + const size_t expected_start = k_first_read + static_cast(k_skip); + std::vector expected_slice(expected.begin() + expected_start, + expected.begin() + expected_start + k_second_read); + EXPECT_EQ(second, expected_slice); +} + +// 7. seekg(off, end): seek backward from end-of-file. +TEST_F(ParallelReadStreamBufTest, SeekFromEnd) { + constexpr size_t k_size = 2 * 1024; + std::vector expected(k_size); + fill_pattern(expected); + setup_temp_file(expected); + + util::ParallelReadStreamBuf buf(m_tmp_path, 0, /*threshold=*/1); + std::istream stream(&buf); + + constexpr std::streamoff k_from_end = 64; + stream.seekg(-k_from_end, std::ios::end); + ASSERT_TRUE(stream.good()); + + std::vector got(static_cast(k_from_end)); + ASSERT_TRUE(stream.read(got.data(), k_from_end)); + + std::vector tail(expected.end() - k_from_end, expected.end()); + EXPECT_EQ(got, tail); +} + +// 8. seekg(0, end) then tellg() should equal the file (payload) size. +TEST_F(ParallelReadStreamBufTest, TellgAtEnd) { + constexpr size_t k_size = 1024; + std::vector data(k_size, static_cast(0xAA)); + setup_temp_file(data); + + util::ParallelReadStreamBuf buf(m_tmp_path, 0, /*threshold=*/1); + std::istream stream(&buf); + + stream.seekg(0, std::ios::end); + ASSERT_TRUE(stream.good()); + EXPECT_EQ(static_cast(stream.tellg()), k_size); +} + +// 9. Seek with non-zero header_offset: logical pos 0 == file offset (prefix). +// seeking to the end should give the payload size, not the whole file size. +TEST_F(ParallelReadStreamBufTest, SeekRespectsHeaderOffset) { + constexpr size_t k_prefix_size = 256; + constexpr size_t k_payload_size = 1024; + + std::vector payload(k_payload_size); + fill_pattern(payload); + setup_temp_file(payload, k_prefix_size); + + util::ParallelReadStreamBuf buf(m_tmp_path, + static_cast(k_prefix_size), + /*threshold=*/1); + std::istream stream(&buf); + + // tellg at start should be 0 (relative to payload start) + EXPECT_EQ(static_cast(stream.tellg()), 0u); + + // seekg to end, tellg should equal payload size + stream.seekg(0, std::ios::end); + ASSERT_TRUE(stream.good()); + EXPECT_EQ(static_cast(stream.tellg()), k_payload_size); + + // Seek to byte 100 and read 8 bytes + constexpr std::streamoff k_pos = 100; + stream.seekg(k_pos, std::ios::beg); + std::vector got(8); + ASSERT_TRUE(stream.read(got.data(), 8)); + std::vector expected(payload.begin() + k_pos, payload.begin() + k_pos + 8); + EXPECT_EQ(got, expected); +} + +// 10. Out-of-range seek returns pos_type(-1) and leaves stream in a fail state. +TEST_F(ParallelReadStreamBufTest, OutOfRangeSeekFails) { + constexpr size_t k_size = 64; + std::vector data(k_size, static_cast(0x55)); + setup_temp_file(data); + + util::ParallelReadStreamBuf buf(m_tmp_path, 0, /*threshold=*/1); + std::istream stream(&buf); + + // Seek before start + const auto pos = stream.seekg(-1, std::ios::beg).tellg(); + EXPECT_EQ(pos, std::streampos(-1)); +} + +// 11. Reading exactly at EOF: request more bytes than remain – stream.read() +// must return false and gcount() must equal the bytes that were available. +TEST_F(ParallelReadStreamBufTest, ReadAtEof) { + constexpr size_t k_size = 100; + std::vector expected(k_size); + fill_pattern(expected); + setup_temp_file(expected); + + util::ParallelReadStreamBuf buf(m_tmp_path, 0, /*threshold=*/SIZE_MAX); // use underflow path + std::istream stream(&buf); + + // Read all but last 10 bytes + std::vector buf1(k_size - 10); + ASSERT_TRUE(stream.read(buf1.data(), static_cast(k_size - 10))); + + // Now try to read 20 bytes when only 10 remain + std::vector buf2(20, 0); + const bool ok = static_cast(stream.read(buf2.data(), 20)); + EXPECT_FALSE(ok); + EXPECT_TRUE(stream.eof()); + ASSERT_EQ(stream.gcount(), 10); + EXPECT_TRUE(std::equal(buf2.begin(), buf2.begin() + 10, expected.end() - 10)); +} + +// 12. PARALLEL PATH CORRECTNESS – large read (>= 2 MB) with threshold=1 so +// parallel_read() is always invoked. On ≥ 2-core machines the actual +// parallel dispatch fires; on single-core machines num_threads==1 still +// exercises the chunk-boundary math via single_read(). +// +// The test verifies: +// a) The full buffer is byte-exact after a parallel read. +// b) A second consecutive parallel read immediately following also +// produces the correct data (no state corruption between calls). +TEST_F(ParallelReadStreamBufTest, ParallelDispatchFullReadCorrectness) { + // Use hw_threads * 1 MB + 1 byte so that on any N-core machine, + // min(hw, size/1MB) > 1 whenever hw >= 2. To avoid excessive memory / I/O + // on very high-core CI runners, cap the effective hw used for sizing. + constexpr size_t k_max_hw_for_size = 16; + const size_t raw_hw = std::max(size_t{2}, static_cast(std::thread::hardware_concurrency())); + const size_t hw = std::min(k_max_hw_for_size, raw_hw); + const size_t k_size = hw * 1024 * 1024 + 1; + + std::vector expected(k_size); + fill_pattern(expected); + setup_temp_file(expected); + + util::ParallelReadStreamBuf buf(m_tmp_path, 0, /*threshold=*/1); + std::istream stream(&buf); + + // a) First parallel read: the full buffer must be byte-exact. + std::vector got(k_size); + ASSERT_TRUE(stream.read(got.data(), static_cast(k_size))); + EXPECT_EQ(got, expected) << "First parallel read produced incorrect data"; + + // b) Seek back to start and do a second full parallel read immediately. + // Verifies no state corruption (m_file_offset, fd, etc.) between calls. + stream.clear(); + stream.seekg(0, std::ios::beg); + ASSERT_TRUE(stream.good()); + + std::vector got2(k_size); + ASSERT_TRUE(stream.read(got2.data(), static_cast(k_size))); + EXPECT_EQ(got2, expected) << "Second consecutive parallel read produced incorrect data"; +} + +// 13. PARALLEL PATH with NON-ZERO header_offset and a seek in the middle: +// file = 4-KB header + (hw*1 MB) payload. After reading half the payload, +// seek back to position 0 (start of payload), read the whole payload again. +TEST_F(ParallelReadStreamBufTest, ParallelDispatchNonZeroOffset_AndSeek) { + constexpr size_t k_prefix_size = 4 * 1024; + constexpr size_t k_max_hw_for_size = 16; + const size_t hw = + std::min(k_max_hw_for_size, std::max(size_t{2}, static_cast(std::thread::hardware_concurrency()))); + const size_t k_payload_size = hw * 1024 * 1024; + + std::vector payload(k_payload_size); + fill_pattern(payload); + setup_temp_file(payload, k_prefix_size); + + util::ParallelReadStreamBuf buf(m_tmp_path, + static_cast(k_prefix_size), + /*threshold=*/1); + std::istream stream(&buf); + + // First pass: read the first half + const size_t k_half = k_payload_size / 2; + std::vector first_half(k_half); + ASSERT_TRUE(stream.read(first_half.data(), static_cast(k_half))); + EXPECT_TRUE(std::equal(first_half.begin(), first_half.end(), payload.begin())) + << "First-half read produced incorrect data"; + + // Seek back to the logical start of the payload + stream.seekg(0, std::ios::beg); + ASSERT_TRUE(stream.good()); + + // Second pass: read the whole payload + std::vector full_read(k_payload_size); + ASSERT_TRUE(stream.read(full_read.data(), static_cast(k_payload_size))); + EXPECT_EQ(full_read, payload) << "Full read after seek produced incorrect data"; +} + +// 14. Mixed underflow + xsgetn: read a few chars via get() (exercises the +// underflow buffer), then read a large block via read() which triggers +// xsgetn to flush the underflow remainder. +TEST_F(ParallelReadStreamBufTest, MixedUnderflowAndBulkRead) { + constexpr size_t k_size = 10 * 1024; + std::vector expected(k_size); + fill_pattern(expected); + setup_temp_file(expected); + + // threshold > k_size so all reads go through underflow() first, but we mix + // with a large stream.read() to exercise the drain-from-get-area code in xsgetn. + util::ParallelReadStreamBuf buf(m_tmp_path, 0, /*threshold=*/SIZE_MAX); + std::istream stream(&buf); + + // Read 5 chars individually – this fills the 8 KB underflow buffer + std::vector prefix; + for (int i = 0; i < 5; ++i) { + const int ch = stream.get(); + ASSERT_NE(ch, std::char_traits::eof()); + prefix.push_back(static_cast(ch)); + } + EXPECT_EQ(prefix, std::vector(expected.begin(), expected.begin() + 5)); + + // Now do a bulk read for the rest of the file. + std::vector rest(k_size - 5); + ASSERT_TRUE(stream.read(rest.data(), static_cast(k_size - 5))); + EXPECT_EQ(rest, std::vector(expected.begin() + 5, expected.end())) + << "Bulk read after char-by-char prefix produced incorrect data"; +} + +// 15. seekg(0, cur) used as tellg() must reflect the current logical position +// correctly after both underflow-buffered reads and bulk reads. +TEST_F(ParallelReadStreamBufTest, TellgIsConsistent) { + constexpr size_t k_size = 512; + std::vector data(k_size, static_cast(0xBB)); + setup_temp_file(data); + + util::ParallelReadStreamBuf buf(m_tmp_path, 0, /*threshold=*/SIZE_MAX); + std::istream stream(&buf); + + EXPECT_EQ(stream.tellg(), std::streampos(0)); + + // After reading 100 bytes via bulk read + std::vector tmp(100); + stream.read(tmp.data(), 100); + EXPECT_EQ(stream.tellg(), std::streampos(100)); + + // After reading 10 more chars individually + for (int i = 0; i < 10; ++i) { + stream.get(); + } + EXPECT_EQ(stream.tellg(), std::streampos(110)); + + // After seekg + stream.seekg(200, std::ios::beg); + EXPECT_EQ(stream.tellg(), std::streampos(200)); +} + +// 16. showmanyc() / in_avail() reports remaining bytes accurately, including +// both buffered characters (from the underflow buffer) and unbuffered +// bytes still in the underlying file. +TEST_F(ParallelReadStreamBufTest, ShowmanycReflectsRemainingBytes) { + constexpr size_t k_size = 256; + std::vector data(k_size, static_cast(0x77u)); + setup_temp_file(data); + + util::ParallelReadStreamBuf buf(m_tmp_path, 0, /*threshold=*/SIZE_MAX); + std::istream stream(&buf); + + // Before any read, in_avail() should report the full file size. + EXPECT_EQ(stream.rdbuf()->in_avail(), static_cast(k_size)); + + // After a bulk read of 100 bytes (via xsgetn, no underflow buffer involved) + std::vector tmp(100); + stream.read(tmp.data(), 100); + EXPECT_EQ(stream.rdbuf()->in_avail(), static_cast(k_size - 100)); + + // After a single get() which triggers underflow() and fills the 8 KB + // internal buffer with the remaining 156 bytes, in_avail() should still + // reflect the correct total: buffered chars minus the one consumed by get(). + stream.get(); + // The underflow buffer now holds min(UNDERFLOW_BUF, 156) = 156 bytes. + // get() consumed 1, so 155 remain in the buffer. m_file_offset + // advanced past the buffer, so file_remaining == 0. + // Total = 155 + 0 = 155. + EXPECT_EQ(stream.rdbuf()->in_avail(), static_cast(k_size - 100 - 1)); + + // Consume everything that remains + std::vector rest(k_size - 101); + stream.read(rest.data(), static_cast(k_size - 101)); + // Now exhausted + EXPECT_EQ(stream.rdbuf()->in_avail(), -1); +} + +// 17. Backward seek from current position: read some bytes, seek backward +// relative to current, verify the re-read returns the correct earlier +// bytes. Also verifies that the underflow buffer is properly invalidated. +TEST_F(ParallelReadStreamBufTest, BackwardSeekFromCurrent) { + constexpr size_t k_size = 2 * 1024; + std::vector expected(k_size); + fill_pattern(expected); + setup_temp_file(expected); + + // Use SIZE_MAX threshold so reads go through underflow + xsgetn drain, + // making the backward seek invalidate a non-empty underflow buffer. + util::ParallelReadStreamBuf buf(m_tmp_path, 0, /*threshold=*/SIZE_MAX); + std::istream stream(&buf); + + // Read 200 bytes + constexpr size_t k_first_read = 200; + std::vector first(k_first_read); + ASSERT_TRUE(stream.read(first.data(), static_cast(k_first_read))); + EXPECT_EQ(stream.tellg(), std::streampos(k_first_read)); + + // Read 5 chars individually to populate the underflow buffer + for (int i = 0; i < 5; ++i) { + ASSERT_NE(stream.get(), std::char_traits::eof()); + } + EXPECT_EQ(stream.tellg(), std::streampos(205)); + + // Seek backward 100 bytes from current position + stream.seekg(-100, std::ios::cur); + ASSERT_TRUE(stream.good()); + EXPECT_EQ(stream.tellg(), std::streampos(105)); + + // Read 50 bytes; they must match expected[105..154] + constexpr size_t k_reread = 50; + std::vector got(k_reread); + ASSERT_TRUE(stream.read(got.data(), static_cast(k_reread))); + std::vector slice(expected.begin() + 105, expected.begin() + 105 + k_reread); + EXPECT_EQ(got, slice); +} + +// Prefetch: after prefetch(size) the next xsgetn must return the same bytes as +// the underlying file, served from the internal prefetch buffer (no pread per +// call). The observable contract is just "same data"; we cannot directly count +// syscalls, so this test guards correctness rather than performance. +TEST_F(ParallelReadStreamBufTest, PrefetchThenReadReturnsCorrectData) { + constexpr size_t k_size = 64 * 1024; + std::vector expected(k_size); + fill_pattern(expected); + setup_temp_file(expected); + + util::ParallelReadStreamBuf buf(m_tmp_path, /*header_offset=*/0, /*threshold=*/1); + std::istream stream(&buf); + + ASSERT_TRUE(buf.prefetch(static_cast(k_size))); + + std::vector got(k_size); + ASSERT_TRUE(stream.read(got.data(), static_cast(k_size))); + EXPECT_EQ(got, expected); + EXPECT_EQ(stream.tellg(), std::streampos(k_size)); +} + +// Prefetch clamps to remaining file size when the requested size exceeds EOF. +// Reading past the prefetch boundary must still work via the regular file-IO +// path without corruption. +TEST_F(ParallelReadStreamBufTest, PrefetchClampsToFileSizeAndFallsThrough) { + constexpr size_t k_size = 8 * 1024; // 8 KB file + std::vector expected(k_size); + fill_pattern(expected); + setup_temp_file(expected); + + util::ParallelReadStreamBuf buf(m_tmp_path, /*header_offset=*/0, /*threshold=*/1); + std::istream stream(&buf); + + // Request far more than the file holds; prefetch clamps internally. + ASSERT_TRUE(buf.prefetch(static_cast(64 * 1024 * 1024))); + + std::vector got(k_size); + ASSERT_TRUE(stream.read(got.data(), static_cast(k_size))); + EXPECT_EQ(got, expected); + + // A further read must return EOF without corrupting state. + char tail = 0; + stream.read(&tail, 1); + EXPECT_TRUE(stream.eof() || !stream.good()); +} + +// Seeking outside the prefetched window must transparently invalidate the +// buffer; the subsequent read fetches from the file and still matches. +TEST_F(ParallelReadStreamBufTest, PrefetchInvalidatedOnSeekOutsideWindow) { + constexpr size_t k_size = 16 * 1024; + std::vector expected(k_size); + fill_pattern(expected); + setup_temp_file(expected); + + util::ParallelReadStreamBuf buf(m_tmp_path, /*header_offset=*/0, /*threshold=*/1); + std::istream stream(&buf); + + // Prefetch the first 4 KiB only. + constexpr size_t k_prefetch = 4 * 1024; + ASSERT_TRUE(buf.prefetch(static_cast(k_prefetch))); + + // Seek past the prefetched window – this must drop the buffer. + constexpr std::streamoff k_seek_to = 10 * 1024; + stream.seekg(k_seek_to, std::ios::beg); + ASSERT_TRUE(stream.good()); + + // Read 512 bytes from the new position; the data must still match expected. + constexpr size_t k_read = 512; + std::vector got(k_read); + ASSERT_TRUE(stream.read(got.data(), static_cast(k_read))); + std::vector slice(expected.begin() + k_seek_to, expected.begin() + k_seek_to + k_read); + EXPECT_EQ(got, slice); +} + +// Prefetch at EOF or with zero size returns false without changing state. +TEST_F(ParallelReadStreamBufTest, PrefetchEdgeCases) { + constexpr size_t k_size = 1024; + std::vector expected(k_size); + fill_pattern(expected); + setup_temp_file(expected); + + util::ParallelReadStreamBuf buf(m_tmp_path, /*header_offset=*/0, /*threshold=*/1); + std::istream stream(&buf); + + // Zero-size prefetch is a no-op. + EXPECT_FALSE(buf.prefetch(0)); + + // Drain the whole file first, then prefetch past EOF must return false. + std::vector got(k_size); + ASSERT_TRUE(stream.read(got.data(), static_cast(k_size))); + EXPECT_FALSE(buf.prefetch(static_cast(k_size))); +} + +} // namespace ov::test diff --git a/src/inference/tests/unit/single_file_storage.cpp b/src/inference/tests/unit/single_file_storage.cpp index 955990cf49cc..919caabf8172 100644 --- a/src/inference/tests/unit/single_file_storage.cpp +++ b/src/inference/tests/unit/single_file_storage.cpp @@ -193,6 +193,49 @@ TEST_F(SingleFileStorageTest, AppendOnlyCacheEntry) { EXPECT_TRUE(read_called); } +// Large blob test: write >= 4 MB so that the real DEFAULT_THRESHOLD (4 MB) in +// ParallelReadStreamBuf is crossed on the non-mmap read path. This exercises +// the SingleFileStorage → ParallelReadStreamBuf integration end-to-end, +// including the blob alignment padding before the data and the non-zero +// header_offset passed to the streambuf constructor. +TEST_F(SingleFileStorageTest, WriteReadLargeBlob_ParallelPath) { + // 5 MB + 1 byte so that even after padding the blob data itself exceeds the + // 4 MB parallel threshold. + constexpr size_t k_blob_size = 5UL * 1024 * 1024 + 1; + const std::string blob_id = "999"; + + // Build a deterministic pattern so byte-exact comparison is possible. + std::vector expected(k_blob_size); + for (size_t i = 0; i < k_blob_size; ++i) { + expected[i] = static_cast(i % 251u); + } + + m_storage->write_cache_entry(blob_id, [&](std::ostream& s) { + s.write(reinterpret_cast(expected.data()), static_cast(k_blob_size)); + }); + + // --- non-mmap path (ParallelReadStreamBuf) --- + m_storage->read_cache_entry(blob_id, /*enable_mmap=*/false, [&](const ICacheManager::CompiledBlobVariant& cv) { + auto& stream = std::get>(cv).get(); + + std::vector got(k_blob_size); + ASSERT_TRUE(stream.read(reinterpret_cast(got.data()), static_cast(k_blob_size))); + EXPECT_EQ(got, expected) << "Parallel read returned incorrect data for large blob"; + }); + + // Reset and re-open to ensure the content survives a round-trip through + // build_content_index (re-scans the file and rebuilds m_blob_index). + m_storage.reset(); + SingleFileStorage reopened(m_file_path); + reopened.read_cache_entry(blob_id, /*enable_mmap=*/false, [&](const ICacheManager::CompiledBlobVariant& cv) { + auto& stream = std::get>(cv).get(); + + std::vector got(k_blob_size); + ASSERT_TRUE(stream.read(reinterpret_cast(got.data()), static_cast(k_blob_size))); + EXPECT_EQ(got, expected) << "Parallel read after re-open returned incorrect data for large blob"; + }); +} + TEST_F(SingleFileStorageTest, ContextMetaWriteRead) { weight_sharing::Context test_context; test_context.m_weight_registry[1][11] = {100, 200, element::Type_t::f32}; diff --git a/src/plugins/auto/tests/functional/shared_tests_instances/behavior/ov_executable_network/exec_network_base.cpp b/src/plugins/auto/tests/functional/shared_tests_instances/behavior/ov_executable_network/exec_network_base.cpp index 67518b8b7113..82317659eb23 100644 --- a/src/plugins/auto/tests/functional/shared_tests_instances/behavior/ov_executable_network/exec_network_base.cpp +++ b/src/plugins/auto/tests/functional/shared_tests_instances/behavior/ov_executable_network/exec_network_base.cpp @@ -5,6 +5,9 @@ #include "behavior/compiled_model/compiled_model_base.hpp" using namespace ov::test::behavior; + +GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(CompiledModelSetType); + namespace { const std::vector multiConfigs = {{ov::device::priorities(ov::test::utils::DEVICE_TEMPLATE)}}; diff --git a/src/plugins/auto/tests/functional/shared_tests_instances/behavior/ov_plugin/core_integration.cpp b/src/plugins/auto/tests/functional/shared_tests_instances/behavior/ov_plugin/core_integration.cpp index 12bfd8376fff..5db3a29fb468 100644 --- a/src/plugins/auto/tests/functional/shared_tests_instances/behavior/ov_plugin/core_integration.cpp +++ b/src/plugins/auto/tests/functional/shared_tests_instances/behavior/ov_plugin/core_integration.cpp @@ -12,6 +12,12 @@ using namespace ov::test::behavior; +GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(OVClassModelOptionalTestP); +GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(OVClassModelTestP); +GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(OVClassQueryModelTest); +GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(OVClassSeveralDevicesTestCompileModel); +GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(OVClassSeveralDevicesTestQueryModel); + // defined in plugin_name.cpp extern const char* cpu_plugin_file_name; diff --git a/src/plugins/auto/tests/unit/ctput_test.cpp b/src/plugins/auto/tests/unit/ctput_test.cpp index 68d4788596a9..544ad7b979b6 100644 --- a/src/plugins/auto/tests/unit/ctput_test.cpp +++ b/src/plugins/auto/tests/unit/ctput_test.cpp @@ -176,11 +176,14 @@ TEST_P(AutoCTPUTCallMulti, CTPUTDeviceLoadFailedNoExceptionThrowTest) { std::shared_ptr exeNetwork; config.insert({ov::hint::performance_mode(ov::hint::PerformanceMode::CUMULATIVE_THROUGHPUT)}); config.insert(ov::device::priorities(targetDev)); - ON_CALL(*core, - compile_model(::testing::Matcher&>(_), - ::testing::Matcher(StrEq(loadFailedDevice)), - ::testing::Matcher(_))) - .WillByDefault(ov::Throw("GeneralError")); + // gmock 1.11+ reports calls not matching any EXPECT_CALL as "unexpected" + // once any expectation exists for this method, so promote the failing + // device setup from ON_CALL to EXPECT_CALL. + EXPECT_CALL(*core, + compile_model(::testing::Matcher&>(_), + ::testing::Matcher(StrEq(loadFailedDevice)), + ::testing::Matcher(_))) + .WillRepeatedly(ov::Throw("GeneralError")); if (loadFailedDevice != ov::test::utils::DEVICE_CPU) { EXPECT_CALL(*core, compile_model(::testing::Matcher&>(_), diff --git a/src/plugins/auto/tests/unit/parse_meta_device_test.cpp b/src/plugins/auto/tests/unit/parse_meta_device_test.cpp index a9abc1896547..eba87e28a434 100644 --- a/src/plugins/auto/tests/unit/parse_meta_device_test.cpp +++ b/src/plugins/auto/tests/unit/parse_meta_device_test.cpp @@ -147,7 +147,7 @@ const std::vector testConfigs = { {"GPU.0", {}, -1, "", std::string(igpuFullDeviceName) + "_0", 1}, {"GPU.1", {}, -1, "", std::string(dgpuFullDeviceName) + "_1", 1}}, false, - 3}, + 4}, ConfigParams{"CPU(1),GPU(2),OTHER(4)", {{"CPU", {}, 1, "", "CPU_", 0}, {"GPU.0", {}, 2, "", std::string(igpuFullDeviceName) + "_0", 1}, @@ -158,7 +158,7 @@ const std::vector testConfigs = { ConfigParams{"CPU(-1),GPU,OTHER", {}, true, 0}, ConfigParams{"CPU(NA),GPU,OTHER", {}, true, 0}, - ConfigParams{"INVALID_DEVICE", {}, false, 0}, + ConfigParams{"INVALID_DEVICE", {}, false, 1}, // GPU will be expanded to GPU.0 and GPU.1 with same device priority(0). ConfigParams{"GPU", { diff --git a/src/plugins/hetero/src/subgraph_collector.cpp b/src/plugins/hetero/src/subgraph_collector.cpp index a9a5b269e98a..23218c2d3054 100644 --- a/src/plugins/hetero/src/subgraph_collector.cpp +++ b/src/plugins/hetero/src/subgraph_collector.cpp @@ -4,7 +4,18 @@ #include "subgraph_collector.hpp" -#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(_MSC_VER) +# include +#endif #include "graph_debug_dump.hpp" #include "op/device_subgraph.hpp" @@ -19,27 +30,6 @@ #include "transformations/utils/utils.hpp" namespace { -template -static Set intersection(const Set& lhs, const Set& rhs) { - Set result; - const auto& min_size_set = (lhs.size() < rhs.size()) ? lhs : rhs; - const auto& max_size_set = (lhs.size() >= rhs.size()) ? lhs : rhs; - for (auto&& val : min_size_set) - if (max_size_set.find(val) != max_size_set.end()) - result.insert(val); - return result; -} - -template -static bool intersects(const Set& lhs, const Set& rhs) { - const auto& min_size_set = (lhs.size() < rhs.size()) ? lhs : rhs; - const auto& max_size_set = (lhs.size() >= rhs.size()) ? lhs : rhs; - for (auto&& val : min_size_set) - if (max_size_set.find(val) != max_size_set.end()) - return true; - return false; -} - template static std::vector addition(const std::vector& vector1, const std::vector& vector2) { std::vector addition; @@ -77,12 +67,10 @@ ov::hetero::SubgraphCollector::SubgraphCollector(const std::shared_ptrinputs(); - auto& node_input_dependency = _node_input_dependencies[node]; - for (const auto& input : inputs) { - node_input_dependency.insert(input); - auto& input_dependency = _node_input_dependencies[input_node(input)]; - node_input_dependency.insert(input_dependency.begin(), input_dependency.end()); - if (_affinities.at(node) != _affinities.at(input_node(input))) { + const auto& node_affinity = _affinities.at(node); + for (const auto& input : node->inputs()) { + const auto source = input_node(input); + if (node_affinity != _affinities.at(source)) { if (ov::op::util::is_output(node)) { - _affinities[node] = _affinities.at(input_node(input)); + _affinities[node] = _affinities.at(source); } else { _subgraph_inputs.insert(input); } @@ -113,92 +98,672 @@ void ov::hetero::SubgraphCollector::init() { } } } -void ov::hetero::SubgraphCollector::split_cyclic_dependencies() { + +ov::hetero::SubgraphCollector::SubgraphIdsMap ov::hetero::SubgraphCollector::split_cyclic_dependencies() { + // Iteratively detect cross-subgraph cycles (subgraph A feeds B and B feeds A) and break them + // by promoting offending edges into _subgraph_inputs until no new boundaries are added. + // + // Returns the final SubgraphIdsMap (valid w.r.t. _subgraph_inputs at return), so the caller + // does not re-run collect_subgraphs_ids(). The map is also reused across loop boundaries: + // the per-node loop's last iteration always exits without adding boundaries (loop + // condition), so its `subgraph_ids` is still valid for the SCC loop's first iteration; and + // each SCC iteration only recomputes after it actually modified _subgraph_inputs. + const size_t nodes_count = _ordered_ops.size(); + std::unordered_map node_to_index; + node_to_index.reserve(nodes_count); + std::vector ordered_inputs(nodes_count); + std::vector> output_consumer_counts(nodes_count); + for (size_t i = 0; i < nodes_count; ++i) { + node_to_index.emplace(_ordered_ops[i].get(), i); + ordered_inputs[i] = _ordered_ops[i]->inputs(); + const auto outputs = _ordered_ops[i]->outputs(); + auto& consumer_counts = output_consumer_counts[i]; + consumer_counts.reserve(outputs.size()); + for (const auto& output : outputs) { + consumer_counts.push_back(output.get_target_inputs().size()); + } + } + + auto get_index_by_node = [&node_to_index](const ov::Node* node) { + const auto it = node_to_index.find(node); + OPENVINO_ASSERT(it != node_to_index.end()); + return it->second; + }; + + // Bitset helpers. Subgraph-input dependency sets are represented as packed + // 64-bit words indexed by a dense id assigned to each member of + // _subgraph_inputs at the start of every outer iteration. This replaces the + // previous std::set-based propagation: union/intersect/test become + // O(S/64) bitwise ops with no per-element hashing/comparator/allocation, + // typically ~50-100x faster for graphs with hundreds of subgraph inputs. + using Bits = std::vector; + auto ctz64 = [](uint64_t x) -> unsigned { + // Precondition: x != 0. Both __builtin_ctzll(0) and _BitScanForward64 with a zero + // mask are undefined; all call sites guard with `while (bits)` before invoking. +#if defined(_MSC_VER) && (defined(_M_X64) || defined(_M_AMD64)) + unsigned long idx; + _BitScanForward64(&idx, x); + return static_cast(idx); +#elif defined(_MSC_VER) + unsigned idx = 0; + while ((x & 1ULL) == 0) { + x >>= 1; + ++idx; + } + return idx; +#else + return static_cast(__builtin_ctzll(x)); +#endif + }; + auto set_bit = [](Bits& b, size_t i) { + b[i >> 6] |= (1ULL << (i & 63)); + }; + auto bit_or = [](Bits& a, const Bits& b) { + for (size_t i = 0; i < a.size(); ++i) + a[i] |= b[i]; + }; + auto bit_intersects = [](const Bits& a, const Bits& b) { + for (size_t i = 0; i < a.size(); ++i) + if (a[i] & b[i]) + return true; + return false; + }; + auto bit_any = [](const Bits& a) { + for (uint64_t v : a) + if (v) + return true; + return false; + }; + auto bit_all_of = [&](const Bits& a, const auto& pred) { + for (size_t i = 0; i < a.size(); ++i) { + uint64_t bits = a[i]; + while (bits) { + const size_t b = (i << 6) + ctz64(bits); + bits &= bits - 1; + if (!pred(b)) { + return false; + } + } + } + return true; + }; + + // Subgraph-ID state is shared across the per-node loop, the SCC loop, and the return value. + SubgraphIdsMap subgraph_ids; + std::vector subgraph_id_by_index(nodes_count); + // Split cyclic dependencies. for (size_t prev_subgraphs = 0, cyclic_split_step = 0; prev_subgraphs != _subgraph_inputs.size(); ++cyclic_split_step) { OPENVINO_ASSERT(cyclic_split_step < _ordered_ops.size(), "Cannot resolve cycles during submodels split!"); prev_subgraphs = _subgraph_inputs.size(); - auto subgraph_ids = collect_subgraphs_ids(); - // All inputs that belong to the same subgraph as node - NodeMap node_subgraph_input_dependencies; - // All inputs that depends on the same subgraph as node - NodeMap node_subgraph_cyclic_iput_dependencies; + subgraph_ids = collect_subgraphs_ids(); + for (const auto& node : _ordered_ops) { - auto& node_subgraph_input_dependency = node_subgraph_input_dependencies[node]; - auto all_node_subgraph_inputs = intersection(_node_input_dependencies[node], _subgraph_inputs); - for (const auto& subgraph_input : all_node_subgraph_inputs) { - if (subgraph_ids[node] == subgraph_ids[subgraph_input.get_node()->shared_from_this()]) { - node_subgraph_input_dependency.emplace(subgraph_input); + const auto index = get_index_by_node(node.get()); + subgraph_id_by_index[index] = subgraph_ids.at(node); + } + + // === Phase 1: assign a dense bit id to every current subgraph input. === + const size_t S = _subgraph_inputs.size(); + const size_t W = (S + 63) / 64; + struct InputHash { + size_t operator()(const Input& in) const noexcept { + // Input == {Node*, port_index}. Mix the pointer with the port to avoid + // collisions across multiple inputs of the same node. + const auto h1 = std::hash{}(in.get_node()); + const auto h2 = std::hash{}(in.get_index()); + return h1 ^ (h2 + 0x9e3779b97f4a7c15ULL + (h1 << 6) + (h1 >> 2)); + } + }; + std::unordered_map input_to_bit; + input_to_bit.reserve(S * 2); + std::vector bit_to_input; + bit_to_input.reserve(S); + for (const auto& in : _subgraph_inputs) { + input_to_bit.emplace(in, bit_to_input.size()); + bit_to_input.push_back(in); + } + + // === Phase 2: per-bit metadata used by the per-node classification step. === + std::vector bit_owner_subgraph(S); // subgraph of subgraph_input.get_node() + std::vector bit_producer_subgraph( + S); // subgraph of producing node (only meaningful when not graph input) + std::vector bit_is_graph_input(S); + for (size_t b = 0; b < S; ++b) { + const auto& in = bit_to_input[b]; + const auto owner = in.get_node(); + bit_owner_subgraph[b] = subgraph_id_by_index[get_index_by_node(owner)]; + const bool gi = is_graph_input_node(owner); + bit_is_graph_input[b] = gi ? 1 : 0; + bit_producer_subgraph[b] = gi ? static_cast(-1) + : subgraph_id_by_index[get_index_by_node(in.get_source_output().get_node())]; + } + + // === Phase 3: forward-propagate subgraph-input dependencies in topological order. === + // Equivalent to intersection(full_transitive_closure, _subgraph_inputs) + // but bounded by |_subgraph_inputs| via the bitset width. + std::vector node_input_deps(nodes_count, Bits(W, 0ULL)); + for (size_t node_idx = 0; node_idx < nodes_count; ++node_idx) { + const auto& node = _ordered_ops[node_idx]; + auto& deps = node_input_deps[node_idx]; + if (is_graph_input_node(node.get())) { + Input self_input{node.get(), 0}; + const auto it = input_to_bit.find(self_input); + if (it != input_to_bit.end()) { + set_bit(deps, it->second); + } + } else { + for (const auto& input : ordered_inputs[node_idx]) { + const auto it = input_to_bit.find(input); + if (it != input_to_bit.end()) { + set_bit(deps, it->second); + } + const auto source_idx = get_index_by_node(input.get_source_output().get_node()); + bit_or(deps, node_input_deps[source_idx]); } } - auto& node_subgraph_cyclic_input_dependency = node_subgraph_cyclic_iput_dependencies[node]; - for (const auto& subgraph_input : all_node_subgraph_inputs) { - if (!is_graph_input_node(subgraph_input.get_node()) && - subgraph_ids[node] == subgraph_ids[input_node(subgraph_input)]) { - node_subgraph_cyclic_input_dependency.emplace(subgraph_input); + } + + // === Phase 4a: classify each node's deps into same-subgraph and cyclic-feedback subsets. === + std::vector node_subgraph_input_dependencies(nodes_count, Bits(W, 0ULL)); + std::vector node_subgraph_cyclic_input_dependencies(nodes_count, Bits(W, 0ULL)); + for (size_t node_idx = 0; node_idx < nodes_count; ++node_idx) { + const auto& deps = node_input_deps[node_idx]; + const SubgraphId my_sg = subgraph_id_by_index[node_idx]; + auto& sg_dep = node_subgraph_input_dependencies[node_idx]; + auto& cyc_dep = node_subgraph_cyclic_input_dependencies[node_idx]; + for (size_t w = 0; w < W; ++w) { + uint64_t bits = deps[w]; + while (bits) { + const size_t b = (w << 6) + ctz64(bits); + bits &= bits - 1; + if (bit_owner_subgraph[b] == my_sg) { + set_bit(sg_dep, b); + } + if (!bit_is_graph_input[b] && bit_producer_subgraph[b] == my_sg) { + set_bit(cyc_dep, b); + } } } } - for (const auto& node : _ordered_ops) { - auto& node_subgraph_cyclic_input_dependency = node_subgraph_cyclic_iput_dependencies[node]; - if (!node_subgraph_cyclic_input_dependency.empty()) { - // Collect all subgraph inputs that cyclic subgraph output depends on - InputSet cyclic_inputs_dependencies; - for (const auto& cyclicInput : node_subgraph_cyclic_input_dependency) { - for (const auto& input : node_subgraph_input_dependencies[input_node(cyclicInput)]) { - cyclic_inputs_dependencies.emplace(input); + // === Phase 4b: for each node with cyclic feedback, promote offending edges into _subgraph_inputs. === + auto promote_boundaries_for_node = [&](size_t node_idx) { + const auto& cyc_dep = node_subgraph_cyclic_input_dependencies[node_idx]; + if (!bit_any(cyc_dep)) + return; + const SubgraphId my_sg = subgraph_id_by_index[node_idx]; + // Collect all subgraph inputs that the cyclic feedback transitively depends on. + Bits cyclic_inputs_dependencies(W, 0ULL); + for (size_t w = 0; w < W; ++w) { + uint64_t bits = cyc_dep[w]; + while (bits) { + const size_t b = (w << 6) + ctz64(bits); + bits &= bits - 1; + const auto& cyclic_input = bit_to_input[b]; + const auto cyclic_input_idx = get_index_by_node(cyclic_input.get_source_output().get_node()); + bit_or(cyclic_inputs_dependencies, node_subgraph_input_dependencies[cyclic_input_idx]); + } + } + // Also include dependencies at cycle re-entry points: boundary edges where + // data from another subgraph flows back into this one. Without this, the + // intersection check below misses edges that bridge independently-entered + // nodes (e.g., a shared constant) to the cycle's return path. + const auto& sg_dep = node_subgraph_input_dependencies[node_idx]; + for (size_t w = 0; w < W; ++w) { + uint64_t bits = sg_dep[w]; + while (bits) { + const size_t b = (w << 6) + ctz64(bits); + bits &= bits - 1; + if (!bit_is_graph_input[b] && bit_owner_subgraph[b] == my_sg && bit_producer_subgraph[b] != my_sg) { + const auto owner_idx = get_index_by_node(bit_to_input[b].get_node()); + bit_or(cyclic_inputs_dependencies, node_subgraph_input_dependencies[owner_idx]); } } - for (const auto& input : node->inputs()) { - auto& input_node_subgraph_cyclic_input_dependency = - node_subgraph_cyclic_iput_dependencies[input_node(input)]; - auto& input_node_subgraph_input_dependency = node_subgraph_input_dependencies[input_node(input)]; - if (!intersects(node_subgraph_cyclic_input_dependency, - input_node_subgraph_cyclic_input_dependency) && - intersects(cyclic_inputs_dependencies, input_node_subgraph_input_dependency)) { + } + for (const auto& input : ordered_inputs[node_idx]) { + const auto input_source_idx = get_index_by_node(input.get_source_output().get_node()); + const auto& src_cyc_dep = node_subgraph_cyclic_input_dependencies[input_source_idx]; + const auto& src_sg_dep = node_subgraph_input_dependencies[input_source_idx]; + if (!bit_intersects(cyc_dep, src_cyc_dep) && bit_intersects(cyclic_inputs_dependencies, src_sg_dep)) { + const auto source_output = input.get_source_output(); + const bool single_consumer_graph_input_leaf = + output_consumer_counts[input_source_idx][source_output.get_index()] == 1 && + !is_graph_input_node(source_output.get_node()) && !bit_any(src_cyc_dep) && + bit_all_of(src_sg_dep, [&](size_t b) { + const auto& traced_input = bit_to_input[b]; + if (is_graph_input_node(traced_input.get_node())) { + return true; + } + const auto* traced_producer = traced_input.get_source_output().get_node(); + return is_graph_input_node(traced_producer); + }); + if (!single_consumer_graph_input_leaf) { _subgraph_inputs.insert(input); } } } + }; + for (size_t node_idx = 0; node_idx < nodes_count; ++node_idx) { + promote_boundaries_for_node(node_idx); + } + } + + // === Subgraph-level SCC fallback. =========================================================== + // The per-node heuristic above only detects cycles whose re-entry point sits on a node whose + // own cyc_dep bitset is non-empty (same-sg data flows back through a foreign sg into that + // node's inputs). Two classes of subgraph-DAG cycles fall outside its scope, and both are + // first-class cases this fallback exists to handle -- neither is exceptional: + // + // (a) Multi-hop subgraph-DAG cycles (sg_A -> sg_B -> sg_C -> sg_D -> sg_A) where the + // producer and re-entry consumer are several subgraphs apart and no single node sees + // its own sg on the cycle. + // (b) Shared-graph-input cycles, where a Constant (or other graph input) fans out to + // multiple consumers that Union-Find fuses into a single subgraph, and that fused + // subgraph then both produces and consumes data on the same neighbor subgraph. The + // cut edge here is an input of the foreign-sg node, not of the same-sg node whose + // cyc_dep is non-empty, so Phase 4b cannot promote it by construction. + // + // Both arise from Union-Find merging structurally independent regions via shared inputs. + // The ov::Model itself is a DAG; the cycle is purely an artifact of subgraph fusion that + // run()'s topological sort cannot resolve. + // + // Break the cycle by identifying non-trivial SCCs in the subgraph DAG and, per iteration, + // isolating one node out of some SCC-member Union-Find component by promoting all of its + // same-sg input edges to boundary (see isolate_one_scc_node for the rationale and the + // convergence argument). The loop is bounded by the total number of node-input edges; in + // practice it converges in ~#SCC iterations. + + // Helper 1: build the subgraph DAG from cross-subgraph edges already recorded in + // _subgraph_inputs. Parallel edges between the same pair of subgraphs are de-duplicated; + // self-edges (producer_sg == owner_sg) are filtered so single-subgraph SCCs cannot arise. + using SgAdj = std::unordered_map>; + auto build_subgraph_adjacency = + [&](const std::vector& sg_id_by_index) -> std::pair> { + SgAdj adj; + std::unordered_set all_sgs; + for (size_t i = 0; i < nodes_count; ++i) { + all_sgs.insert(sg_id_by_index[i]); + } + for (const auto& inp : _subgraph_inputs) { + if (is_graph_input_node(inp.get_node())) + continue; + const auto owner_sg = sg_id_by_index[get_index_by_node(inp.get_node())]; + const auto producer_sg = sg_id_by_index[get_index_by_node(inp.get_source_output().get_node())]; + if (owner_sg == producer_sg) + continue; + adj[producer_sg].insert(owner_sg); + } + return {std::move(adj), std::move(all_sgs)}; + }; + + // Helper 2: return the set of subgraphs that belong to any non-trivial SCC of `adj`, using + // iterative Tarjan. An exact SCC algorithm is required here: a two-peel (forward + reverse + // Kahn) approximation also flags acyclic bridges between two disjoint cycles (e.g. X in + // A<->B -> X -> C<->D survives both peels), which would either waste a promotion on an + // acyclic subgraph or trip the "no internal edge" assert below when the bridge subgraph has + // no same-sg edge. The loop is iterative to avoid recursion depth issues on large partitions. + auto find_non_trivial_scc_members = + [](const SgAdj& adj, const std::unordered_set& all_sgs) -> std::unordered_set { + std::unordered_set scc_members; + std::unordered_map index_of; + std::unordered_map lowlink; + std::unordered_set on_stack; + std::vector tarjan_stack; + int next_index = 0; + struct Frame { + SubgraphId v; + std::vector neighbors; + size_t next_neighbor; + }; + std::vector call_stack; + auto neighbors_of = [&adj](SubgraphId v) { + std::vector out; + const auto it = adj.find(v); + if (it != adj.end()) + out.assign(it->second.begin(), it->second.end()); + return out; + }; + auto open_node = [&](SubgraphId v) { + index_of[v] = next_index; + lowlink[v] = next_index; + ++next_index; + tarjan_stack.push_back(v); + on_stack.insert(v); + call_stack.push_back({v, neighbors_of(v), 0}); + }; + for (auto start : all_sgs) { + if (index_of.count(start)) + continue; + open_node(start); + while (!call_stack.empty()) { + auto& frame = call_stack.back(); + if (frame.next_neighbor < frame.neighbors.size()) { + const auto w = frame.neighbors[frame.next_neighbor++]; + if (!index_of.count(w)) { + open_node(w); + } else if (on_stack.count(w)) { + lowlink[frame.v] = std::min(lowlink[frame.v], index_of[w]); + } + } else { + const auto v = frame.v; + if (lowlink[v] == index_of[v]) { + std::vector comp; + while (true) { + const auto w = tarjan_stack.back(); + tarjan_stack.pop_back(); + on_stack.erase(w); + comp.push_back(w); + if (w == v) + break; + } + // Only non-trivial SCCs (size > 1) represent real cycles in the subgraph + // DAG; singletons are reported by Tarjan even for nodes with no cycle and + // must be excluded. Self-loops were filtered out by build_subgraph_adjacency. + if (comp.size() > 1) { + for (auto m : comp) + scc_members.insert(m); + } + } + const auto finished = frame.v; + call_stack.pop_back(); + if (!call_stack.empty()) { + lowlink[call_stack.back().v] = std::min(lowlink[call_stack.back().v], lowlink[finished]); + } + } + } } + return scc_members; + }; + + // Helper 3: isolate one Union-Find node from its SCC member by promoting ALL its + // same-subgraph non-boundary input edges into _subgraph_inputs. Returns the number of + // edges promoted (1 .. node's input arity). + // + // Rationale (why this works and the simpler alternatives don't): + // * Promoting a single same-sg input edge per iteration diverges: the chosen node still + // re-merges into the SCC via its OTHER same-sg inputs in the next collect_subgraphs_ids + // round, and "first-input-wins" union-find keeps it in the same component. Observed on + // yolo26s-seg: SCC member count grew 4 -> 26 across iterations. + // * Promoting only edges at entry/exit points of SCC members misses the common + // "shared-Constant fuses regions" case: S = {c_shared, a, b, c, ...} where c_shared is + // a Constant unioning multiple consumers. c_shared has no same-sg consumers in OTHER + // SCC members (its consumers are all in S), so it is neither an entry nor an exit, and + // the only same-sg input that would break the cycle — (a <- c_shared) — is skipped. + // * Dissolving a whole SCC-member subgraph at once explodes the partition. On + // yolo26s-seg the GPU mainland S has 428 nodes / 449 internal edges; full dissolution + // produces ~450 subgraphs and breaks downstream compile_model. + // + // The "isolate one node" cut is the minimum needed: by promoting all of n's same-sg + // inputs, n becomes a Union-Find root on the next round, severed from every upstream node + // in S (including shared-Constant connectors). Each iteration thus strictly reduces the + // size of some SCC member by 1 (n moves to its own singleton component). + // + // Convergence: + // * In any non-trivial SCC (size > 1) of the subgraph DAG, at least one member is not a + // Union-Find singleton: if ALL members were singletons, the SCC-DAG cycle + // sg_X1 -> ... -> sg_Xk -> sg_X1 would unfold into a node-level cycle + // x1 -> ... -> xk -> x1 in the original ov::Model, which is a DAG. + // * A non-singleton Union-Find component of size m has exactly m-1 unification edges, + // i.e. m-1 non-boundary input edges, so at least one node in it has a same-sg input. + // * Each iteration isolates one such node, strictly reducing the total non-singleton + // mass of SCC members. The loop therefore terminates in at most nodes_count iterations + // and well within the total_node_inputs edge budget. + // + // Target selection: among all candidate nodes (in any SCC member with >= 1 same-sg input), + // prefer cuts at actual SCC re-entry nodes and shared connectors. Falling back to the node + // with the fewest same-sg inputs is still valid for convergence, but doing so too early may + // peel ordinary linear compute nodes out of the main device region and create tiny + // Parameter->op->Result submodels. Those are especially expensive for GPU compilation. + auto isolate_one_scc_node = [&](const std::vector& sg_id_by_index, + const std::unordered_set& scc_members) -> size_t { + struct CandidateRank { + size_t lacks_scc_boundary_input = 1; + size_t lacks_shared_same_sg_source = 1; + size_t has_trivial_leaf_input = 1; + size_t is_linear_compute_node = 1; + size_t same_sg_inputs = 0; + size_t node_idx = 0; + }; + + auto is_better_rank = [](const CandidateRank& lhs, const CandidateRank& rhs) { + if (lhs.lacks_scc_boundary_input != rhs.lacks_scc_boundary_input) + return lhs.lacks_scc_boundary_input < rhs.lacks_scc_boundary_input; + if (lhs.lacks_shared_same_sg_source != rhs.lacks_shared_same_sg_source) + return lhs.lacks_shared_same_sg_source < rhs.lacks_shared_same_sg_source; + if (lhs.has_trivial_leaf_input != rhs.has_trivial_leaf_input) + return lhs.has_trivial_leaf_input < rhs.has_trivial_leaf_input; + if (lhs.is_linear_compute_node != rhs.is_linear_compute_node) + return lhs.is_linear_compute_node < rhs.is_linear_compute_node; + if (lhs.same_sg_inputs != rhs.same_sg_inputs) + return lhs.same_sg_inputs < rhs.same_sg_inputs; + return lhs.node_idx < rhs.node_idx; + }; + + auto count_non_result_consumers = [](const std::shared_ptr& node) { + size_t non_result_consumers = 0; + for (const auto& output : node->outputs()) { + for (const auto& target_input : output.get_target_inputs()) { + if (!ov::op::util::is_output(target_input.get_node())) { + ++non_result_consumers; + } + } + } + return non_result_consumers; + }; + std::vector non_result_consumer_counts(nodes_count, static_cast(-1)); + auto count_non_result_consumers_by_index = [&](size_t node_idx) { + auto& cached = non_result_consumer_counts[node_idx]; + if (cached == static_cast(-1)) { + cached = count_non_result_consumers(_ordered_ops[node_idx]); + } + return cached; + }; + + bool have_target = false; + size_t target_idx = 0; + CandidateRank target_rank; + auto is_graph_input_leaf_source = [&](size_t node_idx) { + const auto& node = _ordered_ops[node_idx]; + if (is_graph_input_node(node.get())) + return false; + + if (count_non_result_consumers_by_index(node_idx) != 1) + return false; + + for (const auto& input : ordered_inputs[node_idx]) { + if (!is_graph_input_node(input.get_source_output().get_node())) + return false; + } + return true; + }; + for (size_t i = 0; i < nodes_count; ++i) { + const auto my_sg = sg_id_by_index[i]; + if (!scc_members.count(my_sg)) + continue; + size_t same_sg_inputs = 0; + bool has_scc_boundary_input = false; + bool has_shared_same_sg_source = false; + bool has_trivial_leaf_input = false; + for (const auto& input : ordered_inputs[i]) { + if (_subgraph_inputs.count(input)) { + if (!is_graph_input_node(input.get_node())) { + const auto src_idx = get_index_by_node(input.get_source_output().get_node()); + const auto producer_sg = sg_id_by_index[src_idx]; + has_scc_boundary_input = + has_scc_boundary_input || (producer_sg != my_sg && scc_members.count(producer_sg)); + } + continue; + } + const auto src_idx = get_index_by_node(input.get_source_output().get_node()); + if (sg_id_by_index[src_idx] != my_sg) + continue; + ++same_sg_inputs; + has_shared_same_sg_source = + has_shared_same_sg_source || count_non_result_consumers_by_index(src_idx) > 1; + has_trivial_leaf_input = has_trivial_leaf_input || is_graph_input_leaf_source(src_idx); + } + if (same_sg_inputs == 0) + continue; + + const CandidateRank candidate_rank{ + has_scc_boundary_input ? 0UL : 1UL, + has_shared_same_sg_source ? 0UL : 1UL, + has_trivial_leaf_input ? 1UL : 0UL, + (same_sg_inputs == 1 && count_non_result_consumers_by_index(i) <= 1) ? 1UL : 0UL, + same_sg_inputs, + i}; + const bool better_target = !have_target || is_better_rank(candidate_rank, target_rank); + if (better_target) { + have_target = true; + target_idx = i; + target_rank = candidate_rank; + } + } + OPENVINO_ASSERT(have_target, + "Subgraph SCC fallback found a cyclic subgraph DAG but every node in " + "every SCC member is a Union-Find singleton; that would require a " + "node-level cycle in the original ov::Model, which is impossible on a DAG."); + + size_t promoted = 0; + const auto target_sg = sg_id_by_index[target_idx]; + for (const auto& input : ordered_inputs[target_idx]) { + if (_subgraph_inputs.count(input)) + continue; + const auto src_idx = get_index_by_node(input.get_source_output().get_node()); + if (sg_id_by_index[src_idx] != target_sg) + continue; + _subgraph_inputs.insert(input); + ++promoted; + } + return promoted; + }; + + size_t total_node_inputs = 0; + for (size_t i = 0; i < nodes_count; ++i) { + total_node_inputs += ordered_inputs[i].size(); + } + // subgraph_ids / subgraph_id_by_index reach this point already valid w.r.t. the current + // _subgraph_inputs: the per-node loop exits only when its last iteration adds no boundaries, + // so the ids it computed at the top of that final iteration are still in sync. Recompute + // only after the SCC step actually modifies _subgraph_inputs. + bool ids_valid = true; + for (size_t scc_step = 0;; ++scc_step) { + OPENVINO_ASSERT(scc_step < total_node_inputs + 1, + "Subgraph SCC fallback did not converge: exceeded node-input edge budget"); + if (!ids_valid) { + subgraph_ids = collect_subgraphs_ids(); + for (size_t i = 0; i < nodes_count; ++i) { + subgraph_id_by_index[i] = subgraph_ids.at(_ordered_ops[i]); + } + ids_valid = true; + } + const size_t inputs_before_step = _subgraph_inputs.size(); + + const auto sg_graph = build_subgraph_adjacency(subgraph_id_by_index); + const auto& sg_adj = sg_graph.first; + const auto& all_sgs = sg_graph.second; + const auto scc_members = find_non_trivial_scc_members(sg_adj, all_sgs); + if (scc_members.empty()) { + break; // subgraph DAG is acyclic, fix-point reached. + } + + // Isolate one Union-Find node from any SCC member by promoting ALL its same-sg input + // edges. See isolate_one_scc_node for why a single-edge cut diverges, why entry/exit + // cuts miss shared-Constant SCCs, and the convergence argument (the candidate always + // exists because singleton-only SCCs are impossible on a DAG). + const size_t promoted = isolate_one_scc_node(subgraph_id_by_index, scc_members); + OPENVINO_ASSERT(promoted > 0, + "Subgraph SCC fallback found a cyclic subgraph DAG but the chosen node " + "had no same-subgraph inputs to promote; helper invariant violated."); + // Defensive: each iteration must grow _subgraph_inputs strictly. If insert() ever found + // all promoted edges already present (logic bug), surface it here instead of looping + // silently until the edge budget runs out. + OPENVINO_ASSERT(_subgraph_inputs.size() > inputs_before_step, + "Subgraph SCC fallback promoted edges but _subgraph_inputs did not grow"); + ids_valid = false; // _subgraph_inputs grew; next iteration must rebuild ids. + } + + // Edge case: if init() produced no _subgraph_inputs at all, the per-node loop never ran and + // subgraph_ids is empty. Materialize the final mapping in that case. + if (subgraph_ids.empty()) { + subgraph_ids = collect_subgraphs_ids(); } + return subgraph_ids; } ov::hetero::SubgraphCollector::SubgraphIdsMap ov::hetero::SubgraphCollector::collect_subgraphs_ids() { - std::deque subgraph_ids; - NodeMap subgraph_id_ptrs; - for (const auto& node : _ordered_ops) { - auto all_node_inputs = node->inputs(); - InputVector inputs; - for (const auto& input : all_node_inputs) { + // Assign a SubgraphId to every node via Union-Find: nodes connected by non-boundary edges + // share an id. IDs are allocated as a counter at root creation, and merges keep the first + // input's id (matches the original numbering for backward-compatible mapping/tests). + const size_t n = _ordered_ops.size(); + std::unordered_map idx; + idx.reserve(n); + for (size_t i = 0; i < n; ++i) { + idx.emplace(_ordered_ops[i].get(), i); + } + + std::vector parent(n); + std::iota(parent.begin(), parent.end(), size_t{0}); + std::vector comp_id(n, -1); + auto find = [&parent](size_t x) { + while (parent[x] != x) { + parent[x] = parent[parent[x]]; + x = parent[x]; + } + return x; + }; + // Always parent `b`'s root under `a`'s root, so `a`'s root stays canonical + // and its comp_id (the first input's id) wins. + auto unite_keep_first = [&](size_t a, size_t b) { + const auto ra = find(a); + const auto rb = find(b); + if (ra != rb) { + parent[rb] = ra; + } + }; + + SubgraphId counter = 0; + InputVector srcs_buf; + for (size_t i = 0; i < n; ++i) { + srcs_buf.clear(); + for (const auto& input : _ordered_ops[i]->inputs()) { if (_subgraph_inputs.find(input) == _subgraph_inputs.end()) { - inputs.emplace_back(std::move(input)); + srcs_buf.emplace_back(input); } } - if (inputs.empty()) { - subgraph_ids.push_back(static_cast(subgraph_ids.size())); - subgraph_id_ptrs.emplace(node, &(subgraph_ids.back())); + if (srcs_buf.empty()) { + comp_id[find(i)] = counter++; } else { - auto first_input_subgraph_id_ptr = subgraph_id_ptrs[input_node(inputs.front())]; - for (const auto& input : inputs) { - auto input_id = *subgraph_id_ptrs[input_node(input)]; - for (auto& subgraph_id : subgraph_ids) { - if (subgraph_id == input_id) { - subgraph_id = *first_input_subgraph_id_ptr; - } - } + const auto first_src_it = idx.find(srcs_buf.front().get_source_output().get_node()); + OPENVINO_ASSERT(first_src_it != idx.end()); + const size_t first_src = first_src_it->second; + const SubgraphId first_id = comp_id[find(first_src)]; + unite_keep_first(first_src, i); + for (size_t k = 1; k < srcs_buf.size(); ++k) { + const auto src_it = idx.find(srcs_buf[k].get_source_output().get_node()); + OPENVINO_ASSERT(src_it != idx.end()); + unite_keep_first(first_src, src_it->second); } - subgraph_id_ptrs.emplace(node, first_input_subgraph_id_ptr); + // first_src's root is preserved by unite_keep_first; reassert its id. + comp_id[find(first_src)] = first_id; } } + SubgraphIdsMap result; - for (const auto& subgraph_id_ptr : subgraph_id_ptrs) { - result.emplace(subgraph_id_ptr.first, *(subgraph_id_ptr.second)); + result.reserve(n); + for (size_t i = 0; i < n; ++i) { + const auto id = comp_id[find(i)]; + OPENVINO_ASSERT(id != static_cast(-1), + "SubgraphCollector: node '", + _ordered_ops[i]->get_friendly_name(), + "' was not assigned a subgraph id (Union-Find invariant violated)."); + result.emplace(_ordered_ops[i], id); } return result; } + void ov::hetero::SubgraphCollector::split_subgraphs_by_parameter_results() { // Sort subgraph inputs by order InputVector ordered_subgraph_inputs; diff --git a/src/plugins/hetero/src/subgraph_collector.hpp b/src/plugins/hetero/src/subgraph_collector.hpp index 4d16ad5554e7..428e87e1f700 100644 --- a/src/plugins/hetero/src/subgraph_collector.hpp +++ b/src/plugins/hetero/src/subgraph_collector.hpp @@ -55,7 +55,9 @@ class SubgraphCollector { private: void init(); bool is_graph_input_node(const ov::Node* node) const; - void split_cyclic_dependencies(); + // Splits cyclic subgraph dependencies and returns the final SubgraphIdsMap valid + // w.r.t. the resulting _subgraph_inputs, so the caller does not need to recompute it. + SubgraphIdsMap split_cyclic_dependencies(); void split_subgraphs_by_parameter_results(); SubgraphIdsMap collect_subgraphs_ids(); std::unordered_map collect_subgraphs(); @@ -70,7 +72,6 @@ class SubgraphCollector { ov::ParameterVector _intermediate_parameters; ov::ResultVector _intermediate_results; AffinitiesMap _affinities; - NodeMap _node_input_dependencies; InputSet _subgraph_inputs; SubgraphIdsMap _subgraph_ids; ParameterResultMap _subgraph_parameter_to_prev_result; diff --git a/src/plugins/hetero/tests/functional/shared_tests_instances/behavior/ov_compiled_model/compiled_model_base.cpp b/src/plugins/hetero/tests/functional/shared_tests_instances/behavior/ov_compiled_model/compiled_model_base.cpp index 265fe1b55498..6b38ea15b61c 100644 --- a/src/plugins/hetero/tests/functional/shared_tests_instances/behavior/ov_compiled_model/compiled_model_base.cpp +++ b/src/plugins/hetero/tests/functional/shared_tests_instances/behavior/ov_compiled_model/compiled_model_base.cpp @@ -5,6 +5,9 @@ #include "behavior/compiled_model/compiled_model_base.hpp" using namespace ov::test::behavior; + +GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(CompiledModelSetType); + namespace { const std::vector configs = { diff --git a/src/plugins/hetero/tests/functional/shared_tests_instances/behavior/ov_compiled_model/properties.cpp b/src/plugins/hetero/tests/functional/shared_tests_instances/behavior/ov_compiled_model/properties.cpp index e8ba0b96d2c8..bca2dc0a39c3 100644 --- a/src/plugins/hetero/tests/functional/shared_tests_instances/behavior/ov_compiled_model/properties.cpp +++ b/src/plugins/hetero/tests/functional/shared_tests_instances/behavior/ov_compiled_model/properties.cpp @@ -77,6 +77,9 @@ INSTANTIATE_TEST_SUITE_P(smoke_OVClassCompiledModelGetIncorrectPropertyTest, OVClassCompiledModelGetIncorrectPropertyTest, ::testing::Values("HETERO:TEMPLATE")); +GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(OVClassHeteroCompiledModelGetMetricTest_TARGET_FALLBACK); +GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(OVClassHeteroCompiledModelGetMetricTest_EXEC_DEVICES); + INSTANTIATE_TEST_SUITE_P(smoke_OVClassHeteroCompiledModelGetMetricTest, OVClassHeteroCompiledModelGetMetricTest_TARGET_FALLBACK, ::testing::Values(ov::test::utils::DEVICE_TEMPLATE)); diff --git a/src/plugins/hetero/tests/functional/shared_tests_instances/behavior/ov_plugin/core_threading_tests.cpp b/src/plugins/hetero/tests/functional/shared_tests_instances/behavior/ov_plugin/core_threading_tests.cpp index 4eea2951b483..bba2c78cbcf5 100644 --- a/src/plugins/hetero/tests/functional/shared_tests_instances/behavior/ov_plugin/core_threading_tests.cpp +++ b/src/plugins/hetero/tests/functional/shared_tests_instances/behavior/ov_plugin/core_threading_tests.cpp @@ -4,6 +4,8 @@ #include "behavior/ov_plugin/core_threading.hpp" +GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(CoreThreadingTestsWithCacheEnabled); + namespace { const Params params[] = { std::tuple{ov::test::utils::DEVICE_HETERO, diff --git a/src/plugins/hetero/tests/unit/subgraph_collector.cpp b/src/plugins/hetero/tests/unit/subgraph_collector.cpp index e5beac1b2ed3..e75b27a80a9a 100644 --- a/src/plugins/hetero/tests/unit/subgraph_collector.cpp +++ b/src/plugins/hetero/tests/unit/subgraph_collector.cpp @@ -6,6 +6,9 @@ #include +#include +#include + #include "common_test_utils/graph_comparator.hpp" #include "common_test_utils/test_assertions.hpp" #include "op/device_subgraph.hpp" @@ -15,7 +18,8 @@ using namespace ov::hetero; namespace { -std::shared_ptr create_test_model() { +// input -> add -> sub -> reshape -> result +std::shared_ptr create_linear_chain_model() { auto param = std::make_shared(ov::element::i64, ov::PartialShape{1, 3, 2, 2}); param->set_friendly_name("input"); auto const_value = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{1, 1, 1, 1}, {1}); @@ -32,7 +36,9 @@ std::shared_ptr create_test_model() { result->set_friendly_name("res"); return std::make_shared(ov::ResultVector{result}, ov::ParameterVector{param}); } -std::shared_ptr create_test_model2() { +// input -> add1 -> add2 -> sub -> add3 -> reshape -> result +// └──────────────────→ add3 (diamond topology) +std::shared_ptr create_diamond_chain_model() { auto param = std::make_shared(ov::element::i64, ov::PartialShape{1, 3, 2, 2}); param->set_friendly_name("input"); auto const_value = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{1, 1, 1, 1}, {1}); @@ -53,6 +59,7 @@ std::shared_ptr create_test_model2() { result->set_friendly_name("res"); return std::make_shared(ov::ResultVector{result}, ov::ParameterVector{param}); } +// Subgraph: param -> add -> result (output: add) std::shared_ptr create_subgraph_add() { auto param = std::make_shared(ov::element::i64, ov::PartialShape{1, 3, 2, 2}); param->set_friendly_name("input"); @@ -62,6 +69,7 @@ std::shared_ptr create_subgraph_add() { add->set_friendly_name("add"); return std::make_shared(ov::OutputVector{add->output(0)}, ov::ParameterVector{param}); } +// Subgraph: input -> add1 -> add2 (outputs: add2, add1) std::shared_ptr create_subgraph_add_add() { auto param = std::make_shared(ov::element::i64, ov::PartialShape{1, 3, 2, 2}); param->set_friendly_name("input"); @@ -73,6 +81,7 @@ std::shared_ptr create_subgraph_add_add() { add2->set_friendly_name("add2"); return std::make_shared(ov::OutputVector{add2->output(0), add1->output(0)}, ov::ParameterVector{param}); } +// Subgraph: input -> sub (output: sub) std::shared_ptr create_subgraph_sub() { auto param = std::make_shared(ov::element::i64, ov::PartialShape{1, 3, 2, 2}); auto const_value = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{1, 1, 1, 1}, {1}); @@ -81,6 +90,7 @@ std::shared_ptr create_subgraph_sub() { sub->set_friendly_name("sub"); return std::make_shared(ov::OutputVector{sub->output(0)}, ov::ParameterVector{param}); } +// Subgraph: (param0, param1) -> add -> reshape -> result std::shared_ptr create_subgraph_add_reshape() { auto param0 = std::make_shared(ov::element::i64, ov::PartialShape{1, 3, 2, 2}); auto param1 = std::make_shared(ov::element::i64, ov::PartialShape{1, 3, 2, 2}); @@ -94,6 +104,7 @@ std::shared_ptr create_subgraph_add_reshape() { result->set_friendly_name("res"); return std::make_shared(ov::ResultVector{result}, ov::ParameterVector{param0, param1}); } +// Subgraph: input -> reshape -> result std::shared_ptr create_subgraph_reshape() { auto param = std::make_shared(ov::element::i64, ov::PartialShape{1, 3, 2, 2}); auto reshape_val = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{1}, {-1}); @@ -104,11 +115,565 @@ std::shared_ptr create_subgraph_reshape() { result->set_friendly_name("res"); return std::make_shared(ov::ResultVector{result}, ov::ParameterVector{param}); } +// Model: param → A → B → C → D → result (linear chain for alternating device tests) +std::shared_ptr create_alternating_chain_model() { + auto param = std::make_shared(ov::element::f32, ov::PartialShape{4}); + param->set_friendly_name("input"); + auto c1 = ov::op::v0::Constant::create(ov::element::f32, ov::Shape{1}, {1.0f}); + c1->set_friendly_name("c1"); + auto a = std::make_shared(param, c1); + a->set_friendly_name("A"); + auto b = std::make_shared(a, c1); + b->set_friendly_name("B"); + auto c = std::make_shared(b, c1); + c->set_friendly_name("C"); + auto d = std::make_shared(c, c1); + d->set_friendly_name("D"); + auto result = std::make_shared(d); + result->set_friendly_name("res"); + return std::make_shared(ov::ResultVector{result}, ov::ParameterVector{param}); +} +// Model: param1 → add1 → res1, param2 → add2 → res2 (two independent paths) +std::shared_ptr create_independent_paths_model() { + auto param1 = std::make_shared(ov::element::f32, ov::PartialShape{2}); + param1->set_friendly_name("input1"); + auto param2 = std::make_shared(ov::element::f32, ov::PartialShape{2}); + param2->set_friendly_name("input2"); + auto c1 = ov::op::v0::Constant::create(ov::element::f32, ov::Shape{1}, {1.0f}); + c1->set_friendly_name("c1"); + auto c2 = ov::op::v0::Constant::create(ov::element::f32, ov::Shape{1}, {1.0f}); + c2->set_friendly_name("c2"); + auto add1 = std::make_shared(param1, c1); + add1->set_friendly_name("add1"); + auto add2 = std::make_shared(param2, c2); + add2->set_friendly_name("add2"); + auto result1 = std::make_shared(add1); + result1->set_friendly_name("res1"); + auto result2 = std::make_shared(add2); + result2->set_friendly_name("res2"); + return std::make_shared(ov::ResultVector{result1, result2}, ov::ParameterVector{param1, param2}); +} +// Model: param1 → add1(+c1) → add2(+c2) → res, param2 unused (for merge_independent test) +std::shared_ptr create_merge_independent_model() { + auto param1 = std::make_shared(ov::element::f32, ov::PartialShape{1, 3, 2, 2}); + param1->set_friendly_name("input1"); + auto param2 = std::make_shared(ov::element::f32, ov::PartialShape{1, 3, 2, 2}); + param2->set_friendly_name("input2"); + auto const_value1 = ov::op::v0::Constant::create(ov::element::f32, ov::Shape{1, 1, 1, 1}, {1}); + const_value1->set_friendly_name("const_val1"); + auto add1 = std::make_shared(param1, const_value1); + add1->set_friendly_name("add1"); + auto const_value2 = ov::op::v0::Constant::create(ov::element::f32, ov::Shape{1, 1, 1, 1}, {1}); + const_value2->set_friendly_name("const_val2"); + auto add2 = std::make_shared(add1, const_value2); + add2->set_friendly_name("add2"); + auto result = std::make_shared(add2); + result->set_friendly_name("res"); + return std::make_shared(ov::ResultVector{result}, ov::ParameterVector{param1, param2}); +} +// Model: param → add → sub1 → res1, add → sub2 → res2 (diverging paths from shared node) +std::shared_ptr create_diverging_paths_model() { + auto param = std::make_shared(ov::element::f32, ov::PartialShape{2}); + param->set_friendly_name("input"); + auto c1 = ov::op::v0::Constant::create(ov::element::f32, ov::Shape{1}, {1.0f}); + c1->set_friendly_name("c1"); + auto add = std::make_shared(param, c1); + add->set_friendly_name("add"); + auto sub1 = std::make_shared(add, c1); + sub1->set_friendly_name("sub1"); + auto sub2 = std::make_shared(add, c1); + sub2->set_friendly_name("sub2"); + auto result1 = std::make_shared(sub1); + result1->set_friendly_name("res1"); + auto result2 = std::make_shared(sub2); + result2->set_friendly_name("res2"); + return std::make_shared(ov::ResultVector{result1, result2}, ov::ParameterVector{param}); +} + +// Model: input → a1 → b1 → a2 → b2 → a3 → res, with a1 also feeding a3 (skip edge). +// Designed to stress SubgraphCollector::split_cyclic_dependencies() with nested cycles: +// when {a1, a2, a3} are on MOCK.0 and {b1, b2} on MOCK.1, the initial M0 group contains +// two stacked cyclic dependencies (via b1 and via b2) that require multiple split iterations +// of the fixed-point loop to resolve. +std::shared_ptr create_nested_cyclic_chain_model() { + auto param = std::make_shared(ov::element::f32, ov::PartialShape{4}); + param->set_friendly_name("input"); + auto c1 = ov::op::v0::Constant::create(ov::element::f32, ov::Shape{1}, {1.0f}); + c1->set_friendly_name("c1"); + auto a1 = std::make_shared(param, c1); + a1->set_friendly_name("a1"); + auto b1 = std::make_shared(a1, c1); + b1->set_friendly_name("b1"); + auto a2 = std::make_shared(b1, c1); + a2->set_friendly_name("a2"); + auto b2 = std::make_shared(a2, c1); + b2->set_friendly_name("b2"); + auto a3 = std::make_shared(b2, a1); + a3->set_friendly_name("a3"); + auto result = std::make_shared(a3); + result->set_friendly_name("res"); + return std::make_shared(ov::ResultVector{result}, ov::ParameterVector{param}); +} + +// Model: shared constant bridges disjoint paths into one subgraph via indirect Union-Find merge. +// +// Topology: param(M0) → A(M0,+other_const) → B(M1) → C(M0,+shared_const) → res1 +// A → X(M0,+shared_const) → res2 +// +// Union-Find merges {param, other_const, A, X, shared_const, C, res1, res2} into one subgraph +// because X (reachable via res2) connects A to shared_const, and shared_const connects to C. +// This subgraph depends on B (via C←B) and B depends on the same subgraph (via B←A) → cycle. +// split_cyclic_dependencies() must detect the cycle re-entry point (C←B) and promote +// C.input(1) (from shared_const) as a boundary to break the cycle. +std::shared_ptr create_shared_const_indirect_bridge_cycle_model() { + auto param = std::make_shared(ov::element::f32, ov::PartialShape{4}); + param->set_friendly_name("input"); + auto shared_const = ov::op::v0::Constant::create(ov::element::f32, ov::Shape{4}, {1.0f, 1.0f, 1.0f, 1.0f}); + shared_const->set_friendly_name("shared_const"); + auto other_const = ov::op::v0::Constant::create(ov::element::f32, ov::Shape{4}, {2.0f, 2.0f, 2.0f, 2.0f}); + other_const->set_friendly_name("other_const"); + // A: DEV0, uses param + other_const (NOT shared_const!) + auto a = std::make_shared(param, other_const); + a->set_friendly_name("A"); + // X: DEV0, uses A + shared_const — bridges shared_const into A's subgraph via Union-Find. + // Crucially, X leads to res2, so it IS in get_ordered_ops(). + auto x = std::make_shared(a, shared_const); + x->set_friendly_name("X"); + // B: DEV1, uses A (cross-device boundary, unary op to minimize noise) + auto b = std::make_shared(a); + b->set_friendly_name("B"); + // C: DEV0, uses B + shared_const (after DEV1 detour, merges with A's sg via shared_const) + auto c = std::make_shared(b, shared_const); + c->set_friendly_name("C"); + auto res1 = std::make_shared(c); + res1->set_friendly_name("res1"); + // res2 makes X reachable — the key to triggering the bug + auto res2 = std::make_shared(x); + res2->set_friendly_name("res2"); + return std::make_shared(ov::ResultVector{res1, res2}, ov::ParameterVector{param}); +} + +// Model: shared constant IS the cycle source — it directly feeds a cross-device op AND merges +// with the cycle re-entry node through a purely internal chain. +// Topology: param(DEV0) → A(DEV0) → B(DEV1, +shared_const) → C(DEV0) → res1 +// shared_const(DEV0) → X(DEV0) → C(DEV0) +// X → res2 +// +// A→B: cross-device (DEV0→DEV1) → boundary. +// shared_const→B: cross-device (DEV0→DEV1) → boundary (constant IS the cycle source!). +// B→C: cross-device (DEV1→DEV0) → boundary. +// shared_const→X: same device → NOT boundary → merge via Union-Find. +// X→C: same device → NOT boundary → merge via Union-Find. +// +// Union-Find: SG0={param,A}, SG1={B}, SG2={shared_const,X,C,res1,res2}. +// Cycle: SG2→SG1 (shared_const→B) and SG1→SG2 (B→C). +// split_cyclic_dependencies() must promote C.input(1) (from X) as a boundary, +// yielding 4 subgraphs: {param,A}(DEV0), {B}(DEV1), {shared_const,X,res2}(DEV0), {C,res1}(DEV0). +std::shared_ptr create_shared_const_as_cycle_source_model() { + auto param = std::make_shared(ov::element::f32, ov::PartialShape{4}); + param->set_friendly_name("param"); + auto shared_const = ov::op::v0::Constant::create(ov::element::f32, ov::Shape{4}, {1.0f, 1.0f, 1.0f, 1.0f}); + shared_const->set_friendly_name("shared_const"); + // A: DEV0, uses param only + auto a = std::make_shared(param); + a->set_friendly_name("A"); + // B: DEV1, uses A + shared_const. shared_const→B is the KEY cross-device edge making + // shared_const the cycle source (SG2 produces output to SG1). + auto b = std::make_shared(a, shared_const); + b->set_friendly_name("B"); + // X: DEV0, uses shared_const. Merges with shared_const (same device, internal). + auto x = std::make_shared(shared_const); + x->set_friendly_name("X"); + // C: DEV0, uses B (boundary, re-entry from SG1) + X (internal, merged with shared_const). + // Without the fix, the intersection check fails because cyclic_inputs_dependencies + // doesn't include shared_const's self-boundary bit. + auto c = std::make_shared(b, x); + c->set_friendly_name("C"); + auto res1 = std::make_shared(c); + res1->set_friendly_name("res1"); + auto res2 = std::make_shared(x); + res2->set_friendly_name("res2"); + return std::make_shared(ov::ResultVector{res1, res2}, ov::ParameterVector{param}); +} + +// Negative test: shared constant merges nodes via Union-Find but NO cycle exists. +// Topology: param(DEV0) → A(DEV0, +shared_const) → B(DEV1) → res +// shared_const(DEV0) → X(DEV0) → res2 +// +// Union-Find: SG0={param, shared_const, A, X, res2}, SG1={B, res}. +// Data flows only SG0→SG1 (A→B), never back — NO cycle. +// split_cyclic_dependencies() must NOT promote any boundary (no over-splitting). +// Expected: 2 subgraphs, not 3. +std::shared_ptr create_shared_const_no_cycle_model() { + auto param = std::make_shared(ov::element::f32, ov::PartialShape{4}); + param->set_friendly_name("param"); + auto shared_const = ov::op::v0::Constant::create(ov::element::f32, ov::Shape{4}, {1.0f, 1.0f, 1.0f, 1.0f}); + shared_const->set_friendly_name("shared_const"); + // A: DEV0, uses param + shared_const + auto a = std::make_shared(param, shared_const); + a->set_friendly_name("A"); + // X: DEV0, uses shared_const (merges with shared_const via Union-Find) + auto x = std::make_shared(shared_const); + x->set_friendly_name("X"); + // B: DEV1, uses A only (one-way cross-device, no feedback) + auto b = std::make_shared(a); + b->set_friendly_name("B"); + auto res = std::make_shared(b); + res->set_friendly_name("res"); + auto res2 = std::make_shared(x); + res2->set_friendly_name("res2"); + return std::make_shared(ov::ResultVector{res, res2}, ov::ParameterVector{param}); +} + +// Negative test mirroring create_shared_const_indirect_bridge_cycle_model node-for-node, with +// the SOLE difference that the B→C feedback edge is removed (C is fed from A instead of B), so +// the M0 group depends on the M1 group via A→B but the M1 group never feeds back into M0. The +// indirect Union-Find merge through shared_const is preserved exactly as in the positive case +// (X and C both consume shared_const, pulling shared_const + C + X + A into one M0 group), so +// any spurious "re-entry candidate" detection that fires on the merge shape alone would +// incorrectly over-split this model. Expected: 2 subgraphs, no promotion of shared_const→C. +// +// Topology: param(M0) → A(M0,+other_const) → B(M1) → res_b +// A → X(M0,+shared_const) → res2 +// A → C(M0,+shared_const) → res1 +std::shared_ptr create_shared_const_indirect_bridge_no_cycle_model() { + auto param = std::make_shared(ov::element::f32, ov::PartialShape{4}); + param->set_friendly_name("input"); + auto shared_const = ov::op::v0::Constant::create(ov::element::f32, ov::Shape{4}, {1.0f, 1.0f, 1.0f, 1.0f}); + shared_const->set_friendly_name("shared_const"); + auto other_const = ov::op::v0::Constant::create(ov::element::f32, ov::Shape{4}, {2.0f, 2.0f, 2.0f, 2.0f}); + other_const->set_friendly_name("other_const"); + auto a = std::make_shared(param, other_const); + a->set_friendly_name("A"); + // X: same role as in the cycle case — bridges shared_const into A's subgraph. + auto x = std::make_shared(a, shared_const); + x->set_friendly_name("X"); + // B: same cross-device hop as in the cycle case, but its output now terminates at res_b. + auto b = std::make_shared(a); + b->set_friendly_name("B"); + // C: only difference vs. the cycle case — fed from A (M0) instead of B (M1), so no + // M1→M0 back-edge exists; the indirect shared_const merge {A, X, C, shared_const} is + // preserved. + auto c = std::make_shared(a, shared_const); + c->set_friendly_name("C"); + auto res1 = std::make_shared(c); + res1->set_friendly_name("res1"); + auto res2 = std::make_shared(x); + res2->set_friendly_name("res2"); + auto res_b = std::make_shared(b); + res_b->set_friendly_name("res_b"); + return std::make_shared(ov::ResultVector{res1, res2, res_b}, ov::ParameterVector{param}); +} + +// Multiple shared constants where only ONE participates in the cyclic path. +// Topology: param(M0) → A(M0,+C1) → X(M0,+C2) → res_x +// A → B(M1) → C(M0,+C1) → res_c +// C2 → Z(M0) → res_z +// +// Both C1 and C2 are shared (have >= 2 consumers in the M0 union-find group). Only C1 reaches +// the cycle re-entry node C, so only C1 must be duplicated into the split-off subgraph. C2's +// consumers (X and Z) both stay in sg0, so C2 must NOT be cloned. This locks the contract +// "duplicate the cyclic-boundary Constant, not every shared Constant in the group". +std::shared_ptr create_multiple_shared_constants_partial_cycle_model() { + auto param = std::make_shared(ov::element::f32, ov::PartialShape{4}); + param->set_friendly_name("param"); + auto c1 = ov::op::v0::Constant::create(ov::element::f32, ov::Shape{4}, {1.0f, 1.0f, 1.0f, 1.0f}); + c1->set_friendly_name("C1"); + auto c2 = ov::op::v0::Constant::create(ov::element::f32, ov::Shape{4}, {2.0f, 2.0f, 2.0f, 2.0f}); + c2->set_friendly_name("C2"); + auto a = std::make_shared(param, c1); + a->set_friendly_name("A"); + auto x = std::make_shared(a, c2); + x->set_friendly_name("X"); + auto b = std::make_shared(a); + b->set_friendly_name("B"); + auto c = std::make_shared(b, c1); + c->set_friendly_name("C"); + auto z = std::make_shared(c2); + z->set_friendly_name("Z"); + auto res_c = std::make_shared(c); + res_c->set_friendly_name("res_c"); + auto res_x = std::make_shared(x); + res_x->set_friendly_name("res_x"); + auto res_z = std::make_shared(z); + res_z->set_friendly_name("res_z"); + return std::make_shared(ov::ResultVector{res_c, res_x, res_z}, ov::ParameterVector{param}); +} + +// Mixed Parameter/Constant bridges in the same cycle: param2 and C_const both bridge into the +// cyclic M0 group via shared consumption. Two independent re-entry nodes (C and F) carry one +// bridge each. After promotion their boundaries are realized DIFFERENTLY: +// - C_const is duplicated into F's split-off subgraph (does NOT appear in +// _submodels_input_to_prev_output). +// - param2 cannot be duplicated (would alter the model input set), so it appears as an +// explicit cross-subgraph edge in _submodels_input_to_prev_output. +// This case exercises both boundary-realization paths in the same model. +// +// Topology: param1(M0) ─┐ +// param2(M0) ─┴→ A(M0) ──┬→ B(M1) ──┬→ C(M0,+param2) → res_c +// │ └→ B2(M1) → F(M0,+C_const) → res_f +// └→ X(M0,+C_const) → res_x +// A consumes param2 and X consumes C_const, anchoring both in sg0 (the producer side). +// B2 is an extra M1 Abs on F's path so that F is strictly deeper than C in topological +// order. This pins the subgraph-ID assignment (the collector visits Results in topo +// order); without it, C and F would be symmetric forks of B and the resulting sg2/sg3 +// labeling — and therefore every NodeInfo in the expected mapping — would be order- +// dependent across builds. +std::shared_ptr create_mixed_parameter_constant_bridge_cycle_model() { + auto param1 = std::make_shared(ov::element::f32, ov::PartialShape{4}); + param1->set_friendly_name("param1"); + auto param2 = std::make_shared(ov::element::f32, ov::PartialShape{4}); + param2->set_friendly_name("param2"); + auto c_const = ov::op::v0::Constant::create(ov::element::f32, ov::Shape{4}, {1.0f, 1.0f, 1.0f, 1.0f}); + c_const->set_friendly_name("C_const"); + auto a = std::make_shared(param1, param2); + a->set_friendly_name("A"); + auto x = std::make_shared(a, c_const); + x->set_friendly_name("X"); + auto b = std::make_shared(a); + b->set_friendly_name("B"); + // C: re-entry #1, Parameter bridge. + auto c = std::make_shared(b, param2); + c->set_friendly_name("C"); + // F: re-entry #2, Constant bridge. Extra Abs (B2) pins topo order so F-subgraph > C-subgraph. + auto b2 = std::make_shared(b); + b2->set_friendly_name("B2"); + auto f = std::make_shared(b2, c_const); + f->set_friendly_name("F"); + auto res_c = std::make_shared(c); + res_c->set_friendly_name("res_c"); + auto res_f = std::make_shared(f); + res_f->set_friendly_name("res_f"); + auto res_x = std::make_shared(x); + res_x->set_friendly_name("res_x"); + auto res_b = std::make_shared(b); + res_b->set_friendly_name("res_b"); + return std::make_shared(ov::ResultVector{res_c, res_f, res_x, res_b}, + ov::ParameterVector{param1, param2}); +} + +// Model: shared constant fans out to three consumers across two devices. +// Topology: param(GPU) → Node_A(GPU, +shared_const) → Node_B(CPU, +shared_const) → Node_C(GPU, +shared_const) → res +// +// shared_const(GPU) → Node_A(GPU): same affinity → NOT boundary, merged via Union-Find. +// shared_const(GPU) → Node_B(CPU): different affinity → IS boundary (cross-device edge). +// shared_const(GPU) → Node_C(GPU): same affinity → NOT boundary, merged via Union-Find. +// +// Union-Find produces one GPU subgraph {param, shared_const, Node_A, Node_C, res} + one CPU {Node_B}. +// GPU depends on Node_B (Node_C←Node_B) and Node_B depends on GPU (Node_B←Node_A) → cycle. +// Note: Node_C.input(0) from Node_B is already a boundary from init() (CPU→GPU). +// split_cyclic_dependencies() promotes Node_C.input(1) (from shared_const) to separate Node_C +// from the shared_const/Node_A group, yielding 3 subgraphs: +// {param, shared_const, Node_A}(GPU), {Node_B}(CPU), {Node_C, res}(GPU). +std::shared_ptr create_shared_const_cross_device_fanout_model() { + auto param = std::make_shared(ov::element::f32, ov::PartialShape{4}); + param->set_friendly_name("param"); + auto shared_const = ov::op::v0::Constant::create(ov::element::f32, ov::Shape{4}, {1.0f, 1.0f, 1.0f, 1.0f}); + shared_const->set_friendly_name("shared_const"); + // Node_A: GPU, consumes param + shared_const (same device → merged with shared_const) + auto node_a = std::make_shared(param, shared_const); + node_a->set_friendly_name("Node_A"); + // Node_B: CPU, consumes Node_A + shared_const + // Node_A→Node_B is boundary (GPU→CPU) + // shared_const→Node_B is boundary (GPU→CPU) — this is the KEY edge + auto node_b = std::make_shared(node_a, shared_const); + node_b->set_friendly_name("Node_B"); + // Node_C: GPU, consumes Node_B + shared_const + // Node_B→Node_C is boundary (CPU→GPU) + // shared_const→Node_C is NOT boundary (GPU→GPU) — merges Node_C into shared_const's subgraph + auto node_c = std::make_shared(node_b, shared_const); + node_c->set_friendly_name("Node_C"); + auto res = std::make_shared(node_c); + res->set_friendly_name("res"); + return std::make_shared(ov::ResultVector{res}, ov::ParameterVector{param}); +} + +// Multi-hop subgraph-level SCC. Two regions of M0 get fused into one Union-Find subgraph via a +// shared Constant (c_top: feeds X1 in region 1 and X2 in region 2). The resulting M0 subgraph +// then participates in a 4-subgraph cycle that no single node can detect with the per-node +// heuristic (the producer and re-entry consumer of the cycle are different nodes far apart in +// topology). Only the subgraph-DAG SCC fallback can break it. +// +// Topology (M0 = MOCK.0, M1 = MOCK.1): +// +// in1(M0) ─┐ ┌─ X1(M0,+c_top) ─ res_x1 +// ├─ A1(M0) ─ B1(M1) ─ C1(M0) ─ D1(M1) ┘ +// c_top(M0) ──────────────┐ +// in2(M0) ─┐ │ +// ├─ A2(M0) ─ B2(M1) ┘ ┌─ X2(M0,+c_top) ─ res_x2 +// │ +// ├─ A2(M0) ─────── C2(M1) ─ D2(M0) ───┘ +// +// Initial Union-Find groups (M0 only): {in1,A1,C1}, {in2,A2,D2,X2,c_top,X1}. The shared c_top +// merges X1 (region 1) and X2 (region 2) into the same M0 subgraph, call it M0_big. +// Cross-subgraph data edges then form: M0_big -> M1 (A1->B1, A2->B2, A2->C2), +// M1 -> M0_big (D1->X1-via-c_top-region, D2 already inside M0_big). After the per-node fix-point +// loop, the subgraph DAG still contains M0_big -> M1 -> M0_big -> M1 -> M0_big, but no single +// node in M0_big has a producer-in-my-sg cyclic dependency (X1's producers are D1 in M1 and +// c_top which is a graph input). SCC fallback must split M0_big into multiple subgraphs. +std::shared_ptr create_multi_hop_scc_cycle_model() { + auto in1 = std::make_shared(ov::element::f32, ov::PartialShape{4}); + in1->set_friendly_name("in1"); + auto in2 = std::make_shared(ov::element::f32, ov::PartialShape{4}); + in2->set_friendly_name("in2"); + auto c_top = ov::op::v0::Constant::create(ov::element::f32, ov::Shape{4}, {1.0f, 1.0f, 1.0f, 1.0f}); + c_top->set_friendly_name("c_top"); + auto a1 = std::make_shared(in1); + a1->set_friendly_name("A1"); + auto b1 = std::make_shared(a1); + b1->set_friendly_name("B1"); + auto c1 = std::make_shared(b1); + c1->set_friendly_name("C1"); + auto d1 = std::make_shared(c1); + d1->set_friendly_name("D1"); + auto x1 = std::make_shared(d1, c_top); + x1->set_friendly_name("X1"); + auto a2 = std::make_shared(in2); + a2->set_friendly_name("A2"); + auto b2 = std::make_shared(a2); + b2->set_friendly_name("B2"); + auto c2 = std::make_shared(b2); + c2->set_friendly_name("C2"); + auto d2 = std::make_shared(c2); + d2->set_friendly_name("D2"); + auto x2 = std::make_shared(d2, c_top); + x2->set_friendly_name("X2"); + auto res_x1 = std::make_shared(x1); + res_x1->set_friendly_name("res_x1"); + auto res_x2 = std::make_shared(x2); + res_x2->set_friendly_name("res_x2"); + return std::make_shared(ov::ResultVector{res_x1, res_x2}, ov::ParameterVector{in1, in2}); +} + +// Bridge-between-cycles topology. Two independent 2-subgraph SCCs sit on the left and right; +// a multi-node bridge subgraph X on a third device sits between them, with one incoming edge +// from the left SCC and one outgoing edge to the right SCC. The bridge is acyclic in the +// subgraph DAG (it lies on a single path between the two cycles, not in any cycle itself). +// +// Subgraph DAG after initial partitioning: +// +// A_L(M0) ↔ B_L(M1) ──► X(M2) ──► A_R(M0) ↔ B_R(M1) +// +// Each 2-cycle is formed without per-node cyclic inputs (the round-trip goes through nodes in +// different subgraphs), so split_cyclic_dependencies()'s per-node fix-point loop cannot break +// them; only the subgraph-DAG SCC fallback can. This is the structural ingredient that exposes +// the difference between an exact SCC algorithm and a forward+reverse Kahn approximation: the +// approximation marks X as cyclic (it survives both peels because every subgraph has both +// incoming and outgoing edges), and the promotion loop would eventually split the bridge by +// promoting x_bridge1 → x_bridge2. An exact SCC algorithm classifies X as a singleton SCC and +// never touches its internal edges, preserving the bridge as a single subgraph. +// +// The test below asserts the latter: x_bridge1 and x_bridge2 must end up in the same subgraph +// after run() converges. +std::shared_ptr create_bridge_between_cycles_model() { + // Left cycle: c_LA fuses {in_L, a_L1, a_L2} into A_L (M0); c_LB fuses {b_L1, b_L2} into B_L (M1). + auto in_L = std::make_shared(ov::element::f32, ov::PartialShape{4}); + in_L->set_friendly_name("in_L"); + auto c_LA = ov::op::v0::Constant::create(ov::element::f32, ov::Shape{4}, {1.0f, 1.0f, 1.0f, 1.0f}); + c_LA->set_friendly_name("c_LA"); + auto c_LB = ov::op::v0::Constant::create(ov::element::f32, ov::Shape{4}, {2.0f, 2.0f, 2.0f, 2.0f}); + c_LB->set_friendly_name("c_LB"); + auto a_L1 = std::make_shared(in_L, c_LA); + a_L1->set_friendly_name("a_L1"); + auto b_L1 = std::make_shared(a_L1, c_LB); // A_L → B_L edge + b_L1->set_friendly_name("b_L1"); + auto b_L2 = std::make_shared(b_L1, c_LB); + b_L2->set_friendly_name("b_L2"); + auto a_L2 = std::make_shared(b_L2, c_LA); // B_L → A_L edge + a_L2->set_friendly_name("a_L2"); + // Bridge: two M2 nodes connected by an internal same-sg edge (x_bridge1 → x_bridge2). This + // internal edge is what the buggy two-peel would wrongly promote. + auto x_bridge1 = std::make_shared(a_L2); // A_L → X edge + x_bridge1->set_friendly_name("x_bridge1"); + auto x_bridge2 = std::make_shared(x_bridge1); + x_bridge2->set_friendly_name("x_bridge2"); + // Right cycle: mirror of left, fed from the bridge tail. + auto c_RA = ov::op::v0::Constant::create(ov::element::f32, ov::Shape{4}, {3.0f, 3.0f, 3.0f, 3.0f}); + c_RA->set_friendly_name("c_RA"); + auto c_RB = ov::op::v0::Constant::create(ov::element::f32, ov::Shape{4}, {4.0f, 4.0f, 4.0f, 4.0f}); + c_RB->set_friendly_name("c_RB"); + auto a_R1 = std::make_shared(x_bridge2, c_RA); // X → A_R edge + a_R1->set_friendly_name("a_R1"); + auto b_R1 = std::make_shared(a_R1, c_RB); // A_R → B_R edge + b_R1->set_friendly_name("b_R1"); + auto b_R2 = std::make_shared(b_R1, c_RB); + b_R2->set_friendly_name("b_R2"); + auto a_R2 = std::make_shared(b_R2, c_RA); // B_R → A_R edge + a_R2->set_friendly_name("a_R2"); + auto res = std::make_shared(a_R2); + res->set_friendly_name("res"); + return std::make_shared(ov::ResultVector{res}, ov::ParameterVector{in_L}); +} + +// Subgraph-DAG SCC where the only same-subgraph promotable edges have a Constant producer. +// A shared M0 Constant `c_shared` is consumed by three M0 nodes (A, C, E) which are interleaved +// with three independent M1 nodes (B, D, F). The interleaving forms two 2-cycles in the +// subgraph DAG (M0_big <-> sg_B and M0_big <-> sg_D), both incident to the fused M0_big +// subgraph; every M0_big internal edge whose other endpoint is not foreign-sg ends up being a +// Constant -> consumer edge (c_shared -> A, c_shared -> C, c_shared -> E). This is the exact +// shape of the failure reproduced on yolo26s-seg: the SCC fallback finds a cyclic subgraph, +// but every candidate same-sg edge has a graph-input producer. The earlier implementation +// filtered those out and tripped the "no internal edge to promote" assert. +// +// Topology (M0 = MOCK.0, M1 = MOCK.1): +// +// in(M0) --> A(M0,+c_shared) --> B(M1) --> C(M0,+c_shared) --> D(M1) --> E(M0,+c_shared) --> F(M1) --> res +// ^ (M1) ^ (M1) ^ (M1) +// | | | +// +--- c_shared(M0) ----------------+---------------------------------+ +// +std::shared_ptr create_shared_const_scc_only_const_promotable_model() { + auto in_node = std::make_shared(ov::element::f32, ov::PartialShape{4}); + in_node->set_friendly_name("in"); + auto c_shared = ov::op::v0::Constant::create(ov::element::f32, ov::Shape{4}, {1.0f, 1.0f, 1.0f, 1.0f}); + c_shared->set_friendly_name("c_shared"); + auto A = std::make_shared(in_node, c_shared); + A->set_friendly_name("A"); + auto B = std::make_shared(A); + B->set_friendly_name("B"); + auto C = std::make_shared(B, c_shared); + C->set_friendly_name("C"); + auto D = std::make_shared(C); + D->set_friendly_name("D"); + auto E = std::make_shared(D, c_shared); + E->set_friendly_name("E"); + auto F = std::make_shared(E); + F->set_friendly_name("F"); + auto res = std::make_shared(F); + res->set_friendly_name("res"); + return std::make_shared(ov::ResultVector{res}, ov::ParameterVector{in_node}); +} + +// Stateful model: param → read_value → add(+c1) → {result, assign(sink)}. +// Single-device by design — exercises Subgraph::_sinks wire-through and +// create_submodel_from_collected_subgraph()'s sink-preserving construction without +// depending on the (currently unspecified) cross-affinity Assign/ReadValue contract. +std::shared_ptr create_stateful_single_device_model() { + const ov::op::util::VariableInfo variable_info{ov::PartialShape{4}, ov::element::f32, "var0"}; + auto variable = std::make_shared(variable_info); + auto param = std::make_shared(ov::element::f32, ov::PartialShape{4}); + param->set_friendly_name("input"); + auto read_value = std::make_shared(param, variable); + read_value->set_friendly_name("read_value"); + auto c1 = ov::op::v0::Constant::create(ov::element::f32, ov::Shape{1}, {1.0f}); + c1->set_friendly_name("c1"); + auto add = std::make_shared(read_value, c1); + add->set_friendly_name("add"); + auto assign = std::make_shared(add, variable); + assign->set_friendly_name("assign"); + auto result = std::make_shared(add); + result->set_friendly_name("res"); + return std::make_shared(ov::ResultVector{result}, ov::SinkVector{assign}, ov::ParameterVector{param}); +} + +std::shared_ptr create_submodel_from_collected_subgraph(const ov::hetero::Subgraph& sg) { + return std::make_shared(sg._results, sg._sinks, sg._parameters); +} } // namespace class SubgraphCollectorTest : public testing::Test { void SetUp() override { - m_model = create_test_model(); + m_model = create_linear_chain_model(); m_model_ref = m_model->clone(); m_submodels = {}; m_submodels.push_back(create_subgraph_add()); @@ -124,74 +689,7 @@ class SubgraphCollectorTest : public testing::Test { std::vector> m_submodels; }; -TEST_F(SubgraphCollectorTest, submodel_split_and_merge) { - const std::map supported_ops = { - {"input", "MOCK.0"}, - {"const_val", "MOCK.0"}, - {"add", "MOCK.0"}, - {"sub", "MOCK.1"}, - {"reshape_val", "MOCK.0"}, - {"reshape", "MOCK.0"}, - {"res", "MOCK.0"}, - }; - const std::map expected_ids = { - {"input", 0}, - {"const_val", 0}, - {"add", 0}, - {"sub", 2}, - {"reshape_val", 3}, - {"reshape", 3}, - {"res", 3}, - }; - const SubgraphsMappingInfo exptected_mapping_info = {{// inputs to submodel inputs - NodeInfo{0, 0}}, - {// outputs to submodel outpts - NodeInfo{2, 0}}, - {// submodel input to previous output - {NodeInfo{1, 0}, NodeInfo{0, 0}}, - {NodeInfo{2, 0}, NodeInfo{1, 0}}}}; - SubgraphCollector::AffinitiesMap affinities; - SubgraphCollector::SubgraphIdsMap exptected_subgraphs_ids; - const auto ordered_ops = m_model->get_ordered_ops(); - for (const auto& node : ordered_ops) { - const auto name = node->get_friendly_name(); - OPENVINO_ASSERT(supported_ops.count(name)); - affinities[node] = supported_ops.at(name); - OPENVINO_ASSERT(expected_ids.count(name)); - exptected_subgraphs_ids[node] = expected_ids.at(name); - } - // Collect subgraphs - SubgraphCollector subgraph_collector(m_model, affinities); - auto actual_subgraphs_ids = subgraph_collector.get_subgraph_ids(); - ASSERT_EQ(exptected_subgraphs_ids, actual_subgraphs_ids); - - const auto& [actual_subgraphs, actual_mapping_info] = subgraph_collector.run(); - - ASSERT_EQ(3, actual_subgraphs.size()); - std::vector> actual_submodels; - for (auto& actual_subgraph : actual_subgraphs) { - actual_submodels.push_back(std::make_shared(actual_subgraph._results, actual_subgraph._parameters)); - } - for (size_t i = 0; i < actual_submodels.size(); i++) { - auto res = compare_functions(m_submodels.at(i), actual_submodels.at(i)); - ASSERT_TRUE(res.first) << res.second; - } - - // Check mapping info - ASSERT_EQ(exptected_mapping_info._inputs_to_submodels_inputs, actual_mapping_info._inputs_to_submodels_inputs); - ASSERT_EQ(exptected_mapping_info._outputs_to_submodels_outputs, actual_mapping_info._outputs_to_submodels_outputs); - ASSERT_EQ(exptected_mapping_info._submodels_input_to_prev_output, - actual_mapping_info._submodels_input_to_prev_output); - - // Merge submodels into one model back - ov::hetero::merge_submodels(actual_submodels, actual_mapping_info._submodels_input_to_prev_output); - ASSERT_EQ(1, actual_submodels.size()); - auto& actual_model = actual_submodels[0]; - auto res = compare_functions(m_model_ref, actual_model); - ASSERT_TRUE(res.first) << res.second; -} - -TEST_F(SubgraphCollectorTest, submodel_replacement_first_device) { +TEST_F(SubgraphCollectorTest, mask_ops_single_device) { const std::map supported_ops_mock0 = { {"input", "MOCK.0"}, {"const_val", "MOCK.0"}, @@ -232,7 +730,7 @@ TEST_F(SubgraphCollectorTest, submodel_replacement_first_device) { ASSERT_TRUE(actual_mapping_info.empty()); } -TEST_F(SubgraphCollectorTest, submodel_replacement_both_devices) { +TEST_F(SubgraphCollectorTest, mask_ops_all_devices) { const std::map supported_ops_mock0 = { {"input", "MOCK.0"}, {"const_val", "MOCK.0"}, @@ -299,7 +797,7 @@ TEST_F(SubgraphCollectorTest, submodel_replacement_both_devices) { class SubgraphCollectorTest2 : public SubgraphCollectorTest { void SetUp() override { - m_model = create_test_model2(); + m_model = create_diamond_chain_model(); m_model_ref = m_model->clone(); m_submodels = {}; m_submodels.push_back(create_subgraph_add_add()); @@ -308,63 +806,7 @@ class SubgraphCollectorTest2 : public SubgraphCollectorTest { } }; -TEST_F(SubgraphCollectorTest2, submodel_split_and_merge) { - const std::map supported_ops = { - {"input", "MOCK.0"}, - {"const_val", "MOCK.0"}, - {"add1", "MOCK.0"}, - {"add2", "MOCK.0"}, - {"sub", "MOCK.1"}, - {"add3", "MOCK.0"}, - {"reshape_val", "MOCK.0"}, - {"reshape", "MOCK.0"}, - {"res", "MOCK.0"}, - }; - const SubgraphsMappingInfo exptected_mapping_info = {{// inputs to submodel inputs - NodeInfo{0, 0}}, - {// outputs to submodel outpts - NodeInfo{2, 0}}, - {// submodel input to previous output - {NodeInfo{1, 0}, NodeInfo{0, 0}}, - {NodeInfo{2, 0}, NodeInfo{0, 1}}, - {NodeInfo{2, 1}, NodeInfo{1, 0}}}}; - SubgraphCollector::AffinitiesMap affinities; - const auto ordered_ops = m_model->get_ordered_ops(); - for (const auto& node : ordered_ops) { - const auto name = node->get_friendly_name(); - OPENVINO_ASSERT(supported_ops.count(name)); - affinities[node] = supported_ops.at(name); - } - // Collect subgraphs - SubgraphCollector subgraph_collector(m_model, affinities); - - const auto& [actual_subgraphs, actual_mapping_info] = subgraph_collector.run(); - - ASSERT_EQ(3, actual_subgraphs.size()); - std::vector> actual_submodels; - for (auto& actual_subgraph : actual_subgraphs) { - actual_submodels.push_back(std::make_shared(actual_subgraph._results, actual_subgraph._parameters)); - } - for (size_t i = 0; i < actual_submodels.size(); i++) { - auto res = compare_functions(m_submodels.at(i), actual_submodels.at(i)); - ASSERT_TRUE(res.first) << res.second; - } - - // Check mapping info - ASSERT_EQ(exptected_mapping_info._inputs_to_submodels_inputs, actual_mapping_info._inputs_to_submodels_inputs); - ASSERT_EQ(exptected_mapping_info._outputs_to_submodels_outputs, actual_mapping_info._outputs_to_submodels_outputs); - ASSERT_EQ(exptected_mapping_info._submodels_input_to_prev_output, - actual_mapping_info._submodels_input_to_prev_output); - - // Merge submodels into one model back - ov::hetero::merge_submodels(actual_submodels, actual_mapping_info._submodels_input_to_prev_output); - ASSERT_EQ(1, actual_submodels.size()); - auto& actual_model = actual_submodels[0]; - auto res = compare_functions(m_model_ref, actual_model); - ASSERT_TRUE(res.first) << res.second; -} - -TEST_F(SubgraphCollectorTest2, submodel_replacement_first_device) { +TEST_F(SubgraphCollectorTest2, mask_ops_single_device) { const std::map supported_ops_mock0 = { {"input", "MOCK.0"}, {"const_val", "MOCK.0"}, @@ -411,7 +853,7 @@ TEST_F(SubgraphCollectorTest2, submodel_replacement_first_device) { ASSERT_TRUE(actual_mapping_info.empty()); } -TEST_F(SubgraphCollectorTest2, submodel_replacement_both_devices) { +TEST_F(SubgraphCollectorTest2, mask_ops_all_devices) { const std::map supported_ops_mock0 = { {"input", "MOCK.0"}, {"const_val", "MOCK.0"}, @@ -479,7 +921,7 @@ TEST_F(SubgraphCollectorTest2, submodel_replacement_both_devices) { actual_mapping_info._submodels_input_to_prev_output); } -TEST_F(SubgraphCollectorTest, submodel_with_different_affinity_parameter) { +TEST_F(SubgraphCollectorTest, mask_ops_mixed_affinity_no_throw) { const std::map supported_ops_with_affinity = { {"input", "MOCK.0"}, {"const_val", "MOCK.0"}, @@ -492,7 +934,7 @@ TEST_F(SubgraphCollectorTest, submodel_with_different_affinity_parameter) { OV_ASSERT_NO_THROW(ov::hetero::mask_model_subgraphs_by_ops(m_model, supported_ops, false, "TEST")); } -TEST_F(SubgraphCollectorTest, submodel_with_constant_subgraphs) { +TEST_F(SubgraphCollectorTest, constant_subgraphs_follow_consumer_affinity) { auto input = std::make_shared(ov::element::u8, ov::PartialShape{1, 3, 16, 16}); input->set_friendly_name("input"); auto convert = std::make_shared(input, ov::element::f32); @@ -535,6 +977,7 @@ TEST_F(SubgraphCollectorTest, submodel_with_constant_subgraphs) { {"constant2", "MOCK.0"}, {"transpose", "MOCK.1"}, {"shapeOf", "MOCK.0"}, + {"reshape_val", "MOCK.0"}, {"reshape", "MOCK.1"}, {"zero", "MOCK.0"}, @@ -548,7 +991,7 @@ TEST_F(SubgraphCollectorTest, submodel_with_constant_subgraphs) { const auto& [ordered_subgraphs, actual_mapping_info] = get_model_subgraphs(model, supported_ops, true, false); for (const auto& subgraph : ordered_subgraphs) { std::set node_set; - auto sub_model = std::make_shared(subgraph._results, subgraph._sinks, subgraph._parameters); + auto sub_model = create_submodel_from_collected_subgraph(subgraph); for (auto& node : sub_model->get_ordered_ops()) { node_set.insert(node->get_friendly_name()); } @@ -562,51 +1005,938 @@ TEST_F(SubgraphCollectorTest, submodel_with_constant_subgraphs) { } } -TEST_F(SubgraphCollectorTest, merge_independent_submodel) { - auto param1 = std::make_shared(ov::element::f32, ov::PartialShape{1, 3, 2, 2}); - param1->set_friendly_name("input1"); - auto param2 = std::make_shared(ov::element::f32, ov::PartialShape{1, 3, 2, 2}); - param2->set_friendly_name("input2"); - auto const_value1 = ov::op::v0::Constant::create(ov::element::f32, ov::Shape{1, 1, 1, 1}, {1}); - const_value1->set_friendly_name("const_val1"); - auto add1 = std::make_shared(param1, const_value1); - add1->set_friendly_name("add1"); - auto const_value2 = ov::op::v0::Constant::create(ov::element::f32, ov::Shape{1, 1, 1, 1}, {1}); - const_value2->set_friendly_name("const_val1"); - auto add2 = std::make_shared(add1, const_value2); - add2->set_friendly_name("add2"); - auto result = std::make_shared(add2); - result->set_friendly_name("res"); - auto model = std::make_shared(ov::ResultVector{result}, ov::ParameterVector{param1, param2}); +// --- Parameterized SubgraphCollector tests --- +// Unified test for all scenarios that directly use SubgraphCollector: +// split_and_merge, merge_independent, and affinity-based subgraph counting. +// Each param specifies model, affinities, and expected results. Optional fields control extra checks. +// +// Sink coverage: the single-device stateful_assign_readvalue case below exercises +// Subgraph::_sinks wire-through (collection → create_submodel_from_collected_subgraph → +// merge_submodels). The cross-affinity case — where paired Assign/ReadValue land in different +// subgraphs — is intentionally not covered here: the expected SubgraphCollector contract for +// splitting stateful variable pairs is not yet specified; that case should be added in a +// follow-up once the contract is clarified. - const std::map supported_ops = { - {"input1", "MOCK.0"}, - {"input2", "MOCK.0"}, - {"const_val1", "MOCK.0"}, - {"add1", "MOCK.0"}, - {"const_val2", "MOCK.1"}, - {"add2", "MOCK.1"}, - {"res", "MOCK.1"}, +struct SubgraphCollectorTestParam { + using ModelFactory = std::function()>; + // --- required fields --- + std::string test_name; + ModelFactory create_model; // factory to build the model under test + std::map affinity_map; // node_name → device; empty = broadcast default + std::string default_affinity; // used when affinity_map is empty + // Expected number of subgraphs from run(). std::nullopt explicitly opts out of the count + // check; use only when the partition shape is an implementation detail but convergence and + // merge round-trip must still hold (e.g. SCC fallback tests). + std::optional expected_subgraph_count; + // --- optional checks (a default-constructed/empty/false value disables the check) --- + std::vector expected_affinities = {}; // sorted affinity list per subgraph + std::map expected_ids = {}; // node_name → expected subgraph ID + std::vector expected_submodel_factories = {}; // reference submodel per subgraph + SubgraphsMappingInfo expected_mapping = {}; // expected mapping info from run() + size_t expected_total_sinks = 0; // sum of sg._sinks.size() across subgraphs (0 = no check) + bool verify_merge_roundtrip = false; // merge submodels back and check size == 1 + bool verify_merge_compare = false; // compare_functions(original, merged) + // Per-resulting-subgraph structural counts. Empty vector = check disabled. When non-empty, + // size MUST equal the actual runtime subgraph count (`subgraphs.size()`); each entry is the + // expected count in the subgraph at the same index. This remains valid even when + // expected_subgraph_count is std::nullopt. Intended primarily as direct evidence of Constant + // duplication after a promoted boundary (see shared_const_*_cycle cases), without requiring + // a full reference submodel via expected_submodel_factories. + std::vector expected_constants_per_submodel = {}; + std::vector expected_parameters_per_submodel = {}; + std::vector expected_results_per_submodel = {}; + std::vector> expected_runtime_subgraph_nodes = + {}; // final ordered subgraph membership by non-interface, non-Constant op names +}; + +class SubgraphCollectorParamTest : public testing::TestWithParam {}; + +TEST_P(SubgraphCollectorParamTest, split_by_affinity) { + const auto& param = GetParam(); + auto model = param.create_model(); + auto model_ref = model->clone(); + + // Test fixture guard: friendly_name uniqueness is not an ov::Model invariant, but every + // factory above relies on it to key affinity_map / expected_ids by name. A duplicate would + // silently corrupt those lookups, so fail fast with a clear message instead. + { + std::set seen_names; + for (const auto& node : model->get_ordered_ops()) { + ASSERT_TRUE(seen_names.insert(node->get_friendly_name()).second) + << "Test fixture bug: duplicate friendly_name '" << node->get_friendly_name() << "'"; + } + } + + SubgraphCollector::AffinitiesMap affinities; + for (const auto& node : model->get_ordered_ops()) { + if (param.affinity_map.empty()) { + affinities[node] = param.default_affinity; + } else { + const auto& name = node->get_friendly_name(); + const auto it = param.affinity_map.find(name); + ASSERT_TRUE(it != param.affinity_map.end()) << "Missing affinity for node '" << name << "'"; + affinities[node] = it->second; + } + } + + SubgraphCollector collector(model, affinities); + + // Check subgraph IDs if expected + if (!param.expected_ids.empty()) { + auto actual_ids = collector.get_subgraph_ids(); + ASSERT_EQ(param.expected_ids.size(), actual_ids.size()); + for (const auto& [node, actual_id] : actual_ids) { + const auto& name = node->get_friendly_name(); + auto it = param.expected_ids.find(name); + ASSERT_TRUE(it != param.expected_ids.end()) << "No expected ID for node: " << name; + ASSERT_EQ(it->second, actual_id) << "ID mismatch for node: " << name; + } + } + + const auto& [subgraphs, mapping] = collector.run(); + + if (param.expected_subgraph_count.has_value()) { + ASSERT_EQ(*param.expected_subgraph_count, subgraphs.size()); + } + + std::map actual_to_expected_subgraph_ids; + std::vector expected_to_actual_subgraph_ids; + if (!param.expected_runtime_subgraph_nodes.empty()) { + ASSERT_EQ(param.expected_runtime_subgraph_nodes.size(), subgraphs.size()); + expected_to_actual_subgraph_ids.resize(subgraphs.size()); + std::set matched_expected_subgraphs; + for (size_t i = 0; i < subgraphs.size(); ++i) { + const auto submodel = create_submodel_from_collected_subgraph(subgraphs[i]); + std::set actual_node_names; + + for (const auto& op : submodel->get_ordered_ops()) { + if (std::dynamic_pointer_cast(op) || + std::dynamic_pointer_cast(op) || + std::dynamic_pointer_cast(op)) { + continue; + } + actual_node_names.insert(op->get_friendly_name()); + } + + ASSERT_FALSE(actual_node_names.empty()) << "Failed to derive canonical nodes for subgraph " << i; + + bool matched = false; + for (size_t expected_idx = 0; expected_idx < param.expected_runtime_subgraph_nodes.size(); ++expected_idx) { + if (param.expected_runtime_subgraph_nodes[expected_idx] != actual_node_names) { + continue; + } + + ASSERT_TRUE(matched_expected_subgraphs.insert(expected_idx).second) + << "Duplicate canonical subgraph match for expected subgraph " << expected_idx; + ASSERT_TRUE(actual_to_expected_subgraph_ids.emplace(i, expected_idx).second) + << "Duplicate canonical ID assignment for actual subgraph " << i; + expected_to_actual_subgraph_ids[expected_idx] = i; + matched = true; + break; + } + + ASSERT_TRUE(matched) << "Failed to match actual subgraph " << i << " to expected runtime membership"; + } + } + + // Check affinities (sorted comparison) + if (!param.expected_affinities.empty()) { + std::vector actual_affinities; + for (const auto& sg : subgraphs) { + actual_affinities.push_back(sg._affinity); + } + std::sort(actual_affinities.begin(), actual_affinities.end()); + auto expected_sorted = param.expected_affinities; + std::sort(expected_sorted.begin(), expected_sorted.end()); + ASSERT_EQ(expected_sorted, actual_affinities); + } + + // Check submodel structure if reference factories provided + if (!param.expected_submodel_factories.empty()) { + std::vector> actual_submodels; + for (const auto& sg : subgraphs) { + actual_submodels.push_back(create_submodel_from_collected_subgraph(sg)); + } + ASSERT_EQ(param.expected_submodel_factories.size(), actual_submodels.size()); + + auto unmatched_actual_submodels = actual_submodels; + for (size_t i = 0; i < param.expected_submodel_factories.size(); i++) { + auto expected_submodel = param.expected_submodel_factories[i](); + bool matched = false; + std::string mismatch_details; + + // Order-independent match: scan remaining actuals, take the first that compares equal, + // and remove it from the pool so each expected pairs with a distinct actual submodel. + for (auto it = unmatched_actual_submodels.begin(); it != unmatched_actual_submodels.end(); ++it) { + auto res = compare_functions(expected_submodel, *it); + if (res.first) { + unmatched_actual_submodels.erase(it); + matched = true; + break; + } + if (mismatch_details.empty()) { + mismatch_details = res.second; + } + } + + ASSERT_TRUE(matched) << "Failed to find a matching actual submodel for expected submodel at index " << i + << (mismatch_details.empty() ? "" : ". Example mismatch: ") << mismatch_details; + } + } + + // Check mapping info if provided + const bool check_mapping = !param.expected_mapping._inputs_to_submodels_inputs.empty() || + !param.expected_mapping._outputs_to_submodels_outputs.empty() || + !param.expected_mapping._submodels_input_to_prev_output.empty(); + if (check_mapping) { + auto normalized_mapping = mapping; + + // Some partition shapes are structurally identical across platforms but receive different + // runtime subgraph numbering. When the expected final subgraph membership is provided, + // normalize the actual subgraph indices back to that canonical numbering, then keep the + // mapping comparison exact. + if (!param.expected_runtime_subgraph_nodes.empty()) { + const auto remap_node_info = [&](const NodeInfo& info) -> NodeInfo { + const auto it = actual_to_expected_subgraph_ids.find(info.first); + if (it == actual_to_expected_subgraph_ids.end()) { + ADD_FAILURE() << "Missing canonical subgraph ID for actual subgraph " << info.first; + return info; + } + return NodeInfo{it->second, info.second}; + }; + + std::transform(normalized_mapping._inputs_to_submodels_inputs.begin(), + normalized_mapping._inputs_to_submodels_inputs.end(), + normalized_mapping._inputs_to_submodels_inputs.begin(), + remap_node_info); + std::transform(normalized_mapping._outputs_to_submodels_outputs.begin(), + normalized_mapping._outputs_to_submodels_outputs.end(), + normalized_mapping._outputs_to_submodels_outputs.begin(), + remap_node_info); + + std::map remapped_prev_outputs; + for (const auto& [submodel_input, prev_output] : normalized_mapping._submodels_input_to_prev_output) { + remapped_prev_outputs.emplace(remap_node_info(submodel_input), remap_node_info(prev_output)); + } + normalized_mapping._submodels_input_to_prev_output = std::move(remapped_prev_outputs); + } + + ASSERT_EQ(param.expected_mapping._inputs_to_submodels_inputs, normalized_mapping._inputs_to_submodels_inputs); + ASSERT_EQ(param.expected_mapping._outputs_to_submodels_outputs, + normalized_mapping._outputs_to_submodels_outputs); + ASSERT_EQ(param.expected_mapping._submodels_input_to_prev_output, + normalized_mapping._submodels_input_to_prev_output); + } + + // Per-subgraph structural counts. Used as direct evidence for promotions that are realized by + // node duplication (e.g. a promoted Constant boundary is satisfied by cloning the Constant + // into the consumer subgraph rather than by an extra cross-subgraph parameter, so it never + // appears in _submodels_input_to_prev_output). Asserting expected_constants_per_submodel + // pins the duplication directly on the public submodel interface. + if (!param.expected_parameters_per_submodel.empty()) { + ASSERT_EQ(param.expected_parameters_per_submodel.size(), subgraphs.size()); + for (size_t i = 0; i < subgraphs.size(); ++i) { + const size_t actual_idx = expected_to_actual_subgraph_ids.empty() ? i : expected_to_actual_subgraph_ids[i]; + EXPECT_EQ(param.expected_parameters_per_submodel[i], subgraphs[actual_idx]._parameters.size()) + << "Parameter count mismatch in subgraph " << i; + } + } + if (!param.expected_results_per_submodel.empty()) { + ASSERT_EQ(param.expected_results_per_submodel.size(), subgraphs.size()); + for (size_t i = 0; i < subgraphs.size(); ++i) { + const size_t actual_idx = expected_to_actual_subgraph_ids.empty() ? i : expected_to_actual_subgraph_ids[i]; + EXPECT_EQ(param.expected_results_per_submodel[i], subgraphs[actual_idx]._results.size()) + << "Result count mismatch in subgraph " << i; + } + } + if (!param.expected_constants_per_submodel.empty()) { + ASSERT_EQ(param.expected_constants_per_submodel.size(), subgraphs.size()); + for (size_t i = 0; i < subgraphs.size(); ++i) { + const size_t actual_idx = expected_to_actual_subgraph_ids.empty() ? i : expected_to_actual_subgraph_ids[i]; + auto submodel = create_submodel_from_collected_subgraph(subgraphs[actual_idx]); + size_t actual = 0; + for (const auto& op : submodel->get_ordered_ops()) { + if (std::dynamic_pointer_cast(op)) { + ++actual; + } + } + EXPECT_EQ(param.expected_constants_per_submodel[i], actual) << "Constant count mismatch in subgraph " << i; + } + } + + // Check total sink count across subgraphs if expected + if (param.expected_total_sinks > 0) { + size_t actual_total_sinks = 0; + for (const auto& sg : subgraphs) { + actual_total_sinks += sg._sinks.size(); + } + ASSERT_EQ(param.expected_total_sinks, actual_total_sinks); + } + + // Optional stronger validation: after the baseline partition checks above, + // verify that merging submodels reconstructs a structurally equivalent model. + if (param.verify_merge_roundtrip) { + std::vector> submodels; + for (const auto& sg : subgraphs) { + submodels.push_back(create_submodel_from_collected_subgraph(sg)); + } + OV_ASSERT_NO_THROW(ov::hetero::merge_submodels(submodels, mapping._submodels_input_to_prev_output)); + ASSERT_EQ(1, submodels.size()); + if (param.verify_merge_compare) { + // This compare is intentionally optional; some cases only check that merge succeeds. + auto res = compare_functions(model_ref, submodels[0]); + ASSERT_TRUE(res.first) << res.second; + } + } +} + +// clang-format off +INSTANTIATE_TEST_SUITE_P( + SubgraphCollector, + SubgraphCollectorParamTest, + testing::Values( + // --- split_and_merge: linear chain model (input → add → sub → reshape → result) --- + SubgraphCollectorTestParam{ + "split_and_merge_linear_chain_model", + create_linear_chain_model, + {{"input", "MOCK.0"}, {"const_val", "MOCK.0"}, {"add", "MOCK.0"}, + {"sub", "MOCK.1"}, {"reshape_val", "MOCK.0"}, {"reshape", "MOCK.0"}, {"res", "MOCK.0"}}, + "", + 3, + {"MOCK.0", "MOCK.1", "MOCK.0"}, + {{"input", 0}, {"const_val", 0}, {"add", 0}, + {"sub", 2}, {"reshape_val", 3}, {"reshape", 3}, {"res", 3}}, + {create_subgraph_add, create_subgraph_sub, create_subgraph_reshape}, + {{NodeInfo{0, 0}}, + {NodeInfo{2, 0}}, + {{NodeInfo{1, 0}, NodeInfo{0, 0}}, + {NodeInfo{2, 0}, NodeInfo{1, 0}}}}, + 0, + true, + true, + }, + // --- split_and_merge: diamond chain with subgraph-level cycle. + // Topology: input → add1(MOCK.0) ─┬→ add2(MOCK.0) → sub(MOCK.1) ─┐ + // └──────────────────────────────┴→ add3(MOCK.0) → reshape → res + // Without cycle splitting, all MOCK.0 nodes (add1/add2/add3/reshape/res) would merge into one + // subgraph that depends on sub(MOCK.1) which itself depends on the same subgraph → cycle. + // Exercises SubgraphCollector::split_cyclic_dependencies(): MOCK.0 must be split into two + // subgraphs ({add1, add2} and {add3, reshape, res}) yielding 3 subgraphs total. + SubgraphCollectorTestParam{ + "split_cyclic_dependency_diamond", + create_diamond_chain_model, + {{"input", "MOCK.0"}, {"const_val", "MOCK.0"}, {"add1", "MOCK.0"}, + {"add2", "MOCK.0"}, {"sub", "MOCK.1"}, {"add3", "MOCK.0"}, + {"reshape_val", "MOCK.0"}, {"reshape", "MOCK.0"}, {"res", "MOCK.0"}}, + "", + 3, + {"MOCK.0", "MOCK.1", "MOCK.0"}, + {}, + {create_subgraph_add_add, create_subgraph_sub, create_subgraph_add_reshape}, + {{NodeInfo{0, 0}}, + {NodeInfo{2, 0}}, + {{NodeInfo{1, 0}, NodeInfo{0, 0}}, + {NodeInfo{2, 0}, NodeInfo{0, 1}}, + {NodeInfo{2, 1}, NodeInfo{1, 0}}}}, + 0, + true, + true, + }, + // --- split_cyclic: nested cycles requiring multi-iteration fixed-point splitting. + // Topology: input → a1(M0) → b1(M1) → a2(M0) → b2(M1) → a3(M0) → res(M0) + // └────────────────────────────────────┘ (a1 also feeds a3) + // Initial M0 group {a1,a2,a3} contains two stacked cyclic deps (via b1 and b2). + // A single split iteration cannot resolve both; the outer fixed-point loop in + // split_cyclic_dependencies() must run >=2 iterations. Expected: 5 subgraphs + // alternating MOCK.0 / MOCK.1 / MOCK.0 / MOCK.1 / MOCK.0 (3×M0 + 2×M1). + SubgraphCollectorTestParam{ + "split_cyclic_dependency_nested", + create_nested_cyclic_chain_model, + {{"input", "MOCK.0"}, {"c1", "MOCK.0"}, {"a1", "MOCK.0"}, + {"b1", "MOCK.1"}, {"a2", "MOCK.0"}, {"b2", "MOCK.1"}, + {"a3", "MOCK.0"}, {"res", "MOCK.0"}}, + "", + 5, + {"MOCK.0", "MOCK.0", "MOCK.0", "MOCK.1", "MOCK.1"}, + // expected_ids: locks in the partition produced by collect_subgraphs_ids() after + // split_cyclic_dependencies() promotes boundaries for {a2 ← c1} and {a3 ← a1}. + // Union-find keeps-first merging causes the c1 placeholder id (1) to be absorbed + // into the input/a1 group (id 0), so allocated ids are {0, 2, 3, 4, 5} — non-contiguous + // by design (same pattern as split_and_merge_linear_chain_model). + {{"input", 0}, {"c1", 0}, {"a1", 0}, + {"b1", 2}, {"a2", 3}, {"b2", 4}, + {"a3", 5}, {"res", 5}}, + {}, + {}, + 0, + true, + true, + }, + // --- merge_independent: param1 → add1(MOCK.0) → add2(MOCK.1) → res, plus unused param2. + // Splits into 3 subgraphs (M0 chain, M1 chain, and an isolated subgraph for the unused + // param2). The merge roundtrip is verified to not throw and to collapse back to a single + // model, but structural equality (verify_merge_compare) is intentionally skipped: + // merge_submodels does not guarantee preservation of the original Parameter ordering when + // an isolated/unused-input subgraph is involved, so compare_functions would report a + // benign ordering mismatch rather than a real regression. + SubgraphCollectorTestParam{ + "merge_independent_submodel", + create_merge_independent_model, + {{"input1", "MOCK.0"}, {"input2", "MOCK.0"}, {"const_val1", "MOCK.0"}, + {"add1", "MOCK.0"}, {"const_val2", "MOCK.1"}, {"add2", "MOCK.1"}, {"res", "MOCK.1"}}, + "", + 3, + {}, + {}, + {}, + {}, + 0, + true, + false, + }, + // --- Affinity: all same device → single subgraph --- + SubgraphCollectorTestParam{ + "all_same_affinity", + create_linear_chain_model, + {}, + "MOCK.0", + 1, + {"MOCK.0"}, + {}, + {}, + {}, + 0, + false, + false, + }, + // --- Affinity: each computational node on different device --- + SubgraphCollectorTestParam{ + "all_different_affinities", + create_linear_chain_model, + {{"input", "MOCK.0"}, {"const_val", "MOCK.0"}, {"add", "MOCK.1"}, + {"sub", "MOCK.2"}, {"reshape_val", "MOCK.3"}, {"reshape", "MOCK.3"}, {"res", "MOCK.3"}}, + "", + 4, + {"MOCK.0", "MOCK.1", "MOCK.2", "MOCK.3"}, + {}, + {}, + {}, + 0, + false, + false, + }, + // --- Affinity: result inherits affinity from its input --- + SubgraphCollectorTestParam{ + "result_inherits_input_affinity", + create_linear_chain_model, + {{"input", "MOCK.0"}, {"const_val", "MOCK.0"}, {"add", "MOCK.0"}, + {"sub", "MOCK.0"}, {"reshape_val", "MOCK.0"}, {"reshape", "MOCK.0"}, {"res", "MOCK.1"}}, + "", + 1, + {"MOCK.0"}, + {}, + {}, + {}, + 0, + false, + false, + }, + // --- Affinity: alternating devices A→B→C→D → 4 subgraphs --- + SubgraphCollectorTestParam{ + "alternating_devices", + create_alternating_chain_model, + {{"input", "MOCK.0"}, {"c1", "MOCK.0"}, {"A", "MOCK.0"}, + {"B", "MOCK.1"}, {"C", "MOCK.0"}, {"D", "MOCK.1"}, {"res", "MOCK.1"}}, + "", + 4, + {"MOCK.0", "MOCK.0", "MOCK.1", "MOCK.1"}, + {}, + {}, + {}, + 0, + false, + false, + }, + // --- Affinity: two disconnected paths, same device → 2 subgraphs --- + SubgraphCollectorTestParam{ + "independent_paths_same_affinity", + create_independent_paths_model, + {}, + "MOCK.0", + 2, + {"MOCK.0", "MOCK.0"}, + {}, + {}, + {}, + 0, + false, + false, + }, + // --- Affinity: diverging paths from shared node → 1 subgraph --- + SubgraphCollectorTestParam{ + "diverging_paths_no_split", + create_diverging_paths_model, + {}, + "MOCK.0", + 1, + {"MOCK.0"}, + {}, + {}, + {}, + 0, + false, + false, + }, + // --- Sink coverage: single-device stateful model (ReadValue/Assign on MOCK.0). + // Verifies that Subgraph::_sinks is populated, survives create_submodel_from_collected_subgraph, + // and is preserved by merge_submodels (compare_functions checks sink-set equality). + SubgraphCollectorTestParam{ + "stateful_assign_readvalue_single_device", + create_stateful_single_device_model, + {}, + "MOCK.0", + 1, + {"MOCK.0"}, + {}, + {}, + {}, + 1, + true, + true, + }, + // --- Shared constant indirect bridge: shared_const bridges A and C into one subgraph via X. + // split_cyclic_dependencies() detects the cycle re-entry point and promotes + // C.input(1) (from shared_const) as a boundary, splitting the M0 group into + // {param, other_const, shared_const, A, X, res2} and {C, res1}, yielding 3 subgraphs: + // {param, other_const, shared_const, A, X, res2}(M0), {B}(M1), {C, res1}(M0). + SubgraphCollectorTestParam{ + "shared_const_indirect_bridge_cycle", + create_shared_const_indirect_bridge_cycle_model, + {{"input", "MOCK.0"}, {"shared_const", "MOCK.0"}, {"other_const", "MOCK.0"}, + {"A", "MOCK.0"}, {"X", "MOCK.0"}, + {"B", "MOCK.1"}, {"C", "MOCK.0"}, {"res1", "MOCK.0"}, {"res2", "MOCK.0"}}, + "", + 3, + {"MOCK.0", "MOCK.0", "MOCK.1"}, + {{"input", 0}, {"other_const", 0}, {"shared_const", 0}, {"A", 0}, {"X", 0}, {"res2", 0}, + {"B", 3}, {"C", 4}, {"res1", 4}}, + {}, + // Inter-subgraph wiring after Phase 4b promotion. Run-time subgraph order: + // sg0 = {input, other_const, shared_const, A, X, res2}(MOCK.0) + // sg1 = {B}(MOCK.1) + // sg2 = {C, res1}(MOCK.0) <- newly split off; shared_const is duplicated here. + // inputs_to_submodels_inputs: input -> sg0 param 0 + // outputs_to_submodels_outputs: res1 -> sg2 output 0 + // res2 -> sg0 output 0 + // submodels_input_to_prev_output (cross-subgraph data edges): + // sg1.in[0] (B.input(0)) <- sg0.out[1] (A) + // sg2.in[0] (C.input(0)) <- sg1.out[0] (B) + // Note: C.input(1) (the PROMOTED edge from shared_const) is satisfied by duplicating + // shared_const inside sg2 rather than as a cross-subgraph parameter, so it does not + // appear in submodels_input_to_prev_output. The direct evidence of duplication is + // expected_constants_per_submodel below: sg0 keeps shared_const + other_const = 2, + // sg2 gets a cloned shared_const = 1. expected_ids only locks the original node's + // placement (sg0) and cannot express the clone in sg2. + SubgraphsMappingInfo{ + /*_inputs_to_submodels_inputs*/ {NodeInfo{0, 0}}, + /*_outputs_to_submodels_outputs*/ {NodeInfo{2, 0}, NodeInfo{0, 0}}, + /*_submodels_input_to_prev_output*/ + {{NodeInfo{1, 0}, NodeInfo{0, 1}}, + {NodeInfo{2, 0}, NodeInfo{1, 0}}}}, + 0, + true, + true, + // Per-subgraph structural counts. Constant counts are the direct evidence that + // shared_const was duplicated into sg2 (sg0 still holds the original shared_const + + // other_const = 2; sg2 holds the cloned shared_const = 1; sg1 holds none). + /*expected_constants_per_submodel*/ {2, 0, 1}, + /*expected_parameters_per_submodel*/ {1, 1, 1}, + /*expected_results_per_submodel*/ {2, 1, 1}, + }, + // --- Shared constant cross-device fanout: shared_const fans out to 3 consumers on 2 devices. + // Union-Find merges same-affinity edges into one GPU subgraph containing Node_A and Node_C. + // split_cyclic_dependencies() promotes Node_C.input(1) (from shared_const) as a boundary, + // yielding 3 subgraphs: {param, shared_const, Node_A}(GPU), {Node_B}(CPU), {Node_C, res}(GPU). + SubgraphCollectorTestParam{ + "shared_const_cross_device_fanout_cycle", + create_shared_const_cross_device_fanout_model, + {{"param", "MOCK.0"}, {"shared_const", "MOCK.0"}, {"Node_A", "MOCK.0"}, + {"Node_B", "MOCK.1"}, {"Node_C", "MOCK.0"}, {"res", "MOCK.0"}}, + "", + 3, + {"MOCK.0", "MOCK.0", "MOCK.1"}, + {{"param", 0}, {"shared_const", 0}, {"Node_A", 0}, + {"Node_B", 2}, {"Node_C", 3}, {"res", 3}}, + {}, + // Inter-subgraph wiring after promotion. Run-time subgraph order: + // sg0 = {param, shared_const, Node_A}(MOCK.0) + // sg1 = {Node_B}(MOCK.1) + // sg2 = {Node_C, res}(MOCK.0) <- newly split off; shared_const is duplicated here. + // inputs_to_submodels_inputs: param -> sg0 param 0 + // outputs_to_submodels_outputs: res -> sg2 output 0 + // submodels_input_to_prev_output: + // sg1.in[0] (Node_B.input(0)) <- sg0.out[0] (Node_A) + // sg2.in[0] (Node_C.input(0)) <- sg1.out[0] (Node_B) + // Note: Node_C.input(1) (the PROMOTED edge from shared_const) is satisfied by + // duplicating shared_const inside sg2 rather than as a cross-subgraph parameter. The + // direct evidence of duplication is expected_constants_per_submodel below: every + // subgraph that consumes shared_const holds its own copy (sg0/sg1/sg2 = 1 each). + // expected_ids only locks the original node's placement (sg0) and cannot express the + // clones in sg1/sg2. + SubgraphsMappingInfo{ + /*_inputs_to_submodels_inputs*/ {NodeInfo{0, 0}}, + /*_outputs_to_submodels_outputs*/ {NodeInfo{2, 0}}, + /*_submodels_input_to_prev_output*/ + {{NodeInfo{1, 0}, NodeInfo{0, 0}}, + {NodeInfo{2, 0}, NodeInfo{1, 0}}}}, + 0, + true, + true, + // Per-subgraph structural counts. Constant counts are the direct evidence that + // shared_const was duplicated across all three subgraphs: sg0 has the original (which + // also feeds Node_A locally), sg1 has a clone feeding Node_B (the cross-device + // consumer), and sg2 has a clone feeding Node_C (the promoted boundary). Without + // promotion sg2 would not exist and only sg0+sg1 would each hold a copy. + /*expected_constants_per_submodel*/ {1, 1, 1}, + /*expected_parameters_per_submodel*/ {1, 1, 1}, + /*expected_results_per_submodel*/ {1, 1, 1}, + }, + // --- Shared constant AS the cycle source: shared_const directly feeds B (cross-device), + // making it the cycle producer. It also merges with the re-entry node C via the internal + // chain shared_const→X→C. split_cyclic_dependencies() promotes C.input(1) (from X), + // yielding 4 subgraphs: {param,A}(DEV0), {B}(DEV1), {shared_const,X,res2}(DEV0), {C,res1}(DEV0). + SubgraphCollectorTestParam{ + "shared_const_as_cycle_source", + create_shared_const_as_cycle_source_model, + {{"param", "MOCK.0"}, {"shared_const", "MOCK.0"}, {"A", "MOCK.0"}, {"X", "MOCK.0"}, + {"B", "MOCK.1"}, {"C", "MOCK.0"}, {"res1", "MOCK.0"}, {"res2", "MOCK.0"}}, + "", + 4, + {"MOCK.0", "MOCK.0", "MOCK.0", "MOCK.1"}, + {{"param", 0}, {"A", 0}, + {"shared_const", 1}, {"X", 1}, {"res2", 1}, + {"B", 2}, {"C", 3}, {"res1", 3}}, + {}, + // Inter-subgraph wiring after Phase 4b promotion. Run-time subgraph order: + // sg0 = {shared_const, X, res2}(MOCK.0) + // sg1 = {param, A}(MOCK.0) + // sg2 = {B}(MOCK.1) + // sg3 = {C, res1}(MOCK.0) <- newly split off via PROMOTED C.input(1) <- X. + // inputs_to_submodels_inputs: param -> sg1 param 0 + // outputs_to_submodels_outputs: res1 -> sg3 output 0 + // res2 -> sg0 output 0 + // submodels_input_to_prev_output: + // sg2.in[0] (B.input(0)) <- sg1.out[0] (A) + // sg3.in[0] (C.input(0)) <- sg2.out[0] (B) <- existing cycle edge + // sg3.in[1] (C.input(1)) <- sg0.out[1] (X) <- PROMOTED boundary + // sg0 exposes X as an extra output (beyond res2) precisely because the promotion + // turned X->C into a cross-subgraph edge; this entry is the direct evidence that + // split_cyclic_dependencies() promoted X (not, e.g., A or shared_const). + SubgraphsMappingInfo{ + /*_inputs_to_submodels_inputs*/ {NodeInfo{1, 0}}, + /*_outputs_to_submodels_outputs*/ {NodeInfo{3, 0}, NodeInfo{0, 0}}, + /*_submodels_input_to_prev_output*/ + {{NodeInfo{2, 0}, NodeInfo{1, 0}}, + {NodeInfo{3, 0}, NodeInfo{2, 0}}, + {NodeInfo{3, 1}, NodeInfo{0, 1}}}}, + 0, + true, + true, + {}, + {}, + {}, + {std::set{"X"}, std::set{"A"}, std::set{"B"}, std::set{"C"}}, + }, + // --- Negative: shared constant merges nodes via Union-Find but NO cycle exists. + // Data flows only SG0→SG1 (A→B), never back. Must produce exactly 2 subgraphs + // without over-splitting — verifies the re-entry scan doesn't fire spuriously. + SubgraphCollectorTestParam{ + "shared_const_no_cycle_no_split", + create_shared_const_no_cycle_model, + {{"param", "MOCK.0"}, {"shared_const", "MOCK.0"}, {"A", "MOCK.0"}, {"X", "MOCK.0"}, + {"B", "MOCK.1"}, {"res", "MOCK.1"}, {"res2", "MOCK.0"}}, + "", + 2, + {"MOCK.0", "MOCK.1"}, + {}, + {}, + // Negative case: exactly ONE cross-subgraph edge (A -> B). Run-time order: + // sg0 = {param, shared_const, A, X, res2}(MOCK.0) + // sg1 = {B, res}(MOCK.1) + // inputs_to_submodels_inputs: param -> sg0 param 0 + // outputs_to_submodels_outputs: res -> sg1 output 0 + // res2 -> sg0 output 0 + // submodels_input_to_prev_output: + // sg1.in[0] (B.input(0)) <- sg0.out[1] (A) + // The SINGLE entry here is the key minimality assertion: any spurious promotion + // would add at least one more entry (e.g. a sg1.in[1] <- sg0.out[k] edge). + SubgraphsMappingInfo{ + /*_inputs_to_submodels_inputs*/ {NodeInfo{0, 0}}, + /*_outputs_to_submodels_outputs*/ {NodeInfo{1, 0}, NodeInfo{0, 0}}, + /*_submodels_input_to_prev_output*/ + {{NodeInfo{1, 0}, NodeInfo{0, 1}}}}, + 0, + true, + true, + }, + // --- Negative mirroring shared_const_indirect_bridge_cycle node-for-node, minus the + // B→C back-edge: C is fed from A (M0) instead of B (M1). The indirect Union-Find merge + // through shared_const ({A, X, C, shared_const} on M0) is preserved exactly as in the + // positive case, but no cycle exists. The collector must NOT over-split — expected: 2 + // subgraphs, with shared_const + other_const both staying in sg0 (no clone in sg1). + // This complements shared_const_no_cycle_no_split with a topology much closer to the + // bug surface (indirect bridge via X, not a single one-way hop). + SubgraphCollectorTestParam{ + "shared_const_indirect_bridge_no_cycle", + create_shared_const_indirect_bridge_no_cycle_model, + {{"input", "MOCK.0"}, {"shared_const", "MOCK.0"}, {"other_const", "MOCK.0"}, + {"A", "MOCK.0"}, {"X", "MOCK.0"}, {"C", "MOCK.0"}, + {"B", "MOCK.1"}, + {"res1", "MOCK.0"}, {"res2", "MOCK.0"}, {"res_b", "MOCK.1"}}, + "", + 2, + {"MOCK.0", "MOCK.1"}, + {}, + {}, + {}, + 0, + true, + true, + // sg0 (MOCK.0) keeps both Constants (shared_const + other_const) — no clone leaks + // into sg1. Together with expected_subgraph_count==2 this is the direct assertion + // that the re-entry scan did NOT fire on this no-cycle shape. + // sg0 holds 3 Results: res1, res2 + an auto-inserted Result exposing A to sg1's + // boundary Parameter. sg1 holds res_b only. + /*expected_constants_per_submodel*/ {2, 0}, + /*expected_parameters_per_submodel*/ {1, 1}, + /*expected_results_per_submodel*/ {3, 1}, + }, + // --- Multiple shared constants, only ONE participating in the cyclic path. Locks the + // contract that the algorithm duplicates the cyclic-boundary Constant only, not every + // shared Constant in the M0 group. After promotion of C.input(1)=C1: + // sg0 = {param, C1, A, C2, X, Z, res_x, res_z}(MOCK.0) + // sg1 = {B}(MOCK.1) + // sg2 = {C, res_c}(MOCK.0) <- cloned C1 inside; C2 is NOT duplicated here. + // inputs_to_submodels_inputs: param -> sg0 param 0 + // outputs_to_submodels_outputs: res_c -> sg2.out[0], res_x -> sg0.out[0], res_z -> sg0.out[1] + // submodels_input_to_prev_output: + // sg1.in[0] (B.input(0)) <- sg0.out[2] (A, auto-Result) + // sg2.in[0] (C.input(0)) <- sg1.out[0] (B) + // expected_constants_per_submodel = {2, 0, 1} is the direct evidence that only C1 was + // cloned into sg2; C2 stayed shared between X and Z within sg0. + SubgraphCollectorTestParam{ + "multiple_shared_constants_partial_cycle", + create_multiple_shared_constants_partial_cycle_model, + {{"param", "MOCK.0"}, {"C1", "MOCK.0"}, {"C2", "MOCK.0"}, + {"A", "MOCK.0"}, {"X", "MOCK.0"}, {"Z", "MOCK.0"}, {"C", "MOCK.0"}, + {"B", "MOCK.1"}, + {"res_c", "MOCK.0"}, {"res_x", "MOCK.0"}, {"res_z", "MOCK.0"}}, + "", + 3, + {"MOCK.0", "MOCK.0", "MOCK.1"}, + {}, + {}, + SubgraphsMappingInfo{ + /*_inputs_to_submodels_inputs*/ {NodeInfo{0, 0}}, + /*_outputs_to_submodels_outputs*/ {NodeInfo{2, 0}, NodeInfo{0, 0}, NodeInfo{0, 1}}, + /*_submodels_input_to_prev_output*/ + {{NodeInfo{1, 0}, NodeInfo{0, 2}}, + {NodeInfo{2, 0}, NodeInfo{1, 0}}}}, + 0, + true, + true, + /*expected_constants_per_submodel*/ {2, 0, 1}, + /*expected_parameters_per_submodel*/ {1, 1, 1}, + /*expected_results_per_submodel*/ {3, 1, 1}, + }, + // --- Mixed Parameter/Constant bridges in the same cycle. Two independent re-entry + // nodes carry one bridge each; the boundary realization is provably different: + // sg0 = {param1, param2, A, C_const, X, res_x}(MOCK.0) + // sg1 = {B}(MOCK.1) + // sg2 = {C, res_c}(MOCK.0) <- receives param2 via cross-subgraph edge (Parameter bridge) + // sg3 = {F, res_f}(MOCK.0) <- cloned C_const inside (Constant bridge, NO mapping entry) + // inputs_to_submodels_inputs: param1 -> sg0 param 0, param2 -> sg0 param 1 + // outputs_to_submodels_outputs: res_c -> sg2.out[0], res_f -> sg3.out[0], + // res_x -> sg0.out[0], res_b -> sg1.out[0] + // submodels_input_to_prev_output: + // sg1.in[0] (B.input(0)) <- sg0.out[1] (A, auto-Result) + // sg2.in[0] (C.input(0)=B) <- sg1.out[2] (B, auto-Result) + // sg2.in[1] (C.input(1)=param2) <- sg0.out[2] (param2, auto-Result) <- PARAMETER BRIDGE + // sg3.in[0] (F.input(0)=B2) <- sg1.out[1] (B2, auto-Result) + // The {2,1} <- {0,2} entry is the literal cross-subgraph edge for param2. The absence of + // any sg3.in[?] <- sg0.out[?] entry for C_const is the literal evidence that C_const was + // realized by duplication (confirmed by expected_constants_per_submodel = {1,0,0,1}). + SubgraphCollectorTestParam{ + "mixed_parameter_constant_bridge_cycle", + create_mixed_parameter_constant_bridge_cycle_model, + {{"param1", "MOCK.0"}, {"param2", "MOCK.0"}, {"C_const", "MOCK.0"}, + {"A", "MOCK.0"}, {"X", "MOCK.0"}, {"C", "MOCK.0"}, {"F", "MOCK.0"}, + {"B", "MOCK.1"}, {"B2", "MOCK.1"}, + {"res_c", "MOCK.0"}, {"res_f", "MOCK.0"}, {"res_x", "MOCK.0"}, {"res_b", "MOCK.1"}}, + "", + 4, + {"MOCK.0", "MOCK.0", "MOCK.0", "MOCK.1"}, + {}, + {}, + SubgraphsMappingInfo{ + /*_inputs_to_submodels_inputs*/ {NodeInfo{0, 0}, NodeInfo{0, 1}}, + /*_outputs_to_submodels_outputs*/ + {NodeInfo{2, 0}, NodeInfo{3, 0}, NodeInfo{0, 0}, NodeInfo{1, 0}}, + /*_submodels_input_to_prev_output*/ + {{NodeInfo{1, 0}, NodeInfo{0, 1}}, + {NodeInfo{2, 0}, NodeInfo{1, 2}}, + {NodeInfo{2, 1}, NodeInfo{0, 2}}, + {NodeInfo{3, 0}, NodeInfo{1, 1}}}}, + 0, + // merge round-trip disabled: the round-trip helper currently does not handle + // Parameter cross-edges (same model Parameter referenced both as a top-level input + // and as a {prev_output, sg_input} bridge entry) and trips Model::add_parameters' + // duplicate-parameter check. The structural claim under test is the mapping shape + // above; the round-trip limitation is orthogonal and out of scope here. + false, + false, + /*expected_constants_per_submodel*/ {1, 0, 0, 1}, + /*expected_parameters_per_submodel*/ {2, 1, 2, 1}, + /*expected_results_per_submodel*/ {3, 3, 1, 1}, + {std::set{"A", "X"}, std::set{"B", "B2"}, std::set{"C"}, std::set{"F"}}, + }, + // --- Multi-hop subgraph-level SCC. Two independent M0 regions get fused through a shared + // Constant (c_top), and the fused M0 subgraph then participates in a cycle that no single + // node can detect (the producer and re-entry consumer are far apart). The per-node + // heuristic in split_cyclic_dependencies() converges without breaking it; only the + // subgraph-DAG SCC fallback can. This case is the minimal synthesis of the + // 4-subgraph cycle observed on yolo26s-seg HETERO:GPU,CPU. The exact partition the SCC + // fallback produces depends on the order it discovers cyclic subgraphs; this test only + // asserts that compile-time topo sort succeeds (i.e., the assertion "Cannot sort + // subgraphs!" does NOT fire) by requiring run() to complete and merge round-trip back to + // the original. + SubgraphCollectorTestParam{ + "multi_hop_subgraph_scc_cycle", + create_multi_hop_scc_cycle_model, + {{"in1", "MOCK.0"}, {"in2", "MOCK.0"}, {"c_top", "MOCK.0"}, + {"A1", "MOCK.0"}, {"B1", "MOCK.1"}, {"C1", "MOCK.0"}, {"D1", "MOCK.1"}, {"X1", "MOCK.0"}, + {"A2", "MOCK.0"}, {"B2", "MOCK.1"}, {"C2", "MOCK.1"}, {"D2", "MOCK.0"}, {"X2", "MOCK.0"}, + {"res_x1", "MOCK.0"}, {"res_x2", "MOCK.0"}}, + "", + // expected_subgraph_count = std::nullopt: the SCC fallback's promotion ordering is an + // implementation detail; the contract under test is "run() does not assert Cannot sort + // subgraphs!" and "merge round-trip succeeds". + std::nullopt, + {}, + {}, + {}, + {}, + 0, + true, + true, + } + ), + [](const testing::TestParamInfo& info) { + return info.param.test_name; + }); +// clang-format on + +// Regression test for the SCC fallback's bridge-between-cycles handling. See the comment on +// create_bridge_between_cycles_model() for the topology and why an exact SCC algorithm is +// required here. The contract under test: an acyclic bridge subgraph lying between two +// disjoint cycles in the subgraph DAG must NOT be split by the SCC fallback. The two M2 ops +// (x_bridge1, x_bridge2) belong to one bridge subgraph in the initial partition; after run() +// converges they must still share a subgraph. A regression that swaps the exact SCC algorithm +// back to a two-peel (forward + reverse Kahn) over-approximation would mark the bridge as +// cyclic and eventually promote x_bridge1 → x_bridge2 in the inner loop, splitting the bridge +// into two singletons — which this test then catches. +TEST(SubgraphCollectorBridgeBetweenCyclesTest, bridge_subgraph_not_split) { + auto model = create_bridge_between_cycles_model(); + const std::map affinity_by_name = { + {"in_L", "MOCK.0"}, + {"c_LA", "MOCK.0"}, + {"c_LB", "MOCK.1"}, + {"a_L1", "MOCK.0"}, + {"b_L1", "MOCK.1"}, + {"b_L2", "MOCK.1"}, + {"a_L2", "MOCK.0"}, + {"x_bridge1", "MOCK.2"}, + {"x_bridge2", "MOCK.2"}, + {"c_RA", "MOCK.0"}, + {"c_RB", "MOCK.1"}, + {"a_R1", "MOCK.0"}, + {"b_R1", "MOCK.1"}, + {"b_R2", "MOCK.1"}, + {"a_R2", "MOCK.0"}, + {"res", "MOCK.0"}, }; SubgraphCollector::AffinitiesMap affinities; - const auto ordered_ops = model->get_ordered_ops(); - for (const auto& node : ordered_ops) { - const auto name = node->get_friendly_name(); - OPENVINO_ASSERT(supported_ops.count(name)); - affinities[node] = supported_ops.at(name); + for (const auto& node : model->get_ordered_ops()) { + const auto it = affinity_by_name.find(node->get_friendly_name()); + ASSERT_TRUE(it != affinity_by_name.end()) << "Missing affinity for node '" << node->get_friendly_name() << "'"; + affinities[node] = it->second; } - // Collect subgraphs - SubgraphCollector subgraph_collector(model, affinities); - const auto& [actual_subgraphs, actual_mapping_info] = subgraph_collector.run(); + SubgraphCollector collector(model, affinities); + const auto result = collector.run(); + const auto& subgraphs = result.first; + // Locate which subgraph each bridge node ended up in by scanning each subgraph's submodel. + auto find_subgraph_containing = [&subgraphs](const std::string& node_name) -> std::optional { + for (size_t i = 0; i < subgraphs.size(); ++i) { + const auto submodel = create_submodel_from_collected_subgraph(subgraphs[i]); + for (const auto& op : submodel->get_ordered_ops()) { + if (op->get_friendly_name() == node_name) + return i; + } + } + return std::nullopt; + }; + const auto idx_x1 = find_subgraph_containing("x_bridge1"); + const auto idx_x2 = find_subgraph_containing("x_bridge2"); + ASSERT_TRUE(idx_x1.has_value()) << "x_bridge1 was not found in any resulting subgraph"; + ASSERT_TRUE(idx_x2.has_value()) << "x_bridge2 was not found in any resulting subgraph"; + EXPECT_EQ(*idx_x1, *idx_x2) << "Bridge subgraph was split: x_bridge1 ended up in subgraph " << *idx_x1 + << " but x_bridge2 ended up in subgraph " << *idx_x2 + << ". This indicates the SCC fallback wrongly classified the acyclic bridge as cyclic" + << " and promoted its internal edge."; +} - ASSERT_EQ(3, actual_subgraphs.size()); - std::vector> actual_submodels; - for (auto& actual_subgraph : actual_subgraphs) { - actual_submodels.push_back(std::make_shared(actual_subgraph._results, actual_subgraph._parameters)); +// Regression test for the SCC fallback when every promotable same-subgraph edge has a +// Constant producer. See create_shared_const_scc_only_const_promotable_model() for the +// topology. The earlier implementation skipped any candidate edge whose source was a graph +// input (Constant/Parameter), so when an SCC consisted entirely of nodes whose only same-sg +// inputs came from a shared Constant, find_promotable_internal_edge() returned nullopt and +// the SCC fallback fired "no internal edge to promote". This is the exact failure mode +// reproduced on yolo26s-seg with HETERO:GPU,CPU. The contract under test: run() converges +// (no assert), and merge round-trip succeeds. +TEST(SubgraphCollectorSharedConstSccTest, scc_with_only_constant_sourced_edges_converges) { + auto model = create_shared_const_scc_only_const_promotable_model(); + auto model_ref = model->clone(); + const std::map affinity_by_name = { + {"in", "MOCK.0"}, + {"c_shared", "MOCK.0"}, + {"A", "MOCK.0"}, + {"B", "MOCK.1"}, + {"C", "MOCK.0"}, + {"D", "MOCK.1"}, + {"E", "MOCK.0"}, + {"F", "MOCK.1"}, + {"res", "MOCK.1"}, + }; + SubgraphCollector::AffinitiesMap affinities; + for (const auto& node : model->get_ordered_ops()) { + const auto it = affinity_by_name.find(node->get_friendly_name()); + ASSERT_TRUE(it != affinity_by_name.end()) << "Missing affinity for node '" << node->get_friendly_name() << "'"; + affinities[node] = it->second; } - // Merge submodels into one model back - OV_ASSERT_NO_THROW( - ov::hetero::merge_submodels(actual_submodels, actual_mapping_info._submodels_input_to_prev_output)); - ASSERT_EQ(1, actual_submodels.size()); -} \ No newline at end of file + + SubgraphCollector collector(model, affinities); + // Must not assert "no internal edge to promote". + const auto& [subgraphs, mapping] = collector.run(); + ASSERT_FALSE(subgraphs.empty()); + + // Merge round-trip: gluing the submodels back together must reproduce the original model. + std::vector> submodels; + submodels.reserve(subgraphs.size()); + for (const auto& sg : subgraphs) + submodels.push_back(create_submodel_from_collected_subgraph(sg)); + OV_ASSERT_NO_THROW(ov::hetero::merge_submodels(submodels, mapping._submodels_input_to_prev_output)); + ASSERT_EQ(1u, submodels.size()); + const auto cmp_result = compare_functions(model_ref, submodels[0]); + EXPECT_TRUE(cmp_result.first) << cmp_result.second; +} diff --git a/src/plugins/intel_cpu/CMakeLists.txt b/src/plugins/intel_cpu/CMakeLists.txt index e5e8b2f4b638..2e533b0307eb 100644 --- a/src/plugins/intel_cpu/CMakeLists.txt +++ b/src/plugins/intel_cpu/CMakeLists.txt @@ -183,6 +183,10 @@ if(NOT X86_64) ${CMAKE_CURRENT_SOURCE_DIR}/src/transformations/tpp/x64/*) endif() +if(X86_64 OR AARCH64) + list(APPEND EXCLUDE_PATHS ${CMAKE_CURRENT_SOURCE_DIR}/src/transformations/snippets/common/shape_inference_fallback.cpp) +endif() + if(NOT (AARCH64 OR ARM)) list(APPEND EXCLUDE_PATHS ${CMAKE_CURRENT_SOURCE_DIR}/src/transformations/cpu_opset/arm/* ${CMAKE_CURRENT_SOURCE_DIR}/src/transformations/tpp/aarch64/* @@ -215,6 +219,15 @@ if (NOT ENABLE_SNIPPETS_LIBXSMM_TPP) ${CMAKE_CURRENT_SOURCE_DIR}/src/transformations/tpp/*) endif () +if(NOT ENABLE_CPU_DEBUG_CAPS) + list(APPEND EXCLUDE_PATHS + ${CMAKE_CURRENT_SOURCE_DIR}/src/utils/debug_capabilities.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/utils/memory_stats_dump.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/utils/node_dumper.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/utils/verbose.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/utils/debug_caps_config.cpp) +endif() + file(GLOB_RECURSE FILES_TO_REMOVE ${EXCLUDE_PATHS}) list(REMOVE_ITEM SOURCES ${FILES_TO_REMOVE}) list(REMOVE_ITEM HEADERS ${FILES_TO_REMOVE}) @@ -319,6 +332,23 @@ cross_compiled_file(${TARGET_NAME} NAME paged_attn_quantkv attn_quant_u8 attn_dequant_u8 attn_quant_by_token attn_quant_by_channel attn_quant_by_channel_u8 attn_dequant_by_channel_u8 NAMESPACE ov::Extensions::Cpu::XARCH ) +cross_compiled_file(${TARGET_NAME} + ARCH AVX512F AVX2 ANY + src/nodes/kernels/scaled_attn/mha_kv_cache_codec.cpp + API src/nodes/kernels/scaled_attn/mha_kv_cache_codec.hpp + NAME mha_kv_cache turboq_head_bytes + NAMESPACE ov::Extensions::Cpu::XARCH +) +cross_compiled_file(${TARGET_NAME} + ARCH AVX512F AVX2 ANY + src/nodes/kernels/scaled_attn/codecs/turboq_quantize.cpp + API src/nodes/kernels/scaled_attn/codecs/turboq_quantize.hpp + NAME turboq_quantize_head turboq_quantize + NAMESPACE ov::Extensions::Cpu::XARCH +) +## turboq_rotation.cpp holds scalar setup only (sign cache, env parsing); +## SIMD WHT butterfly is inline in turboq_rotation.hpp and gets ISA-specialized +## when included by cross-compiled translation units. cross_compiled_file(${TARGET_NAME} ARCH AVX512F ANY @@ -332,7 +362,15 @@ cross_compiled_file(${TARGET_NAME} ARCH AVX512F AVX2 ANY src/nodes/kernels/linear_attn/recurrent_linear_attn.cpp API src/nodes/kernels/linear_attn/recurrent_linear_attn.hpp - NAME recurrent_linear_attn + NAME recurrent_linear_attn recurrent_linear_attn_paged + NAMESPACE ov::Extensions::Cpu::XARCH +) + +cross_compiled_file(${TARGET_NAME} + ARCH AVX512F AVX2 ANY + src/nodes/kernels/paged_causal_conv1d.cpp + API src/nodes/kernels/paged_causal_conv1d.hpp + NAME paged_causal_conv1d_exec NAMESPACE ov::Extensions::Cpu::XARCH ) diff --git a/src/plugins/intel_cpu/README.md b/src/plugins/intel_cpu/README.md index d27014159ca0..a311f0378348 100644 --- a/src/plugins/intel_cpu/README.md +++ b/src/plugins/intel_cpu/README.md @@ -2,7 +2,7 @@ ## Key Contacts -For assistance regarding CPU, contact a member of [openvino-ie-cpu-maintainers](https://github.com/orgs/openvinotoolkit/teams/openvino-ie-cpu-maintainers) group. +For assistance regarding CPU, contact a member of openvino-ie-cpu-maintainers group. ## Components diff --git a/src/plugins/intel_cpu/docs/debug_capabilities/exec_graph_serialization.md b/src/plugins/intel_cpu/docs/debug_capabilities/exec_graph_serialization.md index 1139bb0e9370..e0490054fd91 100644 --- a/src/plugins/intel_cpu/docs/debug_capabilities/exec_graph_serialization.md +++ b/src/plugins/intel_cpu/docs/debug_capabilities/exec_graph_serialization.md @@ -2,8 +2,9 @@ Execution graph serialization is disabled by default and controlled by environment variable **OV_CPU_EXEC_GRAPH_PATH**: ```sh - OV_CPU_EXEC_GRAPH_PATH=
()(std::get(value)) ^ hash(value); - } - -public: - size_t operator()(Tuple value) const { - auto hv = hash(value); - return hv; - } -}; - -// create const object with internal cache with constructor-args as the key -// this helps reduces construction time overhead, and perfectly suitable -// for caching functor/callable. -template -std::shared_ptr make_cacheable(dnnl::engine eng, CArgs... cargs) { - std::shared_ptr sptr; - auto key = std::make_tuple(cargs...); - static std::unordered_map, tuple_hasher> cache; - static std::mutex mutex; - std::lock_guard guard(mutex); - auto it = cache.find(key); - if (it != cache.end()) { - auto& wptr = it->second; - sptr = wptr.lock(); - if (!sptr) { - sptr = std::make_shared(eng, cargs...); - wptr = sptr; - } - } else { - sptr = std::make_shared(eng, cargs...); - cache.emplace(std::make_pair(key, std::weak_ptr(sptr))); +inline dnnl::algorithm moe_activation_to_dnnl_algo(ov::op::internal::MOE::Activation_type act) { + switch (act) { + case ov::op::internal::MOE::Activation_type::GEGLU_TANH: + return dnnl::algorithm::eltwise_gelu_tanh; + case ov::op::internal::MOE::Activation_type::GEGLU_ERF: + return dnnl::algorithm::eltwise_gelu_erf; + case ov::op::internal::MOE::Activation_type::SWIGLU: + default: + return dnnl::algorithm::eltwise_swish; } - return sptr; } -struct onednn_linear { - std::shared_ptr mm; - dnnl::memory weight; - dnnl::memory scale; - dnnl::memory zp; - dnnl::matmul m_prim; - dnnl::memory::dim m_K; - dnnl::memory::dim m_N; - dnnl::memory::dim m_batch; - dnnl::memory::data_type m_a_type; - int bin_post_id; - - static onednn_linear create(dnnl::engine eng, - dnnl::memory::data_type act_dtype, - dnnl::memory::data_type weight_dtype, - int batch, - int ic, - int oc, - int ic_group_size, - onednn_matmul::type t, - dnnl::memory weight, // external weight - dnnl::memory scale, - dnnl::memory zp) { - OV_ITT_SCOPED_TASK(ov::intel_gpu::itt::domains::intel_gpu_plugin, openvino::itt::handle("onednn_linear::create()")); - bool has_zp = static_cast(zp); - auto mm = make_cacheable(eng, act_dtype, weight_dtype, batch, ic, oc, ic_group_size, t, has_zp); - onednn_linear linear; - linear.mm = mm; - linear.bin_post_id = mm->bin_post_id; - linear.m_prim = mm->m_prim; - linear.m_K = mm->m_K; - linear.m_N = mm->m_N; - linear.m_batch = batch; - linear.m_a_type = mm->m_a_type; - linear.weight = weight; - - if (scale) { - // https://uxlfoundation.github.io/oneDNN/page_weights_decompression_matmul_cpp.html - // Quantization Group size for scales. Must be divisible by 32. - linear.scale = scale; - if (zp) { - linear.zp = zp; - } - } - return linear; - } - - void forward(dnnl::stream& stream, int m, dnnl::memory src_mem, dnnl::memory dst_mem, dnnl::memory bin_mem) { - OV_ITT_SCOPED_TASK(ov::intel_gpu::itt::domains::intel_gpu_plugin, openvino::itt::handle("onednn_linear::forward()")); - dnnl::memory::dim M = m; - - if (!(m_batch == 0 || m_batch == M)) { - OPENVINO_THROW("onednn_linear::forward(): invalid batch size m_batch=", m_batch, " M=", M); - } - - std::unordered_map args; - args.insert({DNNL_ARG_SRC, src_mem}); - args.insert({DNNL_ARG_WEIGHTS, weight}); - // args.insert({DNNL_ARG_BIAS, bias_mem}); - args.insert({DNNL_ARG_DST, dst_mem}); - - if (scale) { - args.insert({DNNL_ARG_ATTR_SCALES | DNNL_ARG_WEIGHTS, scale}); - } - if (zp) { - args.insert({DNNL_ARG_ATTR_ZERO_POINTS | DNNL_ARG_WEIGHTS, zp}); - } - if (bin_mem) { - args.insert({DNNL_ARG_ATTR_MULTIPLE_POST_OP(bin_post_id) | DNNL_ARG_SRC_1, bin_mem}); - } - m_prim.execute(stream, args); - } -}; - -class MoE3GemmSwigluSoftMaxTopK : public KernelGenerator { -public: - MoE3GemmSwigluSoftMaxTopK() : KernelGenerator("moe_3gemm_swiglu_fuse", "softmax_topk") {} - -protected: - [[nodiscard]] JitConstants get_jit_constants(const RuntimeParams& params) const override { - auto jit = KernelGenerator::get_jit_constants(params); - auto desc = params.typed_desc(); - jit.make("SOFTMAX_TOPK_ENABLE", 1); - jit.make("TOP_K", desc->_config.top_k); - jit.make("VALUE_NUM", desc->_config.num_expert); - jit.make("MOE_DTYPE", params.get_input_layout(0).data_type == ov::element::f16 ? "half" : "float"); - jit.make("MOE_DTYPE_SIZE", params.get_input_layout(0).data_type == ov::element::f16 ? 2 : 4); - return jit; - } - - [[nodiscard]] Arguments get_arguments_desc(const RuntimeParams& params) const override { - Arguments args; - - return args; - } - - [[nodiscard]] DispatchDataFunc get_dispatch_data_func() const override { - return DispatchDataFunc{[](const RuntimeParams& params, KernelData& kd, ImplRuntimeParams* rt_params) {}}; - } -}; - -class MoE3GemmSwigluSigmoidBiasTopK : public KernelGenerator { -public: - MoE3GemmSwigluSigmoidBiasTopK() : KernelGenerator("moe_3gemm_swiglu_fuse", "sigmoid_bias_topk") {} - -protected: - [[nodiscard]] JitConstants get_jit_constants(const RuntimeParams& params) const override { - auto jit = KernelGenerator::get_jit_constants(params); - auto desc = params.typed_desc(); - jit.make("SIGMOID_BIAS_TOPK_ENABLE", 1); - jit.make("TOP_K", desc->_config.top_k); - jit.make("VALUE_NUM", desc->_config.num_expert); - jit.make("MOE_DTYPE", params.get_input_layout(0).data_type == ov::element::f16 ? "half" : "float"); - jit.make("MOE_DTYPE_SIZE", params.get_input_layout(0).data_type == ov::element::f16 ? 2 : 4); - return jit; - } - - [[nodiscard]] Arguments get_arguments_desc(const RuntimeParams& params) const override { - Arguments args; - - return args; - } - - [[nodiscard]] DispatchDataFunc get_dispatch_data_func() const override { - return DispatchDataFunc{[](const RuntimeParams& params, KernelData& kd, ImplRuntimeParams* rt_params) {}}; - } -}; - class MoE3GemmSwigluGather : public KernelGenerator { public: MoE3GemmSwigluGather() : KernelGenerator("moe_3gemm_swiglu_fuse", "gather") {} @@ -534,9 +196,13 @@ static auto calc_thread_count(RuntimeParams& params, const size_t vector_size, c } class MoE3GemmSwigluPrefillGather : public KernelGenerator { public: - MoE3GemmSwigluPrefillGather() : KernelGenerator("moe_gather_ref", "prefill_gather") {} + explicit MoE3GemmSwigluPrefillGather(bool use_grouped_gemm = false) + : KernelGenerator("moe_gather_ref", "prefill_gather"), + m_use_grouped_gemm(use_grouped_gemm) {} protected: + bool m_use_grouped_gemm; + [[nodiscard]] JitConstants get_jit_constants(const RuntimeParams& params) const override { auto jit = KernelGenerator::get_jit_constants(params); auto desc = params.typed_desc(); @@ -556,6 +222,8 @@ class MoE3GemmSwigluPrefillGather : public KernelGenerator { jit.make("INPUT1_TYPE", "int"); jit.make("OUTPUT_TYPE", "half"); jit.make("OPTIONAL_SHAPE_INFO_ARG", ""); + if (m_use_grouped_gemm) + jit.make("ONEDNN_GROUPED_GEMM_USED", 1); GPU_DEBUG_TRACE_DETAIL << "MoE3GemmSwigluPrefillGather::get_jit_constants(): hidden_size: " << hidden_size << ", block_size: " << block_size << ", local_threads_count: " << local_threads_count << ", batches_per_thread: " << batches_per_thread @@ -577,9 +245,13 @@ class MoE3GemmSwigluPrefillGather : public KernelGenerator { class MoE3GemmSwigluPrefillSwiglu : public KernelGenerator { public: - MoE3GemmSwigluPrefillSwiglu() : KernelGenerator("moe_3gemm_swiglu_fuse", "prefill_swiglu") {} + explicit MoE3GemmSwigluPrefillSwiglu(bool use_grouped_gemm = false) + : KernelGenerator("moe_3gemm_swiglu_fuse", "prefill_swiglu"), + m_use_grouped_gemm(use_grouped_gemm) {} protected: + bool m_use_grouped_gemm; + [[nodiscard]] JitConstants get_jit_constants(const RuntimeParams& params) const override { auto jit = KernelGenerator::get_jit_constants(params); auto desc = params.typed_desc(); @@ -590,6 +262,13 @@ class MoE3GemmSwigluPrefillSwiglu : public KernelGenerator { jit.make("SUBGROUP_SIZE", info.arch >= gpu_arch::xe2 ? 32 : 16); jit.make("INTERMEDIA_SIZE", desc->_config.inter_size); jit.make("MOE_DTYPE", "half"); + if (desc->_config.activation_type == ov::op::internal::MOE::Activation_type::GEGLU_TANH) { + jit.make("GATE_ACT_GELU_TANH", 1); + } else if (desc->_config.activation_type == ov::op::internal::MOE::Activation_type::GEGLU_ERF) { + jit.make("GATE_ACT_GELU_ERF", 1); + } + if (m_use_grouped_gemm) + jit.make("ONEDNN_GROUPED_GEMM_USED", 1); return jit; } @@ -606,9 +285,13 @@ class MoE3GemmSwigluPrefillSwiglu : public KernelGenerator { class MoE3GemmSwigluPrefillScatterReduce : public KernelGenerator { public: - MoE3GemmSwigluPrefillScatterReduce() : KernelGenerator("moe_scatter_reduction_opt", "moe_scatter_reduction_ref") {} + explicit MoE3GemmSwigluPrefillScatterReduce(bool use_grouped_gemm = false) + : KernelGenerator("moe_scatter_reduction_opt", "moe_scatter_reduction_ref"), + m_use_grouped_gemm(use_grouped_gemm) {} protected: + bool m_use_grouped_gemm; + [[nodiscard]] JitConstants get_jit_constants(const RuntimeParams& params) const override { auto jit = KernelGenerator::get_jit_constants(params); auto desc = params.typed_desc(); @@ -634,6 +317,8 @@ class MoE3GemmSwigluPrefillScatterReduce : public KernelGenerator { jit.make("INPUT5_TYPE", "int"); // tokens len for experts jit.make("INPUT6_TYPE", "int"); // expert id jit.make("OUTPUT_TYPE", "half"); // output + if (m_use_grouped_gemm) + jit.make("ONEDNN_GROUPED_GEMM_USED", 1); return jit; } @@ -691,6 +376,22 @@ static void add_common_consts(const RuntimeParams& params, JitConstants& jit) { GPU_DEBUG_TRACE_DETAIL << "[DEBUG] moe_3gemm_swiglu_opt: group_size=" << desc->_config.group_size << ", gate_up_group_size=" << gate_up_group_size << ", down_group_size=" << down_group_size << std::endl; + + // Validate GEMV kernel compatibility: ELEMS_PER_LANE = FAKE_GROUP_SIZE / SUBGROUP_SIZE must be >= 2. + // Smaller values have no kernel branch and would silently produce wrong results. + { + const size_t sg = (info.arch >= gpu_arch::xe2) ? 32u : 16u; + const size_t fake_gs = std::min(gate_up_group_size, size_t{128}); + OPENVINO_ASSERT(fake_gs >= 2 * sg, + "MoE GEMV kernel does not support group_size=", + gate_up_group_size, + " on this hardware (SUBGROUP_SIZE=", + sg, + "). Minimum supported group_size is ", + 2 * sg, + ". Use a larger quantization group size."); + } + jit.make("MAX_TOPK", desc->_config.top_k); jit.make("EXPERT_NUM", desc->_config.num_expert); jit.make("HIDDEN_SIZE", desc->_config.hidden_size); @@ -739,6 +440,11 @@ class MoE3GemmSwigluMLPGateUp : public KernelGenerator { auto desc = params.typed_desc(); add_common_consts(params, jit); jit.make("GATE_UP_ENABLE", 1); + if (desc->_config.activation_type == ov::op::internal::MOE::Activation_type::GEGLU_TANH) { + jit.make("GATE_ACT_GELU_TANH", 1); + } else if (desc->_config.activation_type == ov::op::internal::MOE::Activation_type::GEGLU_ERF) { + jit.make("GATE_ACT_GELU_ERF", 1); + } if (!_disable_shared_experts && desc->_config.num_shared_expert > 0 && params.input_layouts.size() > static_cast(MOE3GemmInputIndex::SHARED_GATE_WEIGHT)) { jit.make("SHARED_EXPERT_ENABLE", 1); @@ -834,13 +540,9 @@ dnnl::memory convert2dnnl(const memory::ptr& ptr, const std::vector& di return ptr->get_onednn_memory(dnnl::memory::desc(dnnl::memory::dims(dim), convert_data_type(ptr->get_layout().data_type), tag), offset); } -static bool use_micro_gemm_prefill; -static bool use_gpu_mask_gen_prefill; class moe_3gemm_swiglu_opt_impl : public PrimitiveImplOCL { public: DECLARE_OBJECT_TYPE_SERIALIZATION(ov::intel_gpu::ocl::MoE3GemmSwigluImpl) - Stage::Ptr softmax_topk = make_stage(); - Stage::Ptr sigmoid_bias_topk = make_stage(); Stage::Ptr gather = make_stage(); Stage::Ptr scatter = make_stage(); Stage::Ptr mlp_gate_up = make_stage(); @@ -855,6 +557,11 @@ class moe_3gemm_swiglu_opt_impl : public PrimitiveImplOCL { Stage::Ptr prefill_scatter_reduce = make_stage(); Stage::Ptr prefill_mask_gen = make_stage(); + // Grouped GEMM path: same OCL kernels but compiled with ONEDNN_GROUPED_GEMM_USED=1 + Stage::Ptr grouped_gemm_prefill_gather = make_stage(/*use_grouped_gemm=*/true); + Stage::Ptr grouped_gemm_prefill_swiglu = make_stage(/*use_grouped_gemm=*/true); + Stage::Ptr grouped_gemm_prefill_scatter_reduce = make_stage(/*use_grouped_gemm=*/true); + struct dnnl_weights { dnnl::memory weight; dnnl::memory scale; @@ -919,6 +626,7 @@ class moe_3gemm_swiglu_opt_impl : public PrimitiveImplOCL { int _shared_intermediate_size; int _gate_up_group_size; int _down_group_size; + ov::op::internal::MOE::Activation_type _activation_type = ov::op::internal::MOE::Activation_type::SWIGLU; bool _has_shared_expert = false; // Shared expert primitives @@ -927,6 +635,12 @@ class moe_3gemm_swiglu_opt_impl : public PrimitiveImplOCL { std::shared_ptr _shared_down_proj; std::shared_ptr _shared_gate_gate_proj; // The scalar gate for shared expert + // Instance-specific flags (not static to avoid race conditions) + bool use_micro_gemm_prefill = false; + bool use_gpu_mask_gen_prefill = false; + bool use_grouped_gemm_prefill = false; + size_t batched_gemv_threshold = 32; // token_num <= threshold uses batched GEMV path + moe_3gemm_swiglu_opt_impl() : PrimitiveImplOCL(moe_3gemm_swiglu_opt::get_type_info_static()) {} moe_3gemm_swiglu_opt_impl(const program_node& node, const RuntimeParams& params) : moe_3gemm_swiglu_opt_impl() { if (m_rt_params == nullptr) { @@ -969,15 +683,32 @@ class moe_3gemm_swiglu_opt_impl : public PrimitiveImplOCL { use_micro_gemm_prefill = false; } - // Don't change the order of stages - auto routing_type = node.as().get_primitive()->_config.routing_type; - if (routing_type == ov::intel_gpu::op::MOECompressed::RoutingType::SOFTMAX) { - add_stage(softmax_topk, params); - } else if (routing_type == ov::intel_gpu::op::MOECompressed::RoutingType::SIGMOID_BIAS) { - add_stage(sigmoid_bias_topk, params); + // grouped_gemm path: single OneDNN grouped matmul per GEMM layer (all experts at once). + // micro_gemm takes priority; grouped_gemm falls back to onednn loop by default. + auto use_grouped_gemm_prefill_str = std::getenv("MOE_USE_GROUPED_GEMM_PREFILL"); + if (use_grouped_gemm_prefill_str) { + GPU_DEBUG_TRACE_DETAIL << "MOE_USE_GROUPED_GEMM_PREFILL = " << use_grouped_gemm_prefill_str << std::endl; + use_grouped_gemm_prefill = std::stoi(use_grouped_gemm_prefill_str) != 0; } else { - OPENVINO_THROW("Unsupported routing type for moe_3gemm_swiglu_opt_impl: ", static_cast(routing_type)); + use_grouped_gemm_prefill = true; + } + // grouped_gemm supersedes micro_gemm + if (use_grouped_gemm_prefill) { + use_micro_gemm_prefill = false; + } + + GPU_DEBUG_TRACE_DETAIL << "[DEBUG] moe_3gemm_swiglu_opt_impl(): use_grouped_gemm_prefill=" << use_grouped_gemm_prefill << std::endl; + + auto batched_gemv_threshold_str = std::getenv("MOE_BATCHED_GEMV_THRESHOLD"); + if (batched_gemv_threshold_str) { + batched_gemv_threshold = std::stoul(batched_gemv_threshold_str); + if (batched_gemv_threshold <= 0) { + batched_gemv_threshold = 1; + } + GPU_DEBUG_TRACE_DETAIL << "MOE_BATCHED_GEMV_THRESHOLD = " << batched_gemv_threshold << std::endl; } + + // Don't change the order of stages add_stage(gather, params); add_stage(scatter, params); add_stage(mlp_gate_up, params); @@ -992,6 +723,11 @@ class moe_3gemm_swiglu_opt_impl : public PrimitiveImplOCL { add_stage(micro_gemm_down, params); add_stage(prefill_scatter_reduce, params); } + if (use_grouped_gemm_prefill) { + add_stage(grouped_gemm_prefill_gather, params); + add_stage(grouped_gemm_prefill_swiglu, params); + add_stage(grouped_gemm_prefill_scatter_reduce, params); + } } void init(const std::shared_ptr& cur_moe) { @@ -999,6 +735,7 @@ class moe_3gemm_swiglu_opt_impl : public PrimitiveImplOCL { _intermediate_size = static_cast(cur_moe->_config.inter_size); _gate_up_group_size = static_cast(cur_moe->_config.group_size); _down_group_size = static_cast(cur_moe->_config.group_size); + _activation_type = cur_moe->_config.activation_type; if (cur_moe->_config.group_size == std::numeric_limits::max()) { _gate_up_group_size = static_cast(cur_moe->_config.hidden_size); @@ -1029,19 +766,55 @@ class moe_3gemm_swiglu_opt_impl : public PrimitiveImplOCL { }; _dnnl_weights.resize(cur_moe->_config.num_expert); + // Per-GEMM ic_group_size from scale shape; config.group_size can't represent gate/up vs down differing. + const auto ic_group_size_from_scale = [](size_t ic, const cldnn::memory::ptr& scale_mem) { + const auto& scale_shape = scale_mem->get_layout().get_shape(); + const size_t num_groups = (scale_shape.size() >= 3) ? scale_shape[2] : 1; + return (num_groups <= 1) ? static_cast(ic) : static_cast(ic / num_groups); + }; for (size_t j = 0; j < cur_moe->_config.num_expert; j++) { auto& dnnl_weights = _dnnl_weights[j]; dnnl_weights.resize(3); dnnl_weights[0].ic = _hidden_size; - dnnl_weights[0].ic_group_size = _gate_up_group_size; + dnnl_weights[0].ic_group_size = ic_group_size_from_scale(_hidden_size, moe_fusion_wei_addr.scale[0]); dnnl_weights[0].oc = _intermediate_size; dnnl_weights[1].ic = _hidden_size; - dnnl_weights[1].ic_group_size = _gate_up_group_size; + dnnl_weights[1].ic_group_size = ic_group_size_from_scale(_hidden_size, moe_fusion_wei_addr.scale[1]); dnnl_weights[1].oc = _intermediate_size; dnnl_weights[2].ic = _intermediate_size; - dnnl_weights[2].ic_group_size = _down_group_size; + dnnl_weights[2].ic_group_size = ic_group_size_from_scale(_intermediate_size, moe_fusion_wei_addr.scale[2]); dnnl_weights[2].oc = _hidden_size; for (int i = 0; i < 3; i++) { + // Cross-check ic/ic_group_size against scale shape (drift caused u8 inf bug). + { + const auto& sshape = moe_fusion_wei_addr.scale[i]->get_layout().get_shape(); + const size_t scale_num_groups = (sshape.size() >= 3) ? sshape[2] : 1; + OPENVINO_ASSERT(dnnl_weights[i].ic_group_size > 0, "moe_3gemm GEMM ", i, " ic_group_size must be > 0"); + OPENVINO_ASSERT(dnnl_weights[i].ic % dnnl_weights[i].ic_group_size == 0, + "moe_3gemm GEMM ", + i, + " ic=", + dnnl_weights[i].ic, + " not divisible by ic_group_size=", + dnnl_weights[i].ic_group_size); + const auto expected_groups = dnnl_weights[i].ic / dnnl_weights[i].ic_group_size; + OPENVINO_ASSERT(static_cast(expected_groups) == scale_num_groups, + "moe_3gemm GEMM ", + i, + " ic_group_size=", + dnnl_weights[i].ic_group_size, + " (=> ", + expected_groups, + " groups) disagrees with scale num_groups=", + scale_num_groups, + " (scale shape=", + sshape, + ")"); + if (cur_moe->_config.has_zp && moe_fusion_wei_addr.zp[i]) { + const auto& zshape = moe_fusion_wei_addr.zp[i]->get_layout().get_shape(); + OPENVINO_ASSERT(zshape == sshape, "moe_3gemm GEMM ", i, " scale shape ", sshape, " does not match zp shape ", zshape); + } + } // weight shape: [ic, oc], type: u4/i8 int64_t wei_offset = j * get_bytes_count(dnnl_weights[i].ic * dnnl_weights[i].oc, moe_fusion_wei_addr.weight[i]->get_layout()); dnnl_weights[i].weight = @@ -1119,10 +892,11 @@ class moe_3gemm_swiglu_opt_impl : public PrimitiveImplOCL { _hidden_size, _shared_intermediate_size, _gate_up_group_size, - t::with_silu_bin_mul, + t::with_gate_act_bin_mul, gate_w, gate_s, - gate_z)); + gate_z, + moe_activation_to_dnnl_algo(_activation_type))); // 3. Scalar Gate (Sigmoid) // It is very small weight with shape of [Hidden, 1], and not need to keep compressed, so KeepMOE3GemmConstPrecision will not keep its precision and @@ -1191,8 +965,20 @@ class moe_3gemm_swiglu_opt_impl : public PrimitiveImplOCL { _shared_down_proj->forward(stream, batch, gate_mem_dnnl, output_dnnl, scalar_gate_dnnl); } + void save(BinaryOutputBuffer& ob) const override { + PrimitiveImplOCL::save(ob); + ob << use_micro_gemm_prefill; + ob << use_gpu_mask_gen_prefill; + ob << use_grouped_gemm_prefill; + } + void load(BinaryInputBuffer& ib) override { PrimitiveImplOCL::load(ib); + // Read execution-path flags before init() so any future init() logic + // that depends on them sees the deserialized (not default) values. + ib >> use_micro_gemm_prefill; + ib >> use_gpu_mask_gen_prefill; + ib >> use_grouped_gemm_prefill; const kernel_impl_params* impl_params = reinterpret_cast(ib.getKernelImplParams()); init(impl_params->typed_desc()); } @@ -1204,6 +990,11 @@ class moe_3gemm_swiglu_opt_impl : public PrimitiveImplOCL { cur_moe->_intermediate_size = _intermediate_size; cur_moe->_gate_up_group_size = _gate_up_group_size; cur_moe->_down_group_size = _down_group_size; + cur_moe->use_micro_gemm_prefill = use_micro_gemm_prefill; + cur_moe->use_gpu_mask_gen_prefill = use_gpu_mask_gen_prefill; + cur_moe->use_grouped_gemm_prefill = use_grouped_gemm_prefill; + cur_moe->batched_gemv_threshold = batched_gemv_threshold; + cur_moe->_activation_type = _activation_type; return cur_moe; } @@ -1219,43 +1010,53 @@ class moe_3gemm_swiglu_opt_impl : public PrimitiveImplOCL { bool has_shared_expert = params.input_layouts.size() > static_cast(MOE3GemmInputIndex::SHARED_GATE_WEIGHT); std::vector internal_buffers; - // softmax+topk - layout layout_topk_id(ov::Shape{token_num, max_topk}, data_types::u32, cldnn::format::bfyx); - layout layout_topk_weights(ov::Shape{token_num, max_topk + (has_shared_expert ? 1 : 0)}, data_type, cldnn::format::bfyx); - internal_buffers.emplace_back(layout_topk_id, true); // 0: topk_id - internal_buffers.emplace_back(layout_topk_weights, true); // 1: topk_weights - // To support micro_gemm, prefill need to allocate max_topk * token_num for input data of micro_gemm auto max_batch = has_shared_expert ? (max_topk + 1) * token_num : max_topk * token_num; layout layout_gateup_out(ov::Shape{max_batch, static_cast(config.inter_size)}, data_type, cldnn::format::bfyx); layout layout_down_out(ov::Shape{max_batch, static_cast(config.hidden_size)}, data_type, cldnn::format::bfyx); - internal_buffers.emplace_back(layout_gateup_out, false); // 2: up output (GPU-only) - internal_buffers.emplace_back(layout_down_out, false); // 3: down output (GPU-only) + internal_buffers.emplace_back(layout_gateup_out, false); // 0: up output (GPU-only) + internal_buffers.emplace_back(layout_down_out, false); // 1: down output (GPU-only) // onednn: scratch.x, scratch.routing_weights = gather(x, ...) // scratch.up = up(scratch.x) // scratch.gate = gate(scratch.x) * scratch.up // scratch.y = down(scratch.gate) * routing_weights - internal_buffers.emplace_back(layout_down_out, false); // 4: up/gate input, scratch.x has same layout with down output (GPU-only) + internal_buffers.emplace_back(layout_down_out, false); // 2: up/gate input, scratch.x has same layout with down output (GPU-only) layout routing_layout(ov::Shape{max_batch}, data_type, cldnn::format::bfyx); - internal_buffers.emplace_back(routing_layout, true); // 5: routing_weights - internal_buffers.emplace_back(layout_gateup_out, false); // 6: gate output, scratch.gate has same layout with up (GPU-only) + internal_buffers.emplace_back(routing_layout, true); // 3: routing_weights + internal_buffers.emplace_back(layout_gateup_out, false); // 4: gate output, scratch.gate has same layout with up (GPU-only) // expert masks for gpu layout index_layout(ov::Shape{expert_num, token_num}, ov::element::i32, cldnn::format::bfyx); - internal_buffers.emplace_back(index_layout, true); // 7: expert_mask_batch - internal_buffers.emplace_back(index_layout, true); // 8: expert_mask_topk + internal_buffers.emplace_back(index_layout, true); // 5: expert_mask_batch + internal_buffers.emplace_back(index_layout, true); // 6: expert_mask_topk - GPU_DEBUG_TRACE_DETAIL << "[DEBUG] get_internal_buffer_descs(): use_micro_gemm_prefill=" << use_micro_gemm_prefill << std::endl; + GPU_DEBUG_TRACE_DETAIL << "[DEBUG] get_internal_buffer_descs(): use_micro_gemm_prefill=" << use_micro_gemm_prefill + << ", use_grouped_gemm_prefill=" << use_grouped_gemm_prefill << std::endl; // for micro_gemm if (use_micro_gemm_prefill && token_num > 1) { layout layout_micro_gemm(ov::Shape{expert_num, token_num}, ov::element::i32, cldnn::format::bfyx); - internal_buffers.emplace_back(layout_micro_gemm, true); // 9: experts_ids for each activated expert - internal_buffers.emplace_back(layout_micro_gemm, true); // 10: token start offset idx (input gather tokens) for each activated expert - internal_buffers.emplace_back(layout_micro_gemm, true); // 11: token len (input gather tokens) for each activated expert + internal_buffers.emplace_back(layout_micro_gemm, true); // 7: experts_ids for each activated expert + internal_buffers.emplace_back(layout_micro_gemm, true); // 8: token start offset idx (input gather tokens) for each activated expert + internal_buffers.emplace_back(layout_micro_gemm, true); // 9: token len (input gather tokens) for each activated expert layout layout_token_idx(ov::Shape{token_num * max_topk}, ov::element::i32, cldnn::format::bfyx); - internal_buffers.emplace_back(layout_token_idx, true); // 12: token idx per expert + internal_buffers.emplace_back(layout_token_idx, true); // 10: token idx per expert layout layout_actual_used_expert_num(ov::Shape{1}, ov::element::i32, cldnn::format::bfyx); - internal_buffers.emplace_back(layout_actual_used_expert_num, false); // 13: actual_used_expert_num + internal_buffers.emplace_back(layout_actual_used_expert_num, false); // 11: actual_used_expert_num + } + // for grouped_gemm: shared metadata buffers (7-11) + int32_t expert-row-offsets (12) + if (use_grouped_gemm_prefill && token_num > 1) { + layout layout_meta(ov::Shape{expert_num, token_num}, ov::element::i32, cldnn::format::bfyx); + internal_buffers.emplace_back(layout_meta, true); // 7: activated expert ids + internal_buffers.emplace_back(layout_meta, true); // 8: token start offset per activated expert + internal_buffers.emplace_back(layout_meta, true); // 9: token len per activated expert + layout layout_token_idx(ov::Shape{token_num * max_topk}, ov::element::i32, cldnn::format::bfyx); + internal_buffers.emplace_back(layout_token_idx, true); // 10: flat token idx per expert (for gather) + layout layout_actual_used_expert_num(ov::Shape{1}, ov::element::i32, cldnn::format::bfyx); + internal_buffers.emplace_back(layout_actual_used_expert_num, false); // 11: actual_used_expert_num + // int32_t end-offsets per expert for OneDNN grouped memory descriptor + // offsets[e] = sum(n_0..n_e), the exclusive end index of expert e in the flat buffer + layout layout_grouped_offsets(ov::Shape{expert_num}, ov::element::i32, cldnn::format::bfyx); + internal_buffers.emplace_back(layout_grouped_offsets, true); // 12: grouped end-offsets } return internal_buffers; } @@ -1263,13 +1064,15 @@ class moe_3gemm_swiglu_opt_impl : public PrimitiveImplOCL { void prepare_internal_buffers(typed_primitive_inst& instance, scratch_buffers& scratch, size_t token_num) { const auto& intermediates_memories = instance.get_intermediates_memories(); auto& engine = instance.get_network().get_engine(); - scratch.topk_id = intermediates_memories[MOE_INTERNAL_BUFFER_TOPK_IDX]; - scratch.topk_weights = intermediates_memories[MOE_INTERNAL_BUFFER_TOPK_WEIGHTS]; + // topk_id / topk_weights are read from inputs (computed by MoERouterFused). + scratch.topk_weights = instance.input_memory_ptr(static_cast(MOE3GemmInputIndex::TOPK_WEIGHTS)); + scratch.topk_id = instance.input_memory_ptr(static_cast(MOE3GemmInputIndex::TOPK_INDICES)); scratch.up = intermediates_memories[MOE_INTERNAL_BUFFER_UP_OUTPUT]; scratch.y = intermediates_memories[MOE_INTERNAL_BUFFER_DOWN_OUTPUT]; + // Routing weights scratch buffer (used in prefill paths and reused as shared_gate_vals in batched GEMV) + scratch.routing_weights = intermediates_memories[MOE_INTERNAL_BUFFER_ROUTING_WEIGHTS]; if (token_num > 1) { scratch.x = intermediates_memories[MOE_INTERNAL_BUFFER_GATE_UP_INPUT]; - scratch.routing_weights = intermediates_memories[MOE_INTERNAL_BUFFER_ROUTING_WEIGHTS]; scratch.gate = intermediates_memories[MOE_INTERNAL_BUFFER_GATE_OUTPUT]; const auto& config = instance.get_typed_desc()->_config; int expert_num = static_cast(config.num_expert); @@ -1298,6 +1101,15 @@ class moe_3gemm_swiglu_opt_impl : public PrimitiveImplOCL { scratch.moe_fusion_wei_addr.scale[2] = instance.input_memory_ptr(static_cast(MOE3GemmInputIndex::SCALE_2)); scratch.moe_fusion_wei_addr.zp[2] = instance.input_memory_ptr(static_cast(MOE3GemmInputIndex::ZP_2)); + // For symmetric quantization (has_zp=false), ZP inputs are element::dynamic placeholders + // with zero-count layout. Use scale memory as a dummy to avoid null pointer issues. + const auto& config = instance.get_typed_desc()->_config; + if (!config.has_zp) { + scratch.moe_fusion_wei_addr.zp[0] = scratch.moe_fusion_wei_addr.scale[0]; + scratch.moe_fusion_wei_addr.zp[1] = scratch.moe_fusion_wei_addr.scale[1]; + scratch.moe_fusion_wei_addr.zp[2] = scratch.moe_fusion_wei_addr.scale[2]; + } + // shared expert size_t dep_count = instance.dependencies().size(); if (dep_count >= static_cast(MOE3GemmInputIndex::SHARED_GATE_GATE_WEIGHT) + 1) { @@ -1318,10 +1130,17 @@ class moe_3gemm_swiglu_opt_impl : public PrimitiveImplOCL { // Scalar Gate - f16 scratch.moe_fusion_wei_addr.shared_weight[3] = instance.input_memory_ptr(static_cast(MOE3GemmInputIndex::SHARED_GATE_GATE_WEIGHT)); + + // For symmetric quantization, shared expert ZPs are also element::dynamic placeholders + if (!config.has_zp) { + scratch.moe_fusion_wei_addr.shared_zp[0] = scratch.moe_fusion_wei_addr.shared_scale[0]; + scratch.moe_fusion_wei_addr.shared_zp[1] = scratch.moe_fusion_wei_addr.shared_scale[1]; + scratch.moe_fusion_wei_addr.shared_zp[2] = scratch.moe_fusion_wei_addr.shared_scale[2]; + } } } - void get_expert_mask_from_gpu(const MOE3GemmFusedCompressed::Config& config, memory::ptr mem, stream& stream, expert_mask_cpu& expert_mask) { + void get_expert_mask_from_gpu(const MOECompressed::Config& config, memory::ptr mem, stream& stream, expert_mask_cpu& expert_mask) { // shape: [token_num, topk] auto layout = mem->get_layout(); const auto& shape = layout.get_shape(); @@ -1451,9 +1270,13 @@ class moe_3gemm_swiglu_opt_impl : public PrimitiveImplOCL { return std::make_tuple(mem, layout); } - cldnn::event::ptr exec_single_token(const std::vector& events, + // Batched GEMV path: handles token_num >= 1 with optimized GEMV kernels. + // Each workgroup processes one (token, expert) pair. Avoids gather/scatter/CPU-sync overhead of prefill paths. + // Supports shared expert: EXPERTS_PER_TOKEN = MAX_TOPK + 1 when shared expert is enabled. + cldnn::event::ptr exec_batched_gemv(const std::vector& events, typed_primitive_inst& instance, - scratch_buffers& scratch) { + scratch_buffers& scratch, + size_t token_num) { auto cur_moe = instance.get_typed_desc(); int max_topk = static_cast(cur_moe->_config.top_k); @@ -1501,14 +1324,18 @@ class moe_3gemm_swiglu_opt_impl : public PrimitiveImplOCL { scratch.moe_fusion_wei_addr.shared_scale[1], scratch.moe_fusion_wei_addr.shared_zp[1], scratch.moe_fusion_wei_addr.shared_weight[3], - routing_mem_ptr}; + scratch.routing_weights}; // reused as shared_gate_out [token_num] extra_args_down = {scratch.moe_fusion_wei_addr.shared_weight[2], scratch.moe_fusion_wei_addr.shared_scale[2], scratch.moe_fusion_wei_addr.shared_zp[2]}; } + + GPU_DEBUG_TRACE_DETAIL << "\nexec_batched_gemv(): token_num=" << token_num << ", max_topk=" << max_topk << ", has_shared=" << _has_shared_expert + << std::endl; + { - // scratch.up = up(x) * silu(gate(x)) + // scratch.up = up(x) * silu(gate(x)) for all (token, expert) pairs std::vector args_gate_up = {batch_mem_ptr, mlp_gate_wei_mem, mlp_gate_scale_mem, mlp_gate_zp_mem, mlp_up_wei_mem, mlp_up_scale_mem, mlp_up_zp_mem}; if (_has_shared_expert) @@ -1520,30 +1347,32 @@ class moe_3gemm_swiglu_opt_impl : public PrimitiveImplOCL { *stage_gate_up, args_gate_up, {scratch.up}, - {compute_experts, subgroup_size, static_cast(_intermediate_size / N_BLOCK)}, + {token_num * compute_experts, subgroup_size, static_cast(_intermediate_size / N_BLOCK)}, {1, subgroup_size, SUBGROUP_NUM}); - // scratch.y = down(scratch.up) * weight[expert_no] + // scratch.y = down(scratch.up) * routing_weight for all (token, expert) pairs std::vector args_down = {batch_mem_ptr, mlp_down_wei_mem, mlp_down_scale_mem, mlp_down_zp_mem}; if (_has_shared_expert) args_down.insert(args_down.end(), extra_args_down.begin(), extra_args_down.end()); args_down.push_back(scratch.up); - args_down.push_back(routing_mem_ptr); + args_down.push_back(routing_mem_ptr); // compact topk_weights [token_num * MAX_TOPK] + if (_has_shared_expert) + args_down.push_back(scratch.routing_weights); // shared_gate_in [token_num] ret_event = execute_stage({ret_event}, instance, *stage_down, args_down, {scratch.y}, - {compute_experts, subgroup_size, static_cast(_hidden_size / N_BLOCK)}, + {token_num * compute_experts, subgroup_size, static_cast(_hidden_size / N_BLOCK)}, {1, subgroup_size, SUBGROUP_NUM}); - // final = sum(scratch.y) + // Per-token reduction: final[t] = sum(scratch.y[t * REDUCE_COUNT .. (t+1) * REDUCE_COUNT - 1]) ret = execute_stage({ret_event}, instance, *stage_reduce, {scratch.y}, {final_hidden_states_mem_ptr}, - {static_cast(1), static_cast(_hidden_size)}, + {token_num, static_cast(_hidden_size)}, {1, std::min(max_work_group_size, size_t{1024})}, instance.needs_completion_event()); } @@ -1842,6 +1671,25 @@ class moe_3gemm_swiglu_opt_impl : public PrimitiveImplOCL { using lru_cache_hash = LruCache, std::shared_ptr, PairHash>; lru_cache_hash _kernels = lru_cache_hash(1024); + + // --- grouped GEMM kernel cache (one primitive set per total-token count) --- + struct grouped_onednn_kernel { + dnnl::matmul gate_prim; + dnnl::matmul up_prim; + dnnl::matmul down_prim; + dnnl::matmul::primitive_desc gate_pd; + dnnl::matmul::primitive_desc up_pd; + dnnl::matmul::primitive_desc down_pd; + dnnl::memory::desc gate_scale_md; + dnnl::memory::desc up_scale_md; + dnnl::memory::desc down_scale_md; + dnnl::memory::desc gate_zp_md; + dnnl::memory::desc up_zp_md; + dnnl::memory::desc down_zp_md; + bool has_zp = false; + }; + using grouped_kernel_lru = LruCache>; + grouped_kernel_lru _grouped_kernels{128}; onednn_kernel& get_kernel(int n_token, int expert_no, typed_primitive_inst& instance) { auto key = std::make_pair(n_token, expert_no); if (_kernels.has(key)) { @@ -1854,6 +1702,9 @@ class moe_3gemm_swiglu_opt_impl : public PrimitiveImplOCL { auto hidden_states_layout_dt = convert_data_type(instance.input_memory_ptr(static_cast(MOE3GemmInputIndex::HIDDEN_STATES))->get_layout().data_type); + auto cur_moe = instance.get_typed_desc(); + const auto gate_activation_algo = moe_activation_to_dnnl_algo(cur_moe->_config.activation_type); + auto& dnnl_weights = _dnnl_weights[expert_no]; auto kernel = std::make_shared(); @@ -1866,10 +1717,11 @@ class moe_3gemm_swiglu_opt_impl : public PrimitiveImplOCL { dnnl_weights[0].ic, dnnl_weights[0].oc, dnnl_weights[0].ic_group_size, - onednn_matmul::type::with_silu_bin_mul, + onednn_matmul::type::with_gate_act_bin_mul, dnnl_weights[0].weight, dnnl_weights[0].scale, - dnnl_weights[0].zp); + dnnl_weights[0].zp, + gate_activation_algo); // up auto up_weight_layout_dt = convert_data_type(instance.input_memory_ptr(static_cast(MOE3GemmInputIndex::WEIGHT_1))->get_layout().data_type); @@ -1902,6 +1754,100 @@ class moe_3gemm_swiglu_opt_impl : public PrimitiveImplOCL { return *_kernels.get(key); } + // Build (and cache) three grouped dnnl::matmul primitives for gate/up/down. + // Cache key is total_tokens only — the per-request max_tokens_per_expert + // dispatch hint is passed as a runtime argument (DNNL_ARG_HINT_MAX_GROUP_SIZE) + // at execute() time, so no recompilation is needed when it changes. + grouped_onednn_kernel& get_grouped_kernel(int total_tokens, typed_primitive_inst& instance) { + auto key = total_tokens; + if (_grouped_kernels.has(key)) { + return *_grouped_kernels.get(key); + } + + auto cur_moe = instance.get_typed_desc(); + const auto& config = cur_moe->_config; + auto& engine = instance.get_network().get_engine(); + auto& onednn_engine = engine.get_onednn_engine(); + + int num_experts = static_cast(config.num_expert); + auto a_dt = convert_data_type(instance.input_memory_ptr(static_cast(MOE3GemmInputIndex::HIDDEN_STATES))->get_layout().data_type); + auto gw_dt = convert_data_type(instance.input_memory_ptr(static_cast(MOE3GemmInputIndex::WEIGHT_0))->get_layout().data_type); + auto uw_dt = convert_data_type(instance.input_memory_ptr(static_cast(MOE3GemmInputIndex::WEIGHT_1))->get_layout().data_type); + auto dw_dt = convert_data_type(instance.input_memory_ptr(static_cast(MOE3GemmInputIndex::WEIGHT_2))->get_layout().data_type); + + // Use the model config to determine ZP presence (symmetric vs asymmetric quantization) + bool has_zp = config.has_zp; + + int K_gu = _hidden_size; // K for gate / up + int N_gu = _intermediate_size; // N for gate / up + int K_d = _intermediate_size; // K for down + int N_d = _hidden_size; // N for down + + // Helper: create one grouped matmul prim-desc [total_tokens, K]*W[E,K,N]->[total_tokens,N] + // Weights layout in memory is [E, N, K] (stored transposed), expressed as acb over dims {E,K,N}. + auto make_pd = [&](int K, int N, int group_size, dnnl::memory::data_type w_dt) { + dnnl::primitive_attr attr; + attr.set_fpmath_mode(dnnl::fpmath_mode::f16, true); + + bool has_k_groups = (group_size < K); + if (has_k_groups) { + // per-expert(0) x per-K-group(1) x per-N-channel(2) + attr.set_scales(DNNL_ARG_WEIGHTS, (1 << 0) | (1 << 1) | (1 << 2), {group_size, 1}, dnnl::memory::data_type::f16); + if (has_zp) { + attr.set_zero_points(DNNL_ARG_WEIGHTS, (1 << 0) | (1 << 1) | (1 << 2), {group_size, 1}, w_dt); + } + } else { + // per-expert(0) x per-N-channel(2), no K-grouping + attr.set_scales(DNNL_ARG_WEIGHTS, (1 << 0) | (1 << 2), {}, dnnl::memory::data_type::f16); + if (has_zp) { + attr.set_zero_points(DNNL_ARG_WEIGHTS, (1 << 0) | (1 << 2), {}, w_dt); + } + } + + // Grouped src/dst: tokens are grouped by expert along axis-0 + auto src_md = dnnl::memory::desc::grouped(dnnl::memory::dims{total_tokens, K}, a_dt, 0, num_experts, dnnl::memory::data_type::s32); + auto dst_md = dnnl::memory::desc::grouped(dnnl::memory::dims{total_tokens, N}, a_dt, 0, num_experts, dnnl::memory::data_type::s32); + // Weight: logical [E, K, N], physical layout acb -> stored as [E, N, K] + auto w_md = dnnl::memory::desc(dnnl::memory::dims{num_experts, K, N}, w_dt, dnnl::memory::format_tag::acb); + + return dnnl::matmul::primitive_desc(onednn_engine, src_md, w_md, dst_md, attr); + }; + + // Helper: create scale/ZP memory descriptor for grouped weights + auto make_quant_md = [&](int E, int K, int group_size, int N, dnnl::memory::data_type dt) { + int num_k_groups = K / group_size; + if (num_k_groups > 1) { + return dnnl::memory::desc({E, num_k_groups, N}, dt, dnnl::memory::format_tag::abc); + } else { + return dnnl::memory::desc({E, N}, dt, dnnl::memory::format_tag::ab); + } + }; + + auto gk = std::make_shared(); + gk->has_zp = has_zp; + + gk->gate_pd = make_pd(K_gu, N_gu, _gate_up_group_size, gw_dt); + gk->gate_prim = dnnl::matmul(gk->gate_pd); + gk->gate_scale_md = make_quant_md(num_experts, K_gu, _gate_up_group_size, N_gu, dnnl::memory::data_type::f16); + if (has_zp) + gk->gate_zp_md = make_quant_md(num_experts, K_gu, _gate_up_group_size, N_gu, gw_dt); + + gk->up_pd = make_pd(K_gu, N_gu, _gate_up_group_size, uw_dt); + gk->up_prim = dnnl::matmul(gk->up_pd); + gk->up_scale_md = gk->gate_scale_md; + if (has_zp) + gk->up_zp_md = gk->gate_zp_md; + + gk->down_pd = make_pd(K_d, N_d, _down_group_size, dw_dt); + gk->down_prim = dnnl::matmul(gk->down_pd); + gk->down_scale_md = make_quant_md(num_experts, K_d, _down_group_size, N_d, dnnl::memory::data_type::f16); + if (has_zp) + gk->down_zp_md = make_quant_md(num_experts, K_d, _down_group_size, N_d, dw_dt); + + _grouped_kernels.add(key, gk); + return *_grouped_kernels.get(key); + } + // inputs 0 is hidden_states, inputs 1 is router_logits[num_tokens, NUM_EXPERTS=128] // extra step Softmax_TopK is fused to give topk-id & router_weights // @@ -2016,6 +1962,249 @@ class moe_3gemm_swiglu_opt_impl : public PrimitiveImplOCL { return result_event; } + // Third prefill path: OneDNN grouped GEMM (one matmul call per GEMM layer, all experts together). + // This avoids the per-expert loop of exec_prefill_onednn while keeping full weight-format + // compatibility (quantized or fp16 weights). + // + // gather_by_expert(hidden_states, topk_id) -> scratch.x [total, hidden] + // grouped_matmul(scratch.x, W_gate) -> scratch.gate [total, inter] + // grouped_matmul(scratch.x, W_up) -> scratch.up [total, inter] + // silu(scratch.gate) * scratch.up -> scratch.gate [total, inter] + // grouped_matmul(scratch.gate, W_down) -> scratch.y [total, hidden] + // scatter_reduce(scratch.y, topk_id, topk_weights) -> output [token_num, hidden] + // + // Note: "total" = token_num * max_topk, sorted by expert assignment. + // + cldnn::event::ptr exec_prefill_grouped_gemm(const std::vector& events, + cldnn::stream& stream, + typed_primitive_inst& instance, + scratch_buffers& scratch) { + OV_ITT_SCOPED_TASK(ov::intel_gpu::itt::domains::intel_gpu_plugin, openvino::itt::handle("moe_3gemm_swiglu_opt_impl::exec_prefill_grouped_gemm")); + + auto cur_moe = instance.get_typed_desc(); + const auto& config = cur_moe->_config; + auto& dnn_stream = stream.get_onednn_stream(); + + auto [hidden_states_mem_ptr, hidden_states_layout] = get_input_info(instance, static_cast(MOE3GemmInputIndex::HIDDEN_STATES)); + auto token_num = get_seq_len(hidden_states_layout); + auto final_hidden_states_mem_ptr = instance.output_memory_ptr(0); + auto batch_mem_ptr = scratch.topk_id; + auto routing_mem_ptr = scratch.topk_weights; + const auto& intermediates_memories = instance.get_intermediates_memories(); + + int num_total_experts = static_cast(config.num_expert); + int max_topk = static_cast(config.top_k); + int num_actually_used_experts = 0; + + // ---------------------------------------------------------------- + // Step 1: CPU mask generation (topk_id already flushed by caller) + // ---------------------------------------------------------------- + cldnn::event::ptr ret_event = events.empty() ? nullptr : events[0]; + expert_mask_cpu expert_mask; + get_expert_mask_from_gpu(config, batch_mem_ptr, stream, expert_mask); + + // Flat list of source token indices per expert – input for prefill_gather + std::vector tokens_per_expert_cpu(static_cast(token_num) * max_topk, -1); + // Compact per-activated-expert metadata reused by scatter_reduce + std::vector tokens_lens_per_expert_cpu(num_total_experts, 0); + std::vector experts_id_cpu(num_total_experts, -1); + // int32_t cumulative end-offsets per expert for OneDNN grouped GEMM + // offsets[e] = sum(n_0..n_e) = exclusive end of expert e in the flat buffer. + // This is the s32 format expected by dnnl::memory::desc::grouped(). + std::vector grouped_offsets_cpu(num_total_experts, 0); + + { + int tokens_iter = 0; + int experts_iter = 0; + int32_t running_offset = 0; + for (int e = 0; e < num_total_experts; e++) { + auto n = static_cast(expert_mask.batch[e].size()); + running_offset += n; + grouped_offsets_cpu[e] = running_offset; // exclusive end of expert e + if (n > 0) { + experts_id_cpu[experts_iter] = e; + tokens_lens_per_expert_cpu[experts_iter] = n; + ++experts_iter; + ++num_actually_used_experts; + for (auto t : expert_mask.batch[e]) + tokens_per_expert_cpu[tokens_iter++] = t; + } + } + } + int total_gathered_tokens = static_cast(token_num) * max_topk; + + // Compute actual max tokens assigned to any single expert. + int max_tokens_per_expert = 0; + if (num_actually_used_experts > 0) { + max_tokens_per_expert = *std::max_element(tokens_lens_per_expert_cpu.begin(), tokens_lens_per_expert_cpu.begin() + num_actually_used_experts); + } + + GPU_DEBUG_TRACE_DETAIL << "\nexec_prefill_grouped_gemm: token_num=" << token_num << ", total_gathered_tokens=" << total_gathered_tokens + << ", max_tokens_per_expert=" << max_tokens_per_expert << ", num_actually_used_experts=" << num_actually_used_experts + << std::endl; + + // Upload scratch metadata for the scatter_reduce and gather kernels + intermediates_memories[MOE_INTERNAL_BUFFER_TOKEN_IDX_PER_EXPERT] + ->copy_from(stream, tokens_per_expert_cpu.data(), 0, 0, tokens_per_expert_cpu.size() * sizeof(int32_t), true); + // When ONEDNN_GROUPED_GEMM_USED, the scatter_reduce kernel reads: + // exp_offset_start = expert_id == 0 ? 0 : experts_start_offset[expert_id - 1] + // So experts_start_offset[k] must equal the start index of expert k+1 in the flat buffer + // = exclusive end of expert k = grouped_offsets_cpu[k]. + { + std::vector expert_start_offsets_per_id(static_cast(num_total_experts - 1)); + for (int e = 0; e < num_total_experts - 1; ++e) + expert_start_offsets_per_id[e] = grouped_offsets_cpu[e]; // end[e] == start[e+1] + intermediates_memories[MOE_INTERNAL_BUFFER_TOKEN_START_OFFSET_PER_EXPERT] + ->copy_from(stream, expert_start_offsets_per_id.data(), 0, 0, expert_start_offsets_per_id.size() * sizeof(int32_t), true); + } + intermediates_memories[MOE_INTERNAL_BUFFER_ACTIVATED_EXPERT_IDS] + ->copy_from(stream, experts_id_cpu.data(), 0, 0, static_cast(num_actually_used_experts) * sizeof(int32_t), true); + intermediates_memories[MOE_INTERNAL_BUFFER_TOKEN_LEN_PER_ACTIVATED_EXPERT] + ->copy_from(stream, tokens_lens_per_expert_cpu.data(), 0, 0, static_cast(num_actually_used_experts) * sizeof(int32_t), true); + intermediates_memories[MOE_INTERNAL_BUFFER_ACTUAL_USED_EXPERT_NUM]->copy_from(stream, &num_actually_used_experts, 0, 0, sizeof(int32_t), true); + // int32_t end-offsets for the OneDNN grouped descriptor + intermediates_memories[MOE_INTERNAL_BUFFER_GROUPED_OFFSETS] + ->copy_from(stream, grouped_offsets_cpu.data(), 0, 0, grouped_offsets_cpu.size() * sizeof(int32_t), true); + + // ---------------------------------------------------------------- + // Step 2: GPU gather – reorder input tokens sorted by expert + // ---------------------------------------------------------------- + { + auto hidden_size = _hidden_size; + auto block_size = get_vec_size(*instance.get_impl_params()); + auto [local_threads_count, batches_per_thread, unaligned_elements] = + calc_thread_count(const_cast(*instance.get_impl_params()), block_size, hidden_size); + + ret_event = execute_stage({ret_event}, + instance, + *grouped_gemm_prefill_gather, + {instance.input_memory_ptr(static_cast(MOE3GemmInputIndex::HIDDEN_STATES)), + intermediates_memories[MOE_INTERNAL_BUFFER_TOKEN_IDX_PER_EXPERT]}, + {scratch.x}, + {static_cast(total_gathered_tokens) * local_threads_count, 1, 1}, + {local_threads_count, 1, 1}); + } + + // ---------------------------------------------------------------- + // Steps 3-5: OneDNN grouped GEMM – gate, up, SiLU, down + // ---------------------------------------------------------------- + // SAFETY CHECK: Verify grouped_offsets buffer exists before accessing + if (intermediates_memories.size() <= MOE_INTERNAL_BUFFER_GROUPED_OFFSETS) { + OPENVINO_THROW("[MOE_3GEMM_GROUPED_BUG] Grouped GEMM path requires buffer ", + MOE_INTERNAL_BUFFER_GROUPED_OFFSETS, + " (GROUPED_OFFSETS) but only ", + intermediates_memories.size(), + " buffers allocated. ", + "This indicates a mismatch between buffer allocation and execution path. ", + "use_grouped_gemm_prefill=", + use_grouped_gemm_prefill); + } + auto& gk = get_grouped_kernel(total_gathered_tokens, instance); + auto row_offsets = intermediates_memories[MOE_INTERNAL_BUFFER_GROUPED_OFFSETS]; + + // Runtime dispatch hint: actual max tokens assigned to any single expert. + // Passed as DNNL_ARG_HINT_MAX_GROUP_SIZE to each grouped matmul execute(), + // allowing the kernel to reduce per-expert workgroup dispatch without + // recompiling the primitive. + auto hint_md = dnnl::memory::desc::host_scalar(dnnl::memory::data_type::s32); + dnnl::memory hint_mem(hint_md, static_cast(max_tokens_per_expert)); + + // gate GEMM: [total, hidden] * W_gate[E,hidden,inter] -> [total, inter] + { + auto src_mem = scratch.x->get_onednn_grouped_memory(gk.gate_pd.src_desc(), *row_offsets); + auto dst_mem = scratch.gate->get_onednn_grouped_memory(gk.gate_pd.dst_desc(), *row_offsets); + auto w_mem = scratch.moe_fusion_wei_addr.weight[0]->get_onednn_memory(gk.gate_pd.weights_desc()); + auto scale_mem = scratch.moe_fusion_wei_addr.scale[0]->get_onednn_memory(gk.gate_scale_md); + + std::unordered_map args{{DNNL_ARG_SRC, src_mem}, + {DNNL_ARG_WEIGHTS, w_mem}, + {DNNL_ARG_DST, dst_mem}, + {DNNL_ARG_ATTR_SCALES | DNNL_ARG_WEIGHTS, scale_mem}, + {DNNL_ARG_HINT_MAX_GROUP_SIZE, hint_mem}}; + if (gk.has_zp) { + args.insert({DNNL_ARG_ATTR_ZERO_POINTS | DNNL_ARG_WEIGHTS, scratch.moe_fusion_wei_addr.zp[0]->get_onednn_memory(gk.gate_zp_md)}); + } + gk.gate_prim.execute(dnn_stream, args); + } + + // up GEMM: [total, hidden] * W_up[E,hidden,inter] -> [total, inter] + { + auto src_mem = scratch.x->get_onednn_grouped_memory(gk.up_pd.src_desc(), *row_offsets); + auto dst_mem = scratch.up->get_onednn_grouped_memory(gk.up_pd.dst_desc(), *row_offsets); + auto w_mem = scratch.moe_fusion_wei_addr.weight[1]->get_onednn_memory(gk.up_pd.weights_desc()); + auto scale_mem = scratch.moe_fusion_wei_addr.scale[1]->get_onednn_memory(gk.up_scale_md); + + std::unordered_map args{{DNNL_ARG_SRC, src_mem}, + {DNNL_ARG_WEIGHTS, w_mem}, + {DNNL_ARG_DST, dst_mem}, + {DNNL_ARG_ATTR_SCALES | DNNL_ARG_WEIGHTS, scale_mem}, + {DNNL_ARG_HINT_MAX_GROUP_SIZE, hint_mem}}; + if (gk.has_zp) { + args.insert({DNNL_ARG_ATTR_ZERO_POINTS | DNNL_ARG_WEIGHTS, scratch.moe_fusion_wei_addr.zp[1]->get_onednn_memory(gk.up_zp_md)}); + } + gk.up_prim.execute(dnn_stream, args); + } + + // Step 4: SiLU(gate) * up -> scratch.gate (OCL prefill_swiglu kernel, compiled with ONEDNN_GROUPED_GEMM_USED) + // gate and up GEMMs are submitted to the same OCL queue as the OCL kernels; + // passing ret_event (from gather) as dependency ensures ordering within the queue. + { + const size_t subgroup_size = instance.get_impl_params()->get_device_info().arch >= gpu_arch::xe2 ? 32 : 16; + + ret_event = execute_stage({ret_event}, + instance, + *grouped_gemm_prefill_swiglu, + {intermediates_memories[MOE_INTERNAL_BUFFER_UP_OUTPUT], intermediates_memories[MOE_INTERNAL_BUFFER_GATE_OUTPUT]}, + {intermediates_memories[MOE_INTERNAL_BUFFER_GATE_OUTPUT]}, + {static_cast(_intermediate_size), static_cast(total_gathered_tokens), 1}, + {subgroup_size, 1, 1}); + } + + // down GEMM: [total, inter] * W_down[E,inter,hidden] -> [total, hidden] + { + auto src_mem = scratch.gate->get_onednn_grouped_memory(gk.down_pd.src_desc(), *row_offsets); + auto dst_mem = scratch.y->get_onednn_grouped_memory(gk.down_pd.dst_desc(), *row_offsets); + auto w_mem = scratch.moe_fusion_wei_addr.weight[2]->get_onednn_memory(gk.down_pd.weights_desc()); + auto scale_mem = scratch.moe_fusion_wei_addr.scale[2]->get_onednn_memory(gk.down_scale_md); + + std::unordered_map args{{DNNL_ARG_SRC, src_mem}, + {DNNL_ARG_WEIGHTS, w_mem}, + {DNNL_ARG_DST, dst_mem}, + {DNNL_ARG_ATTR_SCALES | DNNL_ARG_WEIGHTS, scale_mem}, + {DNNL_ARG_HINT_MAX_GROUP_SIZE, hint_mem}}; + if (gk.has_zp) { + args.insert({DNNL_ARG_ATTR_ZERO_POINTS | DNNL_ARG_WEIGHTS, scratch.moe_fusion_wei_addr.zp[2]->get_onednn_memory(gk.down_zp_md)}); + } + gk.down_prim.execute(dnn_stream, args); + } + + // ---------------------------------------------------------------- + // Step 6: scatter_reduce – weighted accumulate into output + // ---------------------------------------------------------------- + { + auto [local_threads_count, batches_per_thread, _unused] = + calc_thread_count(const_cast(*instance.get_impl_params()), 4, _hidden_size); + + ret_event = execute_stage({ret_event}, + instance, + *grouped_gemm_prefill_scatter_reduce, + {intermediates_memories[MOE_INTERNAL_BUFFER_DOWN_OUTPUT], + batch_mem_ptr, + routing_mem_ptr, + intermediates_memories[MOE_INTERNAL_BUFFER_TOKEN_IDX_PER_EXPERT], + intermediates_memories[MOE_INTERNAL_BUFFER_TOKEN_START_OFFSET_PER_EXPERT], + intermediates_memories[MOE_INTERNAL_BUFFER_TOKEN_LEN_PER_ACTIVATED_EXPERT], + intermediates_memories[MOE_INTERNAL_BUFFER_ACTIVATED_EXPERT_IDS], + intermediates_memories[MOE_INTERNAL_BUFFER_ACTUAL_USED_EXPERT_NUM]}, + {final_hidden_states_mem_ptr}, + {static_cast(token_num) * local_threads_count, 1, 1}, + {local_threads_count, 1, 1}, + true /*needs_completion_event*/); + } + + return ret_event; + } + cldnn::event::ptr execute(const std::vector& events, cldnn::primitive_inst& ins) override { OV_ITT_SCOPED_TASK(ov::intel_gpu::itt::domains::intel_gpu_plugin, openvino::itt::handle("moe_3gemm_swiglu_opt_impl::execute")); auto& instance = reinterpret_cast&>(ins); @@ -2042,71 +2231,63 @@ class moe_3gemm_swiglu_opt_impl : public PrimitiveImplOCL { prepare_internal_buffers(instance, scratch, token_num); kernel_dump_info.clear_entries(); - // routing: softmax+topk or sigmoid+bias+topk - auto lws_size = config.num_expert; - cldnn::event::ptr topk_event; - if (config.routing_type == ov::intel_gpu::op::MOECompressed::RoutingType::SOFTMAX) { - topk_event = execute_stage(events, - instance, - *softmax_topk, - {instance.input_memory_ptr(static_cast(MOE3GemmInputIndex::ROUTING_WEIGHTS))}, - {scratch.topk_id, scratch.topk_weights}, - {token_num, lws_size}, - {1, lws_size}, - instance.needs_completion_event()); - } else if (config.routing_type == ov::intel_gpu::op::MOECompressed::RoutingType::SIGMOID_BIAS) { - topk_event = execute_stage(events, - instance, - *sigmoid_bias_topk, - {instance.input_memory_ptr(static_cast(MOE3GemmInputIndex::ROUTING_WEIGHTS)), - instance.input_memory_ptr(static_cast(MOE3GemmInputIndex::ROUTING_BIAS)), - instance.input_memory_ptr(static_cast(MOE3GemmInputIndex::ROUTING_EPS))}, - {scratch.topk_id, scratch.topk_weights}, - {token_num, lws_size}, - {1, lws_size}, - instance.needs_completion_event()); - } else { - OPENVINO_THROW("Unsupported routing type ", static_cast(config.routing_type)); - } - - // Single token is a special case, we don't need to do gather/scatter, - // and we can apply optimal kernels against memory bound to improve performance. - if (token_num == 1) { - return exec_single_token({topk_event}, instance, scratch); + // Batched GEMV: for small token counts (including single token, MTP/speculative decoding), + // use optimized GEMV kernels with batch dimension. Avoids gather/scatter overhead. + if (token_num <= batched_gemv_threshold) { + return exec_batched_gemv(events, instance, scratch, token_num); } auto final_hidden_states_mem_ptr = instance.output_memory_ptr(0); - // onednn path will accumulate to the output - if (!use_micro_gemm_prefill) { + // only the per-expert onednn loop path accumulates into the output via index_add, + // so it needs the buffer pre-zeroed; micro_gemm and grouped_gemm use scatter_reduce + // which writes atomically and does not require pre-zeroing. + if (!use_micro_gemm_prefill && !use_grouped_gemm_prefill) { final_hidden_states_mem_ptr->fill(stream, false); } - const bool use_gpu_mask_gen = use_gpu_mask_gen_prefill; + // GPU mask gen is only supported for micro_gemm; both grouped_gemm and onednn loop + // always use CPU mask gen and therefore always need topk to be ready first. + const bool use_gpu_mask_gen = use_micro_gemm_prefill && use_gpu_mask_gen_prefill; if (!use_gpu_mask_gen) { - // Wait for topk is ready - topk_event->wait(); + // Wait for input events (topk produced upstream by MoERouterFused) + for (auto& ev : events) { + if (ev) { + ev->wait(); + } + } } GPU_DEBUG_TRACE_DETAIL << "\nMoE3GemmFusedCompressed exec(): token_num=" << token_num << ", max_topk=" << static_cast(config.top_k) - << ", use_micro_gemm_prefill=" << use_micro_gemm_prefill << std::endl; + << ", use_micro_gemm_prefill=" << use_micro_gemm_prefill << ", use_grouped_gemm_prefill=" << use_grouped_gemm_prefill + << std::endl; update_rt_params(instance); if (use_micro_gemm_prefill) { - ret_env = exec_prefill_micro_gemm({topk_event}, instance, scratch, use_gpu_mask_gen); + ret_env = exec_prefill_micro_gemm(events, instance, scratch, use_gpu_mask_gen); + } else if (use_grouped_gemm_prefill) { + ret_env = exec_prefill_grouped_gemm(events, stream, instance, scratch); } else { - ret_env = exec_prefill_onednn({topk_event}, stream, instance, scratch); + ret_env = exec_prefill_onednn(events, stream, instance, scratch); } if (_has_shared_expert) { auto& engine = instance.get_network().get_engine(); init_shared_primitives(engine, scratch.moe_fusion_wei_addr, static_cast(token_num)); - // execute_shared_expert will read the output of moe_gemm_down, so it should be after ret_env is ready, but it doesn't need to wait for ret_env to - // be ready, since execute_shared_expert will be serialized with the following kernels by onednn stream, just make sure ret_env is submitted before - // execute_shared_expert. - // if (ret_env) - // ret_env->wait(); + // Shared expert's down_proj uses sum post-op (output += result), so the + // scatter_reduce must have written the MoE output first. Both are on the + // same in-order OCL queue, so submission order guarantees execution order. + // No explicit wait() is needed — the in-order queue serializes all GPU work, + // and any subsequent primitive on the same queue will see the completed output. + if (use_grouped_gemm_prefill && ret_env) { + // ensure grouped GEMM fully completes before executing shared expert, which relies on its output being ready; + // For grouped_gemm path, scatter_reduce (OCL) is preceded by multiple OCL <--> OneDNN + // transitions inside exec_prefill_grouped_gemm. The implicit ordering between + // the OCL queue and OneDNN's stream cannot be relied upon across this many + // back-and-forth submissions, so the shared expert's down_proj sum post-op + // (which reads+writes final_hidden_states) can race with scatter_reduce. + // Force the scatter_reduce write to be visible before submitting the shared expert. + ret_env->wait(); + } execute_shared_expert(stream.get_onednn_stream(), static_cast(token_num), hidden_states_mem_ptr, final_hidden_states_mem_ptr, scratch); } - // Wait for the final event to be ready - // ret_env->wait(); return ret_env; } }; diff --git a/src/plugins/intel_gpu/src/graph/impls/ocl_v2/moe/moe_3gemm_swiglu_opt.hpp b/src/plugins/intel_gpu/src/graph/impls/ocl_v2/moe/moe_3gemm_swiglu_opt.hpp index 6cb7f5884d4c..e527bdb4855b 100644 --- a/src/plugins/intel_gpu/src/graph/impls/ocl_v2/moe/moe_3gemm_swiglu_opt.hpp +++ b/src/plugins/intel_gpu/src/graph/impls/ocl_v2/moe/moe_3gemm_swiglu_opt.hpp @@ -9,6 +9,7 @@ #include "intel_gpu/primitives/activation.hpp" #include "intel_gpu/primitives/eltwise.hpp" +#include "intel_gpu/primitives/moe_3gemm_fused_compressed.hpp" #include "moe_3gemm_base.hpp" #include "program_node.h" #include "registry/implementation_manager.hpp" @@ -61,16 +62,19 @@ struct moe_3gemm_swiglu_opt : public ImplementationManager { return false; } - // Only support zp: u4, i4, u8, i8 - static constexpr std::array supported_zp_type = { - ov::element::u4, // asym-quant type - ov::element::i4, // sym-quant type - ov::element::u8, // asym-quant type - ov::element::i8, // sym-quant type - }; - const auto& zp_layout = node.get_input_layout(static_cast(MOE3GemmInputIndex::ZP_0)); - if (!one_of(zp_layout.data_type, supported_zp_type)) { - return false; + // Only support zp: u4, i4, u8, i8 (skip check for symmetric quantization where ZP is element::dynamic placeholder) + const auto& config = std::static_pointer_cast(node.get_primitive())->_config; + if (config.has_zp) { + static constexpr std::array supported_zp_type = { + ov::element::u4, // asym-quant type + ov::element::i4, // sym-quant type + ov::element::u8, // asym-quant type + ov::element::i8, // sym-quant type + }; + const auto& zp_layout = node.get_input_layout(static_cast(MOE3GemmInputIndex::ZP_0)); + if (!one_of(zp_layout.data_type, supported_zp_type)) { + return false; + } } return true; diff --git a/src/plugins/intel_gpu/src/graph/impls/ocl_v2/moe/moe_gemm_gen_opt.hpp b/src/plugins/intel_gpu/src/graph/impls/ocl_v2/moe/moe_gemm_gen_opt.hpp index 5ab850b2f2b5..4be2f61de419 100644 --- a/src/plugins/intel_gpu/src/graph/impls/ocl_v2/moe/moe_gemm_gen_opt.hpp +++ b/src/plugins/intel_gpu/src/graph/impls/ocl_v2/moe/moe_gemm_gen_opt.hpp @@ -53,7 +53,7 @@ class MoEGemmOptGeneratorBase : public MoEGemmBase { auto k = (weight_shape.size() == 4) ? weight_shape[2] * weight_shape[3] : weight_shape[2]; auto scale_group_dim = params.input_layouts[moe_cfg.weight_scale_idx].get_shape().size() - 2; auto num_scale_groups = (weight_shape.size() == 4) ? params.input_layouts[moe_cfg.weight_scale_idx].get_shape()[scale_group_dim] : 1; - moe_cfg.weight_group_size = k / num_scale_groups; + moe_cfg.weight_group_size = static_cast(k / num_scale_groups); if (static_cast(params.input_layouts.size()) > moe_cfg.weight_zp_idx) { moe_cfg.is_weight_symmetric_quantized = false; } else { diff --git a/src/plugins/intel_gpu/src/graph/impls/ocl_v2/moe/moe_router_fused_opt.cpp b/src/plugins/intel_gpu/src/graph/impls/ocl_v2/moe/moe_router_fused_opt.cpp new file mode 100644 index 000000000000..8cc77f357a32 --- /dev/null +++ b/src/plugins/intel_gpu/src/graph/impls/ocl_v2/moe/moe_router_fused_opt.cpp @@ -0,0 +1,153 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "moe_router_fused_opt.hpp" + +#include "../common_utils/dispatch_utils.hpp" +#include "../common_utils/jitter.hpp" +#include "../primitive_ocl_base.hpp" +#include "../utils/kernel_generator.hpp" +#include "intel_gpu/primitives/moe_router_fused.hpp" + +namespace ov::intel_gpu::ocl { +namespace { + +class MoeRouterSoftMaxTopK : public KernelGenerator { +public: + MoeRouterSoftMaxTopK() : KernelGenerator("moe_router_fused", "softmax_topk") {} + +protected: + [[nodiscard]] JitConstants get_jit_constants(const RuntimeParams& params) const override { + auto jit = KernelGenerator::get_jit_constants(params); + auto desc = params.typed_desc(); + jit.make("SOFTMAX_TOPK_ENABLE", 1); + jit.make("TOP_K", desc->_config.top_k); + jit.make("VALUE_NUM", desc->_config.num_expert); + jit.make("MOE_DTYPE", params.get_input_layout(0).data_type == ov::element::f16 ? "half" : "float"); + jit.make("MOE_DTYPE_SIZE", params.get_input_layout(0).data_type == ov::element::f16 ? 2 : 4); + return jit; + } + + [[nodiscard]] Arguments get_arguments_desc(const RuntimeParams& params) const override { + Arguments args; + return args; + } + + [[nodiscard]] DispatchDataFunc get_dispatch_data_func() const override { + return DispatchDataFunc{[](const RuntimeParams& params, KernelData& kd, ImplRuntimeParams* rt_params) {}}; + } +}; + +class MoeRouterSigmoidBiasTopK : public KernelGenerator { +public: + MoeRouterSigmoidBiasTopK() : KernelGenerator("moe_router_fused", "sigmoid_bias_topk") {} + +protected: + [[nodiscard]] JitConstants get_jit_constants(const RuntimeParams& params) const override { + auto jit = KernelGenerator::get_jit_constants(params); + auto desc = params.typed_desc(); + jit.make("SIGMOID_BIAS_TOPK_ENABLE", 1); + jit.make("TOP_K", desc->_config.top_k); + jit.make("VALUE_NUM", desc->_config.num_expert); + jit.make("MOE_DTYPE", params.get_input_layout(0).data_type == ov::element::f16 ? "half" : "float"); + jit.make("MOE_DTYPE_SIZE", params.get_input_layout(0).data_type == ov::element::f16 ? 2 : 4); + return jit; + } + + [[nodiscard]] Arguments get_arguments_desc(const RuntimeParams& params) const override { + Arguments args; + return args; + } + + [[nodiscard]] DispatchDataFunc get_dispatch_data_func() const override { + return DispatchDataFunc{[](const RuntimeParams& params, KernelData& kd, ImplRuntimeParams* rt_params) {}}; + } +}; + +class MoeRouterFusedImpl : public PrimitiveImplOCL { +public: + DECLARE_OBJECT_TYPE_SERIALIZATION(ov::intel_gpu::ocl::MoeRouterFusedImpl) + + Stage::Ptr softmax_topk = make_stage(); + Stage::Ptr sigmoid_bias_topk = make_stage(); + + MoeRouterFusedImpl() : PrimitiveImplOCL(MoeRouterFusedOpt::get_type_info_static()) {} + MoeRouterFusedImpl(const program_node& node, const RuntimeParams& params) : MoeRouterFusedImpl() { + auto desc = params.typed_desc(); + if (desc->_config.routing_type == MoERouterFused::RoutingType::SOFTMAX) { + add_stage(softmax_topk, params); + } else { + OPENVINO_ASSERT(desc->_config.routing_type == MoERouterFused::RoutingType::SIGMOID_BIAS, "Unsupported routing type"); + add_stage(sigmoid_bias_topk, params); + } + } + + [[nodiscard]] std::unique_ptr clone() const override { + return make_deep_copy(this); + } + + cldnn::event::ptr execute(const std::vector& events, cldnn::primitive_inst& instance) override { + kernel_dump_info.clear_entries(); + + cldnn::stream& stream = instance.get_network().get_stream(); + auto desc = instance.get_typed_desc(); + const auto& config = desc->_config; + + // Determine token count from input layout + auto input_mem = instance.input_memory_ptr(0); + auto input_layout = instance.dependencies()[0].first->get_impl_params()->get_output_layout(instance.dependencies()[0].second); + auto input_shape = input_layout.get_shape(); + size_t token_num = input_shape[0]; + if (input_shape.size() >= 3) + token_num = input_shape[0] * input_shape[1]; + size_t lws_size = config.num_expert; + + // Select routing stage and build inputs + Stage* routing_stage = nullptr; + std::vector inputs; + if (config.routing_type == MoERouterFused::RoutingType::SOFTMAX) { + routing_stage = softmax_topk.get(); + inputs = {input_mem}; + } else { + routing_stage = sigmoid_bias_topk.get(); + inputs = {input_mem, instance.input_memory_ptr(1), instance.input_memory_ptr(2)}; + } + + // Kernel signature: (input, output_index, output_weights) + // MoERouterFused outputs: 0 = topk_weights, 1 = topk_indices + auto topk_weights_mem = instance.output_memory_ptr(0); + auto topk_indices_mem = instance.output_memory_ptr(1); + + // Build kernel arguments manually (same as execute_stage in moe_3gemm) + cldnn::kernel_arguments_data args; + cldnn::kernel_arguments_desc kargs_desc; + + for (uint32_t i = 0; i < inputs.size(); i++) { + kargs_desc.arguments.push_back({ArgumentDescriptor::Types::INPUT, i}); + args.inputs.push_back(inputs[i]); + } + // Kernel outputs: first = indices (u32), second = weights (f16/f32) + kargs_desc.arguments.push_back({ArgumentDescriptor::Types::OUTPUT, 0}); + args.outputs.push_back(topk_indices_mem); + kargs_desc.arguments.push_back({ArgumentDescriptor::Types::OUTPUT, 1}); + args.outputs.push_back(topk_weights_mem); + + stream.set_arguments(*routing_stage->kernel, kargs_desc, args); + kargs_desc.workGroups.global = {token_num, lws_size}; + kargs_desc.workGroups.local = {1, lws_size}; + + kernel_dump_info.add_entry_point(routing_stage->kernel->get_id()); + + return stream.enqueue_kernel(*routing_stage->kernel, kargs_desc, {}, events, instance.needs_completion_event()); + } +}; + +} // namespace + +std::unique_ptr MoeRouterFusedOpt::create_impl(const program_node& node, const RuntimeParams& params) const { + assert(node.is_type()); + return std::make_unique(node, params); +} + +} // namespace ov::intel_gpu::ocl diff --git a/src/plugins/intel_gpu/src/graph/impls/ocl_v2/moe/moe_router_fused_opt.hpp b/src/plugins/intel_gpu/src/graph/impls/ocl_v2/moe/moe_router_fused_opt.hpp new file mode 100644 index 000000000000..fc529765d6e7 --- /dev/null +++ b/src/plugins/intel_gpu/src/graph/impls/ocl_v2/moe/moe_router_fused_opt.hpp @@ -0,0 +1,34 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#pragma once + +#include +#include + +#include "intel_gpu/primitives/moe_router_fused.hpp" +#include "program_node.h" +#include "registry/implementation_manager.hpp" + +using namespace cldnn; // TODO: Remove once namespaces are aligned +namespace ov::intel_gpu::ocl { + +struct MoeRouterFusedOpt : public ImplementationManager { + OV_GPU_PRIMITIVE_IMPL("ocl::moe::moe_router_fused_opt") + explicit MoeRouterFusedOpt(shape_types shape_type, ValidateFunc vf = nullptr) : ImplementationManager(impl_types::ocl, shape_type, std::move(vf)) {} + [[nodiscard]] std::unique_ptr create_impl(const program_node& node, const RuntimeParams& params) const override; + [[nodiscard]] bool validate_impl(const program_node& node) const override { + static constexpr std::array supported_types = { + ov::element::f16, + }; + + const auto& in_layout = node.get_input_layout(0); + if (!one_of(in_layout.data_type, supported_types)) + return false; + + return true; + } +}; + +} // namespace ov::intel_gpu::ocl diff --git a/src/plugins/intel_gpu/src/graph/impls/ocl_v2/moe_3gemm_swiglu_fuse.cl b/src/plugins/intel_gpu/src/graph/impls/ocl_v2/moe_3gemm_swiglu_fuse.cl index 980dc17c77d3..41be4f3652f3 100644 --- a/src/plugins/intel_gpu/src/graph/impls/ocl_v2/moe_3gemm_swiglu_fuse.cl +++ b/src/plugins/intel_gpu/src/graph/impls/ocl_v2/moe_3gemm_swiglu_fuse.cl @@ -2,165 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 // -#if SOFTMAX_TOPK_ENABLE - -KERNEL(softmax_topk)( - const __global MOE_DTYPE* input, // [input_batch, sort_in_num] - __global uint* output_index, // [input_batch, TOP_K] - __global MOE_DTYPE* output // [input_batch, TOP_K] -) { - // gws [batch, sort_in_num] - const uint batch = (uint)get_global_id(0); - const uint sort_index = (uint)get_global_id(1); - const uint sort_cnt = (uint)get_global_size(1); - - input += batch * sort_cnt + sort_index; - - uint sort_position = 0; - - __local MOE_DTYPE local_input[VALUE_NUM]; - __local MOE_DTYPE local_output[TOP_K]; - __local uint local_index[TOP_K]; - -#if MOE_DTYPE_SIZE == 2 - MOE_DTYPE in_value = as_half(intel_sub_group_block_read_us((const __global ushort*)(input))); -#elif MOE_DTYPE_SIZE == 4 - MOE_DTYPE in_value = as_float(intel_sub_group_block_read((const __global uint*)(input))); -#else -# error "softmax_topk: unsupported MOE_DTYPE_SIZE" -#endif - local_input[sort_index] = in_value; - barrier(CLK_LOCAL_MEM_FENCE); - - __attribute__((opencl_unroll_hint(8))) - for(uint i = 0; i < sort_index; i++) { - MOE_DTYPE value = local_input[i]; - if(value >= in_value) { - sort_position++; - } - } - - __attribute__((opencl_unroll_hint(8))) - for(uint i = sort_index; i < sort_cnt; i++) { - MOE_DTYPE value = local_input[i]; - if(value > in_value) { - sort_position++; - } - } - if (sort_position < TOP_K) { - local_output[sort_position] = in_value; - local_index[sort_position] = sort_index; - } - barrier(CLK_LOCAL_MEM_FENCE); - - if(sort_position == 0) { - float softmax_total = 1.0; - MOE_DTYPE max_v = local_output[0]; - local_output[0] = 1; - for(uint i = 1; i < TOP_K; i++) { - local_output[i] = native_exp(local_output[i] - max_v); - softmax_total += local_output[i]; - } - output_index += batch * TOP_K; - output += batch * TOP_K; - - for(uint i = 0; i < TOP_K; i++) { - output[i] = local_output[i]/softmax_total; - output_index[i] = local_index[i]; - } - } -} - -#elif SIGMOID_BIAS_TOPK_ENABLE - -KERNEL(sigmoid_bias_topk)( - const __global MOE_DTYPE* input, // routing logits [input_batch, num_experts] - const __global MOE_DTYPE* bias, // routing bias [1, num_experts] or [num_experts] - const __global MOE_DTYPE* eps_ptr, // routing epsilon scalar [1] - __global uint* output_index, // [input_batch, TOP_K] - __global MOE_DTYPE* output // [input_batch, TOP_K] -) { - // gws [batch, num_experts] - const uint batch = (uint)get_global_id(0); - const uint sort_index = (uint)get_global_id(1); - const uint sort_cnt = (uint)get_global_size(1); // num_experts - - input += batch * sort_cnt + sort_index; - - __local MOE_DTYPE local_sigmoid[VALUE_NUM]; // raw sigmoid values - __local MOE_DTYPE local_selection[VALUE_NUM]; // sigmoid + bias (for sorting) - __local MOE_DTYPE local_output[TOP_K]; - __local uint local_index[TOP_K]; - - // Compute sigmoid -#if MOE_DTYPE_SIZE == 2 - MOE_DTYPE in_value = as_half(intel_sub_group_block_read_us((const __global ushort*)(input))); -#elif MOE_DTYPE_SIZE == 4 - MOE_DTYPE in_value = as_float(intel_sub_group_block_read((const __global uint*)(input))); -#else -# error "sigmoid_bias_topk: unsupported MOE_DTYPE_SIZE" -#endif - MOE_DTYPE sigmoid_val = (MOE_DTYPE)(1.0f / (1.0f + native_exp(-(float)in_value))); - - // Add bias for selection (determines which experts are chosen) - MOE_DTYPE bias_val = bias[sort_index]; - MOE_DTYPE selection_val = sigmoid_val + bias_val; - - local_sigmoid[sort_index] = sigmoid_val; - local_selection[sort_index] = selection_val; - barrier(CLK_LOCAL_MEM_FENCE); - - // Sort by selection_val (sigmoid + bias) to find top-K - uint sort_position = 0; - uint actual_topk = (TOP_K < sort_cnt) ? TOP_K : sort_cnt; - - __attribute__((opencl_unroll_hint(8))) - for(uint i = 0; i < sort_index; i++) { - MOE_DTYPE value = local_selection[i]; - if(value >= selection_val) { - sort_position++; - } - } - - __attribute__((opencl_unroll_hint(8))) - for(uint i = sort_index; i < sort_cnt; i++) { - MOE_DTYPE value = local_selection[i]; - if(value > selection_val) { - sort_position++; - } - } - - // Store raw sigmoid values (NOT sigmoid+bias) for the top-K experts - if (sort_position < actual_topk) { - local_output[sort_position] = local_sigmoid[sort_index]; - local_index[sort_position] = sort_index; - } - barrier(CLK_LOCAL_MEM_FENCE); - - // Normalize: weights / (sum + eps) - if(sort_position == 0) { - float sum_weights = 0.0f; - for(uint i = 0; i < actual_topk; i++) { - sum_weights += (float)local_output[i]; - } - sum_weights += (float)eps_ptr[0]; // epsilon to avoid division by zero - - output_index += batch * TOP_K; - output += batch * TOP_K; - - for(uint i = 0; i < actual_topk; i++) { - output[i] = (MOE_DTYPE)((float)local_output[i] / sum_weights); - output_index[i] = local_index[i]; - } - // Zero out remaining positions if TOP_K > actual_topk - for(uint i = actual_topk; i < TOP_K; i++) { - output[i] = (MOE_DTYPE)0.0f; - output_index[i] = 0; - } - } -} - -#elif GATHER_ENABLE +#if GATHER_ENABLE __attribute__((intel_reqd_sub_group_size(SUBGROUP_SIZE))) KERNEL (gather_2d_ref)( const __global MOE_DTYPE* src_tok, // input tokens [total_token, hidden_size] - hidden_states_mem_ptr @@ -229,6 +71,39 @@ KERNEL (index_add_)(const __global MOE_DTYPE* src_tok, #define SWISH_BETA 1.0f #define ACC_DTYPE float + +// Tanh-approximation Gelu: 0.5 * x * (1 + tanh(sqrt(2/pi) * (x + 0.044715 * x^3))) +#define GELU_TANH_SQRT_2_OVER_PI 0.7978845608028654f +#define GELU_TANH_C 0.044715f + +// ERF Gelu: 0.5 * x * (1 + erf(x / sqrt(2))); 1/sqrt(2) = 0.7071067811865475 +// Fast erf approximation (A&S 7.1.26) — same coefficients as swiglu_gpu_opt.cl +inline float moe_fast_erf(float x) { + if (x > 4.0f) return 1.0f; + if (x < -4.0f) return -1.0f; + const float p = 0.3275911f; + const float a1 = 0.254829592f; + const float a2 = -0.284496736f; + const float a3 = 1.421413741f; + const float a4 = -1.453152027f; + const float a5 = 1.061405429f; + float z = fabs(x); + float t = 1.0f / (1.0f + p * z); + float y = 1.0f - (((((a5 * t + a4) * t) + a3) * t + a2) * t + a1) * t * native_exp(-(z * z)); + return (x >= 0.0f) ? y : -y; +} + +inline ACC_DTYPE moe_gate_activation(ACC_DTYPE x) { +#if GATE_ACT_GELU_ERF + return 0.5f * x * (1.0f + moe_fast_erf(x * 0.7071067811865475f)); +#elif GATE_ACT_GELU_TANH + return 0.5f * x * (1.0f + (tanh(0.79788458347320556640625f * x * (1.0f + 0.044715f * x * x)))); +#else + // Swish (SwiGLU): x * sigmoid(beta * x) + return x / (1.0f + native_exp(-SWISH_BETA * x)); +#endif +} + __attribute__((intel_reqd_sub_group_size(SUBGROUP_SIZE))) KERNEL(swiglu_ref) ( const __global MOE_DTYPE* up, // [token_len * expert_topK, inter_size] @@ -245,14 +120,14 @@ KERNEL(swiglu_ref) ( const uint offset = token_idx * INTERMEDIA_SIZE + n_offset - sg_id; ACC_DTYPE up_value = as_half(intel_sub_group_block_read_us((const __global ushort *)(up + offset))); ACC_DTYPE gate_value = as_half(intel_sub_group_block_read_us((const __global ushort *)(gate + offset))); - ACC_DTYPE value = gate_value / (1.0f + native_exp(-SWISH_BETA * gate_value)); + ACC_DTYPE value = moe_gate_activation(gate_value); half result = value * up_value; intel_sub_group_block_write_us((__global ushort *)(output + offset), as_ushort(result)); #else const uint offset = token_idx * INTERMEDIA_SIZE + n_offset; ACC_DTYPE gate_value = gate[offset]; ACC_DTYPE up_value = up[offset]; - ACC_DTYPE value = gate_value / (1.0f + native_exp(-SWISH_BETA * gate_value)); + ACC_DTYPE value = moe_gate_activation(gate_value); ACC_DTYPE result = value * up_value; output[offset] = result; #endif diff --git a/src/plugins/intel_gpu/src/graph/impls/ocl_v2/moe_3gemm_swiglu_mlp.cl b/src/plugins/intel_gpu/src/graph/impls/ocl_v2/moe_3gemm_swiglu_mlp.cl index 2cf803473d34..e65d26b2faec 100644 --- a/src/plugins/intel_gpu/src/graph/impls/ocl_v2/moe_3gemm_swiglu_mlp.cl +++ b/src/plugins/intel_gpu/src/graph/impls/ocl_v2/moe_3gemm_swiglu_mlp.cl @@ -3,10 +3,66 @@ // SPDX-License-Identifier: Apache-2.0 // -#define unroll_for __attribute__((opencl_unroll_hint)) for +#include "include/batch_headers/sub_group_block_read.cl" + +// Fake group size for compatibility and computation performance balance. +// Each gk-iteration of the inner GEMV loop processes FAKE_GROUP_SIZE K-elements +// using a single (scale, zp) entry, so FAKE_GROUP_SIZE must divide both +// GATE_UP_GROUP_SIZE and DOWN_GROUP_SIZE. The traditional value is 128 which +// matches the sub_group_block_read tile widths below; for models whose weight +// quantization uses a smaller group (e.g. 64) we shrink FAKE_GROUP_SIZE so the +// per-iteration accumulation stays within one quant group. +#if defined(GATE_UP_GROUP_SIZE) && defined(DOWN_GROUP_SIZE) +# if GATE_UP_GROUP_SIZE < DOWN_GROUP_SIZE +# define MOE_MIN_GROUP_SIZE GATE_UP_GROUP_SIZE +# else +# define MOE_MIN_GROUP_SIZE DOWN_GROUP_SIZE +# endif +# if MOE_MIN_GROUP_SIZE < 128 +# define FAKE_GROUP_SIZE MOE_MIN_GROUP_SIZE +# else +# define FAKE_GROUP_SIZE 128 +# endif +#else +# define FAKE_GROUP_SIZE 128 +#endif -// Fake group size for compatibility and computation performance balance -#define FAKE_GROUP_SIZE 128 +// Number of K-elements each work-item handles per gk-iteration via the +// intel_sub_group_block_read tile. Drives the inner-loop variant selection. +// Supported values: 2 (FAKE_GROUP_SIZE = 1 * SUBGROUP_SIZE * 2, e.g. SG=16 + 32), +// 4 (e.g. SG=32 + FAKE=128 or SG=16 + FAKE=64), and 8 (SG=16 + FAKE=128). +#define ELEMS_PER_LANE (FAKE_GROUP_SIZE / SUBGROUP_SIZE) + +// Experts per token: MAX_TOPK for non-shared, MAX_TOPK+1 with shared expert. +// Used by batched GEMV to decompose flat workgroup ID into (token_idx, expert_slot). +#if SHARED_EXPERT_ENABLE +#define EXPERTS_PER_TOKEN (MAX_TOPK + 1) +#else +#define EXPERTS_PER_TOKEN MAX_TOPK +#endif + +// Gate activation: SwiGLU (Swish, default), GeGLU-Tanh, or GeGLU-ERF. +// GATE_ACT_GELU_ERF takes precedence over GATE_ACT_GELU_TANH when both are set. +#ifdef GATE_ACT_GELU_ERF +// ERF Gelu: 0.5 * x * (1 + erf(x / sqrt(2))); A&S 7.1.26 fast erf approximation +inline float moe_mlp_fast_erf(float x) { + const float p = 0.3275911f; + const float a1 = 0.254829592f; + const float a2 = -0.284496736f; + const float a3 = 1.421413741f; + const float a4 = -1.453152027f; + const float a5 = 1.061405429f; + float z = fabs(x); + float t = 1.0f / (1.0f + p * z); + float y = 1.0f - (((((a5 * t + a4) * t) + a3) * t + a2) * t + a1) * t * exp(-(z * z)); + return (x >= 0.0f) ? y : -y; +} +# define MOE_GATE_ACT(x) (0.5f * (x) * (1.0f + moe_mlp_fast_erf((x) * 0.7071067811865475f))) +#elif defined(GATE_ACT_GELU_TANH) +# define MOE_GATE_ACT(x) (0.5f * (x) * (1.0f + (tanh(0.79788458347320556640625f * (x) * (1.0f + 0.044715f * (x) * (x)))))) +#else +# define MOE_GATE_ACT(x) ((x) / (1.0f + exp(-(x)))) +#endif // HAS_ZP: 1 = asymmetric quantization (subtract zero point), 0 = symmetric (no zero point) #if HAS_ZP @@ -64,10 +120,10 @@ inline void gate_up_gemv_n2x_u4(const __global uchar* weight, half z_hf1 = convert_half(z >> 4); #endif -# if SUBGROUP_SIZE == 32 +# if ELEMS_PER_LANE == 4 half2 sum0; half2 sum1; - half4 a = as_half4(intel_sub_group_block_read_us4((const __local ushort*)x2 + gk * FAKE_GROUP_SIZE)); + half4 a = as_half4(_sub_group_block_read_slm_us4((const __local ushort*)x2 + gk * FAKE_GROUP_SIZE)); uchar2 b = intel_sub_group_block_read_uc2((const __global uchar*)B + gk * FAKE_GROUP_SIZE / 2); uchar2 b2 = intel_sub_group_block_read_uc2((const __global uchar*)(B + (K / 2) + gk * FAKE_GROUP_SIZE / 2)); @@ -88,10 +144,31 @@ inline void gate_up_gemv_n2x_u4(const __global uchar* weight, sum_all0 += (sum0[0] + sum0[1]) * s0; sum_all1 += (sum1[0] + sum1[1]) * s1; #endif +# elif ELEMS_PER_LANE == 2 + // Each lane reads 2 K-elements (1 byte = 2 nibbles). + half sum0; + half sum1; + half2 a = as_half2(_sub_group_block_read_slm_us2((const __local ushort*)x2 + gk * FAKE_GROUP_SIZE)); + uchar b = intel_sub_group_block_read_uc((const __global uchar*)B + gk * FAKE_GROUP_SIZE / 2); + uchar b2 = intel_sub_group_block_read_uc((const __global uchar*)(B + (K / 2) + gk * FAKE_GROUP_SIZE / 2)); + + sum0 = fma(a.s0, (DEQUANT_4BIT_LO(b)), (half)0); + sum0 = fma(a.s1, (DEQUANT_4BIT_HI(b)), sum0); + + sum1 = fma(a.s0, (DEQUANT_4BIT_LO(b2)), (half)0); + sum1 = fma(a.s1, (DEQUANT_4BIT_HI(b2)), sum1); + +#if HAS_ZP + sum_all0 += (sum0 - xg_sum[gk] * z_hf0) * s0; + sum_all1 += (sum1 - xg_sum[gk] * z_hf1) * s1; +#else + sum_all0 += sum0 * s0; + sum_all1 += sum1 * s1; +#endif # else half4 sum0; half4 sum1; - half8 a = as_half8(intel_sub_group_block_read_us8((const __local ushort*)x2 + gk * FAKE_GROUP_SIZE)); + half8 a = as_half8(_sub_group_block_read_slm_us8((const __local ushort*)x2 + gk * FAKE_GROUP_SIZE)); uchar4 b = intel_sub_group_block_read_uc4((const __global uchar*)B + gk * FAKE_GROUP_SIZE / 2); uchar4 b2 = intel_sub_group_block_read_uc4((const __global uchar*)(B + (K / 2) + gk * FAKE_GROUP_SIZE / 2)); @@ -129,8 +206,8 @@ inline void gate_up_gemv_n2x_u4(const __global uchar* weight, sum_all1 = sub_group_reduce_add(sum_all1); if (id_local == 0) { if (silu) { - y[n] *= sum_all0 / (1 + exp(-sum_all0)); - y[n + 1] *= sum_all1 / (1 + exp(-sum_all1)); + y[n] *= MOE_GATE_ACT(sum_all0); + y[n + 1] *= MOE_GATE_ACT(sum_all1); } else { y[n] = sum_all0; y[n + 1] = sum_all1; @@ -171,10 +248,10 @@ inline void gate_up_gemv_n2x_u8(const __global uchar* weight, half z1 = convert_half(Z[zp_offset + 1]); #endif -# if SUBGROUP_SIZE == 32 +# if ELEMS_PER_LANE == 4 float2 sum0; float2 sum1; - half4 a = as_half4(intel_sub_group_block_read_us4((const __local ushort*)x2 + gk * FAKE_GROUP_SIZE)); + half4 a = as_half4(_sub_group_block_read_slm_us4((const __local ushort*)x2 + gk * FAKE_GROUP_SIZE)); uchar4 b = intel_sub_group_block_read_uc4((const __global uchar*)B + gk * FAKE_GROUP_SIZE); uchar4 b2 = intel_sub_group_block_read_uc4((const __global uchar*)(B + K + gk * FAKE_GROUP_SIZE)); @@ -195,10 +272,31 @@ inline void gate_up_gemv_n2x_u8(const __global uchar* weight, sum_all0 += (sum0[0] + sum0[1]) * s0; sum_all1 += (sum1[0] + sum1[1]) * s1; #endif +# elif ELEMS_PER_LANE == 2 + // Each lane reads 2 K-elements (8-bit weights, 1 byte each). + float sum0; + float sum1; + half2 a = as_half2(_sub_group_block_read_slm_us2((const __local ushort*)x2 + gk * FAKE_GROUP_SIZE)); + uchar2 b = intel_sub_group_block_read_uc2((const __global uchar*)B + gk * FAKE_GROUP_SIZE); + uchar2 b2 = intel_sub_group_block_read_uc2((const __global uchar*)(B + K + gk * FAKE_GROUP_SIZE)); + + sum0 = fma((float)a.s0, (float)(DEQUANT_8BIT(b.s0)), 0.0f); + sum0 = fma((float)a.s1, (float)(DEQUANT_8BIT(b.s1)), sum0); + + sum1 = fma((float)a.s0, (float)(DEQUANT_8BIT(b2.s0)), 0.0f); + sum1 = fma((float)a.s1, (float)(DEQUANT_8BIT(b2.s1)), sum1); + +#if HAS_ZP + sum_all0 += (sum0 - xg_sum[gk] * z0) * s0; + sum_all1 += (sum1 - xg_sum[gk] * z1) * s1; +#else + sum_all0 += sum0 * s0; + sum_all1 += sum1 * s1; +#endif # else float4 sum0; float4 sum1; - half8 a = as_half8(intel_sub_group_block_read_us8((const __local ushort*)x2 + gk * FAKE_GROUP_SIZE)); + half8 a = as_half8(_sub_group_block_read_slm_us8((const __local ushort*)x2 + gk * FAKE_GROUP_SIZE)); uchar8 b = intel_sub_group_block_read_uc8((const __global uchar*)B + gk * FAKE_GROUP_SIZE); uchar8 b2 = intel_sub_group_block_read_uc8((const __global uchar*)(B + K + gk * FAKE_GROUP_SIZE)); @@ -236,8 +334,8 @@ inline void gate_up_gemv_n2x_u8(const __global uchar* weight, sum_all1 = sub_group_reduce_add(sum_all1); if (id_local == 0) { if (silu) { - y[n] *= sum_all0 / (1 + exp(-sum_all0)); - y[n + 1] *= sum_all1 / (1 + exp(-sum_all1)); + y[n] *= MOE_GATE_ACT(sum_all0); + y[n + 1] *= MOE_GATE_ACT(sum_all1); } else { y[n] = sum_all0; y[n + 1] = sum_all1; @@ -257,7 +355,7 @@ inline void gate_up_gemv_n2x_f16(const __global half* weight, __global half* y, float sum_all0 = 0; float sum_all1 = 0; unroll_for(int gk = 0; gk < K / FAKE_GROUP_SIZE; gk++) { -# if SUBGROUP_SIZE == 32 +# if ELEMS_PER_LANE == 4 half2 sum0; half2 sum1; half4 a = as_half4(intel_sub_group_block_read_us4((const __global ushort*)x2 + gk * FAKE_GROUP_SIZE)); @@ -276,6 +374,22 @@ inline void gate_up_gemv_n2x_f16(const __global half* weight, __global half* y, sum_all0 += sum0[0] + sum0[1]; sum_all1 += sum1[0] + sum1[1]; +# elif ELEMS_PER_LANE == 2 + // Each lane reads 2 fp16 elements. + half sum0; + half sum1; + half2 a = as_half2(intel_sub_group_block_read_us2((const __global ushort*)x2 + gk * FAKE_GROUP_SIZE)); + half2 b = as_half2(intel_sub_group_block_read_us2((const __global ushort*)B + gk * FAKE_GROUP_SIZE)); + half2 b2 = as_half2(intel_sub_group_block_read_us2((const __global ushort*)B + K + gk * FAKE_GROUP_SIZE)); + + sum0 = fma(a.s0, b.s0, (half)0); + sum0 = fma(a.s1, b.s1, sum0); + + sum1 = fma(a.s0, b2.s0, (half)0); + sum1 = fma(a.s1, b2.s1, sum1); + + sum_all0 += sum0; + sum_all1 += sum1; # else half4 sum0; half4 sum1; @@ -312,8 +426,8 @@ inline void gate_up_gemv_n2x_f16(const __global half* weight, __global half* y, sum_all1 = sub_group_reduce_add(sum_all1); if (id_local == 0) { if (silu) { - y[n] *= sum_all0 / (1 + exp(-sum_all0)); - y[n + 1] *= sum_all1 / (1 + exp(-sum_all1)); + y[n] *= MOE_GATE_ACT(sum_all0); + y[n + 1] *= MOE_GATE_ACT(sum_all1); } else { y[n] = sum_all0; y[n + 1] = sum_all1; @@ -338,17 +452,21 @@ __attribute__((intel_reqd_sub_group_size(SUBGROUP_SIZE))) KERNEL(mlp_gate_up)( const __global MOE_SCALE_DT* shared_up_scale, const __global MOE_ZP_DT* shared_up_zp, const __global half* shared_gate_gate_weight, // [HIDDEN_SIZE] (assuming no scale/zp for now, or pre-dequantized) - __global MOE_DTYPE* routing_weights, // Input routing weights, will append shared gate result at end + __global MOE_DTYPE* shared_gate_out, // [token_num] — output: sigmoid(dot(x, gate_weight)) for shared expert # endif - __global MOE_DTYPE* x, // [1, HIDDEN_SIZE] - __global MOE_DTYPE* y) { // [MAX_TOPK (+1 if shared), INTERMEDIATE_SIZE] - // global: [expert, SUBGROUP_SIZE, N//N_BLOCK],[1, SUBGROUP_SIZE, SUBGROUP_NUM] - int expert_no = get_global_id(0); - y += expert_no * INTERMEDIATE_SIZE; + __global MOE_DTYPE* x, // [token_num, HIDDEN_SIZE] + __global MOE_DTYPE* y) { // [token_num * EXPERTS_PER_TOKEN, INTERMEDIATE_SIZE] + // global: [token_num*EXPERTS_PER_TOKEN, SUBGROUP_SIZE, N//N_BLOCK],[1, SUBGROUP_SIZE, SUBGROUP_NUM] + // Batched GEMV: each workgroup handles one (token, expert) pair. + // For single token (token_num=1), flat_id == expert_slot and token_idx == 0. + int flat_id = get_global_id(0); + int token_idx = flat_id / EXPERTS_PER_TOKEN; + int expert_slot = flat_id % EXPERTS_PER_TOKEN; + y += flat_id * INTERMEDIATE_SIZE; // Check if we are processing the Shared Expert # if SHARED_EXPERT_ENABLE - bool is_shared = (expert_no == MAX_TOPK); // Assuming global size was increased by 1 + bool is_shared = (expert_slot == MAX_TOPK); # else bool is_shared = false; # endif @@ -363,7 +481,7 @@ __attribute__((intel_reqd_sub_group_size(SUBGROUP_SIZE))) KERNEL(mlp_gate_up)( const int expert_zp_size = INTERMEDIATE_SIZE * HIDDEN_SIZE / GATE_UP_GROUP_SIZE; # endif - int expert_id = expert_list[expert_no]; + int expert_id = 0; // gate, [HIDDEN_SIZE, INTERMEDIATE_SIZE] __global MOE_WEI_DT* gate_weight; __global MOE_SCALE_DT* gate_scale; @@ -374,7 +492,7 @@ __attribute__((intel_reqd_sub_group_size(SUBGROUP_SIZE))) KERNEL(mlp_gate_up)( __global MOE_ZP_DT* up_zp; if (!is_shared) { - expert_id = expert_list[expert_no]; + expert_id = expert_list[token_idx * MAX_TOPK + expert_slot]; // gate, [HIDDEN_SIZE, INTERMEDIATE_SIZE] gate_weight = (__global MOE_WEI_DT*)(gate_weight_addr + expert_id * expert_wei_size); gate_scale = (__global MOE_SCALE_DT*)(gate_scale_addr + expert_id * expert_scale_size); @@ -420,7 +538,7 @@ __attribute__((intel_reqd_sub_group_size(SUBGROUP_SIZE))) KERNEL(mlp_gate_up)( int id_sg = get_sub_group_id(); int num_sg = get_num_sub_groups(); int id_local = get_sub_group_local_id(); - half* px = x + id_sg * FAKE_GROUP_SIZE; + half* px = x + token_idx * HIDDEN_SIZE + id_sg * FAKE_GROUP_SIZE; half* px2 = x2 + id_sg * FAKE_GROUP_SIZE; unroll_for(int i = id_sg; i < HIDDEN_SIZE / FAKE_GROUP_SIZE; i += num_sg, px += num_sg * FAKE_GROUP_SIZE, px2 += num_sg * FAKE_GROUP_SIZE) { #if HAS_ZP @@ -447,7 +565,7 @@ __attribute__((intel_reqd_sub_group_size(SUBGROUP_SIZE))) KERNEL(mlp_gate_up)( int id_sg = get_sub_group_id(); int num_sg = get_num_sub_groups(); int id_local = get_sub_group_local_id(); - half* px = x + id_sg * FAKE_GROUP_SIZE; + half* px = x + token_idx * HIDDEN_SIZE + id_sg * FAKE_GROUP_SIZE; half* px2 = x2 + id_sg * FAKE_GROUP_SIZE; unroll_for(int i = id_sg; i < HIDDEN_SIZE / FAKE_GROUP_SIZE; i += num_sg, px += num_sg * FAKE_GROUP_SIZE, px2 += num_sg * FAKE_GROUP_SIZE) { #if HAS_ZP @@ -473,7 +591,7 @@ __attribute__((intel_reqd_sub_group_size(SUBGROUP_SIZE))) KERNEL(mlp_gate_up)( # if SHARED_EXPERT_ENABLE // Compute scalar gate for shared expert using all threads in the workgroup. - // Only the N-block-0 workgroup writes routing_weights[MAX_TOPK]; other N-block workgroups + // Only the N-block-0 workgroup writes shared_gate_out[token_idx]; other N-block workgroups // skip this entirely to avoid redundant work and races. if (is_shared && get_group_id(2) == 0) { // Step 1: every thread accumulates a partial dot product over its slice of HIDDEN_SIZE. @@ -483,7 +601,7 @@ __attribute__((intel_reqd_sub_group_size(SUBGROUP_SIZE))) KERNEL(mlp_gate_up)( int thread_id = id_sg * SUBGROUP_SIZE + id_local; int total_threads = num_sg * SUBGROUP_SIZE; for (int i = thread_id; i < HIDDEN_SIZE; i += total_threads) { - gate_val += (float)x[i] * (float)shared_gate_gate_weight[i]; + gate_val += (float)x[token_idx * HIDDEN_SIZE + i] * (float)shared_gate_gate_weight[i]; } // Step 2: intra-subgroup reduction. @@ -500,11 +618,11 @@ __attribute__((intel_reqd_sub_group_size(SUBGROUP_SIZE))) KERNEL(mlp_gate_up)( float final_val = (id_local < num_sg) ? shared_gate_partial[id_local] : 0.0f; final_val = sub_group_reduce_add(final_val); if (id_local == 0) { - routing_weights[MAX_TOPK] = (MOE_DTYPE)(1.0f / (1.0f + exp(-final_val))); + shared_gate_out[token_idx] = (MOE_DTYPE)(1.0f / (1.0f + exp(-final_val))); } } } - // routing_weights[MAX_TOPK] is consumed by the next kernel (mlp_down) — no barrier needed here. + // shared_gate_out[token_idx] is consumed by the next kernel (mlp_down) — no barrier needed here. # endif # if WEIGHT_COMPRESSEION_DT == 0 @@ -524,14 +642,13 @@ __attribute__((intel_reqd_sub_group_size(SUBGROUP_SIZE))) KERNEL(mlp_gate_up)( inline void down_gemv_n2x_u4(const __global uchar* weight, __global half* scales, __global uchar* zps, - __global MOE_DTYPE* routing_weights, + MOE_DTYPE routing_weight_val, __global half* y, int N, int K, half* x2, float* xg_sum) { int id_local = get_sub_group_local_id(); - int expert_no = get_global_id(0); int n_start = get_global_id(2) * N_BLOCK; int n_end = n_start + N_BLOCK; @@ -554,10 +671,10 @@ inline void down_gemv_n2x_u4(const __global uchar* weight, half z_hf1 = convert_half(z >> 4); #endif -# if SUBGROUP_SIZE == 32 +# if ELEMS_PER_LANE == 4 half2 sum0; half2 sum1; - half4 a = as_half4(intel_sub_group_block_read_us4((const __local ushort*)x2 + gk * FAKE_GROUP_SIZE)); + half4 a = as_half4(_sub_group_block_read_slm_us4((const __local ushort*)x2 + gk * FAKE_GROUP_SIZE)); uchar2 b = intel_sub_group_block_read_uc2((const __global uchar*)B + gk * FAKE_GROUP_SIZE / 2); uchar2 b2 = intel_sub_group_block_read_uc2((const __global uchar*)(B + (K / 2) + gk * FAKE_GROUP_SIZE / 2)); @@ -578,10 +695,31 @@ inline void down_gemv_n2x_u4(const __global uchar* weight, sum_all0 += (sum0[0] + sum0[1]) * s0; sum_all1 += (sum1[0] + sum1[1]) * s1; #endif +# elif ELEMS_PER_LANE == 2 + // Each lane reads 2 K-elements (1 byte = 2 nibbles). + half sum0; + half sum1; + half2 a = as_half2(_sub_group_block_read_slm_us2((const __local ushort*)x2 + gk * FAKE_GROUP_SIZE)); + uchar b = intel_sub_group_block_read_uc((const __global uchar*)B + gk * FAKE_GROUP_SIZE / 2); + uchar b2 = intel_sub_group_block_read_uc((const __global uchar*)(B + (K / 2) + gk * FAKE_GROUP_SIZE / 2)); + + sum0 = fma(a.s0, (DEQUANT_4BIT_LO(b)), (half)0); + sum0 = fma(a.s1, (DEQUANT_4BIT_HI(b)), sum0); + + sum1 = fma(a.s0, (DEQUANT_4BIT_LO(b2)), (half)0); + sum1 = fma(a.s1, (DEQUANT_4BIT_HI(b2)), sum1); + +#if HAS_ZP + sum_all0 += (sum0 - xg_sum[gk] * z_hf0) * s0; + sum_all1 += (sum1 - xg_sum[gk] * z_hf1) * s1; +#else + sum_all0 += sum0 * s0; + sum_all1 += sum1 * s1; +#endif # else half4 sum0; half4 sum1; - half8 a = as_half8(intel_sub_group_block_read_us8((const __local ushort*)x2 + gk * FAKE_GROUP_SIZE)); + half8 a = as_half8(_sub_group_block_read_slm_us8((const __local ushort*)x2 + gk * FAKE_GROUP_SIZE)); uchar4 b = intel_sub_group_block_read_uc4((const __global uchar*)B + gk * FAKE_GROUP_SIZE / 2); uchar4 b2 = intel_sub_group_block_read_uc4((const __global uchar*)(B + (K / 2) + gk * FAKE_GROUP_SIZE / 2)); @@ -617,8 +755,8 @@ inline void down_gemv_n2x_u4(const __global uchar* weight, sum_all0 = sub_group_reduce_add(sum_all0); sum_all1 = sub_group_reduce_add(sum_all1); if (id_local == 0) { - y[n] = sum_all0 * routing_weights[expert_no]; - y[n + 1] = sum_all1 * routing_weights[expert_no]; + y[n] = sum_all0 * routing_weight_val; + y[n + 1] = sum_all1 * routing_weight_val; } } } @@ -626,14 +764,13 @@ inline void down_gemv_n2x_u4(const __global uchar* weight, inline void down_gemv_n2x_u8(const __global uchar* weight, __global half* scales, __global uchar* zps, - __global MOE_DTYPE* routing_weights, + MOE_DTYPE routing_weight_val, __global half* y, int N, int K, half* x2, float* xg_sum) { int id_local = get_sub_group_local_id(); - int expert_no = get_global_id(0); int n_start = get_global_id(2) * N_BLOCK; int n_end = n_start + N_BLOCK; @@ -655,10 +792,10 @@ inline void down_gemv_n2x_u8(const __global uchar* weight, half z1 = convert_half(Z[zp_offset + 1]); #endif -# if SUBGROUP_SIZE == 32 +# if ELEMS_PER_LANE == 4 float2 sum0; float2 sum1; - half4 a = as_half4(intel_sub_group_block_read_us4((const __local ushort*)x2 + gk * FAKE_GROUP_SIZE)); + half4 a = as_half4(_sub_group_block_read_slm_us4((const __local ushort*)x2 + gk * FAKE_GROUP_SIZE)); uchar4 b = intel_sub_group_block_read_uc4((const __global uchar*)B + gk * FAKE_GROUP_SIZE); uchar4 b2 = intel_sub_group_block_read_uc4((const __global uchar*)B + K + gk * FAKE_GROUP_SIZE); @@ -679,10 +816,31 @@ inline void down_gemv_n2x_u8(const __global uchar* weight, sum_all0 += (sum0[0] + sum0[1]) * s0; sum_all1 += (sum1[0] + sum1[1]) * s1; #endif +# elif ELEMS_PER_LANE == 2 + // Each lane reads 2 K-elements (8-bit weights, 1 byte each). + float sum0; + float sum1; + half2 a = as_half2(_sub_group_block_read_slm_us2((const __local ushort*)x2 + gk * FAKE_GROUP_SIZE)); + uchar2 b = intel_sub_group_block_read_uc2((const __global uchar*)B + gk * FAKE_GROUP_SIZE); + uchar2 b2 = intel_sub_group_block_read_uc2((const __global uchar*)B + K + gk * FAKE_GROUP_SIZE); + + sum0 = fma((float)a.s0, (float)(DEQUANT_8BIT(b.s0)), 0.0f); + sum0 = fma((float)a.s1, (float)(DEQUANT_8BIT(b.s1)), sum0); + + sum1 = fma((float)a.s0, (float)(DEQUANT_8BIT(b2.s0)), 0.0f); + sum1 = fma((float)a.s1, (float)(DEQUANT_8BIT(b2.s1)), sum1); + +#if HAS_ZP + sum_all0 += (sum0 - xg_sum[gk] * z0) * s0; + sum_all1 += (sum1 - xg_sum[gk] * z1) * s1; +#else + sum_all0 += sum0 * s0; + sum_all1 += sum1 * s1; +#endif # else float4 sum0; float4 sum1; - half8 a = as_half8(intel_sub_group_block_read_us8((const __local ushort*)x2 + gk * FAKE_GROUP_SIZE)); + half8 a = as_half8(_sub_group_block_read_slm_us8((const __local ushort*)x2 + gk * FAKE_GROUP_SIZE)); uchar8 b = intel_sub_group_block_read_uc8((const __global uchar*)B + gk * FAKE_GROUP_SIZE); uchar8 b2 = intel_sub_group_block_read_uc8((const __global uchar*)(B + K + gk * FAKE_GROUP_SIZE)); @@ -718,15 +876,14 @@ inline void down_gemv_n2x_u8(const __global uchar* weight, sum_all0 = sub_group_reduce_add(sum_all0); sum_all1 = sub_group_reduce_add(sum_all1); if (id_local == 0) { - y[n] = sum_all0 * routing_weights[expert_no]; - y[n + 1] = sum_all1 * routing_weights[expert_no]; + y[n] = sum_all0 * routing_weight_val; + y[n + 1] = sum_all1 * routing_weight_val; } } } -inline void down_gemv_n2x_f16(const __global half* weight, __global MOE_DTYPE* routing_weights, __global half* y, int N, int K, half* x2) { +inline void down_gemv_n2x_f16(const __global half* weight, MOE_DTYPE routing_weight_val, __global half* y, int N, int K, half* x2) { int id_local = get_sub_group_local_id(); - int expert_no = get_global_id(0); int n_start = get_global_id(2) * N_BLOCK; int n_end = n_start + N_BLOCK; @@ -736,7 +893,7 @@ inline void down_gemv_n2x_f16(const __global half* weight, __global MOE_DTYPE* r float sum_all1 = 0; unroll_for(int gk = 0; gk < K / FAKE_GROUP_SIZE; gk++) { -# if SUBGROUP_SIZE == 32 +# if ELEMS_PER_LANE == 4 half2 sum0; half2 sum1; half4 a = as_half4(intel_sub_group_block_read_us4((const __global ushort*)x2 + gk * FAKE_GROUP_SIZE)); @@ -755,10 +912,26 @@ inline void down_gemv_n2x_f16(const __global half* weight, __global MOE_DTYPE* r sum_all0 += sum0[0] + sum0[1]; sum_all1 += sum1[0] + sum1[1]; +# elif ELEMS_PER_LANE == 2 + // Each lane reads 2 fp16 elements. + half sum0; + half sum1; + half2 a = as_half2(intel_sub_group_block_read_us2((const __global ushort*)x2 + gk * FAKE_GROUP_SIZE)); + half2 b = as_half2(intel_sub_group_block_read_us2((const __global ushort*)B + gk * FAKE_GROUP_SIZE)); + half2 b2 = as_half2(intel_sub_group_block_read_us2((const __global ushort*)B + K + gk * FAKE_GROUP_SIZE)); + + sum0 = fma(a.s0, b.s0, (half)0); + sum0 = fma(a.s1, b.s1, sum0); + + sum1 = fma(a.s0, b2.s0, (half)0); + sum1 = fma(a.s1, b2.s1, sum1); + + sum_all0 += sum0; + sum_all1 += sum1; # else half4 sum0; half4 sum1; - half8 a = as_half8(intel_sub_group_block_read_us8((const __local ushort*)x2 + gk * FAKE_GROUP_SIZE)); + half8 a = as_half8(_sub_group_block_read_slm_us8((const __local ushort*)x2 + gk * FAKE_GROUP_SIZE)); half8 b = as_half8(intel_sub_group_block_read_us8((const __global ushort*)B + gk * FAKE_GROUP_SIZE)); half8 b2 = as_half8(intel_sub_group_block_read_us8((const __global ushort*)B + K + gk * FAKE_GROUP_SIZE)); @@ -789,8 +962,8 @@ inline void down_gemv_n2x_f16(const __global half* weight, __global MOE_DTYPE* r sum_all0 = sub_group_reduce_add(sum_all0); sum_all1 = sub_group_reduce_add(sum_all1); if (id_local == 0) { - y[n] = sum_all0 * routing_weights[expert_no]; - y[n + 1] = sum_all1 * routing_weights[expert_no]; + y[n] = sum_all0 * routing_weight_val; + y[n + 1] = sum_all1 * routing_weight_val; } } } @@ -804,16 +977,21 @@ __attribute__((intel_reqd_sub_group_size(SUBGROUP_SIZE))) KERNEL(mlp_down)(const const __global MOE_SCALE_DT* shared_down_scale, const __global MOE_ZP_DT* shared_down_zp, # endif - const __global MOE_DTYPE* x, // [MAX_TOPK, INTERMEDIATE_SIZE] - __global MOE_DTYPE* routing_weights, // [MAX_TOPK] - __global MOE_DTYPE* y) { // [MAX_TOPK, HIDDEN_SIZE] - // global: [expert, SUBGROUP_SIZE, N//N_BLOCK],[1, SUBGROUP_SIZE, SUBGROUP_NUM] - int expert_no = get_global_id(0); - x += expert_no * INTERMEDIATE_SIZE; - y += expert_no * HIDDEN_SIZE; + const __global MOE_DTYPE* x, // [token_num * EXPERTS_PER_TOKEN, INTERMEDIATE_SIZE] + const __global MOE_DTYPE* routing_weights, // [token_num * MAX_TOPK] compact buffer from MoERouterFused +# if SHARED_EXPERT_ENABLE + const __global MOE_DTYPE* shared_gate_in, // [token_num] separate shared expert gate values +# endif + __global MOE_DTYPE* y) { // [token_num * EXPERTS_PER_TOKEN, HIDDEN_SIZE] + // global: [token_num*EXPERTS_PER_TOKEN, SUBGROUP_SIZE, N//N_BLOCK],[1, SUBGROUP_SIZE, SUBGROUP_NUM] + int flat_id = get_global_id(0); + int token_idx = flat_id / EXPERTS_PER_TOKEN; + int expert_slot = flat_id % EXPERTS_PER_TOKEN; + x += flat_id * INTERMEDIATE_SIZE; + y += flat_id * HIDDEN_SIZE; # if SHARED_EXPERT_ENABLE - bool is_shared = (expert_no == MAX_TOPK); + bool is_shared = (expert_slot == MAX_TOPK); # else bool is_shared = false; # endif @@ -827,7 +1005,7 @@ __attribute__((intel_reqd_sub_group_size(SUBGROUP_SIZE))) KERNEL(mlp_down)(const const int expert_scale_size = INTERMEDIATE_SIZE * HIDDEN_SIZE / DOWN_GROUP_SIZE; const int expert_zp_size = INTERMEDIATE_SIZE * HIDDEN_SIZE / DOWN_GROUP_SIZE; # endif - int expert_id = expert_list[expert_no]; + int expert_id = 0; // down, [INTERMEDIATE_SIZE, HIDDEN_SIZE] __global MOE_WEI_DT* weight; @@ -835,7 +1013,7 @@ __attribute__((intel_reqd_sub_group_size(SUBGROUP_SIZE))) KERNEL(mlp_down)(const __global MOE_ZP_DT* zps; if (!is_shared) { - expert_id = expert_list[expert_no]; + expert_id = expert_list[token_idx * MAX_TOPK + expert_slot]; // down, [INTERMEDIATE_SIZE, HIDDEN_SIZE] weight = (__global MOE_WEI_DT*)(down_weight_addr + expert_id * expert_wei_size); scales = (__global MOE_SCALE_DT*)(down_scale_addr + expert_id * expert_scale_size); @@ -922,18 +1100,28 @@ __attribute__((intel_reqd_sub_group_size(SUBGROUP_SIZE))) KERNEL(mlp_down)(const barrier(CLK_LOCAL_MEM_FENCE); + // Compute the routing weight for this (token, expert) work item. + // routing_weights is compact [token_num * MAX_TOPK]; shared expert gate is separate. +# if SHARED_EXPERT_ENABLE + MOE_DTYPE routing_weight_val = is_shared ? shared_gate_in[token_idx] : routing_weights[token_idx * MAX_TOPK + expert_slot]; +# else + MOE_DTYPE routing_weight_val = routing_weights[token_idx * MAX_TOPK + expert_slot]; +# endif + # if WEIGHT_COMPRESSEION_DT == 0 - down_gemv_n2x_u4(weight, scales, zps, routing_weights, y, N, K, x2, xg_sum); + down_gemv_n2x_u4(weight, scales, zps, routing_weight_val, y, N, K, x2, xg_sum); # elif WEIGHT_COMPRESSEION_DT == 1 - down_gemv_n2x_u8(weight, scales, zps, routing_weights, y, N, K, x2, xg_sum); + down_gemv_n2x_u8(weight, scales, zps, routing_weight_val, y, N, K, x2, xg_sum); # elif WEIGHT_COMPRESSEION_DT == 2 - down_gemv_n2x_f16(weight, routing_weights, y, N, K, x2); + down_gemv_n2x_f16(weight, routing_weight_val, y, N, K, x2); # endif } #else -__attribute__((intel_reqd_sub_group_size(SUBGROUP_SIZE))) KERNEL(mlp_reduce)(const __global MOE_DTYPE* x, // [MAX_TOPK, HIDDEN_SIZE] - __global MOE_DTYPE* y) { // [1, HIDDEN_SIZE] +__attribute__((intel_reqd_sub_group_size(SUBGROUP_SIZE))) KERNEL(mlp_reduce)(const __global MOE_DTYPE* x, // [token_num * REDUCE_COUNT, HIDDEN_SIZE] + __global MOE_DTYPE* y) { // [token_num, HIDDEN_SIZE] + // gws={token_num, HIDDEN_SIZE}, lws={1, min(max_wgs, 1024)} + int token_idx = get_global_id(0); int n = get_global_id(1); # if SHARED_EXPERT_ENABLE # define REDUCE_COUNT (MAX_TOPK + 1) @@ -942,11 +1130,11 @@ __attribute__((intel_reqd_sub_group_size(SUBGROUP_SIZE))) KERNEL(mlp_reduce)(con # endif half sum[REDUCE_COUNT] = {0}; __attribute__((opencl_unroll_hint(REDUCE_COUNT))) for (int i = 0; i < REDUCE_COUNT; i++) { - sum[i] = as_half(intel_sub_group_block_read_us((const __global ushort*)(x + i * HIDDEN_SIZE + n))); + sum[i] = as_half(intel_sub_group_block_read_us((const __global ushort*)(x + (token_idx * REDUCE_COUNT + i) * HIDDEN_SIZE + n))); } for (int i = 1; i < REDUCE_COUNT; i++) { sum[0] += sum[i]; } - intel_sub_group_block_write_us((__global ushort*)(y + n), as_ushort(sum[0])); + intel_sub_group_block_write_us((__global ushort*)(y + token_idx * HIDDEN_SIZE + n), as_ushort(sum[0])); } #endif diff --git a/src/plugins/intel_gpu/src/graph/impls/ocl_v2/moe_gemm.cl b/src/plugins/intel_gpu/src/graph/impls/ocl_v2/moe_gemm.cl index 35577e5d2ce6..d9552334262d 100644 --- a/src/plugins/intel_gpu/src/graph/impls/ocl_v2/moe_gemm.cl +++ b/src/plugins/intel_gpu/src/graph/impls/ocl_v2/moe_gemm.cl @@ -14,16 +14,12 @@ * limitations under the License. */ +#include "include/batch_headers/common.cl" #include "include/batch_headers/generic_vector_ops.cl" #include "include/batch_headers/tile_ops.cl" -#ifdef BIAS_DT -DECLARE_2D_TILE(bias_tile_type, BIAS_DT, SUBGROUP_SIZE, ugemm_moe_sg_tile_m, 1, 1, 1) -#endif -DECLARE_2D_TILE(ugemm_moe_c_type_half, half, SUBGROUP_SIZE, ugemm_moe_c_type_block0, ugemm_moe_c_type_block1, ugemm_moe_c_type_nblock0, ugemm_moe_c_type_nblock1) -DECLARE_2D_TILE_COPY_REBLOCK(ugemm_moe_c_type, SUBGROUP_SIZE, ugemm_moe_c_type_block0, ugemm_moe_c_type_block1, ugemm_moe_c_type_nblock0, ugemm_moe_c_type_nblock1, - ugemm_moe_c_type_half, SUBGROUP_SIZE, ugemm_moe_c_type_block0, ugemm_moe_c_type_block1, ugemm_moe_c_type_nblock0, ugemm_moe_c_type_nblock1) - -#define unroll_for __attribute__((opencl_unroll_hint)) for +#define DECORATOR moe +#include "expert_gemm_common.cl" +#include "expert_gemm_compute.cl" __attribute__((intel_reqd_sub_group_size(SUBGROUP_SIZE))) KERNEL(moe_gemm)(OPTIONAL_SHAPE_INFO_ARG @@ -54,6 +50,7 @@ KERNEL(moe_gemm)(OPTIONAL_SHAPE_INFO_ARG , local int* slm #endif ) { + // --- Preamble: per-expert-batch dispatch --- uint batch = get_group_id(2); int input_offset = sub_group_broadcast(input_offset_per_expert[batch], 0); @@ -68,119 +65,32 @@ KERNEL(moe_gemm)(OPTIONAL_SHAPE_INFO_ARG #ifdef POST_PROC_SILU_MUL post_op_input += input_offset * OUTPUT_STRIDE; #endif - INPUT2_TYPE expert_id = sub_group_broadcast(experts_ids[batch], 0); - weight_ptr += expert_id * EXPERT_STRIDE; - - int ld_input = k; -#ifdef WEIGHT_COMPRESSED_INT4 - weight_scales += expert_id * m * NUM_GROUPS; - #ifdef WEIGHT_ZP_DT - #ifdef WEIGHT_COMPRESSED_ZP_INT4 - weight_zps += expert_id * m * NUM_GROUPS / 2; - #else - weight_zps += expert_id * m * NUM_GROUPS; - #endif - #endif -#endif - int ld_weight = k; + int expert_id = sub_group_broadcast(experts_ids[batch], 0); int cur_n_tokens = sub_group_broadcast(n_array[batch], 0); - uint sg_i = sub_group_broadcast(get_local_id(0)/SUBGROUP_SIZE, 0); - uint sg_j = sub_group_broadcast(get_local_id(1), 0); - uint sg_k = sub_group_broadcast(get_local_id(2), 0); - - uint wg_i0 = get_group_id(0) * ugemm_moe_wg_tile_m; - uint wg_j0 = get_group_id(1) * ugemm_moe_wg_tile_n; - uint sg_i0 = wg_i0 + sg_i * ugemm_moe_sg_tile_m; - uint sg_j0 = wg_j0 + sg_j * ugemm_moe_sg_tile_n; + // --- Shared GEMM computation --- + UGEMM_C_TYPE_HALF c_tile_half; + uint sg_i0, sg_j0; + if (!expert_gemm_compute(input_ptr, weight_ptr, #ifdef WEIGHT_COMPRESSED_INT4 -#ifdef SCALE_ZP_NO_TRANSPOSE - /* This parameter is the leading dimension for scales/zp. Since scales/zp are non-transpose, - the leading dimension is the stride between successive groups in the k dimension. */ - uint scale_zp_leading_dim = m; -#else - uint scale_zp_leading_dim = NUM_GROUPS; + weight_scales, +# ifdef WEIGHT_ZP_DT + weight_zps, +# endif #endif +#ifdef BIAS_DT + bias_ptr, #endif - if (wg_j0 >= cur_n_tokens) - return; /* early exit if outside batch */ -#ifdef IS_GENERATE -#ifdef USE_SLM - ugemm_moe_c_type c_tile = ugemm_moe(weight_ptr, ld_weight, input_ptr, ld_input, m, cur_n_tokens, k, wg_i0, wg_j0, 0, sg_i, sg_j, sg_k, slm -#else - ugemm_moe_c_type c_tile = ugemm_moe(weight_ptr, ld_weight, input_ptr, ld_input, m, cur_n_tokens, k, wg_i0, wg_j0, 0, sg_i, sg_j, sg_k, 0 +#ifdef POST_PROC_SILU_MUL + post_op_input, #endif -#else #ifdef USE_SLM - ugemm_moe_c_type c_tile = ugemm_moe(weight_ptr, ld_weight, input_ptr, ld_input, m, cur_n_tokens, k, wg_i0, wg_j0, 0, sg_i, sg_j, slm -#else - ugemm_moe_c_type c_tile = ugemm_moe(weight_ptr, ld_weight, input_ptr, ld_input, m, cur_n_tokens, k, wg_i0, wg_j0, 0, sg_i, sg_j, 0 + slm, #endif -#endif -#ifdef WEIGHT_COMPRESSED_INT4 - , weight_scales -#ifdef WEIGHT_ZP_DT - , weight_zps -#endif - , scale_zp_leading_dim -#endif -); - - // Only the first sg stores data in kparallel microkernels - if (sg_k > 0) + expert_id, cur_n_tokens, m, k, + &c_tile_half, &sg_i0, &sg_j0)) return; - ugemm_moe_c_type_half c_tile_half; - tile_copy_reblock(c_tile, &c_tile_half); - -#ifdef BIAS_DT - bias_ptr += (expert_id * BIAS_STRIDE); - int sglid = get_sub_group_local_id(); - const int br = ugemm_moe_c_type_block0; - const int nbr = ugemm_moe_c_type_nblock0; - const int bc = ugemm_moe_c_type_block1; - const int nbc = ugemm_moe_c_type_nblock1; - int sg = SUBGROUP_SIZE; - for (int j = 0; j < bc * nbc; j++) { - if (sg_j0 + j < cur_n_tokens) { - for (int i0 = 0; i0 < br * nbr; i0 += sg) { - int i = i0 + sglid; - if (sg_i0 + i < m) { - c_tile_half.x[i0 / br + nbr * (j / bc)][(i0 % br)/sg + (j % bc) * (br / sg)] += bias_ptr[sg_i0 + i]; - } - } - } - } -#endif - -#ifdef POST_PROC_SILU_MUL - { - int sglid = get_sub_group_local_id(); - const int br = ugemm_moe_c_type_block0; - const int nbr = ugemm_moe_c_type_nblock0; - const int bc = ugemm_moe_c_type_block1; - const int nbc = ugemm_moe_c_type_nblock1; - int sg = SUBGROUP_SIZE; - - const global OUTPUT_TYPE* post_op_base = post_op_input + sg_j0 * m + sg_i0; - unroll_for (int j = 0; j < bc * nbc; j++) { - if (sg_j0 + j < cur_n_tokens) { - const global OUTPUT_TYPE* post_op_row = post_op_base + j * m; - unroll_for (int i0 = 0; i0 < br * nbr; i0 += sg) { - int i = i0 + sglid; - if (sg_i0 + i < m) { - float post_val = post_op_row[i]; - int reg_idx_i = (i0 / br) + nbr * (j / bc); - int reg_idx_j = (i0 % br)/sg + (j % bc) * (br / sg); - float val = c_tile_half.x[reg_idx_i][reg_idx_j]; - float res = post_val * (val / (1.0f + native_exp(-val))); - c_tile_half.x[reg_idx_i][reg_idx_j] = res; - } - } - } - } - } -#endif - + // --- Contiguous tile store --- tile_store(c_tile_half, out_ptr, m, cur_n_tokens, sg_i0, sg_j0); } diff --git a/src/plugins/intel_gpu/src/graph/impls/ocl_v2/moe_router_fused.cl b/src/plugins/intel_gpu/src/graph/impls/ocl_v2/moe_router_fused.cl new file mode 100644 index 000000000000..5d1d878b9655 --- /dev/null +++ b/src/plugins/intel_gpu/src/graph/impls/ocl_v2/moe_router_fused.cl @@ -0,0 +1,163 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#if SOFTMAX_TOPK_ENABLE + +KERNEL(softmax_topk)( + const __global MOE_DTYPE* input, // [input_batch, sort_in_num] + __global uint* output_index, // [input_batch, TOP_K] + __global MOE_DTYPE* output // [input_batch, TOP_K] +) { + // gws [batch, sort_in_num] + const uint batch = (uint)get_global_id(0); + const uint sort_index = (uint)get_global_id(1); + const uint sort_cnt = (uint)get_global_size(1); + + input += batch * sort_cnt + sort_index; + + uint sort_position = 0; + + __local MOE_DTYPE local_input[VALUE_NUM]; + __local MOE_DTYPE local_output[TOP_K]; + __local uint local_index[TOP_K]; + +#if MOE_DTYPE_SIZE == 2 + MOE_DTYPE in_value = as_half(intel_sub_group_block_read_us((const __global ushort*)(input))); +#elif MOE_DTYPE_SIZE == 4 + MOE_DTYPE in_value = as_float(intel_sub_group_block_read((const __global uint*)(input))); +#else +# error "softmax_topk: unsupported MOE_DTYPE_SIZE" +#endif + local_input[sort_index] = in_value; + barrier(CLK_LOCAL_MEM_FENCE); + + __attribute__((opencl_unroll_hint(8))) + for(uint i = 0; i < sort_index; i++) { + MOE_DTYPE value = local_input[i]; + if(value >= in_value) { + sort_position++; + } + } + + __attribute__((opencl_unroll_hint(8))) + for(uint i = sort_index; i < sort_cnt; i++) { + MOE_DTYPE value = local_input[i]; + if(value > in_value) { + sort_position++; + } + } + if (sort_position < TOP_K) { + local_output[sort_position] = in_value; + local_index[sort_position] = sort_index; + } + barrier(CLK_LOCAL_MEM_FENCE); + + if(sort_position == 0) { + float softmax_total = 1.0; + MOE_DTYPE max_v = local_output[0]; + local_output[0] = 1; + for(uint i = 1; i < TOP_K; i++) { + local_output[i] = native_exp(local_output[i] - max_v); + softmax_total += local_output[i]; + } + output_index += batch * TOP_K; + output += batch * TOP_K; + + for(uint i = 0; i < TOP_K; i++) { + output[i] = local_output[i]/softmax_total; + output_index[i] = local_index[i]; + } + } +} + +#elif SIGMOID_BIAS_TOPK_ENABLE + +KERNEL(sigmoid_bias_topk)( + const __global MOE_DTYPE* input, // routing logits [input_batch, num_experts] + const __global MOE_DTYPE* bias, // routing bias [1, num_experts] or [num_experts] + const __global MOE_DTYPE* eps_ptr, // routing epsilon scalar [1] + __global uint* output_index, // [input_batch, TOP_K] + __global MOE_DTYPE* output // [input_batch, TOP_K] +) { + // gws [batch, num_experts] + const uint batch = (uint)get_global_id(0); + const uint sort_index = (uint)get_global_id(1); + const uint sort_cnt = (uint)get_global_size(1); // num_experts + + input += batch * sort_cnt + sort_index; + + __local MOE_DTYPE local_sigmoid[VALUE_NUM]; // raw sigmoid values + __local MOE_DTYPE local_selection[VALUE_NUM]; // sigmoid + bias (for sorting) + __local MOE_DTYPE local_output[TOP_K]; + __local uint local_index[TOP_K]; + + // Compute sigmoid +#if MOE_DTYPE_SIZE == 2 + MOE_DTYPE in_value = as_half(intel_sub_group_block_read_us((const __global ushort*)(input))); +#elif MOE_DTYPE_SIZE == 4 + MOE_DTYPE in_value = as_float(intel_sub_group_block_read((const __global uint*)(input))); +#else +# error "sigmoid_bias_topk: unsupported MOE_DTYPE_SIZE" +#endif + MOE_DTYPE sigmoid_val = (MOE_DTYPE)(1.0f / (1.0f + native_exp(-(float)in_value))); + + // Add bias for selection (determines which experts are chosen) + MOE_DTYPE bias_val = bias[sort_index]; + MOE_DTYPE selection_val = sigmoid_val + bias_val; + + local_sigmoid[sort_index] = sigmoid_val; + local_selection[sort_index] = selection_val; + barrier(CLK_LOCAL_MEM_FENCE); + + // Sort by selection_val (sigmoid + bias) to find top-K + uint sort_position = 0; + uint actual_topk = (TOP_K < sort_cnt) ? TOP_K : sort_cnt; + + __attribute__((opencl_unroll_hint(8))) + for(uint i = 0; i < sort_index; i++) { + MOE_DTYPE value = local_selection[i]; + if(value >= selection_val) { + sort_position++; + } + } + + __attribute__((opencl_unroll_hint(8))) + for(uint i = sort_index; i < sort_cnt; i++) { + MOE_DTYPE value = local_selection[i]; + if(value > selection_val) { + sort_position++; + } + } + + // Store raw sigmoid values (NOT sigmoid+bias) for the top-K experts + if (sort_position < actual_topk) { + local_output[sort_position] = local_sigmoid[sort_index]; + local_index[sort_position] = sort_index; + } + barrier(CLK_LOCAL_MEM_FENCE); + + // Normalize: weights / (sum + eps) + if(sort_position == 0) { + float sum_weights = 0.0f; + for(uint i = 0; i < actual_topk; i++) { + sum_weights += (float)local_output[i]; + } + sum_weights += (float)eps_ptr[0]; // epsilon to avoid division by zero + + output_index += batch * TOP_K; + output += batch * TOP_K; + + for(uint i = 0; i < actual_topk; i++) { + output[i] = (MOE_DTYPE)((float)local_output[i] / sum_weights); + output_index[i] = local_index[i]; + } + // Zero out remaining positions if TOP_K > actual_topk + for(uint i = actual_topk; i < TOP_K; i++) { + output[i] = (MOE_DTYPE)0.0f; + output_index[i] = 0; + } + } +} + +#endif diff --git a/src/plugins/intel_gpu/src/graph/impls/ocl_v2/pa_kv_cache_reorder_ref.cl b/src/plugins/intel_gpu/src/graph/impls/ocl_v2/pa_kv_cache_reorder_ref.cl index 331b998e086b..8a5c86e76465 100644 --- a/src/plugins/intel_gpu/src/graph/impls/ocl_v2/pa_kv_cache_reorder_ref.cl +++ b/src/plugins/intel_gpu/src/graph/impls/ocl_v2/pa_kv_cache_reorder_ref.cl @@ -67,55 +67,34 @@ inline void FUNC(requantize_and_store_by_channel_block)(__global OUTPUT_TYPE* ke #if defined(IS_KV_COMPRESSED) && defined(IS_KEY_BY_CHANNEL) && IS_INT4_COMPRESSED inline void FUNC(requantize_and_store_by_channel_block_int4)(__global OUTPUT_TYPE* key_cache, const uint dst_base, - UNCOMPRESSED_TYPE* dequant_vals0, - UNCOMPRESSED_TYPE* dequant_vals1) { - UNCOMPRESSED_TYPE min_value0 = UNCOMPRESSED_VAL_MAX; - UNCOMPRESSED_TYPE max_value0 = UNCOMPRESSED_VAL_MIN; - UNCOMPRESSED_TYPE min_value1 = UNCOMPRESSED_VAL_MAX; - UNCOMPRESSED_TYPE max_value1 = UNCOMPRESSED_VAL_MIN; - - for (uint token = 0; token < PAGED_ATTENTION_BLOCK_SIZE; ++token) { - UNCOMPRESSED_TYPE val0 = dequant_vals0[token]; - UNCOMPRESSED_TYPE val1 = dequant_vals1[token]; - min_value0 = fmin(min_value0, val0); - max_value0 = fmax(max_value0, val0); - min_value1 = fmin(min_value1, val1); - max_value1 = fmax(max_value1, val1); + UNCOMPRESSED_TYPE* dequant_vals) { + UNCOMPRESSED_TYPE min_value = UNCOMPRESSED_VAL_MAX; + UNCOMPRESSED_TYPE max_value = UNCOMPRESSED_VAL_MIN; + for (uint t = 0; t < PAGED_ATTENTION_BLOCK_SIZE; ++t) { + min_value = fmin(min_value, dequant_vals[t]); + max_value = fmax(max_value, dequant_vals[t]); } #define ACCUMULATOR_TYPE float - ACCUMULATOR_TYPE range0 = max_value0 - min_value0; - ACCUMULATOR_TYPE range1 = max_value1 - min_value1; - const ACCUMULATOR_TYPE min_range0 = fabs(max_value0 * 0.1f); - const ACCUMULATOR_TYPE min_range1 = fabs(max_value1 * 0.1f); - if (range0 <= min_range0) { - range0 += fmax(1.0f, min_range0); - } - if (range1 <= min_range1) { - range1 += fmax(1.0f, min_range1); + ACCUMULATOR_TYPE range = (max_value == min_value) ? 0.001f : (ACCUMULATOR_TYPE)(max_value - min_value); + const ACCUMULATOR_TYPE min_range = fabs(max_value * 0.1f); + if (range <= min_range) { + range += fmax(1.0f, min_range); } - ACCUMULATOR_TYPE scale_tmp0 = (ACCUMULATOR_TYPE)((UINT4_RANGE) / range0); - ACCUMULATOR_TYPE zp_tmp0 = (ACCUMULATOR_TYPE)(-min_value0 * scale_tmp0); - ACCUMULATOR_TYPE scale_tmp1 = (ACCUMULATOR_TYPE)((UINT4_RANGE) / range1); - ACCUMULATOR_TYPE zp_tmp1 = (ACCUMULATOR_TYPE)(-min_value1 * scale_tmp1); - UNCOMPRESSED_TYPE scale0 = (UNCOMPRESSED_TYPE)(scale_tmp0); - UNCOMPRESSED_TYPE zp0 = (UNCOMPRESSED_TYPE)(zp_tmp0); - UNCOMPRESSED_TYPE scale1 = (UNCOMPRESSED_TYPE)(scale_tmp1); - UNCOMPRESSED_TYPE zp1 = (UNCOMPRESSED_TYPE)(zp_tmp1); + ACCUMULATOR_TYPE scale_tmp = (ACCUMULATOR_TYPE)((UINT4_RANGE) / range); + ACCUMULATOR_TYPE zp_tmp = (ACCUMULATOR_TYPE)(-min_value * scale_tmp); #undef ACCUMULATOR_TYPE - for (uint token = 0; token < PAGED_ATTENTION_BLOCK_SIZE; ++token) { - char2 quantized = 0; - quantized.s0 = convert_char_rte(dequant_vals0[token] * scale0 + zp0); - quantized.s1 = convert_char_rte(dequant_vals1[token] * scale1 + zp1); - key_cache[dst_base + token] = cvt_int8x2_to_uint4x2(quantized); + for (uint t = 0; t < PAGED_ATTENTION_BLOCK_SIZE; t += U4_ELEMS_PER_BYTE) { + char q0 = (char)clamp(convert_int_rte((float)(dequant_vals[t] * scale_tmp + zp_tmp)), 0, UINT4_RANGE); + char q1 = (char)clamp(convert_int_rte((float)(dequant_vals[t + 1] * scale_tmp + zp_tmp)), 0, UINT4_RANGE); + char2 res = {q0, q1}; + key_cache[dst_base + t / U4_ELEMS_PER_BYTE] = cvt_int8x2_to_uint4x2(res); } - UNCOMPRESSED_TYPE* comp_ptr = (UNCOMPRESSED_TYPE*)(key_cache + (dst_base + PAGED_ATTENTION_BLOCK_SIZE)); - comp_ptr[0] = 1.0f / scale0; - comp_ptr[1] = zp0; - comp_ptr[2] = 1.0f / scale1; - comp_ptr[3] = zp1; + UNCOMPRESSED_TYPE* comp_ptr = (UNCOMPRESSED_TYPE*)(key_cache + dst_base + PACKED_PAGED_ATTENTION_BLOCK_SIZE); + comp_ptr[0] = (UNCOMPRESSED_TYPE)(1.0f / scale_tmp); + comp_ptr[1] = (UNCOMPRESSED_TYPE)zp_tmp; } #endif @@ -176,10 +155,15 @@ KERNEL(pa_kv_cache_reorder)( #endif #ifdef IS_KEY_BY_CHANNEL - const uint key_src_base = OUTPUT_OFFSET + src_block * KV_HEADS_NUM * phys_adjusted_k_head_size * ADJUSTED_PAGED_ATTENTION_BLOCK_SIZE + - head_idx * phys_adjusted_k_head_size * ADJUSTED_PAGED_ATTENTION_BLOCK_SIZE; - const uint key_dst_base = OUTPUT_OFFSET + dst_block * KV_HEADS_NUM * phys_adjusted_k_head_size * ADJUSTED_PAGED_ATTENTION_BLOCK_SIZE + - head_idx * phys_adjusted_k_head_size * ADJUSTED_PAGED_ATTENTION_BLOCK_SIZE; + #if IS_KV_COMPRESSED && IS_INT4_COMPRESSED + const uint phys_adjusted_block_size = PACKED_ADJUSTED_PAGED_ATTENTION_BLOCK_SIZE; + #else + const uint phys_adjusted_block_size = ADJUSTED_PAGED_ATTENTION_BLOCK_SIZE; + #endif + const uint key_src_base = OUTPUT_OFFSET + src_block * KV_HEADS_NUM * phys_adjusted_k_head_size * phys_adjusted_block_size + + head_idx * phys_adjusted_k_head_size * phys_adjusted_block_size; + const uint key_dst_base = OUTPUT_OFFSET + dst_block * KV_HEADS_NUM * phys_adjusted_k_head_size * phys_adjusted_block_size + + head_idx * phys_adjusted_k_head_size * phys_adjusted_block_size; #else const uint key_src_base = OUTPUT_OFFSET + src_block * KV_HEADS_NUM * phys_adjusted_k_head_size * PAGED_ATTENTION_BLOCK_SIZE + head_idx * phys_adjusted_k_head_size * PAGED_ATTENTION_BLOCK_SIZE; @@ -218,48 +202,55 @@ KERNEL(pa_kv_cache_reorder)( // 2. copy the src slot to dst slot // 3. if dst is in a different block, re-quantize dst block with updated scale/zp #if IS_INT4_COMPRESSED - for (uint k = sglid; k < phys_k_head_size; k += (uint)SUBGROUP_SIZE) { - const uint src_off = key_src_base + k * ADJUSTED_PAGED_ATTENTION_BLOCK_SIZE + src_slot; - const uint dst_base = key_dst_base + k * ADJUSTED_PAGED_ATTENTION_BLOCK_SIZE; - - const uint src_comp_off = key_src_base + k * ADJUSTED_PAGED_ATTENTION_BLOCK_SIZE + PAGED_ATTENTION_BLOCK_SIZE; - const uint dst_comp_off = key_dst_base + k * ADJUSTED_PAGED_ATTENTION_BLOCK_SIZE + PAGED_ATTENTION_BLOCK_SIZE; - UNCOMPRESSED_TYPE* src_comp_ptr = key_cache + src_comp_off; - UNCOMPRESSED_TYPE* dst_comp_ptr = key_cache + dst_comp_off; - - const UNCOMPRESSED_TYPE src_scale_inv0 = src_comp_ptr[0]; - const UNCOMPRESSED_TYPE src_zp0 = src_comp_ptr[1]; - const UNCOMPRESSED_TYPE src_scale_inv1 = src_comp_ptr[2]; - const UNCOMPRESSED_TYPE src_zp1 = src_comp_ptr[3]; - const UNCOMPRESSED_TYPE dst_scale_inv0 = dst_comp_ptr[0]; - const UNCOMPRESSED_TYPE dst_zp0 = dst_comp_ptr[1]; - const UNCOMPRESSED_TYPE dst_scale_inv1 = dst_comp_ptr[2]; - const UNCOMPRESSED_TYPE dst_zp1 = dst_comp_ptr[3]; - - OUTPUT_TYPE src_packed = key_cache[src_off]; - char2 src_unpacked = unpack_to_char(*(uint4x2_t *)&src_packed); - - const UNCOMPRESSED_TYPE src_val0 = ((UNCOMPRESSED_TYPE)(src_unpacked.s0) - src_zp0) * src_scale_inv0; - const UNCOMPRESSED_TYPE src_val1 = ((UNCOMPRESSED_TYPE)(src_unpacked.s1) - src_zp1) * src_scale_inv1; - - UNCOMPRESSED_TYPE dequant_vals0[PAGED_ATTENTION_BLOCK_SIZE]; - UNCOMPRESSED_TYPE dequant_vals1[PAGED_ATTENTION_BLOCK_SIZE]; - - for (uint token = 0; token < PAGED_ATTENTION_BLOCK_SIZE; ++token) { - OUTPUT_TYPE dst_packed = key_cache[dst_base + token]; - char2 dst_unpacked = unpack_to_char(*(uint4x2_t *)&dst_packed); - - UNCOMPRESSED_TYPE val0 = ((UNCOMPRESSED_TYPE)(dst_unpacked.s0) - dst_zp0) * dst_scale_inv0; - UNCOMPRESSED_TYPE val1 = ((UNCOMPRESSED_TYPE)(dst_unpacked.s1) - dst_zp1) * dst_scale_inv1; - if (token == dst_slot) { - val0 = src_val0; - val1 = src_val1; + if (src_block == dst_block) { + // Fast path: nibble shuffle within the same column. scale/zp unchanged, + // no dequant/requant — avoids accumulating rounding error across reorders. + for (uint k = sglid; k < phys_k_head_size; k += (uint)SUBGROUP_SIZE) { + const uint col_base = key_dst_base + k * PACKED_ADJUSTED_PAGED_ATTENTION_BLOCK_SIZE; + + OUTPUT_TYPE src_byte = key_cache[col_base + src_slot / U4_ELEMS_PER_BYTE]; + char2 src_pair = unpack_to_char(*(uint4x2_t*)&src_byte); + const char src_nibble = (src_slot % U4_ELEMS_PER_BYTE == 0) ? src_pair.s0 : src_pair.s1; + + const uint dst_byte_off = col_base + dst_slot / U4_ELEMS_PER_BYTE; + OUTPUT_TYPE dst_byte = key_cache[dst_byte_off]; + char2 dst_pair = unpack_to_char(*(uint4x2_t*)&dst_byte); + if (dst_slot % U4_ELEMS_PER_BYTE == 0) { + dst_pair.s0 = src_nibble; + } else { + dst_pair.s1 = src_nibble; } - dequant_vals0[token] = val0; - dequant_vals1[token] = val1; + key_cache[dst_byte_off] = cvt_int8x2_to_uint4x2(dst_pair); } + } else { + for (uint k = sglid; k < phys_k_head_size; k += (uint)SUBGROUP_SIZE) { + const uint src_col_base = key_src_base + k * PACKED_ADJUSTED_PAGED_ATTENTION_BLOCK_SIZE; + const uint dst_col_base = key_dst_base + k * PACKED_ADJUSTED_PAGED_ATTENTION_BLOCK_SIZE; + + UNCOMPRESSED_TYPE* src_comp_ptr = (UNCOMPRESSED_TYPE*)(key_cache + src_col_base + PACKED_PAGED_ATTENTION_BLOCK_SIZE); + UNCOMPRESSED_TYPE* dst_comp_ptr = (UNCOMPRESSED_TYPE*)(key_cache + dst_col_base + PACKED_PAGED_ATTENTION_BLOCK_SIZE); + const UNCOMPRESSED_TYPE src_scale_inv = src_comp_ptr[0]; + const UNCOMPRESSED_TYPE src_zp = src_comp_ptr[1]; + const UNCOMPRESSED_TYPE dst_scale_inv = dst_comp_ptr[0]; + const UNCOMPRESSED_TYPE dst_zp = dst_comp_ptr[1]; + + OUTPUT_TYPE src_byte = key_cache[src_col_base + src_slot / U4_ELEMS_PER_BYTE]; + char2 src_pair = unpack_to_char(*(uint4x2_t*)&src_byte); + const char src_nibble = (src_slot % U4_ELEMS_PER_BYTE == 0) ? src_pair.s0 : src_pair.s1; + const UNCOMPRESSED_TYPE src_val = ((UNCOMPRESSED_TYPE)src_nibble - src_zp) * src_scale_inv; + + UNCOMPRESSED_TYPE dequant_vals[PAGED_ATTENTION_BLOCK_SIZE]; + for (uint t = 0; t < PAGED_ATTENTION_BLOCK_SIZE; ++t) { + OUTPUT_TYPE dst_byte = key_cache[dst_col_base + t / U4_ELEMS_PER_BYTE]; + char2 dst_pair = unpack_to_char(*(uint4x2_t*)&dst_byte); + const char dst_nibble = (t % U4_ELEMS_PER_BYTE == 0) ? dst_pair.s0 : dst_pair.s1; + UNCOMPRESSED_TYPE v = ((UNCOMPRESSED_TYPE)dst_nibble - dst_zp) * dst_scale_inv; + if (t == dst_slot) v = src_val; + dequant_vals[t] = v; + } - FUNC_CALL(requantize_and_store_by_channel_block_int4)(key_cache, dst_base, dequant_vals0, dequant_vals1); + FUNC_CALL(requantize_and_store_by_channel_block_int4)(key_cache, dst_col_base, dequant_vals); + } } #else if (src_block == dst_block) { @@ -339,9 +330,11 @@ KERNEL(pa_kv_cache_reorder)( // value cache: per-token quantization #if IS_INT4_COMPRESSED - for (uint v = sglid; v < phys_v_head_size; v += (uint)SUBGROUP_SIZE) { - const uint src_off = val_src_base + src_slot * phys_v_head_size + v; - const uint dst_off = val_dst_base + dst_slot * phys_v_head_size + v; + // u4 per-token layout: each token row = [packed_v_head_size data bytes][scale][zp] + // Copy the whole row in one pass, scale/zp included. + for (uint v = sglid; v < phys_adjusted_v_head_size; v += (uint)SUBGROUP_SIZE) { + const uint src_off = val_src_base + src_slot * phys_adjusted_v_head_size + v; + const uint dst_off = val_dst_base + dst_slot * phys_adjusted_v_head_size + v; value_cache[dst_off] = value_cache[src_off]; } #else @@ -362,6 +355,9 @@ KERNEL(pa_kv_cache_reorder)( } #endif + #if !IS_INT4_COMPRESSED + // INT8 per-token: scale/zp arrays live at end of block, indexed by token slot. + // INT4 per-token already copied scale/zp inline with the token row. if (sglid == 0) { const uint comp_src_base = val_src_base + phys_v_head_size * PAGED_ATTENTION_BLOCK_SIZE; const uint comp_dst_base = val_dst_base + phys_v_head_size * PAGED_ATTENTION_BLOCK_SIZE; @@ -370,6 +366,7 @@ KERNEL(pa_kv_cache_reorder)( dst_comp_ptr[dst_slot] = src_comp_ptr[src_slot]; dst_comp_ptr[PAGED_ATTENTION_BLOCK_SIZE + dst_slot] = src_comp_ptr[PAGED_ATTENTION_BLOCK_SIZE + src_slot]; } + #endif #endif } } \ No newline at end of file diff --git a/src/plugins/intel_gpu/src/graph/impls/ocl_v2/pa_kv_cache_update_ref.cl b/src/plugins/intel_gpu/src/graph/impls/ocl_v2/pa_kv_cache_update_ref.cl index 9bb27521f042..2918fadc7815 100644 --- a/src/plugins/intel_gpu/src/graph/impls/ocl_v2/pa_kv_cache_update_ref.cl +++ b/src/plugins/intel_gpu/src/graph/impls/ocl_v2/pa_kv_cache_update_ref.cl @@ -48,13 +48,49 @@ inline void FUNC(quantize_and_save_per_token)(__global const INPUT0_TYPE* in_dat #undef ACCUMULATOR_TYPE #if IS_INT4_COMPRESSED - char2 res_vec = 0; - uint packed_offset[((K_HEAD_SIZE+V_HEAD_SIZE) / SUBGROUP_SIZE) / U4_ELEMS_PER_BYTE] = {0, }; - unroll_for (uint i = 0; i < num_groups; i+=U4_ELEMS_PER_BYTE) { - res_vec.s0 = convert_char_rte(input_data[i] * scale + zp); - res_vec.s1 = (i+1 < num_groups) ? convert_char_rte(input_data[i+1] * scale + zp) : 0; - uint packed_output_offset = out_data_offset + ((i / U4_ELEMS_PER_BYTE) * SUBGROUP_SIZE + sglid) * out_data_pitch; - out_data[packed_output_offset] = cvt_int8x2_to_uint4x2(res_vec); + // Quantize all groups first (clamp to [0, UINT4_RANGE] before packing) + char quant_data[(K_HEAD_SIZE+V_HEAD_SIZE) / SUBGROUP_SIZE]; + unroll_for (uint i = 0; i < num_groups; i++) { + quant_data[i] = (char)clamp(convert_int_rte((float)(input_data[i] * scale + zp)), 0, UINT4_RANGE); + } + // Adjacent packing: packed byte n = pack(head[2n], head[2n+1]) + // Each packed group of 16 bytes covers 2 input groups (32 head elements) + // Use out_data_pitch: key (pitch=block_size) → head-major, value (pitch=1) → token-major + unroll_for (uint pp = 0; pp < num_groups / U4_ELEMS_PER_BYTE; pp++) { + uint src_lane = (sglid % 8) * 2; + char lo_even = intel_sub_group_shuffle(quant_data[2 * pp], src_lane); + char hi_even = intel_sub_group_shuffle(quant_data[2 * pp], src_lane + 1); + char lo_odd = intel_sub_group_shuffle(quant_data[2 * pp + 1], src_lane); + char hi_odd = intel_sub_group_shuffle(quant_data[2 * pp + 1], src_lane + 1); + char2 res_vec; + res_vec.s0 = (sglid < 8) ? lo_even : lo_odd; + res_vec.s1 = (sglid < 8) ? hi_even : hi_odd; + out_data[out_data_offset + (pp * SUBGROUP_SIZE + sglid) * out_data_pitch] = cvt_int8x2_to_uint4x2(res_vec); + } + // Handle remaining group when num_groups is odd (e.g., head_size=80 → 5 groups) + if (num_groups % U4_ELEMS_PER_BYTE != 0) { + uint last_grp = num_groups - 1; + uint pp = num_groups / U4_ELEMS_PER_BYTE; + uint src_lane = (sglid % 8) * 2; + char lo = intel_sub_group_shuffle(quant_data[last_grp], src_lane); + char hi = intel_sub_group_shuffle(quant_data[last_grp], src_lane + 1); + char2 res_vec = {lo, hi}; + if (sglid < 8) { + out_data[out_data_offset + (pp * SUBGROUP_SIZE + sglid) * out_data_pitch] = cvt_int8x2_to_uint4x2(res_vec); + } + } + // Scale/zp storage depends on layout: + // head-major (pitch>1, key BY_TOKEN): per-token indexed at block end, like INT8 + // token-major (pitch=1, value): embedded per-token at comp_offset + INPUT0_TYPE* comp_ptr = (INPUT0_TYPE*)(out_data + comp_offset); + if (sglid == 0) { + if (out_data_pitch > 1) { + comp_ptr[token_pos_in_block] = 1.0 / scale; + comp_ptr[PAGED_ATTENTION_BLOCK_SIZE + token_pos_in_block] = zp; + } else { + comp_ptr[0] = 1.0 / scale; + comp_ptr[1] = zp; + } } #else // !IS_INT4_COMPRESSED unroll_for (uint i = 0; i < num_groups; i++) { @@ -63,17 +99,22 @@ inline void FUNC(quantize_and_save_per_token)(__global const INPUT0_TYPE* in_dat uint offset = out_data_offset + (i * SUBGROUP_SIZE + sglid) * out_data_pitch; out_data[offset] = res; } - #endif // !IS_INT4_COMPRESSED - INPUT0_TYPE* comp_ptr = out_data + comp_offset; - if (sglid == 0) { - comp_ptr[token_pos_in_block] = 1.0 / scale; - comp_ptr[PAGED_ATTENTION_BLOCK_SIZE + token_pos_in_block] = zp; - } + INPUT0_TYPE* comp_ptr = out_data + comp_offset; + if (sglid == 0) { + comp_ptr[token_pos_in_block] = 1.0 / scale; + comp_ptr[PAGED_ATTENTION_BLOCK_SIZE + token_pos_in_block] = zp; + } + #endif // !IS_INT4_COMPRESSED } #ifdef IS_KEY_BY_CHANNEL +#if IS_INT4_COMPRESSED +// INT4 BY_CHANNEL: packed_block_size = block_size / 2 = 8 bytes of packed u4 data per column +#define COMP_K_OFFSET (PAGED_ATTENTION_BLOCK_SIZE / U4_ELEMS_PER_BYTE) +#else #define COMP_K_OFFSET PAGED_ATTENTION_BLOCK_SIZE +#endif #define NUM_HEAD_SIZE_GROUPS K_HEAD_SIZE / SUBGROUP_SIZE inline void FUNC(quantize_and_save_by_channel_block_with_requantize)(__global const INPUT0_TYPE* in_data, const uint in_data_offset, @@ -158,141 +199,73 @@ inline void FUNC(quantize_and_save_by_channel_block_with_requantize_int4)(__glob const uint new_tokens_num, const uint sglid, const uint is_prefill_stage) { - int head_size_offset = SUBGROUP_SIZE * get_group_id(2) * U4_ELEMS_PER_BYTE; + // INT4 BY_CHANNEL with token-axis packing: + // Each column (head dim) stores packed TOKEN pairs within bytes. + // Column layout: [packed_tokens (8 bytes)] [scale (f16)] [zp (f16)] = 12 bytes + // Byte t/2 in column h: lo nibble = token t, hi nibble = token t+1 + // Each lane (sglid) processes one head dim per iteration. + int head_size_offset = SUBGROUP_SIZE * get_group_id(2); int num_head_size_groups; if (is_prefill_stage) { num_head_size_groups = NUM_HEAD_SIZE_GROUPS; } else { - // But INT4 work groups process U4_ELEMS_PER_BYTE partitions at a time, so multiply by U4_ELEMS_PER_BYTE - num_head_size_groups = (NUM_HEAD_SIZE_GROUPS / NUM_K_HEAD_SIZE_PARTITIONS) * U4_ELEMS_PER_BYTE; - if ((NUM_K_HEAD_SIZE_PARTITIONS % 2) != 0 && NUM_K_HEAD_SIZE_PARTITIONS > 1 && - get_group_id(2) == get_num_groups(2) - 1) { - num_head_size_groups -= 1; - } + num_head_size_groups = NUM_HEAD_SIZE_GROUPS / NUM_K_HEAD_SIZE_PARTITIONS; } - int packed_head_size_offset = SUBGROUP_SIZE * (get_group_id(2)); - - INPUT0_TYPE orig_scale[U4_ELEMS_PER_BYTE] = {0, }; - INPUT0_TYPE orig_zp[U4_ELEMS_PER_BYTE] = {0, }; - OUTPUT_TYPE orig_cache[ADJUSTED_PAGED_ATTENTION_BLOCK_SIZE] = {0, }; - - INPUT0_TYPE scale[U4_ELEMS_PER_BYTE]; - INPUT0_TYPE zp[U4_ELEMS_PER_BYTE]; - OUTPUT_TYPE buffer[ADJUSTED_PAGED_ATTENTION_BLOCK_SIZE] = {0, }; - - MAKE_VECTOR_TYPE(INPUT0_TYPE, 16) cache_data_vec_decompressed[NUM_HEAD_SIZE_GROUPS] = {0, }; - - INPUT0_TYPE* prev_comp_ptr; for (int h_sub = 0; h_sub < num_head_size_groups; h_sub++) { const int hidden_idx = head_size_offset + h_sub * SUBGROUP_SIZE + sglid; - - // cache - const uint order_in_packed = (h_sub % U4_ELEMS_PER_BYTE); - const int packed_hidden_idx = packed_head_size_offset + (h_sub/U4_ELEMS_PER_BYTE) * SUBGROUP_SIZE + sglid; - const uint packed_out_offset_per_wi = out_data_offset + packed_hidden_idx * out_data_pitch; + const uint col_base = out_data_offset + hidden_idx * out_data_pitch; // Read original scale and zp - if (order_in_packed == 0) { - INPUT0_TYPE* comp_ptr = (INPUT0_TYPE*) (&out_data[packed_out_offset_per_wi + COMP_K_OFFSET]); - orig_scale[0] = comp_ptr[0]; - orig_zp[0] = comp_ptr[1]; - orig_scale[1] = comp_ptr[2]; - orig_zp[1] = comp_ptr[3]; - } + INPUT0_TYPE* comp_ptr = (INPUT0_TYPE*)(&out_data[col_base + COMP_K_OFFSET]); + INPUT0_TYPE orig_scale = comp_ptr[0]; + INPUT0_TYPE orig_zp = comp_ptr[1]; + INPUT0_TYPE max_value = INPUT0_VAL_MIN; INPUT0_TYPE min_value = INPUT0_VAL_MAX; - // Read new input - #define READ_SIZE 16 - #define OUT_DATA_VEC MAKE_VECTOR_TYPE(OUTPUT_TYPE, READ_SIZE) - #define IN_DATA_VEC MAKE_VECTOR_TYPE(INPUT0_TYPE, READ_SIZE) - #define VLOAD CAT(vload, READ_SIZE) - - for (int j = 0; j < new_tokens_num; ++j) { - INPUT0_TYPE new_token = BLOCK_READN(INPUT0_TYPE, 1, in_data, in_data_offset + j * K_HEAD_SIZE * KV_HEADS_NUM + hidden_idx); - - cache_data_vec_decompressed[order_in_packed][token_pos_in_block + j] = new_token; - - max_value = fmax(max_value, new_token); - min_value = fmin(min_value, new_token); - } - - { - // Read a hidden dim of the previously quantized cache => decompress - // TODO : current block size is 16 (same as PA block size), - // but when the block size becomes different, this part should be updated as well - OUT_DATA_VEC prev_cache_data_vec; - if (order_in_packed == 0) - prev_cache_data_vec = VLOAD(0, out_data + packed_out_offset_per_wi); - #undef READ_SIZE - #undef VLOAD - #undef DATA_VEC - for (int j = 0; j < token_pos_in_block + new_tokens_num; ++j) { - if (j < token_pos_in_block) { - if (order_in_packed == 0) { - // Generate cache_data_vec_decompressed[0~token_pos_in_block] from key-cache - char temp = prev_cache_data_vec[j]; - MAKE_VECTOR_TYPE(char, U4_ELEMS_PER_BYTE) buff = unpack_to_char(*(uint4x2_t *)&temp); - - if (h_sub + 1 >= num_head_size_groups) { - cache_data_vec_decompressed[0][j] = ((INPUT0_TYPE)buff.s0 - orig_zp[0]) * orig_scale[0]; - orig_cache[j] = buff.s1; - } else { - cache_data_vec_decompressed[0][j] = ((INPUT0_TYPE)buff.s0 - orig_zp[0]) * orig_scale[0]; - cache_data_vec_decompressed[1][j] = ((INPUT0_TYPE)buff.s1 - orig_zp[1]) * orig_scale[1]; - } - } - } - max_value = fmax(max_value, cache_data_vec_decompressed[order_in_packed][j]); - min_value = fmin(min_value, cache_data_vec_decompressed[order_in_packed][j]); + // Decompress existing tokens and collect new tokens + INPUT0_TYPE token_vals[PAGED_ATTENTION_BLOCK_SIZE]; + for (int j = 0; j < (int)(token_pos_in_block + new_tokens_num); ++j) { + if (j < (int)token_pos_in_block) { + // Decompress existing packed token from column + char packed_byte = out_data[col_base + j / U4_ELEMS_PER_BYTE]; + MAKE_VECTOR_TYPE(char, U4_ELEMS_PER_BYTE) buff = unpack_to_char(*(uint4x2_t *)&packed_byte); + char u4_val = (j % U4_ELEMS_PER_BYTE == 0) ? buff.s0 : buff.s1; + token_vals[j] = ((INPUT0_TYPE)u4_val - orig_zp) * orig_scale; + } else { + // Read new token + int new_idx = j - token_pos_in_block; + token_vals[j] = BLOCK_READN(INPUT0_TYPE, 1, in_data, in_data_offset + new_idx * in_data_pitch + hidden_idx); } + max_value = fmax(max_value, token_vals[j]); + min_value = fmin(min_value, token_vals[j]); } - // requantize and store + // Requantize and store with token-axis packing { #define ACCUMULATOR_TYPE float ACCUMULATOR_TYPE range = (max_value == min_value) ? (0.001) : (max_value - min_value); const ACCUMULATOR_TYPE min_range = fabs(max_value * 0.1f); if (range <= min_range) { - // When the range is very small, expand the range to avoid zp overflow range += fmax(1.0f, min_range); } - ACCUMULATOR_TYPE scale_tmp = (ACCUMULATOR_TYPE)((UINT4_RANGE) / range); ACCUMULATOR_TYPE zp_tmp = (ACCUMULATOR_TYPE)(-min_value * scale_tmp); - scale[order_in_packed] = (INPUT1_TYPE)(scale_tmp); - zp[order_in_packed] = (INPUT1_TYPE)(zp_tmp); #undef ACCUMULATOR_TYPE - INPUT0_TYPE* comp_ptr = (INPUT0_TYPE*) (&out_data[packed_out_offset_per_wi + COMP_K_OFFSET]); - // [0,1] 1st scale and zp, [2,3] 2nd scale and zp. It represnets [PACKED_CACHE][Scale][ZP][Scale][ZP] - comp_ptr[2*order_in_packed] = 1.0/scale[order_in_packed]; - comp_ptr[2*order_in_packed+1] = zp[order_in_packed]; - } - - if (order_in_packed == 0 && h_sub + 1 >= num_head_size_groups) { - // NUM_HEAD_SIZE_GROUPS or number of processing groups of this partition is not aligned by U4_ELEMS_PER_BYTE. - // This sub is the last and single size. - for (uint token = 0; token < token_pos_in_block + new_tokens_num; ++token) { - char2 res_vec = 0; - res_vec.s0 = convert_char_rte(cache_data_vec_decompressed[0][token] * scale[0] + zp[0]); - res_vec.s1 = orig_cache[token]; - out_data[packed_out_offset_per_wi + token] = cvt_int8x2_to_uint4x2(res_vec); + // Pack adjacent token pairs into bytes + uint total_tokens = token_pos_in_block + new_tokens_num; + for (uint t = 0; t < total_tokens; t += U4_ELEMS_PER_BYTE) { + char q0 = (char)clamp(convert_int_rte((float)(token_vals[t] * scale_tmp + zp_tmp)), 0, UINT4_RANGE); + char q1 = (t + 1 < total_tokens) ? (char)clamp(convert_int_rte((float)(token_vals[t + 1] * scale_tmp + zp_tmp)), 0, UINT4_RANGE) : 0; + char2 res_vec = {q0, q1}; + out_data[col_base + t / U4_ELEMS_PER_BYTE] = cvt_int8x2_to_uint4x2(res_vec); } - } else { - // Process aligned size sub-group from num_head_size_groups - for (uint token = 0; token < token_pos_in_block + new_tokens_num; ++token) { - if (order_in_packed == (U4_ELEMS_PER_BYTE - 1)) { - char2 res_vec; - res_vec.s0 = buffer[token]; - res_vec.s1 = convert_char_rte(cache_data_vec_decompressed[1][token] * scale[1] + zp[1]); - out_data[packed_out_offset_per_wi + token] = cvt_int8x2_to_uint4x2(res_vec); - } else { - buffer[token] = convert_char_rte(cache_data_vec_decompressed[0][token] * scale[0] + zp[0]); - } - } + // Store scale/zp + comp_ptr[0] = 1.0 / (INPUT0_TYPE)scale_tmp; + comp_ptr[1] = (INPUT0_TYPE)zp_tmp; } } } @@ -306,10 +279,6 @@ inline void FUNC(quantize_and_save_by_channel_prefill)(__global const INPUT0_TYP //const uint token_start_pos_key, const uint sglid) { uint out_offset = out_data_offset; - #if IS_INT4_COMPRESSED - // store the rest of the pair - OUTPUT_TYPE buffer[PAGED_ATTENTION_BLOCK_SIZE] = {0, }; - #endif for (uint i = 0; i < NUM_HEAD_SIZE_GROUPS; i++) { uint key_in_offset_tmp = in_data_offset + i * SUBGROUP_SIZE; INPUT0_TYPE input_data[PAGED_ATTENTION_BLOCK_SIZE]; @@ -347,47 +316,25 @@ inline void FUNC(quantize_and_save_by_channel_prefill)(__global const INPUT0_TYP INPUT0_TYPE* comp_ptr = (INPUT0_TYPE*) (&out_data[out_offset_per_wi + COMP_K_OFFSET]); #if IS_INT4_COMPRESSED - const uint order_in_packed = i % U4_ELEMS_PER_BYTE; - comp_ptr[2 * order_in_packed] = 1.0 / scale; - comp_ptr[2 * order_in_packed + 1] = zp; - #else + // Token-axis packing: one scale/zp per column (head dim) comp_ptr[0] = 1.0 / scale; comp_ptr[1] = zp; - #endif - - // Store quantized key - #if IS_INT4_COMPRESSED - for (uint token_num = 0; token_num < tokens_num; token_num++) { - if (order_in_packed == (U4_ELEMS_PER_BYTE - 1)) { - char2 res_vec = 0; - res_vec.s0 = buffer[token_num]; - res_vec.s1 = convert_char_rte(input_data[token_num] * scale + zp); - out_data[out_offset_per_wi] = cvt_int8x2_to_uint4x2(res_vec); - out_offset_per_wi++; - } else { - buffer[token_num] = convert_char_rte(input_data[token_num] * scale + zp); - - if (i + 1 >= NUM_HEAD_SIZE_GROUPS) { - char2 res_vec = 0; - res_vec.s0 = buffer[token_num]; - res_vec.s1 = 0; - - out_data[out_offset_per_wi] = cvt_int8x2_to_uint4x2(res_vec); - out_offset_per_wi++; - } - } - } - - if (order_in_packed == (U4_ELEMS_PER_BYTE - 1)) { - out_offset += (ADJUSTED_PAGED_ATTENTION_BLOCK_SIZE * SUBGROUP_SIZE); + // Pack adjacent token pairs into bytes within this column + for (uint token_num = 0; token_num < tokens_num; token_num += U4_ELEMS_PER_BYTE) { + char q0 = (char)clamp(convert_int_rte((float)(input_data[token_num] * scale + zp)), 0, UINT4_RANGE); + char q1 = (token_num + 1 < tokens_num) ? (char)clamp(convert_int_rte((float)(input_data[token_num + 1] * scale + zp)), 0, UINT4_RANGE) : 0; + char2 res_vec = {q0, q1}; + out_data[out_offset_per_wi + token_num / U4_ELEMS_PER_BYTE] = cvt_int8x2_to_uint4x2(res_vec); } + out_offset += (ADJUSTED_PAGED_ATTENTION_BLOCK_SIZE * SUBGROUP_SIZE); #else + comp_ptr[0] = 1.0 / scale; + comp_ptr[1] = zp; + for (uint token_num = 0; token_num < tokens_num; token_num++) { OUTPUT_TYPE res = convert_char_rte(input_data[token_num] * scale + zp); - out_data[out_offset_per_wi] = res; - - out_offset_per_wi++; + out_data[out_offset_per_wi + token_num] = res; } out_offset += ADJUSTED_PAGED_ATTENTION_BLOCK_SIZE * SUBGROUP_SIZE; #endif @@ -421,9 +368,11 @@ KERNEL(pa_kv_cache_update)( #endif #if IS_INT4_COMPRESSED - const uint phys_adjusted_k_head_size = PACKED_ADJUSTED_K_HEAD_SIZE; + // INT4 BY_CHANNEL K: head_size is outer dim (not packed), block is inner (packed) + const uint phys_adjusted_k_head_size = PACKED_ADJUSTED_K_HEAD_SIZE; // = K_HEAD_SIZE for BY_CHANNEL + const uint phys_k_head_size = PACKED_K_HEAD_SIZE; // = K_HEAD_SIZE for BY_CHANNEL + // INT4 V: per-token, head_size is inner dim (packed) const uint phys_adjusted_v_head_size = PACKED_ADJUSTED_V_HEAD_SIZE; - const uint phys_k_head_size = PACKED_K_HEAD_SIZE; const uint phys_v_head_size = PACKED_V_HEAD_SIZE; #else const uint phys_adjusted_k_head_size = ADJUSTED_K_HEAD_SIZE; @@ -452,8 +401,13 @@ KERNEL(pa_kv_cache_update)( uint block_k_base_offset = block_idx * KV_HEADS_NUM * phys_adjusted_k_head_size * PAGED_ATTENTION_BLOCK_SIZE + head_idx * phys_adjusted_k_head_size * PAGED_ATTENTION_BLOCK_SIZE; #endif uint block_v_base_offset = block_idx * KV_HEADS_NUM * phys_adjusted_v_head_size * PAGED_ATTENTION_BLOCK_SIZE + head_idx * phys_adjusted_v_head_size * PAGED_ATTENTION_BLOCK_SIZE; + // Key: head-major for both INT4 and INT8 BY_TOKEN (token pos = offset within stride) uint key_out_offset = block_k_base_offset + current_token_pos_in_block; +#if IS_INT4_COMPRESSED + uint value_out_offset = block_v_base_offset + current_token_pos_in_block * phys_adjusted_v_head_size; +#else uint value_out_offset = block_v_base_offset + current_token_pos_in_block * phys_v_head_size; +#endif #if !IS_KV_COMPRESSED #define READ_K_BLOCK_SIZE GENERATE_STAGE_K_BLOCK_SIZE @@ -494,10 +448,19 @@ KERNEL(pa_kv_cache_update)( // key by channel { #if IS_INT4_COMPRESSED + // INT4 BY_CHANNEL K: requantize existing block with new token FUNC_CALL(quantize_and_save_by_channel_block_with_requantize_int4)(key_data, + key_in_offset, + KEY_IN_STRIDE, + key_cache_data, + block_k_base_offset, + k_hidden_stride, + current_token_pos_in_block, + 1, + sglid, + 0); #else FUNC_CALL(quantize_and_save_by_channel_block_with_requantize)(key_data, - #endif key_in_offset, KEY_IN_STRIDE, key_cache_data, @@ -507,11 +470,16 @@ KERNEL(pa_kv_cache_update)( 1, sglid, 0); + #endif } // value per token if (get_group_id(2) == 0) { + #if IS_INT4_COMPRESSED + const uint comp_v_offset = value_out_offset + phys_v_head_size; + #else const uint comp_v_offset = block_v_base_offset + phys_v_head_size * PAGED_ATTENTION_BLOCK_SIZE; + #endif INPUT0_TYPE input_data[V_HEAD_SIZE / SUBGROUP_SIZE]; FUNC_CALL(quantize_and_save_per_token)(value_data, value_in_offset, value_cache_data, value_out_offset, 1, comp_v_offset, current_token_pos_in_block, sglid, V_HEAD_SIZE / SUBGROUP_SIZE, &input_data[0]); @@ -525,7 +493,11 @@ KERNEL(pa_kv_cache_update)( FUNC_CALL(quantize_and_save_per_token)(key_data, key_in_offset, key_cache_data, key_out_offset, PAGED_ATTENTION_BLOCK_SIZE, comp_k_offset, current_token_pos_in_block, sglid, K_HEAD_SIZE / SUBGROUP_SIZE, &input_k_data[0]); +#if IS_INT4_COMPRESSED + const uint comp_v_offset = value_out_offset + phys_v_head_size; +#else const uint comp_v_offset = block_v_base_offset + phys_v_head_size * PAGED_ATTENTION_BLOCK_SIZE; +#endif INPUT0_TYPE input_v_data[V_HEAD_SIZE / SUBGROUP_SIZE]; FUNC_CALL(quantize_and_save_per_token)(value_data, value_in_offset, value_cache_data, value_out_offset, 1, comp_v_offset, current_token_pos_in_block, sglid, V_HEAD_SIZE / SUBGROUP_SIZE, &input_v_data[0]); @@ -570,21 +542,58 @@ KERNEL(pa_kv_cache_update)( uint block_v_base_offset = block_indices[block_offset] * KV_HEADS_NUM * phys_adjusted_v_head_size * PAGED_ATTENTION_BLOCK_SIZE + head_idx * phys_adjusted_v_head_size * PAGED_ATTENTION_BLOCK_SIZE; +#if IS_INT4_COMPRESSED + uint value_out_offset = block_v_base_offset; + value_out_offset += token_start_pos_val * phys_adjusted_v_head_size; + const uint comp_v_offset = value_out_offset + phys_v_head_size; +#else const uint comp_v_offset = block_v_base_offset + phys_v_head_size * PAGED_ATTENTION_BLOCK_SIZE; uint value_out_offset = block_v_base_offset; value_out_offset += token_start_pos_val * phys_v_head_size; +#endif if (tokens_num == PAGED_ATTENTION_BLOCK_SIZE) { // block is full #if defined(IS_KV_COMPRESSED) && defined(IS_KEY_BY_CHANNEL) + #if IS_INT4_COMPRESSED + // INT4 BY_CHANNEL: use by-channel prefill or requantize (same as INT8 BY_CHANNEL) + { + if (token_start_pos_key != 0) { + // mixed mode: need requantize with previous tokens + FUNC_CALL(quantize_and_save_by_channel_block_with_requantize_int4)(key_data, + key_in_offset, + KEY_IN_STRIDE, + key_cache_data, + block_k_base_offset, + k_hidden_stride, + token_start_pos_key, + tokens_num, + sglid, + 1); + } else { + FUNC_CALL(quantize_and_save_by_channel_prefill)(key_data, + key_in_offset, + KEY_IN_STRIDE, + key_cache_data, + key_out_offset, + tokens_num, + sglid); + } + } + // Value per token + for (uint token_num = 0; token_num < tokens_num; token_num++) { + INPUT0_TYPE input_data[V_HEAD_SIZE / SUBGROUP_SIZE]; + const uint comp_v = value_out_offset + phys_v_head_size; + FUNC_CALL(quantize_and_save_per_token)(value_data, value_in_offset, value_cache_data, value_out_offset, 1, + comp_v, token_start_pos_val + token_num, sglid, V_HEAD_SIZE / SUBGROUP_SIZE, &input_data[0]); + value_in_offset += (KV_HEADS_NUM * V_HEAD_SIZE + INPUT1_PAD_AFTER_FEATURE_NUM + INPUT1_PAD_BEFORE_FEATURE_NUM); + value_out_offset += phys_adjusted_v_head_size; + } + #else // Key by channel if (token_start_pos_key != 0) { // mixed mode => need requantize with prev tokens - #if IS_INT4_COMPRESSED - FUNC_CALL(quantize_and_save_by_channel_block_with_requantize_int4)(key_data, - #else FUNC_CALL(quantize_and_save_by_channel_block_with_requantize)(key_data, - #endif key_in_offset, KEY_IN_STRIDE, key_cache_data, @@ -611,6 +620,7 @@ KERNEL(pa_kv_cache_update)( value_in_offset += (KV_HEADS_NUM * V_HEAD_SIZE + INPUT1_PAD_AFTER_FEATURE_NUM + INPUT1_PAD_BEFORE_FEATURE_NUM); value_out_offset += phys_v_head_size; } + #endif #else // !(defined(IS_KV_COMPRESSED) && defined(IS_KEY_BY_CHANNEL)) unroll_for (uint token_num = 0; token_num < PAGED_ATTENTION_BLOCK_SIZE; token_num++) { #if !IS_KV_COMPRESSED @@ -735,26 +745,67 @@ KERNEL(pa_kv_cache_update)( // Value per token INPUT0_TYPE input_v_data[V_HEAD_SIZE / SUBGROUP_SIZE]; +#if IS_INT4_COMPRESSED + const uint cur_comp_v = value_out_offset + phys_v_head_size; + FUNC_CALL(quantize_and_save_per_token)(value_data, value_in_offset, value_cache_data, value_out_offset, 1, + cur_comp_v, token_num, sglid, V_HEAD_SIZE / SUBGROUP_SIZE, &input_v_data[0]); +#else FUNC_CALL(quantize_and_save_per_token)(value_data, value_in_offset, value_cache_data, value_out_offset, 1, comp_v_offset, token_num, sglid, V_HEAD_SIZE / SUBGROUP_SIZE, &input_v_data[0]); +#endif } #endif // IS_KV_COMPRESSED key_in_offset += (KV_HEADS_NUM * K_HEAD_SIZE + INPUT0_PAD_AFTER_FEATURE_NUM + INPUT0_PAD_BEFORE_FEATURE_NUM); key_out_offset += 1; value_in_offset += (KV_HEADS_NUM * V_HEAD_SIZE + INPUT1_PAD_AFTER_FEATURE_NUM + INPUT1_PAD_BEFORE_FEATURE_NUM); +#if IS_INT4_COMPRESSED + value_out_offset += phys_adjusted_v_head_size; +#else value_out_offset += phys_v_head_size; +#endif } #endif // !(defined(IS_KV_COMPRESSED) && defined(IS_KEY_BY_CHANNEL)) } else { #if defined(IS_KV_COMPRESSED) && defined(IS_KEY_BY_CHANNEL) + #if IS_INT4_COMPRESSED + // INT4 BY_CHANNEL: use by-channel requantize (same as INT8 BY_CHANNEL path) + { + if (token_start_pos_key != 0) { + FUNC_CALL(quantize_and_save_by_channel_block_with_requantize_int4)(key_data, + key_in_offset, + KEY_IN_STRIDE, + key_cache_data, + block_k_base_offset, + k_hidden_stride, + token_start_pos_key, + tokens_num, + sglid, + 1); + } else { + FUNC_CALL(quantize_and_save_by_channel_prefill)(key_data, + key_in_offset, + KEY_IN_STRIDE, + key_cache_data, + key_out_offset, + tokens_num, + sglid); + } + } + + // value processing per token + for (uint token_num = 0; token_num < tokens_num; token_num++) { + INPUT0_TYPE input_data[V_HEAD_SIZE / SUBGROUP_SIZE]; + const uint comp_v = value_out_offset + phys_v_head_size; + FUNC_CALL(quantize_and_save_per_token)(value_data, value_in_offset, value_cache_data, value_out_offset, 1, + comp_v, token_start_pos_val + token_num, sglid, V_HEAD_SIZE / SUBGROUP_SIZE, &input_data[0]); + value_in_offset += (KV_HEADS_NUM * V_HEAD_SIZE + INPUT1_PAD_AFTER_FEATURE_NUM + INPUT1_PAD_BEFORE_FEATURE_NUM); + value_out_offset += phys_adjusted_v_head_size; + } + #else // key processing by channel if (token_start_pos_key != 0) { // mixed mode => need requantize with prev tokens - #if IS_INT4_COMPRESSED - FUNC_CALL(quantize_and_save_by_channel_block_with_requantize_int4)(key_data, - #else FUNC_CALL(quantize_and_save_by_channel_block_with_requantize)(key_data, - #endif key_in_offset, KEY_IN_STRIDE, key_cache_data, @@ -782,6 +833,7 @@ KERNEL(pa_kv_cache_update)( value_in_offset += (KV_HEADS_NUM * V_HEAD_SIZE + INPUT1_PAD_AFTER_FEATURE_NUM + INPUT1_PAD_BEFORE_FEATURE_NUM); value_out_offset += phys_v_head_size; } + #endif #else // defined(IS_KV_COMPRESSED) && defined(IS_KEY_BY_CHANNEL) for (uint token_num = 0; token_num < tokens_num; token_num++) { uint head_idx_index = 0; @@ -817,14 +869,24 @@ KERNEL(pa_kv_cache_update)( // value processing INPUT0_TYPE input_v_data[V_HEAD_SIZE / SUBGROUP_SIZE]; +#if IS_INT4_COMPRESSED + const uint cur_comp_v = value_out_offset + phys_v_head_size; + FUNC_CALL(quantize_and_save_per_token)(value_data, value_in_offset, value_cache_data, value_out_offset, 1, + cur_comp_v, token_start_pos_val + token_num, sglid, V_HEAD_SIZE / SUBGROUP_SIZE, &input_v_data[0]); +#else FUNC_CALL(quantize_and_save_per_token)(value_data, value_in_offset, value_cache_data, value_out_offset, 1, comp_v_offset, token_start_pos_val + token_num, sglid, V_HEAD_SIZE / SUBGROUP_SIZE, &input_v_data[0]); +#endif } #endif // IS_KV_COMPRESSED key_in_offset += (KV_HEADS_NUM * K_HEAD_SIZE + INPUT0_PAD_AFTER_FEATURE_NUM + INPUT0_PAD_BEFORE_FEATURE_NUM); key_out_offset += 1; value_in_offset += (KV_HEADS_NUM * V_HEAD_SIZE + INPUT1_PAD_AFTER_FEATURE_NUM + INPUT1_PAD_BEFORE_FEATURE_NUM); +#if IS_INT4_COMPRESSED + value_out_offset += phys_adjusted_v_head_size; +#else value_out_offset += phys_v_head_size; +#endif } #endif // defined(IS_KV_COMPRESSED) && defined(IS_KEY_BY_CHANNEL) } diff --git a/src/plugins/intel_gpu/src/graph/impls/ocl_v2/pa_kv_reorder.cpp b/src/plugins/intel_gpu/src/graph/impls/ocl_v2/pa_kv_reorder.cpp index f285d0ee47a6..bc7a0587e7ad 100644 --- a/src/plugins/intel_gpu/src/graph/impls/ocl_v2/pa_kv_reorder.cpp +++ b/src/plugins/intel_gpu/src/graph/impls/ocl_v2/pa_kv_reorder.cpp @@ -88,14 +88,29 @@ class PaKVReorderGenerator : public KernelGenerator { jit.make("ADJUSTED_V_HEAD_SIZE", adjusted_v_head_size); if (is_int4_compressed) { - const size_t packed_k_head_size = align_to(k_head_size / u4_elems_per_byte, subgroup_size); + size_t packed_k_head_size; + size_t packed_adjusted_k_head_size; + size_t packed_paged_attention_block_size; + size_t packed_adjusted_paged_attention_block_size; + if (is_key_by_channel) { + packed_k_head_size = k_head_size; + packed_adjusted_k_head_size = k_head_size; + packed_paged_attention_block_size = cldnn::paged_attention::block_size / u4_elems_per_byte; + packed_adjusted_paged_attention_block_size = packed_paged_attention_block_size + scales_zp_size; + } else { + packed_k_head_size = align_to(k_head_size / u4_elems_per_byte, subgroup_size); + packed_adjusted_k_head_size = packed_k_head_size + scales_zp_size; + packed_paged_attention_block_size = cldnn::paged_attention::block_size; + packed_adjusted_paged_attention_block_size = cldnn::paged_attention::block_size; + } const size_t packed_v_head_size = align_to(v_head_size / u4_elems_per_byte, subgroup_size); - const size_t packed_adjusted_k_head_size = is_key_by_channel ? packed_k_head_size : packed_k_head_size + scales_zp_size; const size_t packed_adjusted_v_head_size = packed_v_head_size + scales_zp_size; jit.make("PACKED_K_HEAD_SIZE", packed_k_head_size); jit.make("PACKED_V_HEAD_SIZE", packed_v_head_size); jit.make("PACKED_ADJUSTED_K_HEAD_SIZE", packed_adjusted_k_head_size); jit.make("PACKED_ADJUSTED_V_HEAD_SIZE", packed_adjusted_v_head_size); + jit.make("PACKED_PAGED_ATTENTION_BLOCK_SIZE", packed_paged_attention_block_size); + jit.make("PACKED_ADJUSTED_PAGED_ATTENTION_BLOCK_SIZE", packed_adjusted_paged_attention_block_size); } } else { jit.make("ADJUSTED_PAGED_ATTENTION_BLOCK_SIZE", cldnn::paged_attention::block_size); diff --git a/src/plugins/intel_gpu/src/graph/impls/ocl_v2/paged_attention_opt.cl b/src/plugins/intel_gpu/src/graph/impls/ocl_v2/paged_attention_opt.cl index a7038f053869..78617fddeb95 100644 --- a/src/plugins/intel_gpu/src/graph/impls/ocl_v2/paged_attention_opt.cl +++ b/src/plugins/intel_gpu/src/graph/impls/ocl_v2/paged_attention_opt.cl @@ -269,47 +269,39 @@ KERNEL(pa_sdpa_opt)( #endif // Loop for qk_index -#if IS_KV_COMPRESSED && IS_INT4_COMPRESSED +#if IS_KV_COMPRESSED && IS_INT4_COMPRESSED && defined(IS_KEY_BY_CHANNEL) + // INT4 BY_CHANNEL: dim order {0,1,3,2}: [blocks, heads, head_size, packed_block+scales] + // Each column (head dim) = COMP_K_OFFSET packed bytes + 4 bytes scale/zp. + // Token sglid's u4 value is at packed byte sglid/2, nibble sglid%2. + // Lane sglid represents token sglid, reads its nibble from each column. MAKE_VECTOR_TYPE(INPUT0_TYPE, KEY_VEC_SIZE) k_vals[K_HEAD_SIZE / KEY_VEC_SIZE]; - - unroll_for (uint qk_idx = 0; qk_idx < (K_HEAD_SIZE / KEY_VEC_SIZE); qk_idx+=U4_ELEMS_PER_BYTE) { - #ifdef IS_KEY_BY_CHANNEL - INPUT0_TYPE comp_scale[U4_ELEMS_PER_BYTE] = {0, }; - INPUT0_TYPE comp_zp[U4_ELEMS_PER_BYTE] = {0, }; - - uint key_comp_offset = key_block_offset + (qk_idx / U4_ELEMS_PER_BYTE) * SUBGROUP_SIZE * hidden_stride + sglid * hidden_stride + PAGED_ATTENTION_BLOCK_SIZE; - INPUT0_TYPE* key_comp_ptr = key_cache + key_comp_offset; - comp_scale[0] = key_comp_ptr[0]; - comp_zp[0] = key_comp_ptr[1]; - comp_scale[1] = key_comp_ptr[2]; - comp_zp[1] = key_comp_ptr[3]; - #endif - - uint init_index = key_block_offset + 0 * hidden_stride * KEY_VEC_SIZE; - unroll_for (uint i = 0; i < KEY_VEC_SIZE; i++) { - uint index = key_block_offset + (qk_idx/U4_ELEMS_PER_BYTE) * hidden_stride * KEY_VEC_SIZE + i * hidden_stride; - - char packed_cache = BLOCK_READN(INPUT1_TYPE, 1, key_cache, index); - MAKE_VECTOR_TYPE(char, U4_ELEMS_PER_BYTE) buff = unpack_to_char(*(uint4x2_t *)&packed_cache); - - char key_orig = buff.s0; - #ifdef IS_KEY_BY_CHANNEL - INPUT0_TYPE first_scale = sub_group_shuffle(comp_scale[0], i); - INPUT0_TYPE second_scale = sub_group_shuffle(comp_scale[1], i); - INPUT0_TYPE first_zp = sub_group_shuffle(comp_zp[0], i); - INPUT0_TYPE second_zp = sub_group_shuffle(comp_zp[1], i); - k_vals[qk_idx][i] = ((INPUT0_TYPE)key_orig - first_zp) * first_scale; - if (qk_idx + 1 < (K_HEAD_SIZE / KEY_VEC_SIZE)) { - key_orig = buff.s1; - k_vals[qk_idx+1][i] = ((INPUT0_TYPE)key_orig - second_zp) * second_scale; - } - #else - k_vals[qk_idx][i] = ((INPUT0_TYPE)key_orig - comp_zp) * comp_scale; - if (qk_idx + 1 < (K_HEAD_SIZE / KEY_VEC_SIZE)) { - key_orig = buff.s1; - k_vals[qk_idx+1][i] = ((INPUT0_TYPE)key_orig - comp_zp) * comp_scale; - } - #endif + { + unroll_for (uint qk_idx = 0; qk_idx < K_HEAD_SIZE / KEY_VEC_SIZE; qk_idx++) { + // Pre-read scale/zp: lane sglid reads scale for head dim (qk_idx*16 + sglid) + const uint comp_head_dim = qk_idx * KEY_VEC_SIZE + sglid; + const uint comp_col_base = key_block_offset + comp_head_dim * hidden_stride; + INPUT0_TYPE* comp_ptr = (INPUT0_TYPE*)(key_cache + comp_col_base + PACKED_K_BLOCK_SIZE); + INPUT0_TYPE comp_scale = comp_ptr[0]; + INPUT0_TYPE comp_zp = comp_ptr[1]; + + // Optimized: process 2 adjacent head dims per iteration to halve memory reads + // and eliminate redundant unpack + branch operations. + const uint sglid_byte_offset = sglid / U4_ELEMS_PER_BYTE; + const uint is_odd_lane = sglid % U4_ELEMS_PER_BYTE; + unroll_for (uint i = 0; i < KEY_VEC_SIZE; i += 2) { + const uint head_dim0 = qk_idx * KEY_VEC_SIZE + i; + const uint head_dim1 = head_dim0 + 1; + + const uint col_base0 = key_block_offset + head_dim0 * hidden_stride; + const uint col_base1 = key_block_offset + head_dim1 * hidden_stride; + char packed0 = key_cache[col_base0 + sglid_byte_offset]; + char packed1 = key_cache[col_base1 + sglid_byte_offset]; + char u4_val0 = is_odd_lane ? ((uchar)packed0 >> 4) : (packed0 & 0x0F); + char u4_val1 = is_odd_lane ? ((uchar)packed1 >> 4) : (packed1 & 0x0F); + + k_vals[qk_idx][i] = ((INPUT0_TYPE)u4_val0 - _sub_group_shuffle(comp_zp, i)) * _sub_group_shuffle(comp_scale, i); + k_vals[qk_idx][i + 1] = ((INPUT0_TYPE)u4_val1 - _sub_group_shuffle(comp_zp, i + 1)) * _sub_group_shuffle(comp_scale, i + 1); + } } } @@ -332,7 +324,7 @@ KERNEL(pa_sdpa_opt)( } #endif -#else // !(IS_KV_COMPRESSED && IS_INT4_COMPRESSED) +#else // !(IS_KV_COMPRESSED && IS_INT4_COMPRESSED && defined(IS_KEY_BY_CHANNEL)) #define KEY_BLOCK_UNCOMPRESSED MAKE_VECTOR_TYPE(INPUT0_TYPE, KEY_VEC_SIZE) #ifdef DISABLE_QK_LOOP_UNROLL for (uint qk_idx = 0; qk_idx < K_HEAD_SIZE / KEY_VEC_SIZE; qk_idx++) { @@ -341,15 +333,29 @@ KERNEL(pa_sdpa_opt)( #endif #if IS_KV_COMPRESSED - #ifdef IS_KEY_BY_CHANNEL - const uint key_comp_offset = key_block_offset + qk_idx * SUBGROUP_SIZE * hidden_stride + sglid * hidden_stride + PAGED_ATTENTION_BLOCK_SIZE; - INPUT0_TYPE* key_comp_ptr = key_cache + key_comp_offset; - INPUT0_TYPE comp_scale = key_comp_ptr[0]; - INPUT0_TYPE comp_zp = key_comp_ptr[1]; - #endif - + #if IS_INT4_COMPRESSED + // INT4 BY_TOKEN head-major: [blocks, heads, packed_head_size * block_size, scales, zps] + // Each byte packs 2 adjacent head dims (lo nibble, hi nibble) + // Use scalar reads (not BLOCK_READN) to avoid subgroup block read issues with byte types + KEY_BLOCK_UNCOMPRESSED k_vals; + unroll_for (uint i = 0; i < KEY_VEC_SIZE / 2; i++) { + uint packed_h = qk_idx * (KEY_VEC_SIZE / 2) + i; + INPUT1_TYPE packed_byte = key_cache[key_block_offset + packed_h * hidden_stride + sglid]; + MAKE_VECTOR_TYPE(char, U4_ELEMS_PER_BYTE) buff = unpack_to_char(*(uint4x2_t *)&packed_byte); + k_vals[2 * i] = ((INPUT0_TYPE)(char)buff.s0 - comp_zp) * comp_scale; + k_vals[2 * i + 1] = ((INPUT0_TYPE)(char)buff.s1 - comp_zp) * comp_scale; + } + #else KEY_BLOCK_UNCOMPRESSED k_vals; uint init_index = key_block_offset + 0 * hidden_stride * KEY_VEC_SIZE; + #ifdef IS_KEY_BY_CHANNEL + // Per-channel scale/zp for i8 BY_CHANNEL: each column has PAGED_ATTENTION_BLOCK_SIZE data bytes + scale(fp16) + zp(fp16) + const uint comp_head_dim = qk_idx * KEY_VEC_SIZE + sglid; + const uint comp_col_base = key_block_offset + comp_head_dim * hidden_stride; + INPUT0_TYPE* comp_ptr = (INPUT0_TYPE*)(key_cache + comp_col_base + PAGED_ATTENTION_BLOCK_SIZE); + INPUT0_TYPE comp_scale = comp_ptr[0]; + INPUT0_TYPE comp_zp = comp_ptr[1]; + #endif unroll_for (uint i = 0; i < KEY_VEC_SIZE; i++) { k_vals[i] = BLOCK_READN(INPUT1_TYPE, 1, key_cache, key_block_offset + qk_idx * hidden_stride * KEY_VEC_SIZE + i * hidden_stride); #ifdef IS_KEY_BY_CHANNEL @@ -358,6 +364,7 @@ KERNEL(pa_sdpa_opt)( k_vals[i] = (k_vals[i] - comp_zp) * comp_scale; #endif } + #endif // IS_INT4_COMPRESSED #else // !IS_KV_COMPRESSED KEY_BLOCK k_vals = 0; unroll_for (uint i = 0; i < KEY_VEC_SIZE; i++) { @@ -486,7 +493,7 @@ KERNEL(pa_sdpa_opt)( // TODO: const uint global_data_idx = partition_idx * SEQ_LEN_PARTITION_SIZE + local_data_idx const uint global_data_idx = partition_idx * SEQ_LEN_PARTITION_SIZE + qk_idx * (SUBGROUPS_PER_WG * SUBGROUP_SIZE) + sgid * SUBGROUP_SIZE + sglid; -#if SEQ_LEN_PARTITION_SIZE % SUBGROUPS_PER_WG * SUBGROUP_SIZE == 0 +#if (SEQ_LEN_PARTITION_SIZE % (SUBGROUPS_PER_WG * SUBGROUP_SIZE)) == 0 if (global_data_idx < seq_len) { #else if (global_data_idx < seq_len && local_data_idx < SEQ_LEN_PARTITION_SIZE) { @@ -534,7 +541,7 @@ KERNEL(pa_sdpa_opt)( const uint local_data_idx = qk_idx * (SUBGROUPS_PER_WG * SUBGROUP_SIZE) + sgid * SUBGROUP_SIZE + sglid; const uint global_data_idx = partition_idx * SEQ_LEN_PARTITION_SIZE + qk_idx * (SUBGROUPS_PER_WG * SUBGROUP_SIZE) + sgid * SUBGROUP_SIZE + sglid; -#if SEQ_LEN_PARTITION_SIZE % SUBGROUPS_PER_WG * SUBGROUP_SIZE == 0 +#if (SEQ_LEN_PARTITION_SIZE % (SUBGROUPS_PER_WG * SUBGROUP_SIZE)) == 0 if (global_data_idx < seq_len) { #else if (global_data_idx < seq_len && local_data_idx < SEQ_LEN_PARTITION_SIZE) { @@ -603,9 +610,9 @@ KERNEL(pa_sdpa_opt)( #if HAS_SCORE_AGGREGATION && MULTI_TOKENS_PROCESSING // In case of multi tokens processing we have to fill the buffer with zeros to guarantee correct result // for reduction operation - softmax_results[output_offset + i] = partition_idx * SEQ_LEN_PARTITION_SIZE + i >= seq_len ? SOFTMAX_ACCUMULATOR_VAL_ZERO : slm_qk_vals[i]; + softmax_results[output_offset + i] = partition_idx * SEQ_LEN_PARTITION_SIZE + i >= seq_len ? SOFTMAX_ACCUMULATOR_VAL_ZERO : slm_qk_vals[q_idx * SEQ_LEN_PARTITION_SIZE + i]; #else - softmax_results[output_offset + i] = slm_qk_vals[i]; + softmax_results[output_offset + i] = slm_qk_vals[q_idx * SEQ_LEN_PARTITION_SIZE + i]; #endif } } @@ -647,12 +654,17 @@ KERNEL(pa_sdpa_opt)( #if IS_KV_COMPRESSED const uint packed_block_offset = block_indices[start_block_idx + block_num] * KV_HEADS_NUM * phys_adjusted_v_head_size * PAGED_ATTENTION_BLOCK_SIZE + head_idx * phys_adjusted_v_head_size * PAGED_ATTENTION_BLOCK_SIZE; - const uint head_size_offset = ((head_size_idx / SUBGROUP_SIZE) / U4_ELEMS_PER_BYTE) * SUBGROUP_SIZE + (head_size_idx % SUBGROUP_SIZE); - const uint packed_value_offset = packed_block_offset + head_size_offset; +#if IS_INT4_COMPRESSED + // INT4: per-token embedded scales at end of each token's row + INPUT0_TYPE* my_token_comp_ptr = (INPUT0_TYPE*)(value_cache + packed_block_offset + sglid * phys_adjusted_v_head_size + phys_v_head_size); + INPUT0_TYPE comp_scale = my_token_comp_ptr[0]; + INPUT0_TYPE comp_zp = my_token_comp_ptr[1]; +#else const uint value_comp_offset = packed_block_offset + phys_v_head_size * PAGED_ATTENTION_BLOCK_SIZE; INPUT0_TYPE* value_comp_ptr = value_cache + value_comp_offset; INPUT0_TYPE comp_scale = value_comp_ptr[0 + sglid]; INPUT0_TYPE comp_zp = value_comp_ptr[PAGED_ATTENTION_BLOCK_SIZE + sglid]; +#endif #endif @@ -664,20 +676,18 @@ KERNEL(pa_sdpa_opt)( #if IS_KV_COMPRESSED && IS_INT4_COMPRESSED - // INT4 compression for value-cache - const uint idx_packed = (head_size_idx / SUBGROUP_SIZE) % U4_ELEMS_PER_BYTE; + // INT4 adjacent packing: V layout [blocks, heads, block_size, packed_head+scales=68] + // packed_byte = head_size_idx/2, nibble = head_size_idx%2 + // Token stride = phys_adjusted_v_head_size (68) + const uint packed_byte_idx = head_size_idx / 2; + const uint nibble_sel = head_size_idx & 1; VALUE_BLOCK v_vals_packed; unroll_for (uint i = 0; i < VALUE_VEC_SIZE; i++) { - uint index = packed_value_offset + i * phys_v_head_size; - char packed_value = BLOCK_READN(INPUT2_TYPE, 1, value_cache, index); - MAKE_VECTOR_TYPE(char, U4_ELEMS_PER_BYTE) buff = unpack_to_char(*(uint4x2_t *)&packed_value); - - if (idx_packed == 0) { - v_vals_packed[i] = buff.s0; - } else { - v_vals_packed[i] = buff.s1; - } + uint token_offset = packed_block_offset + i * phys_adjusted_v_head_size + packed_byte_idx; + char packed_val = value_cache[token_offset]; + MAKE_VECTOR_TYPE(char, U4_ELEMS_PER_BYTE) buff = unpack_to_char(*(uint4x2_t *)&packed_val); + v_vals_packed[i] = (nibble_sel == 0) ? buff.s0 : buff.s1; } #else // !(IS_KV_COMPRESSED && IS_INT4_COMPRESSED) @@ -717,13 +727,11 @@ KERNEL(pa_sdpa_opt)( #if IS_KV_COMPRESSED && IS_INT4_COMPRESSED const uint packed_block_offset = block_indices[last_block_idx] * KV_HEADS_NUM * phys_adjusted_v_head_size * PAGED_ATTENTION_BLOCK_SIZE + head_idx * phys_adjusted_v_head_size * PAGED_ATTENTION_BLOCK_SIZE; - const uint head_size_offset = ((head_size_idx / SUBGROUP_SIZE) / U4_ELEMS_PER_BYTE) * SUBGROUP_SIZE + (head_size_idx % SUBGROUP_SIZE); - const uint packed_value_offset = packed_block_offset + head_size_offset; - const uint value_comp_offset = packed_block_offset + phys_v_head_size * PAGED_ATTENTION_BLOCK_SIZE; - INPUT0_TYPE* value_comp_ptr = value_cache + value_comp_offset; - INPUT0_TYPE comp_scale = value_comp_ptr[0 + sglid]; - INPUT0_TYPE comp_zp = value_comp_ptr[PAGED_ATTENTION_BLOCK_SIZE + sglid]; + // INT4: per-token embedded scales + INPUT0_TYPE* my_token_comp_ptr = (INPUT0_TYPE*)(value_cache + packed_block_offset + sglid * phys_adjusted_v_head_size + phys_v_head_size); + INPUT0_TYPE comp_scale = my_token_comp_ptr[0]; + INPUT0_TYPE comp_zp = my_token_comp_ptr[1]; #elif IS_KV_COMPRESSED && !IS_INT4_COMPRESSED const uint value_comp_offset = block_offset + V_HEAD_SIZE * PAGED_ATTENTION_BLOCK_SIZE; INPUT0_TYPE* value_comp_ptr = value_cache + value_comp_offset; @@ -731,26 +739,19 @@ KERNEL(pa_sdpa_opt)( INPUT0_TYPE comp_zp = value_comp_ptr[PAGED_ATTENTION_BLOCK_SIZE + sglid]; #endif -#if IS_KV_COMPRESSED && IS_INT4_COMPRESSED - const uint idx_packed = (head_size_idx / SUBGROUP_SIZE) % U4_ELEMS_PER_BYTE; -#endif - MAKE_VECTOR_TYPE(OUTPUT_TYPE, QUERIES_PER_WI) qk_val; unroll_for (uint q_idx = 0; q_idx < QUERIES_PER_WI; q_idx++) { GET_VECTOR_ELEMENT(qk_val, q_idx) = slm_qk_vals[q_idx * SEQ_LEN_PARTITION_SIZE + blocks_num_per_partition * PAGED_ATTENTION_BLOCK_SIZE + sglid]; } for (uint i = 0; i < leftovers; i++) { #if IS_KV_COMPRESSED && IS_INT4_COMPRESSED - INPUT2_TYPE value_packed = 0; - uint index = packed_value_offset + i * phys_v_head_size; - char packed_value = BLOCK_READN(INPUT2_TYPE, 1, value_cache, index); + // INT4 adjacent packing: scalar read of packed byte + nibble selection + const uint packed_byte_idx = head_size_idx / 2; + const uint nibble_sel = head_size_idx & 1; + uint token_offset = packed_block_offset + i * phys_adjusted_v_head_size + packed_byte_idx; + char packed_value = value_cache[token_offset]; MAKE_VECTOR_TYPE(char, U4_ELEMS_PER_BYTE) buff = unpack_to_char(*(uint4x2_t *)&packed_value); - - if (idx_packed == 0) { - value_packed = buff.s0; - } else { - value_packed = buff.s1; - } + INPUT2_TYPE value_packed = (nibble_sel == 0) ? buff.s0 : buff.s1; #else // !(IS_KV_COMPRESSED && IS_INT4_COMPRESSED) INPUT2_TYPE value_packed = BLOCK_READN(INPUT2_TYPE, 1, value_cache, value_offset + i * V_HEAD_SIZE); diff --git a/src/plugins/intel_gpu/src/graph/impls/ocl_v2/paged_causal_conv1d_ref.cl b/src/plugins/intel_gpu/src/graph/impls/ocl_v2/paged_causal_conv1d_ref.cl new file mode 100644 index 000000000000..82fbebfa2b0c --- /dev/null +++ b/src/plugins/intel_gpu/src/graph/impls/ocl_v2/paged_causal_conv1d_ref.cl @@ -0,0 +1,115 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "include/batch_headers/common.cl" + +KERNEL(paged_causal_conv1d_ref) +(__global INPUT0_TYPE* input_embeds, + __global INPUT1_TYPE* conv_state_table, + __global INPUT2_TYPE* conv_weight, + __global INPUT3_TYPE* conv_bias, + __global INPUT4_TYPE* subsequence_begins, + __global INPUT5_TYPE* block_indices, + __global INPUT6_TYPE* block_indices_begins, + __global INPUT7_TYPE* past_lens, + __global INPUT8_TYPE* cache_interval, + __global OUTPUT_TYPE* output_embeds, + int seq_count, + int hidden_size, + int input_token_stride, + int input_hidden_stride, + int state_block_stride, + int state_hidden_stride, + int state_kernel_stride, + int weight_hidden_stride, + int weight_kernel_stride, + int bias_hidden_stride, + int output_token_stride, + int output_hidden_stride, + int num_blocks) { + const int seq = (int)get_global_id(0); + const int h = (int)get_global_id(1); + + if (seq >= seq_count || h >= hidden_size) + return; + + const int token_begin = subsequence_begins[seq]; + const int token_end = subsequence_begins[seq + 1]; + const int blk_begin = block_indices_begins[seq]; + const int blk_end = block_indices_begins[seq + 1]; + + if (token_begin < 0 || token_end <= token_begin) + return; + + const int seq_tokens = token_end - token_begin; + const int block_span = blk_end - blk_begin; + + if (blk_end <= blk_begin || block_span <= 1) { + for (int t = 0; t < seq_tokens; t++) { + const int out_off = (token_begin + t) * output_token_stride + h * output_hidden_stride; + output_embeds[out_off] = TO_OUTPUT_TYPE(0.0f); + } + return; + } + + const int read_physical_block = block_indices[blk_begin]; + if (read_physical_block < 0 || read_physical_block >= num_blocks) { + for (int t = 0; t < seq_tokens; t++) { + const int out_off = (token_begin + t) * output_token_stride + h * output_hidden_stride; + output_embeds[out_off] = TO_OUTPUT_TYPE(0.0f); + } + return; + } + + const int seq_interval = cache_interval[seq]; + const int prev_nums = (seq_interval > 0) ? (past_lens[seq] % seq_interval) : 0; + + float state[KERNEL_SIZE]; + + const int read_state_base = read_physical_block * state_block_stride + h * state_hidden_stride; + for (int k = 0; k < KERNEL_SIZE; k++) { + state[k] = convert_float(conv_state_table[read_state_base + k * state_kernel_stride]); + } + + float bias_val = 0.0f; +#if HAS_BIAS + bias_val = convert_float(conv_bias[h * bias_hidden_stride]); +#endif + + for (int t = 0; t < seq_tokens; t++) { + const int token_idx = token_begin + t; + + for (int k = 0; k + 1 < KERNEL_SIZE; k++) { + state[k] = state[k + 1]; + } + + const int in_off = token_idx * input_token_stride + h * input_hidden_stride; + state[KERNEL_SIZE - 1] = convert_float(input_embeds[in_off]); + + float sum = bias_val; + const int w_base = h * weight_hidden_stride; + for (int k = 0; k < KERNEL_SIZE; k++) { + sum = fma(state[k], convert_float(conv_weight[w_base + k * weight_kernel_stride]), sum); + } + + const int out_off = token_idx * output_token_stride + h * output_hidden_stride; + output_embeds[out_off] = TO_OUTPUT_TYPE(sum); + + const int cached_tokens = prev_nums + (t + 1); + const int interval_hit = (seq_interval > 0) && ((cached_tokens % seq_interval) == 0); + const int is_last_token = (t == seq_tokens - 1); + if (interval_hit || is_last_token) { + const int slot = (seq_interval > 0) ? (1 + (cached_tokens - 1) / seq_interval) : 1; + if (slot >= 1 && slot < block_span) { + const int physical_block = block_indices[blk_begin + slot]; + if (physical_block >= 0 && physical_block < num_blocks) { + const int state_base = physical_block * state_block_stride + h * state_hidden_stride; + for (int k = 0; k < KERNEL_SIZE; k++) { + conv_state_table[state_base + k * state_kernel_stride] = TO_INPUT1_TYPE(state[k]); + } + } + } + } + } +} diff --git a/src/plugins/intel_gpu/src/graph/impls/ocl_v2/paged_causal_conv1d_ref.cpp b/src/plugins/intel_gpu/src/graph/impls/ocl_v2/paged_causal_conv1d_ref.cpp new file mode 100644 index 000000000000..cdf3fb0aa5c2 --- /dev/null +++ b/src/plugins/intel_gpu/src/graph/impls/ocl_v2/paged_causal_conv1d_ref.cpp @@ -0,0 +1,150 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "paged_causal_conv1d_ref.hpp" + +#include "intel_gpu/primitives/paged_causal_conv1d.hpp" +#include "primitive_ocl_base.hpp" +#include "utils/kernel_generator.hpp" + +namespace ov::intel_gpu::ocl { +namespace { + +class PagedCausalConv1DRefGenerator : public KernelGenerator { +public: + PagedCausalConv1DRefGenerator() : KernelGenerator("paged_causal_conv1d_ref") {} + +protected: + [[nodiscard]] JitConstants get_jit_constants(const RuntimeParams& params) const override { + auto jit = KernelGenerator::get_jit_constants(params); + + const auto& input_shape = params.get_input_layout(paged_causal_conv1d::INPUT_EMBEDS).get_partial_shape(); + const auto& state_shape = params.get_input_layout(paged_causal_conv1d::CONV_STATE_TABLE).get_partial_shape(); + const auto& bias_shape = params.get_input_layout(paged_causal_conv1d::CONV_BIAS).get_partial_shape(); + const bool has_bias = bias_shape.rank().is_static() && bias_shape.size() == 1 && bias_shape[0].is_static() && bias_shape[0].get_length() != 0; + + jit.make("HIDDEN_SIZE", static_cast(input_shape[1].get_length())); + jit.make("KERNEL_SIZE", static_cast(state_shape[2].get_length())); + jit.make("HAS_BIAS", has_bias ? 1 : 0); + + return jit; + } + + [[nodiscard]] Arguments get_arguments_desc(const RuntimeParams& params) const override { + Arguments args; + + for (uint32_t i = 0; i < params.input_layouts.size(); i++) { + args.push_back({ArgumentDescriptor::Types::INPUT, i}); + } + + args.push_back({ArgumentDescriptor::Types::OUTPUT, 0}); + + constexpr size_t num_scalars = 13; // Must match scalars count in get_dispatch_data_func + for (size_t i = 0; i < num_scalars; i++) { + args.push_back({ArgumentDescriptor::Types::SCALAR, static_cast(i)}); + } + + return args; + } + + [[nodiscard]] DispatchDataFunc get_dispatch_data_func() const override { + return DispatchDataFunc{[](const RuntimeParams& params, KernelData& kd, ImplRuntimeParams* rt_params) { + assert(!params.is_dynamic()); + auto& wgs = kd.params.workGroups; + + const auto& input_shape = params.get_input_layout(paged_causal_conv1d::INPUT_EMBEDS).get_partial_shape(); + const auto& seq_shape = params.get_input_layout(paged_causal_conv1d::SUBSEQUENCE_BEGINS).get_partial_shape(); + + const int32_t seq_count = seq_shape[0].get_length() > 0 ? static_cast(seq_shape[0].get_length() - 1) : 0; + const int32_t hidden_size = static_cast(input_shape[1].get_length()); + + auto read_pitch = [](const cldnn::layout& layout, size_t idx) -> int32_t { + const auto& pitches = layout.get_pitches(); + if (idx < pitches.size()) { + return static_cast(pitches[idx]); + } + return 1; + }; + + const auto& in_layout = params.input_layouts[paged_causal_conv1d::INPUT_EMBEDS]; + const auto& state_layout = params.input_layouts[paged_causal_conv1d::CONV_STATE_TABLE]; + const auto& weight_layout = params.input_layouts[paged_causal_conv1d::CONV_WEIGHT]; + const auto& bias_layout = params.input_layouts[paged_causal_conv1d::CONV_BIAS]; + const auto& out_layout = params.output_layouts[0]; + const auto& bias_shape = bias_layout.get_partial_shape(); + const bool has_bias = bias_shape.rank().is_static() && bias_shape.size() == 1 && bias_shape[0].is_static() && bias_shape[0].get_length() != 0; + + const int32_t input_token_stride = read_pitch(in_layout, 0); + const int32_t input_hidden_stride = read_pitch(in_layout, 1); + + const int32_t state_block_stride = read_pitch(state_layout, 0); + const int32_t state_hidden_stride = read_pitch(state_layout, 1); + const int32_t state_kernel_stride = read_pitch(state_layout, 2); + + const int32_t weight_hidden_stride = read_pitch(weight_layout, 0); + const int32_t weight_kernel_stride = read_pitch(weight_layout, 2); + + const int32_t bias_hidden_stride = has_bias ? read_pitch(bias_layout, 0) : 0; + + const int32_t output_token_stride = read_pitch(out_layout, 0); + const int32_t output_hidden_stride = read_pitch(out_layout, 1); + + const size_t lws_y = std::min(256, static_cast(hidden_size)); + const size_t gws_y = ((static_cast(hidden_size) + lws_y - 1) / lws_y) * lws_y; + wgs.global = {static_cast(seq_count), gws_y, 1}; + wgs.local = {1, lws_y, 1}; + + kd.params.scalars.clear(); + std::vector scalars{ + seq_count, + hidden_size, + input_token_stride, + input_hidden_stride, + state_block_stride, + state_hidden_stride, + state_kernel_stride, + weight_hidden_stride, + weight_kernel_stride, + bias_hidden_stride, + output_token_stride, + output_hidden_stride, + static_cast(params.get_input_layout(paged_causal_conv1d::CONV_STATE_TABLE).get_partial_shape()[0].get_length()), + }; + + for (auto v : scalars) { + scalar_desc desc; + desc.t = scalar_desc::Types::INT32; + desc.v.s32 = v; + kd.params.scalars.push_back(desc); + } + }}; + } +}; + +class PagedCausalConv1DRefImpl : public PrimitiveImplOCL { +public: + DECLARE_OBJECT_TYPE_SERIALIZATION(ov::intel_gpu::ocl::PagedCausalConv1DRefImpl) + + Stage::Ptr paged_causal_conv1d = make_stage(); + + PagedCausalConv1DRefImpl() : PrimitiveImplOCL(PagedCausalConv1DRef::get_type_info_static()) {} + PagedCausalConv1DRefImpl(const program_node& node, const RuntimeParams& params) : PagedCausalConv1DRefImpl() { + add_stage(paged_causal_conv1d, params); + } + + [[nodiscard]] std::unique_ptr clone() const override { + return make_deep_copy(this); + } +}; + +} // namespace + +std::unique_ptr PagedCausalConv1DRef::create_impl(const program_node& node, const RuntimeParams& params) const { + assert(node.is_type()); + return std::make_unique(node, params); +} + +} // namespace ov::intel_gpu::ocl + +BIND_BINARY_BUFFER_WITH_TYPE(ov::intel_gpu::ocl::PagedCausalConv1DRefImpl) diff --git a/src/plugins/intel_gpu/src/graph/impls/ocl_v2/paged_causal_conv1d_ref.hpp b/src/plugins/intel_gpu/src/graph/impls/ocl_v2/paged_causal_conv1d_ref.hpp new file mode 100644 index 000000000000..9276762112c1 --- /dev/null +++ b/src/plugins/intel_gpu/src/graph/impls/ocl_v2/paged_causal_conv1d_ref.hpp @@ -0,0 +1,58 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#pragma once + +#include +#include + +#include "paged_causal_conv1d_inst.h" +#include "program_node.h" +#include "registry/implementation_manager.hpp" + +using namespace cldnn; // TODO: Remove once namespaces are aligned + +namespace ov::intel_gpu::ocl { + +struct PagedCausalConv1DRef : public ImplementationManager { + OV_GPU_PRIMITIVE_IMPL("ocl::paged_causal_conv1d::ref") + explicit PagedCausalConv1DRef(shape_types shape_type, ValidateFunc vf = nullptr) : ImplementationManager(impl_types::ocl, shape_type, std::move(vf)) {} + [[nodiscard]] std::unique_ptr create_impl(const program_node& node, const RuntimeParams& params) const override; + + [[nodiscard]] bool validate_impl(const program_node& node) const override { + assert(node.is_type()); + static constexpr std::array supported_fmts = { + format::bfyx, + }; + + static constexpr std::array supported_real_types = { + ov::element::f16, + ov::element::f32, + }; + + for (size_t i = 0; i < node.get_dependencies().size(); i++) { + const auto& in_layout = node.get_input_layout(i); + if (!one_of(in_layout.format, supported_fmts)) { + return false; + } + + if (i <= paged_causal_conv1d::CONV_BIAS) { + if (!one_of(in_layout.data_type, supported_real_types)) { + return false; + } + } else if (in_layout.data_type != ov::element::i32) { + return false; + } + } + + const auto& out_layout = node.get_output_layout(0); + if (!one_of(out_layout.format, supported_fmts) || !one_of(out_layout.data_type, supported_real_types)) { + return false; + } + + return true; + } +}; + +} // namespace ov::intel_gpu::ocl diff --git a/src/plugins/intel_gpu/src/graph/impls/ocl_v2/paged_gated_delta_net.cpp b/src/plugins/intel_gpu/src/graph/impls/ocl_v2/paged_gated_delta_net.cpp new file mode 100644 index 000000000000..a7f3d4f7e1f7 --- /dev/null +++ b/src/plugins/intel_gpu/src/graph/impls/ocl_v2/paged_gated_delta_net.cpp @@ -0,0 +1,245 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "paged_gated_delta_net.hpp" + +#include + +#include "intel_gpu/primitives/paged_gated_delta_net.hpp" +#include "primitive_ocl_base.hpp" +#include "utils/kernel_generator.hpp" + +namespace ov::intel_gpu::ocl { +namespace { + +size_t get_v_block_size(size_t v_head_dims) { + // v_block_size is determined by GRF size and performance data + return 4; +} + +size_t get_subgroup_size(gpu_arch arch) { + switch (arch) { + case gpu_arch::gen9: + case gpu_arch::gen11: + case gpu_arch::xe_lp: + case gpu_arch::xe_hp: + case gpu_arch::xe_hpg: + return 8; + case gpu_arch::xe_hpc: + case gpu_arch::xe2: + case gpu_arch::xe3: + default: + return 16; + } +} + +size_t get_vec_size(const RuntimeParams& params) { + const auto& q_layout = params.get_input_layout(paged_gated_delta_net::QUERY); + const auto& q_shape = q_layout.get_partial_shape(); + const auto& v_shape = params.get_input_layout(paged_gated_delta_net::VALUE).get_partial_shape(); + + const size_t k_head_dims = q_shape[2].get_length(); + const size_t v_head_dims = v_shape[2].get_length(); + const size_t subgroup_size = get_subgroup_size(params.get_device_info().arch); + + if ((k_head_dims % 16) != 0 || (v_head_dims % 16) != 0) { + return 1; + } + + size_t vec_size = 1; + switch (q_layout.data_type) { + case ov::element::f16: + vec_size = 8; + break; + case ov::element::f32: + vec_size = 8; + break; + default: + vec_size = 1; + break; + } + + while (vec_size > 1 && ((k_head_dims % (subgroup_size * vec_size)) != 0)) { + vec_size /= 2; + } + + return vec_size; +} + +class PagedGatedDeltaNetBaseGenerator : public KernelGenerator { +public: + PagedGatedDeltaNetBaseGenerator(std::string_view kernel_name, bool force_ref_path) : KernelGenerator(kernel_name), m_force_ref_path(force_ref_path) {} + +protected: + bool m_force_ref_path = false; + + [[nodiscard]] JitConstants get_jit_constants(const RuntimeParams& params) const override { + auto jit = KernelGenerator::get_jit_constants(params); + auto desc = params.typed_desc(); + + const auto& q_shape = params.get_input_layout(paged_gated_delta_net::QUERY).get_partial_shape(); + const auto& v_shape = params.get_input_layout(paged_gated_delta_net::VALUE).get_partial_shape(); + + const size_t k_head_nums = q_shape[1].get_length(); + const size_t k_head_dims = q_shape[2].get_length(); + const size_t v_head_nums = v_shape[1].get_length(); + const size_t v_head_dims = v_shape[2].get_length(); + const float scale_factor = 1.0f / std::sqrt(static_cast(k_head_dims)); + + jit.make("K_HEAD_NUM", k_head_nums); + jit.make("V_HEAD_NUM", v_head_nums); + jit.make("K_HEAD_DIM", k_head_dims); + jit.make("V_HEAD_DIM", v_head_dims); + jit.make("V_BLOCK_SIZE", get_v_block_size(v_head_dims)); + jit.make("SUBGROUP_SIZE", get_subgroup_size(params.get_device_info().arch)); + jit.make("K_VEC_SIZE", m_force_ref_path ? 1 : get_vec_size(params)); + jit.make("FUSE_QK_L2NORM", desc->use_qk_l2norm ? 1 : 0); + jit.make("Q_L2_NORM_EPS", desc->q_l2_norm_eps); + jit.make("K_L2_NORM_EPS", desc->k_l2_norm_eps); + jit.make("SCALE_FACTOR", scale_factor); + + return jit; + } + + [[nodiscard]] Arguments get_arguments_desc(const RuntimeParams& params) const override { + Arguments args; + + for (uint32_t i = 0; i < params.input_layouts.size(); i++) { + args.push_back({ArgumentDescriptor::Types::INPUT, i}); + } + + args.push_back({ArgumentDescriptor::Types::OUTPUT, 0}); + + for (size_t i = 0; i < 10; i++) { + args.push_back({ArgumentDescriptor::Types::SCALAR, static_cast(i)}); + } + + return args; + } + + [[nodiscard]] DispatchDataFunc get_dispatch_data_func() const override { + return DispatchDataFunc{[](const RuntimeParams& params, KernelData& kd, ImplRuntimeParams* rt_params) { + assert(!params.is_dynamic()); + auto& wgs = kd.params.workGroups; + const auto& v_shape = params.get_input_layout(paged_gated_delta_net::VALUE).get_partial_shape(); + const auto& seq_shape = params.get_input_layout(paged_gated_delta_net::SUBSEQUENCE_BEGINS).get_partial_shape(); + + const size_t sequences = seq_shape[0].get_length() > 0 ? seq_shape[0].get_length() - 1 : 0; + const size_t head_nums = v_shape[1].get_length(); + const size_t v_head_dims = v_shape[2].get_length(); + const size_t current_v_block_size = get_v_block_size(v_head_dims); + const size_t v_blocks = (v_head_dims + current_v_block_size - 1) / current_v_block_size; + const size_t subgroup_size = get_subgroup_size(params.get_device_info().arch); + + auto get_head_offset = [](const cldnn::layout& layout, size_t head_dim_idx) { + const auto& lower_pads = layout.data_padding._lower_size; + return lower_pads.size() > head_dim_idx ? lower_pads[head_dim_idx] : 0; + }; + + const auto& q_layout = params.input_layouts[paged_gated_delta_net::QUERY]; + const auto& k_layout = params.input_layouts[paged_gated_delta_net::KEY]; + const auto& v_layout = params.input_layouts[paged_gated_delta_net::VALUE]; + auto read_pitch = [](const cldnn::layout& layout, size_t idx) -> int32_t { + const auto& pitches = layout.get_pitches(); + if (idx < pitches.size()) + return static_cast(pitches[idx]); + return 1; + }; + + const int32_t q_token_stride = read_pitch(q_layout, 0); + const int32_t q_head_stride = read_pitch(q_layout, 1); + const int32_t k_token_stride = read_pitch(k_layout, 0); + const int32_t k_head_stride = read_pitch(k_layout, 1); + const int32_t v_token_stride = read_pitch(v_layout, 0); + const int32_t v_head_stride = read_pitch(v_layout, 1); + + const int32_t query_head_offset = static_cast(get_head_offset(q_layout, 1)); + const int32_t key_head_offset = static_cast(get_head_offset(k_layout, 1)); + const int32_t value_head_offset = static_cast(get_head_offset(v_layout, 1)); + + wgs.global = {sequences, head_nums, v_blocks * subgroup_size}; + wgs.local = {1, 1, subgroup_size}; + + kd.params.scalars.clear(); + std::vector scalars{ + static_cast(sequences), + query_head_offset, + key_head_offset, + value_head_offset, + q_token_stride, + q_head_stride, + k_token_stride, + k_head_stride, + v_token_stride, + v_head_stride, + }; + + for (auto v : scalars) { + scalar_desc desc; + desc.t = scalar_desc::Types::INT32; + desc.v.s32 = v; + kd.params.scalars.push_back(desc); + } + }}; + } +}; + +class PagedGatedDeltaNetRefGenerator : public PagedGatedDeltaNetBaseGenerator { +public: + PagedGatedDeltaNetRefGenerator() : PagedGatedDeltaNetBaseGenerator("paged_gated_delta_net_ref", true) {} +}; + +class PagedGatedDeltaNetOptGenerator : public PagedGatedDeltaNetBaseGenerator { +public: + PagedGatedDeltaNetOptGenerator() : PagedGatedDeltaNetBaseGenerator("paged_gated_delta_net_opt", false) {} +}; + +class PagedGatedDeltaNetRefImpl : public PrimitiveImplOCL { +public: + DECLARE_OBJECT_TYPE_SERIALIZATION(ov::intel_gpu::ocl::PagedGatedDeltaNetRefImpl) + + Stage::Ptr paged_gated_delta_net = make_stage(); + + PagedGatedDeltaNetRefImpl() : PrimitiveImplOCL(PagedGatedDeltaNetRef::get_type_info_static()) {} + PagedGatedDeltaNetRefImpl(const program_node& node, const RuntimeParams& params) : PagedGatedDeltaNetRefImpl() { + add_stage(paged_gated_delta_net, params); + } + + [[nodiscard]] std::unique_ptr clone() const override { + return make_deep_copy(this); + } +}; + +class PagedGatedDeltaNetOptImpl : public PrimitiveImplOCL { +public: + DECLARE_OBJECT_TYPE_SERIALIZATION(ov::intel_gpu::ocl::PagedGatedDeltaNetOptImpl) + + Stage::Ptr paged_gated_delta_net = make_stage(); + + PagedGatedDeltaNetOptImpl() : PrimitiveImplOCL(PagedGatedDeltaNetOpt::get_type_info_static()) {} + PagedGatedDeltaNetOptImpl(const program_node& node, const RuntimeParams& params) : PagedGatedDeltaNetOptImpl() { + add_stage(paged_gated_delta_net, params); + } + + [[nodiscard]] std::unique_ptr clone() const override { + return make_deep_copy(this); + } +}; + +} // namespace + +std::unique_ptr PagedGatedDeltaNetRef::create_impl(const program_node& node, const RuntimeParams& params) const { + assert(node.is_type()); + return std::make_unique(node, params); +} + +std::unique_ptr PagedGatedDeltaNetOpt::create_impl(const program_node& node, const RuntimeParams& params) const { + assert(node.is_type()); + return std::make_unique(node, params); +} + +} // namespace ov::intel_gpu::ocl + +BIND_BINARY_BUFFER_WITH_TYPE(ov::intel_gpu::ocl::PagedGatedDeltaNetRefImpl) +BIND_BINARY_BUFFER_WITH_TYPE(ov::intel_gpu::ocl::PagedGatedDeltaNetOptImpl) diff --git a/src/plugins/intel_gpu/src/graph/impls/ocl_v2/paged_gated_delta_net.hpp b/src/plugins/intel_gpu/src/graph/impls/ocl_v2/paged_gated_delta_net.hpp new file mode 100644 index 000000000000..1d857b7da673 --- /dev/null +++ b/src/plugins/intel_gpu/src/graph/impls/ocl_v2/paged_gated_delta_net.hpp @@ -0,0 +1,89 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#pragma once + +#include +#include +#include + +#include "paged_gated_delta_net_inst.h" +#include "program_node.h" +#include "registry/implementation_manager.hpp" + +using namespace cldnn; // TODO: Remove once namespaces are aligned + +namespace ov::intel_gpu::ocl { + +struct PagedGatedDeltaNetBase : public ImplementationManager { + explicit PagedGatedDeltaNetBase(shape_types shape_type, ValidateFunc vf = nullptr) : ImplementationManager(impl_types::ocl, shape_type, std::move(vf)) {} + + [[nodiscard]] bool validate_impl(const program_node& node) const override { + assert(node.is_type()); + static constexpr std::array supported_fmts = { + format::bfyx, + }; + + static constexpr std::array supported_real_types = { + ov::element::f16, + ov::element::f32, + }; + + for (size_t i = 0; i < node.get_dependencies().size(); i++) { + const auto& in_layout = node.get_input_layout(i); + if (!one_of(in_layout.format, supported_fmts)) { + return false; + } + + if (i <= paged_gated_delta_net::BETA) { + if (!one_of(in_layout.data_type, supported_real_types)) { + return false; + } + } else if (in_layout.data_type != ov::element::i32) { + return false; + } + } + + const auto& out_layout = node.get_output_layout(0); + if (!one_of(out_layout.format, supported_fmts) || !one_of(out_layout.data_type, supported_real_types)) { + return false; + } + const auto& q_shape = node.get_input_layout(paged_gated_delta_net::QUERY).get_partial_shape(); + const auto& v_shape = node.get_input_layout(paged_gated_delta_net::VALUE).get_partial_shape(); + if (q_shape.rank().is_dynamic() || v_shape.rank().is_dynamic() || q_shape.rank().get_length() < 3 || v_shape.rank().get_length() < 3 || + q_shape[2].is_dynamic() || v_shape[2].is_dynamic()) { + return false; + } + return validate_internal(node); + } + +protected: + virtual bool validate_internal(const program_node& node) const { + return true; + } +}; + +struct PagedGatedDeltaNetRef : public PagedGatedDeltaNetBase { + OV_GPU_PRIMITIVE_IMPL("ocl::paged_gated_delta_net::ref") + explicit PagedGatedDeltaNetRef(shape_types shape_type, ValidateFunc vf = nullptr) : PagedGatedDeltaNetBase(shape_type, std::move(vf)) {} + [[nodiscard]] std::unique_ptr create_impl(const program_node& node, const RuntimeParams& params) const override; +}; + +struct PagedGatedDeltaNetOpt : public PagedGatedDeltaNetBase { + OV_GPU_PRIMITIVE_IMPL("ocl::paged_gated_delta_net::opt") + explicit PagedGatedDeltaNetOpt(shape_types shape_type, ValidateFunc vf = nullptr) : PagedGatedDeltaNetBase(shape_type, std::move(vf)) {} + [[nodiscard]] std::unique_ptr create_impl(const program_node& node, const RuntimeParams& params) const override; + +protected: + bool validate_internal(const program_node& node) const override { + const auto& q_shape = node.get_input_layout(paged_gated_delta_net::QUERY).get_partial_shape(); + const auto& v_shape = node.get_input_layout(paged_gated_delta_net::VALUE).get_partial_shape(); + + const auto k_head_dims = q_shape[2].get_length(); + const auto v_head_dims = v_shape[2].get_length(); + return (k_head_dims % 16) == 0 && (v_head_dims % 16) == 0; + } +}; + +} // namespace ov::intel_gpu::ocl diff --git a/src/plugins/intel_gpu/src/graph/impls/ocl_v2/paged_gated_delta_net_opt.cl b/src/plugins/intel_gpu/src/graph/impls/ocl_v2/paged_gated_delta_net_opt.cl new file mode 100644 index 000000000000..1d48cff78b05 --- /dev/null +++ b/src/plugins/intel_gpu/src/graph/impls/ocl_v2/paged_gated_delta_net_opt.cl @@ -0,0 +1,278 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "include/batch_headers/common.cl" +#include "include/batch_headers/sub_group_block_read.cl" +#include "include/batch_headers/sub_group_block_write.cl" +#include "include/batch_headers/sub_group_shuffle.cl" + +#ifndef FUSE_QK_L2NORM +# define FUSE_QK_L2NORM 0 +#endif + +#ifndef Q_L2_NORM_EPS +# define Q_L2_NORM_EPS 1e-6f +#endif + +#ifndef K_L2_NORM_EPS +# define K_L2_NORM_EPS 1e-6f +#endif + +inline float FUNC(l2norm_scale)(float sum, float extra_scale, float eps) { + return rsqrt(sum + eps) * extra_scale; +} + +inline float FUNC(sum8)(float8 v) { + return v.s0 + v.s1 + v.s2 + v.s3 + v.s4 + v.s5 + v.s6 + v.s7; +} + +inline void FUNC(normalize_kq_128)(float8* b_k, float8* b_q) { +#if FUSE_QK_L2NORM + float k_sum = FUNC(sum8)((*b_k) * (*b_k)); + k_sum = sub_group_reduce_add(k_sum); + const float k_scale = FUNC(l2norm_scale)(k_sum, 1.0f, K_L2_NORM_EPS); + *b_k *= k_scale; + + float q_sum = FUNC(sum8)((*b_q) * (*b_q)); + q_sum = sub_group_reduce_add(q_sum); + const float q_scale = FUNC(l2norm_scale)(q_sum, SCALE_FACTOR, Q_L2_NORM_EPS); + *b_q *= q_scale; +#else + *b_q *= SCALE_FACTOR; +#endif +} + +#ifndef K_VEC_SIZE +# define K_VEC_SIZE 1 +#endif + +#if ((K_HEAD_DIM % 16) != 0) || ((V_HEAD_DIM % 16) != 0) +# error "paged_gated_delta_net_opt requires K_HEAD_DIM and V_HEAD_DIM divisible by 16" +#endif + +#define K_LANE_ELEMS (K_HEAD_DIM / SUBGROUP_SIZE) + +typedef MAKE_VECTOR_TYPE(float, K_VEC_SIZE) K_VEC_TYPE; +#if (K_VEC_SIZE == 1) +# define K_VEC_LOAD_Q(ptr, idx) convert_float(BLOCK_READN(INPUT0_TYPE, 1, (ptr), (idx))) +# define K_VEC_LOAD_K(ptr, idx) convert_float(BLOCK_READN(INPUT1_TYPE, 1, (ptr), (idx))) +# define K_VEC_LOAD_STATE(ptr, idx) convert_float(BLOCK_READN(INPUT3_TYPE, 1, (ptr), (idx))) +# define K_VEC_TO_STATE(vec) ((INPUT3_TYPE)(vec)) +# define K_VEC_DOT(a, b) ((a) * (b)) +# define K_VEC_SUM_SQ(a) ((a) * (a)) +#elif (K_VEC_SIZE == 8) +# define K_VEC_LOAD_Q(ptr, idx) convert_float8(BLOCK_READN(INPUT0_TYPE, 8, (ptr), (idx))) +# define K_VEC_LOAD_K(ptr, idx) convert_float8(BLOCK_READN(INPUT1_TYPE, 8, (ptr), (idx))) +# define K_VEC_LOAD_STATE(ptr, idx) convert_float8(BLOCK_READN(INPUT3_TYPE, 8, (ptr), (idx))) +# define K_VEC_TO_STATE(vec) CAT(convert_, CAT(INPUT3_TYPE, 8))(vec) +# define K_VEC_DOT(a, b) FUNC(sum8)((a) * (b)) +# define K_VEC_SUM_SQ(a) FUNC(sum8)((a) * (a)) +#else +# define K_VEC_LOAD_Q(ptr, idx) CAT(convert_float, K_VEC_SIZE)(BLOCK_READN(INPUT0_TYPE, K_VEC_SIZE, (ptr), (idx))) +# define K_VEC_LOAD_K(ptr, idx) CAT(convert_float, K_VEC_SIZE)(BLOCK_READN(INPUT1_TYPE, K_VEC_SIZE, (ptr), (idx))) +# define K_VEC_LOAD_STATE(ptr, idx) CAT(convert_float, K_VEC_SIZE)(BLOCK_READN(INPUT3_TYPE, K_VEC_SIZE, (ptr), (idx))) +# define K_VEC_TO_STATE(vec) CAT(convert_, CAT(INPUT3_TYPE, K_VEC_SIZE))(vec) +# define K_VEC_DOT(a, b) dot((a), (b)) +# define K_VEC_SUM_SQ(a) dot((a), (a)) +#endif + +#define K_VEC_COUNT (K_LANE_ELEMS / K_VEC_SIZE) + +REQD_SUB_GROUP_SIZE(SUBGROUP_SIZE) +KERNEL(paged_gated_delta_net_opt) +(__global INPUT0_TYPE* query, + __global INPUT1_TYPE* key, + __global INPUT2_TYPE* value, + __global INPUT3_TYPE* recurrent_state_table, + __global INPUT4_TYPE* gate, + __global INPUT5_TYPE* beta, + __global INPUT6_TYPE* subsequence_begins, + __global INPUT7_TYPE* block_indices, + __global INPUT8_TYPE* block_indices_begins, + __global INPUT9_TYPE* past_lens, + __global INPUT10_TYPE* cache_interval, + __global OUTPUT_TYPE* output, + int num_sequences, + int query_head_offset, + int key_head_offset, + int value_head_offset, + int q_token_stride, + int q_head_stride, + int k_token_stride, + int k_head_stride, + int v_token_stride, + int v_head_stride) { + const int seq = get_global_id(0); + const int h = get_global_id(1); + const int v_block = get_group_id(2); + const int lid = get_sub_group_local_id(); + const int start_iv = v_block * V_BLOCK_SIZE; + + const int token_begin = subsequence_begins[seq]; + const int token_end = subsequence_begins[seq + 1]; + const int block_begin = block_indices_begins[seq]; + const int past_len = past_lens[seq]; + const int interval = cache_interval[seq]; + const int prev_nums = interval > 0 ? past_len % interval : 0; + + const int group_size = V_HEAD_NUM / K_HEAD_NUM; + const int hk = h / group_size; + const int q_head_base = (hk + query_head_offset) * q_head_stride; + const int k_head_base = (hk + key_head_offset) * k_head_stride; + const int v_head_base = (h + value_head_offset) * v_head_stride; + const int state_stride = K_HEAD_DIM * V_HEAD_DIM; + + K_VEC_TYPE state[V_BLOCK_SIZE][K_VEC_COUNT]; + K_VEC_TYPE q_norm[K_VEC_COUNT]; + K_VEC_TYPE k_norm[K_VEC_COUNT]; + + const int initial_block_id = block_indices[block_begin]; + const int initial_block_base = (initial_block_id * V_HEAD_NUM + h) * state_stride; + +#pragma unroll + for (int v_idx = 0; v_idx < V_BLOCK_SIZE; v_idx++) { + int curr_iv = start_iv + v_idx; + int base = initial_block_base + curr_iv * K_HEAD_DIM; +#if (K_VEC_SIZE == 8) && (K_VEC_COUNT == 1) + state[v_idx][0] = K_VEC_LOAD_STATE(recurrent_state_table, base); +#else +# pragma unroll + for (int kc = 0; kc < K_VEC_COUNT; kc++) { + const int k_base = kc * K_VEC_SIZE * SUBGROUP_SIZE; + state[v_idx][kc] = K_VEC_LOAD_STATE(recurrent_state_table, base + k_base); + } +#endif + } + + int token = token_begin; + int slot = 1; + int tokens_to_next_boundary = interval > 0 ? (prev_nums > 0 ? (interval - prev_nums) : interval) : (token_end - token_begin); + while (token < token_end) { + const int chunk_end = min(token + tokens_to_next_boundary, token_end); + + int q_base = token * q_token_stride + q_head_base; + int k_base = token * k_token_stride + k_head_base; + int v_base = token * v_token_stride + v_head_base; + int g_idx = token * V_HEAD_NUM + h; + + for (; token < chunk_end; token++, q_base += q_token_stride, k_base += k_token_stride, v_base += v_token_stride, g_idx += V_HEAD_NUM) { +#if (K_VEC_SIZE == 8) && (K_VEC_COUNT == 1) + q_norm[0] = K_VEC_LOAD_Q(query, q_base); + k_norm[0] = K_VEC_LOAD_K(key, k_base); + FUNC(normalize_kq_128)(&k_norm[0], &q_norm[0]); +#else + float q_sum_local = 0.0f; + float k_sum_local = 0.0f; +# pragma unroll + for (int kc = 0; kc < K_VEC_COUNT; kc++) { + const int offset = kc * K_VEC_SIZE * SUBGROUP_SIZE; + q_norm[kc] = K_VEC_LOAD_Q(query, q_base + offset); + k_norm[kc] = K_VEC_LOAD_K(key, k_base + offset); + q_sum_local += K_VEC_SUM_SQ(q_norm[kc]); + k_sum_local += K_VEC_SUM_SQ(k_norm[kc]); + } + + float q_scale = SCALE_FACTOR; + float k_scale = 1.0f; +# if FUSE_QK_L2NORM + const float q_sum = sub_group_reduce_add(q_sum_local); + const float k_sum = sub_group_reduce_add(k_sum_local); + + q_scale = FUNC(l2norm_scale)(q_sum, SCALE_FACTOR, Q_L2_NORM_EPS); + k_scale = FUNC(l2norm_scale)(k_sum, 1.0f, K_L2_NORM_EPS); +# endif +# pragma unroll + for (int kc = 0; kc < K_VEC_COUNT; kc++) { + q_norm[kc] *= q_scale; + k_norm[kc] *= k_scale; + } +#endif + + const float b_g = exp(convert_float(gate[g_idx])); + const float b_beta = convert_float(beta[g_idx]); + + float b_v_block[V_BLOCK_SIZE]; + float h_k_block[V_BLOCK_SIZE]; + float update_block[V_BLOCK_SIZE]; + float out_block[V_BLOCK_SIZE]; +#pragma unroll + for (int v_idx = 0; v_idx < V_BLOCK_SIZE; v_idx++) { + const int curr_iv = start_iv + v_idx; + const int v_base_aligned = v_base + (curr_iv & ~(SUBGROUP_SIZE - 1)); + const int v_lane = curr_iv & (SUBGROUP_SIZE - 1); + const float v_val = convert_float(BLOCK_READN(INPUT2_TYPE, 1, value, v_base_aligned)); + b_v_block[v_idx] = sub_group_broadcast(v_val, v_lane); + } + +#pragma unroll + for (int v_idx = 0; v_idx < V_BLOCK_SIZE; v_idx++) { + float h_k_local = 0.0f; +#if (K_VEC_SIZE == 8) && (K_VEC_COUNT == 1) + state[v_idx][0] *= b_g; + h_k_local = FUNC(sum8)(state[v_idx][0] * k_norm[0]); +#else +# pragma unroll + for (int kc = 0; kc < K_VEC_COUNT; kc++) { + state[v_idx][kc] *= b_g; + h_k_local += K_VEC_DOT(state[v_idx][kc], k_norm[kc]); + } +#endif + h_k_block[v_idx] = sub_group_reduce_add(h_k_local); + } + +#pragma unroll + for (int v_idx = 0; v_idx < V_BLOCK_SIZE; v_idx++) { + update_block[v_idx] = (b_v_block[v_idx] - h_k_block[v_idx]) * b_beta; + } + +#pragma unroll + for (int v_idx = 0; v_idx < V_BLOCK_SIZE; v_idx++) { + float out_val_local = 0.0f; +#if (K_VEC_SIZE == 8) && (K_VEC_COUNT == 1) + state[v_idx][0] = fma(k_norm[0], update_block[v_idx], state[v_idx][0]); + out_val_local = FUNC(sum8)(state[v_idx][0] * q_norm[0]); +#else +# pragma unroll + for (int kc = 0; kc < K_VEC_COUNT; kc++) { + state[v_idx][kc] = fma(k_norm[kc], update_block[v_idx], state[v_idx][kc]); + out_val_local += K_VEC_DOT(state[v_idx][kc], q_norm[kc]); + } +#endif + out_block[v_idx] = sub_group_reduce_add(out_val_local); + } + +#pragma unroll + for (int v_idx = 0; v_idx < V_BLOCK_SIZE; v_idx++) { + int curr_iv = start_iv + v_idx; + const float out_val = out_block[v_idx]; + + if (lid == 0) { + const int out_offset = (token * V_HEAD_NUM + h) * V_HEAD_DIM + curr_iv; + output[out_offset] = TO_OUTPUT_TYPE(out_val); + } + } + } + + const int block_id = block_indices[block_begin + slot]; + slot++; + const int block_base = (block_id * V_HEAD_NUM + h) * state_stride; +#pragma unroll + for (int v_idx = 0; v_idx < V_BLOCK_SIZE; v_idx++) { + int curr_iv = start_iv + v_idx; + int base = block_base + curr_iv * K_HEAD_DIM; +#if (K_VEC_SIZE == 8) && (K_VEC_COUNT == 1) + BLOCK_WRITEN(INPUT3_TYPE, K_VEC_SIZE, recurrent_state_table, base, K_VEC_TO_STATE(state[v_idx][0])); +#else +# pragma unroll + for (int kc = 0; kc < K_VEC_COUNT; kc++) { + const int k_base = kc * K_VEC_SIZE * SUBGROUP_SIZE; + BLOCK_WRITEN(INPUT3_TYPE, K_VEC_SIZE, recurrent_state_table, base + k_base, K_VEC_TO_STATE(state[v_idx][kc])); + } +#endif + } + + if (interval > 0) + tokens_to_next_boundary = interval; + } +} diff --git a/src/plugins/intel_gpu/src/graph/impls/ocl_v2/paged_gated_delta_net_ref.cl b/src/plugins/intel_gpu/src/graph/impls/ocl_v2/paged_gated_delta_net_ref.cl new file mode 100644 index 000000000000..c7357fbfb07f --- /dev/null +++ b/src/plugins/intel_gpu/src/graph/impls/ocl_v2/paged_gated_delta_net_ref.cl @@ -0,0 +1,191 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "include/batch_headers/common.cl" +#include "include/batch_headers/sub_group_block_read.cl" +#include "include/batch_headers/sub_group_block_write.cl" +#include "include/batch_headers/sub_group_shuffle.cl" + +#ifndef FUSE_QK_L2NORM +# define FUSE_QK_L2NORM 0 +#endif + +#ifndef Q_L2_NORM_EPS +# define Q_L2_NORM_EPS 1e-6f +#endif + +#ifndef K_L2_NORM_EPS +# define K_L2_NORM_EPS 1e-6f +#endif + +inline float FUNC(l2norm_scale)(float sum, float extra_scale, float eps) { + return rsqrt(sum + eps) * extra_scale; +} + +#define K_SLICE_SIZE ((K_HEAD_DIM + SUBGROUP_SIZE - 1) / SUBGROUP_SIZE) + +REQD_SUB_GROUP_SIZE(SUBGROUP_SIZE) +KERNEL(paged_gated_delta_net_ref) +(__global INPUT0_TYPE* query, + __global INPUT1_TYPE* key, + __global INPUT2_TYPE* value, + __global INPUT3_TYPE* recurrent_state_table, + __global INPUT4_TYPE* gate, + __global INPUT5_TYPE* beta, + __global INPUT6_TYPE* subsequence_begins, + __global INPUT7_TYPE* block_indices, + __global INPUT8_TYPE* block_indices_begins, + __global INPUT9_TYPE* past_lens, + __global INPUT10_TYPE* cache_interval, + __global OUTPUT_TYPE* output, + int num_sequences, + int query_head_offset, + int key_head_offset, + int value_head_offset, + int q_token_stride, + int q_head_stride, + int k_token_stride, + int k_head_stride, + int v_token_stride, + int v_head_stride) { + const int seq = get_global_id(0); + const int h = get_global_id(1); + const int v_block = get_group_id(2); + const int lid = get_sub_group_local_id(); + const int start_iv = v_block * V_BLOCK_SIZE; + + const int token_begin = subsequence_begins[seq]; + const int token_end = subsequence_begins[seq + 1]; + const int block_begin = block_indices_begins[seq]; + const int past_len = past_lens[seq]; + const int interval = cache_interval[seq]; + const int prev_nums = interval > 0 ? past_len % interval : 0; + + const int group_size = V_HEAD_NUM / K_HEAD_NUM; + const int hk = h / group_size; + const int q_head_base = (hk + query_head_offset) * q_head_stride; + const int k_head_base = (hk + key_head_offset) * k_head_stride; + const int v_head_base = (h + value_head_offset) * v_head_stride; + const int state_stride = K_HEAD_DIM * V_HEAD_DIM; + + float state[V_BLOCK_SIZE][K_SLICE_SIZE]; + float q_norm[K_SLICE_SIZE]; + float k_norm[K_SLICE_SIZE]; + + const int initial_block_id = block_indices[block_begin]; + const int initial_block_base = (initial_block_id * V_HEAD_NUM + h) * state_stride; + +#pragma unroll + for (int v_idx = 0; v_idx < V_BLOCK_SIZE; v_idx++) { + int curr_iv = start_iv + v_idx; + if (curr_iv >= V_HEAD_DIM) + continue; + + int base = initial_block_base + curr_iv * K_HEAD_DIM; + for (int ks = 0; ks < K_SLICE_SIZE; ks++) { + const int k_idx = lid + ks * SUBGROUP_SIZE; + if (k_idx < K_HEAD_DIM) { + state[v_idx][ks] = convert_float(recurrent_state_table[base + k_idx]); + } + } + } + + int token = token_begin; + int slot = 1; + int tokens_to_next_boundary = interval > 0 ? (prev_nums > 0 ? (interval - prev_nums) : interval) : (token_end - token_begin); + while (token < token_end) { + const int chunk_end = min(token + tokens_to_next_boundary, token_end); + + int q_base = token * q_token_stride + q_head_base; + int k_base = token * k_token_stride + k_head_base; + int v_base = token * v_token_stride + v_head_base; + int g_idx = token * V_HEAD_NUM + h; + + for (; token < chunk_end; token++, q_base += q_token_stride, k_base += k_token_stride, v_base += v_token_stride, g_idx += V_HEAD_NUM) { + float q_sum_local = 0.0f; + float k_sum_local = 0.0f; + for (int ks = 0; ks < K_SLICE_SIZE; ks++) { + const int k_idx = lid + ks * SUBGROUP_SIZE; + if (k_idx < K_HEAD_DIM) { + const float q_val = convert_float(query[q_base + k_idx]); + const float k_val = convert_float(key[k_base + k_idx]); + q_norm[ks] = q_val; + k_norm[ks] = k_val; + q_sum_local = fma(q_val, q_val, q_sum_local); + k_sum_local = fma(k_val, k_val, k_sum_local); + } else { + q_norm[ks] = 0.0f; + k_norm[ks] = 0.0f; + } + } + + float q_scale = SCALE_FACTOR; + float k_scale = 1.0f; +#if FUSE_QK_L2NORM + const float q_sum = sub_group_reduce_add(q_sum_local); + const float k_sum = sub_group_reduce_add(k_sum_local); + + q_scale = FUNC(l2norm_scale)(q_sum, SCALE_FACTOR, Q_L2_NORM_EPS); + k_scale = FUNC(l2norm_scale)(k_sum, 1.0f, K_L2_NORM_EPS); +#endif + for (int ks = 0; ks < K_SLICE_SIZE; ks++) { + q_norm[ks] *= q_scale; + k_norm[ks] *= k_scale; + } + + const float b_g = exp(convert_float(gate[g_idx])); + const float b_beta = convert_float(beta[g_idx]); + +#pragma unroll + for (int v_idx = 0; v_idx < V_BLOCK_SIZE; v_idx++) { + int curr_iv = start_iv + v_idx; + if (curr_iv >= V_HEAD_DIM) + continue; + float h_k_local = 0.0f; + for (int ks = 0; ks < K_SLICE_SIZE; ks++) { + state[v_idx][ks] *= b_g; + h_k_local = fma(state[v_idx][ks], k_norm[ks], h_k_local); + } + + const float h_k = sub_group_reduce_add(h_k_local); + const int v_idx_offset = v_base + curr_iv; + const float b_v = convert_float(value[v_idx_offset]); + const float update_val = (b_v - h_k) * b_beta; + + float out_val_local = 0.0f; + for (int ks = 0; ks < K_SLICE_SIZE; ks++) { + state[v_idx][ks] = fma(k_norm[ks], update_val, state[v_idx][ks]); + out_val_local = fma(state[v_idx][ks], q_norm[ks], out_val_local); + } + const float out_val = sub_group_reduce_add(out_val_local); + + if (lid == 0) { + const int out_offset = (token * V_HEAD_NUM + h) * V_HEAD_DIM + curr_iv; + output[out_offset] = TO_OUTPUT_TYPE(out_val); + } + } + } + + const int block_id = block_indices[block_begin + slot]; + slot++; + const int block_base = (block_id * V_HEAD_NUM + h) * state_stride; +#pragma unroll + for (int v_idx = 0; v_idx < V_BLOCK_SIZE; v_idx++) { + int curr_iv = start_iv + v_idx; + if (curr_iv >= V_HEAD_DIM) + continue; + + int base = block_base + curr_iv * K_HEAD_DIM; + for (int ks = 0; ks < K_SLICE_SIZE; ks++) { + const int k_idx = lid + ks * SUBGROUP_SIZE; + if (k_idx < K_HEAD_DIM) { + recurrent_state_table[base + k_idx] = (INPUT3_TYPE)(state[v_idx][ks]); + } + } + } + + if (interval > 0) + tokens_to_next_boundary = interval; + } +} diff --git a/src/plugins/intel_gpu/src/graph/impls/ocl_v2/sdpa/paged_attention_opt.cpp b/src/plugins/intel_gpu/src/graph/impls/ocl_v2/sdpa/paged_attention_opt.cpp index b1eeaa47effc..65cfbb46786e 100644 --- a/src/plugins/intel_gpu/src/graph/impls/ocl_v2/sdpa/paged_attention_opt.cpp +++ b/src/plugins/intel_gpu/src/graph/impls/ocl_v2/sdpa/paged_attention_opt.cpp @@ -51,7 +51,7 @@ static size_t get_pa_sg_number_scale_factor(const device_info& info, size_t head if (is_kv_compressed) { const size_t optimal_scale_factor = 2; if (kernel_type == SDPAStage::SINGLE_TOKEN || kernel_type == SDPAStage::MULTI_TOKENS) { - if (head_size * optimal_scale_factor <= info.max_work_group_size) { + if (head_size * optimal_scale_factor < info.max_work_group_size) { return optimal_scale_factor; } } @@ -134,14 +134,14 @@ static int64_t get_aligned_seq_len(const kernel_impl_params& impl_param, const P const auto subsequence_begins_mem = input_mem.at(PagedAttentionInputIdx::SUBSEQUENCE_BEGINS); mem_lock subsequence_begins_mem_lock(subsequence_begins_mem, *impl_param.strm); - auto aligned_seq_len = 0; + int64_t aligned_seq_len = 0; if (stage == PagedAttentionStage::MIXED) { const auto past_lens_mem = input_mem.at(PagedAttentionInputIdx::PAST_LENS); mem_lock past_lens_mem_lock(past_lens_mem, *impl_param.strm); for (size_t i = 0; i < subsequence_begins_mem_lock.size() - 1; i++) { auto past_len = past_lens_mem_lock[i]; - auto seq_length = subsequence_begins_mem_lock[i + 1] - subsequence_begins_mem_lock[i]; + int64_t seq_length = subsequence_begins_mem_lock[i + 1] - subsequence_begins_mem_lock[i]; // Since in MIXED execution mode the present KV-cache can be appended to the past KV-cache at any offset inside block, // to ensure proper alignment and update_kv_cache kernel scheduling, we need to account for the number of unaligned tokens @@ -242,21 +242,27 @@ JitConstants make_uint4_kv_cache_jit_constants(const kernel_impl_params& params) ov::intel_gpu::JitConstants jit; const auto desc = params.typed_desc(); const auto kv_cache_dt = params.get_program().get_config().get_kv_cache_precision(); - const auto is_key_by_channel = desc->is_key_by_channel; auto& kv_dt = params.input_layouts[PagedAttentionInputIdx::KEY].data_type; if (data_type_traits::is_i4_u4(kv_cache_dt)) { - // INT4 compression is packing elements along groups in head which has different scalea and zp - const auto scales_zp_size = get_element_size(kv_dt) * 4; + const auto scales_zp_size = get_element_size(kv_dt) * 2; // fp16 scale + fp16 zp = 4 bytes + const auto is_key_by_channel = desc->is_key_by_channel; jit.make("IS_INT4_COMPRESSED", true); - jit.make("PACKED_K_HEAD_SIZE", kernel_selector::Align(desc->k_head_size / u4_elems_per_byte, subgroup_size)); - jit.make("PACKED_ADJUSTED_V_HEAD_SIZE", (kernel_selector::Align(desc->v_head_size / u4_elems_per_byte, subgroup_size)) + scales_zp_size); - jit.make("PACKED_V_HEAD_SIZE", (kernel_selector::Align(desc->v_head_size / u4_elems_per_byte, subgroup_size))); if (is_key_by_channel) { - jit.make("PACKED_ADJUSTED_K_HEAD_SIZE", (kernel_selector::Align(desc->k_head_size / u4_elems_per_byte, subgroup_size))); + // K BY_CHANNEL: head_size is NOT packed (outer dim), block_size IS packed (inner dim) + jit.make("PACKED_K_HEAD_SIZE", desc->k_head_size); + jit.make("PACKED_ADJUSTED_K_HEAD_SIZE", desc->k_head_size); + jit.make("PACKED_K_BLOCK_SIZE", paged_attention_block_size / u4_elems_per_byte); } else { - jit.make("PACKED_ADJUSTED_K_HEAD_SIZE", (kernel_selector::Align(desc->k_head_size / u4_elems_per_byte, subgroup_size)) + scales_zp_size); + // K BY_TOKEN: head_size IS packed (inner dim), block_size is NOT packed + const auto packed_k_head_size = kernel_selector::Align(desc->k_head_size / u4_elems_per_byte, subgroup_size); + jit.make("PACKED_K_HEAD_SIZE", packed_k_head_size); + jit.make("PACKED_ADJUSTED_K_HEAD_SIZE", packed_k_head_size + scales_zp_size); + jit.make("PACKED_K_BLOCK_SIZE", paged_attention_block_size); } + // V per-token: head_size IS packed (inner dim) + jit.make("PACKED_V_HEAD_SIZE", (kernel_selector::Align(desc->v_head_size / u4_elems_per_byte, subgroup_size))); + jit.make("PACKED_ADJUSTED_V_HEAD_SIZE", (kernel_selector::Align(desc->v_head_size / u4_elems_per_byte, subgroup_size)) + scales_zp_size); } else { jit.make("IS_INT4_COMPRESSED", false); } @@ -292,21 +298,32 @@ class PagedAttentionGeneratorBase : public KernelGenerator { if (is_kv_compressed) { auto& kv_dt = params.input_layouts[PagedAttentionInputIdx::KEY].data_type; auto scales_zp_size = get_element_size(kv_dt) * 2; // scale + zp - if (data_type_traits::is_i4_u4(kv_cache_dt)) - scales_zp_size = get_element_size(kv_dt) * 4; jit.make("SCALE_ZP_SIZE_PER_TOKEN", scales_zp_size); jit.add(make_uint4_kv_cache_jit_constants(params)); - if (is_key_by_channel) { + if (data_type_traits::is_i4_u4(kv_cache_dt)) { + if (is_key_by_channel) { + // INT4 BY_CHANNEL: K dim order {0,1,3,2}, block_size packed in innermost dim + jit.make("IS_KEY_BY_CHANNEL", 1); + jit.make("ADJUSTED_K_HEAD_SIZE", desc->k_head_size); + jit.make("ADJUSTED_PAGED_ATTENTION_BLOCK_SIZE", paged_attention_block_size / u4_elems_per_byte + scales_zp_size); + } else { + // INT4 BY_TOKEN: K dim order {0,1,2,3}, head_size packed + jit.make("ADJUSTED_K_HEAD_SIZE", desc->k_head_size + scales_zp_size); + jit.make("ADJUSTED_PAGED_ATTENTION_BLOCK_SIZE", paged_attention_block_size); + } + jit.make("ADJUSTED_V_HEAD_SIZE", desc->v_head_size + scales_zp_size); + } else if (is_key_by_channel) { jit.make("IS_KEY_BY_CHANNEL", 1); jit.make("ADJUSTED_K_HEAD_SIZE", desc->k_head_size); jit.make("ADJUSTED_PAGED_ATTENTION_BLOCK_SIZE", paged_attention_block_size + scales_zp_size); + jit.make("ADJUSTED_V_HEAD_SIZE", desc->v_head_size + scales_zp_size); } else { jit.make("ADJUSTED_K_HEAD_SIZE", desc->k_head_size + scales_zp_size); jit.make("ADJUSTED_PAGED_ATTENTION_BLOCK_SIZE", paged_attention_block_size); + jit.make("ADJUSTED_V_HEAD_SIZE", desc->v_head_size + scales_zp_size); } - jit.make("ADJUSTED_V_HEAD_SIZE", desc->v_head_size + scales_zp_size); } else { jit.make("ADJUSTED_K_HEAD_SIZE", desc->k_head_size); jit.make("ADJUSTED_V_HEAD_SIZE", desc->v_head_size); @@ -949,13 +966,23 @@ class KVCacheUpdateGenerator : public KernelGenerator { if (is_kv_compressed) { auto data_type = params.input_layouts[PagedAttentionInputIdx::KEY].data_type; // key tensor data size auto scales_zp_size = get_element_size(data_type) * 2; // scale + zp - if (data_type_traits::is_i4_u4(kv_cache_dt)) - scales_zp_size = get_element_size(data_type) * 4; jit.make("SCALE_ZP_SIZE_PER_TOKEN", scales_zp_size); jit.add(make_uint4_kv_cache_jit_constants(params)); - if (is_key_by_channel) { + if (data_type_traits::is_i4_u4(kv_cache_dt)) { + if (is_key_by_channel) { + // INT4 BY_CHANNEL: K dim order {0,1,3,2}, block_size packed in innermost dim + jit.make("IS_KEY_BY_CHANNEL", 1); + jit.make("ADJUSTED_K_HEAD_SIZE", desc->k_head_size); + jit.make("ADJUSTED_PAGED_ATTENTION_BLOCK_SIZE", paged_attention_block_size / u4_elems_per_byte + scales_zp_size); + } else { + // INT4 BY_TOKEN: K dim order {0,1,2,3}, head_size packed + jit.make("ADJUSTED_K_HEAD_SIZE", desc->k_head_size + scales_zp_size); + jit.make("ADJUSTED_PAGED_ATTENTION_BLOCK_SIZE", paged_attention_block_size); + } + jit.make("NUM_K_HEAD_SIZE_PARTITIONS", get_num_k_head_size_partitions(desc->is_key_by_channel, desc->k_head_size)); + } else if (is_key_by_channel) { jit.make("IS_KEY_BY_CHANNEL", 1); jit.make("ADJUSTED_K_HEAD_SIZE", desc->k_head_size); jit.make("ADJUSTED_PAGED_ATTENTION_BLOCK_SIZE", paged_attention_block_size + scales_zp_size); @@ -1023,13 +1050,7 @@ class KVCacheUpdateGenerator : public KernelGenerator { const auto& key_input = params.input_layouts[0]; const auto sequences_number = key_input.get_partial_shape()[0].get_length(); size_t head_size_partition = get_num_k_head_size_partitions(desc->is_key_by_channel, desc->k_head_size); - const auto kv_cache_dt = params.get_program().get_config().get_kv_cache_precision(); - if (data_type_traits::is_i4_u4(kv_cache_dt) && head_size_partition > 1) - wgs.global = {static_cast(sequences_number), - heads_number, - kernel_selector::Align(subgroup_size * head_size_partition / u4_elems_per_byte, subgroup_size)}; - else - wgs.global = {static_cast(sequences_number), heads_number, subgroup_size * head_size_partition}; + wgs.global = {static_cast(sequences_number), heads_number, subgroup_size * head_size_partition}; wgs.local = {1, 1, subgroup_size}; } @@ -1077,12 +1098,16 @@ class KVCacheRotateGenerator : public KernelGenerator { jit.add(make_uint4_kv_cache_jit_constants(params)); auto scales_zp_size = get_element_size(original_cache_dt) * 2; // scale + zp; const auto kv_cache_dt = params.get_program().get_config().get_kv_cache_precision(); - if (data_type_traits::is_i4_u4(kv_cache_dt)) { - scales_zp_size = get_element_size(original_cache_dt) * 4; - } jit.make("SCALE_ZP_SIZE_PER_TOKEN", scales_zp_size); - if (is_key_by_channel) { + if (data_type_traits::is_i4_u4(kv_cache_dt)) { + // INT4 BY_CHANNEL: dim order {0,1,2,3}, scales embedded per-token in head dim + jit.make("IS_KEY_BY_CHANNEL", 1); + jit.make("ADJUSTED_HEAD_SIZE", desc->k_head_size); + jit.make("ADJUSTED_PAGED_ATTENTION_BLOCK_SIZE", paged_attention_block_size); + jit.make("NUM_K_HEAD_SIZE_PARTITIONS", get_num_k_head_size_partitions(desc->is_key_by_channel, desc->k_head_size)); + jit.make("ADJUSTED_V_HEAD_SIZE", desc->v_head_size + scales_zp_size); + } else if (is_key_by_channel) { jit.make("ADJUSTED_HEAD_SIZE", desc->k_head_size); jit.make("ADJUSTED_PAGED_ATTENTION_BLOCK_SIZE", paged_attention_block_size + scales_zp_size); } else { @@ -1160,6 +1185,10 @@ class PagedAttentionSDPAOptGeneratorMultiToken : public SDPAOptGeneratorBase { args.push_back({ArgumentDescriptor::Types::INPUT, PagedAttentionInputIdx::TOKEN_TYPE_IDS}); // token_type_ids } + if (desc->has_sink_input) { + args.push_back({ArgumentDescriptor::Types::INPUT, PagedAttentionInputIdx::SINKS}); // sink + } + args.push_back({ArgumentDescriptor::Types::OUTPUT, 0}); args.push_back({ArgumentDescriptor::Types::INTERNAL_BUFFER, 0}); @@ -1232,6 +1261,12 @@ class PagedAttentionSDPAOptGeneratorMultiToken : public SDPAOptGeneratorBase { jit.make("HAS_TOKEN_TYPE_IDS", 1); } + if (desc->has_sink_input) { + const auto& sink_layout = params.input_layouts[PagedAttentionInputIdx::SINKS]; + jit.make("SINK_DATA_T", to_ocl_type(sink_layout.data_type)); + jit.make("HAS_SINK_INPUT", 1); + } + return jit; } @@ -1324,17 +1359,22 @@ class PagedAttentionOptImpl : public SDPAImplBase { bool supports_micro_sdpa(const kernel_impl_params& params) const { auto& engine = params.get_program().get_engine(); + const auto desc = params.typed_desc(); if (params.get_device_info().supports_immad) { const auto supports_microkernels = cldnn::query_microkernels_supported(engine, params.get_program().get_config()); if (params.get_device_info().arch < gpu_arch::xe_hpg || !supports_microkernels) { return false; } + // WA: Disable micro SDPA on xe3p for head_size <= 64 due to oneDNN micro-kernel + // accuracy issues (produces inf/nan) after oneDNN main branch integration. + if (params.get_device_info().arch == gpu_arch::xe3p && desc->k_head_size <= 64) { + return false; + } } else { return false; } - const auto desc = params.typed_desc(); ov::Dimension head_num = desc->heads_num; ov::Dimension kv_heads_num = desc->kv_heads_num; @@ -1359,15 +1399,9 @@ class PagedAttentionOptImpl : public SDPAImplBase { return false; } - // micro_sdpa does not support 4-bit (u4/i4) KV-cache quantization. - // In MIXED stage, the prefill and generate iterations share the same kernel path; - // because the generate iteration must read the already-quantized KV-cache that was - // written during prefill, every kernel in this stage must be able to dequantize 4-bit - // entries. micro_sdpa currently lacks this dequantization logic, so we fall back to - // the standard paged-attention kernel (pa_multi_tokens / pa_single_token) which does - // support INT4 KV-cache. - if (data_type_traits::is_i4_u4(params.get_program().get_config().get_kv_cache_precision()) && - get_paged_attention_stage(params) == PagedAttentionStage::MIXED) { + // Disable micro SDPA for INT4 BY_TOKEN due to accuracy issues + const auto kv_cache_dt = params.get_program().get_config().get_kv_cache_precision(); + if (data_type_traits::is_i4_u4(kv_cache_dt) && !desc->is_key_by_channel) { return false; } @@ -1608,13 +1642,15 @@ class PagedAttentionOptImpl : public SDPAImplBase { } bool can_use_micro_sdpa = false; #ifdef ENABLE_ONEDNN_FOR_GPU - can_use_micro_sdpa = has_stage(pa_sdpa_micro) && valid_micro_stage(stage); - if (stage == PagedAttentionStage::GENERATE && (rt_params == nullptr || (rt_params != nullptr && rt_params->use_gqa_kernel == false))) - can_use_micro_sdpa = false; - - if (data_type_traits::is_i4_u4(params.get_program().get_config().get_kv_cache_precision()) && - get_paged_attention_stage(params) == PagedAttentionStage::MIXED) { - can_use_micro_sdpa = false; + // Keep internal buffer layout decision aligned with execute() path. + // If runtime params are already prepared, they are the source of truth. + if (rt_params != nullptr && rt_params->num_of_partitions != 0) { + can_use_micro_sdpa = rt_params->use_micro_sdpa; + } else { + can_use_micro_sdpa = supports_micro_sdpa(params) && valid_micro_stage(stage) && desc->has_token_type_ids == false; + if (stage == PagedAttentionStage::GENERATE) { + can_use_micro_sdpa = false; + } } #endif GPU_DEBUG_TRACE_DETAIL << "get_internal_buffer_descs: stage = " << static_cast(stage) << std::endl; @@ -1627,9 +1663,10 @@ class PagedAttentionOptImpl : public SDPAImplBase { const auto indexes_buf_size = static_cast(ceil_div(target_seq_len, target_seq_len_block_size)) * element_size; const bool lockable = true; - internal_buffers.emplace_back(indexes_buf_size, indexes_dt, lockable); // 0 - internal_buffers.emplace_back(indexes_buf_size, indexes_dt, lockable); // 1 - internal_buffers.emplace_back(indexes_buf_size, indexes_dt, lockable); // 2 + const bool not_shareable = false; + internal_buffers.emplace_back(indexes_buf_size, indexes_dt, lockable, not_shareable); // 0 + internal_buffers.emplace_back(indexes_buf_size, indexes_dt, lockable, not_shareable); // 1 + internal_buffers.emplace_back(indexes_buf_size, indexes_dt, lockable, not_shareable); // 2 const auto& input = params.input_layouts[0]; const int64_t total_tokens = input.get_partial_shape()[0].get_length(); @@ -1669,11 +1706,11 @@ class PagedAttentionOptImpl : public SDPAImplBase { // Softmax intermediate output internal_buffers.emplace_back(softmax_buf_elements_count, indexes_dt); // 3 // Precalculated accumulated sequence length offsets for each subsequence - internal_buffers.emplace_back(subsequences_number * element_size, indexes_dt, lockable); // 4 + internal_buffers.emplace_back(subsequences_number * element_size, indexes_dt, lockable, not_shareable); // 4 if (desc->has_score_aggregation) { // Cumulative window size sum buffer - internal_buffers.emplace_back((subsequences_number + 1) * element_size, indexes_dt, lockable); // 5 + internal_buffers.emplace_back((subsequences_number + 1) * element_size, indexes_dt, lockable, not_shareable); // 5 } if (stage == PagedAttentionStage::PREFILL) { @@ -1695,7 +1732,7 @@ class PagedAttentionOptImpl : public SDPAImplBase { const auto multi_tokens_mode = stage == PagedAttentionStage::MIXED; if (multi_tokens_mode && !can_use_micro_sdpa) { - internal_buffers.emplace_back(total_tokens, softmax_accumulator_type, lockable); // 9 + internal_buffers.emplace_back(total_tokens, softmax_accumulator_type, lockable, not_shareable); // 9 } #ifdef ENABLE_ONEDNN_FOR_GPU @@ -1703,7 +1740,7 @@ class PagedAttentionOptImpl : public SDPAImplBase { const auto wg_tile_q = 8; // This is set as the minimum size of query block for sharing between sdpa_micro_prefill and mixed. const auto target_seq_len = std::max(paged_attention_aligned_seq_len, static_cast(1)); const auto indexes_buf_size = ceil_div(target_seq_len, wg_tile_q) * 2; - internal_buffers.emplace_back(indexes_buf_size * 4, indexes_dt, lockable); + internal_buffers.emplace_back(indexes_buf_size * 4, indexes_dt, lockable, not_shareable); } #endif @@ -1726,8 +1763,8 @@ class PagedAttentionOptImpl : public SDPAImplBase { total_matrix_elements += evictable_size * evictable_size; total_vector_elements += evictable_size; } - GPU_DEBUG_TRACE_DETAIL << "Adaptive RKV: Allocating dynamic buffers - " - << "matrix: " << total_matrix_elements << ", vector: " << total_vector_elements << std::endl; + GPU_DEBUG_TRACE_DETAIL << "Adaptive RKV: Allocating dynamic buffers - " << "matrix: " << total_matrix_elements + << ", vector: " << total_vector_elements << std::endl; } else { // Fallback: use maximum size (512) if runtime values not available const size_t max_evictable_size = 512; @@ -1755,6 +1792,18 @@ class PagedAttentionOptImpl : public SDPAImplBase { const bool has_scores_output = desc->has_scores_output(); const bool has_score_aggregation = desc->has_score_aggregation; + const auto required_mixed_stage_index = [&]() -> size_t { + size_t sequential_gws_subseq_mapping_idx = 3; + sequential_gws_subseq_mapping_idx += 3; // exp_sums, max_logits, tmp_out + if (has_scores_output) { + sequential_gws_subseq_mapping_idx += 2; // buffers 3, 4 + if (has_score_aggregation) { + sequential_gws_subseq_mapping_idx += 1; // buffer 5 + } + } + return sequential_gws_subseq_mapping_idx; + }; + if ((stage == PagedAttentionStage::UNKNOWN) || (stage == PagedAttentionStage::GENERATE && !has_scores_output && !use_micro_sdpa)) return; @@ -1835,21 +1884,20 @@ class PagedAttentionOptImpl : public SDPAImplBase { std::unique_ptr> micro_sdpa_block_starts_and_gws_mapping_lock = nullptr; if (stage == PagedAttentionStage::MIXED && !use_micro_sdpa) { - // Calculate the index dynamically based on what buffers were actually allocated - // Base: 0, 1, 2 (3 buffers) - size_t sequential_gws_subseq_mapping_idx = 3; - - // exp_sums, max_logits, tmp_out (3 buffers) - sequential_gws_subseq_mapping_idx += 3; - if (has_scores_output) { - sequential_gws_subseq_mapping_idx += 2; // buffers 3, 4 - if (has_score_aggregation) { - sequential_gws_subseq_mapping_idx += 1; // buffer 5 - } - } - - OPENVINO_ASSERT(intermediates_memories.size() > sequential_gws_subseq_mapping_idx, - "[GPU] Unexpected number of intermediates buffers for Paged Attention for mixed stage"); + const auto sequential_gws_subseq_mapping_idx = required_mixed_stage_index(); + const auto required_intermediate_buffers = sequential_gws_subseq_mapping_idx + 1; + + OPENVINO_ASSERT(intermediates_memories.size() >= required_intermediate_buffers, + "[GPU] Unexpected number of intermediates buffers for Paged Attention for mixed stage: expected at least ", + required_intermediate_buffers, + ", got ", + intermediates_memories.size(), + ", scores_output=", + has_scores_output, + ", score_aggregation=", + has_score_aggregation, + ", micro_sdpa=", + use_micro_sdpa); auto& sequential_gws_subseq_mapping_mem = intermediates_memories[sequential_gws_subseq_mapping_idx]; sequential_gws_subseq_mapping_lock.reset(new mem_lock(sequential_gws_subseq_mapping_mem, stream)); @@ -1857,6 +1905,13 @@ class PagedAttentionOptImpl : public SDPAImplBase { if (use_micro_sdpa) { const auto memory_idx = 3; // intermediate_idx for micro kernel + OPENVINO_ASSERT(intermediates_memories.size() > memory_idx, + "[GPU] Unexpected number of intermediates buffers for Paged Attention for micro SDPA: expected at least ", + memory_idx + 1, + ", got ", + intermediates_memories.size(), + ", mixed_stage_index=", + required_mixed_stage_index()); auto& memory = intermediates_memories[memory_idx]; micro_sdpa_block_starts_and_gws_mapping_lock.reset(new mem_lock(memory, stream)); } diff --git a/src/plugins/intel_gpu/src/graph/impls/ocl_v2/sdpa/sdpa_base.cpp b/src/plugins/intel_gpu/src/graph/impls/ocl_v2/sdpa/sdpa_base.cpp index 814f76db252f..074164458055 100644 --- a/src/plugins/intel_gpu/src/graph/impls/ocl_v2/sdpa/sdpa_base.cpp +++ b/src/plugins/intel_gpu/src/graph/impls/ocl_v2/sdpa/sdpa_base.cpp @@ -74,6 +74,14 @@ size_t get_beam_table_id(const std::shared_ptr SDPABase::get_gqa_params(const kernel_impl_params& params) const { if (params.is_type()) { auto desc = params.typed_desc(); @@ -157,6 +165,14 @@ sdpa_configuration SDPABase::get_sdpa_configuration(const kernel_impl_params& im if (value_shape[value_shape.size() - 1].is_static()) config.v_head_size = value_shape[value_shape.size() - 1].get_length(); + // 4-bit KV-cache: physical V layout has head_size/2 due to u4 packing. + // Use logical head size from query to get the correct un-halved value. + if (desc->is_kv_compressed && SDPABase::is_int4_kv_cache(impl_param)) { + if (query_shape[query_shape.size() - 1].is_static()) { + config.v_head_size = query_shape[query_shape.size() - 1].get_length(); + } + } + config.is_causal = desc->is_causal; if (desc->scale_val.has_value()) { @@ -212,6 +228,7 @@ JitConstants SDPABase::get_jit_constants(const kernel_impl_params& params) const jit.make("HAS_SINK_INPUT", 1); } jit.make("IS_KV_COMPRESSED", desc->is_kv_compressed); + jit.make("IS_INT4_COMPRESSED", desc->is_kv_compressed && SDPABase::is_int4_kv_cache(params)); GPU_DEBUG_TRACE_DETAIL << "desc->is_kv_compressed = " << desc->is_kv_compressed << std::endl; const auto& in_offsets_map = params.in_port_to_shape_info_offset; @@ -295,9 +312,19 @@ JitConstants SDPABase::get_jit_constants(const kernel_impl_params& params) const const auto q_head_size = get_head_size(params.get_input_layout(0), extended_input_q_transpose_order); const auto q_num_head = get_num_heads(params.get_input_layout(0), extended_input_q_transpose_order); - const auto k_head_size = get_head_size(params.get_input_layout(1), extended_input_k_transpose_order); + auto k_head_size = get_head_size(params.get_input_layout(1), extended_input_k_transpose_order); const auto k_num_head = get_num_heads(params.get_input_layout(1), extended_input_k_transpose_order); - const auto v_head_size = get_head_size(params.get_input_layout(2), extended_input_v_transpose_order); + auto v_head_size = get_head_size(params.get_input_layout(2), extended_input_v_transpose_order); + + // 4-bit KV-cache: K/V layouts have head_size/2 due to u4→i8 packing. + // Override with logical head size from query (which is not packed). + { + if (desc->is_kv_compressed && SDPABase::is_int4_kv_cache(params)) { + k_head_size = q_head_size; + v_head_size = q_head_size; + } + } + jit.make("HEAD_SIZE", q_head_size); jit.make("NUM_HEADS", q_num_head); jit.make("K_HEAD_SIZE", k_head_size); diff --git a/src/plugins/intel_gpu/src/graph/impls/ocl_v2/sdpa/sdpa_base.hpp b/src/plugins/intel_gpu/src/graph/impls/ocl_v2/sdpa/sdpa_base.hpp index 6334d4dbb062..78e445906b60 100644 --- a/src/plugins/intel_gpu/src/graph/impls/ocl_v2/sdpa/sdpa_base.hpp +++ b/src/plugins/intel_gpu/src/graph/impls/ocl_v2/sdpa/sdpa_base.hpp @@ -64,6 +64,7 @@ struct SDPABase : public KernelGenerator { const std::vector& input_k_transpose_order, const std::vector& input_v_transpose_order); + static bool is_int4_kv_cache(const kernel_impl_params& params); static bool requires_shape_canonicalization(const kernel_impl_params& impl_params); static kernel_impl_params static_canonicalize_shapes(const kernel_impl_params& impl_params); diff --git a/src/plugins/intel_gpu/src/graph/impls/ocl_v2/sdpa/sdpa_gen_micro.cpp b/src/plugins/intel_gpu/src/graph/impls/ocl_v2/sdpa/sdpa_gen_micro.cpp index e127493b4699..11ab17d05de6 100644 --- a/src/plugins/intel_gpu/src/graph/impls/ocl_v2/sdpa/sdpa_gen_micro.cpp +++ b/src/plugins/intel_gpu/src/graph/impls/ocl_v2/sdpa/sdpa_gen_micro.cpp @@ -55,6 +55,10 @@ micro::Type convert_type(ov::element::Type t) { return micro::Type::u8; case ov::element::i32: return micro::Type::s32; + case ov::element::u4: + return micro::Type::u4; + case ov::element::i4: + return micro::Type::s4; default: break; } @@ -433,6 +437,13 @@ sdpa_config_t xe2_q_h256_s768_2nd_integrated = {64, 16, 16, 16, 16, 1, 16, 1}; sdpa_config_t xe2_q_h256_s512_2nd_integrated = {32, 32, 32, 16, 16, 1, 8, 2}; sdpa_config_t xe2_q_h256_s384_2nd_integrated = {16, 16, 16, 16, 16, 1, 16, 1}; +sdpa_config_t xe3_h128 = {32, 16, 32, 16, 16, 2, 16, 2}; +sdpa_config_t xe3_h256 = {32, 16, 32, 16, 16, 2, 16, 2}; + +sdpa_config_t xe3_h512 = {32, 16, 32, 16, 16, 2, 16, 2}; +sdpa_config_t xe3_h512_2nd = {32, 16, 32, 16, 16, 1, 16, 1}; +sdpa_config_t xe3_q_h512_2nd = {32, 16, 32, 16, 16, 1, 16, 1}; + sdpa_config_t* choose_config_xehpg(int head_size, int seq, bool thin_q, bool quantized, bool is_pa, bool is_prefill) { if (head_size <= 32) { if (seq <= 0 && is_pa) @@ -782,6 +793,25 @@ sdpa_config_t* choose_config_xe2(int head_size, int seq, bool thin_q, bool quant } return choose_config_xehpc(head_size, seq, thin_q, quantized, is_integrated, is_pa, is_prefill); } +sdpa_config_t* choose_config_xe3p(int head_size, int seq, bool thin_q, bool quantized, bool is_integrated, bool is_pa, bool is_prefill) { + if (head_size <= 128) { + return &xe3_h128; + } + if (head_size <= 256) { + return &xe3_h256; + return choose_config_xe2(head_size, seq, thin_q, quantized, is_integrated, is_pa, is_prefill); + } + if (head_size <= 512) { + if (thin_q) { + if (quantized) { + return &xe3_q_h512_2nd; + } + return &xe3_h512_2nd; + } + return &xe3_h512; + } + return choose_config_xe2(head_size, seq, thin_q, quantized, is_integrated, is_pa, is_prefill); +} } // namespace @@ -1030,16 +1060,16 @@ JitConstants SDPAMicroGenerator::get_jit_constants(const kernel_impl_params& par auto data_inputs_num = micro_get_input_num(params, config); - size_t attn_input_idx = 3; size_t scale_input_idx = 4; jit.make("IS_CAUSAL", config.is_causal); if (!config.is_paged_attention) { + const bool has_attn_mask_input = sdpa_has_runtime_attn_mask_input(params); if (config.has_const_attn_mask_val) { jit.make("WITH_ATTN_MASK", 0); jit.make("STATIC_SCALAR_ATTN_MASK_VALUE", config.attn_mask_val); // scale_input_idx -= 1; } else { - jit.make("WITH_ATTN_MASK", data_inputs_num > attn_input_idx); + jit.make("WITH_ATTN_MASK", has_attn_mask_input ? 1 : 0); } } else { jit.make("WITH_ATTN_MASK", 0); @@ -1123,16 +1153,32 @@ JitConstants SDPAMicroGenerator::get_jit_constants(const kernel_impl_params& par auto pa_desc = params.typed_desc(); jit.make("IS_KV_COMPRESSED_PA", true); + const auto kv_precision = params.get_program().get_config().get_kv_cache_precision(); + const bool is_int4_logical = data_type_traits::is_i4_u4(kv_precision); + auto scales_zp_size = 4; // scale + zp - if (pa_desc->is_key_by_channel) { + if (is_int4_logical) { + // INT4 KV cache with BY_CHANNEL K quantization: + // K: dim order {0,1,3,2} (col-major), packed block_size in innermost dim + // physical: [blocks, heads, head_size, packed_block + scales] u8 + // packed_block = block_size/2 bytes, scales = 4 bytes + // V: dim order {0,1,2,3} (row-major), packed head_size in innermost dim + // physical: [blocks, heads, block_size, packed_head + scales] u8 + jit.make("IS_INT4_KV_CACHE", 1); + jit.make("IS_KEY_BY_CHANNEL", 1); + jit.make("ADJUSTED_K_HEAD_SIZE", k_head_size); + jit.make("ADJUSTED_V_HEAD_SIZE", v_head_size / 2 + scales_zp_size); + jit.make("ADJUSTED_PAGED_ATTENTION_BLOCK_SIZE", config.paged_attention_block_size / 2 + scales_zp_size); + } else if (pa_desc->is_key_by_channel) { jit.make("IS_KEY_BY_CHANNEL", 1); jit.make("ADJUSTED_K_HEAD_SIZE", k_head_size); jit.make("ADJUSTED_PAGED_ATTENTION_BLOCK_SIZE", config.paged_attention_block_size + scales_zp_size); + jit.make("ADJUSTED_V_HEAD_SIZE", v_head_size + scales_zp_size); } else { jit.make("ADJUSTED_K_HEAD_SIZE", k_head_size + scales_zp_size); jit.make("ADJUSTED_PAGED_ATTENTION_BLOCK_SIZE", config.paged_attention_block_size); + jit.make("ADJUSTED_V_HEAD_SIZE", v_head_size + scales_zp_size); } - jit.make("ADJUSTED_V_HEAD_SIZE", v_head_size + scales_zp_size); } else if (config.is_paged_attention) { jit.make("ADJUSTED_K_HEAD_SIZE", k_head_size); jit.make("ADJUSTED_V_HEAD_SIZE", v_head_size); @@ -1261,7 +1307,7 @@ JitConstants SDPAMicroGenerator::get_jit_constants(const kernel_impl_params& par jit.add(unit_parameters("VAL")); jit.add(unit_parameters("DST")); - if (data_inputs_num > 3 && !config.has_const_attn_mask_val) { + if (data_inputs_num > 3 && !config.is_paged_attention && sdpa_has_runtime_attn_mask_input(params)) { jit.add(convert_strides("MSK", "INPUT3", {0, 1, 2, 3})); jit.add(unit_parameters("MSK")); } @@ -1326,7 +1372,7 @@ Arguments SDPAMicroGenerator::get_arguments_desc(const kernel_impl_params& param args.push_back({ArgumentDescriptor::Types::OUTPUT, 0}); // A const uint32_t attn_mask_idx = ScaledDotProductAttentionInputIdx::ATTN_MASK; - if (config.input_num > attn_mask_idx && !config.has_const_attn_mask_val) + if (sdpa_has_runtime_attn_mask_input(params)) args.push_back({ArgumentDescriptor::Types::INPUT, attn_mask_idx}); // mask const uint32_t scale_idx = ScaledDotProductAttentionInputIdx::SCALE; if (config.input_num > scale_idx && !config.has_const_scale_val) @@ -1471,32 +1517,27 @@ void SDPAMicroGenerator::init_microkernels(const kernel_impl_params& params, bool is_quantized = (K.data_type == ov::element::u8 || K.data_type == ov::element::i8) || (V.data_type == ov::element::u8 || V.data_type == ov::element::i8); - int32_t nkeys_v = n_keys.is_dynamic() ? 0 : n_keys.get_length(); + int32_t nkeys_v = static_cast(n_keys.is_dynamic() ? 0 : n_keys.get_length()); GPU_DEBUG_TRACE_DETAIL << "k_head_size = " << k_head_size << ", nkeys_v = " << nkeys_v << "\n"; GPU_DEBUG_TRACE_DETAIL << "thin_q = " << thin_q << ", is_quantized = " << is_quantized << "\n"; switch (device_info.arch) { case gpu_arch::xe_hpg: { - config = choose_config_xehpg(static_cast(k_head_size), static_cast(nkeys_v), thin_q, is_quantized, is_paged_attention, is_prefill); + config = choose_config_xehpg(static_cast(k_head_size), nkeys_v, thin_q, is_quantized, is_paged_attention, is_prefill); break; } case gpu_arch::xe_hpc: - config = choose_config_xehpc(static_cast(k_head_size), - static_cast(nkeys_v), - thin_q, - is_quantized, - is_integrated, - is_paged_attention, - is_prefill); + config = choose_config_xehpc(static_cast(k_head_size), nkeys_v, thin_q, is_quantized, is_integrated, is_paged_attention, is_prefill); + break; + case gpu_arch::xe2: + case gpu_arch::xe3: + config = choose_config_xe2(static_cast(k_head_size), nkeys_v, thin_q, is_quantized, is_integrated, is_paged_attention, is_prefill); + break; + case gpu_arch::xe3p: + config = choose_config_xe3p(static_cast(k_head_size), nkeys_v, thin_q, is_quantized, is_integrated, is_paged_attention, is_prefill); break; default: { - config = choose_config_xe2(static_cast(k_head_size), - static_cast(nkeys_v), - thin_q, - is_quantized, - is_integrated, - is_paged_attention, - is_prefill); + config = choose_config_xe2(static_cast(k_head_size), nkeys_v, thin_q, is_quantized, is_integrated, is_paged_attention, is_prefill); break; } } @@ -1514,11 +1555,21 @@ void SDPAMicroGenerator::init_microkernels(const kernel_impl_params& params, problem.Ta_ext = convert_type(K.data_type); problem.Tb_ext = convert_type(Q.data_type); + // Detect INT4 KV cache: stored as u8 but logical precision is u4/i4 + const auto kv_cache_precision = is_paged_attention ? params.get_program().get_config().get_kv_cache_precision() : ov::element::dynamic; + const bool is_int4_kv_cache = data_type_traits::is_i4_u4(kv_cache_precision); + + // For INT4 PA generate: override Ta_ext to u4/i4 so micro-kernel handles u4 unpacking + if (is_int4_kv_cache && is_paged_attention && !is_prefill) { + problem.Ta_ext = convert_type(kv_cache_precision); + } + problem.Ta = problem.Tb = micro::Type::f16; problem.Tc = problem.Tc_ext = micro::Type::f32; problem.Ts = problem.Tc; auto problem_kq = problem; + // Both INT4 and INT8 K cache use dim order {0,1,3,2} (column-major / Layout::N). problem_kq.A.layout = (is_paged_attention && !is_prefill) ? micro::MatrixLayout::N : micro::MatrixLayout::T; /* Set up microkernel options */ @@ -1529,7 +1580,9 @@ void SDPAMicroGenerator::init_microkernels(const kernel_impl_params& params, const bool use_asymmetric_quantization = configuration.use_asymmetric_quantization; if (is_paged_attention && !is_prefill && is_quantized) { - problem.Ta = micro::Type::s8; + if (!is_int4_kv_cache) { + problem.Ta = micro::Type::s8; + } const auto scale_dt = convert_type(ov::element::f16); problem_kq.Ta_scale = scale_dt; @@ -1545,7 +1598,13 @@ void SDPAMicroGenerator::init_microkernels(const kernel_impl_params& params, problem_kq.aOffset = micro::ABOffset::Calc; auto pa_desc = params.typed_desc(); - if (pa_desc->is_key_by_channel) { + if (is_int4_kv_cache) { + // INT4 BY_CHANNEL: per-channel quantization (one scale/zp per head dim for all tokens in block) + // Same quantization grouping as INT8 BY_CHANNEL. + problem_kq.aqGroupM = config->unroll_m_kq * config->wg_m_kq; + problem_kq.aqGroupK = 1; + // BY_CHANNEL scales: Layout::N (stride-1 between rows, ldkq stride between columns) + } else if (pa_desc->is_key_by_channel) { problem_kq.aqGroupM = config->unroll_m_kq * config->wg_m_kq; problem_kq.aqGroupK = 1; } else { @@ -1588,13 +1647,19 @@ void SDPAMicroGenerator::init_microkernels(const kernel_impl_params& params, problem_kq.B.layout = micro::MatrixLayout::Pr; problem_kq.C.layout = micro::MatrixLayout::T; - problem_kq.A.setAlignment(micro::alignment_for_ld(k_head_size * problem.Ta)); + problem_kq.A.setAlignment(micro::alignment_for_ld(static_cast(k_head_size * problem.Ta))); if (is_paged_attention && !is_prefill) { auto pa_desc = params.typed_desc(); const auto paged_attention_block_size = static_cast(paged_attention::block_size); - problem_kq.A.setAlignment(paged_attention_block_size * problem.Ta); - if (is_quantized && pa_desc->is_key_by_channel) { - problem_kq.A.setAlignment(paged_attention_block_size * problem.Ta + 4); // scale - 2 bytes, zp - 2 bytes + if (is_int4_kv_cache) { + // INT4 BY_CHANNEL Layout::N: lda = packed_block_bytes + scales + // = block_size * u4 + 4 = 16 * 0.5 + 4 = 12 bytes + problem_kq.A.setAlignment(paged_attention_block_size * problem.Ta_ext + 4); + } else { + problem_kq.A.setAlignment(paged_attention_block_size * problem.Ta); + if (is_quantized && pa_desc->is_key_by_channel) { + problem_kq.A.setAlignment(paged_attention_block_size * problem.Ta + 4); // scale - 2 bytes, zp - 2 bytes + } } } problem_kq.B.setAlignment(64); // Q is packed in VNNI format in SLM @@ -1638,7 +1703,7 @@ void SDPAMicroGenerator::init_microkernels(const kernel_impl_params& params, problem_kcq.A.layout = micro::MatrixLayout::T; problem_kcq.B.layout = micro::MatrixLayout::Pr; problem_kcq.C.layout = micro::MatrixLayout::T; - problem_kcq.A.setAlignment(micro::alignment_for_ld(k_head_size * problem.Ta)); + problem_kcq.A.setAlignment(micro::alignment_for_ld(static_cast(k_head_size * problem.Ta))); problem_kcq.B.setAlignment(64); // Q is packed in VNNI format in SLM problem_kcq.B.crosspack = 2; problem_kcq.B.tileR = static_cast(d_max); @@ -1663,6 +1728,10 @@ void SDPAMicroGenerator::init_microkernels(const kernel_impl_params& params, /* Update for second GEMM: V*S */ auto problem_vs = problem; problem_vs.Ta_ext = convert_type(V.data_type); + // For INT4 V: override Ta_ext to u4/i4 + if (is_int4_kv_cache && is_paged_attention && !is_prefill) { + problem_vs.Ta_ext = convert_type(kv_cache_precision); + } problem_vs.A.layout = micro::MatrixLayout::N; if (is_paged_attention && !is_prefill && is_quantized) { @@ -1682,6 +1751,13 @@ void SDPAMicroGenerator::init_microkernels(const kernel_impl_params& params, problem_vs.aqGroupM = static_cast(v_head_size); problem_vs.aqGroupK = 1; + // NOTE: V*S scale Layout stays ::N (the default set above), which is correct. + // V*S A = V[m=head_dim, k=tokens], so per-token scales form a [1, tokens] matrix. + // With Layout::N: scale(0, token) = base + token * ldvq = 68-byte stride. Correct. + // (Layout::T would give stride-1 = 2-byte intervals, which is wrong.) + // This differs from K*Q where A = K[m=tokens, k=head_dim], scale is [tokens, 1], + // and Layout::T is needed to get the token stride via ldkq. + opts_vs.scaleA = true; opts_vs.offsetA = true; } else { @@ -1717,7 +1793,14 @@ void SDPAMicroGenerator::init_microkernels(const kernel_impl_params& params, problem_vs.B.layout = micro::MatrixLayout::Pr; problem_vs.C.layout = micro::MatrixLayout::N; - problem_vs.A.setAlignment(micro::alignment_for_ld(v_head_size * problem.Ta)); + + if (is_int4_kv_cache && is_paged_attention && !is_prefill) { + // INT4 V: ldv = packed_head_bytes + scales = v_head_size * u4 + 4 = 68 + problem_vs.A.setAlignment(static_cast(v_head_size * problem_vs.Ta_ext) + 4); + } else { + problem_vs.A.setAlignment(micro::alignment_for_ld(static_cast(v_head_size * problem.Ta))); + } + problem_vs.B.setAlignment(64); // S is packed in SLM problem_vs.B.crosspack = 16; sizes.m = n_values.is_dynamic() ? -1 : n_values.get_length(); @@ -1756,7 +1839,7 @@ void SDPAMicroGenerator::init_microkernels(const kernel_impl_params& params, problem_vcs.B.layout = micro::MatrixLayout::Pr; problem_vcs.C.layout = micro::MatrixLayout::N; - problem_vcs.A.setAlignment(micro::alignment_for_ld(v_head_size * problem.Ta)); + problem_vcs.A.setAlignment(micro::alignment_for_ld(static_cast(v_head_size * problem.Ta))); problem_vcs.B.setAlignment(64); // S is packed in SLM problem_vcs.B.crosspack = 16; sizes.m = n_values.is_dynamic() ? -1 : n_values.get_length(); diff --git a/src/plugins/intel_gpu/src/graph/impls/ocl_v2/sdpa/sdpa_gen_opt.cpp b/src/plugins/intel_gpu/src/graph/impls/ocl_v2/sdpa/sdpa_gen_opt.cpp index cd1ce1a145e9..77ef62ba3351 100644 --- a/src/plugins/intel_gpu/src/graph/impls/ocl_v2/sdpa/sdpa_gen_opt.cpp +++ b/src/plugins/intel_gpu/src/graph/impls/ocl_v2/sdpa/sdpa_gen_opt.cpp @@ -63,15 +63,27 @@ JitConstants SDPAOptGeneratorBase::get_jit_constants_base(const kernel_impl_para auto extended_input_v_transpose_order = extend_order_in_num_heads_dim(desc->input_v_transpose_order); k_head_size = get_head_size(k_layout, extended_input_k_transpose_order); v_head_size = get_head_size(v_layout, extended_input_v_transpose_order); + + // 4-bit KV-cache: K/V layouts have head_size/2 due to u4→i8 packing. + // Override with logical head size from query (which is not packed). + { + if (desc->is_kv_compressed && SDPABase::is_int4_kv_cache(params)) { + auto extended_input_q_transpose_order = extend_order_in_num_heads_dim(desc->input_q_transpose_order); + auto q_head_size = get_head_size(params.get_input_layout(0), extended_input_q_transpose_order); + k_head_size = q_head_size; + v_head_size = q_head_size; + } + } + GPU_DEBUG_TRACE_DETAIL << "k_head_size = " << k_head_size << ", v_head_size = " << v_head_size << "\n"; size_t data_inputs_num = get_data_inputs_num(*desc); size_t attn_mask_idx = ScaledDotProductAttentionInputIdx::ATTN_MASK; + const bool has_attn_mask_input = sdpa_has_runtime_attn_mask_input(params); if (desc->attn_mask_val.has_value()) { jit.make("STATIC_SCALAR_ATTN_MASK_VALUE", desc->attn_mask_val.value()); jit.make("HAS_ATTN_MASK_INPUT", 0); } else { - const bool has_attn_mask_input = data_inputs_num > attn_mask_idx; jit.make("HAS_ATTN_MASK_INPUT", has_attn_mask_input ? 1 : 0); if (has_attn_mask_input) { const auto& attn_mask_layout = params.get_input_layout(attn_mask_idx); @@ -122,8 +134,9 @@ Arguments SDPAOptGeneratorBase::get_arguments_desc_impl(const kernel_impl_params const size_t attn_mask_idx = ScaledDotProductAttentionInputIdx::ATTN_MASK; const size_t scale_idx = ScaledDotProductAttentionInputIdx::SCALE; + const bool has_attn_mask_input = sdpa_has_runtime_attn_mask_input(params); for (uint32_t i = 0; i < data_inputs_num; i++) { - if (i == attn_mask_idx && desc->attn_mask_val.has_value()) + if (i == attn_mask_idx && !has_attn_mask_input) continue; if (i == scale_idx && desc->scale_val.has_value()) continue; @@ -188,7 +201,14 @@ DispatchDataFunc SDPAOptGeneratorSingleToken::get_dispatch_data_func() const { const size_t target_seq_len = get_seq_length(params.get_input_layout(0), extended_input_q_transpose_order); const size_t heads_num = get_num_heads(params.get_output_layout(0), extended_output_transpose_order); const size_t num_of_partitions = get_partitions_num(params, SDPAStage::SINGLE_TOKEN); - const auto head_size = get_head_size(params.get_input_layout(2), extended_input_v_transpose_order); + auto head_size = get_head_size(params.get_input_layout(2), extended_input_v_transpose_order); + + // 4-bit KV-cache: V layout has head_size/2 due to u4→i8 packing. + // Use logical head size from query for work-group dispatch. + if (desc->is_kv_compressed && SDPABase::is_int4_kv_cache(params)) { + head_size = get_head_size(params.get_input_layout(0), extended_input_q_transpose_order); + } + const size_t sg_num_scale = get_sg_number_scale_factor(params.get_device_info(), head_size, SDPAStage::SINGLE_TOKEN); GPU_DEBUG_TRACE_DETAIL << "batch_size = " << batch_size << ", target_seq_len = " << target_seq_len << ", heads_num = " << heads_num << "\n"; GPU_DEBUG_TRACE_DETAIL << "head_size = " << head_size << ", num_of_partitions = " << num_of_partitions << "\n"; @@ -227,7 +247,14 @@ DispatchDataFunc SDPAOptGeneratorMultiToken::get_dispatch_data_func() const { const size_t target_seq_len = get_seq_length(params.get_input_layout(0), extended_input_q_transpose_order); const size_t heads_num = get_num_heads(params.get_output_layout(0), extended_output_transpose_order); const size_t target_seq_len_block_size = get_target_seq_len_block_size(); - const size_t head_size = get_head_size(params.get_input_layout(2), extended_input_v_transpose_order); + auto head_size = get_head_size(params.get_input_layout(2), extended_input_v_transpose_order); + + // 4-bit KV-cache: V layout has head_size/2 due to u4→i8 packing. + // Use logical head size from query for work-group dispatch. + if (desc->is_kv_compressed && SDPABase::is_int4_kv_cache(params)) { + head_size = get_head_size(params.get_input_layout(0), extended_input_q_transpose_order); + } + const size_t sg_num_scale = get_sg_number_scale_factor(params.get_device_info(), head_size, SDPAStage::MULTI_TOKENS); GPU_DEBUG_TRACE_DETAIL << "batch_size = " << batch_size << ", target_seq_len = " << target_seq_len << ", heads_num = " << heads_num << "\n"; @@ -270,13 +297,19 @@ DispatchDataFunc SDPAOptGeneratorFinalization::get_dispatch_data_func() const { const size_t target_seq_len = get_seq_length(params.get_input_layout(0), extended_input_q_transpose_order); const size_t heads_num = get_num_heads(params.get_output_layout(0), extended_output_transpose_order); const size_t num_of_partitions = get_partitions_num(params, SDPAStage::FINALIZATION); - const size_t head_size = get_head_size(params.get_input_layout(2), extended_input_v_transpose_order); + auto head_size = get_head_size(params.get_input_layout(2), extended_input_v_transpose_order); + + // 4-bit KV-cache: V layout has head_size/2 due to u4→i8 packing. + // Use logical head size from query for finalization dispatch. + if (desc->is_kv_compressed && SDPABase::is_int4_kv_cache(params)) { + head_size = get_head_size(params.get_input_layout(0), extended_input_q_transpose_order); + } GPU_DEBUG_TRACE_DETAIL << "batch_size = " << batch_size << ", target_seq_len = " << target_seq_len << ", heads_num = " << heads_num << "\n"; GPU_DEBUG_TRACE_DETAIL << "head_size = " << head_size << ", num_of_partitions = " << num_of_partitions << "\n"; - wgs.global = {batch_size * heads_num, target_seq_len, head_size}; - wgs.local = {1, 1, head_size}; + wgs.global = {batch_size * heads_num, target_seq_len, static_cast(head_size)}; + wgs.local = {1, 1, static_cast(head_size)}; num_of_partitions_scalar.v.u32 = static_cast(num_of_partitions); scalars.push_back(num_of_partitions_scalar); } diff --git a/src/plugins/intel_gpu/src/graph/impls/ocl_v2/sdpa/sdpa_opt.cpp b/src/plugins/intel_gpu/src/graph/impls/ocl_v2/sdpa/sdpa_opt.cpp index a9c28c2a52fd..2d406cc81e00 100644 --- a/src/plugins/intel_gpu/src/graph/impls/ocl_v2/sdpa/sdpa_opt.cpp +++ b/src/plugins/intel_gpu/src/graph/impls/ocl_v2/sdpa/sdpa_opt.cpp @@ -113,9 +113,19 @@ class SDPAOptImpl : public SDPAImplBase { bool is_indirect = need_indirect_load(static_cast(instance)); GPU_DEBUG_TRACE_DETAIL << "execute indirect = " << is_indirect << ", prefill = " << is_prefill << "\n"; update_rt_params(instance); + kernel_dump_info.clear_entries(); + #ifdef ENABLE_ONEDNN_FOR_GPU - if (has_stage(regular_micro_multi_tokens) && is_prefill && !is_indirect) { + // Check if INT4 KV cache is in use (micro kernel doesn't support INT4 for non-PA SDPA) + // Only apply this check when the SDPA node actually uses compressed KV cache (i8/u8/i4/u4 K/V inputs). + // Vision Encoder SDPA nodes have f16 K/V inputs and should not be affected by the global config. + const auto k_dt = new_params.input_layouts[1].data_type; + const bool is_kv_compressed = data_type_traits::is_i8_u8(k_dt) || data_type_traits::is_i4_u4(k_dt); + const auto kv_cache_dt = new_params.get_program().get_config().get_kv_cache_precision(); + const bool is_int4_kv = is_kv_compressed && ov::element::Type(kv_cache_dt).bitwidth() == 4; + + if (has_stage(regular_micro_multi_tokens) && is_prefill && !is_indirect && !is_int4_kv) { GPU_DEBUG_TRACE_DETAIL << "execute regular_micro_multi_tokens for prefill \n"; return execute_stage(events, instance, regular_micro_multi_tokens); } @@ -129,7 +139,7 @@ class SDPAOptImpl : public SDPAImplBase { return execute_stage(events, instance, is_indirect ? indirect_multi_tokens : regular_multi_tokens); } #ifdef ENABLE_ONEDNN_FOR_GPU - if (has_stage(regular_micro_single_token) && !is_indirect) { + if (has_stage(regular_micro_single_token) && !is_indirect && !is_int4_kv) { return execute_stage(events, instance, regular_micro_single_token); } #endif @@ -171,12 +181,20 @@ bool SDPAOpt::supports_micro_sdpa(const RuntimeParams& params) { #ifdef ENABLE_ONEDNN_FOR_GPU auto& engine = params.get_program().get_engine(); const auto& device_info = engine.get_device_info(); + auto desc = params.typed_desc(); if (device_info.supports_immad) { const auto supports_microkernels = cldnn::query_microkernels_supported(engine, params.get_program().get_config()); if (device_info.arch < gpu_arch::xe_hpg || !supports_microkernels) { return false; } + // WA: Disable micro SDPA on xe3p for head_size <= 64 due to oneDNN micro-kernel + // accuracy issues (produces inf/nan) after oneDNN main branch integration. + auto extended_input_k_transpose_order = extend_order_in_num_heads_dim(desc->input_k_transpose_order); + const auto k_head_size = get_head_size(params.get_input_layout(1), extended_input_k_transpose_order); + if (device_info.arch == gpu_arch::xe3p && k_head_size <= 64) { + return false; + } } else { return false; } @@ -184,7 +202,6 @@ bool SDPAOpt::supports_micro_sdpa(const RuntimeParams& params) { const auto& q_layout = params.get_input_layout(0); const auto& k_layout = params.get_input_layout(1); const auto& v_layout = params.get_input_layout(2); - auto desc = params.typed_desc(); // Will check it later to decide whether support micro kernel // if (desc->indirect_axis != -1) { diff --git a/src/plugins/intel_gpu/src/graph/impls/ocl_v2/sdpa/sdpa_ref.cpp b/src/plugins/intel_gpu/src/graph/impls/ocl_v2/sdpa/sdpa_ref.cpp index 8284c9e6dff2..408f37a7a342 100644 --- a/src/plugins/intel_gpu/src/graph/impls/ocl_v2/sdpa/sdpa_ref.cpp +++ b/src/plugins/intel_gpu/src/graph/impls/ocl_v2/sdpa/sdpa_ref.cpp @@ -33,8 +33,7 @@ class SDPARefGenerator : public SDPABase { jit.add(make_type_jit_constants("ACCUMULATOR", get_accumulator_type(params))); size_t data_inputs_num = get_data_inputs_num(*desc); - size_t attn_mask_idx = ScaledDotProductAttentionInputIdx::ATTN_MASK; - if (data_inputs_num > attn_mask_idx) { + if (sdpa_has_runtime_attn_mask_input(params)) { jit.make("HAS_ATTN_MASK_INPUT", 1); } size_t scale_idx = ScaledDotProductAttentionInputIdx::SCALE; @@ -67,6 +66,9 @@ class SDPARefGenerator : public SDPABase { } for (uint32_t i = 0; i < data_inputs_num; i++) { + if (i == ScaledDotProductAttentionInputIdx::ATTN_MASK && !sdpa_has_runtime_attn_mask_input(params)) { + continue; + } args.push_back({ArgumentDescriptor::Types::INPUT, i}); } diff --git a/src/plugins/intel_gpu/src/graph/impls/ocl_v2/sdpa/sdpa_utils.hpp b/src/plugins/intel_gpu/src/graph/impls/ocl_v2/sdpa/sdpa_utils.hpp index 401ef52245cc..f0da19232763 100644 --- a/src/plugins/intel_gpu/src/graph/impls/ocl_v2/sdpa/sdpa_utils.hpp +++ b/src/plugins/intel_gpu/src/graph/impls/ocl_v2/sdpa/sdpa_utils.hpp @@ -27,6 +27,32 @@ inline size_t get_data_inputs_num(const cldnn::scaled_dot_product_attention& des return data_inputs_num; } +inline bool sdpa_has_runtime_attn_mask_input(const cldnn::kernel_impl_params& params) { + if (!params.is_type()) { + return false; + } + + const auto& desc = *params.typed_desc(); + + if (desc.attn_mask_val.has_value()) { + return false; + } + + if (get_data_inputs_num(desc) <= cldnn::scaled_dot_product_attention::ScaledDotProductAttentionInputIdx::ATTN_MASK) { + return false; + } + + const auto& attn_mask_pshape = + params.get_input_layout(cldnn::scaled_dot_product_attention::ScaledDotProductAttentionInputIdx::ATTN_MASK).get_partial_shape(); + + // Keep scalar and 1D placeholders out of the real attention-mask path. + if (attn_mask_pshape.rank().is_static() && attn_mask_pshape.rank().get_length() <= 1) { + return false; + } + + return true; +} + inline size_t get_key_cache_id(const cldnn::scaled_dot_product_attention& desc) { size_t key_cache_id = desc.input_size(); diff --git a/src/plugins/intel_gpu/src/graph/impls/ocl_v2/sdpa_micro.cl b/src/plugins/intel_gpu/src/graph/impls/ocl_v2/sdpa_micro.cl index 8429e49e5e91..b8f8d4f5882d 100644 --- a/src/plugins/intel_gpu/src/graph/impls/ocl_v2/sdpa_micro.cl +++ b/src/plugins/intel_gpu/src/graph/impls/ocl_v2/sdpa_micro.cl @@ -231,17 +231,34 @@ KERNEL(micro_sdpa)(OPTIONAL_SHAPE_INFO_ARG uint ldk = HEAD_SIZE * KV_HEADS_NUM + INPUT1_PAD_BEFORE_FEATURE_NUM + INPUT1_PAD_AFTER_FEATURE_NUM; uint ldv = HEAD_SIZE * KV_HEADS_NUM + INPUT2_PAD_BEFORE_FEATURE_NUM + INPUT2_PAD_AFTER_FEATURE_NUM; #else + #if IS_INT4_KV_CACHE + // INT4 K BY_CHANNEL Layout::N: ldk = column stride in u4 elements. + // ADJUSTED_PAGED_ATTENTION_BLOCK_SIZE is in bytes (packed_block + scale = 12). + // Multiply by 2 for u4: 12 * 2 = 24 u4 elements → 12 byte stride. + uint ldk = ADJUSTED_PAGED_ATTENTION_BLOCK_SIZE * 2; + // INT4 V per-token Layout::N: ldv = row stride in u4 elements. + // ADJUSTED_V_HEAD_SIZE is in bytes (packed_head + scale = 68). + // Multiply by 2 for u4: 68 * 2 = 136 u4 elements → 68 byte stride. + uint ldv = ADJUSTED_V_HEAD_SIZE * 2; + #else uint ldk = ADJUSTED_PAGED_ATTENTION_BLOCK_SIZE; uint ldv = HEAD_SIZE; + #endif uint ldkc = HEAD_SIZE * KV_HEADS_NUM + INPUT1_PAD_BEFORE_FEATURE_NUM + INPUT1_PAD_AFTER_FEATURE_NUM; uint ldvc = HEAD_SIZE * KV_HEADS_NUM + INPUT2_PAD_BEFORE_FEATURE_NUM + INPUT2_PAD_AFTER_FEATURE_NUM; #if IS_KV_COMPRESSED_PA - #if IS_KEY_BY_CHANNEL + #if IS_INT4_KV_CACHE + // INT4 K BY_CHANNEL: scale stride = ADJUSTED_PAGED_ATTENTION_BLOCK_SIZE bytes / 2 in f16 elements uint ldkq = ADJUSTED_PAGED_ATTENTION_BLOCK_SIZE / 2; + // INT4 V per-token: scale stride = ADJUSTED_V_HEAD_SIZE bytes / 2 in f16 elements + uint ldvq = ADJUSTED_V_HEAD_SIZE / 2; + #elif IS_KEY_BY_CHANNEL + uint ldkq = ADJUSTED_PAGED_ATTENTION_BLOCK_SIZE / 2; + uint ldvq = 1; #else uint ldkq = 1; + uint ldvq = 1; #endif - uint ldvq = 1; #endif #endif #else @@ -523,10 +540,22 @@ KERNEL(micro_sdpa)(OPTIONAL_SHAPE_INFO_ARG for (;k0 < past_lens[gws_mapping];) { #endif int k_block_num = k0 / PAGED_ATTENTION_BLOCK_SIZE + sg_i_kq; - global KEY_DATA_T *K0 = K + KV_HEADS_NUM * ADJUSTED_K_HEAD_SIZE * ADJUSTED_PAGED_ATTENTION_BLOCK_SIZE * block_indices[base_block_index + k_block_num] - - PAGED_ATTENTION_BLOCK_SIZE * sg_i_kq; + #if IS_INT4_KV_CACHE + // INT4 BY_CHANNEL Layout::N: micro-kernel adds sg_i_kq * tile_sg_m * sizeof(u4) + // = sg_i_kq * PAGED_ATTENTION_BLOCK_SIZE / 2 bytes + global KEY_DATA_T *K0 = K + KV_HEADS_NUM * ADJUSTED_K_HEAD_SIZE * ADJUSTED_PAGED_ATTENTION_BLOCK_SIZE * block_indices[base_block_index + k_block_num] + - (uint)(PAGED_ATTENTION_BLOCK_SIZE / 2) * sg_i_kq; + #else + global KEY_DATA_T *K0 = K + KV_HEADS_NUM * ADJUSTED_K_HEAD_SIZE * ADJUSTED_PAGED_ATTENTION_BLOCK_SIZE * block_indices[base_block_index + k_block_num] + - PAGED_ATTENTION_BLOCK_SIZE * sg_i_kq; + #endif #if IS_KV_COMPRESSED_PA - #if IS_KEY_BY_CHANNEL + #if IS_INT4_KV_CACHE + // INT4 BY_CHANNEL: scales at packed_block_size offset within each column + // packed_block_size = PAGED_ATTENTION_BLOCK_SIZE / 2 = 8 bytes + global KEY_DATA_T *K0_scales = K0 + (PAGED_ATTENTION_BLOCK_SIZE / 2) * (sg_i_kq + 1); + global KEY_DATA_T *K0_zp = K0_scales + 2; + #elif IS_KEY_BY_CHANNEL global KEY_DATA_T *K0_scales = K0 + PAGED_ATTENTION_BLOCK_SIZE * (sg_i_kq + 1); global KEY_DATA_T *K0_zp = K0_scales + 2; #else @@ -905,8 +934,14 @@ KERNEL(micro_sdpa)(OPTIONAL_SHAPE_INFO_ARG global VAL_DATA_T *Vb0 = V + KV_HEADS_NUM * ADJUSTED_V_HEAD_SIZE * PAGED_ATTENTION_BLOCK_SIZE * block_indices[base_block_index + v_block_num]; int kb_chunk = min(k - k0 - kb0, PAGED_ATTENTION_BLOCK_SIZE); #if IS_KV_COMPRESSED_PA + #if IS_INT4_KV_CACHE + // INT4: scales embedded at HEAD_SIZE/2 offset within each token row + global VAL_DATA_T *Vb0_scales = Vb0 + HEAD_SIZE / 2; + global VAL_DATA_T *Vb0_zp = Vb0_scales + 2; + #else global VAL_DATA_T *Vb0_scales = Vb0 + HEAD_SIZE * PAGED_ATTENTION_BLOCK_SIZE; global VAL_DATA_T *Vb0_zp = Vb0_scales + PAGED_ATTENTION_BLOCK_SIZE * 2; + #endif #endif a_tile_type A_tile1 = ugemm_vs( diff --git a/src/plugins/intel_gpu/src/graph/impls/ocl_v2/sdpa_opt.cl b/src/plugins/intel_gpu/src/graph/impls/ocl_v2/sdpa_opt.cl index 0c12c708be92..e557040ef999 100644 --- a/src/plugins/intel_gpu/src/graph/impls/ocl_v2/sdpa_opt.cl +++ b/src/plugins/intel_gpu/src/graph/impls/ocl_v2/sdpa_opt.cl @@ -4,6 +4,7 @@ #include "include/batch_headers/fetch_data.cl" #include "include/batch_headers/common.cl" +#include "include/batch_headers/int4_utils.cl" #include "include/batch_headers/sub_group_block_read.cl" #include "include/batch_headers/sub_group_block_write.cl" #include "include/batch_headers/sub_group_shuffle.cl" @@ -115,7 +116,11 @@ inline uint FUNC(get_bt_index_value)(OPTIONAL_SHAPE_INFO_ARG uint b, uint f, uin #define OUTPUT_BLOCK_READ(ptr, offset) BLOCK_READN(OUTPUT_TYPE, 1, ptr, offset) #define OUTPUT_BLOCK_WRITE(ptr, offset, val) BLOCK_WRITEN(OUTPUT_TYPE, 1, ptr, offset, val) +#if IS_INT4_COMPRESSED +#define VALUE_BLOCK_READ(ptr, offset) ((ptr)[(offset) + sglid]) +#else #define VALUE_BLOCK_READ(ptr, offset) BLOCK_READN(INPUT2_TYPE, 1, ptr, offset) +#endif #define SUBGROUPS_PER_WG CEIL_DIV(V_HEAD_SIZE * SG_SCALE_FACTOR, SUBGROUP_SIZE) #if IS_KV_COMPRESSED @@ -271,6 +276,15 @@ KERNEL(sdpa_opt)( // Main Gemm1 calculation loop // Each SG performs element-wise multiplications of Q[HEAD_SIZE]xK[HEAD_SIZE] values // HEAD_SIZE / SUBGROUPS_PER_WG times in the loop and saves the result to the qk_local SLM buffer +#if IS_INT4_COMPRESSED && !defined(BEAM_TABLE_TYPE) + #ifdef INPUT1_DIMS_ORDER + const uint key_base_p0 = FUNC_CALL(get_input1_index)(OPTIONAL_SHAPE_INFO_TENSOR b0_idx, b1_idx, 0, 0, 0, 0); + const uint key_packed_pitch_p0 = FUNC_CALL(get_input1_index)(OPTIONAL_SHAPE_INFO_TENSOR b0_idx, b1_idx, 0, 0, 1, 0) - key_base_p0; + #else + const uint key_base_p0 = INPUT1_GET_INDEX(b0_idx, b1_idx, 0, 0); + const uint key_packed_pitch_p0 = INPUT1_SIZE_X; + #endif +#endif for (uint seq_len = sgid; seq_len < partition_seq_len; seq_len += SUBGROUPS_PER_WG) { #ifdef BEAM_TABLE_TYPE const uint b_idx = beam_table[FUNC_CALL(get_bt_index_key)(OPTIONAL_SHAPE_INFO_TENSOR b0_idx, b1_idx, 0, 0, start_partition_idx + seq_len, 0)]; @@ -278,7 +292,9 @@ KERNEL(sdpa_opt)( const uint b_idx = b0_idx; #endif -#ifdef INPUT1_DIMS_ORDER +#if IS_INT4_COMPRESSED && !defined(BEAM_TABLE_TYPE) + uint key_offset = key_base_p0 + (start_partition_idx + seq_len) * key_packed_pitch_p0; +#elif defined(INPUT1_DIMS_ORDER) uint key_offset = FUNC_CALL(get_input1_index)(OPTIONAL_SHAPE_INFO_TENSOR b_idx, b1_idx, 0, 0, start_partition_idx + seq_len, 0); #else uint key_offset = INPUT1_GET_INDEX(b_idx, b1_idx, start_partition_idx + seq_len, 0); @@ -293,6 +309,58 @@ KERNEL(sdpa_opt)( KEY_COMPRESSION_SCALE_TYPE comp_zp = GET_ZP(key_zp, key_scale, comp_offset); #endif #endif + +#if IS_KV_COMPRESSED && IS_INT4_COMPRESSED + // INT4 adjacent packing: each byte holds 2 head dims (lo/hi nibble). + for (uint head_idx_index = 0; head_idx_index + 2 * SUBGROUP_SIZE <= K_HEAD_SIZE; head_idx_index += 2 * SUBGROUP_SIZE) { + #define KEY_BLOCK_READ_1(ptr, offset) ((ptr)[(offset) + sglid]) + INPUT1_TYPE packed_byte = KEY_BLOCK_READ_1(key_input, key_offset + head_idx_index / 2); + char2 unpacked = unpack_to_char(*(uint4x2_t*)&packed_byte); + + KEY_COMPRESSION_SCALE_TYPE key_val0 = (CAT(convert_, KEY_COMPRESSION_SCALE_TYPE)(unpacked.s0) - comp_zp) * comp_scale; + KEY_COMPRESSION_SCALE_TYPE key_val1 = (CAT(convert_, KEY_COMPRESSION_SCALE_TYPE)(unpacked.s1) - comp_zp) * comp_scale; + + unroll_for (uint seq_idx = 0; seq_idx < TARGET_SEQ_LEN_BLOCK_SIZE; seq_idx++) { + uint query_offset = seq_idx * K_HEAD_SIZE + head_idx_index; + INPUT0_TYPE q_val0 = query_local[query_offset + 2 * sglid]; + INPUT0_TYPE q_val1 = query_local[query_offset + 2 * sglid + 1]; + acc[seq_idx] = mad(TO_SOFTMAX_ACCUMULATOR_TYPE(q_val0), TO_SOFTMAX_ACCUMULATOR_TYPE(key_val0), acc[seq_idx]); + acc[seq_idx] = mad(TO_SOFTMAX_ACCUMULATOR_TYPE(q_val1), TO_SOFTMAX_ACCUMULATOR_TYPE(key_val1), acc[seq_idx]); + } + #undef KEY_BLOCK_READ_1 + } + // Handle leftover chunk when K_HEAD_SIZE is not a multiple of 2*SUBGROUP_SIZE +#if (K_HEAD_SIZE % (2 * SUBGROUP_SIZE)) != 0 + { + const uint head_idx_index = (K_HEAD_SIZE / (2 * SUBGROUP_SIZE)) * (2 * SUBGROUP_SIZE); + #define KEY_BLOCK_READ_1(ptr, offset) ((ptr)[(offset) + sglid]) + INPUT1_TYPE packed_byte = (head_idx_index / 2 + sglid < K_HEAD_SIZE / 2) + ? KEY_BLOCK_READ_1(key_input, key_offset + head_idx_index / 2) : (INPUT1_TYPE)0; + char2 unpacked = unpack_to_char(*(uint4x2_t*)&packed_byte); + + KEY_COMPRESSION_SCALE_TYPE key_val0 = (CAT(convert_, KEY_COMPRESSION_SCALE_TYPE)(unpacked.s0) - comp_zp) * comp_scale; + KEY_COMPRESSION_SCALE_TYPE key_val1 = (CAT(convert_, KEY_COMPRESSION_SCALE_TYPE)(unpacked.s1) - comp_zp) * comp_scale; + KEY_COMPRESSION_SCALE_TYPE lane_mask = (head_idx_index + 2 * sglid < K_HEAD_SIZE) ? (KEY_COMPRESSION_SCALE_TYPE)1 : (KEY_COMPRESSION_SCALE_TYPE)0; + key_val0 *= lane_mask; + key_val1 *= lane_mask; + + unroll_for (uint seq_idx = 0; seq_idx < TARGET_SEQ_LEN_BLOCK_SIZE; seq_idx++) { + uint query_offset = seq_idx * K_HEAD_SIZE + head_idx_index; + INPUT0_TYPE q_val0 = query_local[query_offset + 2 * sglid]; + INPUT0_TYPE q_val1 = query_local[query_offset + 2 * sglid + 1]; + acc[seq_idx] = mad(TO_SOFTMAX_ACCUMULATOR_TYPE(q_val0), TO_SOFTMAX_ACCUMULATOR_TYPE(key_val0), acc[seq_idx]); + acc[seq_idx] = mad(TO_SOFTMAX_ACCUMULATOR_TYPE(q_val1), TO_SOFTMAX_ACCUMULATOR_TYPE(key_val1), acc[seq_idx]); + } + #undef KEY_BLOCK_READ_1 + } +#endif + + // Sum up all accumulators accross SG and save result to SLM + unroll_for (uint seq_idx = 0; seq_idx < TARGET_SEQ_LEN_BLOCK_SIZE; seq_idx++) { + acc[seq_idx] = sub_group_reduce_add(acc[seq_idx]); + qk_local[seq_idx * SEQ_LEN_PARTITION_SIZE + seq_len] = acc[seq_idx]; + } +#else // !(IS_KV_COMPRESSED && IS_INT4_COMPRESSED) — original INT8 path uint head_idx_index = 0; #define KEY_BLOCK_SIZE 8 for (; head_idx_index + (KEY_BLOCK_SIZE * SUBGROUP_SIZE) <= K_HEAD_SIZE; head_idx_index += SUBGROUP_SIZE * KEY_BLOCK_SIZE) { @@ -424,6 +492,7 @@ KERNEL(sdpa_opt)( acc[seq_idx] = sub_group_reduce_add(acc[seq_idx]); qk_local[seq_idx * SEQ_LEN_PARTITION_SIZE + seq_len] = acc[seq_idx]; } +#endif // IS_KV_COMPRESSED && IS_INT4_COMPRESSED } { @@ -501,7 +570,7 @@ KERNEL(sdpa_opt)( if (sgid == 0) { for (uint seq_idx = 0; seq_idx < TARGET_SEQ_LEN_BLOCK_SIZE; seq_idx++) { SOFTMAX_ACCUMULATOR_TYPE wg_max = SOFTMAX_ACCUMULATOR_VAL_MIN; - + // Parallel reduction: each lane processes SUBGROUPS_PER_WG/SUBGROUP_SIZE elements const uint num_sg_per_lane = CEIL_DIV(SUBGROUPS_PER_WG, SUBGROUP_SIZE); for (uint i = 0; i < num_sg_per_lane; i++) { @@ -510,10 +579,10 @@ KERNEL(sdpa_opt)( wg_max = SOFTMAX_ACCUMULATOR_MAX_FUNC(wg_max, qk_max_vals[seq_idx * SUBGROUPS_PER_WG + sg_idx]); } } - + // Horizontal reduction across subgroup lanes wg_max = sub_group_reduce_max(wg_max); - + if (sglid == 0) { qk_max_vals[seq_idx * SUBGROUPS_PER_WG] = wg_max; } @@ -573,7 +642,7 @@ KERNEL(sdpa_opt)( if (sgid == 0) { for (uint seq_idx = 0; seq_idx < TARGET_SEQ_LEN_BLOCK_SIZE; seq_idx++) { SOFTMAX_ACCUMULATOR_TYPE wg_sum = SOFTMAX_ACCUMULATOR_VAL_ZERO; - + // Parallel reduction: each lane processes SUBGROUPS_PER_WG/SUBGROUP_SIZE elements const uint num_sg_per_lane = CEIL_DIV(SUBGROUPS_PER_WG, SUBGROUP_SIZE); for (uint i = 0; i < num_sg_per_lane; i++) { @@ -582,10 +651,10 @@ KERNEL(sdpa_opt)( wg_sum += qk_sum_vals[seq_idx * SUBGROUPS_PER_WG + sg_idx]; } } - + // Horizontal reduction across subgroup lanes wg_sum = sub_group_reduce_add(wg_sum); - + if (sglid == 0) { qk_sum_vals[seq_idx * SUBGROUPS_PER_WG] = wg_sum; } @@ -641,10 +710,34 @@ KERNEL(sdpa_opt)( #ifdef INPUT2_DIMS_ORDER uint value_offset = FUNC_CALL(get_input2_index)(OPTIONAL_SHAPE_INFO_TENSOR b0_idx, b1_idx, 0, 0, 0, 0); uint value_offset_next_seq = FUNC_CALL(get_input2_index)(OPTIONAL_SHAPE_INFO_TENSOR b0_idx, b1_idx, 0, 0, 1, 0); + #if IS_INT4_COMPRESSED + const uint value_pitch = value_offset_next_seq - value_offset; + #else const uint value_pitch = value_offset_next_seq - value_offset; + #endif #else + #if IS_INT4_COMPRESSED + const uint value_pitch = INPUT2_SIZE_X; + #else const uint value_pitch = V_HEAD_SIZE; + #endif +#endif #endif + +#if IS_INT4_COMPRESSED + // Adjacent head packing: byte = head_size_idx/2, nibble = head_size_idx%2 + // val_packed_x: base byte offset for BLOCK_READ (aligned to SUBGROUP_SIZE) + const uint val_packed_x = (head_size_idx / (2 * SUBGROUP_SIZE)) * SUBGROUP_SIZE; + // Shuffle source: which lane holds the byte containing our head element + const uint shuffle_src_p0 = (head_size_idx & (2 * SUBGROUP_SIZE - 1)) >> 1; + const uint nibble_sel_p0 = sglid & 1; + #ifndef BEAM_TABLE_TYPE + #ifdef INPUT2_DIMS_ORDER + const uint value_base_p0 = FUNC_CALL(get_input2_index)(OPTIONAL_SHAPE_INFO_TENSOR b0_idx, b1_idx, 0, 0, 0, val_packed_x); + #else + const uint value_base_p0 = INPUT2_GET_INDEX(b0_idx, b1_idx, 0, val_packed_x); + #endif + #endif // !BEAM_TABLE_TYPE #endif #if SG_SCALE_FACTOR > 1 @@ -657,15 +750,22 @@ KERNEL(sdpa_opt)( for (uint seq_len = seq_len_start; seq_len < seq_len_end; seq_len++) { #ifdef BEAM_TABLE_TYPE + #if IS_INT4_COMPRESSED + const uint b_idx = beam_table[FUNC_CALL(get_bt_index_value)(OPTIONAL_SHAPE_INFO_TENSOR b0_idx, b1_idx, 0, 0, start_partition_idx + (seq_len * SUBGROUP_SIZE) + sglid, val_packed_x)]; + uint value_offset = FUNC_CALL(get_input2_index)(OPTIONAL_SHAPE_INFO_TENSOR b_idx, b1_idx, 0, 0, start_partition_idx + (seq_len * SUBGROUP_SIZE) + sglid, val_packed_x); + #else const uint b_idx = beam_table[FUNC_CALL(get_bt_index_value)(OPTIONAL_SHAPE_INFO_TENSOR b0_idx, b1_idx, 0, 0, start_partition_idx + (seq_len * SUBGROUP_SIZE) + sglid, sgid * SUBGROUP_SIZE)]; uint value_offset = FUNC_CALL(get_input2_index)(OPTIONAL_SHAPE_INFO_TENSOR b_idx, b1_idx, 0, 0, start_partition_idx + (seq_len * SUBGROUP_SIZE) + sglid, sgid * SUBGROUP_SIZE); + #endif #else const uint b_idx = b0_idx; -#ifdef INPUT2_DIMS_ORDER - uint value_offset = FUNC_CALL(get_input2_index)(OPTIONAL_SHAPE_INFO_TENSOR b_idx, b1_idx, 0, 0, start_partition_idx + (seq_len * SUBGROUP_SIZE), head_size_idx); -#else - uint value_offset = INPUT2_GET_INDEX(b_idx, b1_idx, start_partition_idx + (seq_len * SUBGROUP_SIZE), head_size_idx); -#endif + #if IS_INT4_COMPRESSED + uint value_offset = value_base_p0 + (start_partition_idx + (seq_len * SUBGROUP_SIZE)) * value_pitch; + #elif defined(INPUT2_DIMS_ORDER) + uint value_offset = FUNC_CALL(get_input2_index)(OPTIONAL_SHAPE_INFO_TENSOR b_idx, b1_idx, 0, 0, start_partition_idx + (seq_len * SUBGROUP_SIZE), head_size_idx); + #else + uint value_offset = INPUT2_GET_INDEX(b_idx, b1_idx, start_partition_idx + (seq_len * SUBGROUP_SIZE), head_size_idx); + #endif #endif #if IS_KV_COMPRESSED @@ -683,12 +783,25 @@ KERNEL(sdpa_opt)( unroll_for (uint i = 0; i < SUBGROUP_SIZE; i++) { #ifdef BEAM_TABLE_TYPE + #if IS_INT4_COMPRESSED + const INPUT2_TYPE value_packed = (val_packed_x + sglid < K_HEAD_SIZE / 2) + ? VALUE_BLOCK_READ(value_input, sub_group_broadcast(value_offset, i)) : (INPUT2_TYPE)0; + #else const INPUT2_TYPE value_packed = VALUE_BLOCK_READ(value_input, sub_group_broadcast(value_offset, i)); + #endif #else const INPUT2_TYPE value_packed = VALUE_BLOCK_READ(value_input, value_offset); #endif -#if IS_KV_COMPRESSED && USE_ASYMMETRIC_QUANTIZATION +#if IS_KV_COMPRESSED && IS_INT4_COMPRESSED + // INT4 adjacent packing: shuffle to get correct byte, select nibble by lane parity + INPUT2_TYPE needed_byte = intel_sub_group_shuffle(value_packed, shuffle_src_p0); + char2 v_unpacked = unpack_to_char(*(uint4x2_t*)&needed_byte); + VALUE_COMPRESSION_SCALE_TYPE value_val = (nibble_sel_p0 == 0 ? + CAT(convert_, VALUE_COMPRESSION_SCALE_TYPE)(v_unpacked.s0) : + CAT(convert_, VALUE_COMPRESSION_SCALE_TYPE)(v_unpacked.s1)); + value_val = (value_val - sub_group_broadcast(comp_zp, i)) * sub_group_broadcast(comp_scale, i); +#elif IS_KV_COMPRESSED && USE_ASYMMETRIC_QUANTIZATION VALUE_COMPRESSION_SCALE_TYPE value_val = (value_packed - sub_group_broadcast(comp_zp, i)) * sub_group_broadcast(comp_scale, i); #elif IS_KV_COMPRESSED VALUE_COMPRESSION_SCALE_TYPE value_val = (value_packed * sub_group_broadcast(comp_scale, i)); @@ -717,7 +830,17 @@ KERNEL(sdpa_opt)( const uint b_idx = b0_idx; #endif -#ifdef INPUT2_DIMS_ORDER +#if IS_INT4_COMPRESSED + #ifdef BEAM_TABLE_TYPE + #ifdef INPUT2_DIMS_ORDER + const uint value_offset = FUNC_CALL(get_input2_index)(OPTIONAL_SHAPE_INFO_TENSOR b_idx, b1_idx, 0, 0, start_partition_idx + seq_len, val_packed_x); + #else + const uint value_offset = INPUT2_GET_INDEX(b_idx, b1_idx, start_partition_idx + seq_len, val_packed_x); + #endif + #else + const uint value_offset = value_base_p0 + (start_partition_idx + seq_len) * value_pitch; + #endif +#elif defined(INPUT2_DIMS_ORDER) const uint value_offset = FUNC_CALL(get_input2_index)(OPTIONAL_SHAPE_INFO_TENSOR b_idx, b1_idx, 0, 0, start_partition_idx + seq_len, head_size_idx); #else const uint value_offset = INPUT2_GET_INDEX(b_idx, b1_idx, start_partition_idx + seq_len, head_size_idx); @@ -736,8 +859,21 @@ KERNEL(sdpa_opt)( qk_val[seq_idx] = qk_local[seq_idx * SEQ_LEN_PARTITION_SIZE + seq_len]; } +#if IS_INT4_COMPRESSED + const INPUT2_TYPE value_packed = (val_packed_x + sglid < K_HEAD_SIZE / 2) + ? VALUE_BLOCK_READ(value_input, value_offset) : (INPUT2_TYPE)0; +#else const INPUT2_TYPE value_packed = VALUE_BLOCK_READ(value_input, value_offset); -#if IS_KV_COMPRESSED && USE_ASYMMETRIC_QUANTIZATION +#endif +#if IS_KV_COMPRESSED && IS_INT4_COMPRESSED + // INT4 adjacent packing: shuffle to get correct byte, select nibble by lane parity + INPUT2_TYPE needed_byte = intel_sub_group_shuffle(value_packed, shuffle_src_p0); + char2 v_rem_unpacked = unpack_to_char(*(uint4x2_t*)&needed_byte); + VALUE_COMPRESSION_SCALE_TYPE value_val = (nibble_sel_p0 == 0 ? + CAT(convert_, VALUE_COMPRESSION_SCALE_TYPE)(v_rem_unpacked.s0) : + CAT(convert_, VALUE_COMPRESSION_SCALE_TYPE)(v_rem_unpacked.s1)); + value_val = (value_val - comp_zp) * comp_scale; +#elif IS_KV_COMPRESSED && USE_ASYMMETRIC_QUANTIZATION const VALUE_COMPRESSION_SCALE_TYPE value_val = (value_packed - comp_zp) * comp_scale; #elif IS_KV_COMPRESSED const VALUE_COMPRESSION_SCALE_TYPE value_val = (value_packed * comp_scale); @@ -949,33 +1085,117 @@ inline SEQ_RANGE FUNC(calc_sliding_window_seq_range)(const SEQ_RANGE default_seq #endif #if HAS_TOKEN_TYPE_IDS - // Bidirectional attention for image token groups (e.g. Gemma3 VLM): - // Compute per-lane effective causal limit to extend the visible range for image tokens - // This is a REFERENCE implementation. +// Cooperative wg search for the first zero idx in token_type_ids. +#define FIND_FIRST_ZERO_IDX_WG_TEMPLATE(CLEAR_VAL, REDUCTION_OP, ADVANCE_EXPRESSION, EDGE_CASE_CHECK, INIT_IDX) \ + const int sgid = get_sub_group_id(); \ + const int sglid = get_sub_group_local_id(); \ + const int lid = get_local_linear_id(); \ + const int num_of_work_items = get_num_sub_groups()*get_sub_group_size(); \ + \ + const int base = start_idx; \ + int idx_this_thread = INIT_IDX; \ + int block_first_idx = CLEAR_VAL; \ + bool work_group_should_contine_loop = true; \ + while (work_group_should_contine_loop) { \ + const bool is_zero = idx_this_thread EDGE_CASE_CHECK ? token_type_ids[idx_this_thread] == 0 : true; \ + int min_idx_this_subgroup = sub_group_reduce_##REDUCTION_OP(is_zero ? idx_this_thread : CLEAR_VAL); \ + \ + if( sglid == 0 ) \ + reduction_buffer[sgid] = min_idx_this_subgroup; \ + \ + barrier(CLK_LOCAL_MEM_FENCE); \ + \ + int reduce_val = CLEAR_VAL; \ + for(int idx = sglid; idx < SUBGROUPS_PER_WG; idx += SUBGROUP_SIZE) { \ + reduce_val = REDUCTION_OP(reduce_val, reduction_buffer[idx]); \ + } \ + \ + block_first_idx = sub_group_reduce_##REDUCTION_OP(reduce_val); \ + idx_this_thread ADVANCE_EXPRESSION num_of_work_items; \ + work_group_should_contine_loop = (block_first_idx == CLEAR_VAL); \ + } \ + return block_first_idx; + + + inline int FUNC(find_first_zero_to_the_right_wg)(const __global int* token_type_ids, + __local int* reduction_buffer, int start_idx, int seq_len) { + FIND_FIRST_ZERO_IDX_WG_TEMPLATE(INT_MAX, min, +=, < seq_len, base + lid) + } + + inline int FUNC(find_first_zero_to_the_left_wg)(const __global int* token_type_ids, + __local int* reduction_buffer, int start_idx, int seq_len) { + FIND_FIRST_ZERO_IDX_WG_TEMPLATE(INT_MIN, max, -=, >= 0, base - num_of_work_items + lid) + } + + // Function calculates the range of bidirectional mask for this work item. inline SEQ_RANGE FUNC(calc_bi_dir_seq_range)( const __global int* token_type_ids, + __local int* reduction_buffer, const int seq_len, - const SEQ_RANGE default_seq_range) { + const SEQ_RANGE default_seq_range, + bool begin_needed) { + + // For this wg, each subgroup's work item min and max seq idx + // will be in the range of: + // [default_seq_range.subgroup_max - SUBGROUP_SIZE, default_seq_range.subgroup_max] + // So we can safetly assume that wg have to check: + // 1) where ends token group starting from default_seq_range.subgroup_max + // 2) where begins token group starting from default_seq_range.subgroup_max - SUBGROUP_SIZE + // becasue it will be the same for all work items. + // Wg will simply cooperatively calculate the first idx for which the token_type_id + // is equal to zero to the left and right from the above range. + + // Optimization assumptions: + // 1) default_block_range_end - default_block_range_begin <= SUBGROUP_SIZE + // 2) all subgroups work on the same default_block_range + + const int default_block_range_begin = max(0, default_seq_range.subgroup_max - SUBGROUP_SIZE); + const int default_block_range_end = default_seq_range.subgroup_max; + int found_block_range_end = default_block_range_end; + int found_block_range_begin = default_block_range_begin; + + // block cooperative search for token group end + if (token_type_ids[default_block_range_end - 1] == 1) { + found_block_range_end = FUNC_CALL(find_first_zero_to_the_right_wg)(token_type_ids, reduction_buffer, default_block_range_end, seq_len); + } + + // block cooperative search for token group start + if (token_type_ids[default_block_range_begin] == 1 && begin_needed) { + found_block_range_begin = FUNC_CALL(find_first_zero_to_the_left_wg)(token_type_ids, reduction_buffer, default_block_range_begin - 1, seq_len) + 1; + } + int token_group_end = default_seq_range.max; int token_group_begin = default_seq_range.max; + // Special case: image group is less than subgroup size. if (token_type_ids[token_group_end] == 1) { int new_group_end = token_group_end + 1; - while (new_group_end < seq_len && token_type_ids[new_group_end] == 1) { + while (new_group_end < default_block_range_end && token_type_ids[new_group_end] == 1) { new_group_end++; } token_group_end = new_group_end - 1; - int new_group_begin = token_group_begin - 1; - while (new_group_begin >= 0 && token_type_ids[new_group_begin] == 1) { - new_group_begin--; + if (token_group_end == (default_block_range_end - 1)) { + token_group_end = found_block_range_end - 1; + } + + if (begin_needed) { + int new_group_begin = token_group_begin - 1; + while (new_group_begin >= default_block_range_begin && token_type_ids[new_group_begin] == 1) { + new_group_begin--; + } + token_group_begin = new_group_begin + 1; + + if (token_group_begin == default_block_range_begin) { + token_group_begin = found_block_range_begin; + } } - token_group_begin = new_group_begin + 1; } + SEQ_RANGE range; range.min = token_group_begin; range.max = token_group_end; - range.subgroup_max = sub_group_reduce_max(token_group_end) + 1; + range.subgroup_max = found_block_range_end; return range; } #endif @@ -1000,6 +1220,9 @@ KERNEL(sdpa_opt)( #endif #if IS_PAGED_ATTENTION && HAS_TOKEN_TYPE_IDS const __global int* token_type_ids, +#endif +#if HAS_SINK_INPUT + const __global SINK_DATA_T* sink_ptr, #endif __global OUTPUT_TYPE* output, #if IS_KV_COMPRESSED @@ -1084,6 +1307,18 @@ KERNEL(sdpa_opt)( __local SOFTMAX_ACCUMULATOR_TYPE slm_max_val_cur[TARGET_SEQ_LEN_BLOCK_SIZE]; #endif +#ifdef HAS_TOKEN_TYPE_IDS + // NOTE: if allocation of reduce_buffer causes SLM spill, + // we can reuse any other big enough allocated buffer. + // Having separate buffer makes the code easier to understand. + __local int reduce_buffer[SUBGROUPS_PER_WG]; + + +#if SUBGROUPS_PER_WG * SUBGROUP_SIZE != SEQ_LEN_PARTITION_SIZE + #error "sdpa_opt.cl: Unsupported configuration with token type ids. SUBGROUPS_PER_WG * SUBGROUP_SIZE must be equal to SEQ_LEN_PARTITION_SIZE." +#endif +#endif + #if IS_PAGED_ATTENTION const uint block_start_pos = blocked_indexes_start[target_seq_dim]; const uint block_end_pos = blocked_indexes_end[target_seq_dim]; @@ -1095,14 +1330,16 @@ KERNEL(sdpa_opt)( #if IS_CAUSAL const SEQ_RANGE default_this_work_item_seq_range = {0, target_seq_idx + sglid, target_seq_idx + seq_idx_end}; SEQ_RANGE this_work_item_seq_range_temp = default_this_work_item_seq_range; - + #if IS_PAGED_ATTENTION #if HAS_TOKEN_TYPE_IDS const SEQ_RANGE bi_dir_range = FUNC_CALL(calc_bi_dir_seq_range)(token_type_ids, + reduce_buffer, SOURCE_SEQ_LEN, - default_this_work_item_seq_range); + default_this_work_item_seq_range, + SLIDING_WINDOW_SIZE != 0); this_work_item_seq_range_temp = bi_dir_range; - #endif //< HAS_TOKEN_TYPE_IDS + #endif //< HAS_TOKEN_TYPE_IDS #if SLIDING_WINDOW_SIZE != 0 const SEQ_RANGE sliding_window_range = FUNC_CALL(calc_sliding_window_seq_range)(default_this_work_item_seq_range); @@ -1274,6 +1511,16 @@ KERNEL(sdpa_opt)( // Q*K calculation loop MAKE_VECTOR_TYPE(OUTPUT_TYPE, TARGET_SEQ_LEN_BLOCK_SIZE) output_acc = OUTPUT_VAL_ZERO; +#if IS_INT4_COMPRESSED && !defined(BEAM_TABLE_TYPE) + #ifdef INPUT1_DIMS_ORDER + const uint key_base_s1 = FUNC_CALL(get_input1_index)(OPTIONAL_SHAPE_INFO_TENSOR b0_idx, b1_idx, 0, 0, 0, 0); + const uint key_packed_pitch_s1 = FUNC_CALL(get_input1_index)(OPTIONAL_SHAPE_INFO_TENSOR b0_idx, b1_idx, 0, 0, 1, 0) - key_base_s1; + #else + const uint key_base_s1 = INPUT1_GET_INDEX(b0_idx, b1_idx, 0, 0); + const uint key_packed_pitch_s1 = INPUT1_SIZE_X; + #endif +#endif + __attribute__((opencl_unroll_hint(1))) for (uint start_partition_idx = 0; start_partition_idx < SOURCE_SEQ_LEN; start_partition_idx += SEQ_LEN_PARTITION_SIZE) { const uint seq_len = start_partition_idx + sgid * SUBGROUP_SIZE; @@ -1294,18 +1541,27 @@ KERNEL(sdpa_opt)( const uint heads_dim = num_heads_dim; #endif #define KEY_SEQ_OFFSET subsequence_begins[gws_seq_indexes_correspondence[target_seq_dim]] +#if IS_INT4_COMPRESSED && !defined(BEAM_TABLE_TYPE) + // INT4 adjacent packing: use physical pitch from tensor layout + uint key_offset = key_base_s1 + seq_len * key_packed_pitch_s1; + const uint key_pitch = key_packed_pitch_s1; +#else const uint key_pitch = (K_HEAD_SIZE * NUM_KV_HEADS + INPUT1_PAD_BEFORE_FEATURE_NUM + INPUT1_PAD_AFTER_FEATURE_NUM); uint key_offset = INPUT1_OFFSET + KEY_SEQ_OFFSET * key_pitch + heads_dim * K_HEAD_SIZE + seq_len * key_pitch; +#endif #else // !IS_PAGED_ATTENTION #ifdef BEAM_TABLE_TYPE const uint b_idx = beam_table[FUNC_CALL(get_bt_index_key)(OPTIONAL_SHAPE_INFO_TENSOR b0_idx, b1_idx, 0, 0, seq_len + sglid, 0)]; const uint key_offset = FUNC_CALL(get_input1_index)(OPTIONAL_SHAPE_INFO_TENSOR b_idx, b1_idx, 0, 0, seq_len + sglid, 0); #else const uint b_idx = b0_idx; - #ifdef INPUT1_DIMS_ORDER + #if IS_INT4_COMPRESSED + uint key_offset = key_base_s1 + seq_len * key_packed_pitch_s1; + const uint key_pitch = key_packed_pitch_s1; + #elif defined(INPUT1_DIMS_ORDER) uint key_offset = FUNC_CALL(get_input1_index)(OPTIONAL_SHAPE_INFO_TENSOR b0_idx, b1_idx, 0, 0, seq_len, 0); uint key_offset_next_seq = FUNC_CALL(get_input1_index)(OPTIONAL_SHAPE_INFO_TENSOR b0_idx, b1_idx, 0, 0, seq_len + 1, 0); const uint key_pitch = key_offset_next_seq - key_offset; @@ -1336,6 +1592,71 @@ KERNEL(sdpa_opt)( KEY_COMPRESSION_SCALE_TYPE comp_zp = GET_ZP(key_zp, key_scale, comp_offset); #endif #endif +#if IS_KV_COMPRESSED && IS_INT4_COMPRESSED + { + // INT4 adjacent packing: scalar read gives 16 head-dim bytes per lane. + // Lane i holds key at head dims (hi + 2*i) and (hi + 2*i + 1). + // Load query values matching adjacent-packed head dims. + #define KEY_BLOCK_READ(ptr, offset) ((ptr)[(offset) + sglid]) + #define QUERY_VEC MAKE_VECTOR_TYPE(INPUT0_TYPE, TARGET_SEQ_LEN_BLOCK_SIZE) + for (uint hi = 0; hi + 2 * SUBGROUP_SIZE <= K_HEAD_SIZE; hi += 2 * SUBGROUP_SIZE) { + QUERY_VEC qvec_lo, qvec_hi; + // qvec_lo[i] = query at head dim (hi + 2*i) for target token sglid + // qvec_hi[i] = query at head dim (hi + 2*i + 1) for target token sglid + unroll_for (uint i = 0; i < SUBGROUP_SIZE; i++) { + qvec_lo[i] = slm_query[(hi + 2 * i) * TARGET_SEQ_LEN_BLOCK_SIZE + sglid]; + qvec_hi[i] = slm_query[(hi + 2 * i + 1) * TARGET_SEQ_LEN_BLOCK_SIZE + sglid]; + } + unroll_for (uint key_row_idx = 0; key_row_idx < TARGET_SEQ_LEN_BLOCK_SIZE; key_row_idx++) { +#ifdef BEAM_TABLE_TYPE + const INPUT1_TYPE packed_byte = KEY_BLOCK_READ(key_input, sub_group_broadcast(key_offset, key_row_idx) + hi / 2); +#else + const INPUT1_TYPE packed_byte = KEY_BLOCK_READ(key_input, key_offset + key_row_idx * key_pitch + hi / 2); +#endif + char2 unpacked = unpack_to_char(*(uint4x2_t*)&packed_byte); + KEY_COMPRESSION_SCALE_TYPE key_lo = (TO_KEY_COMPRESSION_SCALE_TYPE(unpacked.s0) - sub_group_broadcast(comp_zp, key_row_idx)) * sub_group_broadcast(comp_scale, key_row_idx); + KEY_COMPRESSION_SCALE_TYPE key_hi = (TO_KEY_COMPRESSION_SCALE_TYPE(unpacked.s1) - sub_group_broadcast(comp_zp, key_row_idx)) * sub_group_broadcast(comp_scale, key_row_idx); + unroll_for (uint i = 0; i < SUBGROUP_SIZE; i++) { + qk_acc[key_row_idx] = mad(sub_group_broadcast(key_lo, i), qvec_lo[i], qk_acc[key_row_idx]); + qk_acc[key_row_idx] = mad(sub_group_broadcast(key_hi, i), qvec_hi[i], qk_acc[key_row_idx]); + } + } + } + // Handle leftover chunk when K_HEAD_SIZE is not a multiple of 2*SUBGROUP_SIZE +#if (K_HEAD_SIZE % (2 * SUBGROUP_SIZE)) != 0 + { + const uint hi = (K_HEAD_SIZE / (2 * SUBGROUP_SIZE)) * (2 * SUBGROUP_SIZE); + QUERY_VEC qvec_lo, qvec_hi; + unroll_for (uint i = 0; i < SUBGROUP_SIZE; i++) { + qvec_lo[i] = (hi + 2 * i < K_HEAD_SIZE) ? + slm_query[(hi + 2 * i) * TARGET_SEQ_LEN_BLOCK_SIZE + sglid] : (INPUT0_TYPE)0; + qvec_hi[i] = (hi + 2 * i + 1 < K_HEAD_SIZE) ? + slm_query[(hi + 2 * i + 1) * TARGET_SEQ_LEN_BLOCK_SIZE + sglid] : (INPUT0_TYPE)0; + } + unroll_for (uint key_row_idx = 0; key_row_idx < TARGET_SEQ_LEN_BLOCK_SIZE; key_row_idx++) { +#ifdef BEAM_TABLE_TYPE + const INPUT1_TYPE packed_byte = (hi / 2 + sglid < K_HEAD_SIZE / 2) + ? KEY_BLOCK_READ(key_input, sub_group_broadcast(key_offset, key_row_idx) + hi / 2) : (INPUT1_TYPE)0; +#else + const INPUT1_TYPE packed_byte = (hi / 2 + sglid < K_HEAD_SIZE / 2) + ? KEY_BLOCK_READ(key_input, key_offset + key_row_idx * key_pitch + hi / 2) : (INPUT1_TYPE)0; +#endif + char2 unpacked = unpack_to_char(*(uint4x2_t*)&packed_byte); + KEY_COMPRESSION_SCALE_TYPE key_lo = (TO_KEY_COMPRESSION_SCALE_TYPE(unpacked.s0) - sub_group_broadcast(comp_zp, key_row_idx)) * sub_group_broadcast(comp_scale, key_row_idx); + KEY_COMPRESSION_SCALE_TYPE key_hi = (TO_KEY_COMPRESSION_SCALE_TYPE(unpacked.s1) - sub_group_broadcast(comp_zp, key_row_idx)) * sub_group_broadcast(comp_scale, key_row_idx); + KEY_COMPRESSION_SCALE_TYPE lo_mask = (hi + 2 * sglid < K_HEAD_SIZE) ? (KEY_COMPRESSION_SCALE_TYPE)1 : (KEY_COMPRESSION_SCALE_TYPE)0; + KEY_COMPRESSION_SCALE_TYPE hi_mask = (hi + 2 * sglid + 1 < K_HEAD_SIZE) ? (KEY_COMPRESSION_SCALE_TYPE)1 : (KEY_COMPRESSION_SCALE_TYPE)0; + key_lo *= lo_mask; + key_hi *= hi_mask; + unroll_for (uint i = 0; i < SUBGROUP_SIZE; i++) { + qk_acc[key_row_idx] = mad(sub_group_broadcast(key_lo, i), qvec_lo[i], qk_acc[key_row_idx]); + qk_acc[key_row_idx] = mad(sub_group_broadcast(key_hi, i), qvec_hi[i], qk_acc[key_row_idx]); + } + } + } +#endif + } +#else // !IS_INT4_COMPRESSED uint head_idx_index = 0; __attribute__((opencl_unroll_hint(1))) for (; head_idx_index + SUBGROUP_SIZE <= K_HEAD_SIZE; head_idx_index += SUBGROUP_SIZE) { @@ -1383,7 +1704,7 @@ KERNEL(sdpa_opt)( INPUT1_TYPE key_packed = (sglid < K_HEAD_SIZE_LEFTOVER) ? key_input[key_offset + key_row_idx * key_pitch + head_idx_index + sglid] : INPUT1_VAL_ZERO; #endif #if IS_KV_COMPRESSED && USE_ASYMMETRIC_QUANTIZATION - KEY_COMPRESSION_SCALE_TYPE key_vals = (TO_KEY_COMPRESSION_SCALE_TYPE(key_packed) - sub_group_broadcast(comp_zp, key_row_idx)) * sub_group_broadcast(comp_scale, key_row_idx);j + KEY_COMPRESSION_SCALE_TYPE key_vals = (TO_KEY_COMPRESSION_SCALE_TYPE(key_packed) - sub_group_broadcast(comp_zp, key_row_idx)) * sub_group_broadcast(comp_scale, key_row_idx); #elif IS_KV_COMPRESSED KEY_COMPRESSION_SCALE_TYPE key_vals = (TO_KEY_COMPRESSION_SCALE_TYPE(key_packed) * sub_group_broadcast(comp_scale, key_row_idx)); #else @@ -1394,15 +1715,85 @@ KERNEL(sdpa_opt)( } } #endif +#endif // !IS_INT4_COMPRESSED } else if (seq_len_calc_size > 0) { #if IS_KV_COMPRESSED const uint comp_offset = GET_COMPRESSION_INDEX(KEY_COMPRESSION_SCALE, b_idx, b1_idx / BROADCAST_GROUP_SIZE, seq_len + min(sglid, (uint)seq_len_calc_size - 1), 0); - // const uint comp_offset = GET_COMPRESSION_INDEX(KEY_COMPRESSION_SCALE, b_idx, b1_idx / BROADCAST_GROUP_SIZE, seq_len + sglid, 0); KEY_COMPRESSION_SCALE_TYPE comp_scale = GET_SCALE(key_zp, key_scale, comp_offset); #if USE_ASYMMETRIC_QUANTIZATION KEY_COMPRESSION_SCALE_TYPE comp_zp = GET_ZP(key_zp, key_scale, comp_offset); #endif #endif +#if IS_KV_COMPRESSED && IS_INT4_COMPRESSED + { + // INT4 partial block: process 2*SUBGROUP_SIZE logical head dims per iteration + #define KEY_BLOCK_READ(ptr, offset) ((ptr)[(offset) + sglid]) + #define QUERY_VEC_TYPE MAKE_VECTOR_TYPE(INPUT0_TYPE, TARGET_SEQ_LEN_BLOCK_SIZE) + const uint key_pitch_int4 = INPUT1_SIZE_X; + for (uint hi = 0; hi + 2 * SUBGROUP_SIZE <= K_HEAD_SIZE; hi += 2 * SUBGROUP_SIZE) { + QUERY_VEC_TYPE qvec_lo, qvec_hi; + unroll_for (uint i = 0; i < SUBGROUP_SIZE; i++) { + qvec_lo[i] = slm_query[(hi + 2 * i) * TARGET_SEQ_LEN_BLOCK_SIZE + sglid]; + qvec_hi[i] = slm_query[(hi + 2 * i + 1) * TARGET_SEQ_LEN_BLOCK_SIZE + sglid]; + } + unroll_for (uint key_row_idx = 0; key_row_idx < TARGET_SEQ_LEN_BLOCK_SIZE; key_row_idx++) { + KEY_COMPRESSION_SCALE_TYPE key_lo = 0; + KEY_COMPRESSION_SCALE_TYPE key_hi = 0; + if (key_row_idx < seq_len_calc_size) { +#ifdef BEAM_TABLE_TYPE + const INPUT1_TYPE packed_byte = KEY_BLOCK_READ(key_input, sub_group_broadcast(key_offset, key_row_idx) + hi / 2); +#else + const INPUT1_TYPE packed_byte = KEY_BLOCK_READ(key_input, key_offset + key_row_idx * key_pitch_int4 + hi / 2); +#endif + char2 unpacked = unpack_to_char(*(uint4x2_t*)&packed_byte); + key_lo = (TO_KEY_COMPRESSION_SCALE_TYPE(unpacked.s0) - sub_group_broadcast(comp_zp, key_row_idx)) * sub_group_broadcast(comp_scale, key_row_idx); + key_hi = (TO_KEY_COMPRESSION_SCALE_TYPE(unpacked.s1) - sub_group_broadcast(comp_zp, key_row_idx)) * sub_group_broadcast(comp_scale, key_row_idx); + } + unroll_for (uint i = 0; i < SUBGROUP_SIZE; i++) { + qk_acc[key_row_idx] = mad(sub_group_broadcast(key_lo, i), qvec_lo[i], qk_acc[key_row_idx]); + qk_acc[key_row_idx] = mad(sub_group_broadcast(key_hi, i), qvec_hi[i], qk_acc[key_row_idx]); + } + } + } + // Handle leftover chunk when K_HEAD_SIZE is not a multiple of 2*SUBGROUP_SIZE +#if (K_HEAD_SIZE % (2 * SUBGROUP_SIZE)) != 0 + { + const uint hi = (K_HEAD_SIZE / (2 * SUBGROUP_SIZE)) * (2 * SUBGROUP_SIZE); + QUERY_VEC_TYPE qvec_lo, qvec_hi; + unroll_for (uint i = 0; i < SUBGROUP_SIZE; i++) { + qvec_lo[i] = (hi + 2 * i < K_HEAD_SIZE) ? + slm_query[(hi + 2 * i) * TARGET_SEQ_LEN_BLOCK_SIZE + sglid] : (INPUT0_TYPE)0; + qvec_hi[i] = (hi + 2 * i + 1 < K_HEAD_SIZE) ? + slm_query[(hi + 2 * i + 1) * TARGET_SEQ_LEN_BLOCK_SIZE + sglid] : (INPUT0_TYPE)0; + } + unroll_for (uint key_row_idx = 0; key_row_idx < TARGET_SEQ_LEN_BLOCK_SIZE; key_row_idx++) { + KEY_COMPRESSION_SCALE_TYPE key_lo = 0; + KEY_COMPRESSION_SCALE_TYPE key_hi = 0; + if (key_row_idx < seq_len_calc_size) { +#ifdef BEAM_TABLE_TYPE + const INPUT1_TYPE packed_byte = (hi / 2 + sglid < K_HEAD_SIZE / 2) + ? KEY_BLOCK_READ(key_input, sub_group_broadcast(key_offset, key_row_idx) + hi / 2) : (INPUT1_TYPE)0; +#else + const INPUT1_TYPE packed_byte = (hi / 2 + sglid < K_HEAD_SIZE / 2) + ? KEY_BLOCK_READ(key_input, key_offset + key_row_idx * key_pitch_int4 + hi / 2) : (INPUT1_TYPE)0; +#endif + char2 unpacked = unpack_to_char(*(uint4x2_t*)&packed_byte); + key_lo = (TO_KEY_COMPRESSION_SCALE_TYPE(unpacked.s0) - sub_group_broadcast(comp_zp, key_row_idx)) * sub_group_broadcast(comp_scale, key_row_idx); + key_hi = (TO_KEY_COMPRESSION_SCALE_TYPE(unpacked.s1) - sub_group_broadcast(comp_zp, key_row_idx)) * sub_group_broadcast(comp_scale, key_row_idx); + } + KEY_COMPRESSION_SCALE_TYPE lo_mask = (hi + 2 * sglid < K_HEAD_SIZE) ? (KEY_COMPRESSION_SCALE_TYPE)1 : (KEY_COMPRESSION_SCALE_TYPE)0; + KEY_COMPRESSION_SCALE_TYPE hi_mask = (hi + 2 * sglid + 1 < K_HEAD_SIZE) ? (KEY_COMPRESSION_SCALE_TYPE)1 : (KEY_COMPRESSION_SCALE_TYPE)0; + key_lo *= lo_mask; + key_hi *= hi_mask; + unroll_for (uint i = 0; i < SUBGROUP_SIZE; i++) { + qk_acc[key_row_idx] = mad(sub_group_broadcast(key_lo, i), qvec_lo[i], qk_acc[key_row_idx]); + qk_acc[key_row_idx] = mad(sub_group_broadcast(key_hi, i), qvec_hi[i], qk_acc[key_row_idx]); + } + } + } +#endif + } +#else // !IS_INT4_COMPRESSED uint head_idx_index = 0; __attribute__((opencl_unroll_hint(1))) for (; head_idx_index + SUBGROUP_SIZE <= K_HEAD_SIZE; head_idx_index += SUBGROUP_SIZE) { @@ -1479,7 +1870,7 @@ KERNEL(sdpa_opt)( const INPUT1_TYPE key_packed = (sglid < K_HEAD_SIZE_LEFTOVER) ? key_input[key_offset + key_row_idx * key_pitch + head_idx_index + sglid] : INPUT1_VAL_ZERO; #endif #if IS_KV_COMPRESSED && USE_ASYMMETRIC_QUANTIZATION - KEY_COMPRESSION_SCALE_TYPE key_val = (TO_KEY_COMPRESSION_SCALE_TYPE(key_packed) - sub_group_broadcast(comp_zp, key_row_idx)) * sub_group_broadcast(comp_scale, key_row_idx);j + KEY_COMPRESSION_SCALE_TYPE key_val = (TO_KEY_COMPRESSION_SCALE_TYPE(key_packed) - sub_group_broadcast(comp_zp, key_row_idx)) * sub_group_broadcast(comp_scale, key_row_idx); #elif IS_KV_COMPRESSED KEY_COMPRESSION_SCALE_TYPE key_val = (TO_KEY_COMPRESSION_SCALE_TYPE(key_packed) * sub_group_broadcast(comp_scale, key_row_idx)); #else @@ -1490,6 +1881,7 @@ KERNEL(sdpa_opt)( } } #endif // K_HEAD_SIZE_LEFTOVER +#endif // !IS_INT4_COMPRESSED } // softmax_scale @@ -1499,7 +1891,7 @@ KERNEL(sdpa_opt)( #if IS_CAUSAL // causal mask: valid only if m >= n const int curr_seq_idx = seq_len + i; - if ((curr_seq_idx <= this_work_item_seq_range.max) + if ((curr_seq_idx <= this_work_item_seq_range.max) && (curr_seq_idx >= this_work_item_seq_range.min)) { #endif // IS_CAUSAL @@ -1657,7 +2049,7 @@ KERNEL(sdpa_opt)( SOFTMAX_ACCUMULATOR_TYPE exp_sum_new = SOFTMAX_ACCUMULATOR_VAL_ZERO; - for (int i = 0; i < TARGET_SEQ_LEN_BLOCK_SIZE; i++) { + for (int i = 0; i < TARGET_SEQ_LEN_BLOCK_SIZE; i++) { #if IS_CAUSAL const int curr_seq_idx = seq_len + i; if ((curr_seq_idx <= this_work_item_seq_range.max) @@ -1759,12 +2151,34 @@ KERNEL(sdpa_opt)( #ifdef INPUT2_DIMS_ORDER uint value_offset_base = FUNC_CALL(get_input2_index)(OPTIONAL_SHAPE_INFO_TENSOR b0_idx, b1_idx, 0, 0, 0, 0); uint value_offset_next_seq = FUNC_CALL(get_input2_index)(OPTIONAL_SHAPE_INFO_TENSOR b0_idx, b1_idx, 0, 0, 1, 0); + #if IS_INT4_COMPRESSED + const uint value_pitch = value_offset_next_seq - value_offset_base; + #else const uint value_pitch = value_offset_next_seq - value_offset_base; + #endif #else + #if IS_INT4_COMPRESSED + const uint value_pitch = INPUT2_SIZE_X; + #else const uint value_pitch = V_HEAD_SIZE; + #endif #endif #endif +#if IS_INT4_COMPRESSED && !defined(IS_PAGED_ATTENTION) + const uint val_packed_x_s1 = (head_size_idx / (2 * SUBGROUP_SIZE)) * SUBGROUP_SIZE; + // Adjacent head packing: shuffle to get correct byte, select nibble by lane parity + const uint shuffle_src_s1 = (head_size_idx & (2 * SUBGROUP_SIZE - 1)) >> 1; + const uint nibble_sel_s1 = sglid & 1; + #ifndef BEAM_TABLE_TYPE + #ifdef INPUT2_DIMS_ORDER + const uint value_base_s1 = FUNC_CALL(get_input2_index)(OPTIONAL_SHAPE_INFO_TENSOR b0_idx, b1_idx, 0, 0, 0, val_packed_x_s1); + #else + const uint value_base_s1 = INPUT2_GET_INDEX(b0_idx, b1_idx, 0, val_packed_x_s1); + #endif + #endif // !BEAM_TABLE_TYPE +#endif + if (partition_seq_len == SEQ_LEN_PARTITION_SIZE) { uint seq_len_start = (sgid / (SUBGROUPS_PER_WG / SG_SCALE_FACTOR)) * (SEQ_LEN_PARTITION_SIZE / SG_SCALE_FACTOR); for (uint seq_len = seq_len_start; seq_len < seq_len_start + (SEQ_LEN_PARTITION_SIZE / SG_SCALE_FACTOR); seq_len += SUBGROUP_SIZE) { @@ -1781,11 +2195,18 @@ KERNEL(sdpa_opt)( (start_partition_idx + (seq_len)) * value_pitch + head_size_idx; #else #ifdef BEAM_TABLE_TYPE + #if IS_INT4_COMPRESSED + const uint b_idx = beam_table[FUNC_CALL(get_bt_index_value)(OPTIONAL_SHAPE_INFO_TENSOR b0_idx, b1_idx, 0, 0, start_partition_idx + (seq_len) + sglid, val_packed_x_s1)]; + const uint value_offset = FUNC_CALL(get_input2_index)(OPTIONAL_SHAPE_INFO_TENSOR b_idx, b1_idx, 0, 0, start_partition_idx + (seq_len) + sglid, val_packed_x_s1); + #else const uint b_idx = beam_table[FUNC_CALL(get_bt_index_value)(OPTIONAL_SHAPE_INFO_TENSOR b0_idx, b1_idx, 0, 0, start_partition_idx + (seq_len) + sglid, sgid * SUBGROUP_SIZE)]; const uint value_offset = FUNC_CALL(get_input2_index)(OPTIONAL_SHAPE_INFO_TENSOR b_idx, b1_idx, 0, 0, start_partition_idx + (seq_len) + sglid, sgid * SUBGROUP_SIZE); + #endif #else const uint b_idx = b0_idx; - #ifdef INPUT2_DIMS_ORDER + #if IS_INT4_COMPRESSED + uint value_offset = value_base_s1 + (start_partition_idx + (seq_len)) * value_pitch; + #elif defined(INPUT2_DIMS_ORDER) uint value_offset = FUNC_CALL(get_input2_index)(OPTIONAL_SHAPE_INFO_TENSOR b0_idx, b1_idx, 0, 0, start_partition_idx + (seq_len), head_size_idx); #else uint value_offset = INPUT2_GET_INDEX(b0_idx, b1_idx, start_partition_idx + (seq_len), head_size_idx); @@ -1815,7 +2236,14 @@ KERNEL(sdpa_opt)( const INPUT2_TYPE value_packed = VALUE_BLOCK_READ(value_input, value_offset); #endif - #if IS_KV_COMPRESSED && USE_ASYMMETRIC_QUANTIZATION + #if IS_KV_COMPRESSED && IS_INT4_COMPRESSED + INPUT2_TYPE needed_byte = intel_sub_group_shuffle(value_packed, shuffle_src_s1); + char2 v_unpacked = unpack_to_char(*(uint4x2_t*)&needed_byte); + VALUE_COMPRESSION_SCALE_TYPE value_val = (nibble_sel_s1 == 0 ? + CAT(convert_, VALUE_COMPRESSION_SCALE_TYPE)(v_unpacked.s0) : + CAT(convert_, VALUE_COMPRESSION_SCALE_TYPE)(v_unpacked.s1)); + value_val = (value_val - sub_group_broadcast(comp_zp, i)) * sub_group_broadcast(comp_scale, i); + #elif IS_KV_COMPRESSED && USE_ASYMMETRIC_QUANTIZATION VALUE_COMPRESSION_SCALE_TYPE value_val = (value_packed - sub_group_broadcast(comp_zp, i)) * sub_group_broadcast(comp_scale, i); #elif IS_KV_COMPRESSED VALUE_COMPRESSION_SCALE_TYPE value_val = (value_packed * sub_group_broadcast(comp_scale, i)); @@ -1838,7 +2266,14 @@ KERNEL(sdpa_opt)( #else const INPUT2_TYPE value_packed = value_input[value_offset]; #endif - #if IS_KV_COMPRESSED && USE_ASYMMETRIC_QUANTIZATION + #if IS_KV_COMPRESSED && IS_INT4_COMPRESSED + INPUT2_TYPE needed_byte_elt = intel_sub_group_shuffle(value_packed, shuffle_src_s1); + char2 v_unpacked_elt = unpack_to_char(*(uint4x2_t*)&needed_byte_elt); + VALUE_COMPRESSION_SCALE_TYPE value_val = (nibble_sel_s1 == 0 ? + CAT(convert_, VALUE_COMPRESSION_SCALE_TYPE)(v_unpacked_elt.s0) : + CAT(convert_, VALUE_COMPRESSION_SCALE_TYPE)(v_unpacked_elt.s1)); + value_val = (value_val - sub_group_broadcast(comp_zp, i)) * sub_group_broadcast(comp_scale, i); + #elif IS_KV_COMPRESSED && USE_ASYMMETRIC_QUANTIZATION VALUE_COMPRESSION_SCALE_TYPE value_val = (value_packed - sub_group_broadcast(comp_zp, i)) * sub_group_broadcast(comp_scale, i); #elif IS_KV_COMPRESSED VALUE_COMPRESSION_SCALE_TYPE value_val = (value_packed * sub_group_broadcast(comp_scale, i)); @@ -1874,11 +2309,18 @@ KERNEL(sdpa_opt)( (start_partition_idx + (seq_len * SUBGROUP_SIZE)) * value_pitch + head_size_idx; #else // !IS_PAGED_ATTENTION #ifdef BEAM_TABLE_TYPE + #if IS_INT4_COMPRESSED + const uint b_idx = beam_table[FUNC_CALL(get_bt_index_value)(OPTIONAL_SHAPE_INFO_TENSOR b0_idx, b1_idx, 0, 0, start_partition_idx + (seq_len * SUBGROUP_SIZE) + sglid, val_packed_x_s1)]; + uint value_offset = FUNC_CALL(get_input2_index)(OPTIONAL_SHAPE_INFO_TENSOR b_idx, b1_idx, 0, 0, start_partition_idx + (seq_len * SUBGROUP_SIZE) + sglid, val_packed_x_s1); + #else const uint b_idx = beam_table[FUNC_CALL(get_bt_index_value)(OPTIONAL_SHAPE_INFO_TENSOR b0_idx, b1_idx, 0, 0, start_partition_idx + (seq_len * SUBGROUP_SIZE) + sglid, sgid * SUBGROUP_SIZE)]; uint value_offset = FUNC_CALL(get_input2_index)(OPTIONAL_SHAPE_INFO_TENSOR b_idx, b1_idx, 0, 0, start_partition_idx + (seq_len * SUBGROUP_SIZE) + sglid, sgid * SUBGROUP_SIZE); + #endif #else const uint b_idx = b0_idx; - #ifdef INPUT2_DIMS_ORDER + #if IS_INT4_COMPRESSED + uint value_offset = value_base_s1 + (start_partition_idx + (seq_len * SUBGROUP_SIZE)) * value_pitch; + #elif defined(INPUT2_DIMS_ORDER) uint value_offset = FUNC_CALL(get_input2_index)(OPTIONAL_SHAPE_INFO_TENSOR b0_idx, b1_idx, 0, 0, start_partition_idx + (seq_len * SUBGROUP_SIZE), head_size_idx); #else uint value_offset = INPUT2_GET_INDEX(b0_idx, b1_idx, start_partition_idx + (seq_len * SUBGROUP_SIZE), head_size_idx); @@ -1910,7 +2352,14 @@ KERNEL(sdpa_opt)( const INPUT2_TYPE value_packed = VALUE_BLOCK_READ(value_input, value_offset); #endif - #if IS_KV_COMPRESSED && USE_ASYMMETRIC_QUANTIZATION + #if IS_KV_COMPRESSED && IS_INT4_COMPRESSED + INPUT2_TYPE needed_byte = intel_sub_group_shuffle(value_packed, shuffle_src_s1); + char2 v_unpacked = unpack_to_char(*(uint4x2_t*)&needed_byte); + VALUE_COMPRESSION_SCALE_TYPE value_val = (nibble_sel_s1 == 0 ? + CAT(convert_, VALUE_COMPRESSION_SCALE_TYPE)(v_unpacked.s0) : + CAT(convert_, VALUE_COMPRESSION_SCALE_TYPE)(v_unpacked.s1)); + value_val = (value_val - sub_group_broadcast(comp_zp, i)) * sub_group_broadcast(comp_scale, i); + #elif IS_KV_COMPRESSED && USE_ASYMMETRIC_QUANTIZATION VALUE_COMPRESSION_SCALE_TYPE value_val = (value_packed - sub_group_broadcast(comp_zp, i)) * sub_group_broadcast(comp_scale, i); #elif IS_KV_COMPRESSED VALUE_COMPRESSION_SCALE_TYPE value_val = (value_packed * sub_group_broadcast(comp_scale, i)); @@ -1933,7 +2382,14 @@ KERNEL(sdpa_opt)( #else const INPUT2_TYPE value_packed = value_input[value_offset]; #endif - #if IS_KV_COMPRESSED && USE_ASYMMETRIC_QUANTIZATION + #if IS_KV_COMPRESSED && IS_INT4_COMPRESSED + INPUT2_TYPE needed_byte_elt = intel_sub_group_shuffle(value_packed, shuffle_src_s1); + char2 v_unpacked_elt = unpack_to_char(*(uint4x2_t*)&needed_byte_elt); + VALUE_COMPRESSION_SCALE_TYPE value_val = (nibble_sel_s1 == 0 ? + CAT(convert_, VALUE_COMPRESSION_SCALE_TYPE)(v_unpacked_elt.s0) : + CAT(convert_, VALUE_COMPRESSION_SCALE_TYPE)(v_unpacked_elt.s1)); + value_val = (value_val - sub_group_broadcast(comp_zp, i)) * sub_group_broadcast(comp_scale, i); + #elif IS_KV_COMPRESSED && USE_ASYMMETRIC_QUANTIZATION VALUE_COMPRESSION_SCALE_TYPE value_val = (value_packed - sub_group_broadcast(comp_zp, i)) * sub_group_broadcast(comp_scale, i); #elif IS_KV_COMPRESSED VALUE_COMPRESSION_SCALE_TYPE value_val = (value_packed * sub_group_broadcast(comp_scale, i)); @@ -1972,11 +2428,18 @@ KERNEL(sdpa_opt)( #else // !IS_PAGED_ATTENTION #ifdef BEAM_TABLE_TYPE + #if IS_INT4_COMPRESSED + const uint b_idx = beam_table[FUNC_CALL(get_bt_index_value)(OPTIONAL_SHAPE_INFO_TENSOR b0_idx, b1_idx, 0, 0, start_partition_idx + seq_len_leftovers_start + sglid, val_packed_x_s1)]; + const uint value_offset = FUNC_CALL(get_input2_index)(OPTIONAL_SHAPE_INFO_TENSOR b_idx, b1_idx, 0, 0, start_partition_idx + seq_len_leftovers_start + sglid, val_packed_x_s1); + #else const uint b_idx = beam_table[FUNC_CALL(get_bt_index_value)(OPTIONAL_SHAPE_INFO_TENSOR b0_idx, b1_idx, 0, 0, start_partition_idx + seq_len_leftovers_start + sglid, sgid * SUBGROUP_SIZE)]; const uint value_offset = FUNC_CALL(get_input2_index)(OPTIONAL_SHAPE_INFO_TENSOR b_idx, b1_idx, 0, 0, start_partition_idx + seq_len_leftovers_start + sglid, sgid * SUBGROUP_SIZE); + #endif #else const uint b_idx = b0_idx; - #ifdef INPUT2_DIMS_ORDER + #if IS_INT4_COMPRESSED + uint value_offset = value_base_s1 + (start_partition_idx + seq_len_leftovers_start) * value_pitch; + #elif defined(INPUT2_DIMS_ORDER) uint value_offset = FUNC_CALL(get_input2_index)(OPTIONAL_SHAPE_INFO_TENSOR b0_idx, b1_idx, 0, 0, start_partition_idx + seq_len_leftovers_start, head_size_idx); #else uint value_offset = INPUT2_GET_INDEX(b0_idx, b1_idx, start_partition_idx + seq_len_leftovers_start, head_size_idx); @@ -1986,7 +2449,6 @@ KERNEL(sdpa_opt)( #if IS_KV_COMPRESSED const uint comp_offset = GET_COMPRESSION_INDEX(VALUE_COMPRESSION_SCALE, b_idx, b1_idx / BROADCAST_GROUP_SIZE, start_partition_idx + min(seq_len_leftovers_start + sglid, seq_len_end - 1), 0); - // const uint comp_offset = GET_COMPRESSION_INDEX(VALUE_COMPRESSION_SCALE, b_idx, b1_idx / BROADCAST_GROUP_SIZE, start_partition_idx + seq_len_leftovers_start + sglid, 0); VALUE_COMPRESSION_SCALE_TYPE comp_scale = GET_SCALE(val_zp, val_scale, comp_offset); #if USE_ASYMMETRIC_QUANTIZATION VALUE_COMPRESSION_SCALE_TYPE comp_zp = GET_ZP(val_zp, val_scale, comp_offset); @@ -2014,7 +2476,14 @@ KERNEL(sdpa_opt)( #endif #endif // V_HEAD_SIZE_LEFTOVER -#if IS_KV_COMPRESSED && USE_ASYMMETRIC_QUANTIZATION +#if IS_KV_COMPRESSED && IS_INT4_COMPRESSED + INPUT2_TYPE needed_byte = intel_sub_group_shuffle(value_packed, shuffle_src_s1); + char2 v_left_unpacked = unpack_to_char(*(uint4x2_t*)&needed_byte); + VALUE_COMPRESSION_SCALE_TYPE value_val = (nibble_sel_s1 == 0 ? + CAT(convert_, VALUE_COMPRESSION_SCALE_TYPE)(v_left_unpacked.s0) : + CAT(convert_, VALUE_COMPRESSION_SCALE_TYPE)(v_left_unpacked.s1)); + value_val = (value_val - sub_group_broadcast(comp_zp, seq_len_idx)) * sub_group_broadcast(comp_scale, seq_len_idx); +#elif IS_KV_COMPRESSED && USE_ASYMMETRIC_QUANTIZATION VALUE_COMPRESSION_SCALE_TYPE value_val = (value_packed - sub_group_broadcast(comp_zp, seq_len_idx)) * sub_group_broadcast(comp_scale, seq_len_idx); #elif IS_KV_COMPRESSED VALUE_COMPRESSION_SCALE_TYPE value_val = (value_packed * sub_group_broadcast(comp_scale, seq_len_idx)); @@ -2080,6 +2549,25 @@ KERNEL(sdpa_opt)( } /* end of QK*V calculation */ } /* end of iter over source sequence length */ +#if HAS_SINK_INPUT + // Apply attention sink after all KV partitions are processed. + // Sink adds a virtual logit to the softmax denominator with zero V contribution. + { + SOFTMAX_ACCUMULATOR_TYPE sink_val = TO_SOFTMAX_ACCUMULATOR_TYPE(sink_ptr[b1_idx]); + for (uint seq_idx = 0; seq_idx < seq_idx_end; seq_idx++) { + SOFTMAX_ACCUMULATOR_TYPE max_prev = slm_max_val_prev[seq_idx]; + SOFTMAX_ACCUMULATOR_TYPE max_new = SOFTMAX_ACCUMULATOR_MAX_FUNC(max_prev, sink_val); + SOFTMAX_ACCUMULATOR_TYPE correction = native_exp(max_prev - max_new); + // Rescale output_acc (all work items do this for their own register) + output_acc[seq_idx] = TO_OUTPUT_TYPE(TO_SOFTMAX_ACCUMULATOR_TYPE(output_acc[seq_idx]) * correction); + // Update exp_sum (only one thread per seq_idx) + if (sgid == 0 && sglid == 0) { + slm_exp_sum_prev[seq_idx] = slm_exp_sum_prev[seq_idx] * correction + native_exp(sink_val - max_new); + } + } + } +#endif + // Combine results from multiple SGs and store to output buffer if (sgid >= (SUBGROUPS_PER_WG / SG_SCALE_FACTOR)) { @@ -2219,7 +2707,7 @@ KERNEL(sdpa_opt_finalization_stage)( target_seq_idx * (num_of_partitions); __global SOFTMAX_ACCUMULATOR_TYPE* cur_exp_sums = exp_sums + offset; __global SOFTMAX_ACCUMULATOR_TYPE* cur_max_logits = max_logits + offset; - __local SOFTMAX_ACCUMULATOR_TYPE tmp_slm[SUBGROUP_SIZE]; + __local SOFTMAX_ACCUMULATOR_TYPE tmp_slm[SUBGROUPS_PER_WG]; __local SOFTMAX_ACCUMULATOR_TYPE max_logits_u_exp_sum[MAX_PARTITIONS_NUM]; SOFTMAX_ACCUMULATOR_TYPE local_max_logit = SOFTMAX_ACCUMULATOR_VAL_MIN; @@ -2234,8 +2722,15 @@ KERNEL(sdpa_opt_finalization_stage)( } barrier(CLK_LOCAL_MEM_FENCE); - if (sglid < V_HEAD_SIZE / SUBGROUP_SIZE) { - local_max_logit = tmp_slm[sglid]; + { + SOFTMAX_ACCUMULATOR_TYPE folded_max = SOFTMAX_ACCUMULATOR_VAL_MIN; + unroll_for (uint i = 0; i < CEIL_DIV(SUBGROUPS_PER_WG, SUBGROUP_SIZE); i++) { + const uint sg_idx = sglid + i * SUBGROUP_SIZE; + if (sg_idx < SUBGROUPS_PER_WG) { + folded_max = SOFTMAX_ACCUMULATOR_MAX_FUNC(folded_max, tmp_slm[sg_idx]); + } + } + local_max_logit = folded_max; } SOFTMAX_ACCUMULATOR_TYPE global_max = sub_group_reduce_max(local_max_logit); @@ -2251,9 +2746,15 @@ KERNEL(sdpa_opt_finalization_stage)( tmp_slm[sgid] = local_exp_sum; } barrier(CLK_LOCAL_MEM_FENCE); - local_exp_sum = 0; - if (sglid < V_HEAD_SIZE / SUBGROUP_SIZE) { - local_exp_sum = tmp_slm[sglid]; + { + SOFTMAX_ACCUMULATOR_TYPE folded_sum = SOFTMAX_ACCUMULATOR_VAL_ZERO; + unroll_for (uint i = 0; i < CEIL_DIV(SUBGROUPS_PER_WG, SUBGROUP_SIZE); i++) { + const uint sg_idx = sglid + i * SUBGROUP_SIZE; + if (sg_idx < SUBGROUPS_PER_WG) { + folded_sum += tmp_slm[sg_idx]; + } + } + local_exp_sum = folded_sum; } SOFTMAX_ACCUMULATOR_TYPE global_exp_sum = sub_group_reduce_add(local_exp_sum); diff --git a/src/plugins/intel_gpu/src/graph/impls/ocl_v2/utils/fused_ops_jitter.cpp b/src/plugins/intel_gpu/src/graph/impls/ocl_v2/utils/fused_ops_jitter.cpp index 892f8d87d018..48b0b5ded98c 100644 --- a/src/plugins/intel_gpu/src/graph/impls/ocl_v2/utils/fused_ops_jitter.cpp +++ b/src/plugins/intel_gpu/src/graph/impls/ocl_v2/utils/fused_ops_jitter.cpp @@ -497,7 +497,7 @@ JitConstants FusedOpsCodeGenerator::make_op_jit_constants(const FusedOpsConfigur // Input shift if (p->_need_pre_shift) { - op_decls += make_statement(tmp_var.assign(tmp_var + pre_scale)).str(); + op_decls += make_statement(tmp_var.assign(tmp_var + pre_shift)).str(); } // Round operation isn't needed if output type is int8/uint8 and scale coefficient in all output channels is equal to 1.0 @@ -1017,6 +1017,58 @@ JitConstants make_activation_jit_constants(const std::string& suffix, case activation_func::round_half_away_from_zero: jit.add(make_jit_constant(macro_def, round(input))); break; + case activation_func::erfinv: { + // NOTE: exactly the same implementation can be found + // in jitter.cpp - ideally both should be defined + // in common place, but that would require deeper refactoring + // (e.g. class JitTerm is also defined in multiple places and + // it is a different implementation in different jitters) + // which is out of scope for the current change.... + const bool is_f32 = (out_dt == ov::element::f32); + const JitTerm elem_inf{is_f32 ? "INFINITY" : "((half)INFINITY)"}; + auto cf = [&](const char* lit) { + return concat(lit, type_suffix); + }; + auto horner = [&](const JitTerm& s, std::initializer_list coefs) { + auto it = coefs.begin(); + JitTerm r = *it++; + for (; it != coefs.end(); ++it) { + r = r * s + *it; + } + return r; + }; + const JitTerm& x = input; + const JitTerm w = neg(log((cf("1.0") - x) * (cf("1.0") + x))); + const JitTerm s_lo = w - cf("2.5"); + const JitTerm s_hi = sqrt(w) - cf("3.0"); + const JitTerm p_lo = horner(s_lo, + {cf("2.81022636e-08"), + cf("3.43273939e-07"), + cf("-3.5233877e-06"), + cf("-4.39150654e-06"), + cf("0.00021858087"), + cf("-0.00125372503"), + cf("-0.00417768164"), + cf("0.246640727"), + cf("1.50140941")}); + const JitTerm p_hi = horner(s_hi, + {cf("-0.000200214257"), + cf("0.000100950558"), + cf("0.00134934322"), + cf("-0.00367342844"), + cf("0.00573950773"), + cf("-0.0076224613"), + cf("-0.00943887047"), + cf("1.00167406"), + cf("2.83297682")}); + const JitTerm poly = x * ternary(w.lt(cf("5.0")), p_lo, p_hi); + // x = 0 -> poly yields 0 naturally. + // |x| > 1 -> log of a negative produces NaN, propagated by poly. + // x = +/-1 -> log(0) blows up the polynomial; force +/-inf via x * + // INFINITY. + jit.add(make_jit_constant(macro_def, ternary(fabs(x).eq(cf("1.0")), x * elem_inf, poly))); + break; + } case activation_func::none: default: jit.add(make_jit_constant(macro_def, input)); diff --git a/src/plugins/intel_gpu/src/graph/impls/ocl_v2/utils/jitter.cpp b/src/plugins/intel_gpu/src/graph/impls/ocl_v2/utils/jitter.cpp index 0cb4dc777db6..56ed0f607de4 100644 --- a/src/plugins/intel_gpu/src/graph/impls/ocl_v2/utils/jitter.cpp +++ b/src/plugins/intel_gpu/src/graph/impls/ocl_v2/utils/jitter.cpp @@ -320,6 +320,21 @@ JitConstants make_type_jit_constants(const std::string& name, const ov::element: type_size = "4"; is_fp = true; break; + case ov::element::dynamic: + type = "uchar"; + max_val = "UCHAR_MAX"; + min_val = "0"; + val_one = "(uchar) 1"; + val_zero = "(uchar) 0"; + to_type = "convert_uchar(v)"; + to_type_sat = "convert_uchar_sat(v)"; + as_type = "as_uchar(v)"; + max_func = "max"; + min_func = "min"; + abs_func = "abs"; + type_size = "1"; + is_fp = false; + break; default: OPENVINO_THROW("[GPU] Jitter: unsupported data type: ", value); } diff --git a/src/plugins/intel_gpu/src/graph/impls/onednn/concatenation_onednn.cpp b/src/plugins/intel_gpu/src/graph/impls/onednn/concatenation_onednn.cpp index ba5c8774e901..5bd5071247ee 100644 --- a/src/plugins/intel_gpu/src/graph/impls/onednn/concatenation_onednn.cpp +++ b/src/plugins/intel_gpu/src/graph/impls/onednn/concatenation_onednn.cpp @@ -70,7 +70,7 @@ struct concatenation_onednn : typed_primitive_onednn_impl( engine.get_onednn_engine(), output_md, - axis, + static_cast(axis), input_mds, attr); } diff --git a/src/plugins/intel_gpu/src/graph/impls/onednn/concatenation_onednn.hpp b/src/plugins/intel_gpu/src/graph/impls/onednn/concatenation_onednn.hpp index 724b0f0fd0fc..155ee8b19e33 100644 --- a/src/plugins/intel_gpu/src/graph/impls/onednn/concatenation_onednn.hpp +++ b/src/plugins/intel_gpu/src/graph/impls/onednn/concatenation_onednn.hpp @@ -79,7 +79,8 @@ struct ConcatenationImplementationManager : public ImplementationManager { return feature_dim.get_length() % feature_block_size == 0; }; - // onednn concatenation doesn't support non-zero padding which can occur for unaligned feature. + // oneDNN blocked format contract requires zero-filled fsv padding lanes for unaligned feature blocks. + // Apply the same feature alignment requirement to both input and output layouts. if (!is_feature_aligned(out_layout)) { return false; } @@ -96,7 +97,7 @@ struct ConcatenationImplementationManager : public ImplementationManager { if (!one_of(in_layout.format.value, supported_in_fmts)) return false; - if (node.is_dynamic() && !is_feature_aligned(in_layout)) + if (!is_feature_aligned(in_layout)) return false; } diff --git a/src/plugins/intel_gpu/src/graph/impls/onednn/convolution_onednn.cpp b/src/plugins/intel_gpu/src/graph/impls/onednn/convolution_onednn.cpp index 6ed28c6ea5fa..d71f7a688868 100644 --- a/src/plugins/intel_gpu/src/graph/impls/onednn/convolution_onednn.cpp +++ b/src/plugins/intel_gpu/src/graph/impls/onednn/convolution_onednn.cpp @@ -222,6 +222,11 @@ struct convolution_onednn : typed_primitive_onednn_impl { dnnl::memory::data_type& wzp_data_type) { auto attrs = impl_params.attrs_onednn; + // accumulation_mode::any allows oneDNN to use f16 as the accumulation type. + if (impl_params.get_input_layout(0).data_type == data_types::f16) { + attrs->set_accumulation_mode(dnnl::accumulation_mode::any); + } + if (arg.activations_zero_points_term()) { auto& a_zp = arg.activations_zero_points(); auto a_zp_dtype = a_zp.get_output_layout().data_type; diff --git a/src/plugins/intel_gpu/src/graph/impls/onednn/deconvolution_onednn.cpp b/src/plugins/intel_gpu/src/graph/impls/onednn/deconvolution_onednn.cpp index db9a07974692..506a5f71dc15 100644 --- a/src/plugins/intel_gpu/src/graph/impls/onednn/deconvolution_onednn.cpp +++ b/src/plugins/intel_gpu/src/graph/impls/onednn/deconvolution_onednn.cpp @@ -26,7 +26,9 @@ static std::shared_ptr get_deconvol auto output_layout = impl_params.get_output_layout(); dnnl::memory::dims stride(prim->stride.begin(), prim->stride.end()); - dnnl::memory::dims dilation(stride.size(), 1); + auto dilations_ov = prim->dilations; + dilations_ov.resize(stride.size(), 1); + dnnl::memory::dims dilation(dilations_ov.begin(), dilations_ov.end()); dnnl::memory::dims pad_l(prim->pad.begin(), prim->pad.end()); dnnl::memory::dims pad_r(prim->pad.begin(), prim->pad.end()); diff --git a/src/plugins/intel_gpu/src/graph/impls/onednn/fully_connected_onednn.cpp b/src/plugins/intel_gpu/src/graph/impls/onednn/fully_connected_onednn.cpp index 19601d38a865..244f18aeda9e 100644 --- a/src/plugins/intel_gpu/src/graph/impls/onednn/fully_connected_onednn.cpp +++ b/src/plugins/intel_gpu/src/graph/impls/onednn/fully_connected_onednn.cpp @@ -310,8 +310,8 @@ struct fully_connected_onednn : typed_primitive_onednn_impl { auto partial_shape = impl_params->get_input_layout(0).get_partial_shape(); auto innermost_len = partial_shape[partial_shape.size() - 1].get_length(); auto& src_scale_shape = impl_params->input_layouts[src_scale_idx].get_partial_shape(); - int src_scale_ngroups = src_scale_shape[src_scale_shape.size() - 1].get_length(); - int src_group_size = innermost_len / src_scale_ngroups; + int64_t src_scale_ngroups = src_scale_shape[src_scale_shape.size() - 1].get_length(); + int64_t src_group_size = innermost_len / src_scale_ngroups; auto act_scale_data_type = convert_data_type(impl_params->get_input_layout(src_scale_idx).data_type); _attrs->set_scales(DNNL_ARG_SRC, grouped, dnnl::memory::dims{1, src_group_size}, act_scale_data_type); @@ -373,7 +373,7 @@ struct fully_connected_onednn : typed_primitive_onednn_impl { ds_data_type = convert_data_type(scale_layout.data_type); auto ifm = arg.get_dependency(1).get_output_layout().get_dim(weight_rank - 1); auto ngroups = scale_layout.get_dim(weight_rank - 1); - group_size = ifm / ngroups; + group_size = static_cast(ifm / ngroups); OPENVINO_ASSERT((group_size == 1 || ngroups == 1 || group_size % 16 == 0), "[GPU] group_size should be aligned to 16 if it is not a single scale group or the group_size is not one."); if (scale_layout.count() == 1) { @@ -416,8 +416,8 @@ struct fully_connected_onednn : typed_primitive_onednn_impl { auto& partial_shape = impl_params.input_layouts[0].get_partial_shape(); auto innermost_len = partial_shape[partial_shape.size() - 1].get_length(); auto& src_scale_shape = impl_params.input_layouts[src_scale_idx].get_partial_shape(); - int src_scale_ngroups = src_scale_shape[src_scale_shape.size() - 1].get_length(); - int src_group_size = innermost_len / src_scale_ngroups; + int64_t src_scale_ngroups = src_scale_shape[src_scale_shape.size() - 1].get_length(); + int64_t src_group_size = innermost_len / src_scale_ngroups; auto act_scale_data_type = convert_data_type(impl_params.input_layouts[src_scale_idx].data_type); attr->set_scales(DNNL_ARG_SRC, grouped, dnnl::memory::dims{1, src_group_size}, act_scale_data_type); diff --git a/src/plugins/intel_gpu/src/graph/impls/onednn/grouped_matmul_helper.hpp b/src/plugins/intel_gpu/src/graph/impls/onednn/grouped_matmul_helper.hpp new file mode 100644 index 000000000000..9e4b6de881d7 --- /dev/null +++ b/src/plugins/intel_gpu/src/graph/impls/onednn/grouped_matmul_helper.hpp @@ -0,0 +1,316 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#pragma once + +// Inline dnnl::matmul wrappers + a construction-cache, shared by primitives that +// orchestrate their own sort/gather/gemm/scatter pipelines. + +#ifdef ENABLE_ONEDNN_FOR_GPU + +# include +# include +# include +# include +# include + +# include "intel_gpu/runtime/itt.hpp" +# include "openvino/core/except.hpp" + +namespace cldnn { +namespace onednn { + +struct onednn_matmul { + dnnl::matmul m_prim; + dnnl::memory::desc m_wei_md; + dnnl::memory::data_type m_w_type; + dnnl::memory::data_type m_a_type; // activation dtype + dnnl::memory::dim m_K; + dnnl::memory::dim m_N; + dnnl::memory::dim m_M; + dnnl::memory::dim m_K_groups; + + dnnl::primitive_attr attr; + dnnl::post_ops postops; + + onednn_matmul(dnnl::memory::data_type act_dtype, + dnnl::memory::data_type weight_dtype, + int batch_size, + int ic, + int oc, + int ic_group_size = -1, + bool has_zp = true) { + m_a_type = act_dtype; + m_w_type = weight_dtype; + m_K_groups = 0; + m_K = ic; + m_N = oc; + m_M = DNNL_RUNTIME_DIM_VAL; + if (batch_size > 0) { + // jit-gemm kernel only support static batch size + m_M = batch_size; + } + if (ic_group_size >= 0) { + w_scale(ic_group_size); + if (has_zp) + w_zp(ic_group_size); + fpmath_f16(); + } + } + + onednn_matmul& w_scale(int k_group_size) { + if (k_group_size <= 0) { + m_K_groups = 1; + // per-OC, no grouping in K dimension + attr.set_scales(DNNL_ARG_WEIGHTS, (0 << 0) + (1 << 1), {1}, dnnl::memory::data_type::f16); + } else { + OPENVINO_ASSERT((k_group_size % 32) == 0); + OPENVINO_ASSERT((m_K % k_group_size) == 0); + m_K_groups = m_K / k_group_size; + attr.set_scales(DNNL_ARG_WEIGHTS, (1 << 0) + (1 << 1), {k_group_size, 1}, dnnl::memory::data_type::f16); + } + return *this; + } + + onednn_matmul& w_zp(int k_group_size) { + if (k_group_size <= 0) { + OPENVINO_ASSERT(m_K_groups == 1); + attr.set_zero_points(DNNL_ARG_WEIGHTS, (0 << 0) + (1 << 1), {1}, m_w_type); + } else { + OPENVINO_ASSERT((m_K % k_group_size) == 0); + m_K_groups = (m_K / k_group_size); + attr.set_zero_points(DNNL_ARG_WEIGHTS, (1 << 0) + (1 << 1), {k_group_size, 1}, m_w_type); + } + return *this; + } + + onednn_matmul& fpmath_f16() { + attr.set_fpmath_mode(dnnl::fpmath_mode::f16, true); + return *this; + } + onednn_matmul& post_op_gate_activation(dnnl::algorithm algo) { + float alpha = 1.0f; + float beta = 0.0f; + postops.append_eltwise(algo, alpha, beta); + return *this; + } + onednn_matmul& post_op_bin_mul(bool per_oc = true) { + dnnl::memory::dim batch_size = m_M; + if (batch_size == DNNL_RUNTIME_DIM_VAL) + batch_size = 1024 * 1024; // big enough fake static batch + + dnnl::memory::desc bin_mul_md = dnnl::memory::desc(dnnl::memory::dims({batch_size, per_oc ? m_N : 1}), m_a_type, dnnl::memory::format_tag::ab); + postops.append_binary(dnnl::algorithm::binary_mul, bin_mul_md); + return *this; + } + + onednn_matmul& post_op_sum(float scale = 1.f, int32_t zero_point = 0) { + postops.append_sum(scale, zero_point, dnnl::memory::data_type::undef); + return *this; + } + + void create(dnnl::engine eng) { + if (postops.len() > 0) { + attr.set_post_ops(postops); + } + + dnnl::memory::desc src_md = dnnl::memory::desc(dnnl::memory::dims({m_M, m_K}), m_a_type, dnnl::memory::format_tag::ab); + dnnl::memory::desc dst_md = dnnl::memory::desc(dnnl::memory::dims({m_M, m_N}), m_a_type, dnnl::memory::format_tag::ab); + + // use fixed weight-layout to prevent shape-dependent weight-layout changes + dnnl::memory::desc wei_md = dnnl::memory::desc(dnnl::memory::dims({m_K, m_N}), m_w_type, dnnl::memory::format_tag::ba); + + // Create primitive descriptor. + auto matmul_pd = dnnl::matmul::primitive_desc(eng, src_md, wei_md, dst_md, attr); + + // Pre-packed weights stored as int8_t + m_wei_md = matmul_pd.weights_desc(); + + // Create the primitive. + m_prim = dnnl::matmul(matmul_pd); + } + + // this creator is for predefined matmul primitive types + enum class type { + none, + with_bin_mul, + with_bin_mul_per_row, + with_bin_mul_per_row_sum, + with_gate_act, + with_gate_act_bin_mul, + with_sigmoid, + with_bin_mul_sum, + }; + int bin_post_id = -1; + bool bin_per_row = false; + onednn_matmul(dnnl::engine eng, + dnnl::memory::data_type act_dtype, + dnnl::memory::data_type weight_dtype, + int batch, + int ic, + int oc, + int ic_group_size, + type t, + bool has_zp = true, + dnnl::algorithm activation_algo = dnnl::algorithm::eltwise_swish) + : onednn_matmul(act_dtype, weight_dtype, batch, ic, oc, ic_group_size, has_zp) { + if (t == type::with_bin_mul) { + bin_post_id = 0; + post_op_bin_mul(true); + } + if (t == type::with_bin_mul_sum) { + bin_post_id = 0; + post_op_bin_mul(false); + post_op_sum(); + } + if (t == type::with_sigmoid) { + postops.append_eltwise(dnnl::algorithm::eltwise_logistic, 1.0f, 0.0f); + } + if (t == type::with_bin_mul_per_row) { + bin_post_id = 0; + bin_per_row = true; + post_op_bin_mul(false); + } + if (t == type::with_bin_mul_per_row_sum) { + bin_post_id = 0; + bin_per_row = true; + post_op_bin_mul(false); + post_op_sum(); + } + if (t == type::with_gate_act) + post_op_gate_activation(activation_algo); + if (t == type::with_gate_act_bin_mul) { + bin_post_id = 1; + post_op_gate_activation(activation_algo); + post_op_bin_mul(true); + } + + create(eng); + } +}; + +// Stateless functor wrapping a built dnnl primitive; cache returns shared_ptr for thread safety. +template +class tuple_hasher { +private: + typedef std::tuple Tuple; + template + size_t hash(Tuple& value) const { + return 0; + } + template + size_t hash(Tuple& value) const { + constexpr int Index = N - sizeof...(TTail) - 1; + return std::hash()(std::get(value)) ^ hash(value); + } + +public: + size_t operator()(Tuple value) const { + auto hv = hash(value); + return hv; + } +}; + +// Process-wide cache keyed by constructor args; same-shape callers across TUs reuse one instance. +template +std::shared_ptr make_cacheable(dnnl::engine eng, CArgs... cargs) { + std::shared_ptr sptr; + auto key = std::make_tuple(cargs...); + static std::unordered_map, tuple_hasher> cache; + static std::mutex mutex; + std::lock_guard guard(mutex); + auto it = cache.find(key); + if (it != cache.end()) { + auto& wptr = it->second; + sptr = wptr.lock(); + if (!sptr) { + sptr = std::make_shared(eng, cargs...); + wptr = sptr; + } + } else { + sptr = std::make_shared(eng, cargs...); + cache.emplace(std::make_pair(key, std::weak_ptr(sptr))); + } + return sptr; +} + +struct onednn_linear { + std::shared_ptr mm; + dnnl::memory weight; + dnnl::memory scale; + dnnl::memory zp; + dnnl::matmul m_prim; + dnnl::memory::dim m_K; + dnnl::memory::dim m_N; + dnnl::memory::dim m_batch; + dnnl::memory::data_type m_a_type; + int bin_post_id; + + static onednn_linear create(dnnl::engine eng, + dnnl::memory::data_type act_dtype, + dnnl::memory::data_type weight_dtype, + int batch, + int ic, + int oc, + int ic_group_size, + onednn_matmul::type t, + dnnl::memory weight, // external weight + dnnl::memory scale, + dnnl::memory zp, + dnnl::algorithm activation_algo = dnnl::algorithm::eltwise_swish) { + OV_ITT_SCOPED_TASK(ov::intel_gpu::itt::domains::intel_gpu_plugin, openvino::itt::handle("onednn_linear::create()")); + bool has_zp = static_cast(zp); + auto mm = make_cacheable(eng, act_dtype, weight_dtype, batch, ic, oc, ic_group_size, t, has_zp, activation_algo); + onednn_linear linear; + linear.mm = mm; + linear.bin_post_id = mm->bin_post_id; + linear.m_prim = mm->m_prim; + linear.m_K = mm->m_K; + linear.m_N = mm->m_N; + linear.m_batch = batch; + linear.m_a_type = mm->m_a_type; + linear.weight = weight; + + if (scale) { + // https://uxlfoundation.github.io/oneDNN/page_weights_decompression_matmul_cpp.html + // Quantization Group size for scales. Must be divisible by 32. + linear.scale = scale; + if (zp) { + linear.zp = zp; + } + } + return linear; + } + + void forward(dnnl::stream& stream, int m, dnnl::memory src_mem, dnnl::memory dst_mem, dnnl::memory bin_mem) { + OV_ITT_SCOPED_TASK(ov::intel_gpu::itt::domains::intel_gpu_plugin, openvino::itt::handle("onednn_linear::forward()")); + dnnl::memory::dim M = m; + + if (!(m_batch == 0 || m_batch == M)) { + OPENVINO_THROW("onednn_linear::forward(): invalid batch size m_batch=", m_batch, " M=", M); + } + + std::unordered_map args; + args.insert({DNNL_ARG_SRC, src_mem}); + args.insert({DNNL_ARG_WEIGHTS, weight}); + // args.insert({DNNL_ARG_BIAS, bias_mem}); + args.insert({DNNL_ARG_DST, dst_mem}); + + if (scale) { + args.insert({DNNL_ARG_ATTR_SCALES | DNNL_ARG_WEIGHTS, scale}); + } + if (zp) { + args.insert({DNNL_ARG_ATTR_ZERO_POINTS | DNNL_ARG_WEIGHTS, zp}); + } + if (bin_mem) { + args.insert({DNNL_ARG_ATTR_MULTIPLE_POST_OP(bin_post_id) | DNNL_ARG_SRC_1, bin_mem}); + } + m_prim.execute(stream, args); + } +}; + +} // namespace onednn +} // namespace cldnn + +#endif // ENABLE_ONEDNN_FOR_GPU diff --git a/src/plugins/intel_gpu/src/graph/impls/onednn/moe_gemm_onednn.cpp b/src/plugins/intel_gpu/src/graph/impls/onednn/moe_gemm_onednn.cpp index cbce4a21ede6..1d7de588f384 100644 --- a/src/plugins/intel_gpu/src/graph/impls/onednn/moe_gemm_onednn.cpp +++ b/src/plugins/intel_gpu/src/graph/impls/onednn/moe_gemm_onednn.cpp @@ -50,22 +50,42 @@ struct moe_gemm_onednn : typed_primitive_onednn_impl { } if (moe_cfg.is_weight_quantized) { + // cldnn [E,N,G] -> onednn [E,G,N] (matches byfx physical from prepare_quantization). auto& wei_scales = instance.input_memory(moe_cfg.weight_scale_idx); auto wei_scales_shape = wei_scales.get_layout().get_shape(); dnnl::memory::dim d0 = wei_scales_shape[0]; dnnl::memory::dim d1 = wei_scales_shape[1]; dnnl::memory::dim d2 = wei_scales_shape[2]; - dnnl::memory::dims wei_scales_dims = (moe_cfg.weight_group_size == -1) ? dnnl::memory::dims{d0, d2} : dnnl::memory::dims{d0, d1, d2}; - dnnl::memory::format_tag wei_scales_fmt = (moe_cfg.weight_group_size == -1) ? dnnl::memory::format_tag::ab : dnnl::memory::format_tag::abc; + // Cross-check moe_cfg.weight_group_size (from compile-time scale_shape[2]) vs runtime memory. + const auto& weight_layout = instance.get_input_layout(moe_gemm::MoEGemmInputIdx::WEIGHT); + const auto& w_shape = weight_layout.get_shape(); + const dnnl::memory::dim K = (w_shape.size() == 4) ? w_shape[2] * w_shape[3] : w_shape[2]; + const dnnl::memory::dim runtime_num_groups = (moe_cfg.weight_group_size == -1) + ? 1 + : (K / moe_cfg.weight_group_size); + const dnnl::memory::dim scale_num_groups = (wei_scales_shape.size() >= 3) ? d2 : 1; + OPENVINO_ASSERT(scale_num_groups == runtime_num_groups, + "moe_gemm scale shape ", wei_scales_shape, " implies num_groups=", + scale_num_groups, " but moe_cfg.weight_group_size=", moe_cfg.weight_group_size, + " (K=", K, ") implies ", runtime_num_groups); + dnnl::memory::dims wei_scales_dims = (moe_cfg.weight_group_size == -1) + ? dnnl::memory::dims{d0, d1} + : dnnl::memory::dims{d0, d2, d1}; + dnnl::memory::format_tag wei_scales_fmt = (moe_cfg.weight_group_size == -1) + ? dnnl::memory::format_tag::ab + : dnnl::memory::format_tag::abc; dnnl::memory::desc wei_scales_md( - wei_scales_dims, convert_data_type(wei_scales.get_layout().data_type), wei_scales_fmt); + wei_scales_dims, convert_data_type(wei_scales.get_layout().data_type), wei_scales_fmt); dnnl::memory wei_scales_mem = wei_scales.get_onednn_memory(wei_scales_md, 0); args.insert({DNNL_ARG_ATTR_SCALES | DNNL_ARG_WEIGHTS, wei_scales_mem}); if (!moe_cfg.is_weight_symmetric_quantized) { auto& wei_zp = instance.input_memory(moe_cfg.weight_zp_idx); + const auto& zp_shape = wei_zp.get_layout().get_shape(); + OPENVINO_ASSERT(zp_shape == wei_scales_shape, + "moe_gemm scale shape ", wei_scales_shape, " does not match zp shape ", zp_shape); dnnl::memory::desc wei_zp_md( - wei_scales_dims, convert_data_type(wei_zp.get_layout().data_type), wei_scales_fmt); + wei_scales_dims, convert_data_type(wei_zp.get_layout().data_type), wei_scales_fmt); dnnl::memory wei_zp_mem = wei_zp.get_onednn_memory(wei_zp_md, 0); args.insert({DNNL_ARG_ATTR_ZERO_POINTS | DNNL_ARG_WEIGHTS, wei_zp_mem}); } diff --git a/src/plugins/intel_gpu/src/graph/impls/onednn/moe_gemm_onednn.hpp b/src/plugins/intel_gpu/src/graph/impls/onednn/moe_gemm_onednn.hpp index 2f5da9116e11..d437119fb058 100644 --- a/src/plugins/intel_gpu/src/graph/impls/onednn/moe_gemm_onednn.hpp +++ b/src/plugins/intel_gpu/src/graph/impls/onednn/moe_gemm_onednn.hpp @@ -99,10 +99,12 @@ struct MoEGemmImplementationManager : public ImplementationManager { } if (has_quant_weight) { + // prepare_quantization may emit byfx; onednn reads flat regardless. + static const std::vector supported_quant_fmts = {format::bfyx, format::byfx}; size_t quant_params_idx_start = desc.has_bias ? static_cast(moe_gemm::MoEGemmInputIdx::WEIGHT_SCALE) : static_cast(moe_gemm::MoEGemmInputIdx::WEIGHT_SCALE - 1); for (size_t i = quant_params_idx_start; i < node.get_input_layouts().size(); i++) { - if (!one_of(node.get_input_layout(i).format, supported_fmts) || !one_of(node.get_input_layout(i).data_type, supported_quant_param_types)) { + if (!one_of(node.get_input_layout(i).format, supported_quant_fmts) || !one_of(node.get_input_layout(i).data_type, supported_quant_param_types)) { DO_NOT_USE_THIS_KERNEL(layer_id); } } @@ -155,11 +157,11 @@ struct MoEGemmImplementationManager : public ImplementationManager { moe_cfg.weight_zp_idx = moe_gemm::MoEGemmInputIdx::WEIGHT_ZP - 1; } const auto& weight_shape = params.input_layouts[moe_gemm::MoEGemmInputIdx::WEIGHT].get_shape(); - // experts weight : [#experts, ofm, num_groups, group_size] + // Rank-4 [E, ofm, G, GS] vs rank-3 [E, ofm, K] (collapsed by ConvertGatherMatmulToGatherMatmulCompressed). auto k = (weight_shape.size() == 4) ? weight_shape[2] * weight_shape[3] : weight_shape[2]; - // weight scales : [#experts, num_groups, ofm, 1] - auto scale_group_dim = 1; - auto num_scale_groups = (weight_shape.size() == 4) ? params.input_layouts[moe_cfg.weight_scale_idx].get_shape()[scale_group_dim] : 1; + // Scales: [E, ofm, G] or [E, ofm, G, 1]; groups at index 2. + const auto& scale_shape = params.input_layouts[moe_cfg.weight_scale_idx].get_shape(); + auto num_scale_groups = (scale_shape.size() >= 3) ? scale_shape[2] : 1; moe_cfg.weight_group_size = (num_scale_groups == 1) ? -1 : static_cast(k / num_scale_groups); if (static_cast(params.input_layouts.size()) > moe_cfg.weight_zp_idx) { moe_cfg.is_weight_symmetric_quantized = false; diff --git a/src/plugins/intel_gpu/src/graph/impls/onednn/primitive_onednn_base.h b/src/plugins/intel_gpu/src/graph/impls/onednn/primitive_onednn_base.h index 4970986e7f02..394855a1de98 100644 --- a/src/plugins/intel_gpu/src/graph/impls/onednn/primitive_onednn_base.h +++ b/src/plugins/intel_gpu/src/graph/impls/onednn/primitive_onednn_base.h @@ -69,7 +69,8 @@ struct typed_primitive_onednn_impl : public typed_primitive_impl { _pd(), _prim() { _enable_profiling = config.get_enable_profiling(); - GPU_DEBUG_IF(!config.get_dump_profiling_data_path().empty()) { + GPU_DEBUG_IF(!config.get_dump_profiling_data_path().empty() || + !config.get_average_counters().empty()) { _enable_profiling = true; } } diff --git a/src/plugins/intel_gpu/src/graph/impls/onednn/utils.cpp b/src/plugins/intel_gpu/src/graph/impls/onednn/utils.cpp index ce1104785ae7..f5ab9d2b52e1 100644 --- a/src/plugins/intel_gpu/src/graph/impls/onednn/utils.cpp +++ b/src/plugins/intel_gpu/src/graph/impls/onednn/utils.cpp @@ -806,7 +806,8 @@ cldnn::format_traits convert_memory_desc_to_traits(const dnnl::memory::desc& des std::vector> block_sizes(inner_nblks); for (int i = 0; i < inner_nblks; i++) { - block_sizes[i] = std::make_pair(inner_idxs[i] + (is_grouped && inner_idxs[i] == 0 ? 9 : 0) + (is_grouped ? -1 : 0), inner_blks[i]); + const auto idx = inner_idxs[i] + (is_grouped && inner_idxs[i] == 0 ? 9 : 0) + (is_grouped ? -1 : 0); + block_sizes[i] = {static_cast(idx), static_cast(inner_blks[i])}; } // all fmts has at least batch and feature dim for now @@ -832,7 +833,7 @@ cldnn::format_traits convert_memory_desc_to_traits(const dnnl::memory::desc& des auto pos = outer_order.find(c); OPENVINO_ASSERT(pos != std::string::npos, "[GPU] Unknown coord type: ", c); - logic_block_sizes[i] = std::make_pair(order[pos], inner_blks[i]); + logic_block_sizes[i] = std::make_pair(order[pos], static_cast(inner_blks[i])); } format_traits traits; diff --git a/src/plugins/intel_gpu/src/graph/impls/sycl/CMakeLists.txt b/src/plugins/intel_gpu/src/graph/impls/sycl/CMakeLists.txt index bfeba3df2002..4ebb6f169c5c 100644 --- a/src/plugins/intel_gpu/src/graph/impls/sycl/CMakeLists.txt +++ b/src/plugins/intel_gpu/src/graph/impls/sycl/CMakeLists.txt @@ -2,14 +2,25 @@ # SPDX-License-Identifier: Apache-2.0 # -if(NOT OV_COMPILER_IS_INTEL_LLVM) +if(NOT OV_GPU_WITH_SYCL) return() endif() set(TARGET_NAME "openvino_intel_gpu_sycl_obj") +set(SOURCES_OCL_RT "${CMAKE_CURRENT_SOURCE_DIR}/impl_example.cpp") + +if(GPU_RT_TYPE STREQUAL "SYCL") + set(EXCLUDED_SOURCE_PATHS ${SOURCES_OCL_RT}) +else() + file(GLOB EXCLUDED_SOURCE_PATHS "${CMAKE_CURRENT_SOURCE_DIR}/*.cpp") + list(REMOVE_ITEM EXCLUDED_SOURCE_PATHS ${SOURCES_OCL_RT}) +endif() + ov_gpu_add_backend_target( NAME ${TARGET_NAME} + BYPASS + EXCLUDED_SOURCE_PATHS ${EXCLUDED_SOURCE_PATHS} ) add_sycl_to_target(TARGET ${TARGET_NAME}) diff --git a/src/plugins/intel_gpu/src/graph/impls/sycl/eltwise.cpp b/src/plugins/intel_gpu/src/graph/impls/sycl/eltwise.cpp new file mode 100644 index 000000000000..a3c192d8cc2d --- /dev/null +++ b/src/plugins/intel_gpu/src/graph/impls/sycl/eltwise.cpp @@ -0,0 +1,403 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright (C) 2026 FUJITSU LIMITED +// + +#include "eltwise.hpp" +#include "eltwise_inst.h" +#include "intel_gpu/primitives/reorder.hpp" +#include "sycl/sycl_wrapper.hpp" +#include "sycl/sycl_stream.hpp" +#include "sycl/sycl_memory.hpp" +#include "openvino/core/type/element_type.hpp" +#include "primitive_sycl_base.h" +#include "mem_adapter.hpp" +#include "registry/implementation_map.hpp" + +#include +#include + +namespace { + +/** + * @brief Base class for eltwise operation function objects using CRTP (Curiously Recurring Template Pattern) + * + * This base class provides a common interface for all eltwise operations while allowing + * compile-time polymorphism. Each derived class must implement the 'apply' method. + * Supports heterogeneous input types and flexible output types. + * + * @tparam Derived The derived class implementing the specific operation + */ +template +struct EltwiseOpBase { + /** + * @brief Function call operator that delegates to the derived class's apply method + * @tparam T1 The data type for the first operand + * @tparam T2 The data type for the second operand + * @param a First operand + * @param b Second operand + * @return Result of the operation (type determined by derived class implementation) + */ + template + constexpr auto operator()(const T1& a, const T2& b) const noexcept { + return static_cast(this)->apply(a, b); + } +}; + +// ============================================================================ +// Arithmetic Operations +// ============================================================================ + +/** + * @brief Addition operation (a + b) + * Commutative operation that returns the sum of two operands + */ +struct AddOp : public EltwiseOpBase { + template + constexpr auto apply(const T1& a, const T2& b) const noexcept { + return a + b; + } +}; + +/** + * @brief Subtraction operation (a - b) + * Non-commutative operation that returns the difference of two operands + */ +struct SubOp : public EltwiseOpBase { + template + constexpr auto apply(const T1& a, const T2& b) const noexcept { + return a - b; + } +}; + +/** + * @brief Multiplication operation (a * b) + * Commutative operation that returns the product of two operands + */ +struct MulOp : public EltwiseOpBase { + template + constexpr auto apply(const T1& a, const T2& b) const noexcept { + return a * b; + } +}; + +/** + * @brief Division operation (a / b) + * Non-commutative operation that returns the quotient of two operands + */ +struct DivOp : public EltwiseOpBase { + template + constexpr auto apply(const T1& a, const T2& b) const noexcept { + return a / b; + } +}; + +/** + * @brief Power operation (pow(a, b)) + * Non-commutative operation that returns a raised to the power of b + */ +struct PowOp : public EltwiseOpBase { + template + inline auto apply(const T1& a, const T2& b) const noexcept { + return ::sycl::pow(a, b); + } +}; + +// ============================================================================ +// Min/Max Operations +// ============================================================================ + +/** + * @brief Maximum operation (max(a, b)) + * Commutative operation that returns the maximum of two operands + */ +struct MaxOp : public EltwiseOpBase { + template + inline auto apply(const T1& a, const T2& b) const { + return ::sycl::max(a, b); + } +}; + +/** + * @brief Minimum operation (min(a, b)) + * Commutative operation that returns the minimum of two operands + */ +struct MinOp : public EltwiseOpBase { + template + inline auto apply(const T1& a, const T2& b) const { + return ::sycl::min(a, b); + } +}; + +// ============================================================================ +// Special Operations +// ============================================================================ + +/** + * @brief Squared difference operation ((a - b)^2) + * Commutative operation that returns the square of the difference + */ +struct SquaredDiffOp : public EltwiseOpBase { + template + inline auto apply(const T1& a, const T2& b) const { + const auto diff = a - b; + return diff * diff; + } +}; + +// ============================================================================ +// Comparison Operations (return 0 or 1) +// ============================================================================ + +/** + * @brief Equality comparison (a == b) + * Commutative boolean operation that returns 1 if equal, 0 otherwise + */ +struct EqOp : public EltwiseOpBase { + template + inline auto apply(const T1& a, const T2& b) const { + return (a == b) ? 1 : 0; + } +}; + +/** + * @brief Not equal comparison (a != b) + * Commutative boolean operation that returns 1 if not equal, 0 otherwise + */ +struct NeOp : public EltwiseOpBase { + template + inline auto apply(const T1& a, const T2& b) const { + return (a != b) ? 1 : 0; + } +}; + +/** + * @brief Less than comparison (a < b) + * Non-commutative boolean operation that returns 1 if a < b, 0 otherwise + */ +struct LtOp : public EltwiseOpBase { + template + inline auto apply(const T1& a, const T2& b) const { + return (a < b) ? 1 : 0; + } +}; + +/** + * @brief Less than or equal comparison (a <= b) + * Non-commutative boolean operation that returns 1 if a <= b, 0 otherwise + */ +struct LeOp : public EltwiseOpBase { + template + inline auto apply(const T1& a, const T2& b) const { + return (a <= b) ? 1 : 0; + } +}; + +/** + * @brief Greater than comparison (a > b) + * Non-commutative boolean operation that returns 1 if a > b, 0 otherwise + */ +struct GtOp : public EltwiseOpBase { + template + inline auto apply(const T1& a, const T2& b) const { + return (a > b) ? 1 : 0; + } +}; + +/** + * @brief Greater than or equal comparison (a >= b) + * Non-commutative boolean operation that returns 1 if a >= b, 0 otherwise + */ +struct GeOp : public EltwiseOpBase { + template + inline auto apply(const T1& a, const T2& b) const { + return (a >= b) ? 1 : 0; + } +}; + +template +inline void eltwise_kernel(size_t index, InPtrType in0, InPtrType in1, OutPtrType out, size_t in1_stride, OpFunc op) { + auto val0 = in0[index]; + auto val1 = in1[index * in1_stride]; + out[index] = op(val0, val1); +} + +/** + * @brief Get the appropriate operation function object based on eltwise_mode + * + * This function provides a centralized way to map eltwise_mode enum values + * to their corresponding operation function objects. + * + * @param mode The eltwise operation mode + * @return The operation function object as a variant + */ +using EltwiseOperator = std::variant; + +EltwiseOperator get_eltwise_operator(cldnn::eltwise_mode mode) { + switch (mode) { + case cldnn::eltwise_mode::sum: + return AddOp{}; + case cldnn::eltwise_mode::sub: + return SubOp{}; + case cldnn::eltwise_mode::prod: + return MulOp{}; + case cldnn::eltwise_mode::div: + return DivOp{}; + case cldnn::eltwise_mode::max: + return MaxOp{}; + case cldnn::eltwise_mode::min: + return MinOp{}; + case cldnn::eltwise_mode::pow: + return PowOp{}; + case cldnn::eltwise_mode::squared_diff: + return SquaredDiffOp{}; + case cldnn::eltwise_mode::eq: + return EqOp{}; + case cldnn::eltwise_mode::ne: + return NeOp{}; + case cldnn::eltwise_mode::lt: + return LtOp{}; + case cldnn::eltwise_mode::le: + return LeOp{}; + case cldnn::eltwise_mode::gt: + return GtOp{}; + case cldnn::eltwise_mode::ge: + return GeOp{}; + default: + OPENVINO_THROW("Unsupported eltwise mode: ", static_cast(mode)); + } +} + +template +::sycl::event run_eltwise(::sycl::queue& queue, const std::vector<::sycl::event>& events, + const cldnn::memory::ptr& input0, const cldnn::memory::ptr& input1, + const cldnn::memory::ptr& output, + const ov::Shape& in0_shape, const ov::Shape& in1_shape, const ov::Shape& out_shape, + const EltwiseOperator& op) { + auto res_in0 = MemAdapter::from_memory(input0); + auto res_in1 = MemAdapter::from_memory(input1); + auto res_out = MemAdapter::from_memory(output); + size_t num_elements = ov::shape_size(out_shape); + size_t in1_stride = ov::shape_size(in1_shape) > 1 ? 1 : 0; + + return std::visit([&](auto&& operation) { + return queue.submit([&](::sycl::handler& cgh) { + cgh.depends_on(events); + + auto in0 = res_in0.bind_read(cgh); + auto in1 = res_in1.bind_read(cgh); + auto out = res_out.bind_write(cgh); + + cgh.parallel_for(::sycl::range<1>(num_elements), [=](::sycl::item<1> index) { + eltwise_kernel(index[0], in0, in1, out, in1_stride, operation); + }); + }); + }, op); +} + +template class MemAdapter> +::sycl::event run_eltwise(ov::element::Type_t dtype, + ::sycl::queue& queue, const std::vector<::sycl::event>& events, + const cldnn::memory::ptr& input0, const cldnn::memory::ptr& input1, + const cldnn::memory::ptr& output, + const ov::Shape& in0_shape, const ov::Shape& in1_shape, const ov::Shape& out_shape, + const EltwiseOperator& op) { + if (dtype == ov::element::f32) { + return run_eltwise>(queue, events, input0, input1, output, in0_shape, in1_shape, out_shape, op); + } else if (dtype == ov::element::f16) { + return run_eltwise>(queue, events, input0, input1, output, in0_shape, in1_shape, out_shape, op); + } else { + OPENVINO_THROW("No instance for given types found: ", dtype); + } +} + +} // namespace + +namespace cldnn { +namespace sycl { + +struct eltwise_sycl : typed_primitive_sycl_impl { + using parent = typed_primitive_sycl_impl; + using parent::parent; + + DECLARE_OBJECT_TYPE_SERIALIZATION(cldnn::sycl::eltwise_sycl) + + std::unique_ptr clone() const override { + return std::make_unique(*this); + } + + event::ptr execute_impl(const std::vector& events, typed_primitive_inst& instance) override { + auto& network = instance.get_network(); + const auto& desc = instance.get_typed_desc(); + + auto& stream = downcast(network.get_stream()); + ::sycl::queue& sycl_queue = stream.get_sycl_queue(); + std::vector<::sycl::event> sycl_events = to_sycl_events(events); + + const auto& params = instance.get_impl_params(); + auto in0_shape = params->input_layouts[0].get_shape(); + auto in1_shape = params->input_layouts[1].get_shape(); + auto out_shape = params->output_layouts[0].get_shape(); + + auto input0 = instance.input_memory_ptr(0); + auto input1 = instance.input_memory_ptr(1); + auto output = instance.output_memory_ptr(0); + + ov::element::Type_t in0_t = params->input_layouts[0].data_type; + ov::element::Type_t in1_t = params->input_layouts[1].data_type; + ov::element::Type_t out_t = params->output_layouts[0].data_type; + + OPENVINO_ASSERT(in0_t == in1_t && in0_t == out_t); + OPENVINO_ASSERT(in0_shape.size() == in1_shape.size() && in0_shape.size() == out_shape.size()); + + OPENVINO_ASSERT(ov::shape_size(in0_shape) == ov::shape_size(out_shape)); + if (ov::shape_size(in0_shape) != ov::shape_size(in1_shape)) { + if (ov::shape_size(in1_shape) == 1) { + // Handle broadcast case + } else { + OPENVINO_THROW("Eltwise does not support broadcasting except for scalar input"); + } + } + + const auto in0_alloc_type = input0->get_allocation_type(); + const auto in1_alloc_type = input1->get_allocation_type(); + const auto out_alloc_type = output->get_allocation_type(); + + const bool all_sycl_buffer = cldnn::everyone_is(cldnn::allocation_type::sycl_buffer, in0_alloc_type, in1_alloc_type, out_alloc_type); + const bool all_usm = memory_capabilities::is_usm_type(in0_alloc_type) && + memory_capabilities::is_usm_type(in1_alloc_type) && + memory_capabilities::is_usm_type(out_alloc_type); + + auto op = get_eltwise_operator(desc->mode); + + if (all_sycl_buffer) { + auto ev = run_eltwise(out_t, sycl_queue, sycl_events, + input0, input1, output, + in0_shape, in1_shape, out_shape, op); + return stream.create_base_event(ev); + } else if (all_usm) { + auto ev = run_eltwise(out_t, sycl_queue, sycl_events, + input0, input1, output, + in0_shape, in1_shape, out_shape, op); + return stream.create_base_event(ev); + } else { + OPENVINO_THROW("Eltwise SYCL requires all inputs/outputs to be either sycl_buffer or USM. " + "Got in0=", in0_alloc_type, ", in1=", in1_alloc_type, ", out=", out_alloc_type); + } + } + + static std::unique_ptr create(const eltwise_node& arg, const kernel_impl_params& impl_params) { + auto& engine = impl_params.prog->get_engine(); + auto& config = impl_params.prog->get_config(); + return std::make_unique(engine, config, nullptr /*weights_reorder*/); + } +}; + +std::unique_ptr EltwiseImplementationManager::create_impl(const program_node& node, const kernel_impl_params& params) const { + assert(node.is_type()); + return sycl::eltwise_sycl::create(static_cast(node), params); +} + +} // namespace sycl +} // namespace cldnn diff --git a/src/plugins/intel_gpu/src/graph/impls/sycl/eltwise.hpp b/src/plugins/intel_gpu/src/graph/impls/sycl/eltwise.hpp new file mode 100644 index 000000000000..df69b401ba79 --- /dev/null +++ b/src/plugins/intel_gpu/src/graph/impls/sycl/eltwise.hpp @@ -0,0 +1,58 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright (C) 2026 FUJITSU LIMITED +// + +#pragma once + +#include "eltwise_inst.h" +#include "registry/implementation_manager.hpp" + +#include + +namespace cldnn { +namespace sycl { + +struct EltwiseImplementationManager : public ImplementationManager { + OV_GPU_PRIMITIVE_IMPL("EltwiseImplementationManager") + EltwiseImplementationManager(shape_types shape_type, ValidateFunc vf = nullptr) : ImplementationManager(impl_types::sycl, shape_type, vf) {} + std::unique_ptr create_impl(const program_node& node, const kernel_impl_params& params) const override; + + bool validate_impl(const program_node& node) const override { + assert(node.is_type()); + + static const std::vector supported_formats = { + format::any, + format::bfyx, + }; + + static const std::vector supported_types = { + ov::element::f32, + ov::element::f16, + }; + + const auto& eltwise_node = node.as(); + const auto& in0_layout = eltwise_node.get_input_layout(0); + const auto& in1_layout = eltwise_node.get_input_layout(1); + const auto& out_layout = eltwise_node.get_output_layout(0); + auto in0_dt = in0_layout.data_type; + auto in1_dt = in1_layout.data_type; + auto out_dt = out_layout.data_type; + + if (!one_of(in0_dt, supported_types) || !one_of(in1_dt, supported_types) || !one_of(out_dt, supported_types)) + return false; + + if (!one_of(in0_layout.format.value, supported_formats) || !one_of(in1_layout.format.value, supported_formats) || + !one_of(out_layout.format.value, supported_formats)) + return false; + + if (in0_layout.data_padding || in1_layout.data_padding || out_layout.data_padding) + return false; + + return true; + } +}; + +} // namespace sycl +} // namespace cldnn diff --git a/src/plugins/intel_gpu/src/graph/impls/sycl/mem_adapter.hpp b/src/plugins/intel_gpu/src/graph/impls/sycl/mem_adapter.hpp new file mode 100644 index 000000000000..d13b7c8a58b4 --- /dev/null +++ b/src/plugins/intel_gpu/src/graph/impls/sycl/mem_adapter.hpp @@ -0,0 +1,49 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright (C) 2026 FUJITSU LIMITED +// + +#pragma once + +#include "intel_gpu/runtime/memory.hpp" +#include "sycl/sycl_wrapper.hpp" +#include "sycl/sycl_memory.hpp" + +#include +#include + +namespace cldnn { +namespace sycl { + +template +struct BufferAdapter { + using BufferType = decltype(std::declval().get_buffer().reinterpret()); + BufferType buf; + + static BufferAdapter from_memory(const cldnn::memory::ptr& mem) { + return { std::dynamic_pointer_cast(mem)->get_buffer().reinterpret() }; + } + + auto bind_read(::sycl::handler& cgh) { + return buf.template get_access<::sycl::access::mode::read>(cgh); + } + auto bind_write(::sycl::handler& cgh) { + return buf.template get_access<::sycl::access::mode::write>(cgh); + } +}; + +template +struct UsmAdapter { + DType* ptr; + + static UsmAdapter from_memory(const cldnn::memory::ptr& mem) { + return { static_cast(std::dynamic_pointer_cast(mem)->get_buffer().get()) }; + } + + DType* bind_read(::sycl::handler& /*cgh*/) const { return ptr; } + DType* bind_write(::sycl::handler& /*cgh*/) const { return ptr; } +}; + +} // namespace sycl +} // namespace cldnn diff --git a/src/plugins/intel_gpu/src/graph/impls/sycl/primitive_sycl_base.h b/src/plugins/intel_gpu/src/graph/impls/sycl/primitive_sycl_base.h index 4aad423ec3c2..b86746f2968b 100644 --- a/src/plugins/intel_gpu/src/graph/impls/sycl/primitive_sycl_base.h +++ b/src/plugins/intel_gpu/src/graph/impls/sycl/primitive_sycl_base.h @@ -8,11 +8,11 @@ #include "intel_gpu/runtime/memory.hpp" #include "registry/registry.hpp" #include "runtime/ocl/ocl_event.hpp" +#include "sycl/sycl_base_event.hpp" +#include "sycl/sycl_wrapper.hpp" #include -#include "sycl/sycl.hpp" - namespace cldnn { namespace sycl { @@ -51,6 +51,16 @@ struct typed_primitive_sycl_impl : public typed_primitive_impl { } } + static std::vector<::sycl::event> to_sycl_events(std::vector const& deps) { + std::vector<::sycl::event> events; + for (auto& dep : deps) { + if (auto sycl_base_ev = std::dynamic_pointer_cast(dep)) { + events.push_back(sycl_base_ev->get()); + } + } + return events; + } + std::vector get_internal_buffer_descs(const kernel_impl_params&) const override { return {}; } diff --git a/src/plugins/intel_gpu/src/graph/include/gather_matmul_inst.h b/src/plugins/intel_gpu/src/graph/include/gather_matmul_inst.h new file mode 100644 index 000000000000..6b1fb1e63434 --- /dev/null +++ b/src/plugins/intel_gpu/src/graph/include/gather_matmul_inst.h @@ -0,0 +1,49 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#pragma once +#include +#include + +#include "intel_gpu/primitives/gather_matmul.hpp" +#include "primitive_inst.h" + +namespace cldnn { + +template <> +struct typed_program_node : public typed_program_node_base { + using parent = typed_program_node_base; + typed_program_node(const std::shared_ptr prim, program& prog) : parent(prim, prog) { + support_padding_all(true); + } + +public: + using parent::parent; + + std::vector get_shape_infer_dependencies() const override { + return {}; + } + program_node& input() const { + return get_dependency(0); + } +}; + +using gather_matmul_node = typed_program_node; + +template <> +class typed_primitive_inst : public typed_primitive_inst_base { + using parent = typed_primitive_inst_base; + using parent::parent; + +public: + template + static std::vector calc_output_layouts(const gather_matmul_node& node, const kernel_impl_params& impl_param); + static layout calc_output_layout(const gather_matmul_node& node, const kernel_impl_params& impl_param); + static std::string to_string(const gather_matmul_node& node); + + typed_primitive_inst(network& network, const gather_matmul_node& node); +}; + +using gather_matmul_inst = typed_primitive_inst; +} // namespace cldnn diff --git a/src/plugins/intel_gpu/src/graph/include/loop_inst.h b/src/plugins/intel_gpu/src/graph/include/loop_inst.h index a4c4a460f114..eba359647bc1 100644 --- a/src/plugins/intel_gpu/src/graph/include/loop_inst.h +++ b/src/plugins/intel_gpu/src/graph/include/loop_inst.h @@ -49,7 +49,7 @@ struct typed_program_node : public typed_program_node_base { const primitive_id& get_execution_condition_id() const { return execution_condition_id; } const primitive_id& get_num_iterations_id() const { return num_iterations_id; } - const int32_t get_max_num_iteration() const { return get_primitive()->max_num_iterations; } + int32_t get_max_num_iteration() const { return get_primitive()->max_num_iterations; } const std::vector& get_input_primitive_maps() const { return input_primitive_maps; } const std::vector& get_output_primitive_maps() const { return output_primitive_maps; } diff --git a/src/plugins/intel_gpu/src/graph/include/moe_router_fused_inst.h b/src/plugins/intel_gpu/src/graph/include/moe_router_fused_inst.h new file mode 100644 index 000000000000..d363bfd37d31 --- /dev/null +++ b/src/plugins/intel_gpu/src/graph/include/moe_router_fused_inst.h @@ -0,0 +1,51 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#pragma once + +#include +#include +#include + +#include "intel_gpu/primitives/moe_router_fused.hpp" +#include "primitive_inst.h" + +namespace cldnn { + +template <> +struct typed_program_node : public typed_program_node_base { +private: + using parent = typed_program_node_base; + +public: + using parent::parent; + + typed_program_node(std::shared_ptr prim, program& prog) : parent(prim, prog) {} + + using parent::get_kernel_impl_params; + std::unique_ptr get_kernel_impl_params(const std::vector& in_layouts, const std::vector& out_layouts) const override { + auto params = parent::get_kernel_impl_params(in_layouts, out_layouts); + return params; + } + std::vector get_shape_infer_dependencies() const override { return {}; } +}; + +using moe_router_fused_node = typed_program_node; + +template <> +class typed_primitive_inst : public typed_primitive_inst_base { + using parent = typed_primitive_inst_base; + using parent::parent; + +public: + template + static std::vector calc_output_layouts(const moe_router_fused_node& /*node*/, const kernel_impl_params& impl_param); + static layout calc_output_layout(const moe_router_fused_node& /* node */, const kernel_impl_params& impl_param); + static std::string to_string(const moe_router_fused_node& node); + typed_primitive_inst(network& network, const moe_router_fused_node& node); +}; + +using moe_router_fused_inst = typed_primitive_inst; + +} // namespace cldnn diff --git a/src/plugins/intel_gpu/src/graph/include/paged_causal_conv1d_inst.h b/src/plugins/intel_gpu/src/graph/include/paged_causal_conv1d_inst.h new file mode 100644 index 000000000000..b9e7c4b372b5 --- /dev/null +++ b/src/plugins/intel_gpu/src/graph/include/paged_causal_conv1d_inst.h @@ -0,0 +1,43 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#pragma once + +#include "intel_gpu/primitives/paged_causal_conv1d.hpp" +#include "primitive_inst.h" + +namespace cldnn { + +template <> +struct typed_program_node : public typed_program_node_base { + using parent = typed_program_node_base; + +public: + using parent::parent; + + program_node& input(size_t index = 0) const { + return get_dependency(index); + } + std::vector get_shape_infer_dependencies() const override { + return {}; + } +}; +using paged_causal_conv1d_node = typed_program_node; + +template <> +class typed_primitive_inst : public typed_primitive_inst_base { + using parent = typed_primitive_inst_base; + using parent::parent; + +public: + template + static std::vector calc_output_layouts(const paged_causal_conv1d_node& node, const kernel_impl_params& impl_params); + static layout calc_output_layout(const paged_causal_conv1d_node& node, const kernel_impl_params& impl_params); + + static std::string to_string(const paged_causal_conv1d_node& node); + typed_primitive_inst(network& network, const paged_causal_conv1d_node& node); +}; + +using paged_causal_conv1d_inst = typed_primitive_inst; +} // namespace cldnn diff --git a/src/plugins/intel_gpu/src/graph/include/paged_gated_delta_net_inst.h b/src/plugins/intel_gpu/src/graph/include/paged_gated_delta_net_inst.h new file mode 100644 index 000000000000..8f9d0267c389 --- /dev/null +++ b/src/plugins/intel_gpu/src/graph/include/paged_gated_delta_net_inst.h @@ -0,0 +1,39 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#pragma once + +#include "intel_gpu/primitives/paged_gated_delta_net.hpp" +#include "primitive_inst.h" + +namespace cldnn { + +template <> +struct typed_program_node : public typed_program_node_base { + using parent = typed_program_node_base; + +public: + using parent::parent; + + program_node& input(size_t index = 0) const { return get_dependency(index); } + std::vector get_shape_infer_dependencies() const override { return {}; } +}; +using paged_gated_delta_net_node = typed_program_node; + +template <> +class typed_primitive_inst : public typed_primitive_inst_base { + using parent = typed_primitive_inst_base; + using parent::parent; + +public: + template + static std::vector calc_output_layouts(const paged_gated_delta_net_node& node, const kernel_impl_params& impl_params); + static layout calc_output_layout(const paged_gated_delta_net_node& node, const kernel_impl_params& impl_params); + + static std::string to_string(const paged_gated_delta_net_node& node); + typed_primitive_inst(network& network, const paged_gated_delta_net_node& node); +}; + +using paged_gated_delta_net_inst = typed_primitive_inst; +} // namespace cldnn diff --git a/src/plugins/intel_gpu/src/graph/include/primitive_inst.h b/src/plugins/intel_gpu/src/graph/include/primitive_inst.h index ddf93f2d38a1..d67f2fcecaba 100644 --- a/src/plugins/intel_gpu/src/graph/include/primitive_inst.h +++ b/src/plugins/intel_gpu/src/graph/include/primitive_inst.h @@ -47,12 +47,14 @@ class PrimitiveInstTestHelper; struct ImplementationManager; struct BufferDescriptor { - explicit BufferDescriptor(const layout& l, bool lockable = false) : m_lockable(lockable), m_layout(l) {} - BufferDescriptor(const ov::PartialShape& shape, ov::element::Type type, bool lockable = false) - : BufferDescriptor(layout(shape, type, format::bfyx), lockable) {} - BufferDescriptor(size_t elements_count, ov::element::Type type, bool lockable = false) - : BufferDescriptor(layout({static_cast(elements_count)}, type, format::bfyx), lockable) {} + explicit BufferDescriptor(const layout& l, bool lockable = false, bool shareable = true) + : m_lockable(lockable), m_shareable(shareable), m_layout(l) {} + BufferDescriptor(const ov::PartialShape& shape, ov::element::Type type, bool lockable = false, bool shareable = true) + : BufferDescriptor(layout(shape, type, format::bfyx), lockable, shareable) {} + BufferDescriptor(size_t elements_count, ov::element::Type type, bool lockable = false, bool shareable = true) + : BufferDescriptor(layout({static_cast(elements_count)}, type, format::bfyx), lockable, shareable) {} bool m_lockable = false; + bool m_shareable = true; // Whether this buffer can be shared via memory pool across primitives layout m_layout; }; @@ -322,7 +324,6 @@ class primitive_inst { bool mem_allocated() const { return _mem_allocated; } bool is_dynamic() const { return _is_dynamic; } bool can_share_buffer() const { return _can_share_buffer; } - bool can_share_internal_buffer() const { return _can_share_internal_buffer; } bool is_constant() const { return _is_constant; } bool needs_completion_event() const { return _needs_completion_event; } bool has_unfused_subgraph() const { return (_unfused_subgraph != nullptr); } @@ -441,7 +442,6 @@ class primitive_inst { size_t _fused_mem_offset = 0; bool _can_be_optimized = false; bool _can_share_buffer = true; - bool _can_share_internal_buffer = true; bool _is_constant = false; bool _needs_completion_event = false; @@ -451,7 +451,7 @@ class primitive_inst { std::vector allocate_outputs(kernel_impl_params* updated_params = nullptr, bool reset_mem = true, bool runtime_alloc = false); - memory::ptr allocate_internal_buffer(const layout& layout, size_t idx, bool reset = true, bool lockable = false); + memory::ptr allocate_internal_buffer(const layout& layout, size_t idx, bool reset = true, bool lockable = false, bool shareable = true); void allocate_shape_info_memory(); static std::vector build_exec_deps( std::vector> const& mem_deps); diff --git a/src/plugins/intel_gpu/src/graph/include/program_node.h b/src/plugins/intel_gpu/src/graph/include/program_node.h index 556f516790b0..99a9dcc737ed 100644 --- a/src/plugins/intel_gpu/src/graph/include/program_node.h +++ b/src/plugins/intel_gpu/src/graph/include/program_node.h @@ -22,6 +22,8 @@ #include #include #include +#include +#include namespace cldnn { @@ -321,10 +323,6 @@ struct program_node { bool can_share_buffer() const { return share_buffer; } void can_share_buffer(bool share) { share_buffer = share; } - // check/set if the node's internal buffer can be shared - bool can_share_internal_buffer() const { return share_internal_buffer; } - void can_share_internal_buffer(bool share) { share_internal_buffer = share; } - // Sets padding support for all axis void support_padding_all(bool support); // Sets padding support for specified axis @@ -652,24 +650,25 @@ inline RT test_no_input_pad(program_node& node, std::function original_padding(node.get_dependencies().size()); + // Use a map keyed by (dep_node_ptr, port) to handle duplicate dependencies correctly. + // When the same dependency appears multiple times (e.g., eltwise self-multiply), + // we must save and restore its padding only once. + std::map, padding> original_padding; for (size_t i = 0; i < node.get_dependencies().size(); i++) { auto dep_with_port = node.get_dependency_with_port(i); if (dep_with_port.first->is_constant()) continue; - original_padding[i] = dep_with_port.first->get_output_layout(false, dep_with_port.second).data_padding;; - + auto key = std::make_pair(dep_with_port.first, dep_with_port.second); + if (original_padding.count(key)) + continue; + original_padding[key] = dep_with_port.first->get_output_layout(false, dep_with_port.second).data_padding; dep_with_port.first->set_output_padding(padding(), dep_with_port.second); } RT res = f(node); - for (size_t i = 0; i < node.get_dependencies().size(); i++) { - auto dep_with_port = node.get_dependency_with_port(i); - if (dep_with_port.first->is_constant()) - continue; - - dep_with_port.first->set_output_padding(original_padding[i], dep_with_port.second); + for (auto& entry : original_padding) { + entry.first.first->set_output_padding(entry.second, entry.first.second); } return res; diff --git a/src/plugins/intel_gpu/src/graph/include/quantize_inst.h b/src/plugins/intel_gpu/src/graph/include/quantize_inst.h index 727d8c7ecc6f..de6007f05412 100644 --- a/src/plugins/intel_gpu/src/graph/include/quantize_inst.h +++ b/src/plugins/intel_gpu/src/graph/include/quantize_inst.h @@ -192,6 +192,16 @@ struct typed_program_node : public typed_program_node_base { float get_output_lo_val() const { return get_primitive()->out_lo; } float get_output_hi_val() const { return get_primitive()->out_hi; } + bool has_per_tensor_values() const { + return get_scale_shift_opt() && + get_per_tensor_input_scale() && + (get_per_tensor_input_shift() || !get_need_pre_shift()) && + get_per_tensor_input_range() && + get_per_tensor_output_scale() && + (get_per_tensor_output_shift() || !get_need_post_shift()) && + get_per_tensor_output_range(); + } + std::shared_ptr get_fuse_params() const override { return std::make_shared(get_output_layout(), get_primitive()->scale_shift_opt, diff --git a/src/plugins/intel_gpu/src/graph/include/reshape_inst.h b/src/plugins/intel_gpu/src/graph/include/reshape_inst.h index bb2e5978ea90..69e773802dc9 100644 --- a/src/plugins/intel_gpu/src/graph/include/reshape_inst.h +++ b/src/plugins/intel_gpu/src/graph/include/reshape_inst.h @@ -100,14 +100,21 @@ struct typed_program_node : public typed_program_node_base { // Iteratively check the total product of all static innermost dimensions // until the crop dimension value matches or the first dynamic dimension is encountered int64_t mul = 1; + size_t matched_trailing_dims = 0; for (size_t i = output_pshape.size(); i > 1 ; i--) { if (output_pshape[i - 1].is_dynamic() || mul == input_last_dim_val) break; mul *= output_pshape[i - 1].get_length(); + matched_trailing_dims++; } if (input_last_dim_val != mul) return false; + // Reject when reshape drops the cropped axis (e.g. [N,M,1] -> [N,M]): no output axis can + // carry the cropped axis padding, so sibling crop outputs would all point to the same + // base buffer region (aliased). + if (matched_trailing_dims == 0 && output_pshape.size() < input_pshape.size()) + return false; return true; } diff --git a/src/plugins/intel_gpu/src/graph/include/to_string_utils.h b/src/plugins/intel_gpu/src/graph/include/to_string_utils.h index 47967fb03ccb..70939306d74d 100644 --- a/src/plugins/intel_gpu/src/graph/include/to_string_utils.h +++ b/src/plugins/intel_gpu/src/graph/include/to_string_utils.h @@ -79,6 +79,7 @@ inline std::string activation_type_to_str(activation_func activation) { case activation_func::gelu_tanh: return "gelu_tanh"; case activation_func::round_half_to_even: return "round_half_to_even"; case activation_func::round_half_away_from_zero: return "round_half_away_from_zero"; + case activation_func::erfinv: return "erfinv"; default: return "unknown activation"; } } diff --git a/src/plugins/intel_gpu/src/graph/input_layout.cpp b/src/plugins/intel_gpu/src/graph/input_layout.cpp index 6f0eb0462543..5595e521e7f2 100644 --- a/src/plugins/intel_gpu/src/graph/input_layout.cpp +++ b/src/plugins/intel_gpu/src/graph/input_layout.cpp @@ -11,28 +11,30 @@ #include namespace { -bool has_optimized_users(cldnn::input_layout_node const& node) { - for (auto& user : node.get_users()) { - if (user->can_be_optimized()) { - return true; - } + +bool requires_eager_input_allocation(const cldnn::input_layout_node& node) { + if (node.is_dynamic()) { + return false; } - return false; + const auto& out_layout = node.get_output_layout(); + + // Scalars assume input memory is always materialized. Blocked inputs still have + // initialization paths that depend on an eager placeholder allocation. + return out_layout.count() <= 1 || cldnn::format::is_blocked(out_layout.format); } } // namespace namespace cldnn { GPU_DEFINE_PRIMITIVE_TYPE_ID(input_layout) -input_layout_node::typed_program_node(const std::shared_ptr dprim, program& prog) - : parent(dprim, prog) { +input_layout_node::typed_program_node(const std::shared_ptr dprim, program& prog) : parent(dprim, prog) { can_share_buffer(false); } -input_layout_inst::typed_primitive_inst(network& network, input_layout_node const& node) - : parent(network, node, !node.is_dynamic() && (!network.is_internal() || has_optimized_users(node))) { - _has_valid_input = false; // by default input for 'input_layout' is invalid as long as user doesn't call set_data +input_layout_inst::typed_primitive_inst(network& network, const input_layout_node& node) + : parent(network, node, /*allocate_mem*/ requires_eager_input_allocation(node)) { + _has_valid_input = false; // by default input for 'input_layout' is invalid as long as user doesn't call set_data } event::ptr input_layout_inst::set_data(memory::ptr mem, bool need_to_check_memory_to_set) { @@ -49,16 +51,27 @@ event::ptr input_layout_inst::set_data(memory::ptr mem, bool need_to_check_memor // Allow to set dummy simple_attached_memory empty tensor as network input if (mem->is_allocated_by(engine) || mem->get_layout().count() == 0) { - OPENVINO_ASSERT(!_outputs.empty(), "[GPU] Can't set data for empty input memory"); + if (_outputs.empty()) { + _outputs.resize(1); + } + if (_max_output_layout_count.empty()) { + _max_output_layout_count.resize(1); + } + + // Do not use set_output_memory() for externally-owned memory here. + // The same device buffer can be rebound with a different logical layout, + // and primitive_inst::set_output_memory() short-circuits on buffer identity. + // That keeps a stale memory object/layout on input_layout and breaks + // dynamic stateful flows that feed previous outputs back as inputs. _outputs[0] = mem; + _max_output_layout_count[0] = mem->get_layout().get_linear_size(); } else { - if (_outputs.empty() || !_outputs[0]) { + if (_outputs.empty()) { _outputs.resize(1); - _outputs[0] = engine.allocate_memory(mem->get_layout(), engine.get_preferred_memory_allocation_type(), false); } - - if (ol.is_dynamic() && _outputs[0]->size() < mem->size()) { - _outputs[0] = engine.allocate_memory(mem->get_layout(), engine.get_preferred_memory_allocation_type(), false); + // Only allocate if needed and not already large enough + if (!_outputs[0] || _outputs[0]->size() < mem->size()) { + set_output_memory(engine.allocate_memory(mem->get_layout(), engine.get_preferred_memory_allocation_type(), false), false); } mem_lock src(mem, stream); ev = _outputs[0]->copy_from(stream, src.data(), false); @@ -78,7 +91,7 @@ void input_layout_inst::update_shape() { _impl_params->output_layouts[0] = mem_layout; } -std::string input_layout_inst::to_string(input_layout_node const& node) { +std::string input_layout_inst::to_string(const input_layout_node& node) { auto node_info = node.desc_to_json(); std::stringstream primitive_description; diff --git a/src/plugins/intel_gpu/src/graph/kv_cache.cpp b/src/plugins/intel_gpu/src/graph/kv_cache.cpp index a4c2ed3fbd5e..4e9cb52ca07c 100644 --- a/src/plugins/intel_gpu/src/graph/kv_cache.cpp +++ b/src/plugins/intel_gpu/src/graph/kv_cache.cpp @@ -103,6 +103,15 @@ std::vector kv_cache_inst::calc_output_layouts(kv_cache_node const& /*no static const std::map ports_map = {{0, 0}, {1, 2}, {2, 3}, {3, 4}}; + // For INT4 KV-cache, pack two values per byte (halve inner dim) + if (desc->compressed) { + const auto kv_cache_dt = impl_param.get_program().get_config().get_kv_cache_precision(); + if (ov::element::Type(kv_cache_dt).bitwidth() == 4 && output_shapes[0].size() > 0) { + auto inner_dim_idx = output_shapes[0].size() - 1; + output_shapes[0][inner_dim_idx] = output_shapes[0][inner_dim_idx] / 2; + } + } + std::vector out_layouts; for (size_t i = 0; i < desc->num_outputs; i++) { auto out_type = desc->output_data_types[i].value_or(impl_param.get_input_layout(ports_map.at(i)).data_type); diff --git a/src/plugins/intel_gpu/src/graph/layout_optimizer.cpp b/src/plugins/intel_gpu/src/graph/layout_optimizer.cpp index 88a91690bd9f..65d1c362df5b 100644 --- a/src/plugins/intel_gpu/src/graph/layout_optimizer.cpp +++ b/src/plugins/intel_gpu/src/graph/layout_optimizer.cpp @@ -137,10 +137,6 @@ bool layout_optimizer::is_format_supported(program_node& node, format::type fmt) if (node.is_type() && fmt == format::byxf) return false; - if (node.is_type() && fmt == format::b_fs_yx_fsv16 && - node.get_input_layout(0).data_type != data_types::i8 && - node.get_input_layout(0).data_type != data_types::u8) - return false; if (node.is_type()) return node.get_output_layout().format == fmt; @@ -160,7 +156,9 @@ bool layout_optimizer::can_fuse_reorder(program_node& prev, program_node& next, auto next_dt = next.get_output_layout().data_type; auto use_onednn_impls = has_all_enabled_onednn_impls_optimization_attribute(); - if ((prev.is_dynamic() || next.is_dynamic()) && !next.is_type()) + // Under dynamic shape, skip reorder fusion except for primitives whose kernels handle + // cross-layout inputs at runtime: convolution, and MVN (fsv16/fsv32, see rule below). + if ((prev.is_dynamic() || next.is_dynamic()) && !next.is_type() && !next.is_type()) return false; // Not to fuse reorder if this removal changes input format of its next node which has reuse in fused_op @@ -208,6 +206,15 @@ bool layout_optimizer::can_fuse_reorder(program_node& prev, program_node& next, || fmt_next == format::bs_fs_yx_bsv32_fsv16 || fmt_next == format::bs_fs_yx_bsv32_fsv32)) return true; + // MVN kernel can accept cross-layout input between fsv16 and fsv32. + // Symmetric to the producer-direction rule in can_fuse_reorder_to_prev. + if (next.is_type() && + (fmt_prev == format::b_fs_yx_fsv16 || fmt_prev == format::b_fs_yx_fsv32 || + fmt_prev == format::b_fs_zyx_fsv16 || fmt_prev == format::b_fs_zyx_fsv32) && + (fmt_next == format::b_fs_yx_fsv16 || fmt_next == format::b_fs_yx_fsv32 || + fmt_next == format::b_fs_zyx_fsv16 || fmt_next == format::b_fs_zyx_fsv32)) + return true; + if (next.is_type() && (((prev_simple && next_simple) && (prev_dt == next_dt)) || ((fmt_prev == format::b_fs_yx_fsv4 && fmt_next == format::bfyx) && (prev_dt == data_types::u8 || prev_dt == data_types::i8)))) @@ -403,7 +410,8 @@ bool layout_optimizer::can_fuse_reorder_to_prev(program_node& prev, reorder_node bool is_dynamic = false; if (prev.is_dynamic() || (!node.get_users().empty() && node.get_users().front()->is_dynamic())) { if (!prev.is_type() && - !prev.is_type()) { + !prev.is_type() && + !prev.is_type()) { return false; } is_dynamic = true; @@ -448,6 +456,15 @@ bool layout_optimizer::can_fuse_reorder_to_prev(program_node& prev, reorder_node || fmt_next == format::bs_fs_yx_bsv32_fsv16 || fmt_next == format::bs_fs_yx_bsv32_fsv32)) return true; + // MVN kernel can work cross-layout between fsv16 and fsv32. + // Actual kernel support is verified by has_impl_for check in remove_redundant_reorders. + if (prev.is_type() && + (fmt_prev == format::b_fs_yx_fsv16 || fmt_prev == format::b_fs_yx_fsv32 || + fmt_prev == format::b_fs_zyx_fsv16 || fmt_prev == format::b_fs_zyx_fsv32) && + (fmt_next == format::b_fs_yx_fsv16 || fmt_next == format::b_fs_yx_fsv32 || + fmt_next == format::b_fs_zyx_fsv16 || fmt_next == format::b_fs_zyx_fsv32)) + return true; + if (prev.is_type() && !data_type_traits::is_floating_point(dt_prev) && data_type_traits::is_floating_point(dt_next) && @@ -544,8 +561,8 @@ layout_optimizer::layout_optimizer(bool output_size_handling_enabled) } bool layout_optimizer::is_depthwise(const convolution_node& node) const { - const int32_t output_channels = node.get_output_layout(0).feature(); - const int32_t input_channels = node.get_input_layout(0).feature(); + const auto output_channels = node.get_output_layout(0).feature(); + const auto input_channels = node.get_input_layout(0).feature(); return node.get_groups() == static_cast(input_channels) && input_channels == output_channels; } @@ -656,10 +673,26 @@ bool layout_optimizer::convolution_b_fs_yx_fsv16_opt(const layout& input_layout, int32_t required_feature_num = weak_restrictions ? feature_block_size / 2 : feature_block_size; bool correct_in_feature = (input_layout.feature() >= required_feature_num && output_layout.feature() >= required_feature_num); - int32_t in_features_per_group = input_layout.feature() / conv->groups; - int32_t out_features_per_group = output_layout.feature() / conv->groups; - if (!correct_in_feature && input_layout.feature() <= 4 && out_features_per_group >= feature_block_size) - correct_in_feature = true; + auto in_features_per_group = input_layout.feature() / conv->groups; + auto out_features_per_group = output_layout.feature() / conv->groups; + if (!correct_in_feature && input_layout.feature() <= 4 && out_features_per_group >= feature_block_size) { + // Estimate register pressure of bfyx_to_b_fs_yx_fsv16 kernel's line_cache[] array. + // Reject b_fs_yx_fsv16 when input_block_size > 64 to prevent CL_OUT_OF_RESOURCES + // on register-constrained GPUs (e.g. Xe-LPG with 128 GRFs/thread). + constexpr size_t default_block_width = 8; + constexpr size_t sg_size = 16; + size_t stride_x = conv->stride[conv->stride.size() - 1]; + size_t dilation_x = conv->dilation[conv->dilation.size() - 1] + 1; + size_t filter_x = weights_layout.spatial(0); + size_t filter_y = weights_layout.spatial(1); + size_t input_line_size = stride_x * (default_block_width - 1) + (filter_x - 1) * dilation_x + 1; + if (static_cast(input_layout.spatial(0)) < input_line_size) + input_line_size = static_cast(input_layout.spatial(0)); + size_t input_block_size = (input_line_size * filter_y + sg_size - 1) / sg_size; + if (input_block_size <= 64) + correct_in_feature = true; + } + bool depthwise = conv->groups == static_cast(input_layout.feature()); // depthwise conv bool grouped = ((feature_block_size % out_features_per_group == 0) && (feature_block_size % in_features_per_group == 0) && @@ -1414,11 +1447,6 @@ format layout_optimizer::get_preferred_format(program_node& node) { expected = format::get_default_format(node.get_output_layout().get_rank()); } else if (node.is_type()) { expected = get_expected_format(node.as()); - } else if (node.is_type()) { - auto input_layout = node.get_input_layout(0); - if (input_layout.data_type == data_types::f32 || input_layout.data_type == data_types::f16) { - expected = format::get_default_format(input_layout.get_rank()); - } } else if (node.is_type()) { // if the resample is in the last part of the network and there are no users using blocked format, // it is better to reorder to bfyx before resample is done. diff --git a/src/plugins/intel_gpu/src/graph/moe_router_fused.cpp b/src/plugins/intel_gpu/src/graph/moe_router_fused.cpp new file mode 100644 index 000000000000..6f0a8ab20e2e --- /dev/null +++ b/src/plugins/intel_gpu/src/graph/moe_router_fused.cpp @@ -0,0 +1,70 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include + +#include "intel_gpu/runtime/error_handler.hpp" +#include "json_object.h" +#include "moe_router_fused_inst.h" +#include "openvino/core/except.hpp" +#include "primitive_type_base.h" +#include "program_node.h" + +namespace cldnn { +GPU_DEFINE_PRIMITIVE_TYPE_ID(moe_router_fused) + +layout moe_router_fused_inst::calc_output_layout(const moe_router_fused_node& /* node */, const kernel_impl_params& impl_param) { + auto desc = impl_param.typed_desc(); + auto input_layout = impl_param.get_input_layout(0); + auto shape = input_layout.get_shape(); + size_t num_tokens = shape[0]; + if (shape.size() == 3) + num_tokens = shape[0] * shape[1]; + size_t top_k = desc->_config.top_k; + return layout(ov::Shape{num_tokens, top_k}, input_layout.data_type, format::bfyx); +} + +template +std::vector moe_router_fused_inst::calc_output_layouts(const moe_router_fused_node& /* node */, const kernel_impl_params& impl_param) { + auto desc = impl_param.typed_desc(); + auto input_layout = impl_param.get_input_layout(0); + auto input_pshape = input_layout.get_partial_shape(); + size_t top_k = desc->_config.top_k; + + OPENVINO_ASSERT(input_pshape.rank().is_static(), "Input rank must be static"); + ov::PartialShape out_shape; + auto num_tokens = input_pshape[0]; + if (input_pshape.rank().get_length() == 3) { + // 3D input [batch, seq_len, num_experts] -> num_tokens = batch * seq_len + num_tokens = input_pshape[0] * input_pshape[1]; + } + out_shape = ov::PartialShape{num_tokens, static_cast(top_k)}; + + // Output 0: topk_weights [num_tokens, top_k] — same element type as input + layout weights_layout(out_shape, input_layout.data_type, format::bfyx); + // Output 1: topk_indices [num_tokens, top_k] — i32 + layout indices_layout(out_shape, ov::element::i32, format::bfyx); + + return {weights_layout, indices_layout}; +} + +template std::vector moe_router_fused_inst::calc_output_layouts(const moe_router_fused_node& node, const kernel_impl_params& impl_param); + +std::string moe_router_fused_inst::to_string(const moe_router_fused_node& node) { + auto desc = node.get_primitive(); + auto node_info = node.desc_to_json(); + json_composite info; + info.add("num_expert", desc->_config.num_expert); + info.add("top_k", desc->_config.top_k); + info.add("routing_type", static_cast(desc->_config.routing_type)); + node_info->add("moe_router_fused info", info); + + std::stringstream primitive_description; + node_info->dump(primitive_description); + return primitive_description.str(); +} + +moe_router_fused_inst::typed_primitive_inst(network& network, const moe_router_fused_node& node) : parent(network, node) {} + +} // namespace cldnn diff --git a/src/plugins/intel_gpu/src/graph/multiclass_nms.cpp b/src/plugins/intel_gpu/src/graph/multiclass_nms.cpp index 0b709f8f530d..2e3a3db4dc71 100644 --- a/src/plugins/intel_gpu/src/graph/multiclass_nms.cpp +++ b/src/plugins/intel_gpu/src/graph/multiclass_nms.cpp @@ -57,7 +57,7 @@ layout multiclass_nms_inst::calc_output_layout(const multiclass_nms_node& node, num_classes = std::max(1, num_classes - 1); } - int max_output_boxes_per_class = 0; + ov::Dimension::value_type max_output_boxes_per_class = 0; if (attrs.nms_top_k >= 0) { max_output_boxes_per_class = std::min(num_boxes, attrs.nms_top_k); } else { diff --git a/src/plugins/intel_gpu/src/graph/network.cpp b/src/plugins/intel_gpu/src/graph/network.cpp index 8cd172f10941..e7df5f4df3b3 100644 --- a/src/plugins/intel_gpu/src/graph/network.cpp +++ b/src/plugins/intel_gpu/src/graph/network.cpp @@ -55,6 +55,7 @@ #include #include #include +#include #endif namespace cldnn { @@ -139,8 +140,80 @@ void dump_perf_data_raw(std::string dump_path, bool per_iter_mode, const std::li } } +// Dumps a per-primitive averaged execution time CSV with the same schema as +// benchmark_app --report_type average_counters and the CPU plugin's +// OV_CPU_AVERAGE_COUNTERS feature, so that aggregate-average-counters.py and +// other tooling can be reused across plugins. +void dump_average_counters(std::string dump_path, + uint32_t net_id, + const std::list>& exec_order) { + if (dump_path.empty()) + return; + + std::filesystem::path file_name{dump_path}; + file_name += "_" + std::to_string(net_id) + ".csv"; + std::ofstream file(file_name); + if (!file.is_open()) + return; + + const std::string header = "layerName;execStatus;layerType;execType;realTime (ms);cpuTime (ms);"; + file << header << "\n"; + + auto to_ms = [](uint64_t value_us) { + return static_cast(std::chrono::microseconds(value_us).count()) / 1000.0; + }; + + uint64_t total_us = 0; + + for (const auto& inst : exec_order) { + if (inst->is_constant()) + continue; + + // Aggregate inference-stage entries only, mirroring the CPU plugin which + // reports just the executed-kernel time. Other GPU pipeline stages + // (shape_inference, set_arguments, memory_allocation, ...) are host-side + // overhead that has no CPU-plugin counterpart. + const auto& perf_data = inst->get_profiling_data(); + const auto& perf_info = inst->get_profiling_info(); + uint64_t prim_total_us = 0; + size_t prim_total_iters = 0; + std::string impl_name; + size_t max_iters_for_impl = 0; + for (const auto& kv : perf_data) { + const auto& key = perf_info.at(kv.first); + if (key.stage != instrumentation::pipeline_stage::inference) + continue; + const auto cur_time = static_cast(std::get<0>(kv.second)); + const auto cur_iters = std::get<1>(kv.second); + prim_total_us += cur_time; + prim_total_iters += cur_iters; + // For dynamic shapes a primitive may have multiple inference entries + // (one per shape). Pick the impl_name that ran the most iterations + // as the representative execType. + if (cur_iters > max_iters_for_impl) { + max_iters_for_impl = cur_iters; + impl_name = key.impl_name; + } + } + + const uint64_t avg_us = prim_total_iters > 0 ? prim_total_us / prim_total_iters : 0; + const std::string status = avg_us > 0 ? "EXECUTED" : "NOT_RUN"; + const auto cpu_time = to_ms(avg_us); + const auto real_time = cpu_time; + + file << inst->id() << ";" << status << ";" << inst->desc()->type_string() << ";" + << impl_name << ";" << real_time << ";" << cpu_time << ";" << "\n"; + + total_us += avg_us; + } + + const auto total_ms = to_ms(total_us); + file << "Total;;;;" << total_ms << ";" << total_ms << ";\n"; +} + #else void dump_perf_data_raw(std::string, bool per_iter_mode, const std::list>&) {} +void dump_average_counters(std::string, uint32_t, const std::list>&) {} #endif } // namespace @@ -210,9 +283,14 @@ network::~network() { _memory_pool->clear_pool_for_network(net_id); std::string dump_path = GPU_DEBUG_VALUE_OR(get_config().get_dump_profiling_data_path(), ""); + GPU_DEBUG_IF(!dump_path.empty()) { dump_perf_data_raw(dump_path + "/perf_raw" + std::to_string(net_id) + ".csv", false, _exec_order); } + std::string avg_counters_path = GPU_DEBUG_VALUE_OR(get_config().get_average_counters(), ""); + GPU_DEBUG_IF(!avg_counters_path.empty()) { + dump_average_counters(avg_counters_path, net_id, _exec_order); + } } network::ptr network::allocate_network(stream::ptr stream, program::ptr program, bool is_internal, bool is_primary_stream) { @@ -324,10 +402,29 @@ event::ptr network::set_input_data(const primitive_id& id, memory::ptr data, boo if (primitive_inst->type() != input_layout::type_id()) { CLDNN_ERROR_MESSAGE(id, "primitive " + id + " is not an input"); } - auto input = std::static_pointer_cast(primitive_inst); + const bool was_unallocated = !input->output_memory_ptr(); + auto ev = input->set_data(data, need_to_check_memory_to_set); + + if (was_unallocated) { + // The initial set_arguments() skipped nodes whose dep buffer was null — + // force a fresh rebind now that the buffer is available. + _reset_arguments = true; + } - return input->set_data(data, need_to_check_memory_to_set); + // Update the shared mem type hint for the surfaces lock fast-path in execute(). + // We deduplicate by type value to prevent unbounded growth across inferences. + // Note: this vector is a conservative hint - false positives (stale surface types + // after input memory switches back to non-surface) are harmless since execute() + // re-checks live memory state when building in_out_mem. + // TODO: possibly remove or redesign _in_out_shared_mem_types solution + if (input->output_memory_ptr()) { + const auto in_mem_type = input->output_memory_ptr()->get_internal_params().mem_type; + if (std::find(_in_out_shared_mem_types.begin(), _in_out_shared_mem_types.end(), in_mem_type) == _in_out_shared_mem_types.end()) + _in_out_shared_mem_types.push_back(in_mem_type); + } + + return ev; } void network::add_default_output_chains() { @@ -387,7 +484,8 @@ network::output_chains_map::iterator network::add_output_chain(std::shared_ptr

chain; std::stack candidates; auto& eng = get_engine(); - const auto& mem_orig = p_inst->output_memory(); + + const auto& mem_orig = p_inst->output_memory_ptr(); auto add_mdata_chain = [&](primitive_inst* p_inst) { auto mdata_ptr = dynamic_cast(p_inst); @@ -397,12 +495,12 @@ network::output_chains_map::iterator network::add_output_chain(std::shared_ptr

dependencies()) { // check dependencies - if (dep.first->outputs_allocated() && eng.is_the_same_buffer(mem_orig, dep.first->output_memory())) { + if (dep.first->outputs_allocated() && mem_orig && eng.is_the_same_buffer(*mem_orig, dep.first->output_memory())) { chain.push_back(const_cast(dep.first)); } // then second order dependencies for (auto& second_dep : dep.first->dependencies()) { - if (second_dep.first->outputs_allocated() && eng.is_the_same_buffer(mem_orig, second_dep.first->output_memory())) { + if (second_dep.first->outputs_allocated() && mem_orig && eng.is_the_same_buffer(*mem_orig, second_dep.first->output_memory())) { chain.push_back(const_cast(second_dep.first)); } } @@ -412,7 +510,7 @@ network::output_chains_map::iterator network::add_output_chain(std::shared_ptr

get_user_ids(); for (const auto& id : user_ids) { auto usr_prim = get_primitive(id).get(); - if (usr_prim->outputs_allocated() && eng.is_the_same_buffer(mem_orig, usr_prim->output_memory())) { + if (usr_prim->outputs_allocated() && mem_orig && eng.is_the_same_buffer(*mem_orig, usr_prim->output_memory())) { chain.push_back(usr_prim); } } @@ -431,7 +529,7 @@ network::output_chains_map::iterator network::add_output_chain(std::shared_ptr

outputs_allocated() - || (cand->outputs_allocated() && eng.is_the_same_buffer(mem_orig, cand->output_memory()))) { + || (cand->outputs_allocated() && eng.is_the_same_buffer(*mem_orig, cand->output_memory()))) { auto nc_cand = const_cast(cand); chain.push_back(nc_cand); add_mdata_chain(nc_cand); @@ -445,7 +543,7 @@ network::output_chains_map::iterator network::add_output_chain(std::shared_ptr

output_memory(); // Add dep inst to the chain when dep's output is not allocated yet. if (!p_inst->outputs_allocated() - || eng.is_the_same_buffer(mem_orig, mem_dep)) { + || eng.is_the_same_buffer(*mem_orig, mem_dep)) { auto nc_dep = const_cast(dep.first); chain.push_back(nc_dep); add_mdata_chain(nc_dep); @@ -554,6 +652,10 @@ void network::allocate_primitives() { if (!node->get_dependencies().empty() && opt_inst->dependencies().empty()) { opt_inst->build_deps(); } + // Skip if the dependency's memory is not yet allocated (e.g. lazy input_layout). + // The output memory will be set up at runtime when the input becomes available. + if (!opt_inst->dependencies().empty() && opt_inst->dep_memory_ptr(0) == nullptr) + continue; opt_inst->update_output_memory(); } } @@ -873,7 +975,8 @@ std::vector network::get_input_ids() const { std::vector network::get_input_layouts() const { std::vector ret; ret.reserve(_inputs.size()); - for (auto const& input : _inputs) ret.push_back(input->output_memory_ptr()->get_layout()); + for (auto const& input : _inputs) + ret.push_back(input->output_memory_ptr() ? input->output_memory_ptr()->get_layout() : input->get_output_layout()); return ret; } diff --git a/src/plugins/intel_gpu/src/graph/one_hot.cpp b/src/plugins/intel_gpu/src/graph/one_hot.cpp index 834d9f9871fc..34e9c1a513c6 100644 --- a/src/plugins/intel_gpu/src/graph/one_hot.cpp +++ b/src/plugins/intel_gpu/src/graph/one_hot.cpp @@ -15,7 +15,7 @@ namespace cldnn { GPU_DEFINE_PRIMITIVE_TYPE_ID(one_hot) -static bool is_output_bfzyx(const layout& input, int32_t axis) { +static bool is_output_bfzyx(const layout& input, int64_t axis) { if (input.format == format::bfzyx) return true; if (axis == 4) diff --git a/src/plugins/intel_gpu/src/graph/paged_attention.cpp b/src/plugins/intel_gpu/src/graph/paged_attention.cpp index 6d80388bbb6e..7c7b5676260a 100644 --- a/src/plugins/intel_gpu/src/graph/paged_attention.cpp +++ b/src/plugins/intel_gpu/src/graph/paged_attention.cpp @@ -13,7 +13,6 @@ GPU_DEFINE_PRIMITIVE_TYPE_ID(paged_attention) paged_attention_node::typed_program_node(const std::shared_ptr prim, program& prog) : parent(prim, prog) { - can_share_internal_buffer(false); } layout paged_attention_inst::calc_output_layout(const paged_attention_node& /*node*/, kernel_impl_params const& impl_param) { @@ -45,9 +44,12 @@ std::vector paged_attention_inst::calc_output_layouts(paged_attention_no impl_param.get_input_layout(key_cache_idx).data_type == ov::element::u8 || data_type_traits::is_i4_u4(key_cache_dt); auto expected_block_size = desc->has_xattention ? paged_attention::block_size_xattn : paged_attention::block_size; + // Both INT4 and INT8 BY_CHANNEL use dim order {0,1,3,2} with block_size at dim[3]. + const bool is_int4 = data_type_traits::is_i4_u4(key_cache_dt); if (key_cache_compressed && key_cache_quant_mode == ov::internal::CacheQuantMode::BY_CHANNEL) { - if (data_type_traits::is_i4_u4(key_cache_dt)) { - expected_block_size += 8; + if (is_int4) { + // INT4 BY_CHANNEL: block_size dim is packed (u4→u8) + scale/zp = block_size/2 + sizeof(fp16)*2 + expected_block_size = expected_block_size / 2 + 4; } else { constexpr size_t kv_sub_block_size = 16; OPENVINO_ASSERT(expected_block_size % kv_sub_block_size == 0, @@ -63,22 +65,13 @@ std::vector paged_attention_inst::calc_output_layouts(paged_attention_no "[GPU] Paged Attention key cache quantization mode mismatch: prim.is_key_by_channel : ", desc->is_key_by_channel, " but exec_config : ", impl_param.get_program().get_config().get_key_cache_quant_mode()); + // Both INT4 and INT8 BY_CHANNEL use {0,1,3,2} dim order (block_size at dim[3]). const auto block_size_idx = desc->has_xattention ? 2 : 3; bool valid_block_size = key_cache_ps.is_dynamic() || (key_cache_ps[block_size_idx].get_length() == static_cast(expected_block_size)); OPENVINO_ASSERT(valid_block_size, "[GPU] Incorrect block size for Paged Attention operation for key cache quant mode " , key_cache_quant_mode, ". Expected ", expected_block_size, ", but got ", key_cache_ps[block_size_idx].get_length()); - // TODO: as a preview feature, only single sequence is supported so far. Will remove this check once - // full function ready in near future. - if (desc->has_xattention) { - const auto& subseq_begins_ps = impl_param.get_input_layout(PagedAttentionInputIdx::SUBSEQUENCE_BEGINS).get_partial_shape(); - bool valid_subseq_count = subseq_begins_ps.is_dynamic() || - (subseq_begins_ps[0].get_length() == static_cast(2)); - if (!valid_subseq_count) - OPENVINO_THROW("[GPU] Unexpected sub sequences count for XAttention. Got ", subseq_begins_ps[0].get_length() - 1); - } - std::vector output_layouts{ data_layout }; if (desc->has_scores_output()) { diff --git a/src/plugins/intel_gpu/src/graph/paged_causal_conv1d.cpp b/src/plugins/intel_gpu/src/graph/paged_causal_conv1d.cpp new file mode 100644 index 000000000000..64c59a0958e2 --- /dev/null +++ b/src/plugins/intel_gpu/src/graph/paged_causal_conv1d.cpp @@ -0,0 +1,72 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "openvino/op/paged_causal_conv1d.hpp" + +#include +#include + +#include "json_object.h" +#include "paged_causal_conv1d_inst.h" +#include "paged_causal_conv1d_shape_inference.hpp" +#include "primitive_type_base.h" +#include "to_string_utils.h" + +namespace cldnn { +GPU_DEFINE_PRIMITIVE_TYPE_ID(paged_causal_conv1d) + +layout paged_causal_conv1d_inst::calc_output_layout(const paged_causal_conv1d_node& node, const kernel_impl_params& impl_param) { + return calc_output_layouts(node, impl_param)[0]; +} + +template +std::vector paged_causal_conv1d_inst::calc_output_layouts(const paged_causal_conv1d_node& node, const kernel_impl_params& impl_param) { + const auto& all_inputs = node.get_input_layouts(); + OPENVINO_ASSERT(all_inputs.size() == 9, "paged_causal_conv1d must have 9 inputs"); + + const auto input_layout = impl_param.get_input_layout(0); + + std::vector input_shapes; + input_shapes.reserve(all_inputs.size()); + for (size_t i = 0; i < all_inputs.size(); i++) { + input_shapes.push_back(impl_param.get_input_layout(i).get()); + } + + ov::op::internal::PagedCausalConv1D op; + const auto output_shapes = ov::op::internal::shape_infer(&op, input_shapes); + + return {layout(output_shapes[0], input_layout.data_type, input_layout.format)}; +} + +template std::vector paged_causal_conv1d_inst::calc_output_layouts(const paged_causal_conv1d_node& node, + const kernel_impl_params& impl_param); + +std::string paged_causal_conv1d_inst::to_string(const paged_causal_conv1d_node& node) { + auto node_info = node.desc_to_json(); + auto desc = node.get_primitive(); + + std::stringstream primitive_description; + + json_composite pcc_info; + pcc_info.add("input_embeds", node.input(0).id()); + pcc_info.add("conv_state_table", node.input(1).id()); + pcc_info.add("conv_weight", node.input(2).id()); + pcc_info.add("conv_bias", node.input(3).id()); + pcc_info.add("subsequence_begins", node.input(4).id()); + pcc_info.add("block_indices", node.input(5).id()); + pcc_info.add("block_indices_begins", node.input(6).id()); + pcc_info.add("past_lens", node.input(7).id()); + pcc_info.add("cache_interval", node.input(8).id()); + pcc_info.add("hidden_size", desc->hidden_size); + pcc_info.add("kernel_size", desc->kernel_size); + + node_info->add("paged_causal_conv1d_info", pcc_info); + node_info->dump(primitive_description); + + return primitive_description.str(); +} + +paged_causal_conv1d_inst::typed_primitive_inst(network& network, const paged_causal_conv1d_node& node) : parent(network, node) {} + +} // namespace cldnn diff --git a/src/plugins/intel_gpu/src/graph/paged_gated_delta_net.cpp b/src/plugins/intel_gpu/src/graph/paged_gated_delta_net.cpp new file mode 100644 index 000000000000..23901ab12fbd --- /dev/null +++ b/src/plugins/intel_gpu/src/graph/paged_gated_delta_net.cpp @@ -0,0 +1,76 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "openvino/op/paged_gated_delta_net.hpp" + +#include +#include + +#include "json_object.h" +#include "paged_gated_delta_net_inst.h" +#include "paged_gated_delta_net_shape_inference.hpp" +#include "primitive_type_base.h" +#include "to_string_utils.h" + +namespace cldnn { +GPU_DEFINE_PRIMITIVE_TYPE_ID(paged_gated_delta_net) + +layout paged_gated_delta_net_inst::calc_output_layout(const paged_gated_delta_net_node& node, const kernel_impl_params& impl_param) { + return calc_output_layouts(node, impl_param)[0]; +} + +template +std::vector paged_gated_delta_net_inst::calc_output_layouts(const paged_gated_delta_net_node& node, const kernel_impl_params& impl_param) { + const auto& all_inputs = node.get_input_layouts(); + OPENVINO_ASSERT(all_inputs.size() == 11, "paged_gated_delta_net must have 11 inputs"); + + const auto value_layout = impl_param.get_input_layout(2); + + std::vector input_shapes; + input_shapes.reserve(all_inputs.size()); + for (size_t i = 0; i < all_inputs.size(); i++) { + input_shapes.push_back(impl_param.get_input_layout(i).get()); + } + + ov::op::internal::PagedGatedDeltaNet op; + const auto output_shapes = ov::op::internal::shape_infer(&op, input_shapes); + + return {layout(output_shapes[0], value_layout.data_type, value_layout.format)}; +} + +template std::vector paged_gated_delta_net_inst::calc_output_layouts(const paged_gated_delta_net_node& node, + const kernel_impl_params& impl_param); + +std::string paged_gated_delta_net_inst::to_string(const paged_gated_delta_net_node& node) { + auto node_info = node.desc_to_json(); + auto desc = node.get_primitive(); + + std::stringstream primitive_description; + + json_composite paged_gdn_info; + paged_gdn_info.add("query", node.input(0).id()); + paged_gdn_info.add("key", node.input(1).id()); + paged_gdn_info.add("value", node.input(2).id()); + paged_gdn_info.add("recurrent_state_table", node.input(3).id()); + paged_gdn_info.add("gate", node.input(4).id()); + paged_gdn_info.add("beta", node.input(5).id()); + paged_gdn_info.add("subsequence_begins", node.input(6).id()); + paged_gdn_info.add("block_indices", node.input(7).id()); + paged_gdn_info.add("block_indices_begins", node.input(8).id()); + paged_gdn_info.add("past_lens", node.input(9).id()); + paged_gdn_info.add("cache_interval", node.input(10).id()); + paged_gdn_info.add("k_head_size", desc->k_head_size); + paged_gdn_info.add("v_head_size", desc->v_head_size); + paged_gdn_info.add("k_heads_num", desc->k_heads_num); + paged_gdn_info.add("v_heads_num", desc->v_heads_num); + + node_info->add("paged_gated_delta_net_info", paged_gdn_info); + node_info->dump(primitive_description); + + return primitive_description.str(); +} + +paged_gated_delta_net_inst::typed_primitive_inst(network& network, const paged_gated_delta_net_node& node) : parent(network, node) {} + +} // namespace cldnn diff --git a/src/plugins/intel_gpu/src/graph/permute.cpp b/src/plugins/intel_gpu/src/graph/permute.cpp index 0d218d45c8c3..edf9690066eb 100644 --- a/src/plugins/intel_gpu/src/graph/permute.cpp +++ b/src/plugins/intel_gpu/src/graph/permute.cpp @@ -80,7 +80,7 @@ std::vector permute_inst::calc_output_layouts(permute_node const& node, auto permute_order = desc->permute_order; if (permute_order.empty()) { for (int64_t i = 1; i <= input_static_rank; ++i) { - permute_order.emplace_back(input_static_rank - i); + permute_order.emplace_back(static_cast(input_static_rank - i)); } } @@ -141,12 +141,15 @@ void permute_inst::update_output_memory() { if (!can_be_optimized() || _impl_params->is_dynamic()) return; + build_deps(); + + if (input_memory_ptr() == nullptr) + return; + if (_outputs.size() > 0 && static_cast(_outputs[0]) && _network.get_engine().is_the_same_buffer(output_memory(), input_memory())) return; - build_deps(); - GPU_DEBUG_TRACE_DETAIL << id() << " : update_output_memory with mem of input " << get_node().get_dependency(0).id() << " : " << input_memory_ptr()->buffer_ptr() << std::endl; // Can_be_optimized nodes are allocating from memory_pool too. In this case, diff --git a/src/plugins/intel_gpu/src/graph/primitive_inst.cpp b/src/plugins/intel_gpu/src/graph/primitive_inst.cpp index 9f68ae9603b7..07df9c2f7ff6 100644 --- a/src/plugins/intel_gpu/src/graph/primitive_inst.cpp +++ b/src/plugins/intel_gpu/src/graph/primitive_inst.cpp @@ -2,6 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 // +#include + #include "intel_gpu/graph/kernel_impl_params.hpp" #include "intel_gpu/primitives/implementation_desc.hpp" #include "intel_gpu/runtime/stream.hpp" @@ -691,11 +693,11 @@ void primitive_inst::realloc_intermediates() { // we'll need additional handle for that purpose like need_reset_output_memory const bool need_reset = false; if (i < _intermediates_memory.size()) { - _intermediates_memory[i] = allocate_internal_buffer(buffer_descs[i].m_layout, i, need_reset, need_lockable); + _intermediates_memory[i] = allocate_internal_buffer(buffer_descs[i].m_layout, i, need_reset, need_lockable, buffer_descs[i].m_shareable); _max_intermediates_memory_sizes[i] = _intermediates_memory[i]->size(); } else { // i-th layout has not been allocated yet - _intermediates_memory.push_back(allocate_internal_buffer(buffer_descs[i].m_layout, i, need_reset, need_lockable)); + _intermediates_memory.push_back(allocate_internal_buffer(buffer_descs[i].m_layout, i, need_reset, need_lockable, buffer_descs[i].m_shareable)); _max_intermediates_memory_sizes.push_back(_intermediates_memory[i]->size()); } GPU_DEBUG_CODE(memalloc_info += @@ -1077,8 +1079,8 @@ void primitive_inst::realloc_outputs(bool prev_execution_skipped) { if (get_node().is_type() && i != 1) { const auto& desc = get_node().as().get_primitive(); const auto shape_rank = updated_layouts[i].get_shape().size(); - const auto seq_axis = i == 0 ? kv_cache_inst::get_sequence_axis(desc->concat_axis, shape_rank) - : kv_cache_inst::get_scale_zp_sequence_axis(); + const int32_t seq_axis = static_cast(i == 0 ? kv_cache_inst::get_sequence_axis(desc->concat_axis, shape_rank) + : kv_cache_inst::get_scale_zp_sequence_axis()); prealloc_info = sp.predict_preallocation_shape(id(), updated_layouts[i], false, i, tmp_prealloc_count, seq_axis); } else { @@ -1332,10 +1334,10 @@ void primitive_inst::fill_shape_info_data(const layout& runtime_layout, const la if (dynamic_pad[j] == 1) { GPU_DEBUG_TRACE_DETAIL << " shape_info[" << offset << "] = " << lower_pads[j] << "(pad_before for " << j << "-th dim)" << std::endl; - shape_info_ptr[offset++] = lower_pads[j]; // pad_before + shape_info_ptr[offset++] = static_cast(lower_pads[j]); // pad_before GPU_DEBUG_TRACE_DETAIL << " shape_info[" << offset << "] = " << upper_pads[j] << "(pad_after for " << j << "-th dim)" << std::endl; - shape_info_ptr[offset++] = upper_pads[j]; // pad_after + shape_info_ptr[offset++] = static_cast(upper_pads[j]); // pad_after } } } @@ -1949,7 +1951,11 @@ void primitive_inst::do_runtime_in_place_crop() { } crop_in_place_optimization::update_in_place_crop_padding_along_feature(u->get_node(), crop_layout, pred_layout, offsets, crop_axis, true); } else if (crop_in_place_optimization::can_crop_be_optimized_simple_data_format(crop_layout, pred_layout)) { - crop_in_place_optimization::update_in_place_crop_padding_simple_data_format(crop_layout, pred_layout, user_info, offsets, crop_axis, true); + if (!crop_in_place_optimization::update_in_place_crop_padding_simple_data_format(crop_layout, pred_layout, user_info, offsets, crop_axis, true)) { + u->set_can_be_optimized(false); + GPU_DEBUG_TRACE_DETAIL << "[In place crop] " << u->id() << " cannot be optimized due to padding indivisibility" << std::endl; + continue; + } if (user_info.first) { auto reshape_inst = crop_users.front(); reshape_inst->_impl_params->output_layouts[0] = user_info.second; @@ -2287,7 +2293,8 @@ void primitive_inst::execute() { set_out_event(_impl->execute(_impl_params->dep_events, *this)); - GPU_DEBUG_IF(!get_config().get_dump_profiling_data_path().empty()) { + GPU_DEBUG_IF(!get_config().get_dump_profiling_data_path().empty() || + !get_config().get_average_counters().empty()) { auto ev = _impl_params->out_event; get_network().get_stream().wait_for_events({ev}); @@ -2356,7 +2363,6 @@ primitive_inst::primitive_inst(network & network, program_node const& node, bool , _fused_mem_offset((_fused_mem_count > 0 && node.get_first_fused_dep_idx() > 0) ? static_cast(node.get_first_fused_dep_idx()) : 0) , _can_be_optimized(node.can_be_optimized()) , _can_share_buffer(node.can_share_buffer()) - , _can_share_internal_buffer(node.can_share_internal_buffer()) , _is_constant(node.is_constant()) , _needs_completion_event(is_any_user_cpu(node.get_users()) || node.is_output()) { // When dynamic shape node has huge upper boundary which causes bigger mem size than system max allocable mem size, do not allocate in build time. @@ -2394,6 +2400,12 @@ primitive_inst::primitive_inst(network & network, program_node const& node, bool // sum post-op can use the input buffer as the output buffer auto& eltw_node = node.get_dependency(reused_eltwmem_idx); const auto& eltw_inst = _network.get_primitive(eltw_node.id()); + if (eltw_node.is_type() && !eltw_inst->outputs_allocated()) { + auto eltw_mem = _network.get_engine().allocate_memory(eltw_node.get_output_layout(), + _network.get_engine().get_preferred_memory_allocation_type(), + /*reset*/false); + eltw_inst->set_output_memory(eltw_mem, /*check*/false); + } auto& eltw_mem = eltw_inst->output_memory(); auto new_mem = eltw_mem.get_engine()->reinterpret_buffer(eltw_mem, node.get_output_layout()); _outputs.push_back(new_mem); @@ -2428,7 +2440,7 @@ primitive_inst::primitive_inst(network & network, program_node const& node, bool OPENVINO_ASSERT(_max_output_layout_count.size() == get_node().get_output_layouts().size()); } -memory::ptr primitive_inst::allocate_internal_buffer(const layout& layout, size_t idx, bool reset, bool lockable) { +memory::ptr primitive_inst::allocate_internal_buffer(const layout& layout, size_t idx, bool reset, bool lockable, bool shareable) { if (_impl == nullptr || _outputs.empty() || _outputs[0] == nullptr) return nullptr; @@ -2492,7 +2504,7 @@ memory::ptr primitive_inst::allocate_internal_buffer(const layout& layout, size_ *_node, layout, alloc_type, - can_share_internal_buffer(), + shareable, _runtime_memory_dependencies, reset, _intermediates_memory.size() > idx ? _intermediates_memory[idx].get() : nullptr); @@ -2513,7 +2525,7 @@ void primitive_inst::allocate_internal_buffers(bool reset) { for (size_t i = 0; i < buffer_descs.size(); ++i) { if (buffer_descs[i].m_layout.get_linear_size() == 0) continue; - intermediates_memory.push_back(allocate_internal_buffer(buffer_descs[i].m_layout, i, reset)); + intermediates_memory.push_back(allocate_internal_buffer(buffer_descs[i].m_layout, i, reset, buffer_descs[i].m_lockable, buffer_descs[i].m_shareable)); _max_intermediates_memory_sizes.push_back(intermediates_memory[i]->size()); } _intermediates_memory = intermediates_memory; @@ -2624,7 +2636,8 @@ void primitive_inst::update_weights() { reorder_impl->set_arguments(*reorder_inst, args); add_dep_event(reorder_impl->execute({}, *reorder_inst)); - GPU_DEBUG_IF(!get_config().get_dump_profiling_data_path().empty()) { + GPU_DEBUG_IF(!get_config().get_dump_profiling_data_path().empty() || + !get_config().get_average_counters().empty()) { stream.wait_for_events(_impl_params->dep_events); } @@ -2715,10 +2728,14 @@ memory::ptr primitive_inst::allocate_output(engine& _engine, bool is_reorder_weights = node.is_type() && node.as().get_primitive()->weights_reorder_params; if (node.can_be_optimized() || is_reorder_weights) { GPU_DEBUG_LOG << "[" << node.id() << ": output]" << std::endl; - // Use usm_device memory for weights reordering - if (is_internal && is_reorder_weights && - _engine.supports_allocation(allocation_type::usm_device)) - alloc_type = allocation_type::usm_device; + // Use usm_device memory for weights reordering when available. + // Skip memset (reset=false) regardless of the final allocation type — + // the weights reorder kernel writes every output element unconditionally. + if (is_internal && is_reorder_weights) { + if (_engine.supports_allocation(allocation_type::usm_device)) + alloc_type = allocation_type::usm_device; + reset = false; + } return get_memory_from_pool(_engine, net_id, pool, @@ -3096,7 +3113,9 @@ std::shared_ptr ImplementationsFactory::get_primitive_impl_for_p if (!impl_manager->support_shapes(params)) continue; - return impl_manager->create(*node, params); + auto impl = impl_manager->create(*node, params); + if (impl) + return impl; } return nullptr; @@ -3146,11 +3165,12 @@ std::shared_ptr ImplementationsFactory::get_primitive_impl_for_p std::unique_ptr impl = find_impl(&inst.get_node(), updated_params, shape_types::static_shape); - if (impl->get_kernels_source().size() > 0) { + if (impl && impl->get_kernels_source().size() > 0) { auto kernels = _program.get_kernels_cache().compile(updated_params, impl->get_kernels_source()); impl->set_kernels(kernels); } - cache.add(updated_params, std::move(impl)); + if (impl) + cache.add(updated_params, std::move(impl)); }); } @@ -3182,7 +3202,34 @@ std::shared_ptr ImplementationsFactory::get_primitive_impl_for_p // 6. Finally, if no impl found so far, we just enforce static impl compilation auto static_impl = find_impl(node, updated_params, shape_types::static_shape); - OPENVINO_ASSERT(static_impl != nullptr, "No static impl " + node->id()); + if (!static_impl) { + auto stringify_layouts = [](const std::vector& layouts) { + if (layouts.empty()) + return std::string(""); + + std::ostringstream oss; + oss << layouts[0].to_short_string(); + for (size_t i = 1; i < layouts.size(); ++i) { + oss << ", " << layouts[i].to_short_string(); + } + return oss.str(); + }; + + std::ostringstream available_impls_oss; + available_impls_oss << "available_impls: " << m_available_impls.size() << " ["; + for (size_t i = 0; i < m_available_impls.size(); ++i) { + if (i > 0) + available_impls_oss << ","; + const auto& m = m_available_impls[i]; + available_impls_oss << " {type=" << static_cast(m->get_impl_type()) + << ", shape=" << static_cast(m->get_shape_type()) << "}"; + } + available_impls_oss << " ]"; + + OPENVINO_ASSERT(false, "No static impl " + node->id() + ". " + available_impls_oss.str() + + ". Input: " + stringify_layouts(updated_params.input_layouts) + + ". Output: " + stringify_layouts(updated_params.output_layouts)); + } static_impl->set_node_params(*node); if (!inst.can_be_optimized()) { auto& kernels_cache = prog.get_kernels_cache(); diff --git a/src/plugins/intel_gpu/src/graph/prior_box.cpp b/src/plugins/intel_gpu/src/graph/prior_box.cpp index 60b29ee4c2a0..4e88f4f4255f 100644 --- a/src/plugins/intel_gpu/src/graph/prior_box.cpp +++ b/src/plugins/intel_gpu/src/graph/prior_box.cpp @@ -26,10 +26,10 @@ void calculate_prior_box_output(memory::ptr output_mem, stream& stream, layout c // All the inputs for this layer are known at this point, // so the output buffer is written here and not in execute(). - const int layer_width = input_layout.spatial(0); - const int layer_height = input_layout.spatial(1); - const int img_width = argument.img_size.spatial[0]; - const int img_height = argument.img_size.spatial[1]; + const int64_t layer_width = input_layout.spatial(0); + const int64_t layer_height = input_layout.spatial(1); + const int64_t img_width = argument.img_size.spatial[0]; + const int64_t img_height = argument.img_size.spatial[1]; float step_w = argument.step_width; float step_h = argument.step_height; if (!argument.is_clustered() && (step_w == 0 || step_h == 0)) { @@ -37,18 +37,18 @@ void calculate_prior_box_output(memory::ptr output_mem, stream& stream, layout c step_h = static_cast(img_height) / layer_height; } const float offset = argument.offset; - int num_priors = argument.is_clustered() ? - static_cast(argument.widths.size()) : + int64_t num_priors = argument.is_clustered() ? + static_cast(argument.widths.size()) : output_mem->get_layout().spatial(1) / 4 / layer_width / layer_height; int var_size = static_cast(argument.variance.size()); mem_lock lock{output_mem, stream}; auto out_ptr = lock.begin(); - int dim = layer_height * layer_width * num_priors * 4; + int64_t dim = layer_height * layer_width * num_priors * 4; - int idx = 0; - for (int h = 0; h < layer_height; ++h) { - for (int w = 0; w < layer_width; ++w) { + int64_t idx = 0; + for (int64_t h = 0; h < layer_height; ++h) { + for (int64_t w = 0; w < layer_width; ++w) { float center_x, center_y; if (argument.step_width == 0.f || argument.step_height == 0.f) { center_x = (w + 0.5f) * step_w; @@ -60,7 +60,7 @@ void calculate_prior_box_output(memory::ptr output_mem, stream& stream, layout c float box_width, box_height; if (argument.is_clustered()) { - for (int s = 0; s < num_priors; ++s) { + for (int64_t s = 0; s < num_priors; ++s) { box_width = argument.widths[s]; box_height = argument.heights[s]; idx = h * layer_width * num_priors * 4 + w * num_priors * 4 + s * 4; @@ -83,8 +83,8 @@ void calculate_prior_box_output(memory::ptr output_mem, stream& stream, layout c if (argument.fixed_ratio.size() > 0) { for (auto fr : argument.fixed_ratio) { - box_width = fixed_size * sqrt(fr); - box_height = fixed_size / sqrt(fr); + box_width = fixed_size * sqrtf(fr); + box_height = fixed_size / sqrtf(fr); for (size_t r = 0; r < density; ++r) { for (size_t c = 0; c < density; ++c) { @@ -124,8 +124,8 @@ void calculate_prior_box_output(memory::ptr output_mem, stream& stream, layout c continue; } - box_width = fixed_size * sqrt(ar); - box_height = fixed_size / sqrt(ar); + box_width = fixed_size * sqrtf(ar); + box_height = fixed_size / sqrtf(ar); for (size_t r = 0; r < density; ++r) { for (size_t c = 0; c < density; ++c) { @@ -161,7 +161,7 @@ void calculate_prior_box_output(memory::ptr output_mem, stream& stream, layout c if (argument.max_sizes.size() > 0) { float max_size_ = argument.max_sizes[s]; // second prior: aspect_ratio = 1, size = sqrt(min_size * max_size) - box_width = box_height = sqrt(min_size * max_size_); + box_width = box_height = sqrtf(min_size * max_size_); // xmin out_ptr[idx++] = (dtype)((center_x - box_width / 2.f) / img_width); // ymin @@ -180,8 +180,8 @@ void calculate_prior_box_output(memory::ptr output_mem, stream& stream, layout c if (fabs(ar - 1.) < 1e-6) { continue; } - box_width = min_size * sqrt(ar); - box_height = min_size / sqrt(ar); + box_width = min_size * sqrtf(ar); + box_height = min_size / sqrtf(ar); // xmin out_ptr[idx++] = (dtype)((center_x - box_width / 2.f) / img_width); // ymin @@ -204,7 +204,7 @@ void calculate_prior_box_output(memory::ptr output_mem, stream& stream, layout c } // set the variance. - int count = output_mem->get_layout().spatial(0) * output_mem->get_layout().spatial(1); + int64_t count = output_mem->get_layout().spatial(0) * output_mem->get_layout().spatial(1); int var_loop_count = argument.is_clustered() ? var_size : 4; for (int h = 0; h < layer_height; ++h) { for (int w = 0; w < layer_width; ++w) { @@ -228,9 +228,9 @@ std::string vector_to_string(const std::vector& vec) { std::vector normalized_aspect_ratio(const std::vector& aspect_ratio, bool flip) { std::set unique_ratios; for (auto ratio : aspect_ratio) { - unique_ratios.insert(std::round(ratio * 1e6) / 1e6); + unique_ratios.insert(static_cast(std::round(ratio * 1e6) / 1e6)); if (flip) - unique_ratios.insert(std::round(1 / ratio * 1e6) / 1e6); + unique_ratios.insert(static_cast(std::round(1 / ratio * 1e6) / 1e6)); } unique_ratios.insert(1); return std::vector(unique_ratios.begin(), unique_ratios.end()); diff --git a/src/plugins/intel_gpu/src/graph/program.cpp b/src/plugins/intel_gpu/src/graph/program.cpp index d783140eecc9..55a1a8e9fd38 100644 --- a/src/plugins/intel_gpu/src/graph/program.cpp +++ b/src/plugins/intel_gpu/src/graph/program.cpp @@ -8,8 +8,9 @@ #include "openvino/core/type.hpp" #include "openvino/runtime/system_conf.hpp" #include "openvino/runtime/threading/cpu_streams_info.hpp" -#include "openvino/util/weights_path.hpp" #include "openvino/util/file_util.hpp" +#include "openvino/util/parallel_read_streambuf.hpp" +#include "common_utils/parallel_mem_streambuf.hpp" #include "intel_gpu/runtime/memory.hpp" #include "intel_gpu/runtime/engine.hpp" @@ -18,6 +19,7 @@ #include "intel_gpu/runtime/compilation_context.hpp" #include "intel_gpu/graph/program.hpp" + #include "layout_optimizer.h" #include "pass_manager.h" #include "primitive_type.h" @@ -235,10 +237,13 @@ void program::init_program() { if (_task_executor == nullptr) _task_executor = program::make_task_executor(_config); - _kernels_cache = std::unique_ptr(new kernels_cache(_engine, _config, prog_id, _task_executor, - kernel_selector::KernelBase::get_db().get_batch_headers())); - _kernels_cache->set_kernels_reuse(_config.get_enable_kernels_reuse()); + if (_engine.runtime_type() != runtime_types::sycl) { + _kernels_cache = std::unique_ptr(new kernels_cache(_engine, _config, prog_id, _task_executor, + kernel_selector::KernelBase::get_db().get_batch_headers())); + + _kernels_cache->set_kernels_reuse(_config.get_enable_kernels_reuse()); + } if (!_compilation_context) _compilation_context = program::make_compilation_context(_config); @@ -268,6 +273,7 @@ void program::init_primitives() { } kernels_cache& program::get_kernels_cache() const { + OPENVINO_ASSERT(_engine.runtime_type() != runtime_types::sycl, "[GPU] Kernels cache is not available for SYCL runtime"); return *_kernels_cache; } @@ -794,9 +800,9 @@ const std::vector& program::get_allocating_order(bool forced_updat return po.get_processing_number(lhs.get()) < po.get_processing_number(rhs.get()); } - if (rhs_layout.is_dynamic()) + if (rhs_layout.is_dynamic() && !lhs_layout.is_dynamic()) return true; - if (lhs_layout.is_dynamic()) + if (lhs_layout.is_dynamic() && !rhs_layout.is_dynamic()) return false; if (lhs_layout.bytes_count() == rhs_layout.bytes_count()) { @@ -1837,6 +1843,10 @@ void program::cancel_compilation_context() { } void program::save(cldnn::BinaryOutputBuffer& ob) const { + if (_engine.runtime_type() == runtime_types::sycl) { + OPENVINO_THROW("[GPU] program::save is not supported for SYCL runtime"); + } + std::map> mutable_datas_ptrs; ob << nodes_map.size(); @@ -1944,7 +1954,7 @@ void program::save(cldnn::BinaryOutputBuffer& ob) const { } ob << allocating_order.size(); - for (auto const& node_id : allocating_order) { + for (const auto& node_id : allocating_order) { ob << node_id; } @@ -1953,11 +1963,20 @@ void program::save(cldnn::BinaryOutputBuffer& ob) const { ob << state_initializer.first; ob << state_initializer.second; } + + if (!ob.is_encrypted() && !ob.is_offset_page_aligned()) { + std::vector pad(ob.get_bytes_to_page_boundary(), 0); + ob << make_data(pad.data(), pad.size()); + } } void program::load(cldnn::BinaryInputBuffer& ib, std::shared_ptr model_ptr, std::shared_ptr cache_attr_map) { + if (_engine.runtime_type() == runtime_types::sycl) { + OPENVINO_THROW("[GPU] program::load is not supported for SYCL runtime"); + } + init_program(); std::shared_ptr weights_memory = nullptr; @@ -1970,16 +1989,42 @@ void program::load(cldnn::BinaryInputBuffer& ib, weights_memory = std::make_shared(model_ptr); } } else if (!weights_path.empty()) { - ov::util::validate_weights_path(weights_path); weights_memory = std::make_shared(ov::load_mmap_object(ov::util::make_path(weights_path))); } else { OPENVINO_THROW("Weights path or model is required for cache mode OPTIMIZE_SIZE"); } } + const bool can_use_mmap_zero_copy = ib.is_mmap_tensor_4K_aligned() && _engine.get_device_info().arch >= gpu_arch::xe2 && + _engine.get_device_info().dev_type == device_type::integrated_gpu && !_config.get_enable_weightless(); + memory_ptr model_tensor_base_ptr = nullptr; + if (can_use_mmap_zero_copy) { + model_tensor_base_ptr = + ib.get_engine().create_mmap_hostbuffer(ib.get_mmap_tensor(), + ib.get_stream_size(), + allocation_type::usm_host, + layout({{static_cast(ib.get_stream_size()), 1, 1, 1}, data_types::u8, format::bfyx})); + } + size_t num_nodes; ib >> num_nodes; bool is_valid_data_node; + + // Prefetch hook: if the backing streambuf is ParallelReadStreamBuf (or + // ParallelMemStreamBuf wrapping one for a file-backed mmap), ask it to + // collapse the upcoming thousands of small ib >> ... reads for data + // primitives into one bulk parallel pread. The cap keeps the up-front + // dispatch/allocation cost bounded; reads that fall outside the prefetched + // window transparently fall back to file I/O. + { + auto* rdbuf = ib.get_streambuf(); + if (auto* prs = dynamic_cast(rdbuf)) { + prs->prefetch(ov::util::default_parallel_io_prefetch_cap); + } else if (auto* pms = dynamic_cast(rdbuf)) { + pms->prefetch(ov::util::default_parallel_io_prefetch_cap); + } + } + for (size_t i = 0; i < num_nodes; ++i) { ib >> is_valid_data_node; if (!is_valid_data_node) @@ -1988,11 +2033,10 @@ void program::load(cldnn::BinaryInputBuffer& ib, std::shared_ptr prim; ib >> prim; if (auto data_prim = dynamic_cast(prim.get())) { - data_prim->load_weights(ib, weights_memory); + data_prim->load_weights(ib, weights_memory, model_tensor_base_ptr); } get_or_create(prim); } - size_t num_output_sharing_mutable_datas; ib >> num_output_sharing_mutable_datas; for (size_t i = 0; i < num_output_sharing_mutable_datas; ++i) { @@ -2007,6 +2051,18 @@ void program::load(cldnn::BinaryInputBuffer& ib, md_node2.replace_memory(md_node2.typed_desc()->mem); } + // Same prefetch hook for the post-load loop: node_post_load is dominated by + // ~15 small ib >> ... calls per node across thousands of nodes, which maps + // to thousands of single_read dispatches if left unbatched. + { + auto* rdbuf = ib.get_streambuf(); + if (auto* prs = dynamic_cast(rdbuf)) { + prs->prefetch(ov::util::default_parallel_io_prefetch_cap); + } else if (auto* pms = dynamic_cast(rdbuf)) { + pms->prefetch(ov::util::default_parallel_io_prefetch_cap); + } + } + for (size_t i = 0; i < num_nodes; ++i) { primitive_id prim_id; ib >> prim_id; @@ -2135,4 +2191,11 @@ void program::load(cldnn::BinaryInputBuffer& ib, ib >> initializers; state_initializers[variable_id] = initializers; } + + // At the end of load + if (!ib.is_encrypted() && !ib.is_offset_page_aligned()) { + std::vector pad(ib.get_bytes_to_page_boundary(), 0); + ib >> make_data(pad.data(), pad.size()); + } } + diff --git a/src/plugins/intel_gpu/src/graph/program_dump_graph.cpp b/src/plugins/intel_gpu/src/graph/program_dump_graph.cpp index a46f516783ed..8716045ddc61 100644 --- a/src/plugins/intel_gpu/src/graph/program_dump_graph.cpp +++ b/src/plugins/intel_gpu/src/graph/program_dump_graph.cpp @@ -219,7 +219,7 @@ void dump_graph_init(std::ofstream& graph, std::ostringstream oss; if (ptr->is_type()) { auto dyn_quan = ptr->as().get_primitive(); - oss << "\n" << "group_sizes: " << ov::util::join(cldnn::convert_vector(dyn_quan->attrs.group_sizes)); + oss << "\n" << "group_sizes: " << ov::util::join(cldnn::convert_vector(dyn_quan->attrs.group_sizes)); if (dyn_quan->attrs.precomputed_reduction) { oss << "\n" << "precomputed_reduction_dt: " << dyn_quan->attrs.precomputed_reduction_dt; } @@ -288,7 +288,7 @@ void dump_graph_init(std::ofstream& graph, bool doubled = true; auto it = user->get_dependencies().begin(); while (it != user->get_dependencies().end()) { - int input_port = it - user->get_dependencies().begin(); + int input_port = static_cast(it - user->get_dependencies().begin()); if (it->first == node && marked_connection.find({node, input_port}) == marked_connection.end()) { marked_connection.emplace(user, input_port); break; diff --git a/src/plugins/intel_gpu/src/graph/program_helpers.cpp b/src/plugins/intel_gpu/src/graph/program_helpers.cpp index ea4d08e62177..753f9d0c8e84 100644 --- a/src/plugins/intel_gpu/src/graph/program_helpers.cpp +++ b/src/plugins/intel_gpu/src/graph/program_helpers.cpp @@ -149,7 +149,7 @@ add_fusing_type onednn_add_fusing_helpers::get_add_fusing_type( && !dep_node.is_constant() && !p_node.is_type() && !p_node.is_output() - && !(dep_node.get_program().is_body_program() && dep_node.is_type())) { + && !(dep_node.is_type())) { return add_fusing_type::sum; } else if (p_layout.get_tensor() == d_layout.get_tensor()) { return add_fusing_type::binary_per_tensor; diff --git a/src/plugins/intel_gpu/src/graph/program_node.cpp b/src/plugins/intel_gpu/src/graph/program_node.cpp index 8661757bdda3..6b23a623c179 100644 --- a/src/plugins/intel_gpu/src/graph/program_node.cpp +++ b/src/plugins/intel_gpu/src/graph/program_node.cpp @@ -1224,8 +1224,8 @@ dnnl::post_ops program_node::try_optimize_post_ops(std::vector>>>>>>>>>>>>" << std::endl; // Get post-ops size for current node - int64_t post_ops_size = cur_post_ops.size(); + int post_ops_size = static_cast(cur_post_ops.size()); auto get_optimized_eltwise_type = [](onednn_post_op_type type) { switch (type) { diff --git a/src/plugins/intel_gpu/src/graph/reduce.cpp b/src/plugins/intel_gpu/src/graph/reduce.cpp index 6566c9366c4c..9cc8d227dae0 100644 --- a/src/plugins/intel_gpu/src/graph/reduce.cpp +++ b/src/plugins/intel_gpu/src/graph/reduce.cpp @@ -27,7 +27,7 @@ static std::vector convert_axes(std::vector axes, size_t rank std::vector converted_axes; for (auto axis : axes) { if (axis == 0 || axis == 1) { - converted_axes.push_back(axis); + converted_axes.push_back(static_cast(axis)); continue; } diff --git a/src/plugins/intel_gpu/src/graph/registry/eltwise_impls.cpp b/src/plugins/intel_gpu/src/graph/registry/eltwise_impls.cpp index 5777230ca8a8..5fb2f1ab9b93 100644 --- a/src/plugins/intel_gpu/src/graph/registry/eltwise_impls.cpp +++ b/src/plugins/intel_gpu/src/graph/registry/eltwise_impls.cpp @@ -7,12 +7,17 @@ #include "intel_gpu/primitives/eltwise.hpp" #include "primitive_inst.h" +#ifdef OV_GPU_WITH_SYCL_RT + #include "impls/sycl/eltwise.hpp" +#endif + namespace ov::intel_gpu { using namespace cldnn; const std::vector>& Registry::get_implementations() { static const std::vector> impls = { + OV_GPU_CREATE_INSTANCE_SYCL(cldnn::sycl::EltwiseImplementationManager, shape_types::static_shape, not_in_shape_flow()) OV_GPU_GET_INSTANCE_OCL(eltwise, shape_types::static_shape, not_in_shape_flow()) OV_GPU_GET_INSTANCE_OCL(eltwise, shape_types::dynamic_shape, not_in_shape_flow()) OV_GPU_GET_INSTANCE_CPU(eltwise, shape_types::static_shape, in_shape_flow()) diff --git a/src/plugins/intel_gpu/src/graph/registry/gather_matmul_impls.cpp b/src/plugins/intel_gpu/src/graph/registry/gather_matmul_impls.cpp new file mode 100644 index 000000000000..1cbeb1d7437b --- /dev/null +++ b/src/plugins/intel_gpu/src/graph/registry/gather_matmul_impls.cpp @@ -0,0 +1,25 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "intel_gpu/primitives/gather_matmul.hpp" +#include "primitive_inst.h" +#include "registry.hpp" + +#if OV_GPU_WITH_OCL +# include "impls/ocl_v2/gather_matmul/gather_matmul_impl.hpp" +#endif + +namespace ov::intel_gpu { + +using namespace cldnn; + +const std::vector>& Registry::get_implementations() { + static const std::vector> impls = { + OV_GPU_CREATE_INSTANCE_OCL(ocl::GatherMatmulImpl, shape_types::static_shape) + OV_GPU_CREATE_INSTANCE_OCL(ocl::GatherMatmulImpl, shape_types::dynamic_shape)}; + + return impls; +} + +} // namespace ov::intel_gpu diff --git a/src/plugins/intel_gpu/src/graph/registry/moe_router_fused_impls.cpp b/src/plugins/intel_gpu/src/graph/registry/moe_router_fused_impls.cpp new file mode 100644 index 000000000000..84d4352998f0 --- /dev/null +++ b/src/plugins/intel_gpu/src/graph/registry/moe_router_fused_impls.cpp @@ -0,0 +1,23 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "intel_gpu/primitives/moe_router_fused.hpp" +#include "primitive_inst.h" +#include "registry.hpp" + +#if OV_GPU_WITH_OCL +# include "impls/ocl_v2/moe/moe_router_fused_opt.hpp" +#endif + +namespace ov::intel_gpu { + +using namespace cldnn; + +const std::vector>& Registry::get_implementations() { + static const std::vector> impls = {OV_GPU_CREATE_INSTANCE_OCL(ocl::MoeRouterFusedOpt, shape_types::any)}; + + return impls; +} + +} // namespace ov::intel_gpu diff --git a/src/plugins/intel_gpu/src/graph/registry/paged_causal_conv1d_impls.cpp b/src/plugins/intel_gpu/src/graph/registry/paged_causal_conv1d_impls.cpp new file mode 100644 index 000000000000..9fe084f6f146 --- /dev/null +++ b/src/plugins/intel_gpu/src/graph/registry/paged_causal_conv1d_impls.cpp @@ -0,0 +1,27 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "intel_gpu/primitives/paged_causal_conv1d.hpp" +#include "primitive_inst.h" +#include "registry.hpp" + +#if OV_GPU_WITH_OCL +# include "impls/ocl_v2/paged_causal_conv1d_ref.hpp" +#endif + +namespace ov::intel_gpu { + +using namespace cldnn; + +const std::vector>& Registry::get_implementations() { + static const std::vector> impls = { +#if OV_GPU_WITH_OCL + OV_GPU_CREATE_INSTANCE_OCL(ocl::PagedCausalConv1DRef, shape_types::any) +#endif + }; + + return impls; +} + +} // namespace ov::intel_gpu diff --git a/src/plugins/intel_gpu/src/graph/registry/paged_gated_delta_net_impls.cpp b/src/plugins/intel_gpu/src/graph/registry/paged_gated_delta_net_impls.cpp new file mode 100644 index 000000000000..4f6b4963cbaf --- /dev/null +++ b/src/plugins/intel_gpu/src/graph/registry/paged_gated_delta_net_impls.cpp @@ -0,0 +1,28 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "intel_gpu/primitives/paged_gated_delta_net.hpp" +#include "primitive_inst.h" +#include "registry.hpp" + +#if OV_GPU_WITH_OCL +# include "impls/ocl_v2/paged_gated_delta_net.hpp" +#endif + +namespace ov::intel_gpu { + +using namespace cldnn; + +const std::vector>& Registry::get_implementations() { + static const std::vector> impls = { +#if OV_GPU_WITH_OCL + OV_GPU_CREATE_INSTANCE_OCL(ocl::PagedGatedDeltaNetOpt, shape_types::any) + OV_GPU_CREATE_INSTANCE_OCL(ocl::PagedGatedDeltaNetRef, shape_types::any) +#endif + }; + + return impls; +} + +} // namespace ov::intel_gpu diff --git a/src/plugins/intel_gpu/src/graph/registry/pooling_impls.cpp b/src/plugins/intel_gpu/src/graph/registry/pooling_impls.cpp index 1ad2b7218b68..27bb3f37818d 100644 --- a/src/plugins/intel_gpu/src/graph/registry/pooling_impls.cpp +++ b/src/plugins/intel_gpu/src/graph/registry/pooling_impls.cpp @@ -4,6 +4,7 @@ #include "registry.hpp" #include "intel_gpu/primitives/pooling.hpp" +#include "intel_gpu/runtime/device_info.hpp" #include "primitive_inst.h" #if OV_GPU_WITH_ONEDNN diff --git a/src/plugins/intel_gpu/src/graph/registry/registry.hpp b/src/plugins/intel_gpu/src/graph/registry/registry.hpp index e1bce52f9079..6cd5dc1c3442 100644 --- a/src/plugins/intel_gpu/src/graph/registry/registry.hpp +++ b/src/plugins/intel_gpu/src/graph/registry/registry.hpp @@ -18,11 +18,18 @@ #define OV_GPU_WITH_SYCL 0 #endif +#ifdef OV_GPU_WITH_SYCL_RT +#define OV_GPU_WITH_OCL 0 +#else #define OV_GPU_WITH_OCL 1 +#endif #define OV_GPU_WITH_COMMON 1 #define OV_GPU_WITH_CPU 1 #define OV_GPU_WITH_CM 1 +#ifdef EXPAND +#undef EXPAND +#endif #define COUNT_N(_1, _2, _3, _4, _5, N, ...) N #define COUNT(...) EXPAND(COUNT_N(__VA_ARGS__, 5, 4, 3, 2, 1)) #define CAT(a, b) a ## b @@ -60,7 +67,7 @@ # define OV_GPU_CREATE_INSTANCE_ONEDNN(...) #endif -#if OV_GPU_WITH_SYCL +#ifdef OV_GPU_WITH_SYCL_RT # define OV_GPU_CREATE_INSTANCE_SYCL(...) EXPAND(CREATE_INSTANCE(__VA_ARGS__)) #else # define OV_GPU_CREATE_INSTANCE_SYCL(...) @@ -151,7 +158,9 @@ REGISTER_IMPLS(lstm_seq); REGISTER_IMPLS(gru_seq); REGISTER_IMPLS(non_max_suppression); REGISTER_IMPLS(paged_attention); +REGISTER_IMPLS(paged_gated_delta_net); REGISTER_IMPLS(pa_kv_reorder); +REGISTER_IMPLS(paged_causal_conv1d); REGISTER_IMPLS(pooling); REGISTER_IMPLS(reduce); REGISTER_IMPLS(reorder); @@ -172,11 +181,13 @@ REGISTER_IMPLS(tile); REGISTER_IMPLS(col2im); REGISTER_IMPLS(vl_sdpa); REGISTER_IMPLS(moe_3gemm_fused_compressed); +REGISTER_IMPLS(moe_router_fused); REGISTER_IMPLS(moe_mask_gen); REGISTER_IMPLS(moe_mask_gen_reshape); REGISTER_IMPLS(moe_gemm); REGISTER_IMPLS(moe_scatter_reduction); REGISTER_IMPLS(moe_gather); +REGISTER_IMPLS(gather_matmul); REGISTER_DEFAULT_IMPLS(assign, CPU_S, CPU_D); REGISTER_DEFAULT_IMPLS(read_value, CPU_S, CPU_D); diff --git a/src/plugins/intel_gpu/src/graph/registry/reorder_impls.cpp b/src/plugins/intel_gpu/src/graph/registry/reorder_impls.cpp index 65df220dc9c9..c348510d6a34 100644 --- a/src/plugins/intel_gpu/src/graph/registry/reorder_impls.cpp +++ b/src/plugins/intel_gpu/src/graph/registry/reorder_impls.cpp @@ -19,11 +19,23 @@ namespace ov::intel_gpu { using namespace cldnn; +// Formats listed here gate the dynamic-shape OCL reorder path: both in and out +// layouts must be in this set for the dynamic ImplementationManager to be offered. +// Each entry requires a dynamic-capable kernel (Ref covers all; bfyx_to_blocked_format +// handles bfyx/bfzyx -> blocked; fsv handles blocked <-> blocked). Entries below +// match what reorder_gpu_optimization.dynamic_* tests exercise; extend only when a +// production network needs a new dynamic reorder path + a matching optimized kernel. static std::vector supported_dyn_formats = { format::bfyx, format::bfzyx, format::bfwzyx, - format::b_fs_yx_fsv16 + format::b_fs_yx_fsv4, + format::b_fs_yx_fsv16, + format::b_fs_yx_fsv32, + format::b_fs_zyx_fsv16, + format::b_fs_zyx_fsv32, + format::bs_fs_yx_bsv16_fsv16, + format::bs_fs_zyx_bsv16_fsv32, }; const std::vector>& Registry::get_implementations() { @@ -36,8 +48,10 @@ const std::vector>& Registryoutput_shape.sizes(); auto input_sizes = input_layout.get_tensor().sizes(); size_t need_recalc = 0; - uint32_t shape_count = 1; + int64_t shape_count = 1; for (size_t i = 0; i < sizes.size(); i++) { if (sizes[i] == -1) { @@ -137,7 +137,7 @@ layout reshape_inst::calc_output_layout(reshape_node const& node, kernel_impl_pa shape_count *= sizes[i]; } if (need_recalc) - sizes[need_recalc] = static_cast(input_layout.count()) / shape_count; + sizes[need_recalc] = static_cast(input_layout.count()) / shape_count; return layout{input_layout.data_type, input_layout.format, tensor(sizes)}; } @@ -334,9 +334,8 @@ void reshape_inst::update_output_memory() { return; build_deps(); // reshape need deps - if (get_node().get_program().is_new_shape_infer() && input_memory_ptr() == nullptr) + if (input_memory_ptr() == nullptr) return; - OPENVINO_ASSERT(input_memory_ptr() != nullptr, "[GPU] Failed to reuse input in ", id(), " primitive: input memory was not allocated"); // Can_be_optimized nodes are allocating from memory_pool too. In this case, // we need release the legacy output memory from memory pool explicitly. diff --git a/src/plugins/intel_gpu/src/graph/roi_pooling.cpp b/src/plugins/intel_gpu/src/graph/roi_pooling.cpp index dc1e49270c83..8e201ddeb14f 100644 --- a/src/plugins/intel_gpu/src/graph/roi_pooling.cpp +++ b/src/plugins/intel_gpu/src/graph/roi_pooling.cpp @@ -19,8 +19,8 @@ layout roi_pooling_inst::calc_output_layout(roi_pooling_node const& node, kernel auto desc = impl_param.typed_desc(); layout data_layout = impl_param.get_input_layout(0); layout rois_layout = impl_param.get_input_layout(1); - int num_rois = rois_layout.batch(); - int out_fm = desc->position_sensitive ? desc->output_dim : data_layout.feature(); + int64_t num_rois = rois_layout.batch(); + int64_t out_fm = desc->position_sensitive ? desc->output_dim : data_layout.feature(); return layout(data_layout.data_type, data_layout.format, diff --git a/src/plugins/intel_gpu/src/graph/scaled_dot_product_attention.cpp b/src/plugins/intel_gpu/src/graph/scaled_dot_product_attention.cpp index cafbfdcc0e29..828ddff651bf 100644 --- a/src/plugins/intel_gpu/src/graph/scaled_dot_product_attention.cpp +++ b/src/plugins/intel_gpu/src/graph/scaled_dot_product_attention.cpp @@ -19,13 +19,13 @@ GPU_DEFINE_PRIMITIVE_TYPE_ID(scaled_dot_product_attention) layout scaled_dot_product_attention_inst::calc_output_layout(scaled_dot_product_attention_node const& /* node */, kernel_impl_params const& impl_param) { auto desc = impl_param.typed_desc(); - - auto transpose_shape = [&desc](const ov::PartialShape& shape, const std::vector& order) { - if (desc->input_q_transpose_order.empty()) + auto transpose_shape = [](const ov::PartialShape& shape, const std::vector& order) { + if (order.empty()) return shape; auto shape_transposed = ov::PartialShape(shape); auto rank_diff = shape.size() - order.size(); + for (size_t i = 0; i < order.size(); i++) { size_t idx = static_cast(order[i]); shape_transposed[i + rank_diff] = shape[idx + rank_diff]; @@ -35,10 +35,17 @@ layout scaled_dot_product_attention_inst::calc_output_layout(scaled_dot_product_ }; auto input0_layout = impl_param.get_input_layout(0); + auto input2_layout = impl_param.get_input_layout(2); + auto default_out_dt = data_type_traits::is_floating_point(input0_layout.data_type) ? input0_layout.data_type : data_types::f32; auto output_type = desc->output_data_types[0].value_or(default_out_dt); auto output_format = input0_layout.format; - auto output_shape = transpose_shape(input0_layout.get_partial_shape(), desc->input_q_transpose_order); // output shape matches with Q input shape + auto q_shape = transpose_shape(input0_layout.get_partial_shape(), + desc->input_q_transpose_order); + auto output_shape = q_shape; + auto v_shape = transpose_shape(input2_layout.get_partial_shape(), + desc->input_v_transpose_order); + output_shape[output_shape.size() - 1] = v_shape[v_shape.size() - 1]; return { layout{output_shape, output_type, output_format, desc->output_paddings[0]} }; } @@ -63,6 +70,16 @@ std::vector scaled_dot_product_attention_inst::calc_output_layouts(scale input_shapes.push_back(impl_param.get_input_layout(i).get()); } + // For INT4 KV-cache, restore logical head size from query for shape inference + if (prim->is_kv_compressed) { + const auto kv_cache_dt = impl_param.get_program().get_config().get_kv_cache_precision(); + if (ov::element::Type(kv_cache_dt).bitwidth() == 4 && input_shapes.size() >= 3) { + auto q_last = input_shapes[0][input_shapes[0].size() - 1]; + input_shapes[1][input_shapes[1].size() - 1] = q_last; + input_shapes[2][input_shapes[2].size() - 1] = q_last; + } + } + std::vector output_shapes = ov::intel_gpu::op::shape_infer(&op, input_shapes, prim->input_q_transpose_order, diff --git a/src/plugins/intel_gpu/src/graph/scatter_elements_update.cpp b/src/plugins/intel_gpu/src/graph/scatter_elements_update.cpp index 8f580840f692..75d10fd79eb3 100644 --- a/src/plugins/intel_gpu/src/graph/scatter_elements_update.cpp +++ b/src/plugins/intel_gpu/src/graph/scatter_elements_update.cpp @@ -17,8 +17,8 @@ GPU_DEFINE_PRIMITIVE_TYPE_ID(scatter_elements_update) layout scatter_elements_update_inst::calc_output_layout(scatter_elements_update_node const& node, kernel_impl_params const& impl_param) { auto desc = impl_param.typed_desc(); - const int32_t axis = desc->axis; - const size_t input_number_of_dims = impl_param.get_input_layout().get_partial_shape().size(); + const int64_t axis = desc->axis; + const int64_t input_number_of_dims = static_cast(impl_param.get_input_layout().get_partial_shape().size()); auto input_layout = impl_param.get_input_layout(); @@ -30,7 +30,7 @@ layout scatter_elements_update_inst::calc_output_layout(scatter_elements_update_ output_type = impl_param.get_output_element_type(); } - if (static_cast(axis) < 0 || static_cast(axis) >= input_number_of_dims) + if (axis < 0 || axis >= input_number_of_dims) CLDNN_ERROR_MESSAGE(desc->id, "Incorrect axis value for ScatterElementsUpdate: Axis must be positive and less than the input tensor dimension."); return layout{output_shape, output_type, input_format}; @@ -71,6 +71,9 @@ void scatter_elements_update_inst::update_output_memory() { build_deps(); + if (input_memory_ptr() == nullptr) + return; + // Can_be_optimized nodes are allocating from memory_pool too. In this case, // we need release the legacy output memory from memory pool explicitly. if (static_cast(_outputs[0]) && diff --git a/src/plugins/intel_gpu/src/graph/scatter_nd_update.cpp b/src/plugins/intel_gpu/src/graph/scatter_nd_update.cpp index 6911b4d729a7..87229bab29e0 100644 --- a/src/plugins/intel_gpu/src/graph/scatter_nd_update.cpp +++ b/src/plugins/intel_gpu/src/graph/scatter_nd_update.cpp @@ -83,6 +83,9 @@ void scatter_nd_update_inst::update_output_memory() { build_deps(); + if (input_memory_ptr() == nullptr) + return; + // Can_be_optimized nodes are allocating from memory_pool too. In this case, // we need release the legacy output memory from memory pool explicitly. if (static_cast(_outputs[0]) && diff --git a/src/plugins/intel_gpu/src/graph/scatter_update.cpp b/src/plugins/intel_gpu/src/graph/scatter_update.cpp index 7e254df2597b..6948ab827f72 100644 --- a/src/plugins/intel_gpu/src/graph/scatter_update.cpp +++ b/src/plugins/intel_gpu/src/graph/scatter_update.cpp @@ -63,6 +63,9 @@ void scatter_update_inst::update_output_memory() { if (_node != nullptr) build_deps(); + if (input_memory_ptr() == nullptr) + return; + // Can_be_optimized nodes are allocating from memory_pool too. In this case, // we need release the legacy output memory from memory pool explicitly. if (static_cast(_outputs[0]) && diff --git a/src/plugins/intel_gpu/src/graph/slice_scatter.cpp b/src/plugins/intel_gpu/src/graph/slice_scatter.cpp index 7bb0d7d134b8..6362c7966ad2 100644 --- a/src/plugins/intel_gpu/src/graph/slice_scatter.cpp +++ b/src/plugins/intel_gpu/src/graph/slice_scatter.cpp @@ -67,10 +67,13 @@ void slice_scatter_inst::update_output_memory() { if (!can_be_optimized() || _impl_params->is_dynamic()) return; - if (_outputs.size() > 0 && static_cast(_outputs[0]) && _network.get_engine().is_the_same_buffer(output_memory(), input_memory())) + build_deps(); + + if (input_memory_ptr() == nullptr) return; - build_deps(); + if (_outputs.size() > 0 && static_cast(_outputs[0]) && _network.get_engine().is_the_same_buffer(output_memory(), input_memory())) + return; if (static_cast(_outputs[0]) && get_node().get_program().get_config().get_enable_memory_pool()) { _network.get_memory_pool().release_memory(_outputs[0].get(), get_node().get_unique_id(), get_node().id(), _network.get_id()); diff --git a/src/plugins/intel_gpu/src/graph/space_to_batch.cpp b/src/plugins/intel_gpu/src/graph/space_to_batch.cpp index a7b226bd6d84..e09d4801beb5 100644 --- a/src/plugins/intel_gpu/src/graph/space_to_batch.cpp +++ b/src/plugins/intel_gpu/src/graph/space_to_batch.cpp @@ -63,7 +63,7 @@ layout space_to_batch_inst::calc_output_layout(space_to_batch_node const& node, static std::vector tensor_to_vec(const tensor& t, const format f) { std::vector vec(cldnn::format::dimension(f)); for (size_t i = 0; i < vec.size(); ++i) { - vec[i] = t.sizes()[i]; + vec[i] = static_cast(t.sizes()[i]); } std::reverse(vec.begin() + 2, vec.end()); return vec; diff --git a/src/plugins/intel_gpu/src/graph/strided_slice.cpp b/src/plugins/intel_gpu/src/graph/strided_slice.cpp index 9af41e66d493..0a8ff4ed9e63 100644 --- a/src/plugins/intel_gpu/src/graph/strided_slice.cpp +++ b/src/plugins/intel_gpu/src/graph/strided_slice.cpp @@ -196,14 +196,14 @@ void strided_slice_inst::update_output_memory() { if (!can_be_optimized()) return; - if (get_node().get_program().is_new_shape_infer() && input_memory_ptr() == nullptr) + build_deps(); + + if (input_memory_ptr() == nullptr) return; if (static_cast(_outputs[0]) && _network.get_engine().is_the_same_buffer(output_memory(), input_memory())) return; - build_deps(); - GPU_DEBUG_TRACE_DETAIL << id() << " : update_output_memory with mem of input " << get_node().get_dependency(0).id() << " : " << input_memory_ptr()->buffer_ptr() << std::endl; // Can_be_optimized nodes are allocating from memory_pool too. In this case, diff --git a/src/plugins/intel_gpu/src/graph/swiglu.cpp b/src/plugins/intel_gpu/src/graph/swiglu.cpp index eb47de2e55be..ea4065099f6d 100644 --- a/src/plugins/intel_gpu/src/graph/swiglu.cpp +++ b/src/plugins/intel_gpu/src/graph/swiglu.cpp @@ -65,6 +65,7 @@ std::string swiglu_inst::to_string(swiglu_node const& node) { swiglu_info.add("gate_idx", desc->gate_idx); swiglu_info.add("swish_beta", desc->swish_beta); swiglu_info.add("up_add_val", desc->up_add_val); + swiglu_info.add("scale_factor", desc->scale_factor); if (desc->clamp_max != std::numeric_limits::max()) { swiglu_info.add("clamp_max", desc->clamp_max); } diff --git a/src/plugins/intel_gpu/src/kernel_selector/CMakeLists.txt b/src/plugins/intel_gpu/src/kernel_selector/CMakeLists.txt index 30aac28ede0b..3078ac02a441 100644 --- a/src/plugins/intel_gpu/src/kernel_selector/CMakeLists.txt +++ b/src/plugins/intel_gpu/src/kernel_selector/CMakeLists.txt @@ -91,6 +91,12 @@ endif() ov_build_target_faster(${TARGET_NAME} PCH) +if(GPU_RT_TYPE STREQUAL "SYCL") + # Some sources include oneDNN headers that require SYCL backend support. + # Without the SYCL compilation flag, the compiler will fail with "Unsupported compiler" error. + add_sycl_to_target(TARGET ${TARGET_NAME} SOURCES) +endif() + add_custom_command( TARGET ${TARGET_NAME} POST_BUILD COMMAND "${CMAKE_COMMAND}" -E copy_if_different ${CMAKE_CURRENT_SOURCE_DIR}/cache/cache.json ${TUNING_CACHE_PATH}/cache.json) diff --git a/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/arg_max_min_axis.cl b/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/arg_max_min_axis.cl index 548cf5707c55..f7d2ec05b452 100644 --- a/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/arg_max_min_axis.cl +++ b/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/arg_max_min_axis.cl @@ -383,7 +383,7 @@ KERNEL(arg_max_min_modified)( const uint counter_offset = counter_size * OPERATION_NUM; __global uint* merge_counter = OFFSET_GLOBAL_PTR(uint, tmp_buffer1, output_idx * counter_size); __global uint* max_merge_counter = OFFSET_GLOBAL_PTR(uint, tmp_buffer1, counter_offset + output_idx * counter_size); - __global bool* subgroup_done = OFFSET_GLOBAL_PTR(bool, tmp_buffer2, output_idx); + __global bool* subgroup_done = OFFSET_GLOBAL_PTR(bool, tmp_buffer2, output_idx * group_num); #else uint merge_counter[group_num]; uint max_merge_counter[group_num]; diff --git a/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/broadcast_gpu_ref.cl b/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/broadcast_gpu_ref.cl index d05754f6a443..19a6db73fdab 100644 --- a/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/broadcast_gpu_ref.cl +++ b/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/broadcast_gpu_ref.cl @@ -58,7 +58,7 @@ inline uint FUNC(get_idx_pos)(OPTIONAL_SHAPE_INFO_ARG uint out_b, uint out_f, ui (pad_before_y + idx_y) * y_pitch + (pad_before_x + idx_x) * x_pitch; #else // defined(INPUT0_LAYOUT_BFWZYX) && defined(OUTPUT_LAYOUT_BFWZYX) - uint8 input_indices; + uint input_indices[6]; input_indices[0] = INPUT0_BATCH_NUM; input_indices[1] = INPUT0_FEATURE_NUM; @@ -139,7 +139,7 @@ inline uint FUNC(get_idx_pos)(OPTIONAL_SHAPE_INFO_ARG uint out_b, uint out_f, ui (pad_before_x + idx_x) * x_pitch; #else // defined(INPUT0_LAYOUT_BFZYX) && defined(OUTPUT_LAYOUT_BFZYX) - uint8 input_indices; + uint input_indices[5]; input_indices[0] = INPUT0_BATCH_NUM; input_indices[1] = INPUT0_FEATURE_NUM; @@ -211,7 +211,7 @@ inline uint FUNC(get_idx_pos)(OPTIONAL_SHAPE_INFO_ARG uint out_b, uint out_f, ui #else // defined(INPUT0_LAYOUT_BFYX) && defined(OUTPUT_LAYOUT_BFYX) - uint4 input_indices; + uint input_indices[4]; input_indices[0] = INPUT0_BATCH_NUM; input_indices[1] = INPUT0_FEATURE_NUM; diff --git a/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/convolution_gpu_b_fs_yx_fsv4_1x1.cl b/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/convolution_gpu_b_fs_yx_fsv4_1x1.cl index 307ba840c5e2..58d3c4b8ca12 100644 --- a/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/convolution_gpu_b_fs_yx_fsv4_1x1.cl +++ b/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/convolution_gpu_b_fs_yx_fsv4_1x1.cl @@ -130,7 +130,7 @@ KERNEL(convolution)( weights_offset += WEIGHTS_IS_PITCH / FSV * LWG_DEPTH; unroll_for (uint out_fi = 0; out_fi < FEATURES_PER_WI; ++out_fi) { - int wei_i = _sub_group_shuffle(wei_sg[out_fi / SIMD], out_fi % SIMD); + volatile int wei_i = _sub_group_shuffle(wei_sg[out_fi / SIMD], out_fi % SIMD); FILTER_TYPE4 wei_val = AS_FILTER_TYPE4(wei_i); dotProd[out_fi] = IMAD(dotProd[out_fi], in_val, wei_val); diff --git a/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/convolution_gpu_b_fs_zyx_fsv16_imad.cl b/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/convolution_gpu_b_fs_zyx_fsv16_imad.cl index 1fa85ee73983..18fa33fb2a64 100644 --- a/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/convolution_gpu_b_fs_zyx_fsv16_imad.cl +++ b/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/convolution_gpu_b_fs_zyx_fsv16_imad.cl @@ -157,7 +157,16 @@ KERNEL(convolution_gpu_b_fs_zyx_fsv16_imad)( #ifdef SHOULD_USE_DATA_ZP #if ((FILTER_GROUPS_NUM > 1) && (FILTER_IFM_NUM % FSV != 0)) - data_zp_val = as_uint4(vload16(0, activations_zp + data_zp_idx)); + if (in_f_start + (k + 1) * FSV <= FILTER_IFM_NUM) { + data_zp_val = as_uint4(vload16(0, activations_zp + data_zp_idx)); + } else { + data_zp_val = (uint4)(0); + INPUT0_TYPE* data_zp_arr = (INPUT0_TYPE*)&data_zp_val; + __attribute__((opencl_unroll_hint(FILTER_IFM_NUM % FSV))) + for (uint f = 0; f < FILTER_IFM_NUM % FSV; f++) { + data_zp_arr[f] = activations_zp[data_zp_idx + f]; + } + } #else data_zp_val = vload4(0, (__global uint *)(activations_zp + data_zp_idx)); #endif @@ -206,6 +215,15 @@ KERNEL(convolution_gpu_b_fs_zyx_fsv16_imad)( } else { #endif input_val[izb][iyb][ixb] = vload4(0, (__global uint *)(conv_input + input_idx + get_sub_group_local_id() * FSV)); + #if ((FILTER_GROUPS_NUM > 1) && (FILTER_IFM_NUM % FSV != 0)) + if (in_f_start + (k + 1) * FSV >= ALIGN(FILTER_IFM_NUM, FSV)) { + INPUT0_TYPE* inp_p = (INPUT0_TYPE*)&input_val[izb][iyb][ixb]; + __attribute__((opencl_unroll_hint(FSV))) + for (uint f = FILTER_IFM_NUM % FSV; f < FSV; f++) { + inp_p[f] = 0; + } + } + #endif #ifdef SHOULD_USE_DATA_ZP } #endif @@ -215,8 +233,35 @@ KERNEL(convolution_gpu_b_fs_zyx_fsv16_imad)( #ifdef SHOULD_USE_DATA_ZP INPUT0_TYPE* input_zp_int8_arr = (INPUT0_TYPE*) &data_zp_val; #endif - __attribute__((opencl_unroll_hint(FSV))) - for (uint v = 0; v < FSV; v++) { + const bool is_tail_fblock = (in_f_start + (k + 1) * FSV >= ALIGN(FILTER_IFM_NUM, FSV)); + if (is_tail_fblock) { + __attribute__((opencl_unroll_hint(FSV))) + for (uint v = 0; v < FSV; v++) { + if (v < FILTER_IFM_NUM % FSV) { + #ifdef SHOULD_USE_DATA_ZP + if (input_on_padding) { + input_int8_arr[v] = input_zp_int8_arr[v]; + } else { + #endif + if (v + in_f_offset < FSV) { + input_int8_arr[v] = conv_input[input_idx + get_sub_group_local_id() * FSV + v]; + } else { + const uint addr = input_idx + get_sub_group_local_id() * FSV + v + + ((INPUT0_SIZE_X + INPUT0_PAD_BEFORE_SIZE_X + INPUT0_PAD_AFTER_SIZE_X) * + (INPUT0_SIZE_Y + INPUT0_PAD_BEFORE_SIZE_Y + INPUT0_PAD_AFTER_SIZE_Y) * + (INPUT0_SIZE_Z + INPUT0_PAD_BEFORE_SIZE_Z + INPUT0_PAD_AFTER_SIZE_Z) - 1) * FSV; + input_int8_arr[v] = conv_input[addr]; + } + #ifdef SHOULD_USE_DATA_ZP + } + #endif + } else { + input_int8_arr[v] = 0; + } + } + } else { + __attribute__((opencl_unroll_hint(FSV))) + for (uint v = 0; v < FSV; v++) { #ifdef SHOULD_USE_DATA_ZP if (input_on_padding) { input_int8_arr[v] = input_zp_int8_arr[v]; @@ -234,6 +279,7 @@ KERNEL(convolution_gpu_b_fs_zyx_fsv16_imad)( #ifdef SHOULD_USE_DATA_ZP } #endif + } } } #endif @@ -254,6 +300,15 @@ KERNEL(convolution_gpu_b_fs_zyx_fsv16_imad)( } else { #endif input_val[izb][iyb][ixb] = vload4(0, (__global uint *)(conv_input + input_idx + tmp * FSV)); + #if ((FILTER_GROUPS_NUM > 1) && (FILTER_IFM_NUM % FSV != 0)) + if (in_f_start + (k + 1) * FSV >= ALIGN(FILTER_IFM_NUM, FSV)) { + INPUT0_TYPE* inp_p = (INPUT0_TYPE*)&input_val[izb][iyb][ixb]; + __attribute__((opencl_unroll_hint(FSV))) + for (uint f = FILTER_IFM_NUM % FSV; f < FSV; f++) { + inp_p[f] = 0; + } + } + #endif #ifdef SHOULD_USE_DATA_ZP } #endif @@ -263,8 +318,35 @@ KERNEL(convolution_gpu_b_fs_zyx_fsv16_imad)( #ifdef SHOULD_USE_DATA_ZP INPUT0_TYPE* input_zp_int8_arr = (INPUT0_TYPE*) &data_zp_val; #endif - __attribute__((opencl_unroll_hint(FSV))) - for (uint v = 0; v < FSV; v++) { + const bool is_tail_fblock = (in_f_start + (k + 1) * FSV >= ALIGN(FILTER_IFM_NUM, FSV)); + if (is_tail_fblock) { + __attribute__((opencl_unroll_hint(FSV))) + for (uint v = 0; v < FSV; v++) { + if (v < FILTER_IFM_NUM % FSV) { + #ifdef SHOULD_USE_DATA_ZP + if (input_on_padding) { + input_int8_arr[v] = input_zp_int8_arr[v]; + } else { + #endif + if (v + in_f_offset < FSV) { + input_int8_arr[v] = conv_input[input_idx + tmp * FSV + v]; + } else { + const uint addr = input_idx + tmp * FSV + v + + ((INPUT0_SIZE_X + INPUT0_PAD_BEFORE_SIZE_X + INPUT0_PAD_AFTER_SIZE_X) * + (INPUT0_SIZE_Y + INPUT0_PAD_BEFORE_SIZE_Y + INPUT0_PAD_AFTER_SIZE_Y) * + (INPUT0_SIZE_Z + INPUT0_PAD_BEFORE_SIZE_Z + INPUT0_PAD_AFTER_SIZE_Z) - 1) * FSV; + input_int8_arr[v] = conv_input[addr]; + } + #ifdef SHOULD_USE_DATA_ZP + } + #endif + } else { + input_int8_arr[v] = 0; + } + } + } else { + __attribute__((opencl_unroll_hint(FSV))) + for (uint v = 0; v < FSV; v++) { #ifdef SHOULD_USE_DATA_ZP if (input_on_padding) { input_int8_arr[v] = input_zp_int8_arr[v]; @@ -282,6 +364,7 @@ KERNEL(convolution_gpu_b_fs_zyx_fsv16_imad)( #ifdef SHOULD_USE_DATA_ZP } #endif + } } } #endif @@ -301,6 +384,17 @@ KERNEL(convolution_gpu_b_fs_zyx_fsv16_imad)( weights_val[ofb] = vload4(0, (__global uint *)(weights + filter_idx + ofb * filter_idx_diff)); } + #if FILTER_IFM_NUM % FSV != 0 + if (in_f_start + (k + 1) * FSV >= ALIGN(FILTER_IFM_NUM, FSV)) { + unroll_for (uint ofb = 0; ofb < OFM_BLOCKS_PER_SIMD; ++ofb) { + FILTER_TYPE* w_p = (FILTER_TYPE*)&weights_val[ofb]; + unroll_for (uint f = FILTER_IFM_NUM % FSV; f < FSV; f++) { + w_p[f] = 0; + } + } + } + #endif + unroll_for (uint ive = 0; ive < 4; ive++) { unroll_for (uint ofb = 0; ofb < OFM_BLOCKS_PER_SIMD; ++ofb) { #ifdef SHOULD_USE_DATA_ZP diff --git a/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/convolution_gpu_bfyx_f16.cl b/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/convolution_gpu_bfyx_f16.cl index b52ef8a4628c..c5b63fecdf1c 100644 --- a/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/convolution_gpu_bfyx_f16.cl +++ b/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/convolution_gpu_bfyx_f16.cl @@ -476,4 +476,4 @@ KERNEL(convolution_bfyx_f16)( # undef OUTPUTVTYPE # undef TO_OUTPUTVTYPE # undef VSTORE -#endif // OUTPUT_FORMAT_BFYX +#endif // OUTPUT_FORMAT_BFYX \ No newline at end of file diff --git a/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/convolution_gpu_bfyx_f16_1x1.cl b/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/convolution_gpu_bfyx_f16_1x1.cl index bb474e007790..f1a5da9b9eae 100644 --- a/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/convolution_gpu_bfyx_f16_1x1.cl +++ b/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/convolution_gpu_bfyx_f16_1x1.cl @@ -298,4 +298,4 @@ KERNEL(convolution_b_fs_yx_fsv16_1x1)( #undef GET_SRC #undef FEATURE_SLICE_SIZE #undef UNIT_BLOCK_READ_VEC -#undef UNIT_BLOCK_WRITE_VEC +#undef UNIT_BLOCK_WRITE_VEC \ No newline at end of file diff --git a/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/convolution_gpu_bfyx_os_iyx_osv16.cl b/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/convolution_gpu_bfyx_os_iyx_osv16.cl index 6726f998285f..d191715695d7 100644 --- a/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/convolution_gpu_bfyx_os_iyx_osv16.cl +++ b/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/convolution_gpu_bfyx_os_iyx_osv16.cl @@ -144,12 +144,18 @@ KERNEL(convolution_gpu_bfyx_os_iyx_osv16)( // Position in sub-group on which new row need to be read. const uint sg_br_pos = IN_BLOCK_WIDTH - in_block_pos % IN_BLOCK_WIDTH; - if (lid < sg_br_pos) - in[in_block_pos / SUB_GROUP_SIZE] = input[tmp_in_addr + (in_block_pos % IN_BLOCK_WIDTH) * INPUT0_X_PITCH]; + if (lid < sg_br_pos) { + uint idx = tmp_in_addr + (in_block_pos % IN_BLOCK_WIDTH) * INPUT0_X_PITCH; + idx = min(idx, input0_physical_len - 1); + in[in_block_pos / SUB_GROUP_SIZE] = input[idx]; + } // We have row break inside sub-group. Need to move to next line. tmp_in_addr += INPUT0_Y_PITCH; - if (lid >= sg_br_pos) - in[in_block_pos / SUB_GROUP_SIZE] = input[tmp_in_addr - (sg_br_pos * INPUT0_X_PITCH)]; + if (lid >= sg_br_pos) { + uint idx = tmp_in_addr - (sg_br_pos * INPUT0_X_PITCH); + idx = min(idx, input0_physical_len - 1); + in[in_block_pos / SUB_GROUP_SIZE] = input[idx]; + } // If we have another row break, move to the next row. if (in_block_next_x_pos == 2 * IN_BLOCK_WIDTH) diff --git a/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/convolution_gpu_imad.cl b/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/convolution_gpu_imad.cl index c2670d95438f..60524570fe8b 100644 --- a/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/convolution_gpu_imad.cl +++ b/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/convolution_gpu_imad.cl @@ -180,9 +180,13 @@ KERNEL (fused_convolution_eltwise_gpu_imad)( #if INPUT0_LAYOUT_B_FS_YX_FSV16 #if ((FILTER_GROUPS_NUM > 1) && (FILTER_IFM_NUM % PACK != 0)) INPUT0_TYPE* input_zp_int8_arr = (INPUT0_TYPE*) &data_zp_val; - for (uint v = 0; v < PACK; v++) { + uint valid_ifm_in_pack = (kd + 1) * PACK <= FILTER_IFM_NUM ? PACK : (FILTER_IFM_NUM % PACK); + for (uint v = 0; v < valid_ifm_in_pack; v++) { input_zp_int8_arr[v] = activations_zp[feature_location + v]; } + for (uint v = valid_ifm_in_pack; v < PACK; v++) { + input_zp_int8_arr[v] = 0; + } #else data_zp_val = *(__global PACKED_TYPE*)(activations_zp + (kd + g * CEIL_DIV(FILTER_IFM_NUM, PACK)) * PACK); #endif @@ -215,11 +219,15 @@ KERNEL (fused_convolution_eltwise_gpu_imad)( #endif INPUT0_TYPE* input_int8_arr = (INPUT0_TYPE*) &in[reg]; in_addr = in_start_addr + reg * INPUT0_Y_PITCH * FSV; - for (uint v = 0; v < PACK; v++) { + uint valid_ifm_in_pack_input = (kd + 1) * PACK <= FILTER_IFM_NUM ? PACK : (FILTER_IFM_NUM % PACK); + for (uint v = 0; v < valid_ifm_in_pack_input; v++) { int f_addr = ((feature_location + v) / FSV + INPUT0_PAD_BEFORE_FEATURE_NUM / FSV) * \ INPUT0_FEATURE_PITCH * FSV + (feature_location + v) % FSV; input_int8_arr[v] = conv_input[in_addr + f_addr]; } + for (uint v = valid_ifm_in_pack_input; v < PACK; v++) { + input_int8_arr[v] = 0; + } #ifdef SHOULD_USE_DATA_ZP } #endif @@ -262,6 +270,17 @@ KERNEL (fused_convolution_eltwise_gpu_imad)( } #endif + #if FILTER_IFM_NUM % PACK != 0 + if ((kd + 1) * PACK >= ALIGN(FILTER_IFM_NUM, PACK)) { + for (int pf = 0; pf < NUM_FILTERS; pf++) { + FILTER_TYPE* w_p = (FILTER_TYPE*)&w[pf]; + unroll_for (uint in_f = FILTER_IFM_NUM % PACK; in_f < PACK; in_f++) { + w_p[in_f] = 0; + } + } + } + #endif + int wi = 0; // This loop is temporarily not unrolled because the unroll causes TeamCity hangs. //__attribute__((opencl_unroll_hint(FILTER_SIZE_Y))) diff --git a/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/convolution_gpu_mmad_b_fs_yx_fsv32.cl b/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/convolution_gpu_mmad_b_fs_yx_fsv32.cl index 47d77d60e587..df682e2e0d9f 100644 --- a/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/convolution_gpu_mmad_b_fs_yx_fsv32.cl +++ b/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/convolution_gpu_mmad_b_fs_yx_fsv32.cl @@ -173,11 +173,22 @@ KERNEL(convolution_mmad_b_fs_yx_fsv32)( } else { +#if SUB_GROUP_SIZE == 8 line_cache[xb] = AS_TYPE(PACKED_IN_TYPE, _sub_group_block_read((const __global uint*)(input + in_addr + icb * input_fs_pitch + kd * DILATION_SIZE_Z * input_z_pitch + kh * DILATION_SIZE_Y * input_y_pitch + xb * input_x_pitch))); +#else + // SIMD16: _sub_group_block_read reads 16 uints (64 bytes) but only + // 8 uints (ISV_SIZE=32 bytes) exist per spatial position, causing + // OOB reads at boundaries on Xe2+. Use per-lane scalar read instead. + line_cache[xb] = AS_TYPE(PACKED_IN_TYPE, ((const __global uint*)(input + in_addr + + icb * input_fs_pitch + + kd * DILATION_SIZE_Z * input_z_pitch + + kh * DILATION_SIZE_Y * input_y_pitch + + xb * input_x_pitch))[lid]); +#endif } } } diff --git a/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/deconvolution_gpu_ref.cl b/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/deconvolution_gpu_ref.cl index 95d5bd39cd8e..0b9c09063ccc 100644 --- a/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/deconvolution_gpu_ref.cl +++ b/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/deconvolution_gpu_ref.cl @@ -47,9 +47,9 @@ KERNEL(deconvolution_gpu_yxfb_ref)( const uint batch_offset = b_f % INPUT0_BATCH_NUM; #endif - const int x = (int)out_x + PADDING_SIZE_X - (FILTER_SIZE_X - 1); - const int y = (int)out_y + PADDING_SIZE_Y - (FILTER_SIZE_Y - 1); - const int z = (int)out_z + PADDING_SIZE_Z - (FILTER_SIZE_Z - 1); + const int x = (int)out_x + PADDING_SIZE_X - (FILTER_SIZE_X - 1) * DILATION_SIZE_X; + const int y = (int)out_y + PADDING_SIZE_Y - (FILTER_SIZE_Y - 1) * DILATION_SIZE_Y; + const int z = (int)out_z + PADDING_SIZE_Z - (FILTER_SIZE_Z - 1) * DILATION_SIZE_Z; #if GROUPED const uint g = (ofm_offset / FILTER_OFM_NUM); @@ -66,21 +66,21 @@ KERNEL(deconvolution_gpu_yxfb_ref)( for (uint k = 0; k < FILTER_SIZE_Z; k++) { - const int input_offset_z = z + k; + const int input_offset_z = z + k * DILATION_SIZE_Z; const bool zero_z = (input_offset_z >= INPUT0_SIZE_Z * STRIDE_SIZE_Z) || (input_offset_z < 0) || ((input_offset_z % STRIDE_SIZE_Z) != 0); if(!zero_z) { for (uint i = 0; i < FILTER_SIZE_Y; i++) { - const int input_offset_y = y + i; + const int input_offset_y = y + i * DILATION_SIZE_Y; const bool zero_y = (input_offset_y >= INPUT0_SIZE_Y * STRIDE_SIZE_Y) || (input_offset_y < 0) || ((input_offset_y % STRIDE_SIZE_Y) != 0); if(!zero_y) { for (uint j = 0; j < FILTER_SIZE_X; j++) { - const int input_offset_x = x + j; + const int input_offset_x = x + j * DILATION_SIZE_X; const bool zero_x = (input_offset_x >= INPUT0_SIZE_X * STRIDE_SIZE_X) || (input_offset_x < 0) || ((input_offset_x % STRIDE_SIZE_X) != 0); if(!zero_x) diff --git a/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/dynamic_quantize_gpu_kv_cache.cl b/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/dynamic_quantize_gpu_kv_cache.cl index 8351266528e7..22e68ba1dd53 100644 --- a/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/dynamic_quantize_gpu_kv_cache.cl +++ b/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/dynamic_quantize_gpu_kv_cache.cl @@ -2,14 +2,16 @@ // SPDX-License-Identifier: Apache-2.0 // -#include "include/batch_headers/fetch_data.cl" #include "include/batch_headers/fetch_data.cl" #include "include/batch_headers/common.cl" +#include "include/batch_headers/int4_utils.cl" #include "include/batch_headers/sub_group_block_read.cl" #include "include/batch_headers/sub_group_block_write.cl" #include "include/batch_headers/sub_group_shuffle.cl" +#define UINT4_RANGE 15 + #if OUTPUT_DIMS != 4 #error "dynamic_quantize_gpu_kv_cache.cl: Unsupported output dimension" #endif @@ -84,6 +86,70 @@ KERNEL(dynamic_quantize_gpu_kv_cache)( max_value = fmax(max_value, grp_max); #endif +#ifdef APPEND_MODE + APPEND_AXIS_NAME += axis_offset; +#endif + +#if IS_INT4_COMPRESSED + // 4-bit unsigned asymmetric quantization with adjacent head packing + min_value = work_group_reduce_min(min_value); + max_value = work_group_reduce_max(max_value); + + ACCUMULATOR_TYPE diff_value = max_value == min_value ? (ACCUMULATOR_TYPE)(grp_max) + : (ACCUMULATOR_TYPE)(max_value - min_value); + ACCUMULATOR_TYPE scale_tmp = (ACCUMULATOR_TYPE)((UINT4_RANGE) / diff_value); + ACCUMULATOR_TYPE zp_tmp = (ACCUMULATOR_TYPE)(-min_value * scale_tmp); + + // Quantize all values to u4 range + uchar qval[INNERMOST_DIM_VALUE / SUBGROUP_SIZE]; + unroll_for (uint i = 0; i < INNERMOST_DIM_VALUE / SUBGROUP_SIZE; i++) { + qval[i] = (uchar)clamp(convert_int_rte((float)val[i] * scale_tmp + zp_tmp), 0, UINT4_RANGE); + } + + // Pack adjacent head dimensions + const uint output_offset = OUTPUT_GET_INDEX(b, f, y, x); +#define NUM_SUBGROUP_CHUNKS (INNERMOST_DIM_VALUE / SUBGROUP_SIZE) +#define NUM_OUTPUT_ITERS (NUM_SUBGROUP_CHUNKS / 2) + unroll_for (uint i = 0; i < NUM_OUTPUT_ITERS; i++) { + uint pair_lane = 2 * (sglid % 8); + uchar lo_even = intel_sub_group_shuffle(qval[2 * i], pair_lane); + uchar hi_even = intel_sub_group_shuffle(qval[2 * i], pair_lane + 1); + uchar lo_odd = intel_sub_group_shuffle(qval[2 * i + 1], pair_lane); + uchar hi_odd = intel_sub_group_shuffle(qval[2 * i + 1], pair_lane + 1); + uchar q_lo = (sglid < 8) ? lo_even : lo_odd; + uchar q_hi = (sglid < 8) ? hi_even : hi_odd; + char packed = cvt_uint8x2_to_uint4x2((uchar2)(q_lo, q_hi)); + output[output_offset + i * SUBGROUP_SIZE + sglid] = packed; + } +#if (NUM_SUBGROUP_CHUNKS % 2) != 0 + { + uint pair_lane = 2 * (sglid % 8); + uchar q_lo = intel_sub_group_shuffle(qval[NUM_SUBGROUP_CHUNKS - 1], pair_lane); + uchar q_hi = intel_sub_group_shuffle(qval[NUM_SUBGROUP_CHUNKS - 1], pair_lane + 1); + char packed = cvt_uint8x2_to_uint4x2((uchar2)(q_lo, q_hi)); + if (sglid < SUBGROUP_SIZE / 2) + output[output_offset + NUM_OUTPUT_ITERS * SUBGROUP_SIZE + sglid] = packed; + } +#endif +#undef NUM_SUBGROUP_CHUNKS +#undef NUM_OUTPUT_ITERS + + const uint scale_idx = FUNC_CALL(get_scales_offset)(OPTIONAL_SHAPE_INFO_TENSOR b, f, y, x); + if (grouped_indexes == 0 && sglid == 0) { + output_scale[scale_idx] = (OUTPUT1_TYPE)(1.0f / scale_tmp); // dequant scale +#if ASYMMETRIC_QUANTIZATION && !GROUP_SCALES_WITH_ZP + #if OUTPUT2_IS_FP + output_zp[scale_idx] = (OUTPUT2_TYPE)(zp_tmp); + #else + output_zp[scale_idx] = convert_char_rte(zp_tmp); + #endif +#else + output_scale[scale_idx + 1] = (OUTPUT1_TYPE)(zp_tmp); // zero-point (interleaved) +#endif + } + +#else // !IS_INT4_COMPRESSED — original INT8 path + #if ASYMMETRIC_QUANTIZATION min_value = work_group_reduce_min(min_value); max_value = work_group_reduce_max(max_value); @@ -100,10 +166,6 @@ KERNEL(dynamic_quantize_gpu_kv_cache)( OUTPUT1_TYPE scale = 127.0h / max_value; #endif -#ifdef APPEND_MODE - APPEND_AXIS_NAME += axis_offset; -#endif - const uint output_offset = OUTPUT_GET_INDEX(b, f, y, x); unroll_for (uint i = 0; i < INNERMOST_DIM_VALUE / SUBGROUP_SIZE; i++) { #if ASYMMETRIC_QUANTIZATION @@ -134,4 +196,6 @@ KERNEL(dynamic_quantize_gpu_kv_cache)( output_scale[scale_idx] = 1.0h / scale; #endif } + +#endif // IS_INT4_COMPRESSED } diff --git a/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/dynamic_quantize_gpu_opt.cl b/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/dynamic_quantize_gpu_opt.cl index f7f184c4b813..2b5c7133053f 100644 --- a/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/dynamic_quantize_gpu_opt.cl +++ b/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/dynamic_quantize_gpu_opt.cl @@ -137,6 +137,7 @@ KERNEL(dynamic_quantize_gpu_opt)( const uint local_id = get_local_id(1); __local half local_mem_max[BLOCK_NUM]; __local half local_mem_min[BLOCK_NUM]; + FOR_PRECOMPUTED_REDUCTION(__local int local_mem_reduction[BLOCK_NUM]); MAKE_VECTOR_TYPE(INPUT0_TYPE, VEC_SIZE) val; MAKE_VECTOR_TYPE(INPUT0_TYPE, VEC_SIZE) abs_val; @@ -168,20 +169,29 @@ KERNEL(dynamic_quantize_gpu_opt)( min_value = sub_group_reduce_min(grp_min); #endif - const uint block_offset_idx = local_id * QUANTIZE_GROUP_SIZE / block_size; + const uint blocks_per_group = QUANTIZE_GROUP_SIZE / block_size; + const uint group_id = local_id / blocks_per_group; + const uint block_in_group = local_id % blocks_per_group; + const uint group_base_idx = group_id * blocks_per_group; + if (sglid == 0) { - local_mem_max[block_offset_idx + blockid] = max_value; + local_mem_max[local_id] = max_value; #if ASYMMETRIC_QUANTIZATION - local_mem_min[block_offset_idx + blockid] = min_value; + local_mem_min[local_id] = min_value; #endif } barrier(CLK_LOCAL_MEM_FENCE); - for (int j = 0; j < QUANTIZE_GROUP_SIZE / block_size; j++) { - max_value = fmax(max_value, local_mem_max[block_offset_idx + j]); + // Aggregate max/min across all blocks in this quantization group + max_value = local_mem_max[group_base_idx]; #if ASYMMETRIC_QUANTIZATION - min_value = fmin(min_value, local_mem_min[block_offset_idx + j]); + min_value = local_mem_min[group_base_idx]; +#endif + unroll_for (int j = 1; j < QUANTIZE_GROUP_SIZE / SIMD / VEC_SIZE; j++) { + max_value = fmax(max_value, local_mem_max[group_base_idx + j]); +#if ASYMMETRIC_QUANTIZATION + min_value = fmin(min_value, local_mem_min[group_base_idx + j]); #endif } @@ -202,12 +212,31 @@ KERNEL(dynamic_quantize_gpu_opt)( #if GENERATE_PRECOMPUTED_REDUCTION // TODO: Optimize this part + // Calculate local reduction for this work-item int precomputed_reduction = 0; MAKE_VECTOR_TYPE(OUTPUT2_TYPE, VEC_SIZE) val_int = CAT(CONVERT_INT_N, _rte)(val); unroll_for (int j = 0; j < VEC_SIZE; j++) { precomputed_reduction += val_int[j]; } + // Reduce within subgroup precomputed_reduction = sub_group_reduce_add(precomputed_reduction); + + // Store to local memory for cross-block aggregation + if (sglid == 0) { + local_mem_reduction[local_id] = precomputed_reduction; + } + + barrier(CLK_LOCAL_MEM_FENCE); + + // Aggregate reduction across all blocks in this quantization group + // Only the first work-item in each group does this + if (sglid == 0 && block_in_group == 0) { + int total_reduction = 0; + unroll_for (int j = 0; j < QUANTIZE_GROUP_SIZE / SIMD / VEC_SIZE; j++) { + total_reduction += local_mem_reduction[group_base_idx + j]; + } + precomputed_reduction = total_reduction; + } #endif if (sglid == 0 && blockid == 0) { diff --git a/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/experimental_detectron_roi_feature_extractor_ref.cl b/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/experimental_detectron_roi_feature_extractor_ref.cl index 964b5fb3f7b1..0bc5be266cab 100644 --- a/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/experimental_detectron_roi_feature_extractor_ref.cl +++ b/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/experimental_detectron_roi_feature_extractor_ref.cl @@ -11,10 +11,10 @@ inline int FUNC(get_pyramid_level_index)(uint level, uint c, uint y, uint x) { } inline int FUNC(get_pyramid_level_for_roi)(const __global INPUT0_TYPE* current_roi) { - const INPUT0_TYPE canonical_scale = 224.0; + const INPUT0_TYPE canonical_scale = TO_INPUT0_TYPE(224.0f); const int canonical_level = 2; - int result = NUM_PYRAMID_LEVELS; + int result = 0; const INPUT0_TYPE x0 = current_roi[0]; const INPUT0_TYPE y0 = current_roi[1]; @@ -23,7 +23,9 @@ inline int FUNC(get_pyramid_level_for_roi)(const __global INPUT0_TYPE* current_r const INPUT0_TYPE area = (x1 - x0) * (y1 - y0); if (area > 0) { - result = (int)round(canonical_level + log2(sqrt(area) / canonical_scale)); + INPUT0_TYPE level_value = sqrt(area) / canonical_scale; + level_value = log2(level_value + TO_INPUT0_TYPE(1e-6f)); + result = (int)floor(level_value + canonical_level); result = max(0, min(result, NUM_PYRAMID_LEVELS - 1)); } return result; @@ -42,7 +44,7 @@ KERNEL(experimental_detectron_roi_feature_extractor_ref)(const __global INPUT0_T const __global INPUT0_TYPE* current_roi_ptr = &src_rois[r * INPUT0_BATCH_PITCH]; - const int level = FUNC_CALL(get_pyramid_level_for_roi)(current_roi_ptr); + const int level = max(0, min(FUNC_CALL(get_pyramid_level_for_roi)(current_roi_ptr), NUM_PYRAMID_LEVELS - 1)); const __global INPUT1_TYPE* current_level_ptr = LEVEL_PTRS[level]; diff --git a/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/fully_connected_gpu_MMAD.cl b/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/fully_connected_gpu_MMAD.cl index 16d19d63bc8a..3e2994b16f08 100644 --- a/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/fully_connected_gpu_MMAD.cl +++ b/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/fully_connected_gpu_MMAD.cl @@ -245,7 +245,11 @@ KERNEL(fully_connected_gpu_MMAD)( weights_data = AS_TYPE(FILTER_PACKED_TYPE_8, BLOCK_READ_8(weights + filter_idx)); #else weights_data.lo = AS_TYPE(FILTER_PACKED_TYPE_8, BLOCK_READ_8(weights + filter_idx)); +#if MMAD_FILTER_LEFTOVER_HI weights_data.hi = AS_TYPE(FILTER_PACKED_TYPE_8, BLOCK_READ_8(weights + filter_idx + SUB_GROUP_SIZE * 32)); +#else + weights_data.hi = (FILTER_PACKED_TYPE_8)(0); +#endif // MMAD_FILTER_LEFTOVER_HI #endif // SUB_GROUP_SIZE == 8 dotProd = MMAD(activations, weights_data, dotProd); diff --git a/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/fully_connected_gpu_bf_tiled.cl b/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/fully_connected_gpu_bf_tiled.cl index 765f6658632f..1e89ccef827d 100644 --- a/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/fully_connected_gpu_bf_tiled.cl +++ b/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/fully_connected_gpu_bf_tiled.cl @@ -337,7 +337,11 @@ inline void FUNC(fc_bf_tiled_kernel_default)( // For fp16 we need to ensure that all block reads are aligned to 4 byte (2 words) boundary. // To do this solve first input feature separately. { - INPUT0_TYPE tmp_input = input[input_offset + get_sub_group_local_id() % TILE_B * TILE_IN_B_PITCH]; + // Guard OOB batch reads on Xe2+ where OOB access crashes (CL_OUT_OF_RESOURCES). + uint sglid_b = get_sub_group_local_id() % TILE_B; + INPUT0_TYPE tmp_input = (out_b + sglid_b < BATCH_SIZE) + ? input[input_offset + sglid_b * TILE_IN_B_PITCH] + : INPUT0_VAL_ZERO; ACCUMULATOR_VEC_TYPE tmp_wei = TO_ACCUMULATOR_VEC_TYPE(BLOCK_READN(FILTER_TYPE, TILE_OFM, weights, weights_offset)); #if COMPRESSED_WEIGHTS tmp_wei = (tmp_wei - d_zp) * d_scale; @@ -355,10 +359,22 @@ inline void FUNC(fc_bf_tiled_kernel_default)( __attribute__((opencl_unroll_hint(1))) for (uint ni = 0; ni < iterations; ++ni) { // Load input. - #define LOAD_IN_0(bi) do { \ - in_0[bi] = INPUT_BLOCK_READ(input, input_offset); \ - input_offset += TILE_IN_B_PITCH; \ + // Guard OOB batch reads on Xe2+ where OOB access crashes (CL_OUT_OF_RESOURCES). + #if IS_DYNAMIC || BATCH_LEFTOVER + #define LOAD_IN_0(bi) do { \ + if (bi + out_b < BATCH_SIZE) { \ + in_0[bi] = INPUT_BLOCK_READ(input, input_offset);\ + } else { \ + in_0[bi] = 0; \ + } \ + input_offset += TILE_IN_B_PITCH; \ } while (false) + #else + #define LOAD_IN_0(bi) do { \ + in_0[bi] = INPUT_BLOCK_READ(input, input_offset); \ + input_offset += TILE_IN_B_PITCH; \ + } while (false) + #endif CONST_LOOP(TILE_B, LOAD_IN_0); #undef LOAD_IN_0 @@ -651,10 +667,22 @@ inline void FUNC(fc_bf_tiled_kernel_default)( // Handle leftovers in normal case without alignment correction. #define LEFTOVER_IFM (MAIN_LOOP_ELEMENTS_COUNT % (TILE_IFM * SIMD)) { + // Guard OOB batch reads on Xe2+ where OOB access crashes (CL_OUT_OF_RESOURCES). + #if IS_DYNAMIC || BATCH_LEFTOVER + #define LOAD_IN_0(bi) do { \ + if (bi + out_b < BATCH_SIZE) { \ + in_0[bi] = INPUT_BLOCK_READ(input, input_offset);\ + } else { \ + in_0[bi] = 0; \ + } \ + input_offset += TILE_IN_B_PITCH; \ + } while (false) + #else #define LOAD_IN_0(bi) do { \ in_0[bi] = INPUT_BLOCK_READ(input, input_offset); \ input_offset += TILE_IN_B_PITCH; \ } while (false) + #endif CONST_LOOP(TILE_B, LOAD_IN_0); #undef LOAD_IN_0 @@ -1250,10 +1278,15 @@ inline void FUNC(fc_bf_tiled_kernel_dyn_quan)( #endif #if COMPRESSED_WEIGHTS_INT8 - ACCUM_DQ_TYPE modified_calc_buff = ((int *)(&acc_tmp[fi]))[bi] - ((float)(wei_zp[fi]) * activation_sum[bi]); - ((ACCUMULATOR_TYPE*)(&acc[bi]))[fi] += (convert_half)(convert_float(modified_calc_buff) * (float)ds * (float)de_quantize_scale[bi]); + // Keep zero-point correction in fp32. Storing this value in int truncates + // the activation_sum term and diverges from the per-token path below. + float modified_calc_buff = ((float)((int *)(&acc_tmp[fi]))[bi]) - ((float)(wei_zp[fi]) * activation_sum[bi]); + ((ACCUMULATOR_TYPE*)(&acc[bi]))[fi] += (convert_half)(modified_calc_buff * (float)ds * (float)de_quantize_scale[bi]); #else - ((ACCUMULATOR_TYPE*)(&acc[bi]))[fi] += (convert_half)(convert_float(((int *)(&acc_tmp[fi]))[bi]) * (float)de_quantize_scale[bi] * (float)ds); + // Compose DQ and decompression scales before applying them to the accumulator. + // This reduces fp16 intermediate overflow risk on the f16 scale path, but it is not + // a mathematical overflow guard: very large accumulators or final f16 values can still overflow. + ((ACCUMULATOR_TYPE*)(&acc[bi]))[fi] += convert_half(((int *)(&acc_tmp[fi]))[bi]) * (de_quantize_scale[bi] * ds); #endif acc_tmp[fi][bi] = 0; } @@ -1281,10 +1314,15 @@ inline void FUNC(fc_bf_tiled_kernel_dyn_quan)( #endif #if COMPRESSED_WEIGHTS_INT8 - ACCUM_DQ_TYPE modified_calc_buff = ((float)((int *)(&acc_tmp[fi]))[bi]) - ((float)(wei_zp[fi]) * activation_sum[bi]); - ((ACCUMULATOR_TYPE*)(&acc[bi]))[fi] += (convert_half)(convert_float(modified_calc_buff) * (float)ds * (float)de_quantize_scale[bi]); + // Keep zero-point correction in fp32. Storing this value in int truncates + // the activation_sum term and diverges from the per-token path below. + float modified_calc_buff = ((float)((int *)(&acc_tmp[fi]))[bi]) - ((float)(wei_zp[fi]) * activation_sum[bi]); + ((ACCUMULATOR_TYPE*)(&acc[bi]))[fi] += (convert_half)(modified_calc_buff * (float)ds * (float)de_quantize_scale[bi]); #else - ((ACCUMULATOR_TYPE*)(&acc[bi]))[fi] += (convert_half)(convert_float(((int *)(&acc_tmp[fi]))[bi]) * (float)de_quantize_scale[bi] * (float)ds); + // Compose DQ and decompression scales before applying them to the accumulator. + // This reduces fp16 intermediate overflow risk on the f16 scale path, but it is not + // a mathematical overflow guard: very large accumulators or final f16 values can still overflow. + ((ACCUMULATOR_TYPE*)(&acc[bi]))[fi] += convert_half(((int *)(&acc_tmp[fi]))[bi]) * (de_quantize_scale[bi] * ds); #endif acc_tmp[fi][bi] = 0; } @@ -1299,9 +1337,14 @@ inline void FUNC(fc_bf_tiled_kernel_dyn_quan)( ACCUMULATOR_TYPE ds = d_scales[fi % DECOMPRESSION_SCALE_LENGTH]; #if COMPRESSED_WEIGHTS_INT8 float modified_calc_buff = ((float)((int *)(&acc_tmp[fi]))[bi]) - ((float)(wei_zp[fi]) * activation_sum[bi]); - ((ACCUMULATOR_TYPE*)(&acc[bi]))[fi] = (convert_half)(modified_calc_buff) * de_quantize_scale[bi] * ds; + // Keep zero-point corrected scaling in fp32 before the final f16 conversion to avoid + // fp16 intermediate overflow. The final f16 value can still overflow if the true result + // is outside the fp16 range. + ((ACCUMULATOR_TYPE*)(&acc[bi]))[fi] = (convert_half)(modified_calc_buff * (float)de_quantize_scale[bi] * (float)ds); #else - ((ACCUMULATOR_TYPE*)(&acc[bi]))[fi] = convert_half(((int *)(&acc_tmp[fi]))[bi]) * de_quantize_scale[bi] * ds; + // Compose DQ and decompression scales first to reduce fp16 intermediate overflow risk. + // This is still not a mathematical overflow guard if the accumulator or final f16 value is too large. + ((ACCUMULATOR_TYPE*)(&acc[bi]))[fi] = convert_half(((int *)(&acc_tmp[fi]))[bi]) * (de_quantize_scale[bi] * ds); #endif } } diff --git a/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/fully_connected_gpu_gemv.cl b/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/fully_connected_gpu_gemv.cl index 442d6c1349e9..c77fb30f3007 100644 --- a/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/fully_connected_gpu_gemv.cl +++ b/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/fully_connected_gpu_gemv.cl @@ -223,7 +223,7 @@ __attribute__((intel_reqd_sub_group_size(SUBGROUP_SIZE))) KERNEL(fully_connected barrier(CLK_LOCAL_MEM_FENCE); float2 sum_value; - sum_value[0] = as_float(intel_sub_group_block_read((const __local uint*)all_sum_even[thr_id])); + sum_value[0] = as_float(_sub_group_block_read_slm((const __local uint*)all_sum_even[thr_id])); sum_value[0] = sub_group_reduce_add(sum_value[0]); if (wi_id == 0) { int cur_n = n + thr_id; @@ -367,8 +367,8 @@ __attribute__((intel_reqd_sub_group_size(SUBGROUP_SIZE))) KERNEL(fully_connected barrier(CLK_LOCAL_MEM_FENCE); float2 sum_value; - sum_value[0] = as_float(intel_sub_group_block_read((const __local uint*)all_sum_even[thr_id])); - sum_value[1] = as_float(intel_sub_group_block_read((const __local uint*)all_sum_odd[thr_id])); + sum_value[0] = as_float(_sub_group_block_read_slm((const __local uint*)all_sum_even[thr_id])); + sum_value[1] = as_float(_sub_group_block_read_slm((const __local uint*)all_sum_odd[thr_id])); sum_value[0] = sub_group_reduce_add(sum_value[0]); sum_value[1] = sub_group_reduce_add(sum_value[1]); @@ -563,10 +563,10 @@ __attribute__((intel_reqd_sub_group_size(SUBGROUP_SIZE))) KERNEL(fully_connected barrier(CLK_LOCAL_MEM_FENCE); float4 sum_value; - sum_value[0] = as_float(intel_sub_group_block_read((const __local uint*)all_sum_0[thr_id])); - sum_value[1] = as_float(intel_sub_group_block_read((const __local uint*)all_sum_1[thr_id])); - sum_value[2] = as_float(intel_sub_group_block_read((const __local uint*)all_sum_2[thr_id])); - sum_value[3] = as_float(intel_sub_group_block_read((const __local uint*)all_sum_3[thr_id])); + sum_value[0] = as_float(_sub_group_block_read_slm((const __local uint*)all_sum_0[thr_id])); + sum_value[1] = as_float(_sub_group_block_read_slm((const __local uint*)all_sum_1[thr_id])); + sum_value[2] = as_float(_sub_group_block_read_slm((const __local uint*)all_sum_2[thr_id])); + sum_value[3] = as_float(_sub_group_block_read_slm((const __local uint*)all_sum_3[thr_id])); for (int i = 0; i < 4; i++) { sum_value[i] = sub_group_reduce_add(sum_value[i]); diff --git a/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/gather_nonzero_group.cl b/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/gather_nonzero_group.cl new file mode 100644 index 000000000000..93427490f566 --- /dev/null +++ b/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/gather_nonzero_group.cl @@ -0,0 +1,187 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "include/batch_headers/common.cl" + +#define VSIZE 8 +#define VLOAD CAT(vload, VSIZE) +#define VSTORE CAT(vstore,VSIZE) +#define OUTPUT_VTYPE MAKE_VECTOR_TYPE(OUTPUT_TYPE, VSIZE) + +KERNEL (gather_nonzero_ref)( + OPTIONAL_SHAPE_INFO_ARG + const __global INPUT0_TYPE* input, + volatile __global INPUT1_TYPE* output_shape, + __global OUTPUT_TYPE* output) +{ + const uint local_idx = get_local_id(0); + const uint num_work_items = get_global_size(0); + const uint data_size = TOTAL_DATA_SIZE; + const uint items_num = data_size / num_work_items; + const uint leftovers = data_size - (items_num * num_work_items); + + uint workitem_nonzero_count = 0; + uint actual_items_num = items_num; + uint input_idx = (actual_items_num * local_idx) + leftovers; + if (local_idx < leftovers) { + actual_items_num = items_num + 1; + input_idx = actual_items_num * local_idx; + } + uint final_item = input_idx + actual_items_num; + + for (uint iter = 0; iter < actual_items_num; iter++) { + const uint idx = input_idx + iter; + uint count = (input[idx] == INPUT0_VAL_ZERO) ? 0 : 1; + workitem_nonzero_count += count; + } + + work_group_barrier(CLK_LOCAL_MEM_FENCE); + uint local_offset = work_group_scan_exclusive_add(workitem_nonzero_count); + + const int result_size = OV_INPUT_RANK * OUTPUT_FEATURE_NUM; // output shape: [ov_rank, count_nonzero] + + OUTPUT_TYPE* out_mem; + bool use_local_mem = false; + __local OUTPUT_TYPE out_mem_slm[MAX_LOCAL_MEM_SIZE]; +#if IS_DYNAMIC + const int dst_slm_size = TOTAL_DATA_SIZE * OV_INPUT_RANK; + use_local_mem = dst_slm_size < MAX_LOCAL_MEM_SIZE; +#endif // IS_DYNAMIC +#ifdef USE_LOCAL_MEM + use_local_mem = true; +#endif // USE_LOCAL_MEM + if (use_local_mem) { + out_mem = out_mem_slm; + } else { + out_mem = output; + } + + int count_nzero = output_shape[0]; +#if OV_INPUT_RANK == 1 // b + #define ADD_IDXS \ + int b = input_idx_v / INPUT0_BATCH_PITCH; \ + out_mem[local_offset++] = b; +#elif OV_INPUT_RANK == 2 // bf + #define ADD_IDXS \ + int b = input_idx_v / INPUT0_BATCH_PITCH; \ + int f = (input_idx_v - (b * INPUT0_BATCH_PITCH)) / INPUT0_FEATURE_PITCH; \ + int b_pos = local_offset; \ + int f_pos = b_pos + count_nzero; \ + out_mem[b_pos] = b; \ + out_mem[f_pos] = f; \ + local_offset++; +#elif OV_INPUT_RANK == 3 // bfy + #define ADD_IDXS \ + int b = input_idx_v / INPUT0_BATCH_PITCH; \ + int f = (input_idx_v - (b * INPUT0_BATCH_PITCH)) / INPUT0_FEATURE_PITCH; \ + int y = (input_idx_v - (b * INPUT0_BATCH_PITCH) - (f * INPUT0_FEATURE_PITCH)) / INPUT0_Y_PITCH; \ + int b_pos = local_offset; \ + int f_pos = b_pos + count_nzero; \ + int y_pos = f_pos + count_nzero; \ + out_mem[b_pos] = b; \ + out_mem[f_pos] = f; \ + out_mem[y_pos] = y; \ + local_offset++; +#elif OV_INPUT_RANK == 4 // bfyx + #define ADD_IDXS \ + int b = input_idx_v / INPUT0_BATCH_PITCH; \ + int f = (input_idx_v - (b * INPUT0_BATCH_PITCH)) / INPUT0_FEATURE_PITCH; \ + int y = (input_idx_v - (b * INPUT0_BATCH_PITCH) - (f * INPUT0_FEATURE_PITCH)) / INPUT0_Y_PITCH; \ + int x = (input_idx_v - (b * INPUT0_BATCH_PITCH) - (f * INPUT0_FEATURE_PITCH) - (y * INPUT0_Y_PITCH)) / INPUT0_X_PITCH; \ + int b_pos = local_offset; \ + int f_pos = b_pos + count_nzero; \ + int y_pos = f_pos + count_nzero; \ + int x_pos = y_pos + count_nzero; \ + out_mem[b_pos] = b; \ + out_mem[f_pos] = f; \ + out_mem[y_pos] = y; \ + out_mem[x_pos] = x; \ + local_offset++; +#elif OV_INPUT_RANK == 5 // bfzyx + #define ADD_IDXS \ + int b = input_idx_v / INPUT0_BATCH_PITCH; \ + int f = (input_idx_v - (b * INPUT0_BATCH_PITCH)) / INPUT0_FEATURE_PITCH; \ + int z = (input_idx_v - (b * INPUT0_BATCH_PITCH) - (f * INPUT0_FEATURE_PITCH)) / INPUT0_Z_PITCH; \ + int y = (input_idx_v - (b * INPUT0_BATCH_PITCH) - (f * INPUT0_FEATURE_PITCH) - (z * INPUT0_Z_PITCH)) / INPUT0_Y_PITCH; \ + int x = (input_idx_v - (b * INPUT0_BATCH_PITCH) - (f * INPUT0_FEATURE_PITCH) - (z * INPUT0_Z_PITCH) - (y * INPUT0_Y_PITCH)) / INPUT0_X_PITCH; \ + int b_pos = local_offset; \ + int f_pos = b_pos + count_nzero; \ + int z_pos = f_pos + count_nzero; \ + int y_pos = z_pos + count_nzero; \ + int x_pos = y_pos + count_nzero; \ + out_mem[b_pos] = b; \ + out_mem[f_pos] = f; \ + out_mem[z_pos] = z; \ + out_mem[y_pos] = y; \ + out_mem[x_pos] = x; \ + local_offset++; +#elif OV_INPUT_RANK == 6 // bfwzyx + #define ADD_IDXS \ + int b = input_idx_v / INPUT0_BATCH_PITCH; \ + int f = (input_idx_v - (b * INPUT0_BATCH_PITCH)) / INPUT0_FEATURE_PITCH; \ + int w = (input_idx_v - (b * INPUT0_BATCH_PITCH) - (f * INPUT0_FEATURE_PITCH)) / INPUT0_W_PITCH; \ + int z = (input_idx_v - (b * INPUT0_BATCH_PITCH) - (f * INPUT0_FEATURE_PITCH) - (w * INPUT0_W_PITCH)) / INPUT0_Z_PITCH; \ + int y = (input_idx_v - (b * INPUT0_BATCH_PITCH) - (f * INPUT0_FEATURE_PITCH) - (w * INPUT0_W_PITCH) - (z * INPUT0_Z_PITCH)) / INPUT0_Y_PITCH; \ + int x = (input_idx_v - (b * INPUT0_BATCH_PITCH) - (f * INPUT0_FEATURE_PITCH) - (w * INPUT0_W_PITCH) - (z * INPUT0_Z_PITCH) - (y * INPUT0_Y_PITCH)) / INPUT0_X_PITCH; \ + int b_pos = local_offset; \ + int f_pos = b_pos + count_nzero; \ + int w_pos = f_pos + count_nzero; \ + int z_pos = w_pos + count_nzero; \ + int y_pos = z_pos + count_nzero; \ + int x_pos = y_pos + count_nzero; \ + out_mem[b_pos] = b; \ + out_mem[f_pos] = f; \ + out_mem[w_pos] = w; \ + out_mem[z_pos] = z; \ + out_mem[y_pos] = y; \ + out_mem[x_pos] = x; \ + local_offset++; +#endif + + // load to local mem + for (; input_idx + VSIZE <= final_item; input_idx += VSIZE) { + MAKE_VECTOR_TYPE(INPUT0_TYPE, VSIZE) inputs = VLOAD(0, input + input_idx); + for (int v = 0; v < VSIZE; ++v) { + int input_idx_v = input_idx + v; + if (inputs[v] != INPUT0_VAL_ZERO) { + ADD_IDXS; + } + } + } + + // leftovers + for (;input_idx < final_item; ++input_idx) { + int input_idx_v = input_idx; + int v = 0; + if (input[input_idx] != INPUT0_VAL_ZERO) { + ADD_IDXS; + } + } + + work_group_barrier(CLK_LOCAL_MEM_FENCE); + int global_output_offset = 0; + if (use_local_mem && local_idx == 0) { + // write back to global mem + int local_out_iter = 0; + for (; local_out_iter + VSIZE < result_size; local_out_iter += VSIZE) { + VSTORE(VLOAD(0, out_mem + local_out_iter), 0, output + global_output_offset + local_out_iter); + } + // leftover + for (; local_out_iter < result_size; ++local_out_iter) { + output[global_output_offset + local_out_iter] = out_mem[local_out_iter]; + } + } +} +#ifdef VLOAD +#undef VLOAD +#endif +#ifdef VSTORE +#undef VSTORE +#endif +#ifdef VSIZE +#undef VSIZE +#endif +#ifdef OUTPUT_VTYPE +#undef OUTPUT_VTYPE +#endif diff --git a/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/include/algorithm.cl b/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/include/algorithm.cl index 909ccbcc05b6..e7e101a372be 100644 --- a/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/include/algorithm.cl +++ b/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/include/algorithm.cl @@ -3,7 +3,7 @@ // #define DECLARE_LOWER_BOUND(Name, Type, ValType, GetIndex) \ - inline Type* FUNC(Name)(const Type* data, uint first_index, uint last_index, ValType value) { \ + inline uint FUNC(Name)(const Type* data, uint first_index, uint last_index, ValType value) { \ uint count = last_index - first_index; \ while (count > 0) { \ const uint step = count / 2; \ @@ -19,7 +19,7 @@ } #define DECLARE_UPPER_BOUND(Name, Type, ValType, GetIndex) \ - inline Type* FUNC(Name)(const Type* data, uint first_index, uint last_index, ValType value) { \ + inline uint FUNC(Name)(const Type* data, uint first_index, uint last_index, ValType value) { \ uint count = last_index - first_index; \ while (count > 0) { \ const uint step = count / 2; \ diff --git a/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/include/batch_headers/sub_group_block_read.cl b/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/include/batch_headers/sub_group_block_read.cl index 2ea9b3c7768c..39299b7c7eeb 100644 --- a/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/include/batch_headers/sub_group_block_read.cl +++ b/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/include/batch_headers/sub_group_block_read.cl @@ -43,6 +43,19 @@ #define BLOCK_READ_FUNC_size8 _sub_group_block_read_ul #define BLOCK_READ_FUNC(type_size) CAT(BLOCK_READ_FUNC_size, type_size) +// __local-pointer variants. cl_intel_subgroups* only defines __global overloads of +// intel_sub_group_block_read*; the __local overloads come from the separate +// cl_intel_subgroup_local_block_io extension, which NEO 23.x+ no longer declares +// implicitly. The _slm-suffixed family below routes to the intrinsic when the +// extension is advertised and falls back to an inline-emulation per-lane gather +// otherwise — see DECLARE_BLOCK_READ_SLM_EMULATION and the macro blocks at the +// bottom of this file. +#define BLOCK_READ_FUNC_SLM_size1 _sub_group_block_read_slm_uc +#define BLOCK_READ_FUNC_SLM_size2 _sub_group_block_read_slm_us +#define BLOCK_READ_FUNC_SLM_size4 _sub_group_block_read_slm +#define BLOCK_READ_FUNC_SLM_size8 _sub_group_block_read_slm_ul +#define BLOCK_READ_FUNC_SLM(type_size) CAT(BLOCK_READ_FUNC_SLM_size, type_size) + #define BLOCK_READN_FUNC_SIZE_DEF(type_size, vector_size) MAKE_VECTOR_TYPE(BLOCK_READ_FUNC(type_size), vector_size) #define BLOCK_READN_FUNC_size1(vector_size) BLOCK_READN_FUNC_SIZE_DEF(1, vector_size) #define BLOCK_READN_FUNC_size2(vector_size) BLOCK_READN_FUNC_SIZE_DEF(2, vector_size) @@ -50,14 +63,24 @@ #define BLOCK_READN_FUNC_size8(vector_size) BLOCK_READN_FUNC_SIZE_DEF(8, vector_size) #define BLOCK_READN_FUNC(type_size, vector_size) CAT(BLOCK_READN_FUNC_size, type_size)(vector_size) +#define BLOCK_READN_FUNC_SLM_SIZE_DEF(type_size, vector_size) MAKE_VECTOR_TYPE(BLOCK_READ_FUNC_SLM(type_size), vector_size) +#define BLOCK_READN_FUNC_SLM_size1(vector_size) BLOCK_READN_FUNC_SLM_SIZE_DEF(1, vector_size) +#define BLOCK_READN_FUNC_SLM_size2(vector_size) BLOCK_READN_FUNC_SLM_SIZE_DEF(2, vector_size) +#define BLOCK_READN_FUNC_SLM_size4(vector_size) BLOCK_READN_FUNC_SLM_SIZE_DEF(4, vector_size) +#define BLOCK_READN_FUNC_SLM_size8(vector_size) BLOCK_READN_FUNC_SLM_SIZE_DEF(8, vector_size) +#define BLOCK_READN_FUNC_SLM(type_size, vector_size) CAT(BLOCK_READN_FUNC_SLM_size, type_size)(vector_size) + #define BLOCK_READN_RAW(type_size, vector_size, addr_space, ptr, offset) \ BLOCK_READN_FUNC(type_size, vector_size)((const addr_space BLOCK_READ_TYPE(type_size)*)(ptr) + (offset)) +#define BLOCK_READN_RAW_SLM(type_size, vector_size, ptr, offset) \ + BLOCK_READN_FUNC_SLM(type_size, vector_size)((const __local BLOCK_READ_TYPE(type_size)*)(ptr) + (offset)) + #define BLOCK_READN(type, vector_size, ptr, offset) \ AS_TYPE(MAKE_VECTOR_TYPE(type, vector_size), BLOCK_READN_RAW(TYPE_SIZE(type), vector_size, __global, ptr, offset)) #define BLOCK_READN_SLM(type, vector_size, ptr, offset) \ - AS_TYPE(MAKE_VECTOR_TYPE(type, vector_size), BLOCK_READN_RAW(TYPE_SIZE(type), vector_size, __local, ptr, offset)) + AS_TYPE(MAKE_VECTOR_TYPE(type, vector_size), BLOCK_READN_RAW_SLM(TYPE_SIZE(type), vector_size, ptr, offset)) #define DT_INPUT_BLOCK_READ(ptr, offset) BLOCK_READN(INPUT0_TYPE, 1, ptr, offset) #define DT_INPUT_BLOCK_READ2(ptr, offset) BLOCK_READN(INPUT0_TYPE, 2, ptr, offset) @@ -117,6 +140,15 @@ return ret; \ } +#define BLOCK_READ_FUNC_NAME_SLM(type_size, vec_size) MAKE_VECTOR_TYPE(BLOCK_READ_FUNC_SLM(type_size), vec_size) +#define DECLARE_BLOCK_READ_SLM_EMULATION(type_size, vec_size) \ + inline MAKE_VECTOR_TYPE(BLOCK_READ_TYPE(type_size), vec_size) BLOCK_READ_FUNC_NAME_SLM(type_size, vec_size)(const __local BLOCK_READ_TYPE(type_size)* ptr) { \ + uint idx = get_sub_group_local_id(); \ + MAKE_VECTOR_TYPE(BLOCK_READ_TYPE(type_size), vec_size) ret; \ + BLOCK_READ_IMPL(vec_size) \ + return ret; \ +} + #if defined(cl_intel_subgroups) #define _sub_group_block_read(ptr) intel_sub_group_block_read(ptr) #define _sub_group_block_read2(ptr) intel_sub_group_block_read2(ptr) @@ -129,6 +161,18 @@ DECLARE_BLOCK_READ_EMULATION(4, 8) #endif +#if defined(cl_intel_subgroup_local_block_io) + #define _sub_group_block_read_slm(ptr) intel_sub_group_block_read(ptr) + #define _sub_group_block_read_slm2(ptr) intel_sub_group_block_read2(ptr) + #define _sub_group_block_read_slm4(ptr) intel_sub_group_block_read4(ptr) + #define _sub_group_block_read_slm8(ptr) intel_sub_group_block_read8(ptr) +#elif (__OPENCL_C_VERSION__ >= 200) + DECLARE_BLOCK_READ_SLM_EMULATION(4, 1) + DECLARE_BLOCK_READ_SLM_EMULATION(4, 2) + DECLARE_BLOCK_READ_SLM_EMULATION(4, 4) + DECLARE_BLOCK_READ_SLM_EMULATION(4, 8) +#endif + #if defined(cl_intel_subgroups_short) #define _sub_group_block_read_us(ptr) intel_sub_group_block_read_us(ptr) #define _sub_group_block_read_us2(ptr) intel_sub_group_block_read_us2(ptr) @@ -141,6 +185,18 @@ DECLARE_BLOCK_READ_EMULATION(2, 8) #endif +#if defined(cl_intel_subgroup_local_block_io) + #define _sub_group_block_read_slm_us(ptr) intel_sub_group_block_read_us(ptr) + #define _sub_group_block_read_slm_us2(ptr) intel_sub_group_block_read_us2(ptr) + #define _sub_group_block_read_slm_us4(ptr) intel_sub_group_block_read_us4(ptr) + #define _sub_group_block_read_slm_us8(ptr) intel_sub_group_block_read_us8(ptr) +#elif (__OPENCL_C_VERSION__ >= 200) + DECLARE_BLOCK_READ_SLM_EMULATION(2, 1) + DECLARE_BLOCK_READ_SLM_EMULATION(2, 2) + DECLARE_BLOCK_READ_SLM_EMULATION(2, 4) + DECLARE_BLOCK_READ_SLM_EMULATION(2, 8) +#endif + #if defined(cl_intel_subgroups_char) #define _sub_group_block_read_uc(ptr) intel_sub_group_block_read_uc(ptr) #define _sub_group_block_read_uc2(ptr) intel_sub_group_block_read_uc2(ptr) @@ -155,6 +211,20 @@ DECLARE_BLOCK_READ_EMULATION(1, 16) #endif +#if defined(cl_intel_subgroup_local_block_io) + #define _sub_group_block_read_slm_uc(ptr) intel_sub_group_block_read_uc(ptr) + #define _sub_group_block_read_slm_uc2(ptr) intel_sub_group_block_read_uc2(ptr) + #define _sub_group_block_read_slm_uc4(ptr) intel_sub_group_block_read_uc4(ptr) + #define _sub_group_block_read_slm_uc8(ptr) intel_sub_group_block_read_uc8(ptr) + #define _sub_group_block_read_slm_uc16(ptr) intel_sub_group_block_read_uc16(ptr) +#elif (__OPENCL_C_VERSION__ >= 200) + DECLARE_BLOCK_READ_SLM_EMULATION(1, 1) + DECLARE_BLOCK_READ_SLM_EMULATION(1, 2) + DECLARE_BLOCK_READ_SLM_EMULATION(1, 4) + DECLARE_BLOCK_READ_SLM_EMULATION(1, 8) + DECLARE_BLOCK_READ_SLM_EMULATION(1, 16) +#endif + #if defined(cl_intel_subgroups_long) #define _sub_group_block_read_ul(ptr) intel_sub_group_block_read_ul(ptr) #define _sub_group_block_read_ul2(ptr) intel_sub_group_block_read_ul2(ptr) @@ -166,3 +236,15 @@ DECLARE_BLOCK_READ_EMULATION(8, 4) DECLARE_BLOCK_READ_EMULATION(8, 8) #endif + +#if defined(cl_intel_subgroup_local_block_io) + #define _sub_group_block_read_slm_ul(ptr) intel_sub_group_block_read_ul(ptr) + #define _sub_group_block_read_slm_ul2(ptr) intel_sub_group_block_read_ul2(ptr) + #define _sub_group_block_read_slm_ul4(ptr) intel_sub_group_block_read_ul4(ptr) + #define _sub_group_block_read_slm_ul8(ptr) intel_sub_group_block_read_ul8(ptr) +#elif (__OPENCL_C_VERSION__ >= 200) + DECLARE_BLOCK_READ_SLM_EMULATION(8, 1) + DECLARE_BLOCK_READ_SLM_EMULATION(8, 2) + DECLARE_BLOCK_READ_SLM_EMULATION(8, 4) + DECLARE_BLOCK_READ_SLM_EMULATION(8, 8) +#endif diff --git a/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/mvn_gpu_b_fs_yx_bsv32.cl b/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/mvn_gpu_b_fs_yx_bsv32.cl new file mode 100644 index 000000000000..523fd537370a --- /dev/null +++ b/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/mvn_gpu_b_fs_yx_bsv32.cl @@ -0,0 +1,409 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// +// This file is intentionally split from mvn_gpu_b_fs_yx_fsv16.cl. +// Both files share the accumulate/reduce helpers and define their own +// ITEMS_NUM via the preprocessor. Keeping them in one CL file would leak +// ITEMS_NUM across batch-compiled kernels and produce wrong dispatches. +// Do not merge them back without introducing a per-kernel JIT for ITEMS_NUM. + +#include "include/batch_headers/fetch_data.cl" +#include "include/batch_headers/imad.cl" +#include "include/batch_headers/sub_group_block_read.cl" +#include "include/batch_headers/sub_group_block_write.cl" +#include "include/batch_headers/sub_group_shuffle.cl" + +#include "mvn_gpu_b_fs_yx_fsv16_accumulate.cl" +#include "mvn_gpu_b_fs_yx_fsv16_reduce.cl" + +// MVN - performs mean-variance normalization, that is normalizes the input data to have +// 0 mean and if NORMALIZE_VARIANCE is set to have variance 1. +// +// Below is a set of 5 kernels: +// mvn_mean_1, mvn_mean_2, mvn_var_1, mvn_var_2, mvn_final +// that can perform mvn operation in two modes. +// +// Basic mode: +// In this mode only mvn_final kernel is used. It performs required reductions for mean +// and variance in this single kernel using single work-group for slice of data-sets +// and reducing intermidiate values with local memory. +// It does not require any additional jit constants. +// lws: LWS x 1 x 1 +// gws: LWS x feature x batch +// +// Parallel mode: +// In this mode all kernels are used to provide extra paralellism with global memory +// and host side synchronization with evets/in-order queue. +// To calculate mean: +// mvn_mean_1 kernel should be first enqueued, provided extra global memory on second input +// allowing to store intermidate results from all work-groups. +// To activate this kernel MVN_KERNEL_MEAN_1 must be defined and evaluate to true/1. +// lws: LWS x 1 x 1 +// gws: LWS * ITEM_GROUPS x feature x batch +// This kernel will calculate partial results for each ITEM_GROUPS work-groups and store it into global memory. +// +// mvn_mean_2 kernel must be next enqueued in order to further reduce previous results using single work-group. +// This kernel expects on first input the result of mvn_mean_1 and on second input global memory of size +// batch * align(feature, FSV) should be provided to store final mean values. +// It needs to be ensured that mvn_mean_1 kernel has finished and stored its partial results into memory. +// To activate this kernel MVN_KERNEL_MEAN_2 must be defined and evaluate to true/1. +// lws: LWS x 1 x 1 +// gws: LWS x feature x batch +// +// If required analogously the mvn_var_1 and mvn_var_2 kernels should be enqueud, additionally providing results from +// mvn_mean_2 kernel. +// +// Finally the mvn_final kernel should be enqueued with provided buffers with outputs from previous kernels +// (mvn_mean_2, mvn_var_2). To enable parallel mode PRECALC_MEAN and optionally PRECALC_VARIANCE definitions should be +// used. As at this stage there is no further need to synchronize and this kernel will perform simple normalization +// given known mean and inverse of variance. Due to this this kernel can be enqueued with full paralellization, not +// limiting it to single work-group. +// lws: SIMD x 1 x 1 +// gws: (x * y) / SIMD * SIMD x feature x batch +// +// Required jit constants: +// SIMD - Sub-group/simd size. +// LWS - Local work-size along 0th dimension, must be multiple of SIMD. +// GWS - Global work-size along 0th dimension. +// In basic mode this must be equal to LWS. +// In parallel mode this must be equal to LWS * ITEM_GROUPS, except in mvn_final kernel where it has no restrictions. +// ITEM_GROUPS - Number of work-groups performing accumulation in parallel mode. Should be the same in both stages of parallel kernels. + +#define FSV 16 +#define SG_NUM (LWS / SIMD) + +#define INPUT_TYPE2 MAKE_VECTOR_TYPE(INPUT0_TYPE, 2) +#define INPUT_TYPE4 MAKE_VECTOR_TYPE(INPUT0_TYPE, 4) +#define INPUT_TYPE8 MAKE_VECTOR_TYPE(INPUT0_TYPE, 8) +#define INPUT_PACKED_TYPE MAKE_VECTOR_TYPE(INPUT0_TYPE, FSV) +#define OUTPUT_PACKED_TYPE MAKE_VECTOR_TYPE(OUTPUT_TYPE, FSV) +#define MEAN_PACKED_TYPE MAKE_VECTOR_TYPE(MEAN_TYPE, FSV) +#define INT_PACKED_TYPE MAKE_VECTOR_TYPE(int, FSV) +#define ACC_PACKED_TYPE MAKE_VECTOR_TYPE(ACCUMULATOR_TYPE, FSV) +#define ACT_PACKED_TYPE MAKE_VECTOR_TYPE(ACTIVATION_TYPE, FSV) + +#define TO_MEAN_PACKED_TYPE CAT(convert_, MEAN_PACKED_TYPE) +#define TO_ACT_PACKED_TYPE CAT(convert_, ACT_PACKED_TYPE) + +#define ITEMS_NUM (OUTPUT_SIZE_X * OUTPUT_SIZE_Y * OUTPUT_SIZE_Z) + +// ================================================================================================ +#if MVN_KERNEL_MEAN_1 + +DECLARE_PACKED_ACCUMULATE(accumulate_sum_input, ACCUMULATOR_TYPE, INPUT0_TYPE, FSV, INPUT_SLICE_PITCH, ITEMS_NUM, GWS, ACCUMULATE_SUM) + +#if SG_NUM != 1 +DECLARE_WG_PACKED_REDUCE_ADD(reduce_sum_across_sg, ACCUMULATOR_TYPE, FSV, SG_NUM, REDUCE_NO_POST_OP) +#else +DECLARE_SG_PACKED_REDUCE_ADD(reduce_sum_inside_sg, ACCUMULATOR_TYPE, FSV, REDUCE_NO_POST_OP) +#endif + +REQD_SUB_GROUP_SIZE(SIMD) +__attribute__((reqd_work_group_size(LWS, 1, 1))) +KERNEL(mvn_mean_1)(const __global INPUT0_TYPE* input, + __global ACCUMULATOR_TYPE* intermidiate_sum) { + uint b = get_global_id(2); + uint f = get_global_id(1) * FSV; + uint flat_data_set_group = b * CEIL_DIV(OUTPUT_FEATURE_NUM, FSV) + get_global_id(1); + + uint items_group = get_group_id(0); + const uint sgid = get_sub_group_id(); + const uint sglid = get_sub_group_local_id(); + +#if INPUT0_DIMS == 5 + const uint data_sets_offset = INPUT0_GET_INDEX(b, f, 0, 0, 0); +#else // INPUT0_DIMS == 4 + const uint data_sets_offset = INPUT0_GET_INDEX(b, f, 0, 0); +#endif + + ACC_PACKED_TYPE partial_sum = FUNC_CALL(accumulate_sum_input)(input, data_sets_offset, get_global_id(0)); + +#if SG_NUM != 1 + __local int slm_acc[(SG_NUM - 1) * FSV]; + int full_sum = FUNC_CALL(reduce_sum_across_sg)(partial_sum, slm_acc); +#else + int full_sum = FUNC_CALL(reduce_sum_inside_sg)(partial_sum); +#endif + + if (sgid == 0 && (sglid < FSV || SIMD == FSV)) { + intermidiate_sum[flat_data_set_group * ITEM_GROUPS * FSV + items_group * FSV + sglid] = full_sum; + } +} +// ================================================================================================ +#elif MVN_KERNEL_MEAN_2 + +DECLARE_PACKED_ACCUMULATE(accumulate_sum_input, ACCUMULATOR_TYPE, ACCUMULATOR_TYPE, FSV, FSV, ITEM_GROUPS, LWS, ACCUMULATE_SUM) + +#define CALC_MEAN(sum) ((sum) / ITEMS_NUM) +#if SG_NUM != 1 +DECLARE_WG_PACKED_REDUCE_ADD(reduce_mean_across_sg, MEAN_TYPE, FSV, SG_NUM, CALC_MEAN) +#else +DECLARE_SG_PACKED_REDUCE_ADD(reduce_mean_inside_sg, MEAN_TYPE, FSV, CALC_MEAN) +#endif + +REQD_SUB_GROUP_SIZE(SIMD) +__attribute__((reqd_work_group_size(LWS, 1, 1))) +KERNEL(mvn_mean_2)(const __global ACCUMULATOR_TYPE* intermidiate_sum, + __global MEAN_TYPE* intermidiate_mean) { + uint b = get_global_id(2); + uint f = get_global_id(1) * FSV; + uint flat_data_set_group = b * CEIL_DIV(OUTPUT_FEATURE_NUM, FSV) + get_global_id(1); + + const uint sgid = get_sub_group_id(); + const uint sglid = get_sub_group_local_id(); + + const uint data_sets_offset = flat_data_set_group * ITEM_GROUPS * FSV; + + ACC_PACKED_TYPE complete_sum = FUNC_CALL(accumulate_sum_input)(intermidiate_sum, data_sets_offset, get_local_id(0)); + +#if SG_NUM != 1 + __local MEAN_TYPE slm_acc[(SG_NUM - 1) * FSV]; + MEAN_TYPE mean = FUNC_CALL(reduce_mean_across_sg)(TO_MEAN_PACKED_TYPE(complete_sum), slm_acc); +#else + MEAN_TYPE mean = FUNC_CALL(reduce_mean_inside_sg)(TO_MEAN_PACKED_TYPE(complete_sum)); +#endif + + if (sgid == 0 && (sglid < FSV || SIMD == FSV)) { + intermidiate_mean[flat_data_set_group * FSV + sglid] = mean; + } +} +// ================================================================================================ +#elif MVN_KERNEL_VAR_1 + +#define EXTRA_ARGS_DECL_IMPL , MEAN_TYPE mean +#define EXTRA_ARGS_IMPL , mean +#define EXTRA_ARGS_DECL EXTRA_ARGS_DECL_IMPL +#define EXTRA_ARGS EXTRA_ARGS_IMPL +#define ACCUMULATE_SUM_SQ_DEV(curr, next, idx, mean) ACCUMULATE_SUM_SQ(curr, TO_MEAN_TYPE(next) - _sub_group_shuffle(mean, idx), idx) +DECLARE_PACKED_ACCUMULATE_EARGS(accumulate_sum_sq_dev, MEAN_TYPE, INPUT0_TYPE, FSV, INPUT_SLICE_PITCH, ITEMS_NUM, GWS, ACCUMULATE_SUM_SQ_DEV, EXTRA_ARGS_DECL, EXTRA_ARGS) + +#if SG_NUM != 1 +DECLARE_WG_PACKED_REDUCE_ADD(reduce_sum_across_sg, MEAN_TYPE, FSV, SG_NUM, REDUCE_NO_POST_OP) +#else +DECLARE_SG_PACKED_REDUCE_ADD(reduce_sum_inside_sg, MEAN_TYPE, FSV, REDUCE_NO_POST_OP) +#endif + +REQD_SUB_GROUP_SIZE(SIMD) +__attribute__((reqd_work_group_size(LWS, 1, 1))) +KERNEL(mvn_var_1)(const __global INPUT0_TYPE* input, + const __global MEAN_TYPE* means, + __global MEAN_TYPE* intermidiate_sum) { + uint b = get_global_id(2); + uint f = get_global_id(1) * FSV; + uint flat_data_set_group = b * CEIL_DIV(OUTPUT_FEATURE_NUM, FSV) + get_global_id(1); + + uint items_group = get_group_id(0); + const uint sgid = get_sub_group_id(); + const uint sglid = get_sub_group_local_id(); + +#if INPUT0_DIMS == 5 + const uint data_sets_offset = INPUT0_GET_INDEX(b, f, 0, 0, 0); +#else // INPUT0_DIMS == 4 + const uint data_sets_offset = INPUT0_GET_INDEX(b, f, 0, 0); +#endif + + MEAN_TYPE mean = means[flat_data_set_group * FSV + sglid]; + MEAN_PACKED_TYPE partial_sum = FUNC_CALL(accumulate_sum_sq_dev)(input, data_sets_offset, get_global_id(0), mean); + +#if SG_NUM != 1 + __local MEAN_TYPE slm_acc[(SG_NUM - 1) * FSV]; + MEAN_TYPE full_sum = FUNC_CALL(reduce_sum_across_sg)(partial_sum, slm_acc); +#else + MEAN_TYPE full_sum = FUNC_CALL(reduce_sum_inside_sg)(partial_sum); +#endif + + if (sgid == 0 && (sglid < FSV || SIMD == FSV)) { + intermidiate_sum[flat_data_set_group * ITEM_GROUPS * FSV + items_group * FSV + sglid] = full_sum; + } +} +// ================================================================================================ +#elif MVN_KERNEL_VAR_2 + +DECLARE_PACKED_ACCUMULATE(accumulate_sum, MEAN_TYPE, MEAN_TYPE, FSV, FSV, ITEM_GROUPS, LWS, ACCUMULATE_SUM) +#if defined EPS_OUTSIDE_SQRT + #define CALC_INVERSE_VARIANCE(sum_diff_sq) native_powr(native_sqrt((sum_diff_sq) / ITEMS_NUM) + (MEAN_TYPE)EPSILON, (MEAN_TYPE)-1.f); +#elif defined EPS_INSIDE_SQRT + #define CALC_INVERSE_VARIANCE(sum_diff_sq) native_powr((sum_diff_sq) / ITEMS_NUM + (MEAN_TYPE)EPSILON, (MEAN_TYPE)-0.5f) +#endif +#if SG_NUM != 1 +DECLARE_WG_PACKED_REDUCE_ADD(reduce_var_across_sg, MEAN_TYPE, FSV, SG_NUM, CALC_INVERSE_VARIANCE) +#else +DECLARE_SG_PACKED_REDUCE_ADD(reduce_var_inside_sg, MEAN_TYPE, FSV, CALC_INVERSE_VARIANCE) +#endif + +REQD_SUB_GROUP_SIZE(SIMD) +__attribute__((reqd_work_group_size(LWS, 1, 1))) +KERNEL(mvn_var_2)(const __global MEAN_TYPE* intermidiate_sum, + __global MEAN_TYPE* intermidiate_ivar) { + uint b = get_global_id(2); + uint f = get_global_id(1) * FSV; + uint flat_data_set_group = b * CEIL_DIV(OUTPUT_FEATURE_NUM, FSV) + get_global_id(1); + + uint items_group = get_group_id(0); + const uint sgid = get_sub_group_id(); + const uint sglid = get_sub_group_local_id(); + + const uint data_sets_offset = flat_data_set_group * ITEM_GROUPS * FSV; + + MEAN_PACKED_TYPE complete_sum = FUNC_CALL(accumulate_sum)(intermidiate_sum, data_sets_offset, get_local_id(0)); + +#if SG_NUM != 1 + __local MEAN_TYPE slm_acc[(SG_NUM - 1) * FSV]; + MEAN_TYPE inv_variance = FUNC_CALL(reduce_var_across_sg)(complete_sum, slm_acc); +#else + MEAN_TYPE inv_variance = FUNC_CALL(reduce_var_inside_sg)(complete_sum); +#endif + + if (sgid == 0 && (sglid < FSV || SIMD == FSV)) { + intermidiate_ivar[flat_data_set_group * FSV + sglid] = inv_variance; + } +} + +// ================================================================================================ +#elif MVN_KERNEL_MAIN_BSV32 + +REQD_SUB_GROUP_SIZE(SIMD) +KERNEL(mvn_final_bsv32)( + const __global INPUT0_TYPE* input, + __global OUTPUT_TYPE* restrict output +#if HAS_FUSED_OPS_DECLS + , FUSED_OPS_DECLS +#endif + , const __global MEAN_TYPE* means +#if PRECALC_VARIANCE + , const __global MEAN_TYPE* variances +#endif +) { + uint b = get_global_id(2); + uint f = get_global_id(1) * FSV; + uint flat_data_set_group = b * CEIL_DIV(OUTPUT_FEATURE_NUM, FSV) + get_global_id(1); + + MEAN_PACKED_TYPE mean_vals = ((const __global MEAN_PACKED_TYPE*)(means + (flat_data_set_group * FSV)))[0]; + +#if PRECALC_VARIANCE + MEAN_PACKED_TYPE inv_variance = ((const __global MEAN_PACKED_TYPE*)(variances + (flat_data_set_group * FSV)))[0]; +#else // !PRECALC_VARIANCE + MEAN_PACKED_TYPE inv_variance = (MEAN_PACKED_TYPE)(MEAN_VAL_ONE); +#endif + + if (b >= OUTPUT_BATCH_NUM || f >= OUTPUT_FEATURE_NUM) + return; + + const uint output_spatial = get_global_id(0); + uint x = output_spatial % OUTPUT_SIZE_X; + uint y = output_spatial / OUTPUT_SIZE_X; + uint input_offset = INPUT0_GET_INDEX(b, f, y, x); + uint output_offset = OUTPUT_GET_INDEX(b, f, y, x); + + INPUT_PACKED_TYPE in_pack = ((const __global INPUT_PACKED_TYPE*)(input + input_offset))[0]; + ACT_PACKED_TYPE normalized_vec = fma((TO_ACT_PACKED_TYPE(in_pack) - TO_ACT_PACKED_TYPE(mean_vals)), + TO_ACT_PACKED_TYPE(inv_variance), (ACT_PACKED_TYPE)0); + OUTPUT_PACKED_TYPE result_vec = OUTPUT_VAL_ZERO; + + unroll_for (uint fi = 0; fi < FSV; fi++) { + ACTIVATION_TYPE normalized = normalized_vec[fi]; +# if HAS_FUSED_OPS + FUSED_OPS; + result_vec[fi] = FUSED_OPS_RESULT; +# else + result_vec[fi] = TO_OUTPUT_TYPE(normalized); +# endif + } + + vstore16(result_vec, 0, &output[output_offset]); +} + +#elif MVN_KERNEL_MEAN_VAR_BSV32 + +// Mean: +DECLARE_PACKED_ACCUMULATE(accumulate_sum_input, ACCUMULATOR_TYPE, INPUT0_TYPE, FSV, INPUT_SLICE_PITCH, ITEMS_NUM, LWS, ACCUMULATE_SUM) + +#define CALC_MEAN(sum) ((sum) / ITEMS_NUM) +#if SG_NUM != 1 +DECLARE_WG_PACKED_REDUCE_ADD(reduce_mean, MEAN_TYPE, FSV, SG_NUM, CALC_MEAN) +#else +DECLARE_SG_PACKED_REDUCE_ADD(reduce_mean, MEAN_TYPE, FSV, CALC_MEAN) +#endif + +// Variance: +#define EXTRA_ARGS_DECL_IMPL , MEAN_TYPE mean +#define EXTRA_ARGS_IMPL , mean +#define EXTRA_ARGS_DECL EXTRA_ARGS_DECL_IMPL +#define EXTRA_ARGS EXTRA_ARGS_IMPL +#define ACCUMULATE_SUM_SQ_DEV(curr, next, idx, mean) ACCUMULATE_SUM_SQ(curr, TO_MEAN_TYPE(next) - _sub_group_shuffle(mean, idx), idx) +DECLARE_PACKED_ACCUMULATE_EARGS(accumulate_sum_sq_dev, MEAN_TYPE, INPUT0_TYPE, FSV, INPUT_SLICE_PITCH, ITEMS_NUM, LWS, ACCUMULATE_SUM_SQ_DEV, EXTRA_ARGS_DECL, EXTRA_ARGS) + +#if defined EPS_OUTSIDE_SQRT + #define CALC_INVERSE_VARIANCE(sum_diff_sq) native_powr(native_sqrt((sum_diff_sq) / ITEMS_NUM) + (MEAN_TYPE)EPSILON, (MEAN_TYPE)-1.f); +#elif defined EPS_INSIDE_SQRT + #define CALC_INVERSE_VARIANCE(sum_diff_sq) native_powr((sum_diff_sq) / ITEMS_NUM + (MEAN_TYPE)EPSILON, (MEAN_TYPE)-0.5f) +#endif +#if SG_NUM != 1 +DECLARE_WG_PACKED_REDUCE_ADD(reduce_inverse_variance, MEAN_TYPE, FSV, SG_NUM, CALC_INVERSE_VARIANCE) +#else +DECLARE_SG_PACKED_REDUCE_ADD(reduce_inverse_variance, MEAN_TYPE, FSV, CALC_INVERSE_VARIANCE) +#endif + +REQD_SUB_GROUP_SIZE(SIMD) +__attribute__((reqd_work_group_size(LWS, 1, 1))) +KERNEL(mvn_mean_var_bsv32)( + const __global INPUT0_TYPE* input, + __global MEAN_TYPE* means +#if NORMALIZE_VARIANCE + , __global MEAN_TYPE* variances +#endif +) { + uint b = get_global_id(2); + uint f = get_global_id(1) * FSV; + uint flat_data_set_group = b * CEIL_DIV(OUTPUT_FEATURE_NUM, FSV) + get_global_id(1); + + const uint sgid = get_sub_group_id(); + const uint sglid = get_sub_group_local_id(); + const uint data_sets_offset = INPUT0_GET_INDEX(b, f, 0, 0); + + ACC_PACKED_TYPE partial_sum = FUNC_CALL(accumulate_sum_input)(input, data_sets_offset, get_local_id(0)); +#if SG_NUM != 1 + __local MEAN_TYPE slm_mean_acc[(SG_NUM - 1) * FSV]; + MEAN_TYPE mean = FUNC_CALL(reduce_mean)(TO_MEAN_PACKED_TYPE(partial_sum), slm_mean_acc); +#else + MEAN_TYPE mean = FUNC_CALL(reduce_mean)(TO_MEAN_PACKED_TYPE(partial_sum)); +#endif + +#if NORMALIZE_VARIANCE + MEAN_PACKED_TYPE partial_dev = FUNC_CALL(accumulate_sum_sq_dev)(input, data_sets_offset, get_local_id(0), mean); + #if SG_NUM != 1 + __local MEAN_TYPE slm_var_acc[(SG_NUM - 1) * FSV]; + MEAN_TYPE inv_variance = FUNC_CALL(reduce_inverse_variance)(partial_dev, slm_var_acc); + #else + MEAN_TYPE inv_variance = FUNC_CALL(reduce_inverse_variance)(partial_dev); + #endif +#endif + + if (sgid == 0 && (sglid < FSV || SIMD == FSV)) { + means[flat_data_set_group * FSV + sglid] = mean; + #if NORMALIZE_VARIANCE + variances[flat_data_set_group * FSV + sglid] = inv_variance; + #endif + } +} +#endif +// ================================================================================================ + +#undef FSV +#undef INPUT_SLICE_PITCH +#undef SG_NUM +#undef ITEMS_NUM + +#undef INPUT_TYPE2 +#undef INPUT_TYPE4 +#undef INPUT_TYPE8 +#undef INPUT_PACKED_TYPE +#undef OUTPUT_PACKED_TYPE +#undef INT_PACKED_TYPE +#undef MEAN_PACKED_TYPE +#undef TO_MEAN_PACKED_TYPE + +#undef INPUT_PACKED_BLOCK_READ +#undef OUTPUT_PAD_IN_ITEMS + +#undef USE_IMAD diff --git a/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/mvn_gpu_b_fs_yx_fsv16_imad.cl b/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/mvn_gpu_b_fs_yx_fsv16.cl similarity index 79% rename from src/plugins/intel_gpu/src/kernel_selector/cl_kernels/mvn_gpu_b_fs_yx_fsv16_imad.cl rename to src/plugins/intel_gpu/src/kernel_selector/cl_kernels/mvn_gpu_b_fs_yx_fsv16.cl index 591bb949fefd..cdb5da6d8e9f 100644 --- a/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/mvn_gpu_b_fs_yx_fsv16_imad.cl +++ b/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/mvn_gpu_b_fs_yx_fsv16.cl @@ -8,8 +8,8 @@ #include "include/batch_headers/sub_group_block_write.cl" #include "include/batch_headers/sub_group_shuffle.cl" -#include "mvn_gpu_b_fs_yx_fsv16_imad_accumulate.cl" -#include "mvn_gpu_b_fs_yx_fsv16_imad_reduce.cl" +#include "mvn_gpu_b_fs_yx_fsv16_accumulate.cl" +#include "mvn_gpu_b_fs_yx_fsv16_reduce.cl" // MVN - performs mean-variance normalization, that is normalizes the input data to have // 0 mean and if NORMALIZE_VARIANCE is set to have variance 1. @@ -66,6 +66,8 @@ #define FSV 16 #define SG_NUM (LWS / SIMD) +#define ISP_STRIDE (INPUT_SLICE_PITCH / FSV) +#define OSP_STRIDE (OUTPUT_SLICE_PITCH / FSV) #define INPUT_TYPE2 MAKE_VECTOR_TYPE(INPUT0_TYPE, 2) #define INPUT_TYPE4 MAKE_VECTOR_TYPE(INPUT0_TYPE, 4) @@ -80,12 +82,31 @@ #define TO_MEAN_PACKED_TYPE CAT(convert_, MEAN_PACKED_TYPE) #define TO_ACT_PACKED_TYPE CAT(convert_, ACT_PACKED_TYPE) -#define ITEMS_NUM (OUTPUT_SIZE_X * OUTPUT_SIZE_Y * OUTPUT_SIZE_Z) +// ITEMS_NUM must be provided exclusively via JIT by every kernel class +// that uses this CL file (fsv16). +// JIT-defined constants are automatically #undef'd between batch-compiled +// kernels, preventing stale macro leakage across compilation units. +// Do NOT add a #ifndef fallback here — a CL-side #define escapes the +// JIT #undef system and leaks into subsequent batch-compiled kernels. + +// OUTPUT_PAD_IN_ITEMS is used in #if preprocessor directives. +// For dynamic shapes, OUTPUT_PAD_* macros may expand to shape_info[N] +// references that the preprocessor cannot evaluate. Always take the +// padded output path (OUTPUT_GET_INDEX) in dynamic mode for safety. +#if IS_DYNAMIC +#define OUTPUT_PAD_IN_ITEMS 1 +#else +#define OUTPUT_PAD_IN_ITEMS (OUTPUT_PAD_BEFORE_SIZE_X != 0 || OUTPUT_PAD_AFTER_SIZE_X != 0 || OUTPUT_PAD_BEFORE_SIZE_Y != 0) +#endif // ================================================================================================ #if MVN_KERNEL_MEAN_1 +#if IS_DYNAMIC +DECLARE_PACKED_ACCUMULATE_DYN(accumulate_sum_input, ACCUMULATOR_TYPE, INPUT0_TYPE, FSV, INPUT_SLICE_PITCH, GWS, ACCUMULATE_SUM) +#else DECLARE_PACKED_ACCUMULATE(accumulate_sum_input, ACCUMULATOR_TYPE, INPUT0_TYPE, FSV, INPUT_SLICE_PITCH, ITEMS_NUM, GWS, ACCUMULATE_SUM) +#endif #if SG_NUM != 1 DECLARE_WG_PACKED_REDUCE_ADD(reduce_sum_across_sg, ACCUMULATOR_TYPE, FSV, SG_NUM, REDUCE_NO_POST_OP) @@ -94,9 +115,17 @@ DECLARE_SG_PACKED_REDUCE_ADD(reduce_sum_inside_sg, ACCUMULATOR_TYPE, FSV, REDUCE #endif REQD_SUB_GROUP_SIZE(SIMD) +#if !IS_DYNAMIC __attribute__((reqd_work_group_size(LWS, 1, 1))) -KERNEL(mvn_mean_1)(const __global INPUT0_TYPE* input, - __global ACCUMULATOR_TYPE* intermidiate_sum) { +#endif +KERNEL(mvn_mean_1)( + OPTIONAL_SHAPE_INFO_ARG + const __global INPUT0_TYPE* input, + __global ACCUMULATOR_TYPE* intermidiate_sum +#if IS_DYNAMIC + , uint items_num +#endif +) { uint b = get_global_id(2); uint f = get_global_id(1) * FSV; uint flat_data_set_group = b * CEIL_DIV(OUTPUT_FEATURE_NUM, FSV) + get_global_id(1); @@ -111,13 +140,17 @@ KERNEL(mvn_mean_1)(const __global INPUT0_TYPE* input, const uint data_sets_offset = INPUT0_GET_INDEX(b, f, 0, 0); #endif +#if IS_DYNAMIC + ACC_PACKED_TYPE partial_sum = FUNC_CALL(accumulate_sum_input)(input, data_sets_offset, get_global_id(0), items_num); +#else ACC_PACKED_TYPE partial_sum = FUNC_CALL(accumulate_sum_input)(input, data_sets_offset, get_global_id(0)); +#endif #if SG_NUM != 1 - __local int slm_acc[(SG_NUM - 1) * FSV]; - int full_sum = FUNC_CALL(reduce_sum_across_sg)(partial_sum, slm_acc); + __local ACCUMULATOR_TYPE slm_acc[(SG_NUM - 1) * FSV]; + ACCUMULATOR_TYPE full_sum = FUNC_CALL(reduce_sum_across_sg)(partial_sum, slm_acc); #else - int full_sum = FUNC_CALL(reduce_sum_inside_sg)(partial_sum); + ACCUMULATOR_TYPE full_sum = FUNC_CALL(reduce_sum_inside_sg)(partial_sum); #endif if (sgid == 0 && (sglid < FSV || SIMD == FSV)) { @@ -129,17 +162,33 @@ KERNEL(mvn_mean_1)(const __global INPUT0_TYPE* input, DECLARE_PACKED_ACCUMULATE(accumulate_sum_input, ACCUMULATOR_TYPE, ACCUMULATOR_TYPE, FSV, FSV, ITEM_GROUPS, LWS, ACCUMULATE_SUM) +#if IS_DYNAMIC +#if SG_NUM != 1 +DECLARE_WG_PACKED_REDUCE_ADD(reduce_mean_across_sg, MEAN_TYPE, FSV, SG_NUM, REDUCE_NO_POST_OP) +#else +DECLARE_SG_PACKED_REDUCE_ADD(reduce_mean_inside_sg, MEAN_TYPE, FSV, REDUCE_NO_POST_OP) +#endif +#else #define CALC_MEAN(sum) ((sum) / ITEMS_NUM) #if SG_NUM != 1 DECLARE_WG_PACKED_REDUCE_ADD(reduce_mean_across_sg, MEAN_TYPE, FSV, SG_NUM, CALC_MEAN) #else DECLARE_SG_PACKED_REDUCE_ADD(reduce_mean_inside_sg, MEAN_TYPE, FSV, CALC_MEAN) #endif +#endif REQD_SUB_GROUP_SIZE(SIMD) +#if !IS_DYNAMIC __attribute__((reqd_work_group_size(LWS, 1, 1))) -KERNEL(mvn_mean_2)(const __global ACCUMULATOR_TYPE* intermidiate_sum, - __global MEAN_TYPE* intermidiate_mean) { +#endif +KERNEL(mvn_mean_2)( + OPTIONAL_SHAPE_INFO_ARG + const __global ACCUMULATOR_TYPE* intermidiate_sum, + __global MEAN_TYPE* intermidiate_mean +#if IS_DYNAMIC + , uint items_num +#endif +) { uint b = get_global_id(2); uint f = get_global_id(1) * FSV; uint flat_data_set_group = b * CEIL_DIV(OUTPUT_FEATURE_NUM, FSV) + get_global_id(1); @@ -157,6 +206,9 @@ KERNEL(mvn_mean_2)(const __global ACCUMULATOR_TYPE* intermidiate_sum, #else MEAN_TYPE mean = FUNC_CALL(reduce_mean_inside_sg)(TO_MEAN_PACKED_TYPE(complete_sum)); #endif +#if IS_DYNAMIC + mean = mean / items_num; +#endif if (sgid == 0 && (sglid < FSV || SIMD == FSV)) { intermidiate_mean[flat_data_set_group * FSV + sglid] = mean; @@ -170,7 +222,11 @@ KERNEL(mvn_mean_2)(const __global ACCUMULATOR_TYPE* intermidiate_sum, #define EXTRA_ARGS_DECL EXTRA_ARGS_DECL_IMPL #define EXTRA_ARGS EXTRA_ARGS_IMPL #define ACCUMULATE_SUM_SQ_DEV(curr, next, idx, mean) ACCUMULATE_SUM_SQ(curr, TO_MEAN_TYPE(next) - _sub_group_shuffle(mean, idx), idx) +#if IS_DYNAMIC +DECLARE_PACKED_ACCUMULATE_DYN_EARGS(accumulate_sum_sq_dev, MEAN_TYPE, INPUT0_TYPE, FSV, INPUT_SLICE_PITCH, GWS, ACCUMULATE_SUM_SQ_DEV, EXTRA_ARGS_DECL, EXTRA_ARGS) +#else DECLARE_PACKED_ACCUMULATE_EARGS(accumulate_sum_sq_dev, MEAN_TYPE, INPUT0_TYPE, FSV, INPUT_SLICE_PITCH, ITEMS_NUM, GWS, ACCUMULATE_SUM_SQ_DEV, EXTRA_ARGS_DECL, EXTRA_ARGS) +#endif #if SG_NUM != 1 DECLARE_WG_PACKED_REDUCE_ADD(reduce_sum_across_sg, MEAN_TYPE, FSV, SG_NUM, REDUCE_NO_POST_OP) @@ -179,10 +235,18 @@ DECLARE_SG_PACKED_REDUCE_ADD(reduce_sum_inside_sg, MEAN_TYPE, FSV, REDUCE_NO_POS #endif REQD_SUB_GROUP_SIZE(SIMD) +#if !IS_DYNAMIC __attribute__((reqd_work_group_size(LWS, 1, 1))) -KERNEL(mvn_var_1)(const __global INPUT0_TYPE* input, - const __global MEAN_TYPE* means, - __global MEAN_TYPE* intermidiate_sum) { +#endif +KERNEL(mvn_var_1)( + OPTIONAL_SHAPE_INFO_ARG + const __global INPUT0_TYPE* input, + const __global MEAN_TYPE* means, + __global MEAN_TYPE* intermidiate_sum +#if IS_DYNAMIC + , uint items_num +#endif +) { uint b = get_global_id(2); uint f = get_global_id(1) * FSV; uint flat_data_set_group = b * CEIL_DIV(OUTPUT_FEATURE_NUM, FSV) + get_global_id(1); @@ -198,7 +262,11 @@ KERNEL(mvn_var_1)(const __global INPUT0_TYPE* input, #endif MEAN_TYPE mean = means[flat_data_set_group * FSV + sglid]; +#if IS_DYNAMIC + MEAN_PACKED_TYPE partial_sum = FUNC_CALL(accumulate_sum_sq_dev)(input, data_sets_offset, get_global_id(0), items_num, mean); +#else MEAN_PACKED_TYPE partial_sum = FUNC_CALL(accumulate_sum_sq_dev)(input, data_sets_offset, get_global_id(0), mean); +#endif #if SG_NUM != 1 __local MEAN_TYPE slm_acc[(SG_NUM - 1) * FSV]; @@ -215,6 +283,13 @@ KERNEL(mvn_var_1)(const __global INPUT0_TYPE* input, #elif MVN_KERNEL_VAR_2 DECLARE_PACKED_ACCUMULATE(accumulate_sum, MEAN_TYPE, MEAN_TYPE, FSV, FSV, ITEM_GROUPS, LWS, ACCUMULATE_SUM) +#if IS_DYNAMIC +#if SG_NUM != 1 +DECLARE_WG_PACKED_REDUCE_ADD(reduce_var_across_sg, MEAN_TYPE, FSV, SG_NUM, REDUCE_NO_POST_OP) +#else +DECLARE_SG_PACKED_REDUCE_ADD(reduce_var_inside_sg, MEAN_TYPE, FSV, REDUCE_NO_POST_OP) +#endif +#else #if defined EPS_OUTSIDE_SQRT #define CALC_INVERSE_VARIANCE(sum_diff_sq) native_powr(native_sqrt((sum_diff_sq) / ITEMS_NUM) + (MEAN_TYPE)EPSILON, (MEAN_TYPE)-1.f); #elif defined EPS_INSIDE_SQRT @@ -225,11 +300,20 @@ DECLARE_WG_PACKED_REDUCE_ADD(reduce_var_across_sg, MEAN_TYPE, FSV, SG_NUM, CALC_ #else DECLARE_SG_PACKED_REDUCE_ADD(reduce_var_inside_sg, MEAN_TYPE, FSV, CALC_INVERSE_VARIANCE) #endif +#endif REQD_SUB_GROUP_SIZE(SIMD) +#if !IS_DYNAMIC __attribute__((reqd_work_group_size(LWS, 1, 1))) -KERNEL(mvn_var_2)(const __global MEAN_TYPE* intermidiate_sum, - __global MEAN_TYPE* intermidiate_ivar) { +#endif +KERNEL(mvn_var_2)( + OPTIONAL_SHAPE_INFO_ARG + const __global MEAN_TYPE* intermidiate_sum, + __global MEAN_TYPE* intermidiate_ivar +#if IS_DYNAMIC + , uint items_num +#endif +) { uint b = get_global_id(2); uint f = get_global_id(1) * FSV; uint flat_data_set_group = b * CEIL_DIV(OUTPUT_FEATURE_NUM, FSV) + get_global_id(1); @@ -248,137 +332,16 @@ KERNEL(mvn_var_2)(const __global MEAN_TYPE* intermidiate_sum, #else MEAN_TYPE inv_variance = FUNC_CALL(reduce_var_inside_sg)(complete_sum); #endif - - if (sgid == 0 && (sglid < FSV || SIMD == FSV)) { - intermidiate_ivar[flat_data_set_group * FSV + sglid] = inv_variance; - } -} - -// ================================================================================================ -#elif MVN_KERNEL_MAIN_BSV32 - -REQD_SUB_GROUP_SIZE(SIMD) -KERNEL(mvn_final_bsv32)( - const __global INPUT0_TYPE* input, - __global OUTPUT_TYPE* restrict output -#if HAS_FUSED_OPS_DECLS - , FUSED_OPS_DECLS -#endif - , const __global MEAN_TYPE* means -#if PRECALC_VARIANCE - , const __global MEAN_TYPE* variances -#endif -) { - uint b = get_global_id(2); - uint f = get_global_id(1) * FSV; - uint flat_data_set_group = b * CEIL_DIV(OUTPUT_FEATURE_NUM, FSV) + get_global_id(1); - - MEAN_PACKED_TYPE mean_vals = ((const __global MEAN_PACKED_TYPE*)(means + (flat_data_set_group * FSV)))[0]; - -#if PRECALC_VARIANCE - MEAN_PACKED_TYPE inv_variance = ((const __global MEAN_PACKED_TYPE*)(variances + (flat_data_set_group * FSV)))[0]; -#else // !PRECALC_VARIANCE - MEAN_PACKED_TYPE inv_variance = (MEAN_PACKED_TYPE)(MEAN_VAL_ONE); -#endif - - if (b >= OUTPUT_BATCH_NUM || f >= OUTPUT_FEATURE_NUM) - return; - - const uint output_spatial = get_global_id(0); - uint x = output_spatial % OUTPUT_SIZE_X; - uint y = output_spatial / OUTPUT_SIZE_X; - uint input_offset = INPUT0_GET_INDEX(b, f, y, x); - uint output_offset = OUTPUT_GET_INDEX(b, f, y, x); - - INPUT_PACKED_TYPE in_pack = ((const __global INPUT_PACKED_TYPE*)(input + input_offset))[0]; - ACT_PACKED_TYPE normalized_vec = fma((TO_ACT_PACKED_TYPE(in_pack) - TO_ACT_PACKED_TYPE(mean_vals)), - TO_ACT_PACKED_TYPE(inv_variance), (ACT_PACKED_TYPE)0); - OUTPUT_PACKED_TYPE result_vec = OUTPUT_VAL_ZERO; - - unroll_for (uint fi = 0; fi < FSV; fi++) { - ACTIVATION_TYPE normalized = normalized_vec[fi]; -# if HAS_FUSED_OPS - FUSED_OPS; - result_vec[fi] = FUSED_OPS_RESULT; -# else - result_vec[fi] = TO_OUTPUT_TYPE(normalized); -# endif - } - - vstore16(result_vec, 0, &output[output_offset]); -} - -#elif MVN_KERNEL_MEAN_VAR_BSV32 - -// Mean: -DECLARE_PACKED_ACCUMULATE(accumulate_sum_input, ACCUMULATOR_TYPE, INPUT0_TYPE, FSV, INPUT_SLICE_PITCH, ITEMS_NUM, LWS, ACCUMULATE_SUM) - -#define CALC_MEAN(sum) ((sum) / ITEMS_NUM) -#if SG_NUM != 1 -DECLARE_WG_PACKED_REDUCE_ADD(reduce_mean, MEAN_TYPE, FSV, SG_NUM, CALC_MEAN) -#else -DECLARE_SG_PACKED_REDUCE_ADD(reduce_mean, MEAN_TYPE, FSV, CALC_MEAN) -#endif - -// Variance: -#define EXTRA_ARGS_DECL_IMPL , MEAN_TYPE mean -#define EXTRA_ARGS_IMPL , mean -#define EXTRA_ARGS_DECL EXTRA_ARGS_DECL_IMPL -#define EXTRA_ARGS EXTRA_ARGS_IMPL -#define ACCUMULATE_SUM_SQ_DEV(curr, next, idx, mean) ACCUMULATE_SUM_SQ(curr, TO_MEAN_TYPE(next) - _sub_group_shuffle(mean, idx), idx) -DECLARE_PACKED_ACCUMULATE_EARGS(accumulate_sum_sq_dev, MEAN_TYPE, INPUT0_TYPE, FSV, INPUT_SLICE_PITCH, ITEMS_NUM, LWS, ACCUMULATE_SUM_SQ_DEV, EXTRA_ARGS_DECL, EXTRA_ARGS) - +#if IS_DYNAMIC #if defined EPS_OUTSIDE_SQRT - #define CALC_INVERSE_VARIANCE(sum_diff_sq) native_powr(native_sqrt((sum_diff_sq) / ITEMS_NUM) + (MEAN_TYPE)EPSILON, (MEAN_TYPE)-1.f); + inv_variance = native_powr(native_sqrt(inv_variance / items_num) + (MEAN_TYPE)EPSILON, (MEAN_TYPE)-1.f); #elif defined EPS_INSIDE_SQRT - #define CALC_INVERSE_VARIANCE(sum_diff_sq) native_powr((sum_diff_sq) / ITEMS_NUM + (MEAN_TYPE)EPSILON, (MEAN_TYPE)-0.5f) -#endif -#if SG_NUM != 1 -DECLARE_WG_PACKED_REDUCE_ADD(reduce_inverse_variance, MEAN_TYPE, FSV, SG_NUM, CALC_INVERSE_VARIANCE) -#else -DECLARE_SG_PACKED_REDUCE_ADD(reduce_inverse_variance, MEAN_TYPE, FSV, CALC_INVERSE_VARIANCE) -#endif - -REQD_SUB_GROUP_SIZE(SIMD) -__attribute__((reqd_work_group_size(LWS, 1, 1))) -KERNEL(mvn_mean_var_bsv32)( - const __global INPUT0_TYPE* input, - __global MEAN_TYPE* means -#if NORMALIZE_VARIANCE - , __global MEAN_TYPE* variances -#endif -) { - uint b = get_global_id(2); - uint f = get_global_id(1) * FSV; - uint flat_data_set_group = b * CEIL_DIV(OUTPUT_FEATURE_NUM, FSV) + get_global_id(1); - - const uint sgid = get_sub_group_id(); - const uint sglid = get_sub_group_local_id(); - const uint data_sets_offset = INPUT0_GET_INDEX(b, f, 0, 0); - - ACC_PACKED_TYPE partial_sum = FUNC_CALL(accumulate_sum_input)(input, data_sets_offset, get_local_id(0)); -#if SG_NUM != 1 - __local MEAN_TYPE slm_mean_acc[(SG_NUM - 1) * FSV]; - MEAN_TYPE mean = FUNC_CALL(reduce_mean)(TO_MEAN_PACKED_TYPE(partial_sum), slm_mean_acc); -#else - MEAN_TYPE mean = FUNC_CALL(reduce_mean)(TO_MEAN_PACKED_TYPE(partial_sum)); + inv_variance = native_powr(inv_variance / items_num + (MEAN_TYPE)EPSILON, (MEAN_TYPE)-0.5f); #endif - -#if NORMALIZE_VARIANCE - MEAN_PACKED_TYPE partial_dev = FUNC_CALL(accumulate_sum_sq_dev)(input, data_sets_offset, get_local_id(0), mean); - #if SG_NUM != 1 - __local MEAN_TYPE slm_var_acc[(SG_NUM - 1) * FSV]; - MEAN_TYPE inv_variance = FUNC_CALL(reduce_inverse_variance)(partial_dev, slm_var_acc); - #else - MEAN_TYPE inv_variance = FUNC_CALL(reduce_inverse_variance)(partial_dev); - #endif #endif if (sgid == 0 && (sglid < FSV || SIMD == FSV)) { - means[flat_data_set_group * FSV + sglid] = mean; - #if NORMALIZE_VARIANCE - variances[flat_data_set_group * FSV + sglid] = inv_variance; - #endif + intermidiate_ivar[flat_data_set_group * FSV + sglid] = inv_variance; } } @@ -386,14 +349,22 @@ KERNEL(mvn_mean_var_bsv32)( #else // MVN_KERNEL_MAIN // Mean: -DECLARE_PACKED_ACCUMULATE(accumulate_sum_input, int, INPUT0_TYPE, FSV, INPUT_SLICE_PITCH, ITEMS_NUM, LWS, ACCUMULATE_SUM) - +#if IS_DYNAMIC +DECLARE_PACKED_ACCUMULATE_DYN(accumulate_sum_input, ACCUMULATOR_TYPE, INPUT0_TYPE, FSV, INPUT_SLICE_PITCH, LWS, ACCUMULATE_SUM) +#if SG_NUM != 1 +DECLARE_WG_PACKED_REDUCE_ADD(reduce_mean, MEAN_TYPE, FSV, SG_NUM, REDUCE_NO_POST_OP) +#else +DECLARE_SG_PACKED_REDUCE_ADD(reduce_mean, MEAN_TYPE, FSV, REDUCE_NO_POST_OP) +#endif +#else +DECLARE_PACKED_ACCUMULATE(accumulate_sum_input, ACCUMULATOR_TYPE, INPUT0_TYPE, FSV, INPUT_SLICE_PITCH, ITEMS_NUM, LWS, ACCUMULATE_SUM) #define CALC_MEAN(sum) ((sum) / ITEMS_NUM) #if SG_NUM != 1 DECLARE_WG_PACKED_REDUCE_ADD(reduce_mean, MEAN_TYPE, FSV, SG_NUM, CALC_MEAN) #else DECLARE_SG_PACKED_REDUCE_ADD(reduce_mean, MEAN_TYPE, FSV, CALC_MEAN) #endif +#endif // Variance: #define EXTRA_ARGS_DECL_IMPL , MEAN_TYPE mean @@ -401,8 +372,15 @@ DECLARE_SG_PACKED_REDUCE_ADD(reduce_mean, MEAN_TYPE, FSV, CALC_MEAN) #define EXTRA_ARGS_DECL EXTRA_ARGS_DECL_IMPL #define EXTRA_ARGS EXTRA_ARGS_IMPL #define ACCUMULATE_SUM_SQ_DEV(curr, next, idx, mean) ACCUMULATE_SUM_SQ(curr, next - _sub_group_shuffle(mean, idx), idx) +#if IS_DYNAMIC +DECLARE_PACKED_ACCUMULATE_DYN_EARGS(accumulate_sum_sq_dev, MEAN_TYPE, INPUT0_TYPE, FSV, INPUT_SLICE_PITCH, LWS, ACCUMULATE_SUM_SQ_DEV, EXTRA_ARGS_DECL, EXTRA_ARGS) +#if SG_NUM != 1 +DECLARE_WG_PACKED_REDUCE_ADD(reduce_inverse_variance, MEAN_TYPE, FSV, SG_NUM, REDUCE_NO_POST_OP) +#else +DECLARE_SG_PACKED_REDUCE_ADD(reduce_inverse_variance, MEAN_TYPE, FSV, REDUCE_NO_POST_OP) +#endif +#else DECLARE_PACKED_ACCUMULATE_EARGS(accumulate_sum_sq_dev, MEAN_TYPE, INPUT0_TYPE, FSV, INPUT_SLICE_PITCH, ITEMS_NUM, LWS, ACCUMULATE_SUM_SQ_DEV, EXTRA_ARGS_DECL, EXTRA_ARGS) - #if defined EPS_OUTSIDE_SQRT #define CALC_INVERSE_VARIANCE(sum_diff_sq) native_powr(native_sqrt((sum_diff_sq) / ITEMS_NUM) + (MEAN_TYPE)EPSILON, (MEAN_TYPE)-1.f); #elif defined EPS_INSIDE_SQRT @@ -413,14 +391,21 @@ DECLARE_WG_PACKED_REDUCE_ADD(reduce_inverse_variance, MEAN_TYPE, FSV, SG_NUM, CA #else DECLARE_SG_PACKED_REDUCE_ADD(reduce_inverse_variance, MEAN_TYPE, FSV, CALC_INVERSE_VARIANCE) #endif +#endif +#if TYPE_SIZE(INPUT0_TYPE) == 1 #define INPUT_PACKED_BLOCK_READ(ptr) BLOCK_READN(INPUT0_TYPE, FSV, ptr, 0) - -#define OUTPUT_PAD_IN_ITEMS (OUTPUT_PAD_BEFORE_SIZE_X != 0 || OUTPUT_PAD_AFTER_SIZE_X != 0 || OUTPUT_PAD_BEFORE_SIZE_Y != 0) +#else +// sub_group_block_read supports max 8 elements for ushort/uint types, so split into two 8-wide reads +#define INPUT_PACKED_BLOCK_READ(ptr) ((INPUT_PACKED_TYPE)(BLOCK_READN(INPUT0_TYPE, 8, ptr, 0), BLOCK_READN(INPUT0_TYPE, 8, ptr, 8 * SIMD))) +#endif REQD_SUB_GROUP_SIZE(SIMD) +#if !IS_DYNAMIC __attribute__((reqd_work_group_size(LWS, 1, 1))) +#endif KERNEL(mvn_final)( + OPTIONAL_SHAPE_INFO_ARG const __global INPUT0_TYPE* input, __global OUTPUT_TYPE* restrict output #if HAS_FUSED_OPS_DECLS @@ -432,14 +417,25 @@ KERNEL(mvn_final)( #if PRECALC_VARIANCE , const __global MEAN_TYPE* variances #endif +#if IS_DYNAMIC + , uint items_num +#endif ) { uint b = get_global_id(2); uint f = get_global_id(1) * FSV; uint flat_data_set_group = b * CEIL_DIV(OUTPUT_FEATURE_NUM, FSV) + get_global_id(1); -#if GWS != LWS +#if IS_DYNAMIC + // In dynamic mode the JIT constant GWS may be a compile-time placeholder + // that differs from the actual dispatch global work-size set at runtime. + // Use get_global_size(0) to obtain the true runtime value. uint items_group = get_group_id(0); + const uint effective_gws = get_global_size(0); +#elif GWS != LWS + uint items_group = get_group_id(0); + const uint effective_gws = GWS; #else uint items_group = 0; + const uint effective_gws = GWS; #endif const uint sgid = get_sub_group_id() + items_group * SG_NUM; const uint sglid = get_sub_group_local_id(); @@ -454,32 +450,50 @@ KERNEL(mvn_final)( #if PRECALC_MEAN MEAN_TYPE mean = means[flat_data_set_group * FSV + sglid]; #else - INT_PACKED_TYPE partial_sum = FUNC_CALL(accumulate_sum_input)(input, data_sets_offset, get_local_id(0)); +#if IS_DYNAMIC + ACC_PACKED_TYPE partial_sum = FUNC_CALL(accumulate_sum_input)(input, data_sets_offset, get_local_id(0), items_num); +#else + ACC_PACKED_TYPE partial_sum = FUNC_CALL(accumulate_sum_input)(input, data_sets_offset, get_local_id(0)); +#endif # if SG_NUM != 1 __local MEAN_TYPE slm_mean_acc[(SG_NUM - 1) * FSV]; MEAN_TYPE mean = FUNC_CALL(reduce_mean)(TO_MEAN_PACKED_TYPE(partial_sum), slm_mean_acc); # else MEAN_TYPE mean = FUNC_CALL(reduce_mean)(TO_MEAN_PACKED_TYPE(partial_sum)); # endif +#if IS_DYNAMIC + mean = mean / items_num; +#endif #endif #if NORMALIZE_VARIANCE # if PRECALC_VARIANCE MEAN_TYPE inv_variance = variances[flat_data_set_group * FSV + sglid]; # else +#if IS_DYNAMIC + MEAN_PACKED_TYPE partial_dev = FUNC_CALL(accumulate_sum_sq_dev)(input, data_sets_offset, get_local_id(0), items_num, mean); +#else MEAN_PACKED_TYPE partial_dev = FUNC_CALL(accumulate_sum_sq_dev)(input, data_sets_offset, get_local_id(0), mean); +#endif # if SG_NUM != 1 __local MEAN_TYPE slm_var_acc[(SG_NUM - 1) * FSV]; MEAN_TYPE inv_variance = FUNC_CALL(reduce_inverse_variance)(partial_dev, slm_var_acc); # else MEAN_TYPE inv_variance = FUNC_CALL(reduce_inverse_variance)(partial_dev); # endif +#if IS_DYNAMIC +#if defined EPS_OUTSIDE_SQRT + inv_variance = native_powr(native_sqrt(inv_variance / items_num) + (MEAN_TYPE)EPSILON, (MEAN_TYPE)-1.f); +#elif defined EPS_INSIDE_SQRT + inv_variance = native_powr(inv_variance / items_num + (MEAN_TYPE)EPSILON, (MEAN_TYPE)-0.5f); +#endif +#endif # endif #else MEAN_TYPE inv_variance = 1; #endif -#if OUTPUT_IS_FP +#if OUTPUT_IS_FP && (INPUT_SLICE_PITCH == FSV) input_offset = data_sets_offset + sgid * SIMD * FSV; uint output_spatial_base = sgid * SIMD; #if OUTPUT_DIMS == 5 @@ -490,7 +504,7 @@ KERNEL(mvn_final)( // For fused ops to align with non-fp path const uint set_idx = sglid; - for (uint spatial_idx = 0; spatial_idx < ITEMS_NUM / GWS; ++spatial_idx) { + for (uint spatial_idx = 0; spatial_idx < ITEMS_NUM / effective_gws; ++spatial_idx) { INPUT_PACKED_TYPE in_pack = INPUT_PACKED_BLOCK_READ(input + input_offset); unroll_for(uint si = 0; si < SIMD; ++si) { @@ -498,6 +512,7 @@ KERNEL(mvn_final)( MEAN_TYPE normalized = (TO_MEAN_TYPE(in_pack[si]) - mean) * inv_variance; OUTPUT_TYPE result; # if HAS_FUSED_OPS + ACTIVATION_TYPE normalized_activation = TO_ACTIVATION_TYPE(normalized); FUSED_OPS; result = FUSED_OPS_RESULT; # else @@ -519,13 +534,13 @@ KERNEL(mvn_final)( DT_OUTPUT_BLOCK_WRITE(output, output_offset, result); #endif } - input_offset += GWS * FSV; - output_offset += GWS * FSV; - output_spatial_base += GWS; + input_offset += effective_gws * FSV; + output_offset += effective_gws * FSV; + output_spatial_base += effective_gws; } // [constexpr] Number of leftovers after full local work-group iterations. - const uint lws_uniform_leftovers = ITEMS_NUM % GWS; + const uint lws_uniform_leftovers = ITEMS_NUM % effective_gws; // [constexpr] Number of sub-groups that can process leftovers loading SIMD items. const uint lws_uniform_leftovers_full_simds = lws_uniform_leftovers / SIMD; // [constexpr] Number of leftovers after full sub-group processing. @@ -540,6 +555,7 @@ KERNEL(mvn_final)( MEAN_TYPE normalized = (TO_MEAN_TYPE(in_pack[si]) - mean) * inv_variance; OUTPUT_TYPE result; # if HAS_FUSED_OPS + ACTIVATION_TYPE normalized_activation = TO_ACTIVATION_TYPE(normalized); FUSED_OPS; result = FUSED_OPS_RESULT; # else @@ -602,6 +618,7 @@ KERNEL(mvn_final)( MEAN_TYPE normalized = (TO_MEAN_TYPE(in_pack[si]) - mean) * inv_variance; OUTPUT_TYPE result; # if HAS_FUSED_OPS + ACTIVATION_TYPE normalized_activation = TO_ACTIVATION_TYPE(normalized); FUSED_OPS; result = FUSED_OPS_RESULT; # else @@ -624,22 +641,24 @@ KERNEL(mvn_final)( #endif } } -#else // => !OUTPUT_IS_FP - input_offset = data_sets_offset + sgid * SIMD * FSV; +#else // => !OUTPUT_IS_FP || INPUT_SLICE_PITCH != FSV + // Per-spatial-position path: works for both int types and cross-layout (e.g. fsv32 in, fsv16 out) + input_offset = data_sets_offset + sgid * SIMD * INPUT_SLICE_PITCH; #if OUTPUT_DIMS == 5 - uint output_offset = OUTPUT_GET_INDEX(b, f, 0, 0, 0) + sgid * SIMD * FSV; + uint output_offset = OUTPUT_GET_INDEX(b, f, 0, 0, 0) + sgid * SIMD * OUTPUT_SLICE_PITCH; #else // OUTPUT_DIMS == 4 - uint output_offset = OUTPUT_GET_INDEX(b, f, 0, 0) + sgid * SIMD * FSV; + uint output_offset = OUTPUT_GET_INDEX(b, f, 0, 0) + sgid * SIMD * OUTPUT_SLICE_PITCH; #endif uint output_spatial = sgid * SIMD + sglid; - for (uint spatial_idx = 0; spatial_idx < ITEMS_NUM / GWS; ++spatial_idx) { - INPUT_PACKED_TYPE in_pack = ((const __global INPUT_PACKED_TYPE*)(input + input_offset))[sglid]; + for (uint spatial_idx = 0; spatial_idx < ITEMS_NUM / effective_gws; ++spatial_idx) { + INPUT_PACKED_TYPE in_pack = ((const __global INPUT_PACKED_TYPE*)(input + input_offset))[sglid * ISP_STRIDE]; OUTPUT_PACKED_TYPE result; unroll_for(uint set_idx = 0; set_idx < FSV; ++set_idx) { MEAN_TYPE normalized = (TO_MEAN_TYPE(in_pack[set_idx]) - _sub_group_shuffle(mean, set_idx)) * _sub_group_shuffle(inv_variance, set_idx); # if HAS_FUSED_OPS + ACTIVATION_TYPE normalized_activation = TO_ACTIVATION_TYPE(normalized); FUSED_OPS; result[set_idx] = FUSED_OPS_RESULT; # else @@ -647,7 +666,7 @@ KERNEL(mvn_final)( # endif } #if !OUTPUT_PAD_IN_ITEMS - ((__global OUTPUT_PACKED_TYPE*)(output + output_offset))[sglid] = result; + ((__global OUTPUT_PACKED_TYPE*)(output + output_offset))[sglid * OSP_STRIDE] = result; #else # if OUTPUT_DIMS == 5 uint z = output_spatial / (OUTPUT_SIZE_X * OUTPUT_SIZE_Y); @@ -662,13 +681,13 @@ KERNEL(mvn_final)( ((__global OUTPUT_PACKED_TYPE*)(output + output_offset))[0] = result; #endif - input_offset += GWS * FSV; - output_offset += GWS * FSV; - output_spatial += GWS; + input_offset += effective_gws * INPUT_SLICE_PITCH; + output_offset += effective_gws * OUTPUT_SLICE_PITCH; + output_spatial += effective_gws; } // [constexpr] Number of leftovers after full local work-group iterations. - const uint lws_uniform_leftovers = ITEMS_NUM % GWS; + const uint lws_uniform_leftovers = ITEMS_NUM % effective_gws; // [constexpr] Number of sub-groups that can process leftovers loading SIMD items. const uint lws_uniform_leftovers_full_simds = lws_uniform_leftovers / SIMD; // [constexpr] Number of leftovers after full sub-group processing. @@ -676,12 +695,13 @@ KERNEL(mvn_final)( if (lws_uniform_leftovers_full_simds > 0 && sgid < lws_uniform_leftovers_full_simds) { // Process leftovers that can use full sub-group. - INPUT_PACKED_TYPE in_pack = ((const __global INPUT_PACKED_TYPE*)(input + input_offset))[sglid]; + INPUT_PACKED_TYPE in_pack = ((const __global INPUT_PACKED_TYPE*)(input + input_offset))[sglid * ISP_STRIDE]; OUTPUT_PACKED_TYPE result; unroll_for(uint set_idx = 0; set_idx < FSV; ++set_idx) { MEAN_TYPE normalized = (TO_MEAN_TYPE(in_pack[set_idx]) - _sub_group_shuffle(mean, set_idx)) * _sub_group_shuffle(inv_variance, set_idx); # if HAS_FUSED_OPS + ACTIVATION_TYPE normalized_activation = TO_ACTIVATION_TYPE(normalized); FUSED_OPS; result[set_idx] = FUSED_OPS_RESULT; # else @@ -689,7 +709,7 @@ KERNEL(mvn_final)( # endif } #if !OUTPUT_PAD_IN_ITEMS - ((__global OUTPUT_PACKED_TYPE*)(output + output_offset))[sglid] = result; + ((__global OUTPUT_PACKED_TYPE*)(output + output_offset))[sglid * OSP_STRIDE] = result; #else # if OUTPUT_DIMS == 5 uint z = output_spatial / (OUTPUT_SIZE_X * OUTPUT_SIZE_Y); @@ -706,12 +726,13 @@ KERNEL(mvn_final)( } else if (lws_uniform_leftovers > 0 && sg_uniform_leftovers > 0 && sgid == lws_uniform_leftovers_full_simds) { // TODO: May be worth to consider the data here as across sub-group // Rest of leftovers, still use whole sub-group, but change addresses to not load extra data. - INPUT_PACKED_TYPE in_pack = ((const __global INPUT_PACKED_TYPE*)(input + input_offset))[sglid % sg_uniform_leftovers]; + INPUT_PACKED_TYPE in_pack = ((const __global INPUT_PACKED_TYPE*)(input + input_offset))[(sglid % sg_uniform_leftovers) * ISP_STRIDE]; OUTPUT_PACKED_TYPE result; unroll_for(uint set_idx = 0; set_idx < FSV; ++set_idx) { MEAN_TYPE normalized = (TO_MEAN_TYPE(in_pack[set_idx]) - _sub_group_shuffle(mean, set_idx)) * _sub_group_shuffle(inv_variance, set_idx); # if HAS_FUSED_OPS + ACTIVATION_TYPE normalized_activation = TO_ACTIVATION_TYPE(normalized); FUSED_OPS; result[set_idx] = FUSED_OPS_RESULT; # else @@ -720,7 +741,7 @@ KERNEL(mvn_final)( } if (sglid < sg_uniform_leftovers) { #if !OUTPUT_PAD_IN_ITEMS - ((__global OUTPUT_PACKED_TYPE*)(output + output_offset))[sglid] = result; + ((__global OUTPUT_PACKED_TYPE*)(output + output_offset))[sglid * OSP_STRIDE] = result; #else # if OUTPUT_DIMS == 5 uint z = output_spatial / (OUTPUT_SIZE_X * OUTPUT_SIZE_Y); @@ -744,7 +765,10 @@ KERNEL(mvn_final)( #undef FSV #undef INPUT_SLICE_PITCH +#undef OUTPUT_SLICE_PITCH #undef SG_NUM +#undef ISP_STRIDE +#undef OSP_STRIDE #undef INPUT_TYPE2 #undef INPUT_TYPE4 diff --git a/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/mvn_gpu_b_fs_yx_fsv16_imad_accumulate.cl b/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/mvn_gpu_b_fs_yx_fsv16_accumulate.cl similarity index 60% rename from src/plugins/intel_gpu/src/kernel_selector/cl_kernels/mvn_gpu_b_fs_yx_fsv16_imad_accumulate.cl rename to src/plugins/intel_gpu/src/kernel_selector/cl_kernels/mvn_gpu_b_fs_yx_fsv16_accumulate.cl index 1fd6720de0af..d4a941daf724 100644 --- a/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/mvn_gpu_b_fs_yx_fsv16_imad_accumulate.cl +++ b/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/mvn_gpu_b_fs_yx_fsv16_accumulate.cl @@ -94,3 +94,49 @@ inline MAKE_VECTOR_TYPE(AccT, SliceSize) FUNC(Name)(const __global InputT* input #define DECLARE_PACKED_ACCUMULATE(Name, AccT, InputT, SliceSize, SlicePitch, Items, Workers, AccOp) \ DECLARE_PACKED_ACCUMULATE_EARGS(Name, AccT, InputT, SliceSize, SlicePitch, Items, Workers, AccOp, , ) + +// ============================================================================================================================== +// Dynamic variants — Items is a function parameter (uint) instead of a compile-time constant. +// Used in IS_DYNAMIC mode where spatial dimensions are not known at OpenCL compile time. +// ============================================================================================================================== + +#define DECLARE_PACKED_ACCUMULATE_DYN_EARGS(Name, AccT, InputT, SliceSize, SlicePitch, Workers, AccOp, ExtraArgsDecl, ExtraArgs) \ +inline MAKE_VECTOR_TYPE(AccT, SliceSize) FUNC(Name)(const __global InputT* input, \ + uint offset, \ + uint worker_id, \ + uint items_count \ + ExtraArgsDecl) { \ + typedef MAKE_VECTOR_TYPE(InputT, SliceSize) packed_in_t; \ + typedef MAKE_VECTOR_TYPE(AccT, SliceSize) packed_acc_t; \ + \ + packed_acc_t acc = 0; /* Accumulation variable */ \ + \ + uint input_offset = offset + worker_id * (SlicePitch); /* Current input offset */ \ + \ + for (uint spatial_idx = 0; spatial_idx < items_count / (Workers); ++spatial_idx) { \ + packed_in_t in_pack = ((const __global packed_in_t*)(input + input_offset))[0]; \ + \ + input_offset += (Workers) * (SlicePitch); \ + \ + __attribute__((opencl_unroll_hint)) \ + for (uint set_idx = 0; set_idx < (SliceSize); ++set_idx) { \ + acc[set_idx] = AccOp(acc[set_idx], in_pack[set_idx], set_idx ExtraArgs); \ + } \ + } \ + \ + const uint leftovers = items_count % (Workers); \ + \ + if (leftovers > 0 && worker_id < leftovers) { \ + packed_in_t in_pack = ((const __global packed_in_t*)(input + input_offset))[0]; \ + \ + __attribute__((opencl_unroll_hint)) \ + for (uint set_idx = 0; set_idx < (SliceSize); ++set_idx) { \ + acc[set_idx] = AccOp(acc[set_idx], in_pack[set_idx], set_idx ExtraArgs); \ + } \ + } \ + \ + return acc; \ +} + +#define DECLARE_PACKED_ACCUMULATE_DYN(Name, AccT, InputT, SliceSize, SlicePitch, Workers, AccOp) \ + DECLARE_PACKED_ACCUMULATE_DYN_EARGS(Name, AccT, InputT, SliceSize, SlicePitch, Workers, AccOp, , ) diff --git a/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/mvn_gpu_b_fs_yx_fsv16_imad_reduce.cl b/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/mvn_gpu_b_fs_yx_fsv16_reduce.cl similarity index 100% rename from src/plugins/intel_gpu/src/kernel_selector/cl_kernels/mvn_gpu_b_fs_yx_fsv16_imad_reduce.cl rename to src/plugins/intel_gpu/src/kernel_selector/cl_kernels/mvn_gpu_b_fs_yx_fsv16_reduce.cl diff --git a/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/permute_tile_8x8_4x4_fsv.cl b/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/permute_tile_8x8_4x4_fsv.cl index 56c9510ffdc1..0a06f7677909 100644 --- a/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/permute_tile_8x8_4x4_fsv.cl +++ b/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/permute_tile_8x8_4x4_fsv.cl @@ -102,7 +102,11 @@ KERNEL (permute_tile_8x8_4x4_fsv)( #else // !REORDERED_OUTPUT_TILED_ORDER if (F_NO_REMAINDER_CONDITION) { // read and transpose +#ifdef YZ_REMAINDER_CONDITION + unroll_for (uint lh = 0; lh < (((YZ_REMAINDER_CONDITION)) ? YZ_REMAINDER_SIZE : TILE_SIZE); ++lh) { +#else unroll_for (uint lh = 0; lh < TILE_SIZE; ++lh) { +#endif const uint input_idx = INPUT0_GET_TILED_INDEX(INPUT0_TILED_ORDER); INPUTVTYPE read_data = AS_INPUTVTYPE(VLOAD(0, input + input_idx)); @@ -153,7 +157,11 @@ KERNEL (permute_tile_8x8_4x4_fsv)( #ifdef F_REMAINDER_CONDITION else if (F_REMAINDER_CONDITION) { // read and transpose +#ifdef YZ_REMAINDER_CONDITION + unroll_for (uint lh = 0; lh < (((YZ_REMAINDER_CONDITION)) ? YZ_REMAINDER_SIZE : TILE_SIZE); ++lh) { +#else unroll_for (uint lh = 0; lh < TILE_SIZE; ++lh) { +#endif const uint input_idx = INPUT0_GET_TILED_INDEX(INPUT0_TILED_ORDER); INPUTVTYPE read_data = AS_INPUTVTYPE(VLOAD(0, input + input_idx)); unroll_for (uint lw = 0; lw < F_REMAINDER_SIZE; ++lw) { diff --git a/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/pooling_gpu_b_fs_zyx_fsv16_imad.cl b/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/pooling_gpu_b_fs_zyx_fsv16_imad.cl index da33801e9674..e5de237b92d5 100644 --- a/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/pooling_gpu_b_fs_zyx_fsv16_imad.cl +++ b/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/pooling_gpu_b_fs_zyx_fsv16_imad.cl @@ -274,6 +274,10 @@ KERNEL(pooling_gpu_b_fs_zyx_fsv16)( { int input_offset_z = offset_z + pz; bool zero_z = input_offset_z >= INPUT0_SIZE_Z || input_offset_z < 0; + // Clamp offsets to valid range so that masked-off SIMD lanes + // compute safe addresses (prevents OOB on Xe2+ where masked + // reads are no longer silently ignored). + uint safe_z = (uint)clamp(input_offset_z, 0, (int)(INPUT0_SIZE_Z - 1)); if(!zero_z) { __attribute__((opencl_unroll_hint(POOL_SIZE_Y))) @@ -281,6 +285,7 @@ KERNEL(pooling_gpu_b_fs_zyx_fsv16)( { int input_offset_y = offset_y + py; bool zero_y = input_offset_y >= INPUT0_SIZE_Y || input_offset_y < 0; + uint safe_y = (uint)clamp(input_offset_y, 0, (int)(INPUT0_SIZE_Y - 1)); if(!zero_y) { __attribute__((opencl_unroll_hint(POOL_SIZE_X))) @@ -288,9 +293,10 @@ KERNEL(pooling_gpu_b_fs_zyx_fsv16)( { int input_offset_x = offset_x + px; bool zero = input_offset_x >= INPUT0_SIZE_X || input_offset_x < 0; + uint safe_x = (uint)clamp(input_offset_x, 0, (int)(INPUT0_SIZE_X - 1)); if(!zero) { - const uint input_idx = batch_and_feature_offset + input_offset_z*IN_Z_PITCH + input_offset_y*IN_Y_PITCH + input_offset_x*IN_X_PITCH; + const uint input_idx = batch_and_feature_offset + safe_z*IN_Z_PITCH + safe_y*IN_Y_PITCH + safe_x*IN_X_PITCH; IN_VEC16 ch16_data; #if INPUT0_FEATURE_NUM % FEATURE_SLICE_SIZE != 0 if (!last_in_f_group) { diff --git a/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/pooling_gpu_bfyx_block_opt.cl b/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/pooling_gpu_bfyx_block_opt.cl index 0b60aef9a931..fc7db1eac793 100644 --- a/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/pooling_gpu_bfyx_block_opt.cl +++ b/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/pooling_gpu_bfyx_block_opt.cl @@ -53,12 +53,19 @@ KERNEL(pooling_gpu)( // we do max in "x" dimension for(uint j = 0; j < BLOCK_SIZE_Y; j++) { - for(uint i = 0; i < POOL_SIZE_X; i++) - { - max_x[j] = FUNC_CALL(apply_pooling)(max_x[j], TO_ACCUMULATOR_TYPE(input[input_idx])); - input_idx += INPUT0_X_PITCH; + // On Xe2+ OOB reads crash (CL_OUT_OF_RESOURCES) instead of returning zero. + // The last Y-block may extend past the input when output height is not + // divisible by POOL_SIZE_Y. Guard against reading beyond the input buffer. + if (offset_y + (int)j >= 0 && offset_y + (int)j < (int)INPUT0_SIZE_Y) { + for(uint i = 0; i < POOL_SIZE_X; i++) + { + max_x[j] = FUNC_CALL(apply_pooling)(max_x[j], TO_ACCUMULATOR_TYPE(input[input_idx])); + input_idx += INPUT0_X_PITCH; + } + input_idx += (INPUT0_Y_PITCH - POOL_SIZE_X*INPUT0_X_PITCH); + } else { + input_idx += INPUT0_Y_PITCH; } - input_idx += (INPUT0_Y_PITCH - POOL_SIZE_X*INPUT0_X_PITCH); } for(uint i = 0; i < POOL_SIZE_Y; i++) diff --git a/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/prior_box_ref.cl b/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/prior_box_ref.cl index 287bcc7d0468..f6e1c9918e01 100644 --- a/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/prior_box_ref.cl +++ b/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/prior_box_ref.cl @@ -186,9 +186,11 @@ KERNEL(ref) output[OUTPUT_GET_INDEX(1, i, 0, 0)] = VARIANCE[0]; } #elif VARIANCE_SIZE == 4 - for (uint i = start_out_index; i < out_index; ++i) { - for (uint j = 0; j < 4; ++j) { - output[OUTPUT_GET_INDEX(1, i * 4 + j, 0, 0)] = VARIANCE[j]; + // Each prior box produces exactly 4 coordinates (xmin, ymin, xmax, ymax), + // so (out_index - start_out_index) is always a multiple of 4. + for (uint i = start_out_index; i < out_index; i += 4) { + for (uint j = 0; j < 4 && (i + j) < out_index; ++j) { + output[OUTPUT_GET_INDEX(1, i + j, 0, 0)] = VARIANCE[j]; } } #else diff --git a/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/quantize_gpu_scale_shift_opt.cl b/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/quantize_gpu_scale_shift_opt.cl index 7dcc3fc474b7..2e5b836b2be4 100644 --- a/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/quantize_gpu_scale_shift_opt.cl +++ b/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/quantize_gpu_scale_shift_opt.cl @@ -28,6 +28,11 @@ KERNEL(quantize_gpu_scale_shift_opt)(OPTIONAL_SHAPE_INFO_ARG const int b = get_global_id(GWS_BATCH); const int of = get_global_id(GWS_FEATURE); +#if FEATURE_BLOCKED_FORMAT + if (of >= OUTPUT_FEATURE_NUM || b >= OUTPUT_BATCH_NUM) + return; +#endif + #if OUTPUT_DIMS <= 4 const int yx = get_global_id(GWS_YX); @@ -230,9 +235,6 @@ KERNEL(quantize_gpu_scale_shift_opt)(OPTIONAL_SHAPE_INFO_ARG // Common section with results writing // // *********************************** // -#if FEATURE_BLOCKED_FORMAT - if (of < OUTPUT_FEATURE_NUM) -#endif #if OUTPUT_IS_FP output[output_offset] = TO_OUTPUT_TYPE_SAT(val); #else diff --git a/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/reorder_data_b_fs_yx_fsv16_fsv32_to_bfyx.cl b/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/reorder_data_b_fs_yx_fsv16_fsv32_to_bfyx.cl index 2b7fe4c72fda..e78f2c385ab0 100644 --- a/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/reorder_data_b_fs_yx_fsv16_fsv32_to_bfyx.cl +++ b/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/reorder_data_b_fs_yx_fsv16_fsv32_to_bfyx.cl @@ -101,11 +101,7 @@ KERNEL (reorder_data_b_fs_yx_fsv16_fsv32_to_bfyx)( #else const uint sgid_remainder = sub_group_id % 2; - // read const uint input_idx_final = input_idx_tile + sgid_remainder * (DEFAULT_STRIDE * DEFAULT_TILE_SIZE); - INPUTVTYPE read_data = AS_INPUTVTYPE(_sub_group_block_read8((const __global uint*)(input) + input_idx_final)); - INPUTVTYPE_HALF read_half1 = {read_data[0], read_data[2], read_data[4], read_data[6]}; - INPUTVTYPE_HALF read_half2 = {read_data[1], read_data[3], read_data[5], read_data[7]}; // write const uint output_idx = OUTPUT_GET_TILED_INDEX(OUTPUT_TILED_ORDER); @@ -120,32 +116,31 @@ KERNEL (reorder_data_b_fs_yx_fsv16_fsv32_to_bfyx)( if (X_REMAINDER_CONDITION) { const int nloop = X_REMAINDER_SIZE - (TILE_SIZE * sgid_remainder); for (int i = 0 ; i < min(nloop, TILE_SIZE); i++) { - output[output_idx_final + i] = TO_OUTPUT_TYPE(read_half1[i]); + const uint input_idx_remainder = input_idx_final + (uint)i * (DEFAULT_STRIDE * 2); + output[output_idx_final + i] = TO_OUTPUT_TYPE( + AS_INPUT0_TYPE(_sub_group_block_read((const __global uint*)(input) + input_idx_remainder))); #ifdef F_REMAINDER_SIZE if ((f + DEFAULT_STRIDE) < OUTPUT_FEATURE_NUM) #endif { - output[output_idx_final + i + (OUTPUT_FEATURE_PITCH * DEFAULT_STRIDE)] = TO_OUTPUT_TYPE(read_half2[i]); + output[output_idx_final + i + (OUTPUT_FEATURE_PITCH * DEFAULT_STRIDE)] = TO_OUTPUT_TYPE( + AS_INPUT0_TYPE(_sub_group_block_read((const __global uint*)(input) + input_idx_remainder + DEFAULT_STRIDE))); } } - } else { + } else + #endif + { + INPUTVTYPE read_data = AS_INPUTVTYPE(_sub_group_block_read8((const __global uint*)(input) + input_idx_final)); + INPUTVTYPE_HALF read_half1 = {read_data[0], read_data[2], read_data[4], read_data[6]}; + INPUTVTYPE_HALF read_half2 = {read_data[1], read_data[3], read_data[5], read_data[7]}; VSTORE(TO_OUTPUTVTYPE(read_half1), 0, output + output_idx_final); #ifdef F_REMAINDER_SIZE - if ((f + DEFAULT_STRIDE) < OUTPUT_FEATURE_NUM) + if((f + DEFAULT_STRIDE) < OUTPUT_FEATURE_NUM) #endif { VSTORE(TO_OUTPUTVTYPE(read_half2), 0, output + output_idx_final + (OUTPUT_FEATURE_PITCH * DEFAULT_STRIDE)); } } - #else - VSTORE(TO_OUTPUTVTYPE(read_half1), 0, output + output_idx_final); - #ifdef F_REMAINDER_SIZE - if((f + DEFAULT_STRIDE) < OUTPUT_FEATURE_NUM) - #endif - { - VSTORE(TO_OUTPUTVTYPE(read_half2), 0, output + output_idx_final + (OUTPUT_FEATURE_PITCH * DEFAULT_STRIDE)); - } - #endif } #endif } diff --git a/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/reorder_data_bfyx_to_blocked_format.cl b/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/reorder_data_bfyx_to_blocked_format.cl index 504fd3738367..0c89254ecad6 100644 --- a/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/reorder_data_bfyx_to_blocked_format.cl +++ b/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/reorder_data_bfyx_to_blocked_format.cl @@ -51,6 +51,7 @@ } KERNEL (reorder_data_bfyx_to_blocked_format)( + OPTIONAL_SHAPE_INFO_ARG const __global INPUT0_TYPE* input, __global OUTPUT_TYPE* output ) diff --git a/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/reorder_data_fast_b1.cl b/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/reorder_data_fast_b1.cl index bc3e8f1dfa8e..d6812ff87cb8 100644 --- a/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/reorder_data_fast_b1.cl +++ b/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/reorder_data_fast_b1.cl @@ -122,8 +122,13 @@ KERNEL (reorder_data_fast_b1)( const uint b = data_idx - tmp_data_idx * OUTPUT_BATCH_NUM; data_idx = tmp_data_idx; +#if defined FILL_FEATURE_PADDING + tmp_data_idx = data_idx / PADDED_FEATURE_NUM; + const uint f = data_idx - tmp_data_idx * PADDED_FEATURE_NUM; +#else tmp_data_idx = data_idx / OUTPUT_FEATURE_NUM; const uint f = data_idx - tmp_data_idx * OUTPUT_FEATURE_NUM; +#endif data_idx = tmp_data_idx; tmp_data_idx = data_idx / OUTPUT_SIZE_X; @@ -160,7 +165,7 @@ KERNEL (reorder_data_fast_b1)( tmp_data_idx = data_idx / OUTPUT_SIZE_W; const uint w = data_idx - tmp_data_idx * OUTPUT_SIZE_W; -#else // BYXF? +#else // BYXF or blocked formats (b_fs_yx_fsv16, etc.) uint tmp_data_idx = data_idx / OUTPUT_BATCH_NUM; const uint b = data_idx - tmp_data_idx * OUTPUT_BATCH_NUM; data_idx = tmp_data_idx; @@ -173,13 +178,37 @@ KERNEL (reorder_data_fast_b1)( const uint x = data_idx - tmp_data_idx * OUTPUT_SIZE_X; data_idx = tmp_data_idx; +#if defined FILL_FEATURE_PADDING + tmp_data_idx = data_idx / PADDED_FEATURE_NUM; + const uint f = data_idx - tmp_data_idx * PADDED_FEATURE_NUM; +#else tmp_data_idx = data_idx / OUTPUT_FEATURE_NUM; const uint f = data_idx - tmp_data_idx * OUTPUT_FEATURE_NUM; +#endif const uint z = 0; const uint w = 0; #endif #endif +#if defined FILL_FEATURE_PADDING + // For blocked output formats with unaligned features, zero-fill padding positions. + // This prevents NaN propagation when pooled/reused memory contains NaN values, + // since NaN * 0 = NaN in IEEE 754. + // Cannot use get_output_index(b,f,...) because OUTPUT_GET_INDEX may be JIT-optimized + // to a constant when the tensor is scalar (LogicalSize()==1), or use clamping that + // wraps out-of-range feature values. Use OUTPUT_GET_INDEX_RAW which always calls the + // actual layout-specific index function. + if (f >= OUTPUT_FEATURE_NUM) { +#if defined OUTPUT_LAYOUT_B_FS_ZYX_FSV16 + const uint output_idx = OUTPUT_GET_INDEX_RAW(b, f, z, y, x); +#else + const uint output_idx = OUTPUT_GET_INDEX_RAW(b, f, y, x); +#endif + output[output_idx] = TO_OUTPUT_REORDER_TYPE(0); + return; + } +#endif + #if CHANGE_DATA_TYPE_ONLY const uint input_idx = data_idx; const uint output_idx = data_idx; diff --git a/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/reorder_data_fsv.cl b/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/reorder_data_fsv.cl new file mode 100644 index 000000000000..5189de74b627 --- /dev/null +++ b/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/reorder_data_fsv.cl @@ -0,0 +1,102 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// +// Reorder kernel for single-blocked fsv format conversions. +// Converts between b_fs_{yx,zyx}_fsv{4,8,16,32} formats with different fsv sizes. +// Both input and output are blocked only on the feature dimension. + +#include "include/batch_headers/fetch_data.cl" + +#ifdef FSV_VECTORIZED + #define INPUT_VEC_TYPE MAKE_VECTOR_TYPE(INPUT0_TYPE, VEC_SIZE) + #define OUTPUT_VEC_TYPE MAKE_VECTOR_TYPE(OUTPUT_TYPE, VEC_SIZE) + #define CONVERT_OUT_VEC CAT(convert_, OUTPUT_VEC_TYPE) + #define VLOAD_VEC CAT(vload, VEC_SIZE) + #define VSTORE_VEC CAT(vstore, VEC_SIZE) +#endif + +KERNEL (reorder_data_fsv)( + OPTIONAL_SHAPE_INFO_ARG + const __global INPUT0_TYPE* input, + __global OUTPUT_TYPE* output) +{ + const uint x = (uint)get_global_id(0); + const uint yz = (uint)get_global_id(1); + const uint b_fs = (uint)get_global_id(2); + + const uint fs_out = b_fs % OUT_FEATURE_SLICE_NUM; + const uint b = b_fs / OUT_FEATURE_SLICE_NUM; + +#ifdef INPUT0_DIMS_5 + const uint z = yz % INPUT0_SIZE_Z; + const uint y = yz / INPUT0_SIZE_Z; +#else + const uint y = yz; + const uint z = 0; +#endif + + const uint f_base = fs_out * OUT_FSV; + + const uint in_x_pitch = IN_FSV; + const uint in_y_pitch = in_x_pitch * (INPUT0_PAD_BEFORE_SIZE_X + INPUT0_SIZE_X + INPUT0_PAD_AFTER_SIZE_X); +#ifdef INPUT0_DIMS_5 + const uint in_z_pitch = in_y_pitch * (INPUT0_PAD_BEFORE_SIZE_Y + INPUT0_SIZE_Y + INPUT0_PAD_AFTER_SIZE_Y); + const uint in_fs_pitch = in_z_pitch * (INPUT0_PAD_BEFORE_SIZE_Z + INPUT0_SIZE_Z + INPUT0_PAD_AFTER_SIZE_Z); +#else + const uint in_fs_pitch = in_y_pitch * (INPUT0_PAD_BEFORE_SIZE_Y + INPUT0_SIZE_Y + INPUT0_PAD_AFTER_SIZE_Y); +#endif + const uint in_b_pitch = in_fs_pitch * IN_FEATURE_SLICE_NUM; + +#ifdef INPUT0_DIMS_5 + const uint in_spatial_base = (b + INPUT0_PAD_BEFORE_BATCH_NUM) * in_b_pitch + (INPUT0_PAD_BEFORE_FEATURE_NUM / IN_FSV) * in_fs_pitch + (z + INPUT0_PAD_BEFORE_SIZE_Z) * in_z_pitch + (y + INPUT0_PAD_BEFORE_SIZE_Y) * in_y_pitch + (x + INPUT0_PAD_BEFORE_SIZE_X) * IN_FSV; +#define OUT_INDEX(f) OUTPUT_GET_INDEX(b, (f), z, y, x) +#else + const uint in_spatial_base = (b + INPUT0_PAD_BEFORE_BATCH_NUM) * in_b_pitch + (INPUT0_PAD_BEFORE_FEATURE_NUM / IN_FSV) * in_fs_pitch + (y + INPUT0_PAD_BEFORE_SIZE_Y) * in_y_pitch + (x + INPUT0_PAD_BEFORE_SIZE_X) * IN_FSV; +#define OUT_INDEX(f) OUTPUT_GET_INDEX(b, (f), y, x) +#endif + +#ifdef FSV_VECTORIZED + // Vectorized path: read/write VEC_SIZE elements at a time. + // RATIO = max(IN_FSV, OUT_FSV) / min(IN_FSV, OUT_FSV) + #if (OUT_FSV > IN_FSV) + // Upscale: e.g. fsv16 -> fsv32. Read RATIO input slices into one output slice. + unroll_for (uint r = 0; r < RATIO; ++r) { + const uint fs_in = fs_out * RATIO + r; + const uint f_chunk = f_base + r * VEC_SIZE; + if (f_chunk < INPUT0_FEATURE_NUM) { + INPUT_VEC_TYPE v = VLOAD_VEC(0, input + in_spatial_base + fs_in * in_fs_pitch); + VSTORE_VEC(CONVERT_OUT_VEC(v), 0, output + OUT_INDEX(f_chunk)); + } else { + OUTPUT_VEC_TYPE zero = (OUTPUT_VEC_TYPE)(0); + VSTORE_VEC(zero, 0, output + OUT_INDEX(f_chunk)); + } + } + #else + // Downscale: e.g. fsv32 -> fsv16. Read one chunk from a larger input slice. + const uint fs_in = fs_out / RATIO; + const uint sub = fs_out % RATIO; + if (f_base < INPUT0_FEATURE_NUM) { + INPUT_VEC_TYPE v = VLOAD_VEC(0, input + in_spatial_base + fs_in * in_fs_pitch + sub * VEC_SIZE); + VSTORE_VEC(CONVERT_OUT_VEC(v), 0, output + OUT_INDEX(f_base)); + } else { + OUTPUT_VEC_TYPE zero = (OUTPUT_VEC_TYPE)(0); + VSTORE_VEC(zero, 0, output + OUT_INDEX(f_base)); + } + #endif +#else + // Scalar fallback for small fsv sizes (fsv4, fsv8) or non-power-of-2 ratios. + for (uint i = 0; i < OUT_FSV; ++i) { + const uint f = f_base + i; + if (f < INPUT0_FEATURE_NUM) { + const uint fs_in = f / IN_FSV; + const uint fsv_in = f % IN_FSV; + const uint in_offset = in_spatial_base + fs_in * in_fs_pitch + fsv_in; + output[OUT_INDEX(f)] = ACTIVATION(TO_OUTPUT_TYPE(input[in_offset]), ACTIVATION_PARAMS); + } else { + output[OUT_INDEX(f)] = TO_OUTPUT_TYPE(0); + } + } +#endif + +#undef OUT_INDEX +} diff --git a/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/reorder_fs_b_yx_fsv32_to_bfyx.cl b/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/reorder_fs_b_yx_fsv32_to_bfyx.cl index 8b4647555eb7..8b7f751e0e15 100644 --- a/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/reorder_fs_b_yx_fsv32_to_bfyx.cl +++ b/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/reorder_fs_b_yx_fsv32_to_bfyx.cl @@ -23,7 +23,14 @@ KERNEL (reorder_fs_b_yx_fsv32_to_bfyx)( __attribute__((opencl_unroll_hint(X_BLOCK_SIZE))) for (int i = 0; i < X_BLOCK_SIZE; i++) { +#if defined(LEFTOVERS_OX) + if (x + i < INPUT0_SIZE_X) + in_data[sglid * X_BLOCK_SIZE + i] = input[in_idx + (i * FSV) + sglid]; + else + in_data[sglid * X_BLOCK_SIZE + i] = 0; +#else in_data[sglid * X_BLOCK_SIZE + i] = input[in_idx + (i * FSV) + sglid]; +#endif } __attribute__((opencl_unroll_hint(X_BLOCK_SIZE))) diff --git a/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/reorder_weights_opt.cl b/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/reorder_weights_opt.cl index 4f9848dd96c8..a320e53df461 100644 --- a/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/reorder_weights_opt.cl +++ b/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/reorder_weights_opt.cl @@ -49,8 +49,13 @@ KERNEL(reorder_weights_opt)(const __global INPUT0_TYPE* input, __global OUTPUT_T const int g_io = get_global_id(0); #if OSV_FIRST #if OUTPUT_GROUPED +#if defined(IFM_PADDING) + const int i = (g_io % (IFM_PADDED_NUM / SECOND_BLOCK_SIZE)) * SECOND_BLOCK_SIZE; + const int g = (g_io / (IFM_PADDED_NUM / SECOND_BLOCK_SIZE)); +#else const int i = (g_io % (OUTPUT_IFM_NUM / SECOND_BLOCK_SIZE)) * SECOND_BLOCK_SIZE; const int g = (g_io / (OUTPUT_IFM_NUM / SECOND_BLOCK_SIZE)); +#endif #else const int i = g_io * SECOND_BLOCK_SIZE; #endif // OUTPUT_GROUPED @@ -81,29 +86,61 @@ KERNEL(reorder_weights_opt)(const __global INPUT0_TYPE* input, __global OUTPUT_T int input_idx = GET_INDEX(INPUT0, IDX_ORDER); const int output_idx = GET_INDEX(OUTPUT, BLOCK_IDX_ORDER); +#if OUTPUT_LEFTOVERS +#if OSV_FIRST + const bool valid_lane = o < OUTPUT_OFM_NUM; +#else + const bool valid_lane = i < OUTPUT_IFM_NUM; +#endif +#else + const bool valid_lane = true; +#endif + + // For blocked formats with IFM padding (e.g., isv16 with IFM=3), skip input reads + // for padding positions and write zero. This prevents NaN in weight padding from + // propagating through convolution (NaN * 0 = NaN in IEEE 754). +#if defined(IFM_PADDING) && OSV_FIRST + const bool ifm_valid = (i < ACTUAL_IFM_NUM); +#else + const bool ifm_valid = true; +#endif + #if SECOND_BLOCK_SIZE == 1 - const OUTPUT_TYPE val = TO_OUTPUT_TYPE(input[input_idx]); + OUTPUT_TYPE val; +#if defined(IFM_PADDING) && OSV_FIRST + if (ifm_valid && valid_lane) { + val = TO_OUTPUT_TYPE(input[input_idx]); + } else { + val = (OUTPUT_TYPE)0; + } +#else + val = valid_lane ? TO_OUTPUT_TYPE(input[input_idx]) : (OUTPUT_TYPE)0; +#endif #else OUTPUT_VEC_TYPE val = 0; unroll_for (int b = 0; b < SECOND_BLOCK_SIZE; b++) { - val[b] = TO_OUTPUT_TYPE(input[input_idx]); +#if defined(IFM_PADDING) && OSV_FIRST + if (valid_lane && (i + b) < ACTUAL_IFM_NUM) { + val[b] = TO_OUTPUT_TYPE(input[input_idx]); + } +#else + val[b] = valid_lane ? TO_OUTPUT_TYPE(input[input_idx]) : (OUTPUT_TYPE)0; +#endif input_idx += PITCH; } #endif // SECOND_BLOCK_SIZE == 1 #if OUTPUT_LEFTOVERS #if OSV_FIRST - const bool doWrite = o < OUTPUT_OFM_NUM; if (o_blocked >= OUTPUT_OFM_NUM - FIRST_BLOCK_SIZE) { #else - const bool doWrite = i < OUTPUT_IFM_NUM; if (i_blocked >= OUTPUT_IFM_NUM - FIRST_BLOCK_SIZE) { #endif // OSV_FIRST #if SECOND_BLOCK_SIZE > 1 unroll_for(int b = 0; b < SECOND_BLOCK_SIZE; b++) - if (doWrite) + if (valid_lane) output[output_idx + b * SECOND_SIZE + lid] = val[b]; #else - if (doWrite) + if (valid_lane) output[output_idx + lid] = val; #endif // SECOND_BLOCK_SIZE > 1 } diff --git a/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/resample_onnx.cl b/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/resample_onnx.cl index f615fd5bc038..91869aba8f0f 100644 --- a/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/resample_onnx.cl +++ b/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/resample_onnx.cl @@ -184,27 +184,36 @@ KERNEL (resample_onnx)(__global INPUT0_TYPE* input, bool brOutOfBounds = in_y2 < 0 || in_y2 >= in_size[3] || in_x2 < 0 || in_x2 >= in_size[4]; #endif // PADDING_USED == 1 -#if OUTPUT_DIMS == 5 +#if PADDING_USED == 1 + // Clamp coordinates to valid range before reading to avoid OOB memory access + // (Xe2+ raises CL_OUT_OF_RESOURCES on OOB reads instead of returning zero) + int safe_y1 = clamp(in_y1, 0, (int)INPUT0_SIZE_Y - 1); + int safe_y2 = clamp(in_y2, 0, (int)INPUT0_SIZE_Y - 1); + int safe_x1 = clamp(in_x1, 0, (int)INPUT0_SIZE_X - 1); + int safe_x2 = clamp(in_x2, 0, (int)INPUT0_SIZE_X - 1); + #if OUTPUT_DIMS == 5 + acc_vec_t top_left = tlOutOfBounds ? INPUT0_VAL_ZERO : TO_ACC_VEC_TYPE(READ_FUNC(input, INPUT0_GET_INDEX(b, feature_block, z, safe_y1, safe_x1))); + acc_vec_t top_right = trOutOfBounds ? INPUT0_VAL_ZERO : TO_ACC_VEC_TYPE(READ_FUNC(input, INPUT0_GET_INDEX(b, feature_block, z, safe_y1, safe_x2))); + acc_vec_t bottom_left = blOutOfBounds ? INPUT0_VAL_ZERO : TO_ACC_VEC_TYPE(READ_FUNC(input, INPUT0_GET_INDEX(b, feature_block, z, safe_y2, safe_x1))); + acc_vec_t bottom_right = brOutOfBounds ? INPUT0_VAL_ZERO : TO_ACC_VEC_TYPE(READ_FUNC(input, INPUT0_GET_INDEX(b, feature_block, z, safe_y2, safe_x2))); + #else + acc_vec_t top_left = tlOutOfBounds ? INPUT0_VAL_ZERO : TO_ACC_VEC_TYPE(READ_FUNC(input, INPUT0_GET_INDEX(b, feature_block, safe_y1, safe_x1))); + acc_vec_t top_right = trOutOfBounds ? INPUT0_VAL_ZERO : TO_ACC_VEC_TYPE(READ_FUNC(input, INPUT0_GET_INDEX(b, feature_block, safe_y1, safe_x2))); + acc_vec_t bottom_left = blOutOfBounds ? INPUT0_VAL_ZERO : TO_ACC_VEC_TYPE(READ_FUNC(input, INPUT0_GET_INDEX(b, feature_block, safe_y2, safe_x1))); + acc_vec_t bottom_right = brOutOfBounds ? INPUT0_VAL_ZERO : TO_ACC_VEC_TYPE(READ_FUNC(input, INPUT0_GET_INDEX(b, feature_block, safe_y2, safe_x2))); + #endif +#else + #if OUTPUT_DIMS == 5 acc_vec_t top_left = TO_ACC_VEC_TYPE(READ_FUNC(input, INPUT0_GET_INDEX(b, feature_block, z, in_y1, in_x1))); acc_vec_t top_right = TO_ACC_VEC_TYPE(READ_FUNC(input, INPUT0_GET_INDEX(b, feature_block, z, in_y1, in_x2))); acc_vec_t bottom_left = TO_ACC_VEC_TYPE(READ_FUNC(input, INPUT0_GET_INDEX(b, feature_block, z, in_y2, in_x1))); acc_vec_t bottom_right = TO_ACC_VEC_TYPE(READ_FUNC(input, INPUT0_GET_INDEX(b, feature_block, z, in_y2, in_x2))); -#else + #else acc_vec_t top_left = TO_ACC_VEC_TYPE(READ_FUNC(input, INPUT0_GET_INDEX(b, feature_block, in_y1, in_x1))); acc_vec_t top_right = TO_ACC_VEC_TYPE(READ_FUNC(input, INPUT0_GET_INDEX(b, feature_block, in_y1, in_x2))); acc_vec_t bottom_left = TO_ACC_VEC_TYPE(READ_FUNC(input, INPUT0_GET_INDEX(b, feature_block, in_y2, in_x1))); acc_vec_t bottom_right = TO_ACC_VEC_TYPE(READ_FUNC(input, INPUT0_GET_INDEX(b, feature_block, in_y2, in_x2))); -#endif - -#if PADDING_USED == 1 - if (tlOutOfBounds) - top_left = TO_OUT_VEC_TYPE(INPUT0_VAL_ZERO); - if (trOutOfBounds) - top_right = TO_OUT_VEC_TYPE(INPUT0_VAL_ZERO); - if (blOutOfBounds) - bottom_left = TO_OUT_VEC_TYPE(INPUT0_VAL_ZERO); - if (brOutOfBounds) - bottom_right = TO_OUT_VEC_TYPE(INPUT0_VAL_ZERO); + #endif #endif // PADDING_USED == 1 acc_vec_t res = TO_ACC_VEC_TYPE(dx2 * dy2 * top_left) + TO_ACC_VEC_TYPE(dx1 * dy2 * top_right) + diff --git a/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/rms_gpu_bfyx_opt.cl b/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/rms_gpu_bfyx_opt.cl index 53efd93775e8..771df857aeb7 100644 --- a/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/rms_gpu_bfyx_opt.cl +++ b/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/rms_gpu_bfyx_opt.cl @@ -43,7 +43,7 @@ KERNEL(rms_gpu_bfyx_opt)( const uint items_num = data_size / workers_per_data; const uint leftovers = data_size % workers_per_data; - #if HAS_DYNAMIC_PADDING + #if HAS_PADDING uint b_idx = 0; uint f_idx = 0; uint z_idx = 0; diff --git a/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/scatter_elements_update_ref.cl b/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/scatter_elements_update_ref.cl index 9835c33861a9..f146cf224b24 100644 --- a/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/scatter_elements_update_ref.cl +++ b/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/scatter_elements_update_ref.cl @@ -204,6 +204,9 @@ KERNEL(scatter_elements_update_ref)(OPTIONAL_SHAPE_INFO_ARG #endif #if REDUCE_MODE != 0 , __global INPUT1_TYPE* output_fp +#if REDUCE_MODE == MEAN_MODE + , __global INPUT1_TYPE* count_k +#endif #if ITER >= 1 #ifdef NO_LOCAL_MEMORY , __global INPUT1_TYPE* reduction_v @@ -211,9 +214,6 @@ KERNEL(scatter_elements_update_ref)(OPTIONAL_SHAPE_INFO_ARG , __local INPUT1_TYPE* reduction_v , __local INPUT1_TYPE* reduction_thread #endif -#if REDUCE_MODE == MEAN_MODE - , __global INPUT1_TYPE* count_k -#endif #endif #endif ) @@ -253,8 +253,17 @@ KERNEL(scatter_elements_update_ref)(OPTIONAL_SHAPE_INFO_ARG output[output_idx] = ACTIVATION(val, ACTIVATION_PARAMS); #endif #else - INPUT1_TYPE val_fp = FUNC_CALL(to_fixed_point)(val); - output_fp[output_idx] = val_fp; + #if REDUCE_MODE == MEAN_MODE && USE_INIT_VAL == 0 + // For MEAN with USE_INIT_VAL=0: init to neutral so only scattered updates + // contribute to the sum. Non-scattered positions restored in ITER=2. + output_fp[output_idx] = FUNC_CALL(to_fixed_point)(REDUCTION_NEUTRAL_VALUE); + #else + INPUT1_TYPE val_fp = FUNC_CALL(to_fixed_point)(val); + output_fp[output_idx] = val_fp; + #endif + #if REDUCE_MODE == MEAN_MODE + count_k[output_idx] = INPUT1_VAL_ZERO; + #endif #endif #elif ITER == 1 @@ -292,21 +301,19 @@ KERNEL(scatter_elements_update_ref)(OPTIONAL_SHAPE_INFO_ARG #endif reduction_v[output_idx] = FUNC_CALL(to_fixed_point)(REDUCTION_NEUTRAL_VALUE); - #if REDUCE_MODE == MEAN_MODE - count_k[output_idx] = INPUT1_VAL_ZERO; - #endif - barrier(CLK_LOCAL_MEM_FENCE); int val_fixed = FUNC_CALL(to_fixed_point)(val); #ifndef NO_LOCAL_MEMORY INPUT1_TYPE write_thread = FUNC_CALL(count_add_local)(&reduction_thread[output_idx], 1); - if (write_thread == 0) { - #if USE_INIT_VAL == 0 - output_fp[output_idx] = FUNC_CALL(to_fixed_point)(REDUCTION_NEUTRAL_VALUE); - #endif - } + #if REDUCE_MODE != MEAN_MODE + if (write_thread == 0) { + #if USE_INIT_VAL == 0 + output_fp[output_idx] = FUNC_CALL(to_fixed_point)(REDUCTION_NEUTRAL_VALUE); + #endif + } + #endif if (reduction_thread[output_idx] > 1) { FUNC_CALL(atomic_reduce_local)(&reduction_v[output_idx], val_fixed); } else { @@ -327,7 +334,7 @@ KERNEL(scatter_elements_update_ref)(OPTIONAL_SHAPE_INFO_ARG FUNC_CALL(count_add_global)(&count_k[output_idx], INPUT1_VAL_ONE); #endif - #if USE_INIT_VAL == 0 + #if REDUCE_MODE != MEAN_MODE && USE_INIT_VAL == 0 output_fp[output_idx] = FUNC_CALL(to_fixed_point)(REDUCTION_NEUTRAL_VALUE); #endif @@ -400,8 +407,15 @@ KERNEL(scatter_elements_update_ref)(OPTIONAL_SHAPE_INFO_ARG #endif #if REDUCE_MODE == MEAN_MODE - if (count_k[output_idx] != 0) + if (count_k[output_idx] != 0) { val /= (count_k[output_idx] + USE_INIT_VAL); + } + #if USE_INIT_VAL == 0 + else { + // Position was not scattered to, keep original data + val = TO_OUTPUT_TYPE(data[input_idx]); + } + #endif #endif #if HAS_FUSED_OPS diff --git a/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/scatter_update_ref.cl b/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/scatter_update_ref.cl index c63f35036e0c..2934e920c283 100644 --- a/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/scatter_update_ref.cl +++ b/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/scatter_update_ref.cl @@ -13,6 +13,9 @@ #define GET_OUTPUT_INDEX(idx_order) OUTPUT_GET_INDEX(idx_order) #define GET_INPUT_INDEX(idx_order) INPUT0_GET_INDEX(idx_order) +#define GET_UPDATES_INDEX(idx_order) UPDATES_GET_INDEX(idx_order) + +#define INDICES_SIZE INPUT1_LENGTH #if OUTPUT_DIMS == 4 #define ORDER b,f,y,x @@ -22,7 +25,7 @@ #define ORDER b,f,w,z,y,x #endif -#ifdef BLOCKED_LAYOUT +#ifdef USE_LAYOUT_AWARE_INDEXING inline void FUNC(planar_to_bfyx)(const uint planar_index, const uint batch_num, const uint channel_num, const uint height, const uint width, uint* dst_b, uint* dst_f, uint* dst_y, uint* dst_x) @@ -85,7 +88,7 @@ inline void FUNC(planar_to_bfwzyx)(const uint planar_index, *dst_x = dst_xy % width; } #endif // INPUT2_DIMS -#endif // BLOCKED_LAYOUT +#endif // USE_LAYOUT_AWARE_INDEXING KERNEL(scatter_update_ref)(OPTIONAL_SHAPE_INFO_ARG const __global INPUT0_TYPE* dictionary, @@ -187,7 +190,7 @@ KERNEL(scatter_update_ref)(OPTIONAL_SHAPE_INFO_ARG #endif #endif - #ifdef BLOCKED_LAYOUT + #ifdef USE_LAYOUT_AWARE_INDEXING const uint planar_axis_idx = OUTPUT_INDEX_ON_AXIS; uint b_b, b_f, b_w, b_z, b_y, b_x; FUNC_CALL(planar_to_bfyx)(planar_axis_idx, INPUT1_BATCH_NUM, INPUT1_FEATURE_NUM, INPUT1_SIZE_Y, INPUT1_SIZE_X, @@ -200,7 +203,7 @@ KERNEL(scatter_update_ref)(OPTIONAL_SHAPE_INFO_ARG const uint output_idx = GET_OUTPUT_INDEX(SECOND_ITER_OUTPUT_INDEX_ORDER); - #ifdef BLOCKED_LAYOUT + #ifdef USE_LAYOUT_AWARE_INDEXING const uint planar_updates_idx = GET_UPDATES_INDEX(UPDATES_INDEX_ORDER); #if INPUT2_DIMS == 4 @@ -235,6 +238,8 @@ KERNEL(scatter_update_ref)(OPTIONAL_SHAPE_INFO_ARG #endif } +#undef INDICES_SIZE +#undef GET_UPDATES_INDEX #undef GET_OUTPUT_INDEX #undef GET_INPUT_INDEX #undef ORDER diff --git a/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/softmax_gpu_bf.cl b/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/softmax_gpu_bf.cl index 900528a79fbb..5d9e96d031d8 100644 --- a/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/softmax_gpu_bf.cl +++ b/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/softmax_gpu_bf.cl @@ -252,7 +252,7 @@ KERNEL (softmax_gpu_continuous_bfyx)( { ACTIVATION_TYPE dequantized = my_chunk[output_idx] / my_sum; FUSED_OPS_MAIN; - output[aligned_data_offset + get_sub_group_local_id() + i * get_sub_group_size()] = FUSED_OPS_RESULT_MAIN; + output[aligned_data_offset + get_sub_group_local_id() + output_idx * get_sub_group_size()] = FUSED_OPS_RESULT_MAIN; } if (in_data_set_idx < aligned_offset) @@ -433,7 +433,7 @@ KERNEL (softmax_gpu_continuous_bfyx)( { ACTIVATION_TYPE dequantized = output[aligned_data_offset + get_sub_group_local_id() + output_idx * get_sub_group_size()] / my_sum; FUSED_OPS_MAIN; - output[aligned_data_offset + get_sub_group_local_id() + i * get_sub_group_size()] = FUSED_OPS_RESULT_MAIN; + output[aligned_data_offset + get_sub_group_local_id() + output_idx * get_sub_group_size()] = FUSED_OPS_RESULT_MAIN; } if (in_data_set_idx < aligned_offset) diff --git a/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/swiglu_gpu_opt.cl b/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/swiglu_gpu_opt.cl index 7ef372a410f2..9772b5d6ca73 100644 --- a/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/swiglu_gpu_opt.cl +++ b/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/swiglu_gpu_opt.cl @@ -65,8 +65,20 @@ KERNEL(swiglu_gpu_opt)( ACCUMULATOR_TYPE value = input[y + GLU_STRIDE]; #endif #else - ACCUMULATOR_TYPE gate = input[y + GLU_STRIDE]; + #if GLU_STRIDE == 2 + // alternating mode: pair is at positions y, y+1 — gate is the odd neighbor + ACCUMULATOR_TYPE gate = input[y + 1]; + #else + ACCUMULATOR_TYPE gate = input[y + GLU_STRIDE]; + #endif ACCUMULATOR_TYPE value = input[y]; +#endif +#ifdef SCALE_FACTOR + // Restore original scale before clamp / swish / up_add_val so that + // clamp bounds and UP_ADD_VAL stay in the original (unscaled) range. + const ACCUMULATOR_TYPE scale_factor = SCALE_FACTOR; + gate *= scale_factor; + value *= scale_factor; #endif #if GLU_TYPE == 0 // Swish #if defined(CLAMP_MAX) && defined(CLAMP_MIN) @@ -80,6 +92,9 @@ KERNEL(swiglu_gpu_opt)( gate = (GEGLU_HALF * gate * (ACCUMULATOR_VAL_ONE + (tanh(GEGLU_SQUARE_2_OVER_PI * gate * (ACCUMULATOR_VAL_ONE + GEGLU_MULT * gate * gate))))); #endif value = (value + UP_ADD_VAL) * gate; +#ifdef SCALE_FACTOR + value /= scale_factor; +#endif output[x] = TO_OUTPUT_TYPE(value); } diff --git a/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/swiglu_gpu_ref.cl b/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/swiglu_gpu_ref.cl index 08a8e8799a73..b577c9bbab1c 100644 --- a/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/swiglu_gpu_ref.cl +++ b/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/swiglu_gpu_ref.cl @@ -64,6 +64,13 @@ KERNEL(swiglu_gpu_ref)( #endif ACCUMULATOR_TYPE gate = (ACCUMULATOR_TYPE) input[gate_idx]; ACCUMULATOR_TYPE up = (ACCUMULATOR_TYPE) input[input_idx]; +#ifdef SCALE_FACTOR + // Restore original scale before clamp / swish / up_add_val so that + // clamp bounds and UP_ADD_VAL stay in the original (unscaled) range. + const ACCUMULATOR_TYPE scale_factor = SCALE_FACTOR; + gate *= scale_factor; + up *= scale_factor; +#endif #if GLU_TYPE == 0 // Swish #if defined(CLAMP_MIN) && defined(CLAMP_MAX) gate = ACCUMULATOR_MIN_FUNC(TO_OUTPUT_TYPE(CLAMP_MAX), gate); @@ -76,6 +83,9 @@ KERNEL(swiglu_gpu_ref)( gate = (GEGLU_HALF * gate * (ACCUMULATOR_VAL_ONE + (tanh(GEGLU_SQUARE_2_OVER_PI * gate * (ACCUMULATOR_VAL_ONE + GEGLU_MULT * gate * gate))))); #endif ACCUMULATOR_TYPE res = ((ACCUMULATOR_TYPE)up + UP_ADD_VAL) * gate; +#ifdef SCALE_FACTOR + res /= scale_factor; +#endif output[output_idx] = TO_OUTPUT_TYPE(res); } diff --git a/src/plugins/intel_gpu/src/kernel_selector/common_types.h b/src/plugins/intel_gpu/src/kernel_selector/common_types.h index b7dbdf3b09cf..0f0c11712f33 100644 --- a/src/plugins/intel_gpu/src/kernel_selector/common_types.h +++ b/src/plugins/intel_gpu/src/kernel_selector/common_types.h @@ -197,7 +197,8 @@ enum class ActivationFunction { GELU, GELU_TANH, ROUND_HALF_TO_EVEN, - ROUND_HALF_AWAY_FROM_ZERO + ROUND_HALF_AWAY_FROM_ZERO, + ERFINV }; //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -320,7 +321,8 @@ enum class EltwiseMode { LEFT_SHIFT, BITWISE_AND, BITWISE_OR, - BITWISE_XOR + BITWISE_XOR, + ATAN2 }; //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/src/plugins/intel_gpu/src/kernel_selector/jitter.cpp b/src/plugins/intel_gpu/src/kernel_selector/jitter.cpp index 9344d8057175..d0112f65aecf 100644 --- a/src/plugins/intel_gpu/src/kernel_selector/jitter.cpp +++ b/src/plugins/intel_gpu/src/kernel_selector/jitter.cpp @@ -43,6 +43,11 @@ class JitTerm { return jit_term; } + JitTerm lt(const JitTerm& rhs) const { + JitTerm jit_term {"(" + text + "<" + rhs.str() + ")"}; + return jit_term; + } + private: std::string text; }; @@ -103,6 +108,16 @@ JitTerm log(const JitTerm& arg) { return jit_term; } +JitTerm sqrt(const JitTerm& arg) { + JitTerm jit_term{"(sqrt(" + arg.str() + "))"}; + return jit_term; +} + +JitTerm fabs(const JitTerm& arg) { + JitTerm jit_term{"(fabs(" + arg.str() + "))"}; + return jit_term; +} + JitTerm operator"" _jit(const char* str, size_t) { JitTerm jit_term{str}; return jit_term; @@ -369,7 +384,7 @@ JitDefinitions DataTensorJitConstant::GetDefinitions() const { _tensor.GetLayout() == DataLayout::bfzyx || _tensor.GetLayout() == DataLayout::bfwzyx || _tensor.GetLayout() == DataLayout::bfuwzyx || _tensor.GetLayout() == DataLayout::bfvuwzyx || _tensor.GetLayout() == DataLayout::b_fs_yx_fsv16 || _tensor.GetLayout() == DataLayout::b_fs_yx_fsv32 || - _tensor.GetLayout() == DataLayout::b_fs_zyx_fsv16) { + _tensor.GetLayout() == DataLayout::b_fs_zyx_fsv16 || _tensor.GetLayout() == DataLayout::b_fs_zyx_fsv32) { definitions.push_back({_name + "_X_PITCH", "1"}); definitions.push_back({_name + "_Y_PITCH", dims_padded.x()}); definitions.push_back({_name + "_Z_PITCH", toVectorMulString({dims_padded.x(), dims_padded.y()})}); @@ -1368,6 +1383,50 @@ JitConstants MakeActivationJitConstants(ActivationFunction activation_function, case ActivationFunction::ROUND_HALF_AWAY_FROM_ZERO: jitConstants.AddConstant(MakeJitConstant(macro_def, "(round(input))")); break; + case ActivationFunction::ERFINV: { + // NOTE: exactly the same implementation can be found + // in fused_ops_jitter.cpp - ideally both should be defined + // in common place, but that would require deeper refactoring + // (e.g. class JitTerm is also defined in multiple places and + // it is a different implementation in different jitters) + // which is out of scope for the current change.... + const bool is_f32 = (out_dt == Datatype::F32); + const std::string type_suffix = is_f32 ? "f" : "h"; + const JitTerm elem_inf{is_f32 ? "INFINITY" : "((half)INFINITY)"}; + auto cf = [&](const char* lit) { return JitTerm{lit + type_suffix}; }; + auto horner = [&](const JitTerm& s, + std::initializer_list coefs) { + auto it = coefs.begin(); + JitTerm r = *it++; + for (; it != coefs.end(); ++it) { + r = r * s + *it; + } + return r; + }; + const JitTerm& x = input; + const JitTerm w = neg(log((cf("1.0") - x) * (cf("1.0") + x))); + const JitTerm s_lo = w - cf("2.5"); + const JitTerm s_hi = sqrt(w) - cf("3.0"); + const JitTerm p_lo = horner( + s_lo, + {cf("2.81022636e-08"), cf("3.43273939e-07"), cf("-3.5233877e-06"), + cf("-4.39150654e-06"), cf("0.00021858087"), cf("-0.00125372503"), + cf("-0.00417768164"), cf("0.246640727"), cf("1.50140941")}); + const JitTerm p_hi = horner( + s_hi, + {cf("-0.000200214257"), cf("0.000100950558"), cf("0.00134934322"), + cf("-0.00367342844"), cf("0.00573950773"), cf("-0.0076224613"), + cf("-0.00943887047"), cf("1.00167406"), cf("2.83297682")}); + const JitTerm poly = x * ternary(w.lt(cf("5.0")), p_lo, p_hi); + // x = 0 -> poly yields 0 naturally. + // |x| > 1 -> log of a negative produces NaN, propagated by poly. + // x = +/-1 -> log(0) blows up the polynomial; force +/-inf via x * + // INFINITY. + jitConstants.AddConstant(MakeJitConstant( + macro_def, + ternary(fabs(x).eq(cf("1.0")), x * elem_inf, poly).str())); + break; + } case ActivationFunction::NONE: default: jitConstants.AddConstant(MakeJitConstant(macro_def, "input")); diff --git a/src/plugins/intel_gpu/src/kernel_selector/kernel_selector_common.cpp b/src/plugins/intel_gpu/src/kernel_selector/kernel_selector_common.cpp index d43b75e81c75..4d5d9e8a000a 100644 --- a/src/plugins/intel_gpu/src/kernel_selector/kernel_selector_common.cpp +++ b/src/plugins/intel_gpu/src/kernel_selector/kernel_selector_common.cpp @@ -83,6 +83,7 @@ std::string toString(ActivationFunction activation) { case ActivationFunction::GELU_TANH: method = "GELU_TANH"; break; case ActivationFunction::ROUND_HALF_TO_EVEN: method = "ROUND_HALF_TO_EVEN"; break; case ActivationFunction::ROUND_HALF_AWAY_FROM_ZERO: method = "ROUND_HALF_AWAY_FROM_ZERO"; break; + case ActivationFunction::ERFINV: method = "ERFINV"; break; default: break; } return method; @@ -208,6 +209,7 @@ std::string toString(EltwiseMode b_mode) { case EltwiseMode::SQRT: return "SQRT"; case EltwiseMode::RSQRT: return "RSQRT"; case EltwiseMode::ASSIGN: return "ASSIGN"; + case EltwiseMode::ATAN2: return "ATAN2"; default: return ""; } } diff --git a/src/plugins/intel_gpu/src/kernel_selector/kernel_selector_params.h b/src/plugins/intel_gpu/src/kernel_selector/kernel_selector_params.h index 25c98da4058d..98faaa15f14c 100644 --- a/src/plugins/intel_gpu/src/kernel_selector/kernel_selector_params.h +++ b/src/plugins/intel_gpu/src/kernel_selector/kernel_selector_params.h @@ -366,9 +366,7 @@ enum class gpu_arch { xe_hpc = 6, xe2 = 7, xe3 = 8, - xe3p_35_10 = 9, - xe3p_35_11 = 10, - xe3p_unknown = 11, + xe3p = 9, }; diff --git a/src/plugins/intel_gpu/src/kernel_selector/kernels/arg_max_min/arg_max_min_kernel_axis.cpp b/src/plugins/intel_gpu/src/kernel_selector/kernels/arg_max_min/arg_max_min_kernel_axis.cpp index 6d4a247cdb80..63733dbf1f01 100644 --- a/src/plugins/intel_gpu/src/kernel_selector/kernels/arg_max_min/arg_max_min_kernel_axis.cpp +++ b/src/plugins/intel_gpu/src/kernel_selector/kernels/arg_max_min/arg_max_min_kernel_axis.cpp @@ -114,6 +114,28 @@ ArgMaxMinKernelBase::DispatchData ArgMaxMinKernelAxis::SetDefault(const arg_max_ return dispatchData; } +namespace { +std::vector get_internal_buffer_sizes(const arg_max_min_params& params) { + const size_t elem_size = params.inputs[0].ElementSize(); + const size_t iav_type_size = Align(elem_size + 4, 4); + const size_t sort_size = getSortSize(params); + const size_t ops_size = getOperationNumber(params); + const size_t group_size = params.topK >= 8 ? params.topK : 8; + + if (0 == sort_size || 0 == ops_size) { + return {0, 0, 0}; + } + const size_t first_buff_size = (params.topK == 1) ? iav_type_size * sort_size * 2 : iav_type_size * sort_size * ops_size * 2; + + const size_t group_num = (sort_size + group_size - 1) / group_size; + const size_t second_buff_size = 4 * group_num * ops_size * 2; + + const size_t third_buff_size = Align(ops_size * group_num, elem_size); + + return {first_buff_size, second_buff_size, third_buff_size}; +} +} // namespace + void ArgMaxMinKernelAxis::GetUpdateDispatchDataFunc(KernelData& kd) const { kd.update_dispatch_data_func = [this](const Params& params, KernelData& kd) { const auto& prim_params = static_cast(params); @@ -123,17 +145,7 @@ void ArgMaxMinKernelAxis::GetUpdateDispatchDataFunc(KernelData& kd) const { kd.kernels[0].params.workGroups.local = dispatchData.lws; kd.kernels[0].skip_execution = KernelData::SkipKernelExecution(prim_params); - const size_t elem_size = prim_params.inputs[0].ElementSize(); - const size_t iav_type_size = Align(elem_size + 4, 4); - const size_t sort_size = getSortSize(prim_params); - const size_t ops_size = getOperationNumber(prim_params); - const size_t group_size = prim_params.topK >= 8 ? prim_params.topK : 8; - const size_t group_num = ((sort_size - 1) / group_size) + 1; - - kd.internalBuffers.clear(); - kd.internalBuffers.push_back(iav_type_size * sort_size * ops_size * 2); - kd.internalBuffers.push_back(4 * group_num * ops_size * 2); - kd.internalBuffers.push_back(ops_size * elem_size); + kd.internalBuffers = get_internal_buffer_sizes(prim_params); kd.internalBufferDataType = prim_params.inputs[0].GetDType(); }; } @@ -172,9 +184,7 @@ KernelsData ArgMaxMinKernelAxis::GetKernelsData(const Params& params) const { kernel.params.arguments.push_back({ArgumentDescriptor::Types::INTERNAL_BUFFER, 0}); kernel.params.arguments.push_back({ArgumentDescriptor::Types::INTERNAL_BUFFER, 1}); kernel.params.arguments.push_back({ArgumentDescriptor::Types::INTERNAL_BUFFER, 2}); - kd.internalBuffers.push_back(orgParams.inputs[0].PhysicalSizeInBytes()); - kd.internalBuffers.push_back(orgParams.inputs[0].PhysicalSizeInBytes()); - kd.internalBuffers.push_back(orgParams.inputs[0].PhysicalSizeInBytes()); + kd.internalBuffers = get_internal_buffer_sizes(orgParams); kd.internalBufferDataType = orgParams.inputs[0].GetDType(); } diff --git a/src/plugins/intel_gpu/src/kernel_selector/kernels/concatenation/concatenation_kernel_simple_ref.cpp b/src/plugins/intel_gpu/src/kernel_selector/kernels/concatenation/concatenation_kernel_simple_ref.cpp index d4781f41ec89..8c9caf798f97 100644 --- a/src/plugins/intel_gpu/src/kernel_selector/kernels/concatenation/concatenation_kernel_simple_ref.cpp +++ b/src/plugins/intel_gpu/src/kernel_selector/kernels/concatenation/concatenation_kernel_simple_ref.cpp @@ -36,6 +36,8 @@ ParamsKey ConcatenationKernel_simple_Ref::GetSupportedKey() const { k.EnableOutputLayout(DataLayout::bfwzyx); k.EnableInputLayout(DataLayout::b_fs_zyx_fsv16); k.EnableOutputLayout(DataLayout::b_fs_zyx_fsv16); + k.EnableInputLayout(DataLayout::b_fs_zyx_fsv32); + k.EnableOutputLayout(DataLayout::b_fs_zyx_fsv32); k.EnableInputLayout(DataLayout::bs_fs_zyx_bsv16_fsv16); k.EnableOutputLayout(DataLayout::bs_fs_zyx_bsv16_fsv16); k.EnableInputLayout(DataLayout::bs_fs_yx_bsv16_fsv16); @@ -68,8 +70,10 @@ bool ConcatenationKernel_simple_Ref::Validate(const Params& p) const { auto same_layout = params.inputs[0].GetLayout(); for (const auto& lt : params.inputs) { auto cur_layout = lt.GetLayout(); - if ((cur_layout == DataLayout::bfzyx || cur_layout == DataLayout::b_fs_zyx_fsv16 || cur_layout == DataLayout::bs_fs_zyx_bsv16_fsv16) && - (same_layout == DataLayout::bfzyx || same_layout == DataLayout::b_fs_zyx_fsv16 || same_layout == DataLayout::bs_fs_zyx_bsv16_fsv16 + if ((cur_layout == DataLayout::bfzyx || cur_layout == DataLayout::b_fs_zyx_fsv16 || + cur_layout == DataLayout::b_fs_zyx_fsv32 || cur_layout == DataLayout::bs_fs_zyx_bsv16_fsv16) && + (same_layout == DataLayout::bfzyx || same_layout == DataLayout::b_fs_zyx_fsv16 || + same_layout == DataLayout::b_fs_zyx_fsv32 || same_layout == DataLayout::bs_fs_zyx_bsv16_fsv16 || same_layout == DataLayout::bs_fs_yx_bsv32_fsv32)) { continue; } else if (cur_layout != same_layout) { diff --git a/src/plugins/intel_gpu/src/kernel_selector/kernels/convolution/convolution_kernel_b_fs_yx_fsv16_1x1.cpp b/src/plugins/intel_gpu/src/kernel_selector/kernels/convolution/convolution_kernel_b_fs_yx_fsv16_1x1.cpp index 5f380d974816..6585352900c5 100644 --- a/src/plugins/intel_gpu/src/kernel_selector/kernels/convolution/convolution_kernel_b_fs_yx_fsv16_1x1.cpp +++ b/src/plugins/intel_gpu/src/kernel_selector/kernels/convolution/convolution_kernel_b_fs_yx_fsv16_1x1.cpp @@ -2,7 +2,6 @@ // SPDX-License-Identifier: Apache-2.0 // -#include #include "convolution_kernel_b_fs_yx_fsv16_1x1.h" #include "kernel_selector_utils.h" #include @@ -273,10 +272,10 @@ JitConstants ConvolutionKernel_b_fs_yx_fsv16_1x1::GetJitConstants(const convolut DimensionAccessHelperJit output_dims(params.outputs[0]); DimensionAccessHelperJit output_padded_dims(params.outputs[0], true); - const auto padded_input = "(" + input0_padded_dims.x_pad().first + "+" + input0_padded_dims.x_pad().first + ") != 0"; + const auto padded_input = "(" + input0_padded_dims.x_pad().first + "+" + input0_padded_dims.x_pad().second + ") != 0"; jit.AddConstant(MakeJitConstant("PADDED_INPUT", padded_input)); - const auto padded_output = "(" + output_padded_dims.x_pad().first + "+" + output_padded_dims.x_pad().first + ") != 0"; + const auto padded_output = "(" + output_padded_dims.x_pad().first + "+" + output_padded_dims.x_pad().second + ") != 0"; jit.AddConstant(MakeJitConstant("PADDED_OUTPUT", padded_output)); // In shape agnostic kernel, the fused shape cannot be specified at build time or run time. diff --git a/src/plugins/intel_gpu/src/kernel_selector/kernels/convolution/convolution_kernel_bfyx_os_iyx_osv16.cpp b/src/plugins/intel_gpu/src/kernel_selector/kernels/convolution/convolution_kernel_bfyx_os_iyx_osv16.cpp index c4d968e1777b..9a3bb95fb8a2 100644 --- a/src/plugins/intel_gpu/src/kernel_selector/kernels/convolution/convolution_kernel_bfyx_os_iyx_osv16.cpp +++ b/src/plugins/intel_gpu/src/kernel_selector/kernels/convolution/convolution_kernel_bfyx_os_iyx_osv16.cpp @@ -237,9 +237,19 @@ JitConstants ConvolutionKernel_bfyx_os_iyx_osv16::GetJitConstants(const convolut jit.AddConstant(MakeJitConstant("IN_BLOCK_WIDTH", dispatchData.cldnnStyle.inputBlockWidth)); jit.AddConstant(MakeJitConstant("PREFETCH", dispatchData.cldnnStyle.prefetch)); - const size_t large_filter_size = 1024; // This acceptable size was decided by heuristics - auto filter_size = params.filterSize.x * params.filterSize.y; - if (filter_size >= large_filter_size) { + // The LOOP macro fully unrolls filter_y × filter_x iterations at the preprocessor level, + // and within each copy, the compiler will also unroll the block_height × block_width inner loops. + // The total effective instruction count is proportional to: + // filter_y * filter_x * block_height * block_width + // When this product is large, the resulting code causes excessive compilation time, + // instruction cache pressure, and register spilling. Fall back to compiler-hinted + // unrolling (unroll_for) beyond the threshold. + const size_t unroll_ops_threshold = 1024; + const size_t total_unroll_ops = static_cast(params.filterSize.x) * + static_cast(params.filterSize.y) * + dispatchData.cldnnStyle.blockWidth * + dispatchData.cldnnStyle.blockHeight; + if (total_unroll_ops >= unroll_ops_threshold) { jit.AddConstant(MakeJitConstant("DISABLE_MANUAL_UNROLL", 1)); } diff --git a/src/plugins/intel_gpu/src/kernel_selector/kernels/convolution/convolution_kernel_bfyx_os_iyx_osv32.cpp b/src/plugins/intel_gpu/src/kernel_selector/kernels/convolution/convolution_kernel_bfyx_os_iyx_osv32.cpp index 54c24ff93a27..a136788347b1 100644 --- a/src/plugins/intel_gpu/src/kernel_selector/kernels/convolution/convolution_kernel_bfyx_os_iyx_osv32.cpp +++ b/src/plugins/intel_gpu/src/kernel_selector/kernels/convolution/convolution_kernel_bfyx_os_iyx_osv32.cpp @@ -192,6 +192,19 @@ bool ConvolutionKernel_bfyx_os_iyx_osv32::Validate(const Params& p) const { DO_NOT_USE_THIS_KERNEL(p.layerID); } + // On Xe2+ the scatter input reads in this kernel can access memory beyond + // the allocated input buffer for certain dynamic-shape configurations. + // Pre-Xe2 hardware silently returns zero for OOB reads but Xe2 raises + // CL_OUT_OF_RESOURCES. Disable for dynamic shapes on Xe2+. + if (p.is_shape_agnostic) { + const auto ip_major = static_cast(p.engineInfo.ip_version >> 16); + const bool is_xe2_or_later = p.engineInfo.arch >= gpu_arch::xe2 || + (p.engineInfo.arch == gpu_arch::unknown && ip_major >= 20); + if (is_xe2_or_later) { + DO_NOT_USE_THIS_KERNEL(p.layerID); + } + } + // To prevent big sized filter which causes lots of CL build time. const size_t acceptable_filter_size = 1024; // This acceptable size was decided by heuristics const auto& params = static_cast(p); diff --git a/src/plugins/intel_gpu/src/kernel_selector/kernels/convolution/convolution_kernel_bfyx_to_b_fs_yx_fsv16.cpp b/src/plugins/intel_gpu/src/kernel_selector/kernels/convolution/convolution_kernel_bfyx_to_b_fs_yx_fsv16.cpp index 2980b5bcbb5e..3a34a4eb77b1 100644 --- a/src/plugins/intel_gpu/src/kernel_selector/kernels/convolution/convolution_kernel_bfyx_to_b_fs_yx_fsv16.cpp +++ b/src/plugins/intel_gpu/src/kernel_selector/kernels/convolution/convolution_kernel_bfyx_to_b_fs_yx_fsv16.cpp @@ -116,6 +116,16 @@ bool ConvolutionKernel_bfyx_to_bfyx_f16::Validate(const Params& p) const { DO_NOT_USE_THIS_KERNEL(p.layerID); } + // Large private memory (line_cache) combined with fused ops causes register pressure overflow + // on some GPU architectures (e.g. Xe-LPG). Reject this kernel in such cases. + auto blockWidth = GetAutoTuneOptions(p, -1).blockWidth; + size_t input_line_size = std::min(params.stride.x * (blockWidth - 1) + (params.filterSize.x - 1) * params.dilation.x + 1, + input.X().v + input.X().pad.Total()); + size_t input_block_size = CeilDiv(input_line_size * params.filterSize.y, sub_group_size); + if (input_block_size > 64 && !params.fused_ops.empty()) { + DO_NOT_USE_THIS_KERNEL(p.layerID); + } + return true; } diff --git a/src/plugins/intel_gpu/src/kernel_selector/kernels/convolution/convolution_kernel_yxfb_yxio_b8.cpp b/src/plugins/intel_gpu/src/kernel_selector/kernels/convolution/convolution_kernel_yxfb_yxio_b8.cpp index c8e38b055458..6e4633f58db8 100644 --- a/src/plugins/intel_gpu/src/kernel_selector/kernels/convolution/convolution_kernel_yxfb_yxio_b8.cpp +++ b/src/plugins/intel_gpu/src/kernel_selector/kernels/convolution/convolution_kernel_yxfb_yxio_b8.cpp @@ -71,6 +71,12 @@ bool ConvolutionKernel_yxfb_yxio_b8::Validate(const Params& p) const { const auto filterOfmNum = params.weights.OFM().v; const auto batchSize = params.outputs[0].Batch().v; + // Kernel uses TRANSPOSE_BLOCK_8 and subgroup block reads/writes designed for SIMD8. + // With wider SIMD (e.g. SIMD16), lanes beyond 7 produce OOB filter accesses via + // filter_idx2 = filter_idx + 8 (ofm_offset + sub_group_id + 8 exceeds FILTER_OFM_NUM). + if (!IsSIMDSizeSupported(params.engineInfo, 8)) + DO_NOT_USE_THIS_KERNEL(p.layerID); + const bool bInputValidated = (filterOfmNum > 0) && (batchSize > 0) && (params.outputs[0].Feature().v == filterOfmNum); if (!bInputValidated) { diff --git a/src/plugins/intel_gpu/src/kernel_selector/kernels/deconvolution/deconvolution_kernel_b_fs_zyx_fsv16.cpp b/src/plugins/intel_gpu/src/kernel_selector/kernels/deconvolution/deconvolution_kernel_b_fs_zyx_fsv16.cpp index 39c073e7c019..85b2499a6b6d 100644 --- a/src/plugins/intel_gpu/src/kernel_selector/kernels/deconvolution/deconvolution_kernel_b_fs_zyx_fsv16.cpp +++ b/src/plugins/intel_gpu/src/kernel_selector/kernels/deconvolution/deconvolution_kernel_b_fs_zyx_fsv16.cpp @@ -33,6 +33,7 @@ ParamsKey DeconvolutionKernel_b_fs_zyx_fsv16::GetSupportedKey() const { k.EnableNonBiasTerm(); k.EnableBatching(); k.EnableDifferentTypes(); + k.EnableDilation(); return k; } diff --git a/src/plugins/intel_gpu/src/kernel_selector/kernels/deconvolution/deconvolution_kernel_base.cpp b/src/plugins/intel_gpu/src/kernel_selector/kernels/deconvolution/deconvolution_kernel_base.cpp index 52ace612d39c..a422962b95b4 100644 --- a/src/plugins/intel_gpu/src/kernel_selector/kernels/deconvolution/deconvolution_kernel_base.cpp +++ b/src/plugins/intel_gpu/src/kernel_selector/kernels/deconvolution/deconvolution_kernel_base.cpp @@ -142,6 +142,12 @@ Datatype DeconvolutionKernelBase::GetAccumulatorType(const deconvolution_params& if (params.inputs[0].GetDType() == Datatype::INT8 || params.inputs[0].GetDType() == Datatype::UINT8) return Datatype::INT32; + // Use fp32 accumulator for dilated fp16 deconvolutions to avoid precision loss + // during accumulation with large effective kernel sizes. + if (params.inputs[0].GetDType() == Datatype::F16 && + (params.dilation.x > 1 || params.dilation.y > 1 || params.dilation.z > 1)) + return Datatype::F32; + // input is either fp32 or fp16 // for fp32->fp16 accumulate to fp16, otherwise accumulate to input type if (params.outputs[0].GetDType() == Datatype::F16) diff --git a/src/plugins/intel_gpu/src/kernel_selector/kernels/deconvolution/deconvolution_kernel_ref.cpp b/src/plugins/intel_gpu/src/kernel_selector/kernels/deconvolution/deconvolution_kernel_ref.cpp index 35c10f141bda..29899b72b1d1 100644 --- a/src/plugins/intel_gpu/src/kernel_selector/kernels/deconvolution/deconvolution_kernel_ref.cpp +++ b/src/plugins/intel_gpu/src/kernel_selector/kernels/deconvolution/deconvolution_kernel_ref.cpp @@ -62,6 +62,7 @@ ParamsKey DeconvolutionKernelRef::GetSupportedKey() const { k.EnableGroupedConvolution(); k.EnableDifferentTypes(); k.EnableDifferentInputWeightsTypes(); + k.EnableDilation(); return k; } diff --git a/src/plugins/intel_gpu/src/kernel_selector/kernels/dynamic_quantize/dynamic_quantize_kernel_opt.cpp b/src/plugins/intel_gpu/src/kernel_selector/kernels/dynamic_quantize/dynamic_quantize_kernel_opt.cpp index 2460ea8db757..e7b8a1af9070 100644 --- a/src/plugins/intel_gpu/src/kernel_selector/kernels/dynamic_quantize/dynamic_quantize_kernel_opt.cpp +++ b/src/plugins/intel_gpu/src/kernel_selector/kernels/dynamic_quantize/dynamic_quantize_kernel_opt.cpp @@ -204,6 +204,20 @@ bool DynamicQuantizeKernelOpt::Validate(const Params& params) const { if (((bf.second) % (simd * 2)) != 0) DO_NOT_USE_THIS_KERNEL(params.layerID); + // For MODE_LARGE_GS, ensure the quantization group fits within a single work_group + // There is no cross work_group synchronization. + if (get_dynamic_quantize_mode(dq_params) == DynQuanMode::LARGE_GS) { + auto vec_size = get_match_vector_size(dq_params); + size_t block_size = simd * vec_size; + size_t blocks_per_group = dq_params.group_sizes.back() / block_size; + + // BLOCK_NUM is limited to 32 in GetJitConstants, so we can only handle + // quantization groups that require <= 32 blocks + if (blocks_per_group > 32) { + DO_NOT_USE_THIS_KERNEL(params.layerID); + } + } + if (dq_params.inputs[0].GetPaddedVal() != 0 || dq_params.outputs[0].GetPaddedVal() != 0) DO_NOT_USE_THIS_KERNEL(params.layerID); diff --git a/src/plugins/intel_gpu/src/kernel_selector/kernels/dynamic_quantize/dynamic_quantize_kernel_opt_kv_cache.cpp b/src/plugins/intel_gpu/src/kernel_selector/kernels/dynamic_quantize/dynamic_quantize_kernel_opt_kv_cache.cpp index ec4c40affa82..c0a47c23dc26 100644 --- a/src/plugins/intel_gpu/src/kernel_selector/kernels/dynamic_quantize/dynamic_quantize_kernel_opt_kv_cache.cpp +++ b/src/plugins/intel_gpu/src/kernel_selector/kernels/dynamic_quantize/dynamic_quantize_kernel_opt_kv_cache.cpp @@ -88,6 +88,9 @@ ParamsKey DynamicQuantizeKernelKVCache::GetSupportedKey() const { ParamsKey k; k.EnableInputDataType(Datatype::F16); k.EnableOutputDataType(Datatype::INT8); + k.EnableOutputDataType(Datatype::UINT8); + k.EnableOutputDataType(Datatype::INT4); + k.EnableOutputDataType(Datatype::UINT4); k.EnableDifferentTypes(); k.EnableAllInputLayout(); k.EnableAllOutputLayout(); @@ -141,6 +144,7 @@ JitConstants DynamicQuantizeKernelKVCache::GetJitConstants(const dynamic_quantiz jit.AddConstant(MakeJitConstant("ITERATIONS_NUMBER", iterations_number)); jit.AddConstant(MakeJitConstant("ASYMMETRIC_QUANTIZATION", params.use_asymmetric_quantization)); jit.AddConstant(MakeJitConstant("GROUP_SCALES_WITH_ZP", params.combine_scales_and_zp)); + jit.AddConstant(MakeJitConstant("IS_INT4_COMPRESSED", params.is_int4_compressed)); // Use FP32 accumulator type for scale/zp calculation jit.Merge(MakeTypeJitConstants(Datatype::F32, "ACCUMULATOR")); diff --git a/src/plugins/intel_gpu/src/kernel_selector/kernels/dynamic_quantize/dynamic_quantize_kernel_ref.h b/src/plugins/intel_gpu/src/kernel_selector/kernels/dynamic_quantize/dynamic_quantize_kernel_ref.h index c44e8dcaaedc..6b7985aef25b 100644 --- a/src/plugins/intel_gpu/src/kernel_selector/kernels/dynamic_quantize/dynamic_quantize_kernel_ref.h +++ b/src/plugins/intel_gpu/src/kernel_selector/kernels/dynamic_quantize/dynamic_quantize_kernel_ref.h @@ -21,6 +21,7 @@ struct dynamic_quantize_params : public base_params { bool use_asymmetric_quantization = false; bool combine_scales_and_zp = false; bool generate_precomputed_reduction = false; + bool is_int4_compressed = false; }; class DynamicQuantizeKernelRef : public KernelBaseOpenCL { diff --git a/src/plugins/intel_gpu/src/kernel_selector/kernels/eltwise/eltwise_kernel_base.cpp b/src/plugins/intel_gpu/src/kernel_selector/kernels/eltwise/eltwise_kernel_base.cpp index 9cbc4f478044..e526c60ba6db 100644 --- a/src/plugins/intel_gpu/src/kernel_selector/kernels/eltwise/eltwise_kernel_base.cpp +++ b/src/plugins/intel_gpu/src/kernel_selector/kernels/eltwise/eltwise_kernel_base.cpp @@ -54,6 +54,7 @@ uint32_t GetNumberOfInputs(EltwiseMode m) { case EltwiseMode::BITWISE_AND: case EltwiseMode::BITWISE_OR: case EltwiseMode::BITWISE_XOR: + case EltwiseMode::ATAN2: return 2; case EltwiseMode::SQRT: case EltwiseMode::RSQRT: @@ -241,14 +242,17 @@ JitConstants EltwiseKernelBase::GetOperationsJitConstants(const eltwise_params& auto input_0_type = params.inputs[0].GetDType(); auto input_1_type = params.inputs[1].GetDType(); + auto is_integer_type = [](kernel_selector::Datatype type) { + return type == kernel_selector::Datatype::INT8 || type == kernel_selector::Datatype::UINT8 || + type == kernel_selector::Datatype::INT16 || type == kernel_selector::Datatype::UINT16 || + type == kernel_selector::Datatype::INT32 || type == kernel_selector::Datatype::UINT32 || + type == kernel_selector::Datatype::INT64; + }; + // input_0 == int - if (input_0_type == kernel_selector::Datatype::INT8 || - input_0_type == kernel_selector::Datatype::INT32 || - input_0_type == kernel_selector::Datatype::INT64) { + if (is_integer_type(input_0_type)) { // input_0 == int && input_1 == int - if (input_1_type == kernel_selector::Datatype::INT8 || - input_1_type == kernel_selector::Datatype::INT32 || - input_1_type == kernel_selector::Datatype::INT64) { + if (is_integer_type(input_1_type)) { if (ew.mode == EltwiseMode::MODULU) op += input0_str + " % " + input1_str; else @@ -257,9 +261,7 @@ JitConstants EltwiseKernelBase::GetOperationsJitConstants(const eltwise_params& // input_0 == int && input_1 != int op += cast_type + "f" + mode + "(convert_float(" + input0_str + "), " + input1_str + ")"; } - } else if (input_1_type == kernel_selector::Datatype::INT8 || - input_1_type == kernel_selector::Datatype::INT32 || - input_1_type == kernel_selector::Datatype::INT64) { + } else if (is_integer_type(input_1_type)) { // input_0 != int && input_1 == int op += cast_type + "f" + mode + "(" + input0_str + ", convert_float(" + input1_str + "))"; } else { @@ -270,6 +272,10 @@ JitConstants EltwiseKernelBase::GetOperationsJitConstants(const eltwise_params& case EltwiseMode::POW: op += cast_type + "pow(" + input0_str + ", " + input1_str + ")"; break; + case EltwiseMode::ATAN2: + // input0 = y (lhs of atan2), input1 = x (rhs). + op += cast_type + "atan2(" + input0_str + ", " + input1_str + ")"; + break; case EltwiseMode::SQRT: op += cast_type + "sqrt(" + input0_str + ")"; break; diff --git a/src/plugins/intel_gpu/src/kernel_selector/kernels/fully_connected/fully_connected_kernel_mmad.cpp b/src/plugins/intel_gpu/src/kernel_selector/kernels/fully_connected/fully_connected_kernel_mmad.cpp index 7504af775476..f1f1812ff6d5 100644 --- a/src/plugins/intel_gpu/src/kernel_selector/kernels/fully_connected/fully_connected_kernel_mmad.cpp +++ b/src/plugins/intel_gpu/src/kernel_selector/kernels/fully_connected/fully_connected_kernel_mmad.cpp @@ -212,6 +212,19 @@ JitConstants FullyConnectedKernelMMAD::GetJitConstants(const fully_connected_par jit.AddConstant(MakeJitConstant("HAS_FEATURE_LEFTOVERS", has_feature_leftovers)); jit.AddConstant(MakeJitConstant("FEATURE_BLOCKS_COUNT", tuning_data.feature_blocks_count)); + + // For SUB_GROUP_SIZE==16 with feature leftovers, check if the second ISA8 block + // (the .hi half of the BLOCK_READ_8 pair) exists in the weight buffer. + // Weight layout is os_is_yx_isa8_osv16_isv4: each IS block = ISA8 * OSV16 * ISV4 = 512 bytes. + // One "fblock" in the kernel = 2 IS blocks = 1024 bytes (.lo + .hi). + // The .hi read is valid only if there are at least 2 IS blocks remaining after the main loop. + if (has_feature_leftovers && tuning_data.sub_group_size == 16) { + auto input_feature = output.GetLayout() == DataLayout::bfyx ? input.Y().v : input.Feature().v; + size_t is_blocks = CeilDiv(input_feature, (size_t)32); + size_t required_is_blocks = tuning_data.feature_blocks_count * 2 + 2; + bool hi_valid = is_blocks >= required_is_blocks; + jit.AddConstant(MakeJitConstant("MMAD_FILTER_LEFTOVER_HI", hi_valid ? 1 : 0)); + } jit.AddConstant(MakeJitConstant("SLM_DIV_FACTOR", tuning_data.slm_div_factor)); jit.AddConstant(MakeJitConstant("UNROLL_FACTOR", tuning_data.unroll_factor)); jit.AddConstant(MakeJitConstant("FULL_UNROLL_FACTOR", tuning_data.full_unroll_factor)); diff --git a/src/plugins/intel_gpu/src/kernel_selector/kernels/gemm/gemm_kernel_mmad_int8.cpp b/src/plugins/intel_gpu/src/kernel_selector/kernels/gemm/gemm_kernel_mmad_int8.cpp index 3baf60524dc7..0c23a6fa5ab5 100644 --- a/src/plugins/intel_gpu/src/kernel_selector/kernels/gemm/gemm_kernel_mmad_int8.cpp +++ b/src/plugins/intel_gpu/src/kernel_selector/kernels/gemm/gemm_kernel_mmad_int8.cpp @@ -124,7 +124,7 @@ GemmKernelMMADint8::GemmTuningData GemmKernelMMADint8::SetTuningParams(const gem if (!leftovers_simd16x2 && very_big_matrices && no_input2) { simd_size = 16; tile_num = 2; } - else if ((leftovers_simd16 && !leftovers_simd8) || small_matrices) + else if (((leftovers_simd16 && !leftovers_simd8) || small_matrices) && IsSIMDSizeSupported(params.engineInfo, 8)) { simd_size = 8; } tuning_data.simd_size = simd_size; diff --git a/src/plugins/intel_gpu/src/kernel_selector/kernels/matrix_nms/matrix_nms_kernel_ref.cpp b/src/plugins/intel_gpu/src/kernel_selector/kernels/matrix_nms/matrix_nms_kernel_ref.cpp index f3dc5dc38126..4c678e5b13e0 100644 --- a/src/plugins/intel_gpu/src/kernel_selector/kernels/matrix_nms/matrix_nms_kernel_ref.cpp +++ b/src/plugins/intel_gpu/src/kernel_selector/kernels/matrix_nms/matrix_nms_kernel_ref.cpp @@ -49,8 +49,8 @@ MatrixNmsKernelRef::DispatchData SetDefault(const matrix_nms_params& params, siz } std::tuple GetMaxBoxes(const matrix_nms_params& params) { - const int classes_num = static_cast(params.inputs[1].Feature().v); - const int boxes_num = static_cast(params.inputs[0].Feature().v); + const int classes_num = static_cast(params.inputs[1].Feature().v); + const int boxes_num = static_cast(params.inputs[0].Feature().v); int max_boxes_per_class{boxes_num}; if (params.nms_top_k >= 0) @@ -79,8 +79,8 @@ KernelsData MatrixNmsKernelRef::GetKernelsData(const Params& params) const { constexpr size_t BOX_INFO_SIZE{16}; - const int batches_num = static_cast(new_params.inputs[1].Batch().v); - const int classes_num = static_cast(new_params.inputs[1].Feature().v); + const int batches_num = static_cast(new_params.inputs[1].Batch().v); + const int classes_num = static_cast(new_params.inputs[1].Feature().v); int max_boxes_per_class, max_boxes_per_batch; std::tie(max_boxes_per_class, max_boxes_per_batch) = GetMaxBoxes(new_params); diff --git a/src/plugins/intel_gpu/src/kernel_selector/kernels/multiclass_nms/multiclass_nms_kernel_ref.cpp b/src/plugins/intel_gpu/src/kernel_selector/kernels/multiclass_nms/multiclass_nms_kernel_ref.cpp index c426fd070a17..b2c2490f07a2 100644 --- a/src/plugins/intel_gpu/src/kernel_selector/kernels/multiclass_nms/multiclass_nms_kernel_ref.cpp +++ b/src/plugins/intel_gpu/src/kernel_selector/kernels/multiclass_nms/multiclass_nms_kernel_ref.cpp @@ -89,14 +89,14 @@ JitConstants MulticlassNmsKernelRef::GetJitConstants(const multiclass_nms_params int64_t max_output_boxes_per_class = 0; if (params.nms_top_k >= 0) { - max_output_boxes_per_class = std::min(static_cast(num_boxes), params.nms_top_k); + max_output_boxes_per_class = std::min(static_cast(num_boxes), params.nms_top_k); } else { max_output_boxes_per_class = num_boxes; } auto max_output_boxes_per_batch = max_output_boxes_per_class * real_num_classes; if (params.keep_top_k >= 0) - max_output_boxes_per_batch = std::min(max_output_boxes_per_batch, params.keep_top_k); + max_output_boxes_per_batch = std::min(max_output_boxes_per_batch, params.keep_top_k); jit.AddConstants({ MakeJitConstant("SORT_RESULT_TYPE", static_cast(params.sort_result_type)), diff --git a/src/plugins/intel_gpu/src/kernel_selector/kernels/mvn/mvn_kernel_b_fs_yx_fsv16.cpp b/src/plugins/intel_gpu/src/kernel_selector/kernels/mvn/mvn_kernel_b_fs_yx_fsv16.cpp new file mode 100644 index 000000000000..ea6ad36f31bb --- /dev/null +++ b/src/plugins/intel_gpu/src/kernel_selector/kernels/mvn/mvn_kernel_b_fs_yx_fsv16.cpp @@ -0,0 +1,713 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "mvn_kernel_b_fs_yx_fsv16.hpp" +#include "common_tools.h" + +#include +#include +#include + +namespace kernel_selector { + +static constexpr size_t simd = 16; +static constexpr size_t fsv = 16; +static constexpr size_t pref_work_groups = 16; + +ParamsKey MVNKernel_b_fs_yx_fsv16::GetSupportedKey() const { + ParamsKey k; + + k.EnableInputDataType(Datatype::F16); + k.EnableInputDataType(Datatype::F32); + k.EnableInputDataType(Datatype::INT8); + k.EnableInputDataType(Datatype::UINT8); + + k.EnableOutputDataType(Datatype::F16); + k.EnableOutputDataType(Datatype::F32); + k.EnableOutputDataType(Datatype::INT8); + k.EnableOutputDataType(Datatype::UINT8); + + k.EnableInputLayout(DataLayout::b_fs_yx_fsv16); + k.EnableOutputLayout(DataLayout::b_fs_yx_fsv16); + k.EnableInputLayout(DataLayout::b_fs_zyx_fsv16); + k.EnableOutputLayout(DataLayout::b_fs_zyx_fsv16); + k.EnableInputLayout(DataLayout::b_fs_yx_fsv32); + k.EnableOutputLayout(DataLayout::b_fs_yx_fsv32); + k.EnableInputLayout(DataLayout::b_fs_zyx_fsv32); + k.EnableOutputLayout(DataLayout::b_fs_zyx_fsv32); + k.EnableTensorOffset(); + k.EnableTensorPitches(); + k.EnableDifferentTypes(); + k.EnableBatching(); + // TODO Add support for across channels + // k.EnableMVNMode(MVNMode::ACROSS_CHANNELS); + k.EnableMVNMode(MVNMode::WITHIN_CHANNELS); + k.EnableMVNNormalizeVariance(); + k.EnableDynamicShapesSupport(); + + return k; +} + +DeviceFeaturesKey MVNKernel_b_fs_yx_fsv16::get_required_device_features_key(const Params& params) const { + auto k = get_common_subgroups_device_features_key(params); + k.requires_subgroup_shuffle(); + k.requires_subgroup_reduce(); + + return k; +} + +bool MVNKernel_b_fs_yx_fsv16::Validate(const Params& p) const { + if (!Parent::Validate(p)) + DO_NOT_USE_THIS_KERNEL(p.layerID); + + auto params = static_cast(p); + + // TODO Add support for input padding via iterating over y (parallel or in kernel). + // Skip padding check for dynamic tensors (padding not known at compile time). + if (!params.has_dynamic_tensors()) { + if (params.inputs[0].X().pad.Total() != 0 || params.inputs[0].Y().pad.Total() != 0 || + params.inputs[0].Z().pad.Total() != 0) + DO_NOT_USE_THIS_KERNEL(p.layerID); + } + + return true; +} + +MVNKernelBase::DispatchData MVNKernel_b_fs_yx_fsv16::SetDefault(const mvn_params& params) const { + auto dispatchData = Parent::SetDefault(params); + + auto max_wg = params.engineInfo.maxWorkGroupSize; + auto slm_per_sg = fsv * 4; + auto max_slm = params.engineInfo.maxLocalMemSize; + auto max_sgs = max_slm / slm_per_sg; + auto max_lws = std::min(max_wg, max_sgs * simd); + + if (params.has_dynamic_tensors()) { + // Fixed LWS for shape-agnostic compilation (basic single-workgroup mode). + // LWS is baked into reqd_work_group_size; GWS[1]/[2] updated at runtime. + auto lws = std::max(max_lws / simd, (size_t)1) * simd; + dispatchData.gws[0] = lws; + dispatchData.gws[1] = 1; + dispatchData.gws[2] = 1; + dispatchData.lws[0] = lws; + dispatchData.lws[1] = 1; + dispatchData.lws[2] = 1; + } else { + auto items_num = params.outputs[0].X().v * params.outputs[0].Y().v * params.outputs[0].Z().v; + auto lws = std::max(std::min(items_num, max_lws) / simd, (size_t)1) * simd; + dispatchData.gws[0] = lws; + dispatchData.gws[1] = CeilDiv(params.outputs[0].Feature().v, fsv); + dispatchData.gws[2] = params.outputs[0].Batch().v; + dispatchData.lws[0] = lws; + dispatchData.lws[1] = 1; + dispatchData.lws[2] = 1; + } + + dispatchData.itemsNum = 1; + + return dispatchData; +} + +Datatype MVNKernel_b_fs_yx_fsv16::GetAccumulatorType(const mvn_params& params) const { + const auto& input_dt = params.inputs[0].GetDType(); + + switch (input_dt) { + case Datatype::F32: + case Datatype::F16: + return Datatype::F32; + case Datatype::INT8: + case Datatype::UINT8: + return Datatype::INT32; + default: return Datatype::F32; + } +} + +JitConstants MVNKernel_b_fs_yx_fsv16::GetJitConstants(const mvn_params& params, DispatchData dispatchData) const { + auto jits = Parent::GetJitConstants(params, dispatchData); + + auto activation_dt = GetActivationType(params); + jits.Merge(MakeTypeJitConstants(activation_dt, "ACTIVATION")); + jits.Merge(MakeTypeJitConstants(Datatype::F32, "MEAN")); + jits.Merge(MakeTypeJitConstants(GetAccumulatorType(params), "ACCUMULATOR")); + jits.AddConstant(MakeJitConstant("SIMD", simd)); + jits.AddConstant(MakeJitConstant("LWS", dispatchData.lws[0])); + jits.AddConstant(MakeJitConstant("GWS", dispatchData.gws[0])); + jits.AddConstant(MakeJitConstant("ITEM_GROUPS", dispatchData.itemsNum)); + auto input_layout = params.inputs[0].GetLayout(); + size_t input_slice_pitch = (input_layout == DataLayout::b_fs_yx_fsv32 || + input_layout == DataLayout::b_fs_zyx_fsv32) ? 32 : 16; + jits.AddConstant(MakeJitConstant("INPUT_SLICE_PITCH", input_slice_pitch)); + auto output_layout = params.outputs[0].GetLayout(); + size_t output_slice_pitch = (output_layout == DataLayout::b_fs_yx_fsv32 || + output_layout == DataLayout::b_fs_zyx_fsv32) ? 32 : 16; + jits.AddConstant(MakeJitConstant("OUTPUT_SLICE_PITCH", output_slice_pitch)); + if (params.has_dynamic_tensors()) { + // For dynamic shapes, ITEMS_NUM is passed as a scalar kernel argument + // because inline accumulate functions can't access shape_info buffer. + jits.AddConstant(MakeJitConstant("ITEMS_NUM", "items_num")); + } else { + // Define ITEMS_NUM via JIT so the batch-compilation #undef system + // cleans it up between kernels sharing the same CL source file. + jits.AddConstant(MakeJitConstant("ITEMS_NUM", + params.outputs[0].X().v * params.outputs[0].Y().v * params.outputs[0].Z().v)); + } + if (!params.fused_ops.empty()) { + std::vector idx_order; + + if (params.inputs[0].GetDims().size() <= 4) { + idx_order = {"b", + "(f + set_idx)", + "(output_spatial / OUTPUT_SIZE_X)", + "(output_spatial % OUTPUT_SIZE_X)"}; + } else if (params.inputs[0].GetDims().size() == 5) { + idx_order = {"b", + "(f + set_idx)", + "(output_spatial / (OUTPUT_SIZE_X * OUTPUT_SIZE_Y))", + "((output_spatial / OUTPUT_SIZE_X) % OUTPUT_SIZE_Y)", + "(output_spatial % OUTPUT_SIZE_X)"}; + } + + auto conf = FusedOpsConfiguration("", idx_order, "normalized_activation", activation_dt); + if (params.has_dynamic_tensors()) { + conf.SetBoundaryCheck(FusedOpsConfiguration::BoundaryCheck::ENABLED); + } + jits.Merge(MakeFusedOpsJitConstants(params, {conf})); + } + return jits; +} + +MVNKernel_b_fs_yx_fsv16::MultiDispatchData MVNKernel_b_fs_yx_fsv16::SetDefaultForMulti( + const mvn_params& params) const { + MultiDispatchData dispatchData; + + auto max_wg = params.engineInfo.maxWorkGroupSize; + auto slm_per_sg = fsv * 4; + auto max_slm = params.engineInfo.maxLocalMemSize; + auto max_sgs = max_slm / slm_per_sg; + auto max_lws = std::min(max_wg, max_sgs * simd); + + size_t lws; + if (params.has_dynamic_tensors()) { + // Use hardware-max LWS for dynamic (data size unknown at compile time) + lws = std::max(max_lws / simd, (size_t)1) * simd; + } else { + auto items_num = params.outputs[0].X().v * params.outputs[0].Y().v * params.outputs[0].Z().v; + lws = std::max(std::min(items_num, max_lws) / simd, (size_t)1) * simd; + } + + // TODO Check if larger number of work-groups does not provide benefit + size_t item_groups = pref_work_groups; + dispatchData.item_groups = item_groups; + + size_t stage1_lws = lws; + + dispatchData.stage_1.gws[0] = stage1_lws * item_groups; + dispatchData.stage_1.gws[1] = params.has_dynamic_tensors() ? 1 : CeilDiv(params.outputs[0].Feature().v, fsv); + dispatchData.stage_1.gws[2] = params.has_dynamic_tensors() ? 1 : params.outputs[0].Batch().v; + + dispatchData.stage_1.lws[0] = stage1_lws; + dispatchData.stage_1.lws[1] = 1; + dispatchData.stage_1.lws[2] = 1; + + dispatchData.stage_1.itemsNum = item_groups; + + size_t stage2_lws = std::max(std::min(item_groups, max_lws) / simd, (size_t)1) * simd; + + dispatchData.stage_2.gws[0] = stage2_lws; + dispatchData.stage_2.gws[1] = params.has_dynamic_tensors() ? 1 : CeilDiv(params.outputs[0].Feature().v, fsv); + dispatchData.stage_2.gws[2] = params.has_dynamic_tensors() ? 1 : params.outputs[0].Batch().v; + + dispatchData.stage_2.lws[0] = stage2_lws; + dispatchData.stage_2.lws[1] = 1; + dispatchData.stage_2.lws[2] = 1; + + dispatchData.stage_2.itemsNum = item_groups; + + if (params.has_dynamic_tensors()) { + dispatchData.stage_final.gws[0] = simd; // Placeholder, updated at runtime + dispatchData.stage_final.gws[1] = 1; + dispatchData.stage_final.gws[2] = 1; + } else { + auto items_num = params.outputs[0].X().v * params.outputs[0].Y().v * params.outputs[0].Z().v; + dispatchData.stage_final.gws[0] = std::max(items_num / simd, (size_t)1) * simd; + dispatchData.stage_final.gws[1] = CeilDiv(params.outputs[0].Feature().v, fsv); + dispatchData.stage_final.gws[2] = params.outputs[0].Batch().v; + } + + dispatchData.stage_final.lws[0] = simd; + dispatchData.stage_final.lws[1] = 1; + dispatchData.stage_final.lws[2] = 1; + + dispatchData.stage_final.itemsNum = 1; + + return dispatchData; +} + +KernelsData MVNKernel_b_fs_yx_fsv16::GetMultiStageKernelsData(const mvn_params& params) const { + if (!Validate(params)) + return {}; + + constexpr size_t intermidiate_bytes = 4; + const mvn_params& orgParams = static_cast(params); + + auto dispatchData = SetDefaultForMulti(orgParams); + + size_t kernels_num = params.mvnNormalizeVariance ? 5 : 3; + KernelData kd = KernelData::Default(params, kernels_num); + + auto finalKernelName = GetKernelName(orgParams); + size_t entry_part_id = 0; + { + // Mean first stage + auto cldnn_jit = GetJitConstants(orgParams, dispatchData.stage_1); + cldnn_jit.AddConstant(MakeJitConstant("MVN_KERNEL_MEAN_1", 1)); + auto entry_point = GetEntryPoint(finalKernelName, orgParams.layerID, params, entry_part_id++); + auto jit = CreateJit(finalKernelName, cldnn_jit, entry_point); + auto& kernel = kd.kernels[0]; + FillCLKernelData(kernel, + dispatchData.stage_1, + params.engineInfo, + finalKernelName, + jit, + entry_point, + "", + false, + false, + 0, + 0); + kernel.params.arguments.clear(); // Clear original output argument + kernel.params.arguments.push_back({ArgumentDescriptor::Types::INPUT, 0}); + kernel.params.arguments.push_back({ArgumentDescriptor::Types::INTERNAL_BUFFER, 0}); + kd.internalBuffers.push_back(params.outputs[0].Batch().v * Align(params.outputs[0].Feature().v, fsv) * + dispatchData.item_groups * intermidiate_bytes); + } + { + // Mean second stage + auto cldnn_jit = GetJitConstants(orgParams, dispatchData.stage_2); + cldnn_jit.AddConstant(MakeJitConstant("MVN_KERNEL_MEAN_2", 1)); + auto entry_point = GetEntryPoint(finalKernelName, orgParams.layerID, params, entry_part_id++); + auto jit = CreateJit(finalKernelName, cldnn_jit, entry_point); + auto& kernel = kd.kernels[1]; + FillCLKernelData(kernel, + dispatchData.stage_2, + params.engineInfo, + finalKernelName, + jit, + entry_point, + "", + false, + false, + 0, + 0); + kernel.params.arguments.clear(); // Clear original output argument + kernel.params.arguments.push_back({ArgumentDescriptor::Types::INTERNAL_BUFFER, 0}); + kernel.params.arguments.push_back({ArgumentDescriptor::Types::INTERNAL_BUFFER, 1}); + kd.internalBuffers.push_back(params.outputs[0].Batch().v * Align(params.outputs[0].Feature().v, fsv) * + intermidiate_bytes); + } + if (params.mvnNormalizeVariance) { + // Variance first stage + auto cldnn_jit = GetJitConstants(orgParams, dispatchData.stage_1); + cldnn_jit.AddConstant(MakeJitConstant("MVN_KERNEL_VAR_1", 1)); + auto entry_point = GetEntryPoint(finalKernelName, orgParams.layerID, params, entry_part_id++); + auto jit = CreateJit(finalKernelName, cldnn_jit, entry_point); + auto& kernel = kd.kernels[2]; + FillCLKernelData(kernel, + dispatchData.stage_1, + params.engineInfo, + finalKernelName, + jit, + entry_point, + "", + false, + false, + 0, + 0); + kernel.params.arguments.clear(); // Clear original output argument + kernel.params.arguments.push_back({ArgumentDescriptor::Types::INPUT, 0}); + kernel.params.arguments.push_back({ArgumentDescriptor::Types::INTERNAL_BUFFER, 1}); + kernel.params.arguments.push_back({ArgumentDescriptor::Types::INTERNAL_BUFFER, 0}); + } + if (params.mvnNormalizeVariance) { + // Variance second stage + auto cldnn_jit = GetJitConstants(orgParams, dispatchData.stage_2); + cldnn_jit.AddConstant(MakeJitConstant("MVN_KERNEL_VAR_2", 1)); + auto entry_point = GetEntryPoint(finalKernelName, orgParams.layerID, params, entry_part_id++); + auto jit = CreateJit(finalKernelName, cldnn_jit, entry_point); + auto& kernel = kd.kernels[3]; + FillCLKernelData(kernel, + dispatchData.stage_2, + params.engineInfo, + finalKernelName, + jit, + entry_point, + "", + false, + false, + 0, + 0); + kernel.params.arguments.clear(); // Clear original output argument + kernel.params.arguments.push_back({ArgumentDescriptor::Types::INTERNAL_BUFFER, 0}); + kernel.params.arguments.push_back({ArgumentDescriptor::Types::INTERNAL_BUFFER, 2}); + kd.internalBuffers.push_back(params.outputs[0].Batch().v * Align(params.outputs[0].Feature().v, fsv) * + intermidiate_bytes); + } + { // Final + auto cldnn_jit = GetJitConstants(orgParams, dispatchData.stage_final); + cldnn_jit.AddConstant(MakeJitConstant("MVN_KERNEL_MAIN", 1)); + cldnn_jit.AddConstant(MakeJitConstant("PRECALC_MEAN", 1)); + cldnn_jit.AddConstant(MakeJitConstant("PRECALC_VARIANCE", params.mvnNormalizeVariance)); + auto entry_point = GetEntryPoint(finalKernelName, orgParams.layerID, params, entry_part_id); + auto jit = CreateJit(finalKernelName, cldnn_jit, entry_point); + auto& kernel = kd.kernels[kernels_num - 1]; + FillCLKernelData(kernel, + dispatchData.stage_final, + params.engineInfo, + finalKernelName, + jit, + entry_point, + "", + false, + false, + 1, + GetFusedPrimitiveInputsCount(params)); + kernel.params.arguments.push_back({ArgumentDescriptor::Types::INTERNAL_BUFFER, 1}); + if (params.mvnNormalizeVariance) { + kernel.params.arguments.push_back({ArgumentDescriptor::Types::INTERNAL_BUFFER, 2}); + } + } + kd.internalBufferDataType = Datatype::F32; + + GetUpdateDispatchDataFunc(kd); + + return {kd}; +} + +KernelsData MVNKernel_b_fs_yx_fsv16::GetDynamicMultiStageKernelsData(const mvn_params& params) const { + if (!Validate(params)) + return {}; + + const mvn_params& orgParams = static_cast(params); + bool has_variance = params.mvnNormalizeVariance; + size_t multi_stage_kernels = has_variance ? 5 : 3; + size_t total_kernels = 1 + multi_stage_kernels; // kernel[0]=basic + multi-stage + + KernelData kd = KernelData::Default(params, total_kernels); + auto finalKernelName = GetKernelName(orgParams); + size_t entry_part_id = 0; + + // Helper to add items_num scalar argument to a kernel + auto add_items_num_scalar = [](clKernelData& kernel) { + ScalarDescriptor items_num; + items_num.t = ScalarDescriptor::Types::UINT32; + items_num.v.u32 = 1; // placeholder; updated at runtime + kernel.params.scalars.push_back(items_num); + kernel.params.arguments.push_back({ArgumentDescriptor::Types::SCALAR, 0}); + }; + + // ---- Kernel[0]: Basic mode (single-kernel mean+var+normalize) ---- + { + auto dispatchData = SetDefault(orgParams); + auto cldnn_jit = GetJitConstants(orgParams, dispatchData); + auto entry_point = GetEntryPoint(finalKernelName, orgParams.layerID, params, entry_part_id++); + auto jit = CreateJit(finalKernelName, cldnn_jit, entry_point); + auto& kernel = kd.kernels[0]; + FillCLKernelData(kernel, + dispatchData, + params.engineInfo, + finalKernelName, + jit, + entry_point, + "", + false, + false, + 1, + GetFusedPrimitiveInputsCount(params), + 1, + orgParams.is_shape_agnostic); + add_items_num_scalar(kernel); + } + + // ---- Multi-stage kernels [1..N] ---- + auto dispatchData = SetDefaultForMulti(orgParams); + + // Kernel[1]: Mean first stage + { + auto cldnn_jit = GetJitConstants(orgParams, dispatchData.stage_1); + cldnn_jit.AddConstant(MakeJitConstant("MVN_KERNEL_MEAN_1", 1)); + auto entry_point = GetEntryPoint(finalKernelName, orgParams.layerID, params, entry_part_id++); + auto jit = CreateJit(finalKernelName, cldnn_jit, entry_point); + auto& kernel = kd.kernels[1]; + FillCLKernelData(kernel, + dispatchData.stage_1, + params.engineInfo, + finalKernelName, + jit, + entry_point, + "", + false, + false, + 0, + 0); + kernel.params.arguments.clear(); + kernel.params.arguments.push_back({ArgumentDescriptor::Types::SHAPE_INFO, 0}); + kernel.params.arguments.push_back({ArgumentDescriptor::Types::INPUT, 0}); + kernel.params.arguments.push_back({ArgumentDescriptor::Types::INTERNAL_BUFFER, 0}); + add_items_num_scalar(kernel); + } + + // Kernel[2]: Mean second stage + { + auto cldnn_jit = GetJitConstants(orgParams, dispatchData.stage_2); + cldnn_jit.AddConstant(MakeJitConstant("MVN_KERNEL_MEAN_2", 1)); + auto entry_point = GetEntryPoint(finalKernelName, orgParams.layerID, params, entry_part_id++); + auto jit = CreateJit(finalKernelName, cldnn_jit, entry_point); + auto& kernel = kd.kernels[2]; + FillCLKernelData(kernel, + dispatchData.stage_2, + params.engineInfo, + finalKernelName, + jit, + entry_point, + "", + false, + false, + 0, + 0); + kernel.params.arguments.clear(); + kernel.params.arguments.push_back({ArgumentDescriptor::Types::SHAPE_INFO, 0}); + kernel.params.arguments.push_back({ArgumentDescriptor::Types::INTERNAL_BUFFER, 0}); + kernel.params.arguments.push_back({ArgumentDescriptor::Types::INTERNAL_BUFFER, 1}); + add_items_num_scalar(kernel); + } + + if (has_variance) { + // Kernel[3]: Variance first stage + { + auto cldnn_jit = GetJitConstants(orgParams, dispatchData.stage_1); + cldnn_jit.AddConstant(MakeJitConstant("MVN_KERNEL_VAR_1", 1)); + auto entry_point = GetEntryPoint(finalKernelName, orgParams.layerID, params, entry_part_id++); + auto jit = CreateJit(finalKernelName, cldnn_jit, entry_point); + auto& kernel = kd.kernels[3]; + FillCLKernelData(kernel, + dispatchData.stage_1, + params.engineInfo, + finalKernelName, + jit, + entry_point, + "", + false, + false, + 0, + 0); + kernel.params.arguments.clear(); + kernel.params.arguments.push_back({ArgumentDescriptor::Types::SHAPE_INFO, 0}); + kernel.params.arguments.push_back({ArgumentDescriptor::Types::INPUT, 0}); + kernel.params.arguments.push_back({ArgumentDescriptor::Types::INTERNAL_BUFFER, 1}); + kernel.params.arguments.push_back({ArgumentDescriptor::Types::INTERNAL_BUFFER, 0}); + add_items_num_scalar(kernel); + } + + // Kernel[4]: Variance second stage + { + auto cldnn_jit = GetJitConstants(orgParams, dispatchData.stage_2); + cldnn_jit.AddConstant(MakeJitConstant("MVN_KERNEL_VAR_2", 1)); + auto entry_point = GetEntryPoint(finalKernelName, orgParams.layerID, params, entry_part_id++); + auto jit = CreateJit(finalKernelName, cldnn_jit, entry_point); + auto& kernel = kd.kernels[4]; + FillCLKernelData(kernel, + dispatchData.stage_2, + params.engineInfo, + finalKernelName, + jit, + entry_point, + "", + false, + false, + 0, + 0); + kernel.params.arguments.clear(); + kernel.params.arguments.push_back({ArgumentDescriptor::Types::SHAPE_INFO, 0}); + kernel.params.arguments.push_back({ArgumentDescriptor::Types::INTERNAL_BUFFER, 0}); + kernel.params.arguments.push_back({ArgumentDescriptor::Types::INTERNAL_BUFFER, 2}); + add_items_num_scalar(kernel); + } + } + + // Final kernel (last index) + { + auto cldnn_jit = GetJitConstants(orgParams, dispatchData.stage_final); + cldnn_jit.AddConstant(MakeJitConstant("MVN_KERNEL_MAIN", 1)); + cldnn_jit.AddConstant(MakeJitConstant("PRECALC_MEAN", 1)); + cldnn_jit.AddConstant(MakeJitConstant("PRECALC_VARIANCE", params.mvnNormalizeVariance)); + auto entry_point = GetEntryPoint(finalKernelName, orgParams.layerID, params, entry_part_id); + auto jit = CreateJit(finalKernelName, cldnn_jit, entry_point); + auto& kernel = kd.kernels[total_kernels - 1]; + FillCLKernelData(kernel, + dispatchData.stage_final, + params.engineInfo, + finalKernelName, + jit, + entry_point, + "", + false, + false, + 1, + GetFusedPrimitiveInputsCount(params), + 1, + orgParams.is_shape_agnostic); + kernel.params.arguments.push_back({ArgumentDescriptor::Types::INTERNAL_BUFFER, 1}); + if (has_variance) { + kernel.params.arguments.push_back({ArgumentDescriptor::Types::INTERNAL_BUFFER, 2}); + } + add_items_num_scalar(kernel); + } + + // Internal buffers with placeholder sizes (resized at runtime) + kd.internalBuffers.push_back(InternalBuffer(1)); // buf0: partial sums + kd.internalBuffers.push_back(InternalBuffer(1)); // buf1: mean values + if (has_variance) { + kd.internalBuffers.push_back(InternalBuffer(1)); // buf2: variance values + } + kd.internalBufferDataType = Datatype::F32; + + GetUpdateDispatchDataFunc(kd); + + return {kd}; +} + +void MVNKernel_b_fs_yx_fsv16::GetUpdateDispatchDataFunc(KernelData& kd) const { + kd.update_dispatch_data_func = [](const Params& params, KernelData& kd) { + const auto& prim_params = static_cast(params); + constexpr size_t local_fsv = 16; + constexpr size_t local_simd = 16; + constexpr size_t local_pref_work_groups = 16; + constexpr size_t intermediate_bytes = 4; + + auto items_num = prim_params.outputs[0].X().v + * prim_params.outputs[0].Y().v + * prim_params.outputs[0].Z().v; + auto batch = prim_params.outputs[0].Batch().v; + auto feature = prim_params.outputs[0].Feature().v; + + // Update items_num scalar in all kernels that have it + ScalarDescriptor items_num_scalar; + items_num_scalar.t = ScalarDescriptor::Types::UINT32; + items_num_scalar.v.u32 = static_cast(items_num); + for (size_t i = 0; i < kd.kernels.size(); ++i) { + if (!kd.kernels[i].params.scalars.empty()) { + kd.kernels[i].params.scalars[0] = items_num_scalar; + } + } + + if (kd.kernels.size() == 1) { + // Basic mode only (single kernel) + kd.kernels[0].params.workGroups.global[1] = CeilDiv(feature, local_fsv); + kd.kernels[0].params.workGroups.global[2] = batch; + kd.kernels[0].skip_execution = KernelData::SkipKernelExecution(prim_params); + } else { + // Dynamic multi-stage: kernel[0]=basic, kernel[1..]=multi-stage + size_t num_kernels = kd.kernels.size(); + bool has_variance = num_kernels > 4; // 6 = basic + 5 stages + + auto max_wg = prim_params.engineInfo.maxWorkGroupSize; + auto slm_per_sg = local_fsv * 4; + auto max_slm = prim_params.engineInfo.maxLocalMemSize; + auto max_sgs = max_slm / slm_per_sg; + auto max_lws = std::min(max_wg, max_sgs * local_simd); + + // Runtime decision: use multi-stage when spatial size is large enough + bool enough_slm = max_lws / local_simd * local_simd * slm_per_sg <= max_slm; + bool enough_lws = max_lws / local_simd >= 1; + bool enough_items = items_num >= max_lws / local_simd * local_simd * local_pref_work_groups; + bool use_multi_stage = enough_slm && enough_lws && enough_items; + + // Skip execution flags + kd.kernels[0].skip_execution = use_multi_stage || KernelData::SkipKernelExecution(prim_params); + for (size_t i = 1; i < num_kernels; ++i) { + kd.kernels[i].skip_execution = !use_multi_stage || KernelData::SkipKernelExecution(prim_params); + } + + // Always update basic mode GWS (in case it gets selected) + kd.kernels[0].params.workGroups.global[1] = CeilDiv(feature, local_fsv); + kd.kernels[0].params.workGroups.global[2] = batch; + + if (use_multi_stage) { + auto lws = std::max(std::min(items_num, max_lws) / local_simd, (size_t)1) * local_simd; + size_t item_groups = local_pref_work_groups; + + // Stage 1 (mean_1): kernel[1] + kd.kernels[1].params.workGroups.global = {lws * item_groups, CeilDiv(feature, local_fsv), batch}; + kd.kernels[1].params.workGroups.local[0] = lws; + + // Stage 2 (mean_2): kernel[2] + auto stage2_lws = std::max(std::min(item_groups, max_lws) / local_simd, (size_t)1) * local_simd; + kd.kernels[2].params.workGroups.global = {stage2_lws, CeilDiv(feature, local_fsv), batch}; + kd.kernels[2].params.workGroups.local[0] = stage2_lws; + + if (has_variance) { + // var_1: kernel[3] + kd.kernels[3].params.workGroups.global = {lws * item_groups, CeilDiv(feature, local_fsv), batch}; + kd.kernels[3].params.workGroups.local[0] = lws; + + // var_2: kernel[4] + kd.kernels[4].params.workGroups.global = {stage2_lws, CeilDiv(feature, local_fsv), batch}; + kd.kernels[4].params.workGroups.local[0] = stage2_lws; + + // final: kernel[5] + auto final_gws0 = std::max(items_num / local_simd, (size_t)1) * local_simd; + kd.kernels[5].params.workGroups.global = {final_gws0, CeilDiv(feature, local_fsv), batch}; + } else { + // final: kernel[3] + auto final_gws0 = std::max(items_num / local_simd, (size_t)1) * local_simd; + kd.kernels[3].params.workGroups.global = {final_gws0, CeilDiv(feature, local_fsv), batch}; + } + + // Resize internal buffers if needed + size_t buf0_size = batch * Align(feature, local_fsv) * item_groups * intermediate_bytes; + size_t buf1_size = batch * Align(feature, local_fsv) * intermediate_bytes; + + if (kd.internalBuffers.size() >= 2 && + (kd.internalBuffers[0].byte_count < buf0_size || + kd.internalBuffers[1].byte_count < buf1_size)) { + kd.internalBuffers.clear(); + kd.internalBuffers.push_back(buf0_size); + kd.internalBuffers.push_back(buf1_size); + if (has_variance) { + kd.internalBuffers.push_back(buf1_size); + } + } + } + } + }; +} + +KernelsData MVNKernel_b_fs_yx_fsv16::GetKernelsData(const Params& params) const { + const mvn_params& orgParams = static_cast(params); + + // For dynamic shapes, compile both basic and multi-stage kernels; + // the runtime update_dispatch_data_func selects the optimal path. + if (orgParams.has_dynamic_tensors()) { + return GetDynamicMultiStageKernelsData(orgParams); + } + + auto max_slm = params.engineInfo.maxLocalMemSize; + auto slm_per_sg = fsv * 4; + auto max_lws = params.engineInfo.maxWorkGroupSize; + auto items_num = orgParams.outputs[0].X().v * orgParams.outputs[0].Y().v * orgParams.outputs[0].Z().v; + + auto enough_slm = max_lws / simd * simd * slm_per_sg <= max_slm; + auto enough_lws = max_lws / simd >= 1; + auto enough_items = items_num >= max_lws / simd * simd * pref_work_groups; + + if (enough_slm && enough_lws && enough_items) + return GetMultiStageKernelsData(orgParams); + else + return GetCommonKernelsData(params); +} + +KernelsPriority MVNKernel_b_fs_yx_fsv16::GetKernelsPriority(const Params& /*params*/) const { + return FORCE_PRIORITY_4; +} +} // namespace kernel_selector diff --git a/src/plugins/intel_gpu/src/kernel_selector/kernels/mvn/mvn_kernel_b_fs_yx_fsv16_imad.hpp b/src/plugins/intel_gpu/src/kernel_selector/kernels/mvn/mvn_kernel_b_fs_yx_fsv16.hpp similarity index 77% rename from src/plugins/intel_gpu/src/kernel_selector/kernels/mvn/mvn_kernel_b_fs_yx_fsv16_imad.hpp rename to src/plugins/intel_gpu/src/kernel_selector/kernels/mvn/mvn_kernel_b_fs_yx_fsv16.hpp index 06282a0cc261..43b6f840fb63 100644 --- a/src/plugins/intel_gpu/src/kernel_selector/kernels/mvn/mvn_kernel_b_fs_yx_fsv16_imad.hpp +++ b/src/plugins/intel_gpu/src/kernel_selector/kernels/mvn/mvn_kernel_b_fs_yx_fsv16.hpp @@ -9,11 +9,11 @@ #include namespace kernel_selector { -class MVNKernel_b_fs_yx_fsv16_imad : public MVNKernelBase { +class MVNKernel_b_fs_yx_fsv16 : public MVNKernelBase { public: using Parent = MVNKernelBase; - MVNKernel_b_fs_yx_fsv16_imad() : MVNKernelBase("mvn_gpu_b_fs_yx_fsv16_imad") {} - virtual ~MVNKernel_b_fs_yx_fsv16_imad() {} + MVNKernel_b_fs_yx_fsv16() : MVNKernelBase("mvn_gpu_b_fs_yx_fsv16") {} + virtual ~MVNKernel_b_fs_yx_fsv16() {} KernelsData GetKernelsData(const Params& params) const override; KernelsPriority GetKernelsPriority(const Params& params) const override; @@ -32,15 +32,18 @@ class MVNKernel_b_fs_yx_fsv16_imad : public MVNKernelBase { bool Validate(const Params&) const override; DispatchData SetDefault(const mvn_params& params) const override; JitConstants GetJitConstants(const mvn_params& params, DispatchData dispatchData) const override; + void GetUpdateDispatchDataFunc(KernelData& kd) const override; std::vector GetSupportedFusedOps() const override { return { FusedOpType::ACTIVATION, FusedOpType::QUANTIZE, - FusedOpType::ELTWISE + FusedOpType::ELTWISE, + FusedOpType::REORDER }; } KernelsData GetMultiStageKernelsData(const mvn_params& params) const; + KernelsData GetDynamicMultiStageKernelsData(const mvn_params& params) const; MultiDispatchData SetDefaultForMulti(const mvn_params& params) const; private: diff --git a/src/plugins/intel_gpu/src/kernel_selector/kernels/mvn/mvn_kernel_b_fs_yx_fsv16_imad.cpp b/src/plugins/intel_gpu/src/kernel_selector/kernels/mvn/mvn_kernel_b_fs_yx_fsv16_imad.cpp deleted file mode 100644 index 043be2dccb43..000000000000 --- a/src/plugins/intel_gpu/src/kernel_selector/kernels/mvn/mvn_kernel_b_fs_yx_fsv16_imad.cpp +++ /dev/null @@ -1,354 +0,0 @@ -// Copyright (C) 2018-2026 Intel Corporation -// SPDX-License-Identifier: Apache-2.0 -// - -#include "mvn_kernel_b_fs_yx_fsv16_imad.hpp" -#include "common_tools.h" - -#include -#include -#include - -namespace kernel_selector { - -static constexpr size_t simd = 16; -static constexpr size_t fsv = 16; -static constexpr size_t pref_work_groups = 16; - -ParamsKey MVNKernel_b_fs_yx_fsv16_imad::GetSupportedKey() const { - ParamsKey k; - - k.EnableInputDataType(Datatype::INT8); - k.EnableInputDataType(Datatype::UINT8); - - k.EnableOutputDataType(Datatype::F16); - k.EnableOutputDataType(Datatype::F32); - k.EnableOutputDataType(Datatype::INT8); - k.EnableOutputDataType(Datatype::UINT8); - - k.EnableInputLayout(DataLayout::b_fs_yx_fsv16); - k.EnableOutputLayout(DataLayout::b_fs_yx_fsv16); - k.EnableInputLayout(DataLayout::b_fs_zyx_fsv16); - k.EnableOutputLayout(DataLayout::b_fs_zyx_fsv16); - k.EnableTensorOffset(); - k.EnableTensorPitches(); - k.EnableDifferentTypes(); - k.EnableBatching(); - // TODO Add support for across channels - // k.EnableMVNMode(MVNMode::ACROSS_CHANNELS); - k.EnableMVNMode(MVNMode::WITHIN_CHANNELS); - k.EnableMVNNormalizeVariance(); - - return k; -} - -DeviceFeaturesKey MVNKernel_b_fs_yx_fsv16_imad::get_required_device_features_key(const Params& params) const { - auto k = get_common_subgroups_device_features_key(params); - k.requires_subgroup_shuffle(); - k.requires_subgroup_reduce(); - - return k; -} - -bool MVNKernel_b_fs_yx_fsv16_imad::Validate(const Params& p) const { - if (!Parent::Validate(p)) - DO_NOT_USE_THIS_KERNEL(p.layerID); - - auto params = static_cast(p); - - // TODO Add support for input padding via iterating over y (parallel or in kernel). - if (params.inputs[0].X().pad.Total() != 0 || params.inputs[0].Y().pad.Total() != 0 || - params.inputs[0].Z().pad.Total() != 0) - DO_NOT_USE_THIS_KERNEL(p.layerID); - - return true; -} - -MVNKernelBase::DispatchData MVNKernel_b_fs_yx_fsv16_imad::SetDefault(const mvn_params& params) const { - auto dispatchData = Parent::SetDefault(params); - - auto items_num = params.outputs[0].X().v * params.outputs[0].Y().v * params.outputs[0].Z().v; - auto max_wg = params.engineInfo.maxWorkGroupSize; - auto slm_per_sg = fsv * 4; - auto max_slm = params.engineInfo.maxLocalMemSize; - auto max_sgs = max_slm / slm_per_sg; - - auto max_lws = std::min(max_wg, max_sgs * simd); - - auto lws = std::max(std::min(items_num, max_lws) / simd, (size_t)1) * simd; - - dispatchData.gws[0] = lws; - dispatchData.gws[1] = CeilDiv(params.outputs[0].Feature().v, fsv); - dispatchData.gws[2] = params.outputs[0].Batch().v; - - dispatchData.lws[0] = lws; - dispatchData.lws[1] = 1; - dispatchData.lws[2] = 1; - - dispatchData.itemsNum = 1; - - return dispatchData; -} - -Datatype MVNKernel_b_fs_yx_fsv16_imad::GetAccumulatorType(const mvn_params& params) const { - const auto& input_dt = params.inputs[0].GetDType(); - - switch (input_dt) { - case Datatype::F32: - case Datatype::F16: - return Datatype::F32; - case Datatype::INT8: - case Datatype::UINT8: - return Datatype::INT32; - default: return Datatype::F32; - } -} - -JitConstants MVNKernel_b_fs_yx_fsv16_imad::GetJitConstants(const mvn_params& params, DispatchData dispatchData) const { - auto jits = Parent::GetJitConstants(params, dispatchData); - - auto activation_dt = GetActivationType(params); - jits.Merge(MakeTypeJitConstants(activation_dt, "ACTIVATION")); - jits.Merge(MakeTypeJitConstants(activation_dt, "MEAN")); - jits.Merge(MakeTypeJitConstants(GetAccumulatorType(params), "ACCUMULATOR")); - jits.AddConstant(MakeJitConstant("SIMD", simd)); - jits.AddConstant(MakeJitConstant("LWS", dispatchData.lws[0])); - jits.AddConstant(MakeJitConstant("GWS", dispatchData.gws[0])); - jits.AddConstant(MakeJitConstant("ITEM_GROUPS", dispatchData.itemsNum)); - jits.AddConstant(MakeJitConstant("INPUT_SLICE_PITCH", 16)); - if (!params.fused_ops.empty()) { - std::vector idx_order; - - if (params.inputs[0].GetDims().size() <= 4) { - idx_order = {"b", - "(f + set_idx)", - "(output_spatial / OUTPUT_SIZE_X)", - "(output_spatial % OUTPUT_SIZE_X)"}; - } else if (params.inputs[0].GetDims().size() == 5) { - idx_order = {"b", - "(f + set_idx)", - "(output_spatial / (OUTPUT_SIZE_X * OUTPUT_SIZE_Y))", - "((output_spatial / OUTPUT_SIZE_X) % OUTPUT_SIZE_Y)", - "(output_spatial % OUTPUT_SIZE_X)"}; - } - - auto conf = FusedOpsConfiguration("", idx_order, "normalized", activation_dt); - jits.Merge(MakeFusedOpsJitConstants(params, {conf})); - } - return jits; -} - -MVNKernel_b_fs_yx_fsv16_imad::MultiDispatchData MVNKernel_b_fs_yx_fsv16_imad::SetDefaultForMulti( - const mvn_params& params) const { - MultiDispatchData dispatchData; - - auto items_num = params.outputs[0].X().v * params.outputs[0].Y().v * params.outputs[0].Z().v; - auto max_wg = params.engineInfo.maxWorkGroupSize; - auto slm_per_sg = fsv * 4; - auto max_slm = params.engineInfo.maxLocalMemSize; - auto max_sgs = max_slm / slm_per_sg; - - auto max_lws = std::min(max_wg, max_sgs * simd); - auto lws = std::max(std::min(items_num, max_lws) / simd, (size_t)1) * simd; - - // TODO Check if larger number of work-groups does not provide benefit - size_t item_groups = pref_work_groups; - dispatchData.item_groups = item_groups; - - size_t stage1_lws = lws; - - dispatchData.stage_1.gws[0] = stage1_lws * item_groups; - dispatchData.stage_1.gws[1] = CeilDiv(params.outputs[0].Feature().v, fsv); - dispatchData.stage_1.gws[2] = params.outputs[0].Batch().v; - - dispatchData.stage_1.lws[0] = stage1_lws; - dispatchData.stage_1.lws[1] = 1; - dispatchData.stage_1.lws[2] = 1; - - dispatchData.stage_1.itemsNum = item_groups; - - size_t stage2_lws = std::max(std::min(item_groups, max_lws) / simd, (size_t)1) * simd; - - dispatchData.stage_2.gws[0] = stage2_lws; - dispatchData.stage_2.gws[1] = CeilDiv(params.outputs[0].Feature().v, fsv); - dispatchData.stage_2.gws[2] = params.outputs[0].Batch().v; - - dispatchData.stage_2.lws[0] = stage2_lws; - dispatchData.stage_2.lws[1] = 1; - dispatchData.stage_2.lws[2] = 1; - - dispatchData.stage_2.itemsNum = item_groups; - - dispatchData.stage_final.gws[0] = std::max(items_num / simd, (size_t)1) * simd; - dispatchData.stage_final.gws[1] = CeilDiv(params.outputs[0].Feature().v, fsv); - dispatchData.stage_final.gws[2] = params.outputs[0].Batch().v; - - dispatchData.stage_final.lws[0] = simd; - dispatchData.stage_final.lws[1] = 1; - dispatchData.stage_final.lws[2] = 1; - - dispatchData.stage_final.itemsNum = 1; - - return dispatchData; -} - -KernelsData MVNKernel_b_fs_yx_fsv16_imad::GetMultiStageKernelsData(const mvn_params& params) const { - if (!Validate(params)) - return {}; - - constexpr size_t intermidiate_bytes = 4; - const mvn_params& orgParams = static_cast(params); - - auto dispatchData = SetDefaultForMulti(orgParams); - - size_t kernels_num = params.mvnNormalizeVariance ? 5 : 3; - KernelData kd = KernelData::Default(params, kernels_num); - - auto finalKernelName = GetKernelName(orgParams); - size_t entry_part_id = 0; - { - // Mean first stage - auto cldnn_jit = GetJitConstants(orgParams, dispatchData.stage_1); - cldnn_jit.AddConstant(MakeJitConstant("MVN_KERNEL_MEAN_1", 1)); - auto entry_point = GetEntryPoint(finalKernelName, orgParams.layerID, params, entry_part_id++); - auto jit = CreateJit(finalKernelName, cldnn_jit, entry_point); - auto& kernel = kd.kernels[0]; - FillCLKernelData(kernel, - dispatchData.stage_1, - params.engineInfo, - finalKernelName, - jit, - entry_point, - "", - false, - false, - 0, - 0); - kernel.params.arguments.clear(); // Clear original output argument - kernel.params.arguments.push_back({ArgumentDescriptor::Types::INPUT, 0}); - kernel.params.arguments.push_back({ArgumentDescriptor::Types::INTERNAL_BUFFER, 0}); - kd.internalBuffers.push_back(params.outputs[0].Batch().v * Align(params.outputs[0].Feature().v, fsv) * - dispatchData.item_groups * intermidiate_bytes); - } - { - // Mean second stage - auto cldnn_jit = GetJitConstants(orgParams, dispatchData.stage_2); - cldnn_jit.AddConstant(MakeJitConstant("MVN_KERNEL_MEAN_2", 1)); - auto entry_point = GetEntryPoint(finalKernelName, orgParams.layerID, params, entry_part_id++); - auto jit = CreateJit(finalKernelName, cldnn_jit, entry_point); - auto& kernel = kd.kernels[1]; - FillCLKernelData(kernel, - dispatchData.stage_2, - params.engineInfo, - finalKernelName, - jit, - entry_point, - "", - false, - false, - 0, - 0); - kernel.params.arguments.clear(); // Clear original output argument - kernel.params.arguments.push_back({ArgumentDescriptor::Types::INTERNAL_BUFFER, 0}); - kernel.params.arguments.push_back({ArgumentDescriptor::Types::INTERNAL_BUFFER, 1}); - kd.internalBuffers.push_back(params.outputs[0].Batch().v * Align(params.outputs[0].Feature().v, fsv) * - intermidiate_bytes); - } - if (params.mvnNormalizeVariance) { - // Variance first stage - auto cldnn_jit = GetJitConstants(orgParams, dispatchData.stage_1); - cldnn_jit.AddConstant(MakeJitConstant("MVN_KERNEL_VAR_1", 1)); - auto entry_point = GetEntryPoint(finalKernelName, orgParams.layerID, params, entry_part_id++); - auto jit = CreateJit(finalKernelName, cldnn_jit, entry_point); - auto& kernel = kd.kernels[2]; - FillCLKernelData(kernel, - dispatchData.stage_1, - params.engineInfo, - finalKernelName, - jit, - entry_point, - "", - false, - false, - 0, - 0); - kernel.params.arguments.clear(); // Clear original output argument - kernel.params.arguments.push_back({ArgumentDescriptor::Types::INPUT, 0}); - kernel.params.arguments.push_back({ArgumentDescriptor::Types::INTERNAL_BUFFER, 1}); - kernel.params.arguments.push_back({ArgumentDescriptor::Types::INTERNAL_BUFFER, 0}); - } - if (params.mvnNormalizeVariance) { - // Variance second stage - auto cldnn_jit = GetJitConstants(orgParams, dispatchData.stage_2); - cldnn_jit.AddConstant(MakeJitConstant("MVN_KERNEL_VAR_2", 1)); - auto entry_point = GetEntryPoint(finalKernelName, orgParams.layerID, params, entry_part_id++); - auto jit = CreateJit(finalKernelName, cldnn_jit, entry_point); - auto& kernel = kd.kernels[3]; - FillCLKernelData(kernel, - dispatchData.stage_2, - params.engineInfo, - finalKernelName, - jit, - entry_point, - "", - false, - false, - 0, - 0); - kernel.params.arguments.clear(); // Clear original output argument - kernel.params.arguments.push_back({ArgumentDescriptor::Types::INTERNAL_BUFFER, 0}); - kernel.params.arguments.push_back({ArgumentDescriptor::Types::INTERNAL_BUFFER, 2}); - kd.internalBuffers.push_back(params.outputs[0].Batch().v * Align(params.outputs[0].Feature().v, fsv) * - intermidiate_bytes); - } - { // Final - auto cldnn_jit = GetJitConstants(orgParams, dispatchData.stage_final); - cldnn_jit.AddConstant(MakeJitConstant("MVN_KERNEL_MAIN", 1)); - cldnn_jit.AddConstant(MakeJitConstant("PRECALC_MEAN", 1)); - cldnn_jit.AddConstant(MakeJitConstant("PRECALC_VARIANCE", params.mvnNormalizeVariance)); - auto entry_point = GetEntryPoint(finalKernelName, orgParams.layerID, params, entry_part_id); - auto jit = CreateJit(finalKernelName, cldnn_jit, entry_point); - auto& kernel = kd.kernels[kernels_num - 1]; - FillCLKernelData(kernel, - dispatchData.stage_final, - params.engineInfo, - finalKernelName, - jit, - entry_point, - "", - false, - false, - 1, - GetFusedPrimitiveInputsCount(params)); - kernel.params.arguments.push_back({ArgumentDescriptor::Types::INTERNAL_BUFFER, 1}); - if (params.mvnNormalizeVariance) { - kernel.params.arguments.push_back({ArgumentDescriptor::Types::INTERNAL_BUFFER, 2}); - } - } - kd.internalBufferDataType = Datatype::F32; - - return {kd}; -} - -KernelsData MVNKernel_b_fs_yx_fsv16_imad::GetKernelsData(const Params& params) const { - const mvn_params& orgParams = static_cast(params); - - auto max_slm = params.engineInfo.maxLocalMemSize; - auto slm_per_sg = fsv * 4; - auto max_lws = params.engineInfo.maxWorkGroupSize; - auto items_num = orgParams.outputs[0].X().v * orgParams.outputs[0].Y().v * orgParams.outputs[0].Z().v; - - auto enough_slm = max_lws / simd * simd * slm_per_sg <= max_slm; - auto enough_lws = max_lws / simd >= 1; - auto enough_items = items_num >= max_lws / simd * simd * pref_work_groups; - - if (enough_slm && enough_lws && enough_items) - return GetMultiStageKernelsData(orgParams); - else - return GetCommonKernelsData(params); -} - -KernelsPriority MVNKernel_b_fs_yx_fsv16_imad::GetKernelsPriority(const Params& /*params*/) const { - return FORCE_PRIORITY_4; -} -} // namespace kernel_selector diff --git a/src/plugins/intel_gpu/src/kernel_selector/kernels/mvn/mvn_kernel_bs_fs_yx_bsv32.hpp b/src/plugins/intel_gpu/src/kernel_selector/kernels/mvn/mvn_kernel_bs_fs_yx_bsv32.hpp index 3b6d3c373faf..1bb499d39509 100644 --- a/src/plugins/intel_gpu/src/kernel_selector/kernels/mvn/mvn_kernel_bs_fs_yx_bsv32.hpp +++ b/src/plugins/intel_gpu/src/kernel_selector/kernels/mvn/mvn_kernel_bs_fs_yx_bsv32.hpp @@ -12,7 +12,7 @@ namespace kernel_selector { class MVNKernel_bs_fs_yx_bsv32 : public MVNKernelBase { public: using Parent = MVNKernelBase; - MVNKernel_bs_fs_yx_bsv32() : MVNKernelBase("mvn_gpu_b_fs_yx_fsv16_imad") {} + MVNKernel_bs_fs_yx_bsv32() : MVNKernelBase("mvn_gpu_b_fs_yx_bsv32") {} virtual ~MVNKernel_bs_fs_yx_bsv32() {} KernelsData GetKernelsData(const Params& params) const override; diff --git a/src/plugins/intel_gpu/src/kernel_selector/kernels/mvn/mvn_kernel_selector.cpp b/src/plugins/intel_gpu/src/kernel_selector/kernels/mvn/mvn_kernel_selector.cpp index 10f02cfc4ed5..d721ac695c53 100644 --- a/src/plugins/intel_gpu/src/kernel_selector/kernels/mvn/mvn_kernel_selector.cpp +++ b/src/plugins/intel_gpu/src/kernel_selector/kernels/mvn/mvn_kernel_selector.cpp @@ -5,14 +5,14 @@ #include "mvn_kernel_selector.h" #include "mvn_kernel_ref.h" #include "mvn_kernel_bfyx_opt.h" -#include "mvn_kernel_b_fs_yx_fsv16_imad.hpp" +#include "mvn_kernel_b_fs_yx_fsv16.hpp" #include "mvn_kernel_bs_fs_yx_bsv32.hpp" namespace kernel_selector { mvn_kernel_selector::mvn_kernel_selector() { Attach(); Attach(); - Attach(); + Attach(); Attach(); } diff --git a/src/plugins/intel_gpu/src/kernel_selector/kernels/non_zero/gather_nonzero_kernel_group.cpp b/src/plugins/intel_gpu/src/kernel_selector/kernels/non_zero/gather_nonzero_kernel_group.cpp new file mode 100644 index 000000000000..deb1e4ddfad1 --- /dev/null +++ b/src/plugins/intel_gpu/src/kernel_selector/kernels/non_zero/gather_nonzero_kernel_group.cpp @@ -0,0 +1,126 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "gather_nonzero_kernel_group.h" +#include "kernel_selector_utils.h" +#include + +namespace kernel_selector { +ParamsKey GatherNonzeroKernelGroup::GetSupportedKey() const { + ParamsKey k; + k.EnableInputDataType(Datatype::F16); + k.EnableInputDataType(Datatype::F32); + k.EnableInputDataType(Datatype::INT8); + k.EnableInputDataType(Datatype::UINT8); + k.EnableInputDataType(Datatype::INT32); + k.EnableInputDataType(Datatype::UINT32); + k.EnableInputDataType(Datatype::INT64); + k.EnableOutputDataType(Datatype::F16); + k.EnableOutputDataType(Datatype::F32); + k.EnableOutputDataType(Datatype::UINT8); + k.EnableOutputDataType(Datatype::INT8); + k.EnableOutputDataType(Datatype::INT32); + k.EnableOutputDataType(Datatype::UINT32); + k.EnableOutputDataType(Datatype::INT64); + k.EnableInputLayout(DataLayout::bfyx); + k.EnableOutputLayout(DataLayout::bfyx); + k.EnableInputLayout(DataLayout::bfzyx); + k.EnableOutputLayout(DataLayout::bfzyx); + k.EnableInputLayout(DataLayout::bfwzyx); + k.EnableOutputLayout(DataLayout::bfwzyx); + k.EnableTensorOffset(); + k.EnableTensorPitches(); + k.EnableBatching(); + k.EnableDifferentTypes(); + k.EnableDynamicShapesSupport(); + return k; +} + +JitConstants GatherNonzeroKernelGroup::GetJitConstants(const gather_nonzero_params& params) const { + JitConstants jit = MakeBaseParamsJitConstants(params); + const auto& input = params.inputs[0]; + jit.AddConstant(MakeJitConstant("OV_INPUT_RANK", params.ov_input_rank)); + + // Reserve 4KiB SLM for work_group_scan_exclusive_add + auto max_local_mem_size = (params.engineInfo.maxLocalMemSize - 4096) / params.outputs[0].ElementSize(); + jit.AddConstant(MakeJitConstant("MAX_LOCAL_MEM_SIZE", max_local_mem_size)); + + if (input.is_dynamic()) { + DimensionAccessHelperJit dims(input); + const std::string total_data_size = toVectorMulString({dims.x(), dims.y(), dims.z(), dims.w(), dims.f(), dims.b()}); + jit.AddConstant(MakeJitConstant("TOTAL_DATA_SIZE", total_data_size)); + } else { + jit.AddConstant(MakeJitConstant("TOTAL_DATA_SIZE", params.inputs[0].LogicalSize())); + if (params.inputs[0].LogicalSize() * params.ov_input_rank < max_local_mem_size) { + jit.AddConstant(MakeJitConstant("USE_LOCAL_MEM", 1)); + } + } + return jit; +} + +CommonDispatchData GatherNonzeroKernelGroup::SetDefault(const gather_nonzero_params& params) const { + CommonDispatchData dispatchData; + const auto& input = params.inputs[0]; + + // Set 1 work group to avoid synchornization issue for summation of nonzero counting. + size_t max_dim_size = std::min(input.LogicalSize(), params.engineInfo.maxWorkGroupSize); + dispatchData.lws = dispatchData.gws = {max_dim_size, 1, 1}; + + return dispatchData; +} + +void GatherNonzeroKernelGroup::GetUpdateDispatchDataFunc(KernelData& kd) const { + kd.update_dispatch_data_func = [this](const Params& params, KernelData& kd) { + const auto& prim_params = static_cast(params); + auto dispatchData = SetDefault(prim_params); + OPENVINO_ASSERT(kd.kernels.size() == 1, "[GPU] Invalid kernels size for update dispatch data func"); + kd.kernels[0].params.workGroups.global = dispatchData.gws; + kd.kernels[0].params.workGroups.local = dispatchData.lws; + kd.kernels[0].skip_execution = KernelData::SkipKernelExecution(prim_params); + }; +} + +KernelsData GatherNonzeroKernelGroup::GetKernelsData(const Params& params) const { + assert(params.GetType() == KernelType::GATHER_NONZERO); + + KernelData kd = KernelData::Default(params); + gather_nonzero_params& newParams = *static_cast(kd.params.get()); + + auto dispatchData = SetDefault(newParams); + auto entry_point = GetEntryPoint(kernelName, newParams.layerID, params); + auto cldnn_jit = GetJitConstants(newParams); + auto jit = CreateJit(kernelName, cldnn_jit, entry_point); + + auto& kernel = kd.kernels[0]; + + GetUpdateDispatchDataFunc(kd); + + FillCLKernelData(kernel, + dispatchData, + params.engineInfo, + kernelName, + jit, + entry_point, + "", + false, + false, + 2, + GetFusedPrimitiveInputsCount(params), + 1, + newParams.is_shape_agnostic); + + return {kd}; +} + +KernelsPriority GatherNonzeroKernelGroup::GetKernelsPriority(const Params& /*params*/) const { + return FORCE_PRIORITY_9; +} + +bool GatherNonzeroKernelGroup::Validate(const Params& p) const { + if (!KernelBaseOpenCL::Validate(p)) + DO_NOT_USE_THIS_KERNEL(p.layerID); + + return true; +} +} // namespace kernel_selector diff --git a/src/plugins/intel_gpu/src/kernel_selector/kernels/non_zero/gather_nonzero_kernel_group.h b/src/plugins/intel_gpu/src/kernel_selector/kernels/non_zero/gather_nonzero_kernel_group.h new file mode 100644 index 000000000000..d5cee31f52bc --- /dev/null +++ b/src/plugins/intel_gpu/src/kernel_selector/kernels/non_zero/gather_nonzero_kernel_group.h @@ -0,0 +1,26 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#pragma once + +#include "kernel_base_opencl.h" +#include "gather_nonzero_params.h" + +namespace kernel_selector { +class GatherNonzeroKernelGroup : public KernelBaseOpenCL { +public: + GatherNonzeroKernelGroup() : KernelBaseOpenCL("gather_nonzero_group") {} + virtual ~GatherNonzeroKernelGroup() {} + + virtual JitConstants GetJitConstants(const gather_nonzero_params& params) const; + virtual CommonDispatchData SetDefault(const gather_nonzero_params& params) const; + KernelsData GetKernelsData(const Params& params) const override; + KernelsPriority GetKernelsPriority(const Params& params) const override; + ParamsKey GetSupportedKey() const override; + +protected: + bool Validate(const Params& pp) const override; + void GetUpdateDispatchDataFunc(KernelData& kd) const override; +}; +} // namespace kernel_selector diff --git a/src/plugins/intel_gpu/src/kernel_selector/kernels/non_zero/gather_nonzero_kernel_ref.h b/src/plugins/intel_gpu/src/kernel_selector/kernels/non_zero/gather_nonzero_kernel_ref.h index 13e8b31d5316..6f712bd25436 100644 --- a/src/plugins/intel_gpu/src/kernel_selector/kernels/non_zero/gather_nonzero_kernel_ref.h +++ b/src/plugins/intel_gpu/src/kernel_selector/kernels/non_zero/gather_nonzero_kernel_ref.h @@ -5,16 +5,9 @@ #pragma once #include "kernel_base_opencl.h" +#include "gather_nonzero_params.h" namespace kernel_selector { -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// gather_nonzero_params -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -struct gather_nonzero_params : public base_params { - gather_nonzero_params() : base_params(KernelType::GATHER_NONZERO) {} - int32_t ov_input_rank = -1; -}; - class GatherNonzeroKernelRef : public KernelBaseOpenCL { public: GatherNonzeroKernelRef() : KernelBaseOpenCL("gather_nonzero_ref") {} diff --git a/src/plugins/intel_gpu/src/kernel_selector/kernels/non_zero/gather_nonzero_kernel_selector.cpp b/src/plugins/intel_gpu/src/kernel_selector/kernels/non_zero/gather_nonzero_kernel_selector.cpp index 3b9140c54889..1c6868a5b041 100644 --- a/src/plugins/intel_gpu/src/kernel_selector/kernels/non_zero/gather_nonzero_kernel_selector.cpp +++ b/src/plugins/intel_gpu/src/kernel_selector/kernels/non_zero/gather_nonzero_kernel_selector.cpp @@ -3,11 +3,15 @@ // #include "gather_nonzero_kernel_selector.h" +#include "gather_nonzero_kernel_group.h" #include "gather_nonzero_kernel_ref.h" namespace kernel_selector { -gather_nonzero_kernel_selector::gather_nonzero_kernel_selector() { Attach(); } +gather_nonzero_kernel_selector::gather_nonzero_kernel_selector() { + Attach(); + Attach(); +} KernelsData gather_nonzero_kernel_selector::GetBestKernels(const Params& params) const { return GetNaiveBestKernel(params, KernelType::GATHER_NONZERO); diff --git a/src/plugins/intel_gpu/src/kernel_selector/kernels/non_zero/gather_nonzero_params.h b/src/plugins/intel_gpu/src/kernel_selector/kernels/non_zero/gather_nonzero_params.h new file mode 100644 index 000000000000..ed1e50d3056a --- /dev/null +++ b/src/plugins/intel_gpu/src/kernel_selector/kernels/non_zero/gather_nonzero_params.h @@ -0,0 +1,14 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#pragma once + +#include "kernel_base_opencl.h" + +namespace kernel_selector { +struct gather_nonzero_params : public base_params { + gather_nonzero_params() : base_params(KernelType::GATHER_NONZERO) {} + int32_t ov_input_rank = -1; +}; +} // namespace kernel_selector diff --git a/src/plugins/intel_gpu/src/kernel_selector/kernels/permute/permute_kernel_f_y_axes.cpp b/src/plugins/intel_gpu/src/kernel_selector/kernels/permute/permute_kernel_f_y_axes.cpp index c80869d666d0..87bc925a3a77 100644 --- a/src/plugins/intel_gpu/src/kernel_selector/kernels/permute/permute_kernel_f_y_axes.cpp +++ b/src/plugins/intel_gpu/src/kernel_selector/kernels/permute/permute_kernel_f_y_axes.cpp @@ -63,9 +63,13 @@ size_t GetTileWidth(const permute_params& params) { min_divisor = std::min(min_divisor, cSimpleMemCopyOpDivider); } - // i64 only supports tile size 4 + // i64 uses a smaller tile to reduce register/SLM pressure, but only if + // the resulting subgroup size is supported by the platform (Xe2+ lacks SIMD8). if ((input_type == Datatype::INT64) || (output_type == Datatype::INT64)) { - min_divisor = min_divisor >= 4 ? min_divisor / 2 : min_divisor; + size_t halved = min_divisor >= 4 ? min_divisor / 2 : min_divisor; + const auto& supported = params.engineInfo.supportedSimdSizes; + if (std::any_of(supported.begin(), supported.end(), [halved](size_t s) { return s == halved; }) || supported.empty()) + min_divisor = halved; } if (input_type == Datatype::F16) { min_divisor = min_divisor * 2; diff --git a/src/plugins/intel_gpu/src/kernel_selector/kernels/pooling/pooling_kernel_gpu_b_fs_yx_fsv16.cpp b/src/plugins/intel_gpu/src/kernel_selector/kernels/pooling/pooling_kernel_gpu_b_fs_yx_fsv16.cpp index 5d93026a288e..ffb4f500a98b 100644 --- a/src/plugins/intel_gpu/src/kernel_selector/kernels/pooling/pooling_kernel_gpu_b_fs_yx_fsv16.cpp +++ b/src/plugins/intel_gpu/src/kernel_selector/kernels/pooling/pooling_kernel_gpu_b_fs_yx_fsv16.cpp @@ -48,7 +48,7 @@ size_t PoolingKernel_b_fs_yx_fsv16::GetBlockSize(const pooling_params& params) c size_t PoolingKernel_b_fs_yx_fsv16::GetSimdSize(const pooling_params& params) const { auto& out = params.outputs[0]; // Use smaller simd size in case of global pooling and small channels count to have more threads - if (out.X().v == 1 && out.Y().v == 1 && out.Feature().v < 64) + if (out.X().v == 1 && out.Y().v == 1 && out.Feature().v < 64 && IsSIMDSizeSupported(params.engineInfo, 8)) return 8; else return 16; diff --git a/src/plugins/intel_gpu/src/kernel_selector/kernels/reorder/reorder_kernel_bfyx_to_blocked_format.cpp b/src/plugins/intel_gpu/src/kernel_selector/kernels/reorder/reorder_kernel_bfyx_to_blocked_format.cpp index 5b965cb67a45..1a400c6488e6 100644 --- a/src/plugins/intel_gpu/src/kernel_selector/kernels/reorder/reorder_kernel_bfyx_to_blocked_format.cpp +++ b/src/plugins/intel_gpu/src/kernel_selector/kernels/reorder/reorder_kernel_bfyx_to_blocked_format.cpp @@ -40,6 +40,7 @@ ParamsKey ReorderKernel_bfyx_to_blocked_format::GetSupportedKey() const { k.EnableBatching(); k.EnableTensorOffset(); k.EnableTensorPitches(); + k.EnableDynamicShapesSupport(); return k; } @@ -119,7 +120,9 @@ static inline size_t GetTileSize(const reorder_params& params) { return MIN_TILE_SIZE; } - if (params.inputs[0].Feature().v < DEFAULT_TILE_SIZE) { + // For shape-agnostic (dynamic shapes), Feature count is unknown at compile time. + // Use DEFAULT_TILE_SIZE; remainder path handles small features. + if (!params.is_shape_agnostic && params.inputs[0].Feature().v < DEFAULT_TILE_SIZE) { return MIN_TILE_SIZE; } @@ -178,53 +181,93 @@ CommonDispatchData ReorderKernel_bfyx_to_blocked_format::SetDefault(const reorde JitConstants ReorderKernel_bfyx_to_blocked_format::GetJitConstants(const reorder_params& params) const { auto jit = ReorderKernelBase::GetJitConstants(params); - const size_t b = params.inputs[0].Batch().v; - const size_t f = params.inputs[0].Feature().v; - const size_t x = params.inputs[0].X().v; const size_t tile_size = GetTileSize(params); const size_t input_ndims = params.inputs[0].GetDims().size(); const size_t fsv_alignment = GetFsvAlignment(params); - const auto& gws = GetGWS(params); - const auto& lws = GetBestLwsFromGws(params, gws, tile_size, tile_size); - const uint64_t total_lws = lws[0] * lws[1] * lws[2]; - jit.AddConstant(MakeJitConstant("INPUT0_TILED_ORDER", GetTiledInputOrder(input_ndims))); - jit.AddConstant(MakeJitConstant("INPUT0_FEATURE_SLICE_NUM", CeilDiv(f, fsv_alignment))); jit.AddConstant(MakeJitConstant("TILE_SIZE", tile_size)); jit.AddConstant(MakeJitConstant("FSV_ALIGNMENT", fsv_alignment)); - jit.AddConstant(MakeJitConstant("TRANS_BUF_SIZE", tile_size * total_lws)); if (params.outputs[0].GetLayout() == DataLayout::fs_b_yx_fsv32) { jit.AddConstant(MakeJitConstant("FS_B_YX_FSV", 1)); } - if (params.outputs[0].GetLayout() == DataLayout::bs_fs_yx_bsv16_fsv16 || + const bool is_double_blocked = params.outputs[0].GetLayout() == DataLayout::bs_fs_yx_bsv16_fsv16 || params.outputs[0].GetLayout() == DataLayout::bs_fs_yx_bsv16_fsv32 || params.outputs[0].GetLayout() == DataLayout::bs_fs_zyx_bsv16_fsv16 || params.outputs[0].GetLayout() == DataLayout::bs_fs_zyx_bsv16_fsv32 || params.outputs[0].GetLayout() == DataLayout::bs_fs_zyx_bsv32_fsv16 || - params.outputs[0].GetLayout() == DataLayout::bs_fs_zyx_bsv32_fsv32) { - const size_t bsv_alignment = GetBsvAlignment(params); - jit.AddConstant(MakeJitConstant("DOUBLE_BLOCKED_FORMAT", 1)); - jit.AddConstant(MakeJitConstant("INPUT0_BATCH_SLICE_NUM", CeilDiv(b, bsv_alignment))); - jit.AddConstant(MakeJitConstant("BSV_ALIGNMENT", bsv_alignment)); - } - - // whether F is tile_size-aligned - if (f % tile_size == 0) { - jit.AddConstant(MakeJitConstant("F_NO_REMAINDER_CONDITION", "(f < INPUT0_FEATURE_NUM)")); + params.outputs[0].GetLayout() == DataLayout::bs_fs_zyx_bsv32_fsv32; + + if (params.is_shape_agnostic) { + // Runtime expressions for shape-dependent constants + jit.AddConstant(MakeJitConstant("INPUT0_FEATURE_SLICE_NUM", + "((INPUT0_FEATURE_NUM + " + std::to_string(fsv_alignment) + " - 1) / " + std::to_string(fsv_alignment) + ")")); + + // TRANS_BUF_SIZE: use maximum possible LWS product to guarantee SLM is large enough at any runtime shape + const size_t elem_size = params.outputs[0].ElementSize(); + const size_t max_local_mem_size = params.engineInfo.maxLocalMemSize; + const size_t max_work_group_size = params.engineInfo.maxWorkGroupSize; + size_t max_num_work_items = std::min(max_work_group_size, max_local_mem_size / (elem_size * tile_size * tile_size)); + jit.AddConstant(MakeJitConstant("TRANS_BUF_SIZE", tile_size * max_num_work_items)); + + // F remainder: always emit both paths (remainder size unknown at compile time) + std::string ts = std::to_string(tile_size); + jit.AddConstant(MakeJitConstant("F_REMAINDER_SIZE", "(INPUT0_FEATURE_NUM % " + ts + ")")); + jit.AddConstant(MakeJitConstant("F_REMAINDER_CONDITION", + "(f >= (INPUT0_FEATURE_NUM - (INPUT0_FEATURE_NUM % " + ts + "))) && (f < INPUT0_FEATURE_NUM) && ((INPUT0_FEATURE_NUM % " + ts + ") != 0)")); + jit.AddConstant(MakeJitConstant("F_NO_REMAINDER_CONDITION", + "((f < INPUT0_FEATURE_NUM) && ((INPUT0_FEATURE_NUM % " + ts + ") == 0 || f < (INPUT0_FEATURE_NUM - (INPUT0_FEATURE_NUM % " + ts + "))))")); + + // X remainder: always emit both paths + jit.AddConstant(MakeJitConstant("X_REMAINDER_SIZE", "(INPUT0_SIZE_X % " + ts + ")")); + jit.AddConstant(MakeJitConstant("X_REMAINDER_CONDITION", + "(x >= (INPUT0_SIZE_X - (INPUT0_SIZE_X % " + ts + "))) && (x < INPUT0_SIZE_X) && ((INPUT0_SIZE_X % " + ts + ") != 0)")); + jit.AddConstant(MakeJitConstant("X_NO_REMAINDER_CONDITION", + "((x < INPUT0_SIZE_X) && ((INPUT0_SIZE_X % " + ts + ") == 0 || x < (INPUT0_SIZE_X - (INPUT0_SIZE_X % " + ts + "))))")); + + if (is_double_blocked) { + const size_t bsv_alignment = GetBsvAlignment(params); + jit.AddConstant(MakeJitConstant("DOUBLE_BLOCKED_FORMAT", 1)); + jit.AddConstant(MakeJitConstant("INPUT0_BATCH_SLICE_NUM", + "((INPUT0_BATCH_NUM + " + std::to_string(bsv_alignment) + " - 1) / " + std::to_string(bsv_alignment) + ")")); + jit.AddConstant(MakeJitConstant("BSV_ALIGNMENT", bsv_alignment)); + } } else { - jit.AddConstant(MakeJitConstant("F_REMAINDER_SIZE", f % tile_size)); - jit.AddConstant(MakeJitConstant("F_REMAINDER_CONDITION", "(f >= (INPUT0_FEATURE_NUM - F_REMAINDER_SIZE)) && (f < INPUT0_FEATURE_NUM)")); - jit.AddConstant(MakeJitConstant("F_NO_REMAINDER_CONDITION", "(f < (INPUT0_FEATURE_NUM - F_REMAINDER_SIZE))")); - } + const size_t b = params.inputs[0].Batch().v; + const size_t f = params.inputs[0].Feature().v; + const size_t x = params.inputs[0].X().v; + + jit.AddConstant(MakeJitConstant("INPUT0_FEATURE_SLICE_NUM", CeilDiv(f, fsv_alignment))); + + const auto& gws = GetGWS(params); + const auto& lws = GetBestLwsFromGws(params, gws, tile_size, tile_size); + const uint64_t total_lws = lws[0] * lws[1] * lws[2]; + jit.AddConstant(MakeJitConstant("TRANS_BUF_SIZE", tile_size * total_lws)); + + if (is_double_blocked) { + const size_t bsv_alignment = GetBsvAlignment(params); + jit.AddConstant(MakeJitConstant("DOUBLE_BLOCKED_FORMAT", 1)); + jit.AddConstant(MakeJitConstant("INPUT0_BATCH_SLICE_NUM", CeilDiv(b, bsv_alignment))); + jit.AddConstant(MakeJitConstant("BSV_ALIGNMENT", bsv_alignment)); + } - // whether x is tile_size-aligned - if (x % tile_size != 0) { - jit.AddConstant(MakeJitConstant("X_REMAINDER_SIZE", x % tile_size)); - jit.AddConstant(MakeJitConstant("X_REMAINDER_CONDITION", "(x >= (INPUT0_SIZE_X - X_REMAINDER_SIZE)) && (x < INPUT0_SIZE_X)")); - jit.AddConstant(MakeJitConstant("X_NO_REMAINDER_CONDITION", "(x < (INPUT0_SIZE_X - X_REMAINDER_SIZE))")); + // whether F is tile_size-aligned + if (f % tile_size == 0) { + jit.AddConstant(MakeJitConstant("F_NO_REMAINDER_CONDITION", "(f < INPUT0_FEATURE_NUM)")); + } else { + jit.AddConstant(MakeJitConstant("F_REMAINDER_SIZE", f % tile_size)); + jit.AddConstant(MakeJitConstant("F_REMAINDER_CONDITION", "(f >= (INPUT0_FEATURE_NUM - F_REMAINDER_SIZE)) && (f < INPUT0_FEATURE_NUM)")); + jit.AddConstant(MakeJitConstant("F_NO_REMAINDER_CONDITION", "(f < (INPUT0_FEATURE_NUM - F_REMAINDER_SIZE))")); + } + + // whether x is tile_size-aligned + if (x % tile_size != 0) { + jit.AddConstant(MakeJitConstant("X_REMAINDER_SIZE", x % tile_size)); + jit.AddConstant(MakeJitConstant("X_REMAINDER_CONDITION", "(x >= (INPUT0_SIZE_X - X_REMAINDER_SIZE)) && (x < INPUT0_SIZE_X)")); + jit.AddConstant(MakeJitConstant("X_NO_REMAINDER_CONDITION", "(x < (INPUT0_SIZE_X - X_REMAINDER_SIZE))")); + } } return jit; @@ -275,6 +318,12 @@ bool ReorderKernel_bfyx_to_blocked_format::Validate(const Params& p) const { KernelsPriority ReorderKernel_bfyx_to_blocked_format::GetKernelsPriority(const Params& p) const { const reorder_params& params = static_cast(p); + + // For dynamic shapes, skip shape-dependent heuristics + if (params.is_shape_agnostic) { + return FORCE_PRIORITY_5; + } + const auto& input = params.inputs[0]; const auto& output = params.outputs[0]; diff --git a/src/plugins/intel_gpu/src/kernel_selector/kernels/reorder/reorder_kernel_fast_b1.cpp b/src/plugins/intel_gpu/src/kernel_selector/kernels/reorder/reorder_kernel_fast_b1.cpp index 5bb216bc07c1..378b2f887e00 100644 --- a/src/plugins/intel_gpu/src/kernel_selector/kernels/reorder/reorder_kernel_fast_b1.cpp +++ b/src/plugins/intel_gpu/src/kernel_selector/kernels/reorder/reorder_kernel_fast_b1.cpp @@ -5,6 +5,8 @@ #include "reorder_kernel_fast_b1.h" #include "kernel_selector_utils.h" +#include + namespace kernel_selector { ParamsKey ReorderKernelFastBatch1::GetSupportedKey() const { ParamsKey k; @@ -58,6 +60,20 @@ bool ReorderKernelFastBatch1::Validate(const Params& p) const { return true; } +// Returns the feature sub-vector size for blocked output formats, or 0 for non-blocked formats. +static size_t GetOutputFeatureBlockSize(DataLayout layout) { + switch (layout) { + case DataLayout::b_fs_yx_fsv16: + case DataLayout::b_fs_zyx_fsv16: + return 16; + case DataLayout::b_fs_yx_fsv32: + case DataLayout::b_fs_zyx_fsv32: + return 32; + default: + return 0; + } +} + JitConstants ReorderKernelFastBatch1::GetJitConstants(const reorder_params& params) const { auto jit = ReorderKernelBase::GetJitConstants(params); jit.Merge(GetTensorFriendlyWorkGroupsJit(params.inputs[0])); @@ -66,10 +82,25 @@ JitConstants ReorderKernelFastBatch1::GetJitConstants(const reorder_params& para reorder_params& newParams = *static_cast(kd.params.get()); const auto& input = newParams.inputs[0]; - jit.AddConstant(MakeJitConstant("ELEMENTS_COUNT", input.LogicalSize())); - const auto& output = newParams.outputs[0]; + // For blocked output formats (e.g., b_fs_yx_fsv16), when the feature count is not + // aligned to the block size, we must zero-fill the padding positions. Otherwise, + // uninitialized memory (potentially containing NaN) in padding positions can corrupt + // subsequent computations (since NaN * 0 = NaN in IEEE 754). + size_t fsv = GetOutputFeatureBlockSize(output.GetLayout()); + size_t feature_count = output.Feature().v; + if (fsv > 0 && (feature_count % fsv) != 0) { + size_t padded_features = Align(feature_count, fsv); + size_t spatial_size = output.LogicalSize() / (output.Batch().v * feature_count); + size_t padded_count = output.Batch().v * padded_features * spatial_size; + jit.AddConstant(MakeJitConstant("ELEMENTS_COUNT", padded_count)); + jit.AddConstant(MakeJitConstant("PADDED_FEATURE_NUM", padded_features)); + jit.AddConstant(MakeJitConstant("FILL_FEATURE_PADDING", 1)); + } else { + jit.AddConstant(MakeJitConstant("ELEMENTS_COUNT", input.LogicalSize())); + } + if (input.GetLayout() == output.GetLayout() && input.SameDimsSizes(output) && !input.PitchesDifferFromLogicalDims() && !output.PitchesDifferFromLogicalDims() && input.GetDType() != output.GetDType() && !params.has_padded_output && @@ -85,9 +116,22 @@ ReorderKernelFastBatch1::DispatchData ReorderKernelFastBatch1::SetDefault(const const auto& output = params.outputs[0]; - unsigned int gws = (unsigned int)output.LogicalSize(); + size_t gws = output.LogicalSize(); + + // For blocked output formats with unaligned features, expand GWS to cover padding positions + size_t fsv = GetOutputFeatureBlockSize(output.GetLayout()); + size_t feature_count = output.Feature().v; + if (fsv > 0 && (feature_count % fsv) != 0) { + size_t padded_features = Align(feature_count, fsv); + size_t spatial_size = output.LogicalSize() / (output.Batch().v * feature_count); + gws = output.Batch().v * padded_features * spatial_size; + } + + OPENVINO_ASSERT(gws <= static_cast(std::numeric_limits::max()), + "[GPU] reorder_data_fast_b1 global work size exceeds 32-bit index range: ", + gws); - dispatchData.gws[0] = Align(gws, 32); + dispatchData.gws[0] = Align(gws, size_t{32}); dispatchData.gws[1] = 1; dispatchData.gws[2] = 1; diff --git a/src/plugins/intel_gpu/src/kernel_selector/kernels/reorder/reorder_kernel_fsv.cpp b/src/plugins/intel_gpu/src/kernel_selector/kernels/reorder/reorder_kernel_fsv.cpp new file mode 100644 index 000000000000..9b8d4e7dc343 --- /dev/null +++ b/src/plugins/intel_gpu/src/kernel_selector/kernels/reorder/reorder_kernel_fsv.cpp @@ -0,0 +1,166 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "reorder_kernel_fsv.h" +#include "kernel_selector_utils.h" +#include + +namespace kernel_selector { + +static size_t GetFsv(DataLayout layout) { + switch (layout) { + case DataLayout::b_fs_yx_fsv4: + case DataLayout::b_fs_zyx_fsv4: + return 4; + case DataLayout::b_fs_yx_fsv8: + case DataLayout::b_fs_zyx_fsv8: + return 8; + case DataLayout::b_fs_yx_fsv16: + case DataLayout::b_fs_zyx_fsv16: + return 16; + case DataLayout::b_fs_yx_fsv32: + case DataLayout::b_fs_zyx_fsv32: + return 32; + default: + return 0; + } +} + +static bool IsSingleBlockedFsv(DataLayout layout) { + return GetFsv(layout) != 0; +} + +ParamsKey ReorderKernel_fsv::GetSupportedKey() const { + ParamsKey k; + + k.EnableAllInputDataType(); + k.EnableAllOutputDataType(); + + k.EnableInputLayout(DataLayout::b_fs_yx_fsv4); + k.EnableInputLayout(DataLayout::b_fs_yx_fsv8); + k.EnableInputLayout(DataLayout::b_fs_yx_fsv16); + k.EnableInputLayout(DataLayout::b_fs_yx_fsv32); + k.EnableInputLayout(DataLayout::b_fs_zyx_fsv4); + k.EnableInputLayout(DataLayout::b_fs_zyx_fsv8); + k.EnableInputLayout(DataLayout::b_fs_zyx_fsv16); + k.EnableInputLayout(DataLayout::b_fs_zyx_fsv32); + + k.EnableOutputLayout(DataLayout::b_fs_yx_fsv4); + k.EnableOutputLayout(DataLayout::b_fs_yx_fsv8); + k.EnableOutputLayout(DataLayout::b_fs_yx_fsv16); + k.EnableOutputLayout(DataLayout::b_fs_yx_fsv32); + k.EnableOutputLayout(DataLayout::b_fs_zyx_fsv4); + k.EnableOutputLayout(DataLayout::b_fs_zyx_fsv8); + k.EnableOutputLayout(DataLayout::b_fs_zyx_fsv16); + k.EnableOutputLayout(DataLayout::b_fs_zyx_fsv32); + + k.EnableDifferentTypes(); + k.EnableBatching(); + k.EnableTensorOffset(); + k.EnableTensorPitches(); + k.EnableDynamicShapesSupport(); + + return k; +} + +bool ReorderKernel_fsv::Validate(const Params& p) const { + if (!ReorderKernelBase::Validate(p)) { + DO_NOT_USE_THIS_KERNEL(p.layerID); + } + + const reorder_params& params = static_cast(p); + const auto& input = params.inputs[0]; + const auto& output = params.outputs[0]; + + if (!IsSingleBlockedFsv(input.GetLayout()) || !IsSingleBlockedFsv(output.GetLayout())) { + DO_NOT_USE_THIS_KERNEL(p.layerID); + } + + if (input.GetDims().size() != output.GetDims().size()) { + DO_NOT_USE_THIS_KERNEL(p.layerID); + } + + size_t in_fsv = GetFsv(input.GetLayout()); + size_t out_fsv = GetFsv(output.GetLayout()); + if (in_fsv == out_fsv) { + DO_NOT_USE_THIS_KERNEL(p.layerID); + } + + return true; +} + +CommonDispatchData ReorderKernel_fsv::SetDefault(const reorder_params& params) const { + CommonDispatchData dispatchData; + + const auto& input = params.inputs[0]; + const size_t out_fsv = GetFsv(params.outputs[0].GetLayout()); + + // GWS[0] = x + // GWS[1] = y * z + // GWS[2] = b * fs_out (feature slices in output blocking) + size_t x = input.X().v; + size_t y = input.Y().v; + size_t z = input.Z().v; + size_t b = input.Batch().v; + size_t f = input.Feature().v; + + dispatchData.gws = { x, y * z, b * CeilDiv(f, out_fsv) }; + dispatchData.lws = GetOptimalLocalWorkGroupSizes(dispatchData.gws, params.engineInfo); + + return dispatchData; +} + +JitConstants ReorderKernel_fsv::GetJitConstants(const reorder_params& params) const { + auto jit = ReorderKernelBase::GetJitConstants(params); + + const size_t in_fsv = GetFsv(params.inputs[0].GetLayout()); + const size_t out_fsv = GetFsv(params.outputs[0].GetLayout()); + const size_t ndims = params.inputs[0].GetDims().size(); + + jit.AddConstant(MakeJitConstant("IN_FSV", in_fsv)); + jit.AddConstant(MakeJitConstant("OUT_FSV", out_fsv)); + + if (ndims == 5) { + jit.AddConstant(MakeJitConstant("INPUT0_DIMS_5", 1)); + } + + // Vectorized path: when both fsv sizes are >= 16 and one is a multiple of the other + // Threshold of 16 is chosen so vload16/vstore16 intrinsics map to a single + // SIMD16 transaction. fsv4/fsv8 layouts fall back to the scalar loop in the .cl + // because a smaller vector width would not amortize the added jit branches. + const size_t min_fsv = std::min(in_fsv, out_fsv); + const size_t max_fsv = std::max(in_fsv, out_fsv); + if (min_fsv >= 16 && max_fsv % min_fsv == 0) { + jit.AddConstant(MakeJitConstant("FSV_VECTORIZED", 1)); + jit.AddConstant(MakeJitConstant("VEC_SIZE", min_fsv)); + jit.AddConstant(MakeJitConstant("RATIO", max_fsv / min_fsv)); + } + + if (params.is_shape_agnostic) { + jit.AddConstant(MakeJitConstant("IN_FEATURE_SLICE_NUM", + "((INPUT0_FEATURE_NUM + " + std::to_string(in_fsv) + " - 1) / " + std::to_string(in_fsv) + ")")); + jit.AddConstant(MakeJitConstant("OUT_FEATURE_SLICE_NUM", + "((INPUT0_FEATURE_NUM + " + std::to_string(out_fsv) + " - 1) / " + std::to_string(out_fsv) + ")")); + } else { + const size_t f = params.inputs[0].Feature().v; + jit.AddConstant(MakeJitConstant("IN_FEATURE_SLICE_NUM", CeilDiv(f, in_fsv))); + jit.AddConstant(MakeJitConstant("OUT_FEATURE_SLICE_NUM", CeilDiv(f, out_fsv))); + } + + return jit; +} + +KernelsData ReorderKernel_fsv::GetKernelsData(const Params& params) const { + assert(params.GetType() == KernelType::REORDER); + + const reorder_params& orgParams = static_cast(params); + + return GetCommonKernelsData(orgParams); +} + +KernelsPriority ReorderKernel_fsv::GetKernelsPriority(const Params& p) const { + return FORCE_PRIORITY_4; +} + +} // namespace kernel_selector diff --git a/src/plugins/intel_gpu/src/kernel_selector/kernels/reorder/reorder_kernel_fsv.h b/src/plugins/intel_gpu/src/kernel_selector/kernels/reorder/reorder_kernel_fsv.h new file mode 100644 index 000000000000..66f4b137ddf5 --- /dev/null +++ b/src/plugins/intel_gpu/src/kernel_selector/kernels/reorder/reorder_kernel_fsv.h @@ -0,0 +1,22 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#pragma once + +#include "reorder_kernel_base.h" + +namespace kernel_selector { +class ReorderKernel_fsv : public ReorderKernelBase { +public: + ReorderKernel_fsv() : ReorderKernelBase("reorder_data_fsv") {} + + bool Validate(const Params& p) const override; + KernelsData GetKernelsData(const Params& params) const override; + KernelsPriority GetKernelsPriority(const Params& params) const override; + ParamsKey GetSupportedKey() const override; +protected: + JitConstants GetJitConstants(const reorder_params& params) const override; + CommonDispatchData SetDefault(const reorder_params& params) const override; +}; +} // namespace kernel_selector diff --git a/src/plugins/intel_gpu/src/kernel_selector/kernels/reorder/reorder_kernel_selector.cpp b/src/plugins/intel_gpu/src/kernel_selector/kernels/reorder/reorder_kernel_selector.cpp index 3e2185027bef..bffa3e97f0d5 100644 --- a/src/plugins/intel_gpu/src/kernel_selector/kernels/reorder/reorder_kernel_selector.cpp +++ b/src/plugins/intel_gpu/src/kernel_selector/kernels/reorder/reorder_kernel_selector.cpp @@ -12,6 +12,7 @@ #include "reorder_kernel_fs_b_yx_fsv32_to_bfyx.h" #include "reorder_kernel_bfyx_to_blocked_format.h" #include "reorder_kernel_b_fs_yx_fsv16_fsv32_to_bfyx.h" +#include "reorder_kernel_fsv.h" namespace kernel_selector { @@ -25,6 +26,7 @@ reorder_kernel_selector::reorder_kernel_selector() { Attach(); Attach(); Attach(); + Attach(); } KernelsData reorder_kernel_selector::GetBestKernels(const Params& params) const { diff --git a/src/plugins/intel_gpu/src/kernel_selector/kernels/reorder/reorder_weights_opt.cpp b/src/plugins/intel_gpu/src/kernel_selector/kernels/reorder/reorder_weights_opt.cpp index 9b186d19da97..df87d77b44be 100644 --- a/src/plugins/intel_gpu/src/kernel_selector/kernels/reorder/reorder_weights_opt.cpp +++ b/src/plugins/intel_gpu/src/kernel_selector/kernels/reorder/reorder_weights_opt.cpp @@ -157,7 +157,14 @@ ReorderWeightsOpt::DispatchData ReorderWeightsOpt::SetDefault( : subgroup_size; if (osv_first) { - dispatchData.gws = { output.G().v * (output.IFM().v / ifm_block), + // When IFM is not aligned to the ISV block size, expand GWS to cover padding + // positions so they can be zero-filled by the kernel. + auto slice_sizes = GetSliceSizes(output_layout); + size_t isv_size = slice_sizes.first; + size_t ifm_padded = (isv_size > 1 && (output.IFM().v % isv_size) != 0) + ? Align(output.IFM().v, isv_size) + : output.IFM().v; + dispatchData.gws = { output.G().v * (ifm_padded / ifm_block), output.Z().v * output.Y().v * output.X().v, Align(output.OFM().v, ofm_block) }; } else { @@ -197,6 +204,19 @@ JitConstants ReorderWeightsOpt::GetJitConstants(const reorder_weights_params& pa if (leftovers) jit.AddConstant(MakeJitConstant("OUTPUT_LEFTOVERS", leftovers)); + // For blocked weight formats with OSV_FIRST (e.g., os_is_yx_isv16_osv16), when the + // input feature count is not aligned to the ISV block size, we must zero-fill IFM + // padding positions. Otherwise, uninitialized memory (potentially containing NaN) + // in weight padding can propagate through convolution via NaN * 0 = NaN (IEEE 754). + if (osv_first) { + size_t isv_size = slice_sizes.first; + if (isv_size > 1 && (output.IFM().v % isv_size) != 0) { + jit.AddConstant(MakeJitConstant("IFM_PADDING", 1)); + jit.AddConstant(MakeJitConstant("ACTUAL_IFM_NUM", output.IFM().v)); + jit.AddConstant(MakeJitConstant("IFM_PADDED_NUM", Align(output.IFM().v, isv_size))); + } + } + return jit; } diff --git a/src/plugins/intel_gpu/src/kernel_selector/kernels/resample/resample_kernel_pil_ref.cpp b/src/plugins/intel_gpu/src/kernel_selector/kernels/resample/resample_kernel_pil_ref.cpp index d47a9dc01e78..07d61e435408 100644 --- a/src/plugins/intel_gpu/src/kernel_selector/kernels/resample/resample_kernel_pil_ref.cpp +++ b/src/plugins/intel_gpu/src/kernel_selector/kernels/resample/resample_kernel_pil_ref.cpp @@ -93,22 +93,22 @@ std::size_t GetKernelsNum(const resample_params& params) { } std::size_t GetFirstRow(const resample_params& params) { - float scale = static_cast(getInputVerticalSize(params, true)) / getOutputVerticalSize(params); + float scale = static_cast(getInputVerticalSize(params, true)) / static_cast(getOutputVerticalSize(params)); float filter_scale = std::max(1.f, scale); float support = params.resampleType == ResampleType::BILINEAR_PILLOW ? 1.f : 2.f * filter_scale; - float center = 0.5 * scale; - auto xmin = std::max(0, static_cast(center - support + 0.5)); + float center = 0.5f * scale; + auto xmin = std::max(0, static_cast(center - support + 0.5f)); return xmin; } std::size_t GetLastRow(const resample_params& params) { auto inputVerticalSize = getInputVerticalSize(params, true); auto outputVerticalSize = getOutputVerticalSize(params); - float scale = static_cast(inputVerticalSize) / outputVerticalSize; + float scale = static_cast(inputVerticalSize) / static_cast(outputVerticalSize); float filter_scale = std::max(1.f, scale); float support = params.resampleType == ResampleType::BILINEAR_PILLOW ? 1.f : 2.f * filter_scale; - float center = (outputVerticalSize - 0.5) * scale; - auto xmax = std::min(inputVerticalSize, static_cast(center + support + 0.5)); + float center = (outputVerticalSize - 0.5f) * scale; + auto xmax = std::min(inputVerticalSize, static_cast(center + support + 0.5f)); return xmax; } @@ -379,7 +379,7 @@ JitConstants ResampleKernelPilRef::GetJitConstantsForKernel(KernelId id, const r MakeJitConstant("ENABLE_VERTICAL_PASS", NeedVerticalPass(params)), MakeJitConstant("ENABLE_HORIZONTAL_PASS", needHorizontalPass), MakeJitConstant("KSIZE", ksize), - }); + }); if (needHorizontalPass) { jit_constants.AddConstant(MakeJitConstant("RESAMPLE_VERTICAL_INPUT_TYPE", "INTERMEDIATE_BUF_TYPE")); } else { diff --git a/src/plugins/intel_gpu/src/kernel_selector/kernels/rms/rms_kernel_bfyx_opt.cpp b/src/plugins/intel_gpu/src/kernel_selector/kernels/rms/rms_kernel_bfyx_opt.cpp index 56513be1772d..ffb52b9a8fe5 100644 --- a/src/plugins/intel_gpu/src/kernel_selector/kernels/rms/rms_kernel_bfyx_opt.cpp +++ b/src/plugins/intel_gpu/src/kernel_selector/kernels/rms/rms_kernel_bfyx_opt.cpp @@ -54,12 +54,20 @@ DeviceFeaturesKey RMSKernelBfyxOpt::get_required_device_features_key(const Param JitConstants RMSKernelBfyxOpt::GetJitConstants(const rms_params& params, DispatchData dispatchData) const { auto jit = Parent::GetJitConstants(params, dispatchData); - bool has_dynamic_padding = false; - for (const auto& dim : params.inputs[0].GetDims()) - has_dynamic_padding |= dim.pad.is_dynamic; + // Check for any padding (dynamic or static) on input dimensions. + // The flat addressing path (data_idx * data_size) assumes contiguous memory, + // which breaks when padding introduces gaps between slices (e.g., from in-place crop). + // Switch to per-dimension indexed addressing via get_input_index() in that case. + bool has_padding = false; + for (const auto& dim : params.inputs[0].GetDims()) { + if (dim.pad.is_dynamic || dim.pad.before != 0 || dim.pad.after != 0) { + has_padding = true; + break; + } + } - if (has_dynamic_padding) - jit.AddConstant(MakeJitConstant("HAS_DYNAMIC_PADDING", 1)); + if (has_padding) + jit.AddConstant(MakeJitConstant("HAS_PADDING", 1)); if (params.has_dynamic_tensors()) { const auto& input = params.inputs[0]; diff --git a/src/plugins/intel_gpu/src/kernel_selector/kernels/scatter_update/scatter_elements_update_kernel_ref.cpp b/src/plugins/intel_gpu/src/kernel_selector/kernels/scatter_update/scatter_elements_update_kernel_ref.cpp index 8c31c7540457..bc0243e3c3fc 100644 --- a/src/plugins/intel_gpu/src/kernel_selector/kernels/scatter_update/scatter_elements_update_kernel_ref.cpp +++ b/src/plugins/intel_gpu/src/kernel_selector/kernels/scatter_update/scatter_elements_update_kernel_ref.cpp @@ -262,11 +262,11 @@ void ScatterElementsUpdateKernelRef::GetUpdateDispatchDataFunc(KernelData& kd) c if (prim_params.mode != ScatterUpdateReduction::NONE) { kd.internalBuffers.clear(); kd.internalBuffers.push_back(output.PhysicalSizeInBytes() * 2); - - if (!use_local_memory) { + if (prim_params.mode == ScatterUpdateReduction::MEAN) { kd.internalBuffers.push_back(output.PhysicalSizeInBytes() * 2); } - if (prim_params.mode == ScatterUpdateReduction::MEAN) { + + if (!use_local_memory) { kd.internalBuffers.push_back(output.PhysicalSizeInBytes() * 2); } kd.internalBufferDataType = Datatype::INT32; @@ -318,15 +318,15 @@ KernelsData ScatterElementsUpdateKernelRef::GetKernelsData(const Params& params) if (!params.is_shape_agnostic && newParams.mode != ScatterUpdateReduction::NONE) { kd.internalBuffers.clear(); kd.internalBuffers.push_back(output.PhysicalSizeInBytes() * 2); // fixed point output + if (newParams.mode == ScatterUpdateReduction::MEAN) { + kd.internalBuffers.push_back(output.PhysicalSizeInBytes() * 2); // count for mean + } if (!use_local_memory_for_static_shape) { kd.internalBuffers.push_back(output.PhysicalSizeInBytes() * 2); // reduction value output kd.internalBuffers.push_back(output.PhysicalSizeInBytes() * 2); // reduction_thread_count output } kd.internalBufferDataType = Datatype::INT32; - if (newParams.mode == ScatterUpdateReduction::MEAN) { - kd.internalBuffers.push_back(output.PhysicalSizeInBytes() * 2); // calculate mean - } } // Define adjustment map for ITER based on kernel index @@ -377,6 +377,11 @@ KernelsData ScatterElementsUpdateKernelRef::GetKernelsData(const Params& params) // store output in fixed point kernel.params.arguments.push_back({ArgumentDescriptor::Types::INTERNAL_BUFFER, buf_idx++}); + // count_k buffer for mean (available for all ITERs including ITER=0) + if (newParams.mode == ScatterUpdateReduction::MEAN) { + kernel.params.arguments.push_back({ArgumentDescriptor::Types::INTERNAL_BUFFER, buf_idx++}); + } + if (i >= 1) { bool use_local_memory = use_local_memory_for_static_shape; if (params.is_shape_agnostic) { @@ -390,9 +395,6 @@ KernelsData ScatterElementsUpdateKernelRef::GetKernelsData(const Params& params) // identify thread for perform write kernel.params.arguments.push_back({ArgumentDescriptor::Types::INTERNAL_BUFFER, buf_idx++}); } - if (newParams.mode == ScatterUpdateReduction::MEAN) { - kernel.params.arguments.push_back({ArgumentDescriptor::Types::INTERNAL_BUFFER, buf_idx++}); - } } } } diff --git a/src/plugins/intel_gpu/src/kernel_selector/kernels/scatter_update/scatter_update_kernel_ref.cpp b/src/plugins/intel_gpu/src/kernel_selector/kernels/scatter_update/scatter_update_kernel_ref.cpp index 8a19e30b84f0..f3bb2517fc85 100644 --- a/src/plugins/intel_gpu/src/kernel_selector/kernels/scatter_update/scatter_update_kernel_ref.cpp +++ b/src/plugins/intel_gpu/src/kernel_selector/kernels/scatter_update/scatter_update_kernel_ref.cpp @@ -180,43 +180,19 @@ static std::string GetSecondIterOutputIndexOrder(const scatter_update_params& pa return GetOrderString(output_order); } -static std::vector GetPlanarPitches(const Tensor::NDims& dims) { - const auto ndims = dims.size(); - std::vector pitches(ndims); - size_t pitch = 1; - for (size_t i = 0; i < ndims; ++i) { - pitches[i] = std::to_string(pitch); - pitch *= dims[i].v; - } - std::reverse(pitches.begin(), pitches.end()); - return pitches; -} - -static std::vector GetDynamicPitches(const Tensor::NDims& dims, size_t tensor_idx) { - std::vector pitches(dims.size()); - size_t shape_info_rank = DataTensor::max_rank(); - std::string pitch = "1"; - for (size_t i = 0; i < pitches.size(); ++i) { - size_t bf_idx = dims.size() - i - 1; - size_t dim_offset = bf_idx > 1 ? shape_info_rank - i - 1 : bf_idx; - - pitches[i] = pitch; - pitch += "*" + toCodeString(dims[i], tensor_idx * shape_info_rank + dim_offset); - } - std::reverse(pitches.begin(), pitches.end()); - return pitches; -} - JitConstants ScatterUpdateKernelRef::GetJitConstants(const scatter_update_params& params) const { JitConstants jit = MakeBaseParamsJitConstants(params); size_t axis_value = GetScatterUpdateChannelIndex(params); - const auto blocked_layout = !(SimpleLayout(params.inputs[0].GetLayout()) && - SimpleLayout(params.inputs[1].GetLayout()) && - SimpleLayout(params.inputs[2].GetLayout())); + const auto input2_has_padding = params.inputs[2].has_dynamic_pad() || params.inputs[2].PitchesDifferFromLogicalDims(); - if (blocked_layout) { - jit.AddConstant(MakeJitConstant("BLOCKED_LAYOUT", "1")); + // In case of padded input2 (updates), we also need non-planar indexing, because UPDATES_INDEX is calculated based on output sizes + const auto use_layout_aware_indexing = !(SimpleLayout(params.inputs[0].GetLayout()) && + SimpleLayout(params.inputs[1].GetLayout()) && + SimpleLayout(params.inputs[2].GetLayout())) || input2_has_padding; + + if (use_layout_aware_indexing) { + jit.AddConstant(MakeJitConstant("USE_LAYOUT_AWARE_INDEXING", "1")); } jit.AddConstant(MakeJitConstant("UPDATES_INDEX_ORDER", GetUpdatesIndexOrder(params))); @@ -225,54 +201,28 @@ JitConstants ScatterUpdateKernelRef::GetJitConstants(const scatter_update_params jit.AddConstant(MakeJitConstant("OUTPUT_INDEX_ON_AXIS", GetOutputIndexOnAxis(params, GetScatterUpdateChannelIndex(params)))); jit.AddConstant(MakeJitConstant("AXIS_VALUE", axis_value)); - const auto& input1 = params.inputs[1]; - if (input1.is_dynamic()) { - const auto& input1_dims = input1.GetDims(); - size_t input_offset = DataTensor::max_rank(); - std::string indices_size = "("; - - for (size_t i = 0; i < input1_dims.size(); ++i) { - size_t bf_idx = input1_dims.size() - i - 1; - size_t dim_offset = bf_idx > 1 ? input_offset - i - 1 : bf_idx; - - indices_size += toCodeString(input1_dims[i], input_offset + dim_offset) + "*"; - } - indices_size.back() = ')'; - jit.AddConstant(MakeJitConstant("INDICES_SIZE", indices_size)); - } else { - jit.AddConstant(MakeJitConstant("INDICES_SIZE", params.inputs[1].LogicalSize())); - } - - std::vector pitches; const auto& output = params.outputs[0]; - if (output.is_dynamic()) { - size_t tensor_idx = params.inputs.size() + GetFusedPrimitiveInputsCount(params); - for (auto input : params.inputs) { - if (!input.is_dynamic()) - tensor_idx--; - } - for (auto fused_op : params.fused_ops) { - if (!fused_op.output_tensor.is_dynamic()) - tensor_idx--; - } - pitches = GetDynamicPitches(output.GetDims(), tensor_idx); - } else { - pitches = GetPlanarPitches(output.GetDims()); - } auto default_order = GetDefaultOrder(output.GetDims().size()); size_t dims = default_order.size(); - std::string get_update_idx = "(INPUT2_OFFSET)"; + // UPDATES_GET_INDEX just calculates planar index + // In case of simple planar layout, this index will be used directly, so need to add INPUT2_OFFSET + // In case of non-planar layout, INPUT2_GET_INDEX macro will be used, which adds the offset internally + std::string get_update_idx = use_layout_aware_indexing ? "0" : "(INPUT2_OFFSET)"; + std::string get_update_idx_src = "UPDATES_GET_INDEX("; for (size_t i = 0; i < dims; ++i) { - if (i >= axis_value) { + if (i == dims-1) { + get_update_idx_src += default_order[i] + ")"; std::string def_pitch = "UPDATES_" + GetAxisName(dims, i) + "_PITCH"; - std::string src_pitch = "(" + pitches[i] + ")"; + std::string src_pitch = "1"; jit.AddConstant(MakeJitConstant(def_pitch, src_pitch)); - } else if (i == (axis_value - 1)) { + } else if (axis_value > 0 && i == (axis_value - 1)) { + get_update_idx_src += default_order[i] + ", "; std::string def_pitch = "UPDATES_" + GetAxisName(dims, i) + "_PITCH"; - std::string src_pitch = "(" + pitches[i + 1] + " * INDICES_SIZE)"; + std::string src_pitch = "(UPDATES_" + GetAxisName(dims, i + 1) + "_PITCH * INDICES_SIZE)"; jit.AddConstant(MakeJitConstant(def_pitch, src_pitch)); - } else { // i < axis_value - 1 + } else { + get_update_idx_src += default_order[i] + ", "; std::string def_pitch = "UPDATES_" + GetAxisName(dims, i) + "_PITCH" + ""; std::string output_size_name; if (i == 0) @@ -284,7 +234,7 @@ JitConstants ScatterUpdateKernelRef::GetJitConstants(const scatter_update_params } get_update_idx = get_update_idx + " + (" + default_order[i] + ")*(UPDATES_" + GetAxisName(dims, i) + "_PITCH)"; } - jit.AddConstant(MakeJitConstant("GET_UPDATES_INDEX(idx_order)", get_update_idx)); + jit.AddConstant(MakeJitConstant(get_update_idx_src, get_update_idx)); if (!params.fused_ops.empty()) { FusedOpsConfiguration conf1 = { "_FIRST_KERNEL", GetDefaultOrder(output.GetDims().size()), "val", params.inputs[0].GetDType() }; diff --git a/src/plugins/intel_gpu/src/kernel_selector/kernels/softmax/softmax_kernel_bf.cpp b/src/plugins/intel_gpu/src/kernel_selector/kernels/softmax/softmax_kernel_bf.cpp index 26a8efd7f71b..3784e62db88d 100644 --- a/src/plugins/intel_gpu/src/kernel_selector/kernels/softmax/softmax_kernel_bf.cpp +++ b/src/plugins/intel_gpu/src/kernel_selector/kernels/softmax/softmax_kernel_bf.cpp @@ -157,7 +157,7 @@ JitConstants SoftmaxKernel_bf::GetJitConstants(const softmax_params& params, Dis if (!params.fused_ops.empty()) { FusedOpsConfiguration conf_main = {"_MAIN", - {"data_set_offset", "in_data_set_idx + i * workers_per_data_set", "0", "0"}, + {"data_set_offset", "in_data_set_idx + output_idx * workers_per_data_set", "0", "0"}, "dequantized", activation_dt}; FusedOpsConfiguration conf_leftovers = {"_LEFTOVERS", diff --git a/src/plugins/intel_gpu/src/kernel_selector/kernels/swiglu/swiglu_kernel_base.cpp b/src/plugins/intel_gpu/src/kernel_selector/kernels/swiglu/swiglu_kernel_base.cpp index e0fca152c346..5ed21289ccab 100644 --- a/src/plugins/intel_gpu/src/kernel_selector/kernels/swiglu/swiglu_kernel_base.cpp +++ b/src/plugins/intel_gpu/src/kernel_selector/kernels/swiglu/swiglu_kernel_base.cpp @@ -44,6 +44,9 @@ JitConstants SwiGLUKernelBase::GetJitConstants(const swiglu_params& params, cons } jit.AddConstants({MakeJitConstant("SWISH_BETA", static_cast(params.swish_beta))}); jit.AddConstants({MakeJitConstant("UP_ADD_VAL", static_cast(params.up_add_val))}); + if (params.scale_factor > 0.0f) { + jit.AddConstants({MakeJitConstant("SCALE_FACTOR", static_cast(params.scale_factor))}); + } return jit; } diff --git a/src/plugins/intel_gpu/src/kernel_selector/kernels/swiglu/swiglu_kernel_base.h b/src/plugins/intel_gpu/src/kernel_selector/kernels/swiglu/swiglu_kernel_base.h index b58808db9f3f..cc55777b5535 100644 --- a/src/plugins/intel_gpu/src/kernel_selector/kernels/swiglu/swiglu_kernel_base.h +++ b/src/plugins/intel_gpu/src/kernel_selector/kernels/swiglu/swiglu_kernel_base.h @@ -22,7 +22,8 @@ struct swiglu_params : public base_params { clamp_min(std::numeric_limits::lowest()), clamp_max(std::numeric_limits::max()), swish_beta(1.0f), - up_add_val(0.0f) {} + up_add_val(0.0f), + scale_factor(-1.0f) {} int32_t axis; int32_t glu_stride; ov::op::internal::GLU::GluType glu_type; @@ -31,10 +32,12 @@ struct swiglu_params : public base_params { float clamp_max; float swish_beta; float up_add_val; + float scale_factor; }; struct swiglu_fuse_params : fuse_params { - explicit swiglu_fuse_params(int32_t axis, size_t glu_stride, size_t gate_idx, size_t clamp_min, size_t clamp_max, float swish_beta, float up_add_val) + explicit swiglu_fuse_params(int32_t axis, size_t glu_stride, size_t gate_idx, float clamp_min, float clamp_max, + float swish_beta, float up_add_val, float scale_factor = -1.0f) : fuse_params(KernelType::SWIGLU), axis(axis), glu_stride(glu_stride), @@ -42,7 +45,8 @@ struct swiglu_fuse_params : fuse_params { clamp_min(clamp_min), clamp_max(clamp_max), swish_beta(swish_beta), - up_add_val(up_add_val) {} + up_add_val(up_add_val), + scale_factor(scale_factor) {} int32_t axis; size_t glu_stride; size_t gate_idx; @@ -50,6 +54,7 @@ struct swiglu_fuse_params : fuse_params { float clamp_max; float swish_beta; float up_add_val; + float scale_factor; }; class SwiGLUKernelBase : public KernelBaseOpenCL { diff --git a/src/plugins/intel_gpu/src/kernel_selector/primitive_db_gen.py b/src/plugins/intel_gpu/src/kernel_selector/primitive_db_gen.py index b5930993005b..e7d2aa4e86f7 100644 --- a/src/plugins/intel_gpu/src/kernel_selector/primitive_db_gen.py +++ b/src/plugins/intel_gpu/src/kernel_selector/primitive_db_gen.py @@ -1,6 +1,6 @@ -#!/usr/bin/python3 # Copyright (C) 2018-2026 Intel Corporation # SPDX-License-Identifier: Apache-2.0 + # To add new kernel please add a .cl file to kernels directory # the database name will be the part of the file name up to first '.' character # the trailing characters are a tag to allow multiple primitive implementations @@ -136,7 +136,7 @@ def found_potential_macro_user(self, macro, contents_list): if line.find(macro) >= 0: return True if line.find("CAT") >= 0: - words = ' '.join(re.split("(\W)", line)).split() + words = ' '.join(re.split(r'(\W)', line)).split() iter_w = 0 while iter_w < len(words): if words[iter_w] != "CAT": @@ -154,11 +154,11 @@ def reduce_macros(self, contents): idx = 0 while idx < len(contents_list): line = contents_list[idx] - is_macro = re.search('#\s*define', line) + is_macro = re.search(r'#\s*define', line) macro = "" if is_macro: - words = ' '.join(re.split("(\W)", line)).split() + words = ' '.join(re.split(r'(\W)', line)).split() macro = words[words.index("define") + 1] if len(macro) == 0 or self.found_potential_macro_user(macro, contents_list): diff --git a/src/plugins/intel_gpu/src/plugin/compiled_model.cpp b/src/plugins/intel_gpu/src/plugin/compiled_model.cpp index 4cfc59ccd6a8..fce3963fd3f3 100644 --- a/src/plugins/intel_gpu/src/plugin/compiled_model.cpp +++ b/src/plugins/intel_gpu/src/plugin/compiled_model.cpp @@ -6,7 +6,6 @@ #include "openvino/runtime/intel_gpu/properties.hpp" #include "openvino/runtime/internal_properties.hpp" #include "openvino/runtime/plugin_config.hpp" -#include "openvino/util/weights_path.hpp" #include "intel_gpu/graph/serialization/binary_buffer.hpp" #include "intel_gpu/runtime/itt.hpp" diff --git a/src/plugins/intel_gpu/src/plugin/custom_layer.cpp b/src/plugins/intel_gpu/src/plugin/custom_layer.cpp index 4bb14beea122..6613e817569c 100644 --- a/src/plugins/intel_gpu/src/plugin/custom_layer.cpp +++ b/src/plugins/intel_gpu/src/plugin/custom_layer.cpp @@ -107,6 +107,10 @@ void CustomLayer::ProcessBuffersNode(const pugi::xml_node & node) { kp.type = ParamType::Input; } else if (typeStr.compare("output") == 0) { kp.type = ParamType::Output; + } else if (typeStr.compare("internal") == 0) { + kp.type = ParamType::Internal; + kp.size_expr = get_str_attr(tensorNode, "size", ""); + CheckAndReturnError(kp.size_expr.empty(), "Internal buffer requires a size attribute"); } else { CheckAndReturnError(true, "Tensor node has an invalid type: " << typeStr); } diff --git a/src/plugins/intel_gpu/src/plugin/graph.cpp b/src/plugins/intel_gpu/src/plugin/graph.cpp index 09619554284e..a0def541c947 100644 --- a/src/plugins/intel_gpu/src/plugin/graph.cpp +++ b/src/plugins/intel_gpu/src/plugin/graph.cpp @@ -137,12 +137,12 @@ Graph::~Graph() { auto print_entry = [this, &get_time_str, &log_level](std::string name, HostTimeProfilingEntry& entry, int64_t iters_num = 1) { if (log_level == 1) { - GPU_DEBUG_COUT << "[stream_id=" << m_stream_id << "] " << name << " infer enqueue host time: " + GPU_DEBUG_COUT << "[network_id=" << m_network->get_id() << " stream_id=" << m_stream_id << " iter_num=" << iters_num << "] " << name << " infer enqueue host time: " << get_time_str(entry.enqueue, iters_num) << std::endl; } else if (log_level >= 2) { auto total_time = entry.inputs_processing + entry.enqueue + entry.wait + entry.outputs_processing; - GPU_DEBUG_COUT << "[stream_id=" << m_stream_id << "] " << name << " infer host time: " + GPU_DEBUG_COUT << "[network_id=" << m_network->get_id() << " stream_id=" << m_stream_id << " iter_num=" << iters_num << "] " << name << " infer host time: " << get_time_str(total_time, iters_num) << std::endl; GPU_DEBUG_COUT << " - " << " Inputs processing: " << get_time_str(entry.inputs_processing, iters_num) << std::endl; GPU_DEBUG_COUT << " - " << " Enqueue: " << get_time_str(entry.enqueue, iters_num) << std::endl; @@ -160,13 +160,13 @@ Graph::~Graph() { const auto begin = std::begin(host_exec_times) + 1; const auto end = std::end(host_exec_times); - avg.inputs_processing = std::accumulate(begin, end, 0, + avg.inputs_processing = std::accumulate(begin, end, int64_t{0}, [](int64_t sum, const HostTimeProfilingEntry& entry) { return sum + entry.inputs_processing; }); - avg.enqueue = std::accumulate(begin, end, 0, + avg.enqueue = std::accumulate(begin, end, int64_t{0}, [](int64_t sum, const HostTimeProfilingEntry& entry) { return sum + entry.enqueue; }); - avg.wait = std::accumulate(begin, end, 0, + avg.wait = std::accumulate(begin, end, int64_t{0}, [](int64_t sum, const HostTimeProfilingEntry& entry) { return sum + entry.wait; }); - avg.outputs_processing = std::accumulate(begin, end, 0, + avg.outputs_processing = std::accumulate(begin, end, int64_t{0}, [](int64_t sum, const HostTimeProfilingEntry& entry) { return sum + entry.outputs_processing; }); const auto iters_num = host_exec_times.size() - 1; @@ -248,6 +248,7 @@ std::shared_ptr Graph::get_runtime_model(std::vector(kv_in_ptr.data(), kv_out_ptr.data(), in_offset, out_offset); - else if (ov::element::Type(kv_layout.data_type).size() == 2) + else if (ov::element::Type(kv_layout.data_type).size() == 4) copy_element(kv_in_ptr.data(), kv_out_ptr.data(), in_offset, out_offset); } } @@ -165,7 +165,7 @@ VariableStateIndirectKVCacheCompressed::VariableStateIndirectKVCacheCompressed( const std::vector& output_layouts, size_t beam_idx, size_t concat_idx, - bool has_zp_state = false) + bool has_zp_state) : VariableStateIndirectKVCache(info, context, shape_predictor, beam_idx, concat_idx), m_has_zp_state(has_zp_state) { OPENVINO_ASSERT((has_zp_state && output_layouts.size() == 3) || diff --git a/src/plugins/intel_gpu/src/plugin/ops/atan2.cpp b/src/plugins/intel_gpu/src/plugin/ops/atan2.cpp new file mode 100644 index 000000000000..2039c4455156 --- /dev/null +++ b/src/plugins/intel_gpu/src/plugin/ops/atan2.cpp @@ -0,0 +1,28 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "intel_gpu/op/atan2.hpp" +#include "intel_gpu/plugin/program_builder.hpp" +#include "intel_gpu/plugin/common_utils.hpp" +#include "intel_gpu/primitives/eltwise.hpp" + +namespace ov::op::internal { +using Atan2 = ov::intel_gpu::op::Atan2; +} + +namespace ov::intel_gpu { + +static void CreateAtan2Op(ProgramBuilder& p, const std::shared_ptr& op) { + validate_inputs_count(op, {2}); + auto inputs = p.GetInputInfo(op); + std::string primitive_name = layer_type_name_ID(op); + + auto prim = cldnn::eltwise(primitive_name, inputs, cldnn::eltwise_mode::atan2); + prim.output_data_types = get_output_data_types(op); + p.add_primitive(*op, prim); +} + +REGISTER_FACTORY_IMPL(internal, Atan2); + +} // namespace ov::intel_gpu diff --git a/src/plugins/intel_gpu/src/plugin/ops/constant.cpp b/src/plugins/intel_gpu/src/plugin/ops/constant.cpp index 02fe2d382788..626dee0b4863 100644 --- a/src/plugins/intel_gpu/src/plugin/ops/constant.cpp +++ b/src/plugins/intel_gpu/src/plugin/ops/constant.cpp @@ -6,6 +6,7 @@ #include "intel_gpu/plugin/common_utils.hpp" #include "intel_gpu/op/convolution.hpp" +#include "openvino/core/weight_sharing_util.hpp" #include "openvino/op/constant.hpp" #include "openvino/op/convolution.hpp" #include "openvino/op/convert.hpp" @@ -145,6 +146,7 @@ static void create_data(ProgramBuilder& p, const ov::Shape& const_shape, const s } else { std::memcpy(&buf[0], &data[0], bufSize); } + ov::wsh::Extension::hint_evict(*op); p.add_primitive(*op, cldnn::data(initialconstPrimID, mem)); p.blobMemCache[cache_key] = initialconstPrimID; constPrimID = initialconstPrimID; @@ -286,14 +288,11 @@ static void CreateConstantOp(ProgramBuilder& p, const std::shared_ptr [1,1,1,d] - // index 1 (weight): [d] -> [1,1,d,1] - // Rank <4 non-1D: right-align into 4D: e.g. [M,K] -> [1,1,M,K], [B,M,K]->[1,B,M,K] + // Only reshape 1D and 2D constants to fix getConstTensor batch/feature + // mis-mapping. For rank >= 3, getConstTensor already produces layouts + // compatible with gemm_inst::transform_input_layouts (trailing 1s align + // with weight_rank extraction). Reshaping rank >= 3 would prepend 1s, + // breaking the first-N-dims extraction in transform_input_layouts. if (constDims.size() == 1) { ov::Shape reshaped_const_dims(const_static_max_dims, 1); const size_t const_idx = (user_index == 0)? @@ -301,7 +300,7 @@ static void CreateConstantOp(ProgramBuilder& p, const std::shared_ptrget_output_partial_shape(0); cldnn::primitive_id weights = inputs[op::Convolution::Args::WEIGHTS].pid; - const uint32_t groups = std::max(op->get_groups(), 1); + const uint32_t groups = static_cast(std::max(op->get_groups(), 1)); const bool weights_have_group_dim = op->get_groups() > 0; auto strides = op->get_strides(); @@ -99,11 +99,6 @@ static void CreateConvolutionBackpropDataOp(ProgramBuilder& p, const std::shared std::string layerName = layer_type_name_ID(op); auto dilations = op->get_dilations(); - for (auto d : dilations) { - if (d != 1) { - OPENVINO_THROW("Unsupported dilation in ConvolutionBackpropData ", op->get_friendly_name()); - } - } auto weightsName = inputs[1]; // WA: For the cases like Const(weights)->Sub(zp)->Deconv. And also for the cases with real runtime weights. @@ -184,11 +179,6 @@ static void CreateGroupConvolutionBackpropDataOp(ProgramBuilder& p, const std::s std::string layerName = layer_type_name_ID(op); auto dilations = op->get_dilations(); - for (auto d : dilations) { - if (d != 1) { - OPENVINO_THROW("Unsupported dilation in GroupConvolutionBackpropData ", op->get_friendly_name()); - } - } uint32_t groups = static_cast(op->get_input_shape(1)[0]); @@ -288,8 +278,8 @@ static void DeformableConvolutionImpl(ProgramBuilder& p, weights, "", true, - groups, - deformable_groups_num, + static_cast(groups), + static_cast(deformable_groups_num), strides, dilations, padding, diff --git a/src/plugins/intel_gpu/src/plugin/ops/custom.cpp b/src/plugins/intel_gpu/src/plugin/ops/custom.cpp index 6f428ae5cd69..a04b52bf6639 100644 --- a/src/plugins/intel_gpu/src/plugin/ops/custom.cpp +++ b/src/plugins/intel_gpu/src/plugin/ops/custom.cpp @@ -165,6 +165,13 @@ void CreateCustomOp(ProgramBuilder& p, const std::shared_ptr& op, Cust outputFormats.push_back(param.format); break; } + case CustomLayer::ParamType::Internal: { + kernelParameters.resize(kernelParameters.size() > size_t(param.paramIndex + 1) ? kernelParameters.size() : size_t(param.paramIndex + 1)); + kernelParameters[param.paramIndex].type = cldnn::custom_gpu_primitive::arg_internal; + kernelParameters[param.paramIndex].index = static_cast(param.portIndex); + kernelParameters[param.paramIndex].size_expr = param.size_expr; + break; + } default: OPENVINO_THROW("Invalid custom layer param type: ", param.type, " in operation: ", op->get_friendly_name()); } diff --git a/src/plugins/intel_gpu/src/plugin/ops/eltwise.cpp b/src/plugins/intel_gpu/src/plugin/ops/eltwise.cpp index e6be20ee37bd..341812519d0d 100644 --- a/src/plugins/intel_gpu/src/plugin/ops/eltwise.cpp +++ b/src/plugins/intel_gpu/src/plugin/ops/eltwise.cpp @@ -167,7 +167,8 @@ static void CreatePowerOp(ProgramBuilder& p, const std::shared_ptr(op->get_input_node_shared_ptr(1)); if (power_node) { - if (ov::shape_size(power_node->get_output_shape(0)) == 1) { + if (ov::shape_size(power_node->get_output_shape(0)) == 1 && + op->get_input_partial_shape(0).same_scheme(op->get_output_partial_shape(0))) { float pow; if (!ov::op::util::get_single_value(power_node, pow)) OPENVINO_THROW("Invalid parameter size in ", op->get_friendly_name(), " (", op->get_type_name(), ")"); diff --git a/src/plugins/intel_gpu/src/plugin/ops/experimental_detectron_roi_feature_extractor.cpp b/src/plugins/intel_gpu/src/plugin/ops/experimental_detectron_roi_feature_extractor.cpp index dbf7fb6e5306..9bd96647e988 100644 --- a/src/plugins/intel_gpu/src/plugin/ops/experimental_detectron_roi_feature_extractor.cpp +++ b/src/plugins/intel_gpu/src/plugin/ops/experimental_detectron_roi_feature_extractor.cpp @@ -20,9 +20,9 @@ static void CreateExperimentalDetectronROIFeatureExtractorOp(ProgramBuilder& p, if (p.use_new_shape_infer()) { cldnn::experimental_detectron_roi_feature_extractor prim(layer_type_name_ID(op), inputs, - operation_attributes.output_size, + static_cast(operation_attributes.output_size), operation_attributes.pyramid_scales, - operation_attributes.sampling_ratio, + static_cast(operation_attributes.sampling_ratio), operation_attributes.aligned); prim.num_outputs = op->get_output_size(); prim.output_data_types = get_output_data_types(op, {{ov::element::i64, ov::element::i32}}); @@ -46,9 +46,9 @@ static void CreateExperimentalDetectronROIFeatureExtractorOp(ProgramBuilder& p, cldnn::experimental_detectron_roi_feature_extractor experimentalDetectronPrim(layerName, inputs, - operation_attributes.output_size, + static_cast(operation_attributes.output_size), operation_attributes.pyramid_scales, - operation_attributes.sampling_ratio, + static_cast(operation_attributes.sampling_ratio), operation_attributes.aligned); p.add_primitive(*op, experimentalDetectronPrim); diff --git a/src/plugins/intel_gpu/src/plugin/ops/eye.cpp b/src/plugins/intel_gpu/src/plugin/ops/eye.cpp index c5afa207ab2e..e6a60da7a770 100644 --- a/src/plugins/intel_gpu/src/plugin/ops/eye.cpp +++ b/src/plugins/intel_gpu/src/plugin/ops/eye.cpp @@ -37,7 +37,7 @@ static void CreateEyeOp(ProgramBuilder& p, const std::shared_ptrget_data_ptr(); break; case ov::element::Type_t::i64: - shift = *constant->get_data_ptr(); + shift = static_cast(*constant->get_data_ptr()); break; default: throw std::runtime_error{"Input type can be only either i32 or i64"}; diff --git a/src/plugins/intel_gpu/src/plugin/ops/gather_matmul.cpp b/src/plugins/intel_gpu/src/plugin/ops/gather_matmul.cpp new file mode 100644 index 000000000000..4ce01eab114e --- /dev/null +++ b/src/plugins/intel_gpu/src/plugin/ops/gather_matmul.cpp @@ -0,0 +1,37 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "intel_gpu/primitives/gather_matmul.hpp" + +#include "intel_gpu/plugin/common_utils.hpp" +#include "intel_gpu/plugin/program_builder.hpp" +#include "ov_ops/gather_matmul_compressed.hpp" + +namespace ov::intel_gpu { +using namespace cldnn; + +static void CreateGatherMatmulCompressedOp(ProgramBuilder& p, const std::shared_ptr& op) { + auto inputs = p.GetInputInfo(op); + // Inputs: A, B, indices, bias, scales, zp (placeholder when absent). + validate_inputs_count(op, {6}); + + const auto& a_shape = op->get_input_partial_shape(0); + OPENVINO_ASSERT(a_shape.rank().is_static() && a_shape.size() >= 1 && a_shape[0].is_static(), + "GatherMatmulCompressed requires static n_activated_experts (input A dim[0]), got shape ", + a_shape); + const int32_t n_activated_experts = static_cast(a_shape[0].get_length()); + + // Placeholder inputs have shape_size <= 1. + const bool has_bias = ov::shape_size(op->get_input_shape(3)) > 1; + const bool has_zp = ov::shape_size(op->get_input_shape(5)) > 1; + + const std::string layerName = layer_type_name_ID(op); + const cldnn::gather_matmul bgm(layerName, inputs, has_bias, has_zp, n_activated_experts); + + p.add_primitive(*op, bgm); +} + +REGISTER_FACTORY_IMPL(internal, GatherMatmulCompressed); + +} // namespace ov::intel_gpu diff --git a/src/plugins/intel_gpu/src/plugin/ops/gather_nd.cpp b/src/plugins/intel_gpu/src/plugin/ops/gather_nd.cpp index 2ed55ae48f56..ffa984ba5603 100644 --- a/src/plugins/intel_gpu/src/plugin/ops/gather_nd.cpp +++ b/src/plugins/intel_gpu/src/plugin/ops/gather_nd.cpp @@ -17,9 +17,9 @@ static void CreateGatherNDOp(ProgramBuilder& p, const std::shared_ptr(op->get_input_partial_shape(0).size()); - auto indices_rank = static_cast(op->get_input_partial_shape(1).size()); - auto batch_dims = static_cast(op->get_batch_dims()); + auto input_rank = static_cast(op->get_input_partial_shape(0).size()); + auto indices_rank = static_cast(op->get_input_partial_shape(1).size()); + auto batch_dims = static_cast(op->get_batch_dims()); auto primitive = cldnn::gather_nd(layerName, inputs[0], @@ -38,9 +38,9 @@ static void CreateGatherNDOp(ProgramBuilder& p, const std::shared_ptr(op->get_input_partial_shape(0).size()); - auto indices_rank = static_cast(op->get_input_partial_shape(1).size()); - auto batch_dims = static_cast(op->get_batch_dims()); + auto input_rank = static_cast(op->get_input_partial_shape(0).size()); + auto indices_rank = static_cast(op->get_input_partial_shape(1).size()); + auto batch_dims = static_cast(op->get_batch_dims()); auto primitive = cldnn::gather_nd(layerName, inputs[0], diff --git a/src/plugins/intel_gpu/src/plugin/ops/interpolate.cpp b/src/plugins/intel_gpu/src/plugin/ops/interpolate.cpp index 75715155653e..b9f61bc10e8b 100644 --- a/src/plugins/intel_gpu/src/plugin/ops/interpolate.cpp +++ b/src/plugins/intel_gpu/src/plugin/ops/interpolate.cpp @@ -74,6 +74,7 @@ static void CreateInterpolateOp(ProgramBuilder& p, const std::shared_ptrget_attrs(); + const float cube_coeff = static_cast(attrs.cube_coeff); auto sizes_constant = ov::as_type_ptr(op->get_input_node_shared_ptr(SIZES_INDEX)); std::vector sizes = sizes_constant ? sizes_constant->cast_vector() : std::vector{}; @@ -101,7 +102,7 @@ static void CreateInterpolateOp(ProgramBuilder& p, const std::shared_ptrget_attrs(); + const float cube_coeff = static_cast(attrs.cube_coeff); auto scales_or_sizes_constant = ov::as_type_ptr(op->get_input_node_shared_ptr(eScalesOrSizesIndex)); std::vector scales = scales_or_sizes_constant && attrs.shape_calculation_mode == ov::op::v11::Interpolate::ShapeCalcMode::SCALES ? @@ -181,7 +183,7 @@ static void CreateInterpolateOp(ProgramBuilder& p, const std::shared_ptr #include +#include using Loop = ov::op::v5::Loop; using TensorIterator = ov::op::v0::TensorIterator; @@ -109,7 +110,7 @@ static void SetLoopInputOutputMap(ProgramBuilder& p, // set output mapping if (use_new_shape_infer) { for (const auto& loop_output_desc : loop_output_descs) { - cldnn::input_info external_input_info(layerName, loop_output_desc->m_output_index); + cldnn::input_info external_input_info(layerName, static_cast(loop_output_desc->m_output_index)); p.primitive_ids[layerName] = layerName; const auto& body_output = body_outputs.at(loop_output_desc->m_body_value_index); @@ -136,7 +137,7 @@ static void SetLoopInputOutputMap(ProgramBuilder& p, } } else { for (const auto& loop_output_desc : loop_output_descs) { - const uint64_t output_idx = loop_output_desc->m_output_index; + const int32_t output_idx = static_cast(loop_output_desc->m_output_index); // Add additional mutable_data for multiple outputs // primitive ID should be . if output_idx > 0 @@ -209,7 +210,11 @@ static void CreateCommonLoopOp(ProgramBuilder& p, const std::shared_ptris_dynamic(); - int64_t num_iterations = op->get_num_iterations(); + OPENVINO_ASSERT(op->get_num_iterations() >= std::numeric_limits::min() && + op->get_num_iterations() <= std::numeric_limits::max(), + "num_iterations (", op->get_num_iterations(), ") exceeds int32_t range"); + + int32_t num_iterations = static_cast(op->get_num_iterations()); auto num_outputs = is_dynamic? op->get_output_size() : 1; auto ov_model = op->get_function(); diff --git a/src/plugins/intel_gpu/src/plugin/ops/moe.cpp b/src/plugins/intel_gpu/src/plugin/ops/moe.cpp index 6c4b336907b8..500f2be81537 100644 --- a/src/plugins/intel_gpu/src/plugin/ops/moe.cpp +++ b/src/plugins/intel_gpu/src/plugin/ops/moe.cpp @@ -9,87 +9,17 @@ #include #include -#include "intel_gpu/op/moe_3gemm_fused_compressed.hpp" -#include "intel_gpu/op/moe_compressed.hpp" -#include "intel_gpu/plugin/common_utils.hpp" +#include "ov_ops/moe_compressed.hpp" #include "intel_gpu/plugin/program_builder.hpp" +#include "intel_gpu/plugin/common_utils.hpp" #include "intel_gpu/primitives/moe_3gemm_fused_compressed.hpp" #include "intel_gpu/primitives/moe_gemm.hpp" #include "intel_gpu/primitives/moe_mask_gen.hpp" #include "openvino/op/constant.hpp" -namespace ov { -namespace op { -namespace internal { -using MOE3GemmFusedCompressed = ov::intel_gpu::op::MOE3GemmFusedCompressed; -using MOECompressed = ov::intel_gpu::op::MOECompressed; -} // namespace internal -} // namespace op -} // namespace ov - namespace ov::intel_gpu { using namespace cldnn; -static void CreateMOE3GemmFusedCompressedOp(ProgramBuilder& p, const std::shared_ptr& op) { - auto inputs = p.GetInputInfo(op); - const auto& config = op->get_config(); - /// 0: hidden_states - input tensor with hidden representations - /// 1: routing_weights - [num_seq, num_experts] routing weights for all experts - /// 2: w0_weight - expert weights for first projection, - /// shape [num_experts, inter_size, group_num, group_size] - /// 3: w0_scale - expert scale for first projection for compressed experts, - /// shape [num_experts, inter_size, group_num, 1] - /// 4: w0_zp - expert zp for first projection for compressed experts, - /// shape [num_experts, inter_size, group_num, 1] - /// 5: w1_weight - expert weights for second projection, - /// shape [num_experts, inter_size, group_num, group_size] - /// 6: w1_scale - expert scale for second projection for compressed experts, - /// shape [num_experts, inter_size, group_num, 1] - /// 7: w1_zp - expert zp for second projection for compressed experts, - /// shape [num_experts, inter_size, group_num, 1] - /// 8: w2_weight - expert weights for final projection, - /// shape [num_experts, hidden_size, group_num, group_size] - /// 9: w2_scale - expert scale for final projection for compressed experts, - /// shape [num_experts, hidden_size, group_num, 1] - /// 10: w2_zp - expert zp for final projection for compressed experts, - /// shape [num_experts, hidden_size, group_num, 1] - /// 11: routing_bias (optional, SIGMOID_BIAS only; dummy placeholder for SOFTMAX+shared) - - /// [1, num_experts] routing bias for sigmoid routing - /// 12: routing_eps (optional, SIGMOID_BIAS only; dummy placeholder for SOFTMAX+shared) - - /// scalar epsilon for normalization - /// - /// Options for shared experts (if config.num_shared_expert > 0, always starting at index 13): - /// 13: shared_gate_weight - shared expert weights for first projection, - /// shape [1, inter_size, group_num, group_size] - /// 14: shared_gate_scale - shared expert scale for first projection, - /// shape [1, inter_size, group_num, 1] - /// 15: shared_gate_zp - shared expert zp for first projection, - /// shape [1, inter_size, group_num, 1] - /// 16: shared_up_weight - shared expert weights for second projection, - /// shape [1, inter_size, group_num, group_size] - /// 17: shared_up_scale - shared expert scale for second projection, - /// shape [1, inter_size, group_num, 1] - /// 18: shared_up_zp - shared expert zp for second projection, - /// shape [1, inter_size, group_num, 1] - /// 19: shared_down_weight - shared expert weights for final projection, - /// shape [1, hidden_size, group_num, group_size] - /// 20: shared_down_scale - shared expert scale for final projection, - /// shape [1, hidden_size, group_num, 1] - /// 21: shared_down_zp - shared expert zp for final projection, - /// shape [1, hidden_size, group_num, 1] - /// 22: shared_gate_gate_weight - shared expert gate weight for gating, - /// shape [hidden_size] - const size_t expected_inputs = config.num_shared_expert > 0 ? 23 - : config.routing_type == op::MOECompressed::RoutingType::SIGMOID_BIAS ? 13 - : 11; - validate_inputs_count(op, {expected_inputs}); - - const std::string layerName = layer_type_name_ID(op); - const cldnn::moe_3gemm_fused_compressed moe(layerName, inputs, config); - - p.add_primitive(*op, moe); -} - static void CreateMOECompressedOp(ProgramBuilder& p, const std::shared_ptr& op) { auto inputs = p.GetInputInfo(op); auto& config = op->get_config(); @@ -98,31 +28,14 @@ static void CreateMOECompressedOp(ProgramBuilder& p, const std::shared_ptr

output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); ASSERT_EQ(output_ptr.size(), p.expected_values.size()); for (size_t i = 0; i < output_ptr.size(); ++i) { @@ -740,7 +740,7 @@ TEST_P(grid_sample_gpu_dynamic, basic) { const auto outputs = network.execute(); auto output = outputs.at("plane_grid_sample").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); ASSERT_EQ(output_ptr.size(), p.expected_values.size()); for (size_t i = 0; i < output_ptr.size(); ++i) { diff --git a/src/plugins/intel_gpu/tests/unit/test_cases/group_normalization_gpu_test.cpp b/src/plugins/intel_gpu/tests/unit/test_cases/group_normalization_gpu_test.cpp index 766663220fc8..b0cdcc81ffdf 100644 --- a/src/plugins/intel_gpu/tests/unit/test_cases/group_normalization_gpu_test.cpp +++ b/src/plugins/intel_gpu/tests/unit/test_cases/group_normalization_gpu_test.cpp @@ -13,9 +13,12 @@ #include "intel_gpu/primitives/reorder.hpp" #include "intel_gpu/primitives/permute.hpp" #include "intel_gpu/primitives/eltwise.hpp" +#include "intel_gpu/primitives/activation.hpp" #include "openvino/reference/group_normalization.hpp" #include "intel_gpu/runtime/compilation_context.hpp" +#include + using namespace cldnn; using namespace ::tests; @@ -194,6 +197,18 @@ INSTANTIATE_TEST_SUITE_P( ::testing::ValuesIn(f_4d_formats), ::testing::ValuesIn({padding(), padding({0, 16, 0, 0})}))); +INSTANTIATE_TEST_SUITE_P( + GroupNormalizationGPUTest_blocked_layouts_support_4d_fused, GroupNormalizationGPUTest, + ::testing::Combine( + ::testing::ValuesIn({std::vector{3, 64, 32, 64}, std::vector{3, 128, 97, 61}, + std::vector{1, 48, 151, 1}, std::vector{1, 16, 2175, 1}}), + ::testing::ValuesIn(std::vector{4, 8}), + ::testing::Values(0.0025), + ::testing::ValuesIn(f_blocked_4d_formats), + ::testing::Values(padding()), + ::testing::ValuesIn(f_blocked_4d_formats), + ::testing::Values(padding()))); + INSTANTIATE_TEST_SUITE_P( GroupNormalizationGPUTest_planar_layouts_support_5d, GroupNormalizationGPUTest, ::testing::Combine( @@ -357,4 +372,253 @@ TEST(group_normalization, basic_b_fs_yx_fsv16) { for (std::size_t i = 0; i < reference_output.size(); i++) { ASSERT_NEAR(reference_output[i], output_mem_lock[i], 0.01); } -} \ No newline at end of file +} + +TEST(group_normalization, basic_b_fs_yx_fsv16_fused) { + auto& engine = get_test_engine(); + + const ov::Shape input_shape = {1, 128, 256, 256}; + const ov::Shape param_shape = {128, 1, 1, 1}; + auto in_layout = layout{input_shape, data_types::f32, format::bfyx}; + auto scale_layout = layout{param_shape, data_types::f32, format::bfyx}; + auto bias_layout = layout{param_shape, data_types::f32, format::bfyx}; + const float epsilon = 1e-5f; + const int64_t num_groups = 32; + + auto input_mem = engine.allocate_memory(in_layout); + auto scale_mem = engine.allocate_memory(scale_layout); + auto bias_mem = engine.allocate_memory(bias_layout); + + tests::random_generator rg{"GroupNormalizationGPUTest"}; + + std::vector in_vals = rg.generate_random_1d(ov::shape_size(input_shape), -1, 1); + std::vector scale_vals = rg.generate_random_1d(input_shape[1], -1, 1); + std::vector bias_vals = rg.generate_random_1d(input_shape[1], -1, 1); + + set_values(input_mem, in_vals); + set_values(scale_mem, scale_vals); + set_values(bias_mem, bias_vals); + + topology topology(input_layout("input_bfyx_f32", in_layout), + input_layout("scale_bfyx_f32", scale_layout), + input_layout("bias_bfyx_f32", bias_layout), + reorder("input_fsv16_f32", input_info("input_bfyx_f32"), format::b_fs_yx_fsv16, data_types::f32), + reorder("scale_fsv16_f32", input_info("scale_bfyx_f32"), format::b_fs_yx_fsv16, data_types::f32), + reorder("bias_fsv16_f32", input_info("bias_bfyx_f32"), format::b_fs_yx_fsv16, data_types::f32), + group_normalization("group_normalization_fsv16_fused_f32", + input_info("input_fsv16_f32"), + input_info("scale_fsv16_f32"), + input_info("bias_fsv16_f32"), + num_groups, + epsilon), + reorder("output_bfyx_f32", input_info("group_normalization_fsv16_fused_f32"), format::bfyx, data_types::f32)); + + ExecutionConfig config = get_test_default_config(engine); + ov::intel_gpu::ImplementationDesc gn_impl = {format::b_fs_yx_fsv16, "group_normalization_fsv16", impl_types::ocl}; + config.set_property(ov::intel_gpu::force_implementations(ov::intel_gpu::ImplForcingMap{{"group_normalization_fsv16_fused_f32", gn_impl}})); + config.set_property(ov::intel_gpu::allow_new_shape_infer(true)); + config.set_property(ov::intel_gpu::optimize_data(true)); + + network network(engine, topology, config); + network.set_input_data("input_bfyx_f32", input_mem); + network.set_input_data("scale_bfyx_f32", scale_mem); + network.set_input_data("bias_bfyx_f32", bias_mem); + + auto outputs = network.execute(); + auto output = outputs.at("output_bfyx_f32").get_memory(); + cldnn::mem_lock output_mem_lock(output, get_test_stream()); + + cldnn::mem_lock input_mem_lock(input_mem, get_test_stream()); + cldnn::mem_lock scale_mem_lock(scale_mem, get_test_stream()); + cldnn::mem_lock bias_mem_lock(bias_mem, get_test_stream()); + + std::vector reference_output(output_mem_lock.size()); + ov::reference::group_normalization(input_mem_lock.data(), + scale_mem_lock.data(), + bias_mem_lock.data(), + reference_output.data(), + input_shape, + num_groups, + epsilon); + + for (std::size_t i = 0; i < reference_output.size(); i++) { + ASSERT_NEAR(reference_output[i], output_mem_lock[i], 0.01); + } +} + +// Tests fused eltwise_sum with a separate 4D input into the group_normalization fused kernel. +// This covers the case where FUSED_OP0_LOAD indexes a separate tensor using (b),(f),(y),(x). +TEST(group_normalization, basic_b_fs_yx_fsv16_fused_eltwise_sum) { + auto& engine = get_test_engine(); + + const ov::Shape input_shape = {1, 32, 64, 64}; + const ov::Shape param_shape = {32, 1, 1, 1}; + auto in_layout = layout{input_shape, data_types::f16, format::bfyx}; + auto eltwise_layout = layout{input_shape, data_types::f16, format::bfyx}; + auto scale_layout = layout{param_shape, data_types::f16, format::bfyx}; + auto bias_layout = layout{param_shape, data_types::f16, format::bfyx}; + const float epsilon = 1e-5f; + const int64_t num_groups = 8; + + auto input_mem = engine.allocate_memory(in_layout); + auto eltwise_mem = engine.allocate_memory(eltwise_layout); + auto scale_mem = engine.allocate_memory(scale_layout); + auto bias_mem = engine.allocate_memory(bias_layout); + + tests::random_generator rg{"GroupNormalizationGPUTest_fused_eltwise"}; + + auto in_vals = rg.generate_random_1d(ov::shape_size(input_shape), -1, 1); + auto eltwise_vals = rg.generate_random_1d(ov::shape_size(input_shape), -1, 1); + auto scale_vals = rg.generate_random_1d(input_shape[1], -1, 1); + auto bias_vals = rg.generate_random_1d(input_shape[1], -1, 1); + + set_values(input_mem, in_vals); + set_values(eltwise_mem, eltwise_vals); + set_values(scale_mem, scale_vals); + set_values(bias_mem, bias_vals); + + // Topology: group_norm -> eltwise_sum(with separate input) -> output + // The eltwise_sum should be fused into the group_normalization kernel. + topology topology( + input_layout("input", in_layout), + input_layout("eltwise_input", eltwise_layout), + input_layout("scale", scale_layout), + input_layout("bias", bias_layout), + reorder("input_fsv16", input_info("input"), format::b_fs_yx_fsv16, data_types::f16), + reorder("eltwise_input_fsv16", input_info("eltwise_input"), format::b_fs_yx_fsv16, data_types::f16), + reorder("scale_fsv16", input_info("scale"), format::b_fs_yx_fsv16, data_types::f16), + reorder("bias_fsv16", input_info("bias"), format::b_fs_yx_fsv16, data_types::f16), + group_normalization("group_norm", + input_info("input_fsv16"), + input_info("scale_fsv16"), + input_info("bias_fsv16"), + num_groups, + epsilon), + eltwise("eltwise_sum", + {input_info("group_norm"), input_info("eltwise_input_fsv16")}, + eltwise_mode::sum), + reorder("output", input_info("eltwise_sum"), format::bfyx, data_types::f16)); + + ExecutionConfig config = get_test_default_config(engine); + ov::intel_gpu::ImplementationDesc gn_impl = {format::b_fs_yx_fsv16, "group_normalization_fsv16", impl_types::ocl}; + config.set_property(ov::intel_gpu::force_implementations(ov::intel_gpu::ImplForcingMap{{"group_norm", gn_impl}})); + config.set_property(ov::intel_gpu::allow_new_shape_infer(true)); + config.set_property(ov::intel_gpu::optimize_data(true)); + + network network(engine, topology, config); + network.set_input_data("input", input_mem); + network.set_input_data("eltwise_input", eltwise_mem); + network.set_input_data("scale", scale_mem); + network.set_input_data("bias", bias_mem); + + auto outputs = network.execute(); + + // Verify eltwise_sum was fused into group_norm (not executed as separate primitive) + auto executed_primitives = network.get_executed_primitives(); + ASSERT_TRUE(executed_primitives.count("eltwise_sum") == 0); + + auto output = outputs.at("output").get_memory(); + cldnn::mem_lock output_mem_lock(output, get_test_stream()); + + // Compute reference: group_norm + eltwise_sum + std::vector in_f32(in_vals.begin(), in_vals.end()); + std::vector scale_f32(scale_vals.begin(), scale_vals.end()); + std::vector bias_f32(bias_vals.begin(), bias_vals.end()); + std::vector ref_gn(ov::shape_size(input_shape)); + ov::reference::group_normalization(in_f32.data(), scale_f32.data(), bias_f32.data(), + ref_gn.data(), input_shape, num_groups, epsilon); + + for (std::size_t i = 0; i < ref_gn.size(); i++) { + float expected = ref_gn[i] + static_cast(eltwise_vals[i]); + ASSERT_NEAR(static_cast(output_mem_lock[i]), expected, 0.05) << "at index " << i; + } +} + +// Tests fused eltwise_sum + ReLU activation into the group_normalization fused kernel. +// This matches the SegNext model pattern: GroupNorm -> Add -> ReLU. +TEST(group_normalization, basic_b_fs_yx_fsv16_fused_eltwise_sum_relu) { + auto& engine = get_test_engine(); + + const ov::Shape input_shape = {1, 32, 33, 17}; // Non-power-of-2 spatial dims to stress y/x derivation and CHUNK_SIZE tail handling + const ov::Shape param_shape = {32, 1, 1, 1}; + auto in_layout = layout{input_shape, data_types::f16, format::bfyx}; + auto eltwise_layout = layout{input_shape, data_types::f16, format::bfyx}; + auto scale_layout = layout{param_shape, data_types::f16, format::bfyx}; + auto bias_layout = layout{param_shape, data_types::f16, format::bfyx}; + const float epsilon = 1e-5f; + const int64_t num_groups = 8; + + auto input_mem = engine.allocate_memory(in_layout); + auto eltwise_mem = engine.allocate_memory(eltwise_layout); + auto scale_mem = engine.allocate_memory(scale_layout); + auto bias_mem = engine.allocate_memory(bias_layout); + + tests::random_generator rg{"GroupNormalizationGPUTest_fused_eltwise_relu"}; + + auto in_vals = rg.generate_random_1d(ov::shape_size(input_shape), -1, 1); + auto eltwise_vals = rg.generate_random_1d(ov::shape_size(input_shape), -1, 1); + auto scale_vals = rg.generate_random_1d(input_shape[1], -1, 1); + auto bias_vals = rg.generate_random_1d(input_shape[1], -1, 1); + + set_values(input_mem, in_vals); + set_values(eltwise_mem, eltwise_vals); + set_values(scale_mem, scale_vals); + set_values(bias_mem, bias_vals); + + topology topology( + input_layout("input", in_layout), + input_layout("eltwise_input", eltwise_layout), + input_layout("scale", scale_layout), + input_layout("bias", bias_layout), + reorder("input_fsv16", input_info("input"), format::b_fs_yx_fsv16, data_types::f16), + reorder("eltwise_input_fsv16", input_info("eltwise_input"), format::b_fs_yx_fsv16, data_types::f16), + reorder("scale_fsv16", input_info("scale"), format::b_fs_yx_fsv16, data_types::f16), + reorder("bias_fsv16", input_info("bias"), format::b_fs_yx_fsv16, data_types::f16), + group_normalization("group_norm", + input_info("input_fsv16"), + input_info("scale_fsv16"), + input_info("bias_fsv16"), + num_groups, + epsilon), + eltwise("eltwise_sum", + {input_info("group_norm"), input_info("eltwise_input_fsv16")}, + eltwise_mode::sum), + activation("relu", input_info("eltwise_sum"), activation_func::relu), + reorder("output", input_info("relu"), format::bfyx, data_types::f16)); + + ExecutionConfig config = get_test_default_config(engine); + ov::intel_gpu::ImplementationDesc gn_impl = {format::b_fs_yx_fsv16, "group_normalization_fsv16", impl_types::ocl}; + config.set_property(ov::intel_gpu::force_implementations(ov::intel_gpu::ImplForcingMap{{"group_norm", gn_impl}})); + config.set_property(ov::intel_gpu::allow_new_shape_infer(true)); + config.set_property(ov::intel_gpu::optimize_data(true)); + + network network(engine, topology, config); + network.set_input_data("input", input_mem); + network.set_input_data("eltwise_input", eltwise_mem); + network.set_input_data("scale", scale_mem); + network.set_input_data("bias", bias_mem); + + auto outputs = network.execute(); + + // Verify eltwise_sum and relu were fused into group_norm (not executed as separate primitives) + auto executed_primitives = network.get_executed_primitives(); + ASSERT_TRUE(executed_primitives.count("eltwise_sum") == 0); + ASSERT_TRUE(executed_primitives.count("relu") == 0); + + auto output = outputs.at("output").get_memory(); + cldnn::mem_lock output_mem_lock(output, get_test_stream()); + + // Compute reference: group_norm + eltwise_sum + relu + std::vector in_f32(in_vals.begin(), in_vals.end()); + std::vector scale_f32(scale_vals.begin(), scale_vals.end()); + std::vector bias_f32(bias_vals.begin(), bias_vals.end()); + std::vector ref_gn(ov::shape_size(input_shape)); + ov::reference::group_normalization(in_f32.data(), scale_f32.data(), bias_f32.data(), + ref_gn.data(), input_shape, num_groups, epsilon); + + for (std::size_t i = 0; i < ref_gn.size(); i++) { + float sum_val = ref_gn[i] + static_cast(eltwise_vals[i]); + float expected = std::max(0.0f, sum_val); + ASSERT_NEAR(static_cast(output_mem_lock[i]), expected, 0.05) << "at index " << i; + } +} diff --git a/src/plugins/intel_gpu/tests/unit/test_cases/hash_key_gpu_test.cpp b/src/plugins/intel_gpu/tests/unit/test_cases/hash_key_gpu_test.cpp index 4e17b3a09ce7..28d79dbc45bd 100644 --- a/src/plugins/intel_gpu/tests/unit/test_cases/hash_key_gpu_test.cpp +++ b/src/plugins/intel_gpu/tests/unit/test_cases/hash_key_gpu_test.cpp @@ -24,6 +24,44 @@ using namespace tests; class check_hash_value: public ::testing::Test { public: + // These tests verify that primitive and parameter hashes are non-zero and + // consistent between cached and non-cached networks. + // + // Limitation: because cldnn::hash_combine (runtime/utils.hpp) relies on std::hash, + // whose output is implementation-defined and varies across compilers and versions, + // these tests cannot compare against fixed golden values. As a consequence, they will + // not detect silent changes to the set of fields fed into a hash function (e.g. a new + // field accidentally omitted from primitive::hash()). Sensitivity tests that mutate + // individual fields and assert hash changes would cover that gap. + struct hash_pair { + size_t primitive_hash; + size_t params_hash; + }; + + hash_pair get_hashes(cldnn::network& net, const std::string& key_prim_id) { + const auto prim_inst = net.get_primitive(key_prim_id); + const auto prim = prim_inst->desc(); + return { prim->hash(), prim_inst->get_impl_params()->hash() }; + } + + hash_pair get_fc_hashes(cldnn::network& net, const std::string& key_prim_id) { + const auto prim_inst = net.get_primitive(key_prim_id); + const auto prim = prim_inst->desc(); + return { prim->hash(), prim->type->get_fake_aligned_params(*prim_inst->get_impl_params()).hash() }; + } + + void verify_hash(const hash_pair& h, const std::string& label) { + ASSERT_NE(h.primitive_hash, 0UL) << label << ": primitive hash should be non-zero"; + ASSERT_NE(h.params_hash, 0UL) << label << ": params hash should be non-zero"; + } + + void verify_hash_consistency(const hash_pair& ref, const hash_pair& cached, const std::string& label) { + ASSERT_EQ(cached.primitive_hash, ref.primitive_hash) + << label << ": primitive hash must be preserved across caching"; + ASSERT_EQ(cached.params_hash, ref.params_hash) + << label << ": params hash must be preserved across caching"; + } + void test_eltwise_basic(bool is_caching_test) { auto& engine = get_test_engine(); @@ -36,15 +74,13 @@ class check_hash_value: public ::testing::Test { topology.add(input_layout("input2", input2->get_layout())); topology.add(eltwise(key_prim_id, { input_info("input"), input_info("input2") }, eltwise_mode::sum)); - cldnn::network::ptr net = get_network(engine, topology, get_test_default_config(engine), get_test_stream_ptr(), is_caching_test); - const auto prim_inst = net->get_primitive(key_prim_id); - const auto primitve = prim_inst->desc(); - - const auto primitive_hash = primitve->hash(); - const auto params_hash = prim_inst->get_impl_params()->hash(); + auto ref_net = std::make_shared(engine, topology, get_test_default_config(engine)); + auto ref = get_hashes(*ref_net, key_prim_id); + verify_hash(ref, "eltwise"); - ASSERT_EQ(primitive_hash, 4145865612957978777UL); - ASSERT_EQ(params_hash, 1717643793116242977UL); + cldnn::network::ptr net = get_network(engine, topology, get_test_default_config(engine), get_test_stream_ptr(), is_caching_test); + auto h = get_hashes(*net, key_prim_id); + verify_hash_consistency(ref, h, "eltwise"); } void test_fc_basic(bool is_caching_test) { @@ -57,26 +93,25 @@ class check_hash_value: public ::testing::Test { auto bias_prim = engine.allocate_memory({ { out_f }, data_types::f16, format::bfyx }); const auto key_prim_id = "fc"; - topology topology( - input_layout("input", input_prim->get_layout()), - data("weights", weights_prim), - data("bias", bias_prim), - fully_connected(key_prim_id, input_info("input"), "weights", "bias") - ); - + // Program build may detach primitive data memory, so use independent topologies. + auto make_topology = [&]() { + return topology( + input_layout("input", input_prim->get_layout()), + data("weights", weights_prim), + data("bias", bias_prim), + fully_connected(key_prim_id, input_info("input"), "weights", "bias") + ); + }; + + auto ref_topology = make_topology(); + auto ref_net = std::make_shared(engine, ref_topology, get_test_default_config(engine)); + auto ref = get_fc_hashes(*ref_net, key_prim_id); + verify_hash(ref, "fc"); + + auto topology = make_topology(); cldnn::network::ptr net = get_network(engine, topology, get_test_default_config(engine), get_test_stream_ptr(), is_caching_test); - const auto prim_inst = net->get_primitive(key_prim_id); - const auto primitve = prim_inst->desc(); - - const auto primitive_hash = primitve->hash(); - const auto params_hash = primitve->type->get_fake_aligned_params(*prim_inst->get_impl_params()).hash(); - if (!engine.get_device_info().supports_immad) { - ASSERT_EQ(primitive_hash, 7598234300934878892UL); - ASSERT_EQ(params_hash, 14588531505879944960UL); - } else { - ASSERT_EQ(primitive_hash, 7598234300934878892UL); - ASSERT_EQ(params_hash, 2121456519277740670UL); - } + auto h = get_fc_hashes(*net, key_prim_id); + verify_hash_consistency(ref, h, "fc"); } void test_gather_basic(bool is_caching_test) { @@ -97,15 +132,13 @@ class check_hash_value: public ::testing::Test { gather(key_prim_id, input_info("InputDictionary"), input_info("InputText"), axis, 5, ov::Shape{3, 2, 3, 3, 2}, batch_dim, negative_indexes) ); - cldnn::network::ptr net = get_network(engine, topology, get_test_default_config(engine), get_test_stream_ptr(), is_caching_test); - const auto prim_inst = net->get_primitive(key_prim_id); - const auto primitve = prim_inst->desc(); - - const auto primitive_hash = primitve->hash(); - const auto params_hash = prim_inst->get_impl_params()->hash(); + auto ref_net = std::make_shared(engine, topology, get_test_default_config(engine)); + auto ref = get_hashes(*ref_net, key_prim_id); + verify_hash(ref, "gather"); - ASSERT_EQ(primitive_hash, 7823853951962111674UL); - ASSERT_EQ(params_hash, 5049423120420866837UL); + cldnn::network::ptr net = get_network(engine, topology, get_test_default_config(engine), get_test_stream_ptr(), is_caching_test); + auto h = get_hashes(*net, key_prim_id); + verify_hash_consistency(ref, h, "gather"); } void test_gemm_basic(bool is_caching_test) { @@ -121,14 +154,13 @@ class check_hash_value: public ::testing::Test { topology.add(crop("crop.1", input_info("input"), { 1, 1, 4, 3 }, { 0, 1, 0, 0 })); topology.add(gemm(key_prim_id, { input_info("crop.1"), input_info("input2") }, data_types::f32, false, true)); - cldnn::network::ptr net = get_network(engine, topology, get_test_default_config(engine), get_test_stream_ptr(), is_caching_test); - const auto prim_inst = net->get_primitive(key_prim_id); - const auto primitve = prim_inst->desc(); + auto ref_net = std::make_shared(engine, topology, get_test_default_config(engine)); + auto ref = get_hashes(*ref_net, key_prim_id); + verify_hash(ref, "gemm"); - const auto primitive_hash = primitve->hash(); - const auto params_hash = prim_inst->get_impl_params()->hash(); - ASSERT_EQ(primitive_hash, 13388149315122571178UL); - ASSERT_EQ(params_hash, 17362657208739837157UL); + cldnn::network::ptr net = get_network(engine, topology, get_test_default_config(engine), get_test_stream_ptr(), is_caching_test); + auto h = get_hashes(*net, key_prim_id); + verify_hash_consistency(ref, h, "gemm"); } void test_permute_basic(bool is_caching_test) { @@ -141,15 +173,13 @@ class check_hash_value: public ::testing::Test { input_layout("input", input->get_layout()), permute(key_prim_id, input_info("input"), { 0, 1, 2, 3 })); - cldnn::network::ptr net = get_network(engine, topology, get_test_default_config(engine), get_test_stream_ptr(), is_caching_test); - const auto prim_inst = net->get_primitive(key_prim_id); - const auto primitve = prim_inst->desc(); + auto ref_net = std::make_shared(engine, topology, get_test_default_config(engine)); + auto ref = get_hashes(*ref_net, key_prim_id); + verify_hash(ref, "permute"); - const auto primitive_hash = primitve->hash(); - const auto params_hash = prim_inst->get_impl_params()->hash(); - - ASSERT_EQ(primitive_hash, 4658575237077439700UL); - ASSERT_EQ(params_hash, 15976735712435632434UL); + cldnn::network::ptr net = get_network(engine, topology, get_test_default_config(engine), get_test_stream_ptr(), is_caching_test); + auto h = get_hashes(*net, key_prim_id); + verify_hash_consistency(ref, h, "permute"); } void test_reorder_basic(bool is_caching_test) { @@ -168,15 +198,13 @@ class check_hash_value: public ::testing::Test { input_layout("input", input->get_layout()), reorder(key_prim_id, input_info("input"), output_layout)); - cldnn::network::ptr net = get_network(engine, topology, get_test_default_config(engine), get_test_stream_ptr(), is_caching_test); - const auto prim_inst = net->get_primitive(key_prim_id); - const auto primitve = prim_inst->desc(); + auto ref_net = std::make_shared(engine, topology, get_test_default_config(engine)); + auto ref = get_hashes(*ref_net, key_prim_id); + verify_hash(ref, "reorder"); - const auto primitive_hash = primitve->hash(); - const auto params_hash = prim_inst->get_impl_params()->hash(); - - ASSERT_EQ(primitive_hash, 16293979194373117692UL); - ASSERT_EQ(params_hash, 3897060862522441010UL); + cldnn::network::ptr net = get_network(engine, topology, get_test_default_config(engine), get_test_stream_ptr(), is_caching_test); + auto h = get_hashes(*net, key_prim_id); + verify_hash_consistency(ref, h, "reorder"); } void test_reshape_basic(bool is_caching_test) { @@ -194,15 +222,13 @@ class check_hash_value: public ::testing::Test { reshape_prim.output_paddings = {padding({0, 0, 2, 2})}; topology.add(reshape_prim); - cldnn::network::ptr net = get_network(engine, topology, get_test_default_config(engine), get_test_stream_ptr(), is_caching_test); - const auto prim_inst = net->get_primitive(key_prim_id); - const auto primitve = prim_inst->desc(); - - const auto primitive_hash = primitve->hash(); - const auto params_hash = prim_inst->get_impl_params()->hash(); + auto ref_net = std::make_shared(engine, topology, get_test_default_config(engine)); + auto ref = get_hashes(*ref_net, key_prim_id); + verify_hash(ref, "reshape"); - ASSERT_EQ(primitive_hash, 1534749073560581535UL); - ASSERT_EQ(params_hash, 6426521365118381035UL); + cldnn::network::ptr net = get_network(engine, topology, get_test_default_config(engine), get_test_stream_ptr(), is_caching_test); + auto h = get_hashes(*net, key_prim_id); + verify_hash_consistency(ref, h, "reshape"); } void test_conv_basic(bool is_caching_test) { @@ -213,21 +239,23 @@ class check_hash_value: public ::testing::Test { auto biases = engine.allocate_memory({ { 1, 1, 1, 1 }, data_types::f32, format::bfyx }); auto key_prim_id = "convolution"; - topology topology( - input_layout("input", input->get_layout()), - data("weights", weights), - data("biases", biases), - convolution(key_prim_id, input_info("input"), "weights", "biases", 1, {1, 1, 1}, {1, 1, 1}, {0, 0, 0}, {0, 0, 0}, false)); - + auto make_topology = [&]() { + return topology( + input_layout("input", input->get_layout()), + data("weights", weights), + data("biases", biases), + convolution(key_prim_id, input_info("input"), "weights", "biases", 1, {1, 1, 1}, {1, 1, 1}, {0, 0, 0}, {0, 0, 0}, false)); + }; + + auto ref_topology = make_topology(); + auto ref_net = std::make_shared(engine, ref_topology, get_test_default_config(engine)); + auto ref = get_hashes(*ref_net, key_prim_id); + verify_hash(ref, "conv"); + + auto topology = make_topology(); cldnn::network::ptr net = get_network(engine, topology, get_test_default_config(engine), get_test_stream_ptr(), is_caching_test); - const auto prim_inst = net->get_primitive(key_prim_id); - const auto primitve = prim_inst->desc(); - - const auto primitive_hash = primitve->hash(); - const auto params_hash = prim_inst->get_impl_params()->hash(); - - ASSERT_EQ(primitive_hash, 10199782087454290518UL); - ASSERT_EQ(params_hash, 655948409235783525UL); + auto h = get_hashes(*net, key_prim_id); + verify_hash_consistency(ref, h, "conv"); } void test_quantize_basic(bool is_caching_test) { @@ -240,24 +268,28 @@ class check_hash_value: public ::testing::Test { auto output_high = engine.allocate_memory({ { 1, 1, 1, 1 }, data_types::f32,format::bfyx }); auto key_prim_id = "quantize"; - topology topology; - topology.add( - input_layout("input", input->get_layout()), - data("input_low", input_low), - data("input_high", input_high), - data("output_low", output_low), - data("output_high", output_high), - quantize(key_prim_id, input_info("input"), input_info("input_low"), input_info("input_high"), input_info("output_low"), input_info("output_high"), 256, data_types::u8) - ); - + auto make_topology = [&]() { + topology topology; + topology.add( + input_layout("input", input->get_layout()), + data("input_low", input_low), + data("input_high", input_high), + data("output_low", output_low), + data("output_high", output_high), + quantize(key_prim_id, input_info("input"), input_info("input_low"), input_info("input_high"), input_info("output_low"), input_info("output_high"), 256, data_types::u8) + ); + return topology; + }; + + auto ref_topology = make_topology(); + auto ref_net = std::make_shared(engine, ref_topology, get_test_default_config(engine)); + auto ref = get_hashes(*ref_net, key_prim_id); + verify_hash(ref, "quantize"); + + auto topology = make_topology(); cldnn::network::ptr net = get_network(engine, topology, get_test_default_config(engine), get_test_stream_ptr(), is_caching_test); - const auto prim_inst = net->get_primitive(key_prim_id); - const auto primitve = prim_inst->desc(); - - const auto primitive_hash = primitve->hash(); - const auto params_hash = prim_inst->get_impl_params()->hash(); - ASSERT_EQ(primitive_hash, 4135863035456568493UL); - ASSERT_EQ(params_hash, 9610563181439837451UL); + auto h = get_hashes(*net, key_prim_id); + verify_hash_consistency(ref, h, "quantize"); } }; diff --git a/src/plugins/intel_gpu/tests/unit/test_cases/loop_gpu_test.cpp b/src/plugins/intel_gpu/tests/unit/test_cases/loop_gpu_test.cpp index f68d3c07e25c..e293d4a75dc3 100644 --- a/src/plugins/intel_gpu/tests/unit/test_cases/loop_gpu_test.cpp +++ b/src/plugins/intel_gpu/tests/unit/test_cases/loop_gpu_test.cpp @@ -288,7 +288,7 @@ void test_loop_gpu_basic_concat_nested(bool is_caching_test) 1.f, -2.f, 3.f, -4.f }; - size_t inner_trip_count = input_data.size() / inner_eltwise_operand.size(); + int32_t inner_trip_count = static_cast(input_data.size() / inner_eltwise_operand.size()); int inner_initial_condition = 1; int outer_trip_count = 8; int outer_initial_condition = 1; diff --git a/src/plugins/intel_gpu/tests/unit/test_cases/lrn_gpu_test.cpp b/src/plugins/intel_gpu/tests/unit/test_cases/lrn_gpu_test.cpp index ced15be9d673..938938661669 100644 --- a/src/plugins/intel_gpu/tests/unit/test_cases/lrn_gpu_test.cpp +++ b/src/plugins/intel_gpu/tests/unit/test_cases/lrn_gpu_test.cpp @@ -44,7 +44,7 @@ void test_fp32_basic(bool is_caching_test) { auto outputs = network->execute(); auto output = outputs.at("lrn").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = { 0.f, 1.99901f, 3.99486f, 5.98519f, @@ -96,7 +96,7 @@ void test_fp32_basic2(bool is_caching_test) { auto outputs = network->execute(); auto output = outputs.at("lrn").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = { 0.f, 1.99889f, 3.99525f, 5.98696f, @@ -148,7 +148,7 @@ void test_fp16_basic1(bool is_caching_test) { auto outputs = network->execute(); auto output = outputs.at("lrn").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = { 0.f, 1.99889f, 3.99525f, 5.98696f, @@ -200,7 +200,7 @@ void test_fp32_basic3(bool is_caching_test) { auto outputs = network->execute(); auto output = outputs.at("lrn").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = { 0.f, 0.999792f, 1.99911f, 2.99755f, 3.99466f, 4.99f, 5.98313f, 6.97361f, 7.96102f, 8.94493f, 9.92493f, diff --git a/src/plugins/intel_gpu/tests/unit/test_cases/lru_caches_gpu_test.cpp b/src/plugins/intel_gpu/tests/unit/test_cases/lru_caches_gpu_test.cpp index 5767c26a9a49..e574b811f54b 100644 --- a/src/plugins/intel_gpu/tests/unit/test_cases/lru_caches_gpu_test.cpp +++ b/src/plugins/intel_gpu/tests/unit/test_cases/lru_caches_gpu_test.cpp @@ -125,9 +125,11 @@ TEST(lru_cache, custom_data_type) { } namespace { -struct ImplHasher { - size_t operator()(const kernel_impl_params &k) const { - return k.hash(); +// Force all entries to collide so the test exercises the cache's collision-handling +// logic regardless of the std::hash implementation (which is not portable across compilers). +struct CollidingImplHasher { + size_t operator()(const kernel_impl_params& /*k*/) const { + return 42; } }; } // namespace @@ -141,7 +143,7 @@ TEST(lru_cache, collisions) { auto shape_of1_prim = std::make_shared("shape_of1", input_info("input1"), data_types::i64); auto shape_of2_prim = std::make_shared("shape_of2", input_info("input2"), data_types::i64); - using ImplementationsCache = cldnn::LruCacheThreadSafe, ImplHasher>; + using ImplementationsCache = cldnn::LruCacheThreadSafe, CollidingImplHasher>; ImplementationsCache cache(0); program prog(get_test_engine()); @@ -153,7 +155,7 @@ TEST(lru_cache, collisions) { program_wrapper::add_connection(prog, input2_node, shape_of2_node); auto params1 = *shape_of1_node.get_kernel_impl_params(); - auto params2 = *shape_of1_node.get_kernel_impl_params(); + auto params2 = *shape_of2_node.get_kernel_impl_params(); auto out_layouts1 = shape_of_inst::calc_output_layouts(shape_of1_node, params1); auto out_layouts2 = shape_of_inst::calc_output_layouts(shape_of2_node, params2); @@ -167,15 +169,12 @@ TEST(lru_cache, collisions) { auto impl1 = shape_of1_node.type()->create_impl(shape_of1_node); auto impl2 = shape_of2_node.type()->create_impl(shape_of2_node); - // Ensure that hashes for primitive, input layouts and full impl params are same due to collision - ASSERT_EQ(shape_of1_prim->hash(), shape_of2_prim->hash()); - ASSERT_EQ(l1.hash(), l2.hash()); - ASSERT_EQ(shape_of1_node.get_kernel_impl_params()->hash(), shape_of2_node.get_kernel_impl_params()->hash()); - ASSERT_FALSE(shape_of1_node.get_kernel_impl_params() == shape_of2_node.get_kernel_impl_params()); + // Params must differ so the cache stores both despite the forced hash collision + ASSERT_FALSE(*shape_of1_node.get_kernel_impl_params() == *shape_of2_node.get_kernel_impl_params()); cache.add(*shape_of1_node.get_kernel_impl_params(), impl1->clone()); cache.add(*shape_of2_node.get_kernel_impl_params(), impl2->clone()); - // But cache still contains both entries, as input layouts are differenet - thus kernels are different + // Cache still contains both entries, as input layouts are different — thus kernels are different ASSERT_EQ(cache.size(), 2); } diff --git a/src/plugins/intel_gpu/tests/unit/test_cases/matrix_nms_gpu_test.cpp b/src/plugins/intel_gpu/tests/unit/test_cases/matrix_nms_gpu_test.cpp index 7720aba21c8f..ee4bbb29f030 100644 --- a/src/plugins/intel_gpu/tests/unit/test_cases/matrix_nms_gpu_test.cpp +++ b/src/plugins/intel_gpu/tests/unit/test_cases/matrix_nms_gpu_test.cpp @@ -113,7 +113,7 @@ struct matrix_nms_gpu_test : public testing::TestWithParamexecute(); auto output = outputs.at("matrix_nms").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); cldnn::mem_lock selected_boxes_ptr(selected_boxes, get_test_stream()); cldnn::mem_lock valid_outputs_ptr(valid_outputs, get_test_stream()); diff --git a/src/plugins/intel_gpu/tests/unit/test_cases/memory_test.cpp b/src/plugins/intel_gpu/tests/unit/test_cases/memory_test.cpp index 1919af97e8e4..f4c9cc76b545 100644 --- a/src/plugins/intel_gpu/tests/unit/test_cases/memory_test.cpp +++ b/src/plugins/intel_gpu/tests/unit/test_cases/memory_test.cpp @@ -15,6 +15,8 @@ #include #include #include +#include +#include #include using namespace cldnn; @@ -91,7 +93,9 @@ class memory_pool: public ::testing::Test { network->set_input_data("input", input); auto outputs = network->execute(); - ASSERT_EQ(engine->get_max_used_device_memory(), (uint64_t)64); + // input_layout no longer pre-allocates memory at network construction time + // so we have only 16 bytes usm_host for the input buffer, 16 bytes for "relu" output, and 16 bytes for "relu5" output. + ASSERT_EQ(engine->get_max_used_device_memory(), 48ull); } void test_basic_non_padded_relu_and_pooling_pipe(bool is_caching_test) { @@ -123,7 +127,11 @@ class memory_pool: public ::testing::Test { network->set_input_data("input", input); auto outputs = network->execute(); - ASSERT_EQ(engine->get_max_used_device_memory(), (uint64_t)896); + // input_layout no longer pre-allocates memory at network construction time, + // The remaining peak is 640 bytes: + // 256 bytes host for the input, 256 bytes device for relu output, + // 64 bytes device for pool1 output, and 64 bytes host for relu5 output. + ASSERT_EQ(engine->get_max_used_device_memory(), 640ull); } void test_multi_outputs_network(bool is_caching_test) { @@ -158,7 +166,20 @@ class memory_pool: public ::testing::Test { network->set_input_data("input", input); auto outputs = network->execute(); - ASSERT_EQ(engine->get_max_used_device_memory(), (uint64_t) 1536); + auto input_inst = network->get_primitive("input"); + auto relu_inst = network->get_primitive("relu"); + auto relu2_inst = network->get_primitive("relu2"); + + // Both direct consumers of the lazy-allocated input must see the same external buffer. + ASSERT_TRUE(engine->is_the_same_buffer(*input_inst->output_memory_ptr(), *input)); + ASSERT_TRUE(engine->is_the_same_buffer(*relu_inst->dep_memory_ptr(0), *input)); + ASSERT_TRUE(engine->is_the_same_buffer(*relu2_inst->dep_memory_ptr(0), *input)); + + // input_layout no longer pre-allocates memory at network construction time, + // The remaining peak is 1280 bytes: + // 256 bytes host for the input, 256 bytes host for relu4 output, 256 bytes host for relu7 output, + // and 256 bytes device for two outputs from the first relu on a branch + ASSERT_EQ(engine->get_max_used_device_memory(), 1280ull); } void test_oooq(bool is_caching_test) { @@ -196,7 +217,12 @@ class memory_pool: public ::testing::Test { network->set_input_data("input", input); auto outputs = network->execute(); - ASSERT_EQ(engine->get_max_used_device_memory(), (uint64_t) 2560); + // input_layout no longer pre-allocates memory at network construction time, + // The remaining peak is 2304 bytes: + // 256 bytes host for the input, 768 bytes host for the final relu6 output, + // and 1280 bytes device for intermediates: 256 bytes each for relu1, + // relu2, and the lower branch feeding concat2, plus 512 bytes for concat1 / relu4. + ASSERT_EQ(engine->get_max_used_device_memory(), 2304ull); } void test_shared_mem_pool_same_topology_twice() { @@ -243,7 +269,7 @@ class memory_pool: public ::testing::Test { auto output_memory_first = outputs.at("relu6").get_memory(); auto output_layout_first = output_memory_first->get_layout(); - cldnn::mem_lock output_ptr_first(output_memory_first, get_test_stream()); + cldnn::mem_lock output_ptr_first(output_memory_first, get_test_stream()); ASSERT_EQ(engine->get_max_used_device_memory(), (uint64_t) 2560); @@ -253,7 +279,7 @@ class memory_pool: public ::testing::Test { auto output_memory_second = outputs_second.at("relu6").get_memory(); auto output_layout_second = output_memory_second->get_layout(); - cldnn::mem_lock output_ptr_second(output_memory_second, get_test_stream()); + cldnn::mem_lock output_ptr_second(output_memory_second, get_test_stream()); ASSERT_EQ(engine->get_max_used_device_memory(), (uint64_t) 3328); @@ -323,7 +349,7 @@ class memory_pool: public ::testing::Test { auto output_memory_first = outputs.at("softmax").get_memory(); auto output_layout_first = output_memory_first->get_layout(); - cldnn::mem_lock output_ptr_first(output_memory_first, get_test_stream()); + cldnn::mem_lock output_ptr_first(output_memory_first, get_test_stream()); network network_second(*engine, topology, config); network_second.set_input_data("input", input); @@ -331,7 +357,7 @@ class memory_pool: public ::testing::Test { auto output_memory_second = outputs_second.at("softmax").get_memory(); auto output_layout_second = output_memory_second->get_layout(); - cldnn::mem_lock output_ptr_second(output_memory_second, get_test_stream()); + cldnn::mem_lock output_ptr_second(output_memory_second, get_test_stream()); cl_mem_result = 1224; usm_result = 1992; // USM have a higher peak, since transfering memory to device adds temporay memory bytes allocated. Old memory is deallocated quickly, but max peak is higher. @@ -380,6 +406,7 @@ class memory_pool: public ::testing::Test { auto input_1 = engine->allocate_memory(lay_batch_1); auto input_8 = engine->allocate_memory(lay_batch_8); auto weights = engine->allocate_memory({ dt, fmt, { 1, 3, 3, 2 } }); + // so far we allocated 192+1536+72 = 1800 bytes host memory std::vector dummy_input_data_1 = rg.generate_random_1d(batch_1 * feature_num * inp_x_size * inp_y_size, 0, 1); std::vector dummy_input_data_8 = rg.generate_random_1d(batch_8 * feature_num * inp_x_size * inp_y_size, 0, 1); @@ -403,15 +430,174 @@ class memory_pool: public ::testing::Test { network_first->set_input_data("input", input_8); auto outputs = network_first->execute(); - auto dev_info = engine->get_device_info(); - ASSERT_EQ(engine->get_max_used_device_memory(), (uint64_t)4744); + // Compute expected allocations from actual primitive layouts chosen by the runtime, + // rather than hardcoding device-dependent byte totals. + // get_max_used_device_memory() sums independent peaks per allocation type (cl_mem, + // usm_host, usm_device). On USM devices the sum of per-type peaks may slightly + // exceed the true simultaneous peak, producing device-dependent totals. + auto sum_internal_alloc = [](const network::ptr& net) -> uint64_t { + uint64_t total = 0; + for (auto& pi : net->get_primitives_info()) { + if (pi.original_id != "input") + total += pi.output_layout.bytes_count(); + } + return total; + }; + + uint64_t manual_allocs = lay_batch_1.bytes_count() + lay_batch_8.bytes_count() + + weights->get_layout().bytes_count(); + uint64_t net1_internal = sum_internal_alloc(network_first); + uint64_t expected_peak1 = manual_allocs + net1_internal; + ASSERT_EQ(engine->get_max_used_device_memory(), expected_peak1); topo.change_input_layout("input", input_1->get_layout());//change input layout to batch=1 network::ptr network_second = get_network(*engine, topo, config, get_test_stream_ptr(), is_caching_test); network_second->set_input_data("input", input_1); auto outputs_second = network_second->execute(); - ASSERT_EQ(engine->get_max_used_device_memory(), (uint64_t)5912); + + uint64_t net2_internal = sum_internal_alloc(network_second); + auto peak_after_second = engine->get_max_used_device_memory(); + auto delta = peak_after_second - expected_peak1; + + // Delta should equal net2's internal allocations. Memory pool may reuse one + // intermediate buffer (e.g. in-place softmax on cl_mem), reducing delta by up + // to one intermediate buffer size. + uint64_t min_intermediate = std::numeric_limits::max(); + for (auto& pi : network_second->get_primitives_info()) { + if (pi.original_id != "input" && pi.original_id.find("weights") == std::string::npos) + min_intermediate = std::min(min_intermediate, static_cast(pi.output_layout.bytes_count())); + } + if (min_intermediate == std::numeric_limits::max()) + min_intermediate = 0; + + ASSERT_LE(delta, net2_internal); + ASSERT_GE(delta, net2_internal - min_intermediate); + // Smaller batch must use less or equal internal memory + ASSERT_LE(net2_internal, net1_internal); + } + + void test_rebind_external_memory_multiple_times(bool is_caching_test) { + auto engine = create_test_engine(); + layout input_lay{data_types::f32, format::bfyx, {1, 1, 2, 4}}; + auto input_first = engine->allocate_memory(input_lay); + auto input_second = engine->allocate_memory(input_lay); + + const std::vector first_values = {-1.0f, 2.0f, -3.0f, 4.0f, -5.0f, 6.0f, -7.0f, 8.0f}; + const std::vector second_values = {10.0f, -20.0f, 30.0f, -40.0f, 50.0f, -60.0f, 70.0f, -80.0f}; + set_values(input_first, first_values); + set_values(input_second, second_values); + + topology topology; + topology.add(input_layout("input", input_lay)); + topology.add(activation("relu", input_info("input"), activation_func::relu)); + + ExecutionConfig config = get_test_default_config(*engine); + config.set_property(ov::intel_gpu::optimize_data(true)); + + network::ptr network = get_network(*engine, topology, config, get_test_stream_ptr(), is_caching_test); + + auto input_inst = network->get_primitive("input"); + auto relu_inst = network->get_primitive("relu"); + + // Lazy input allocation means input_layout should stay unmaterialized until set_input_data(). + ASSERT_EQ(input_inst->output_memory_ptr(), nullptr); + + network->set_input_data("input", input_first); + auto outputs_first = network->execute(); + + // After the first bind, both input_layout and the downstream consumer must point to the + // external memory object passed through set_input_data(). + ASSERT_TRUE(engine->is_the_same_buffer(*input_inst->output_memory_ptr(), *input_first)); + ASSERT_TRUE(engine->is_the_same_buffer(*relu_inst->dep_memory_ptr(0), *input_first)); + + auto first_bound_input_ptr = relu_inst->dep_memory_ptr(0)->buffer_ptr(); + { + cldnn::mem_lock first_output_ptr(outputs_first.at("relu").get_memory(), get_test_stream()); + ASSERT_EQ(first_output_ptr[0], 0.0f); + ASSERT_EQ(first_output_ptr[1], 2.0f); + ASSERT_EQ(first_output_ptr[2], 0.0f); + ASSERT_EQ(first_output_ptr[3], 4.0f); + ASSERT_EQ(first_output_ptr[4], 0.0f); + ASSERT_EQ(first_output_ptr[5], 6.0f); + ASSERT_EQ(first_output_ptr[6], 0.0f); + ASSERT_EQ(first_output_ptr[7], 8.0f); + } + + network->set_input_data("input", input_second); + auto outputs_second = network->execute(); + + // Rebinding must refresh the consumer dependency as well, not only input_layout itself. + ASSERT_TRUE(engine->is_the_same_buffer(*input_inst->output_memory_ptr(), *input_second)); + ASSERT_TRUE(engine->is_the_same_buffer(*relu_inst->dep_memory_ptr(0), *input_second)); + + auto second_bound_input_ptr = relu_inst->dep_memory_ptr(0)->buffer_ptr(); + // The consumer should no longer reference the buffer from the first inference. + ASSERT_NE(first_bound_input_ptr, second_bound_input_ptr); + + cldnn::mem_lock second_output_ptr(outputs_second.at("relu").get_memory(), get_test_stream()); + ASSERT_EQ(second_output_ptr[0], 10.0f); + ASSERT_EQ(second_output_ptr[1], 0.0f); + ASSERT_EQ(second_output_ptr[2], 30.0f); + ASSERT_EQ(second_output_ptr[3], 0.0f); + ASSERT_EQ(second_output_ptr[4], 50.0f); + ASSERT_EQ(second_output_ptr[5], 0.0f); + ASSERT_EQ(second_output_ptr[6], 70.0f); + ASSERT_EQ(second_output_ptr[7], 0.0f); + } + + void test_aliasing_through_chain_of_optimized_ops(bool is_caching_test) { + auto engine = create_test_engine(); + layout input_lay{ov::PartialShape{5, 6, 1, 1}, data_types::f32, format::bfyx}; + auto input = engine->allocate_memory(input_lay); + + set_values(input, { 0.0f, 1.0f, 2.0f, 3.0f, 4.0f, 5.0f, + 0.0f, 1.0f, 2.0f, 3.0f, 4.0f, 5.0f, + 0.0f, 1.0f, 2.0f, 3.0f, 4.0f, 5.0f, + 0.0f, 1.0f, 2.0f, 3.0f, 4.0f, 5.0f, + 0.0f, 1.0f, 2.0f, 3.0f, 4.0f, 5.0f }); + + topology topology; + topology.add(input_layout("input", input_lay)); + topology.add(crop("crop1", {input_info("input")}, tensor(5, 5, 1, 1), {tensor(0, 0, 0, 0)})); + topology.add(crop("crop2", {input_info("crop1")}, tensor(5, 4, 1, 1), {tensor(0, 0, 0, 0)})); + topology.add(reorder("reorder_out", input_info("crop2"), layout{ov::PartialShape{5, 4, 1, 1}, data_types::f32, format::bfyx})); + + ExecutionConfig config = get_test_default_config(*engine); + config.set_property(ov::intel_gpu::optimize_data(true)); + + network::ptr network = get_network(*engine, topology, config, get_test_stream_ptr(), is_caching_test); + + auto input_inst = network->get_primitive("input"); + auto crop1_inst = network->get_primitive("crop1"); + auto crop2_inst = network->get_primitive("crop2"); + auto reorder_inst = network->get_primitive("reorder_out"); + + ASSERT_EQ(input_inst->output_memory_ptr(), nullptr); + + network->set_input_data("input", input); + auto outputs = network->execute(); + + ASSERT_TRUE(crop1_inst->can_be_optimized()); + ASSERT_TRUE(crop2_inst->can_be_optimized()); + + // The optimized crop chain should stay backed by the original external input buffer. + ASSERT_TRUE(engine->is_the_same_buffer(*input_inst->output_memory_ptr(), *input)); + ASSERT_TRUE(engine->is_the_same_buffer(*network->get_output_memory("crop1"), *input)); + ASSERT_TRUE(engine->is_the_same_buffer(*network->get_output_memory("crop2"), *input)); + + // The final consumer must receive the aliased view from the last optimized op in the chain. + ASSERT_TRUE(engine->is_the_same_buffer(*reorder_inst->dep_memory_ptr(0), *network->get_output_memory("crop2"))); + + cldnn::mem_lock output_ptr(outputs.at("reorder_out").get_memory(), get_test_stream()); + const std::vector expected = { 0.0f, 1.0f, 2.0f, 3.0f, + 0.0f, 1.0f, 2.0f, 3.0f, + 0.0f, 1.0f, 2.0f, 3.0f, + 0.0f, 1.0f, 2.0f, 3.0f, + 0.0f, 1.0f, 2.0f, 3.0f }; + for (size_t i = 0; i < expected.size(); ++i) { + ASSERT_EQ(output_ptr[i], expected[i]); + } } void test_shared_dep_two_output(bool is_caching_test) { @@ -723,6 +909,14 @@ TEST_F(memory_pool, shared_mem_pool_diff_batches) { this->test_shared_mem_pool_diff_batches(false); } +TEST_F(memory_pool, rebind_external_memory_multiple_times) { + this->test_rebind_external_memory_multiple_times(false); +} + +TEST_F(memory_pool, aliasing_through_chain_of_optimized_ops) { + this->test_aliasing_through_chain_of_optimized_ops(false); +} + TEST_F(memory_pool, shared_dep_two_output) { this->test_shared_dep_two_output(false); } @@ -770,6 +964,14 @@ TEST_F(memory_pool, shared_mem_pool_diff_batches_cached) { this->test_shared_mem_pool_diff_batches(true); } +TEST_F(memory_pool, rebind_external_memory_multiple_times_cached) { + this->test_rebind_external_memory_multiple_times(true); +} + +TEST_F(memory_pool, aliasing_through_chain_of_optimized_ops_cached) { + this->test_aliasing_through_chain_of_optimized_ops(true); +} + TEST_F(memory_pool, shared_dep_two_output_cached) { this->test_shared_dep_two_output(true); } diff --git a/src/plugins/intel_gpu/tests/unit/test_cases/moe_3gemm_gpu_test.cpp b/src/plugins/intel_gpu/tests/unit/test_cases/moe_3gemm_gpu_test.cpp index 91bfa0ca34b3..d6167d4a0b65 100644 --- a/src/plugins/intel_gpu/tests/unit/test_cases/moe_3gemm_gpu_test.cpp +++ b/src/plugins/intel_gpu/tests/unit/test_cases/moe_3gemm_gpu_test.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -392,7 +393,7 @@ struct Moe3GemmTestParams { bool is_signed = false; }; -class moe_3gemm_compressed_gpu_random : public ::testing::TestWithParam> {}; +class moe_3gemm_compressed_gpu_random : public ::testing::TestWithParam> {}; TEST_P(moe_3gemm_compressed_gpu_random, moe_accuracy_test_random) { const auto& [routing_type, param] = GetParam(); @@ -446,13 +447,6 @@ TEST_P(moe_3gemm_compressed_gpu_random, moe_accuracy_test_random) { return mem; }; - auto create_zp_tensor = [&](const std::vector& values, int64_t b, int64_t f, int64_t y, int64_t x) { - auto dt = config.is_u4 ? data_types::u4 : data_types::u8; - auto mem = engine.allocate_memory({dt, format::bfyx, {b, f, y, x}}); - set_values(mem, values); - get_test_stream().finish(); - return mem; - }; auto create_f16_tensor = [&](const std::vector& values, int64_t b, int64_t f, int64_t y, int64_t x) { auto mem = engine.allocate_memory({data_types::f16, format::bfyx, {b, f, y, x}}); @@ -477,17 +471,36 @@ TEST_P(moe_3gemm_compressed_gpu_random, moe_accuracy_test_random) { size_t group_num = config.hidden_size / config.group_size; size_t group_num2 = config.inter_size / config.group_size; + // Logical [E, ofm, G]; physical [E, G, ofm] via byfx for G>1, plain bfyx for per-channel. + auto make_scale = [&](const std::vector& v, int64_t E, int64_t ofm, int64_t G) { + auto fmt = G > 1 ? format::byfx : format::bfyx; + auto shape = G > 1 ? ov::PartialShape{E, ofm, G, 1} : ov::PartialShape{E, ofm, 1}; + auto mem = engine.allocate_memory(layout{shape, data_types::f16, fmt}); + set_values(mem, v); + get_test_stream().finish(); + return mem; + }; + auto make_zp = [&](const std::vector& v, int64_t E, int64_t ofm, int64_t G) { + auto dt = config.is_u4 ? data_types::u4 : data_types::u8; + auto fmt = G > 1 ? format::byfx : format::bfyx; + auto shape = G > 1 ? ov::PartialShape{E, ofm, G, 1} : ov::PartialShape{E, ofm, 1}; + auto mem = engine.allocate_memory(layout{shape, dt, fmt}); + set_values(mem, v); + get_test_stream().finish(); + return mem; + }; + auto w0_weight_mem = create_weight_tensor(w0_q_packed, config.num_experts, config.inter_size, config.group_size, group_num); - auto w0_scale_mem = create_f16_tensor(w0_scale, config.num_experts, group_num, 1, config.inter_size); - auto w0_zp_mem = create_zp_tensor(w0_zp_packed, config.num_experts, group_num, 1, config.inter_size); + auto w0_scale_mem = make_scale(w0_scale, config.num_experts, config.inter_size, group_num); + auto w0_zp_mem = make_zp(w0_zp_packed, config.num_experts, config.inter_size, group_num); auto w1_weight_mem = create_weight_tensor(w1_q_packed, config.num_experts, config.inter_size, config.group_size, group_num); - auto w1_scale_mem = create_f16_tensor(w1_scale, config.num_experts, group_num, 1, config.inter_size); - auto w1_zp_mem = create_zp_tensor(w1_zp_packed, config.num_experts, group_num, 1, config.inter_size); + auto w1_scale_mem = make_scale(w1_scale, config.num_experts, config.inter_size, group_num); + auto w1_zp_mem = make_zp(w1_zp_packed, config.num_experts, config.inter_size, group_num); auto w2_weight_mem = create_weight_tensor(w2_q_packed, config.num_experts, config.hidden_size, config.group_size, group_num2); - auto w2_scale_mem = create_f16_tensor(w2_scale, config.num_experts, group_num2, 1, config.hidden_size); - auto w2_zp_mem = create_zp_tensor(w2_zp_packed, config.num_experts, group_num2, 1, config.hidden_size); + auto w2_scale_mem = make_scale(w2_scale, config.num_experts, config.hidden_size, group_num2); + auto w2_zp_mem = make_zp(w2_zp_packed, config.num_experts, config.hidden_size, group_num2); // Build topology topology topology; @@ -503,18 +516,36 @@ TEST_P(moe_3gemm_compressed_gpu_random, moe_accuracy_test_random) { topology.add(data("w2_scale", w2_scale_mem)); topology.add(data("w2_zp", w2_zp_mem)); - cldnn::MOE3GemmFusedCompressed::Config moe_config; + // Create MoERouterFused primitive + MoERouterFused::Config router_config; + router_config.num_expert = config.num_experts; + router_config.top_k = config.top_k; + std::vector router_inputs{input_info("routing_weights")}; + if (routing_type == cldnn::MoERouterFused::RoutingType::SIGMOID_BIAS) { + router_config.routing_type = MoERouterFused::RoutingType::SIGMOID_BIAS; + topology.add(data("routing_bias", routing_bias_mem)); + auto routing_eps_mem = engine.allocate_memory({data_types::f16, format::bfyx, {1, 1, 1, 1}}); + set_values(routing_eps_mem, {routing_eps_val}); + get_test_stream().finish(); + topology.add(data("routing_eps", routing_eps_mem)); + router_inputs.push_back(input_info("routing_bias")); + router_inputs.push_back(input_info("routing_eps")); + } + topology.add(moe_router_fused("router", router_inputs, router_config)); + + // Create MOE3GemmFusedCompressed primitive + cldnn::MOECompressed::Config moe_config; moe_config.hidden_size = config.hidden_size; moe_config.inter_size = config.inter_size; moe_config.num_expert = config.num_experts; moe_config.top_k = config.top_k; moe_config.group_size = config.group_size; moe_config.out_type = data_types::f16; - moe_config.routing_type = routing_type; moe_config.has_zp = true; std::vector moe_inputs{input_info("hidden_states"), - input_info("routing_weights"), + input_info("router", 0), // topk_weights + input_info("router", 1), // topk_indices input_info("w0_weight"), input_info("w0_scale"), input_info("w0_zp"), @@ -524,21 +555,12 @@ TEST_P(moe_3gemm_compressed_gpu_random, moe_accuracy_test_random) { input_info("w2_weight"), input_info("w2_scale"), input_info("w2_zp")}; - if (routing_type == cldnn::MOE3GemmFusedCompressed::RoutingType::SIGMOID_BIAS) { - topology.add(data("routing_bias", routing_bias_mem)); - moe_inputs.push_back(input_info("routing_bias")); - auto routing_eps_mem = engine.allocate_memory({data_types::f16, format::bfyx, {1, 1, 1, 1}}); - set_values(routing_eps_mem, {routing_eps_val}); - get_test_stream().finish(); - topology.add(data("routing_eps", routing_eps_mem)); - moe_inputs.push_back(input_info("routing_eps")); - } - - auto moe_prim = moe_3gemm_fused_compressed("moe_3gemm_fused_compressed", moe_inputs, moe_config); - topology.add(moe_prim); + topology.add(moe_3gemm_fused_compressed("moe_3gemm_fused_compressed", moe_inputs, moe_config)); - network network(engine, topology, get_test_default_config(engine)); + auto net_config = get_test_default_config(engine); + net_config.set_property(ov::intel_gpu::allow_new_shape_infer(true)); + network network(engine, topology, net_config); network.set_input_data("hidden_states", hidden_states_mem); network.set_input_data("routing_weights", routing_weights_mem); @@ -547,12 +569,12 @@ TEST_P(moe_3gemm_compressed_gpu_random, moe_accuracy_test_random) { get_test_stream().flush(); cldnn::mem_lock output_ptr(output_prim, get_test_stream()); - auto ref_output = routing_type == cldnn::MOE3GemmFusedCompressed::RoutingType::SIGMOID_BIAS + auto ref_output = routing_type == cldnn::MoERouterFused::RoutingType::SIGMOID_BIAS ? ref.run_reference_sigmoid(hidden_states, routing_weights, routing_bias_data, routing_eps_val, w0_data, w1_data, w2_data) : ref.run_reference_softmax(hidden_states, routing_weights, w0_data, w1_data, w2_data); // SigmoidBias routing performs all routing math (sigmoid, bias, normalization) in f16 on the kernel side, // while the reference uses f32, leading to slightly larger numerical divergence than Softmax. - const float base_tolerance = routing_type == cldnn::MOE3GemmFusedCompressed::RoutingType::SIGMOID_BIAS ? 0.2f : 0.1f; + const float base_tolerance = routing_type == cldnn::MoERouterFused::RoutingType::SIGMOID_BIAS ? 0.2f : 0.1f; const float tolerance = base_tolerance * (config.hidden_size / 128); for (size_t i = 0; i < ref_output.size(); ++i) { ASSERT_NEAR(static_cast(output_ptr[i]), static_cast(ref_output[i]), tolerance); @@ -561,8 +583,8 @@ TEST_P(moe_3gemm_compressed_gpu_random, moe_accuracy_test_random) { INSTANTIATE_TEST_SUITE_P(smoke, moe_3gemm_compressed_gpu_random, - ::testing::Combine(::testing::Values(cldnn::MOE3GemmFusedCompressed::RoutingType::SOFTMAX, - cldnn::MOE3GemmFusedCompressed::RoutingType::SIGMOID_BIAS), + ::testing::Combine(::testing::Values(cldnn::MoERouterFused::RoutingType::SOFTMAX, + cldnn::MoERouterFused::RoutingType::SIGMOID_BIAS), ::testing::Values(Moe3GemmTestParams{1, true, 128, 256, 4, 2, 128}, Moe3GemmTestParams{16, true, 128, 256, 4, 2, 128}, Moe3GemmTestParams{1, false, 128, 256, 4, 2, 128}, @@ -572,7 +594,40 @@ INSTANTIATE_TEST_SUITE_P(smoke, Moe3GemmTestParams{1, true, 512, 512, 4, 2, 512}, Moe3GemmTestParams{1, false, 512, 512, 4, 2, 512}))); -class moe_3gemm_compressed_gpu_u4 : public ::testing::TestWithParam {}; +// Sub-128 weight quantization group size (e.g. trinity-mini afmoe uses group_size=64 +// with SIGMOID_BIAS routing). Limited to SIGMOID_BIAS to mirror real model usage and +// to avoid known accuracy noise of the SOFTMAX path on some platforms. +INSTANTIATE_TEST_SUITE_P(smoke_sub128_group_size, + moe_3gemm_compressed_gpu_random, + ::testing::Combine(::testing::Values(cldnn::MoERouterFused::RoutingType::SIGMOID_BIAS), + ::testing::Values(Moe3GemmTestParams{1, true, 128, 256, 4, 2, 64}, + Moe3GemmTestParams{16, true, 128, 256, 4, 2, 64}, + Moe3GemmTestParams{1, false, 128, 256, 4, 2, 64}, + Moe3GemmTestParams{1, true, 256, 512, 4, 2, 64}, + Moe3GemmTestParams{1, false, 256, 512, 4, 2, 64}))); + +// Batched GEMV: MTP/speculative decoding scenarios where token_num is 2-16. +// These small batch sizes exercise the batched GEMV path (exec_batched_gemv) +// which avoids gather/scatter/CPU-sync overhead of the prefill GEMM path. +INSTANTIATE_TEST_SUITE_P(smoke_batched_gemv_mtp, + moe_3gemm_compressed_gpu_random, + ::testing::Combine(::testing::Values(cldnn::MoERouterFused::RoutingType::SOFTMAX, + cldnn::MoERouterFused::RoutingType::SIGMOID_BIAS), + ::testing::Values(Moe3GemmTestParams{2, true, 128, 256, 4, 2, 128}, + Moe3GemmTestParams{4, true, 128, 256, 4, 2, 128}, + Moe3GemmTestParams{8, true, 128, 256, 4, 2, 128}, + Moe3GemmTestParams{2, false, 128, 256, 4, 2, 128}, + Moe3GemmTestParams{4, false, 128, 256, 4, 2, 128}, + Moe3GemmTestParams{8, false, 128, 256, 4, 2, 128}, + Moe3GemmTestParams{2, true, 256, 512, 4, 2, 256}, + Moe3GemmTestParams{4, true, 256, 512, 4, 2, 256}, + Moe3GemmTestParams{2, false, 256, 512, 4, 2, 256}, + Moe3GemmTestParams{4, false, 256, 512, 4, 2, 256}, + // Sub-128 group_size batched GEMV coverage. + Moe3GemmTestParams{2, true, 128, 256, 4, 2, 64}, + Moe3GemmTestParams{2, false, 128, 256, 4, 2, 64}))); + +class moe_3gemm_compressed_gpu_u4 : public ::testing::TestWithParam {}; class moe_3gemm_compressed_gpu_shared_random : public ::testing::TestWithParam {}; @@ -655,13 +710,6 @@ TEST_P(moe_3gemm_compressed_gpu_shared_random, moe_accuracy_test_shared_expert_r return mem; }; - auto create_zp_tensor = [&](const std::vector& values, int64_t b, int64_t f, int64_t y, int64_t x) { - auto dt = config.is_u4 ? data_types::u4 : data_types::u8; - auto mem = engine.allocate_memory({dt, format::bfyx, {b, f, y, x}}); - set_values(mem, values); - get_test_stream().finish(); - return mem; - }; auto create_f16_tensor = [&](const std::vector& values, int64_t b, int64_t f, int64_t y, int64_t x) { auto mem = engine.allocate_memory({data_types::f16, format::bfyx, {b, f, y, x}}); @@ -689,32 +737,49 @@ TEST_P(moe_3gemm_compressed_gpu_shared_random, moe_accuracy_test_shared_expert_r size_t group_num2 = config.inter_size / config.group_size; // Expert Weights + // Logical [E, ofm, G]; physical [E, G, ofm] via byfx for G>1, plain bfyx for per-channel. + auto make_scale = [&](const std::vector& v, int64_t E, int64_t ofm, int64_t G) { + auto fmt = G > 1 ? format::byfx : format::bfyx; + auto shape = G > 1 ? ov::PartialShape{E, ofm, G, 1} : ov::PartialShape{E, ofm, 1}; + auto mem = engine.allocate_memory(layout{shape, data_types::f16, fmt}); + set_values(mem, v); + get_test_stream().finish(); + return mem; + }; + auto make_zp = [&](const std::vector& v, int64_t E, int64_t ofm, int64_t G) { + auto dt = config.is_u4 ? data_types::u4 : data_types::u8; + auto fmt = G > 1 ? format::byfx : format::bfyx; + auto shape = G > 1 ? ov::PartialShape{E, ofm, G, 1} : ov::PartialShape{E, ofm, 1}; + auto mem = engine.allocate_memory(layout{shape, dt, fmt}); + set_values(mem, v); + get_test_stream().finish(); + return mem; + }; + auto w0_weight_mem = create_weight_tensor(w0_q_packed, config.num_experts, config.inter_size, config.group_size, group_num); - auto w0_scale_mem = create_f16_tensor(w0_scale, config.num_experts, group_num, 1, config.inter_size); - auto w0_zp_mem = create_zp_tensor(w0_zp_packed, config.num_experts, group_num, 1, config.inter_size); + auto w0_scale_mem = make_scale(w0_scale, config.num_experts, config.inter_size, group_num); + auto w0_zp_mem = make_zp(w0_zp_packed, config.num_experts, config.inter_size, group_num); auto w1_weight_mem = create_weight_tensor(w1_q_packed, config.num_experts, config.inter_size, config.group_size, group_num); - auto w1_scale_mem = create_f16_tensor(w1_scale, config.num_experts, group_num, 1, config.inter_size); - auto w1_zp_mem = create_zp_tensor(w1_zp_packed, config.num_experts, group_num, 1, config.inter_size); + auto w1_scale_mem = make_scale(w1_scale, config.num_experts, config.inter_size, group_num); + auto w1_zp_mem = make_zp(w1_zp_packed, config.num_experts, config.inter_size, group_num); auto w2_weight_mem = create_weight_tensor(w2_q_packed, config.num_experts, config.hidden_size, config.group_size, group_num2); - auto w2_scale_mem = create_f16_tensor(w2_scale, config.num_experts, group_num2, 1, config.hidden_size); - auto w2_zp_mem = create_zp_tensor(w2_zp_packed, config.num_experts, group_num2, 1, config.hidden_size); + auto w2_scale_mem = make_scale(w2_scale, config.num_experts, config.hidden_size, group_num2); + auto w2_zp_mem = make_zp(w2_zp_packed, config.num_experts, config.hidden_size, group_num2); // Shared Expert Weights (num_experts = 1) - // create_weight_tensor(values, b, f, y, x) -> b=1, f=inter_size, y=group_size, x=group_num auto s_gate_weight_mem = create_weight_tensor(s_gate_q_packed, 1, config.inter_size, config.group_size, group_num); - auto s_gate_scale_mem = create_f16_tensor(s_gate_scale, 1, group_num, 1, config.inter_size); - auto s_gate_zp_mem = create_zp_tensor(s_gate_zp_packed, 1, group_num, 1, config.inter_size); + auto s_gate_scale_mem = make_scale(s_gate_scale, 1, config.inter_size, group_num); + auto s_gate_zp_mem = make_zp(s_gate_zp_packed, 1, config.inter_size, group_num); auto s_up_weight_mem = create_weight_tensor(s_up_q_packed, 1, config.inter_size, config.group_size, group_num); - auto s_up_scale_mem = create_f16_tensor(s_up_scale, 1, group_num, 1, config.inter_size); - auto s_up_zp_mem = create_zp_tensor(s_up_zp_packed, 1, group_num, 1, config.inter_size); + auto s_up_scale_mem = make_scale(s_up_scale, 1, config.inter_size, group_num); + auto s_up_zp_mem = make_zp(s_up_zp_packed, 1, config.inter_size, group_num); - // down: b=1, f=hidden_size, y=group_num2, x=group_size auto s_down_weight_mem = create_weight_tensor(s_down_q_packed, 1, config.hidden_size, config.group_size, group_num2); - auto s_down_scale_mem = create_f16_tensor(s_down_scale, 1, group_num2, 1, config.hidden_size); - auto s_down_zp_mem = create_zp_tensor(s_down_zp_packed, 1, group_num2, 1, config.hidden_size); + auto s_down_scale_mem = make_scale(s_down_scale, 1, config.hidden_size, group_num2); + auto s_down_zp_mem = make_zp(s_down_zp_packed, 1, config.hidden_size, group_num2); auto s_gate_scalar_mem = create_scalar_gate_tensor(s_gate_scalar_data); @@ -734,16 +799,6 @@ TEST_P(moe_3gemm_compressed_gpu_shared_random, moe_accuracy_test_shared_expert_r topology.add(data("w2_zp", w2_zp_mem)); // Add shared inputs - // Insert dummy routing_bias/eps placeholders at indices 11-12 (SOFTMAX + shared expert) - auto dummy_bias_mem = engine.allocate_memory({data_types::f16, format::bfyx, {1, 1, 1, 1}}); - set_values(dummy_bias_mem, {ov::float16(0.0f)}); - get_test_stream().finish(); - auto dummy_eps_mem = engine.allocate_memory({data_types::f16, format::bfyx, {1, 1, 1, 1}}); - set_values(dummy_eps_mem, {ov::float16(0.0f)}); - get_test_stream().finish(); - topology.add(data("dummy_routing_bias", dummy_bias_mem)); - topology.add(data("dummy_routing_eps", dummy_eps_mem)); - topology.add(data("s_gate_weight", s_gate_weight_mem)); topology.add(data("s_gate_scale", s_gate_scale_mem)); topology.add(data("s_gate_zp", s_gate_zp_mem)); @@ -755,7 +810,14 @@ TEST_P(moe_3gemm_compressed_gpu_shared_random, moe_accuracy_test_shared_expert_r topology.add(data("s_down_zp", s_down_zp_mem)); topology.add(data("s_gate_scalar", s_gate_scalar_mem)); - cldnn::MOE3GemmFusedCompressed::Config moe_config; + // Create MoERouterFused primitive (softmax for shared expert tests) + MoERouterFused::Config router_config; + router_config.num_expert = config.num_experts; + router_config.top_k = config.top_k; + router_config.routing_type = MoERouterFused::RoutingType::SOFTMAX; + topology.add(moe_router_fused("router", {input_info("routing_weights")}, router_config)); + + cldnn::MOECompressed::Config moe_config; moe_config.hidden_size = config.hidden_size; moe_config.inter_size = config.inter_size; moe_config.num_expert = config.num_experts; @@ -764,40 +826,37 @@ TEST_P(moe_3gemm_compressed_gpu_shared_random, moe_accuracy_test_shared_expert_r moe_config.out_type = data_types::f16; moe_config.num_shared_expert = 1; moe_config.has_zp = true; - // has_batch_dim default is 0. - - // Create Primitive with extended inputs - auto moe_prim = moe_3gemm_fused_compressed("moe_3gemm_fused_compressed", - {input_info("hidden_states"), - input_info("routing_weights"), - input_info("w0_weight"), - input_info("w0_scale"), - input_info("w0_zp"), - input_info("w1_weight"), - input_info("w1_scale"), - input_info("w1_zp"), - input_info("w2_weight"), - input_info("w2_scale"), - input_info("w2_zp"), - // Dummy placeholders for routing_bias/eps (indices 11-12) - input_info("dummy_routing_bias"), - input_info("dummy_routing_eps"), - // Shared Expert Inputs (indices 13-22) - input_info("s_gate_weight"), - input_info("s_gate_scale"), - input_info("s_gate_zp"), - input_info("s_up_weight"), - input_info("s_up_scale"), - input_info("s_up_zp"), - input_info("s_down_weight"), - input_info("s_down_scale"), - input_info("s_down_zp"), - input_info("s_gate_scalar")}, - moe_config); - - topology.add(moe_prim); - - network network(engine, topology, get_test_default_config(engine)); + + // Create Primitive with new input layout + topology.add(moe_3gemm_fused_compressed("moe_3gemm_fused_compressed", + {input_info("hidden_states"), + input_info("router", 0), // topk_weights + input_info("router", 1), // topk_indices + input_info("w0_weight"), + input_info("w0_scale"), + input_info("w0_zp"), + input_info("w1_weight"), + input_info("w1_scale"), + input_info("w1_zp"), + input_info("w2_weight"), + input_info("w2_scale"), + input_info("w2_zp"), + // Shared Expert Inputs (indices 12-21) + input_info("s_gate_weight"), + input_info("s_gate_scale"), + input_info("s_gate_zp"), + input_info("s_up_weight"), + input_info("s_up_scale"), + input_info("s_up_zp"), + input_info("s_down_weight"), + input_info("s_down_scale"), + input_info("s_down_zp"), + input_info("s_gate_scalar")}, + moe_config)); + + auto net_config = get_test_default_config(engine); + net_config.set_property(ov::intel_gpu::allow_new_shape_infer(true)); + network network(engine, topology, net_config); network.set_input_data("hidden_states", hidden_states_mem); network.set_input_data("routing_weights", routing_weights_mem); @@ -825,7 +884,29 @@ INSTANTIATE_TEST_SUITE_P(smoke, Moe3GemmTestParams{1, true, 256, 512, 4, 2, 256}, Moe3GemmTestParams{1, false, 256, 512, 4, 2, 256}, Moe3GemmTestParams{1, true, 512, 512, 4, 2, 512}, - Moe3GemmTestParams{1, false, 512, 512, 4, 2, 512})); + Moe3GemmTestParams{1, false, 512, 512, 4, 2, 512}, + // Sub-128 group_size (trinity-mini afmoe shape). + Moe3GemmTestParams{1, true, 128, 256, 4, 2, 64}, + Moe3GemmTestParams{1, false, 128, 256, 4, 2, 64}, + Moe3GemmTestParams{1, true, 256, 512, 4, 2, 64})); + +// Batched GEMV with shared expert: MTP/speculative decoding scenarios with shared expert enabled. +// Exercises the batched GEMV path with EXPERTS_PER_TOKEN = MAX_TOPK + 1. +INSTANTIATE_TEST_SUITE_P(smoke_batched_gemv_mtp_shared, + moe_3gemm_compressed_gpu_shared_random, + ::testing::Values(Moe3GemmTestParams{2, true, 128, 256, 4, 2, 128}, + Moe3GemmTestParams{4, true, 128, 256, 4, 2, 128}, + Moe3GemmTestParams{8, true, 128, 256, 4, 2, 128}, + Moe3GemmTestParams{2, false, 128, 256, 4, 2, 128}, + Moe3GemmTestParams{4, false, 128, 256, 4, 2, 128}, + Moe3GemmTestParams{8, false, 128, 256, 4, 2, 128}, + Moe3GemmTestParams{2, true, 256, 512, 4, 2, 256}, + Moe3GemmTestParams{4, true, 256, 512, 4, 2, 256}, + Moe3GemmTestParams{2, false, 256, 512, 4, 2, 256}, + Moe3GemmTestParams{4, false, 256, 512, 4, 2, 256}, + // Sub-128 group_size batched GEMV coverage. + Moe3GemmTestParams{2, true, 128, 256, 4, 2, 64}, + Moe3GemmTestParams{2, false, 128, 256, 4, 2, 64})); TEST_P(moe_3gemm_compressed_gpu_u4, moe_accuracy_test_u4) { auto routing_type = GetParam(); @@ -915,18 +996,35 @@ TEST_P(moe_3gemm_compressed_gpu_u4, moe_accuracy_test_u4) { topology.add(data("w2_zp", w2_zp)); // Create MOE3GemmFusedCompressed config - cldnn::MOE3GemmFusedCompressed::Config config; + cldnn::MOECompressed::Config config; config.hidden_size = hidden_size; config.inter_size = inter_size; config.num_expert = num_experts; config.top_k = top_k; config.group_size = group_size; config.out_type = data_types::f16; - config.routing_type = routing_type; config.has_zp = true; + // Create MoERouterFused primitive + MoERouterFused::Config router_config; + router_config.num_expert = num_experts; + router_config.top_k = top_k; + std::vector router_inputs{input_info("routing_weights")}; + if (routing_type == cldnn::MoERouterFused::RoutingType::SIGMOID_BIAS) { + router_config.routing_type = MoERouterFused::RoutingType::SIGMOID_BIAS; + topology.add(data("routing_bias", routing_bias)); + auto routing_eps_mem = engine.allocate_memory({data_types::f16, format::bfyx, {1, 1, 1, 1}}); + set_values(routing_eps_mem, {ov::float16(1e-6f)}); + get_test_stream().finish(); + topology.add(data("routing_eps", routing_eps_mem)); + router_inputs.push_back(input_info("routing_bias")); + router_inputs.push_back(input_info("routing_eps")); + } + topology.add(moe_router_fused("router", router_inputs, router_config)); + std::vector moe_inputs{input_info("hidden_states"), - input_info("routing_weights"), + input_info("router", 0), // topk_weights + input_info("router", 1), // topk_indices input_info("w0_weight"), input_info("w0_scale"), input_info("w0_zp"), @@ -936,21 +1034,14 @@ TEST_P(moe_3gemm_compressed_gpu_u4, moe_accuracy_test_u4) { input_info("w2_weight"), input_info("w2_scale"), input_info("w2_zp")}; - if (routing_type == cldnn::MOE3GemmFusedCompressed::RoutingType::SIGMOID_BIAS) { - topology.add(data("routing_bias", routing_bias)); - moe_inputs.push_back(input_info("routing_bias")); - auto routing_eps_mem = engine.allocate_memory({data_types::f16, format::bfyx, {1, 1, 1, 1}}); - set_values(routing_eps_mem, {ov::float16(1e-6f)}); - get_test_stream().finish(); - topology.add(data("routing_eps", routing_eps_mem)); - moe_inputs.push_back(input_info("routing_eps")); - } // Create MOECompressed primitive topology.add(moe_3gemm_fused_compressed("moe_3gemm_fused_compressed", moe_inputs, config)); // Create and execute network - network network(engine, topology, get_test_default_config(engine)); + auto net_config = get_test_default_config(engine); + net_config.set_property(ov::intel_gpu::allow_new_shape_infer(true)); + network network(engine, topology, net_config); network.set_input_data("hidden_states", hidden_states); network.set_input_data("routing_weights", routing_weights); @@ -967,7 +1058,7 @@ TEST_P(moe_3gemm_compressed_gpu_u4, moe_accuracy_test_u4) { EXPECT_EQ(output_layout.batch(), batch_size); EXPECT_EQ(output_layout.feature(), seq_len); - const auto& output_reference = routing_type == cldnn::MOE3GemmFusedCompressed::RoutingType::SIGMOID_BIAS ? output_ref_sigmoid_bias : output_ref_softmax; + const auto& output_reference = routing_type == cldnn::MoERouterFused::RoutingType::SIGMOID_BIAS ? output_ref_sigmoid_bias : output_ref_softmax; for (size_t i = 0; i < batch_size * seq_len * hidden_size; ++i) { EXPECT_NEAR(static_cast(output_ptr[i]), static_cast(output_reference[i]), 1e-3f); } @@ -975,10 +1066,10 @@ TEST_P(moe_3gemm_compressed_gpu_u4, moe_accuracy_test_u4) { INSTANTIATE_TEST_SUITE_P(smoke, moe_3gemm_compressed_gpu_u4, - ::testing::Values(cldnn::MOE3GemmFusedCompressed::RoutingType::SOFTMAX, cldnn::MOE3GemmFusedCompressed::RoutingType::SIGMOID_BIAS)); + ::testing::Values(cldnn::MoERouterFused::RoutingType::SOFTMAX, cldnn::MoERouterFused::RoutingType::SIGMOID_BIAS)); // Symmetric quantization tests (i4/i8 weights, no zero points) -class moe_3gemm_compressed_gpu_symmetric_random : public ::testing::TestWithParam> { +class moe_3gemm_compressed_gpu_symmetric_random : public ::testing::TestWithParam> { }; TEST_P(moe_3gemm_compressed_gpu_symmetric_random, moe_accuracy_test_symmetric) { @@ -1026,7 +1117,6 @@ TEST_P(moe_3gemm_compressed_gpu_symmetric_random, moe_accuracy_test_symmetric) { // Create tensors auto weight_dt = config.is_u4 ? data_types::i4 : data_types::i8; - auto zp_dt = config.is_u4 ? data_types::u4 : data_types::u8; auto create_weight_tensor = [&](const std::vector& values, int64_t b, int64_t f, int64_t y, int64_t x) { auto mem = engine.allocate_memory({weight_dt, format::bfyx, {b, f, y, x}}); @@ -1035,11 +1125,17 @@ TEST_P(moe_3gemm_compressed_gpu_symmetric_random, moe_accuracy_test_symmetric) { return mem; }; - auto create_zp_tensor = [&](int64_t b, int64_t f, int64_t y, int64_t x) { - auto mem = engine.allocate_memory({zp_dt, format::bfyx, {b, f, y, x}}); - // Zero-fill (symmetric has no ZP) - auto lock = cldnn::mem_lock(mem, get_test_stream()); - std::memset(lock.data(), 0, lock.size()); + // Symmetric: ZP is element::dynamic placeholder (matches real model where the transformation + // creates Constant(element::dynamic, Shape{0}) for absent ZP in symmetric quantization). + auto create_dynamic_zp_placeholder = [&]() { + auto one_byte_mem = engine.allocate_memory(cldnn::layout(ov::PartialShape{1}, data_types::u8, format::bfyx), false); + return engine.reinterpret_buffer(*one_byte_mem, cldnn::layout(ov::PartialShape{0}, data_types::dynamic, format::bfyx)); + }; + auto make_scale_sym = [&](const std::vector& v, int64_t E, int64_t ofm, int64_t G) { + auto fmt = G > 1 ? format::byfx : format::bfyx; + auto shape = G > 1 ? ov::PartialShape{E, ofm, G, 1} : ov::PartialShape{E, ofm, 1}; + auto mem = engine.allocate_memory(layout{shape, data_types::f16, fmt}); + set_values(mem, v); get_test_stream().finish(); return mem; }; @@ -1068,16 +1164,16 @@ TEST_P(moe_3gemm_compressed_gpu_symmetric_random, moe_accuracy_test_symmetric) { size_t group_num2 = config.inter_size / config.group_size; auto w0_weight_mem = create_weight_tensor(w0_q_packed, config.num_experts, config.inter_size, config.group_size, group_num); - auto w0_scale_mem = create_f16_tensor(w0_scale, config.num_experts, group_num, 1, config.inter_size); - auto w0_zp_mem = create_zp_tensor(config.num_experts, group_num, 1, config.inter_size); + auto w0_scale_mem = make_scale_sym(w0_scale, config.num_experts, config.inter_size, group_num); + auto w0_zp_mem = create_dynamic_zp_placeholder(); auto w1_weight_mem = create_weight_tensor(w1_q_packed, config.num_experts, config.inter_size, config.group_size, group_num); - auto w1_scale_mem = create_f16_tensor(w1_scale, config.num_experts, group_num, 1, config.inter_size); - auto w1_zp_mem = create_zp_tensor(config.num_experts, group_num, 1, config.inter_size); + auto w1_scale_mem = make_scale_sym(w1_scale, config.num_experts, config.inter_size, group_num); + auto w1_zp_mem = create_dynamic_zp_placeholder(); auto w2_weight_mem = create_weight_tensor(w2_q_packed, config.num_experts, config.hidden_size, config.group_size, group_num2); - auto w2_scale_mem = create_f16_tensor(w2_scale, config.num_experts, group_num2, 1, config.hidden_size); - auto w2_zp_mem = create_zp_tensor(config.num_experts, group_num2, 1, config.hidden_size); + auto w2_scale_mem = make_scale_sym(w2_scale, config.num_experts, config.hidden_size, group_num2); + auto w2_zp_mem = create_dynamic_zp_placeholder(); // Build topology topology topology; @@ -1093,18 +1189,35 @@ TEST_P(moe_3gemm_compressed_gpu_symmetric_random, moe_accuracy_test_symmetric) { topology.add(data("w2_scale", w2_scale_mem)); topology.add(data("w2_zp", w2_zp_mem)); - cldnn::MOE3GemmFusedCompressed::Config moe_config; + // Create MoERouterFused primitive + MoERouterFused::Config router_config; + router_config.num_expert = config.num_experts; + router_config.top_k = config.top_k; + std::vector router_inputs{input_info("routing_weights")}; + if (routing_type == cldnn::MoERouterFused::RoutingType::SIGMOID_BIAS) { + router_config.routing_type = MoERouterFused::RoutingType::SIGMOID_BIAS; + topology.add(data("routing_bias", routing_bias_mem)); + auto routing_eps_mem = engine.allocate_memory({data_types::f16, format::bfyx, {1, 1, 1, 1}}); + set_values(routing_eps_mem, {routing_eps_val}); + get_test_stream().finish(); + topology.add(data("routing_eps", routing_eps_mem)); + router_inputs.push_back(input_info("routing_bias")); + router_inputs.push_back(input_info("routing_eps")); + } + topology.add(moe_router_fused("router", router_inputs, router_config)); + + cldnn::MOECompressed::Config moe_config; moe_config.hidden_size = config.hidden_size; moe_config.inter_size = config.inter_size; moe_config.num_expert = config.num_experts; moe_config.top_k = config.top_k; moe_config.group_size = config.group_size; moe_config.out_type = data_types::f16; - moe_config.routing_type = routing_type; moe_config.has_zp = false; std::vector moe_inputs{input_info("hidden_states"), - input_info("routing_weights"), + input_info("router", 0), // topk_weights + input_info("router", 1), // topk_indices input_info("w0_weight"), input_info("w0_scale"), input_info("w0_zp"), @@ -1114,20 +1227,12 @@ TEST_P(moe_3gemm_compressed_gpu_symmetric_random, moe_accuracy_test_symmetric) { input_info("w2_weight"), input_info("w2_scale"), input_info("w2_zp")}; - if (routing_type == cldnn::MOE3GemmFusedCompressed::RoutingType::SIGMOID_BIAS) { - topology.add(data("routing_bias", routing_bias_mem)); - moe_inputs.push_back(input_info("routing_bias")); - auto routing_eps_mem = engine.allocate_memory({data_types::f16, format::bfyx, {1, 1, 1, 1}}); - set_values(routing_eps_mem, {routing_eps_val}); - get_test_stream().finish(); - topology.add(data("routing_eps", routing_eps_mem)); - moe_inputs.push_back(input_info("routing_eps")); - } - auto moe_prim = moe_3gemm_fused_compressed("moe_3gemm_fused_compressed", moe_inputs, moe_config); - topology.add(moe_prim); + topology.add(moe_3gemm_fused_compressed("moe_3gemm_fused_compressed", moe_inputs, moe_config)); - network network(engine, topology, get_test_default_config(engine)); + auto net_config = get_test_default_config(engine); + net_config.set_property(ov::intel_gpu::allow_new_shape_infer(true)); + network network(engine, topology, net_config); network.set_input_data("hidden_states", hidden_states_mem); network.set_input_data("routing_weights", routing_weights_mem); @@ -1136,12 +1241,12 @@ TEST_P(moe_3gemm_compressed_gpu_symmetric_random, moe_accuracy_test_symmetric) { get_test_stream().flush(); cldnn::mem_lock output_ptr(output_prim, get_test_stream()); - auto ref_output = routing_type == cldnn::MOE3GemmFusedCompressed::RoutingType::SIGMOID_BIAS + auto ref_output = routing_type == cldnn::MoERouterFused::RoutingType::SIGMOID_BIAS ? ref.run_reference_sigmoid(hidden_states, routing_weights, routing_bias_data, routing_eps_val, w0_data, w1_data, w2_data) : ref.run_reference_softmax(hidden_states, routing_weights, w0_data, w1_data, w2_data); - const float base_tolerance = routing_type == cldnn::MOE3GemmFusedCompressed::RoutingType::SIGMOID_BIAS ? 0.2f : 0.1f; - const float tolerance = base_tolerance * (config.hidden_size / 128); + const float base_tolerance = routing_type == cldnn::MoERouterFused::RoutingType::SIGMOID_BIAS ? 0.25f : 0.15f; + const float tolerance = base_tolerance * std::max(1, static_cast(config.hidden_size / 128)); for (size_t i = 0; i < ref_output.size(); ++i) { ASSERT_NEAR(static_cast(output_ptr[i]), static_cast(ref_output[i]), tolerance); } @@ -1149,8 +1254,8 @@ TEST_P(moe_3gemm_compressed_gpu_symmetric_random, moe_accuracy_test_symmetric) { INSTANTIATE_TEST_SUITE_P(smoke, moe_3gemm_compressed_gpu_symmetric_random, - ::testing::Combine(::testing::Values(cldnn::MOE3GemmFusedCompressed::RoutingType::SOFTMAX, - cldnn::MOE3GemmFusedCompressed::RoutingType::SIGMOID_BIAS), + ::testing::Combine(::testing::Values(cldnn::MoERouterFused::RoutingType::SOFTMAX, + cldnn::MoERouterFused::RoutingType::SIGMOID_BIAS), ::testing::Values(Moe3GemmTestParams{1, true, 128, 256, 4, 2, 128, true}, Moe3GemmTestParams{16, true, 128, 256, 4, 2, 128, true}, Moe3GemmTestParams{1, false, 128, 256, 4, 2, 128, true}, @@ -1158,7 +1263,10 @@ INSTANTIATE_TEST_SUITE_P(smoke, Moe3GemmTestParams{1, true, 256, 512, 4, 2, 256, true}, Moe3GemmTestParams{1, false, 256, 512, 4, 2, 256, true}, Moe3GemmTestParams{1, true, 512, 512, 4, 2, 512, true}, - Moe3GemmTestParams{1, false, 512, 512, 4, 2, 512, true}))); + Moe3GemmTestParams{1, false, 512, 512, 4, 2, 512, true}, + // Sub-128 group_size for symmetric quantization. + Moe3GemmTestParams{1, true, 128, 256, 4, 2, 64, true}, + Moe3GemmTestParams{1, false, 128, 256, 4, 2, 64, true}))); // Symmetric quantization test with shared expert class moe_3gemm_compressed_gpu_shared_symmetric_random : public ::testing::TestWithParam {}; @@ -1228,7 +1336,6 @@ TEST_P(moe_3gemm_compressed_gpu_shared_symmetric_random, moe_accuracy_test_share // Create tensors with signed weight types auto weight_dt = config.is_u4 ? data_types::i4 : data_types::i8; - auto zp_dt = config.is_u4 ? data_types::u4 : data_types::u8; auto create_weight_tensor = [&](const std::vector& values, int64_t b, int64_t f, int64_t y, int64_t x) { auto mem = engine.allocate_memory({weight_dt, format::bfyx, {b, f, y, x}}); @@ -1237,10 +1344,16 @@ TEST_P(moe_3gemm_compressed_gpu_shared_symmetric_random, moe_accuracy_test_share return mem; }; - auto create_zp_tensor = [&](int64_t b, int64_t f, int64_t y, int64_t x) { - auto mem = engine.allocate_memory({zp_dt, format::bfyx, {b, f, y, x}}); - auto lock = cldnn::mem_lock(mem, get_test_stream()); - std::memset(lock.data(), 0, lock.size()); + // Symmetric: ZP is element::dynamic placeholder (matches real model). + auto create_dynamic_zp_placeholder = [&]() { + auto one_byte_mem = engine.allocate_memory(cldnn::layout(ov::PartialShape{1}, data_types::u8, format::bfyx), false); + return engine.reinterpret_buffer(*one_byte_mem, cldnn::layout(ov::PartialShape{0}, data_types::dynamic, format::bfyx)); + }; + auto make_scale_sym = [&](const std::vector& v, int64_t E, int64_t ofm, int64_t G) { + auto fmt = G > 1 ? format::byfx : format::bfyx; + auto shape = G > 1 ? ov::PartialShape{E, ofm, G, 1} : ov::PartialShape{E, ofm, 1}; + auto mem = engine.allocate_memory(layout{shape, data_types::f16, fmt}); + set_values(mem, v); get_test_stream().finish(); return mem; }; @@ -1270,29 +1383,29 @@ TEST_P(moe_3gemm_compressed_gpu_shared_symmetric_random, moe_accuracy_test_share // Expert Weights auto w0_weight_mem = create_weight_tensor(w0_q_packed, config.num_experts, config.inter_size, config.group_size, group_num); - auto w0_scale_mem = create_f16_tensor(w0_scale, config.num_experts, group_num, 1, config.inter_size); - auto w0_zp_mem = create_zp_tensor(config.num_experts, group_num, 1, config.inter_size); + auto w0_scale_mem = make_scale_sym(w0_scale, config.num_experts, config.inter_size, group_num); + auto w0_zp_mem = create_dynamic_zp_placeholder(); auto w1_weight_mem = create_weight_tensor(w1_q_packed, config.num_experts, config.inter_size, config.group_size, group_num); - auto w1_scale_mem = create_f16_tensor(w1_scale, config.num_experts, group_num, 1, config.inter_size); - auto w1_zp_mem = create_zp_tensor(config.num_experts, group_num, 1, config.inter_size); + auto w1_scale_mem = make_scale_sym(w1_scale, config.num_experts, config.inter_size, group_num); + auto w1_zp_mem = create_dynamic_zp_placeholder(); auto w2_weight_mem = create_weight_tensor(w2_q_packed, config.num_experts, config.hidden_size, config.group_size, group_num2); - auto w2_scale_mem = create_f16_tensor(w2_scale, config.num_experts, group_num2, 1, config.hidden_size); - auto w2_zp_mem = create_zp_tensor(config.num_experts, group_num2, 1, config.hidden_size); + auto w2_scale_mem = make_scale_sym(w2_scale, config.num_experts, config.hidden_size, group_num2); + auto w2_zp_mem = create_dynamic_zp_placeholder(); // Shared Expert Weights auto s_gate_weight_mem = create_weight_tensor(s_gate_q_packed, 1, config.inter_size, config.group_size, group_num); - auto s_gate_scale_mem = create_f16_tensor(s_gate_scale, 1, group_num, 1, config.inter_size); - auto s_gate_zp_mem = create_zp_tensor(1, group_num, 1, config.inter_size); + auto s_gate_scale_mem = make_scale_sym(s_gate_scale, 1, config.inter_size, group_num); + auto s_gate_zp_mem = create_dynamic_zp_placeholder(); auto s_up_weight_mem = create_weight_tensor(s_up_q_packed, 1, config.inter_size, config.group_size, group_num); - auto s_up_scale_mem = create_f16_tensor(s_up_scale, 1, group_num, 1, config.inter_size); - auto s_up_zp_mem = create_zp_tensor(1, group_num, 1, config.inter_size); + auto s_up_scale_mem = make_scale_sym(s_up_scale, 1, config.inter_size, group_num); + auto s_up_zp_mem = create_dynamic_zp_placeholder(); auto s_down_weight_mem = create_weight_tensor(s_down_q_packed, 1, config.hidden_size, config.group_size, group_num2); - auto s_down_scale_mem = create_f16_tensor(s_down_scale, 1, group_num2, 1, config.hidden_size); - auto s_down_zp_mem = create_zp_tensor(1, group_num2, 1, config.hidden_size); + auto s_down_scale_mem = make_scale_sym(s_down_scale, 1, config.hidden_size, group_num2); + auto s_down_zp_mem = create_dynamic_zp_placeholder(); auto s_gate_scalar_mem = create_scalar_gate_tensor(s_gate_scalar_data); @@ -1311,15 +1424,6 @@ TEST_P(moe_3gemm_compressed_gpu_shared_symmetric_random, moe_accuracy_test_share topology.add(data("w2_scale", w2_scale_mem)); topology.add(data("w2_zp", w2_zp_mem)); - auto dummy_bias_mem = engine.allocate_memory({data_types::f16, format::bfyx, {1, 1, 1, 1}}); - set_values(dummy_bias_mem, {ov::float16(0.0f)}); - get_test_stream().finish(); - auto dummy_eps_mem = engine.allocate_memory({data_types::f16, format::bfyx, {1, 1, 1, 1}}); - set_values(dummy_eps_mem, {ov::float16(0.0f)}); - get_test_stream().finish(); - topology.add(data("dummy_routing_bias", dummy_bias_mem)); - topology.add(data("dummy_routing_eps", dummy_eps_mem)); - topology.add(data("s_gate_weight", s_gate_weight_mem)); topology.add(data("s_gate_scale", s_gate_scale_mem)); topology.add(data("s_gate_zp", s_gate_zp_mem)); @@ -1331,7 +1435,14 @@ TEST_P(moe_3gemm_compressed_gpu_shared_symmetric_random, moe_accuracy_test_share topology.add(data("s_down_zp", s_down_zp_mem)); topology.add(data("s_gate_scalar", s_gate_scalar_mem)); - cldnn::MOE3GemmFusedCompressed::Config moe_config; + // Create MoERouterFused primitive (softmax for shared expert tests) + MoERouterFused::Config router_config; + router_config.num_expert = config.num_experts; + router_config.top_k = config.top_k; + router_config.routing_type = MoERouterFused::RoutingType::SOFTMAX; + topology.add(moe_router_fused("router", {input_info("routing_weights")}, router_config)); + + cldnn::MOECompressed::Config moe_config; moe_config.hidden_size = config.hidden_size; moe_config.inter_size = config.inter_size; moe_config.num_expert = config.num_experts; @@ -1341,20 +1452,35 @@ TEST_P(moe_3gemm_compressed_gpu_shared_symmetric_random, moe_accuracy_test_share moe_config.num_shared_expert = 1; moe_config.has_zp = false; - auto moe_prim = moe_3gemm_fused_compressed("moe_3gemm_fused_compressed", - {input_info("hidden_states"), input_info("routing_weights"), input_info("w0_weight"), - input_info("w0_scale"), input_info("w0_zp"), input_info("w1_weight"), - input_info("w1_scale"), input_info("w1_zp"), input_info("w2_weight"), - input_info("w2_scale"), input_info("w2_zp"), input_info("dummy_routing_bias"), - input_info("dummy_routing_eps"), input_info("s_gate_weight"), input_info("s_gate_scale"), - input_info("s_gate_zp"), input_info("s_up_weight"), input_info("s_up_scale"), - input_info("s_up_zp"), input_info("s_down_weight"), input_info("s_down_scale"), - input_info("s_down_zp"), input_info("s_gate_scalar")}, - moe_config); - - topology.add(moe_prim); - - network network(engine, topology, get_test_default_config(engine)); + topology.add(moe_3gemm_fused_compressed("moe_3gemm_fused_compressed", + {input_info("hidden_states"), + input_info("router", 0), // topk_weights + input_info("router", 1), // topk_indices + input_info("w0_weight"), + input_info("w0_scale"), + input_info("w0_zp"), + input_info("w1_weight"), + input_info("w1_scale"), + input_info("w1_zp"), + input_info("w2_weight"), + input_info("w2_scale"), + input_info("w2_zp"), + // Shared Expert Inputs (indices 12-21) + input_info("s_gate_weight"), + input_info("s_gate_scale"), + input_info("s_gate_zp"), + input_info("s_up_weight"), + input_info("s_up_scale"), + input_info("s_up_zp"), + input_info("s_down_weight"), + input_info("s_down_scale"), + input_info("s_down_zp"), + input_info("s_gate_scalar")}, + moe_config)); + + auto net_config = get_test_default_config(engine); + net_config.set_property(ov::intel_gpu::allow_new_shape_infer(true)); + network network(engine, topology, net_config); network.set_input_data("hidden_states", hidden_states_mem); network.set_input_data("routing_weights", routing_weights_mem); @@ -1381,4 +1507,8 @@ INSTANTIATE_TEST_SUITE_P(smoke, Moe3GemmTestParams{1, true, 256, 512, 4, 2, 256, true}, Moe3GemmTestParams{1, false, 256, 512, 4, 2, 256, true}, Moe3GemmTestParams{1, true, 512, 512, 4, 2, 512, true}, - Moe3GemmTestParams{1, false, 512, 512, 4, 2, 512, true})); \ No newline at end of file + Moe3GemmTestParams{1, false, 512, 512, 4, 2, 512, true}, + // Sub-128 group_size for symmetric shared expert. + Moe3GemmTestParams{1, true, 128, 256, 4, 2, 64, true}, + Moe3GemmTestParams{1, false, 128, 256, 4, 2, 64, true}, + Moe3GemmTestParams{1, true, 256, 512, 4, 2, 64, true})); diff --git a/src/plugins/intel_gpu/tests/unit/test_cases/moe_gather_gpu_test.cpp b/src/plugins/intel_gpu/tests/unit/test_cases/moe_gather_gpu_test.cpp index f8e9dc822359..e9eaa32350a8 100644 --- a/src/plugins/intel_gpu/tests/unit/test_cases/moe_gather_gpu_test.cpp +++ b/src/plugins/intel_gpu/tests/unit/test_cases/moe_gather_gpu_test.cpp @@ -1,5 +1,7 @@ // Copyright (C) 2018-2026 Intel Corporation // SPDX-License-Identifier: Apache-2.0 +// + #include #include #include "test_utils.h" @@ -9,7 +11,7 @@ #include #include #include -#include "intel_gpu/op/moe_compressed.hpp" +#include "ov_ops/moe_compressed.hpp" using namespace cldnn; using namespace ov::intel_gpu; @@ -33,7 +35,7 @@ void test_moe_gather(bool is_caching_test, int k) { size_t hidden_size = k; int32_t num_experts_per_token = 2; - ov::intel_gpu::op::MOECompressed::Config moe_config; + ov::op::internal::MOECompressed::Config moe_config; moe_config.top_k = 2; moe_config.hidden_size = k; moe_config.num_expert = num_total_experts; @@ -139,4 +141,4 @@ TEST(moe_unit, moe_gather_test_multi_batch_unaligned) { TEST(moe_unit, moe_gather_test_cached) { test_moe_gather(true, 64); -} \ No newline at end of file +} diff --git a/src/plugins/intel_gpu/tests/unit/test_cases/moe_gemm_gpu_test.cpp b/src/plugins/intel_gpu/tests/unit/test_cases/moe_gemm_gpu_test.cpp index 8beacc5f5d44..242dd39663c5 100644 --- a/src/plugins/intel_gpu/tests/unit/test_cases/moe_gemm_gpu_test.cpp +++ b/src/plugins/intel_gpu/tests/unit/test_cases/moe_gemm_gpu_test.cpp @@ -11,7 +11,7 @@ #include #include #include -#include "intel_gpu/op/moe_compressed.hpp" +#include "ov_ops/moe_compressed.hpp" using namespace cldnn; using namespace ov::intel_gpu; @@ -52,7 +52,6 @@ void get_reference(const std::vector& input, bool is_weight_symmetric_quant, cldnn::data_types weight_dt, const std::vector& bias) { - std::cout << "get_reference" << std::endl; size_t elements_per_byte = (ov::element::Type(weight_dt).bitwidth() == 4) ? 2 : 1; auto ld_w = K / elements_per_byte; auto ld_in = K; @@ -94,7 +93,6 @@ void get_reference(const std::vector& input, bool is_weight_symmetric_quant, cldnn::data_types weight_dt, const std::vector& bias) { - std::cout << "get_reference" << std::endl; size_t elements_per_byte = (ov::element::Type(weight_dt).bitwidth() == 4) ? 2 : 1; auto ld_w = K / elements_per_byte; auto ld_in = K; @@ -274,7 +272,7 @@ struct MoEGemmTest : public ::testing::TestWithParam { void create_weight_data_and_topology(T& p, topology& topo, std::vector& experts_data_f16, std::vector& experts_data_quant, std::vector& scales_data, std::vector& zp_data_f16, std::vector& zp_data, bool is_weight_compressed) { - ov::intel_gpu::op::MOECompressed::Config moe_config; + ov::op::internal::MOECompressed::Config moe_config; moe_config.top_k = p.num_experts_per_token; moe_config.num_expert = p.num_total_experts; moe_config.has_batch_dim = !p.is_pa; @@ -337,9 +335,11 @@ struct MoEGemmTest : public ::testing::TestWithParam { } quantize_4bit(experts_data_f16, experts_data_quant, p.weight_dt, p.num_total_experts, p.out_N, p.hidden_size, p.scale_group_size, p.weight_symmetric_quant, scales_data, zp_data_f16, zp_data); set_values(experts_mem, experts_data_quant); - auto scale_shape = num_scale_groups > 1 ? ov::PartialShape{ov::Dimension(p.num_total_experts), ov::Dimension(num_scale_groups), ov::Dimension(p.out_N), ov::Dimension(1)} : + // Logical [E, N, G]; physical E×G×N from transpose_weight_scales matches byfx. + auto scale_shape = num_scale_groups > 1 ? ov::PartialShape{ov::Dimension(p.num_total_experts), ov::Dimension(p.out_N), ov::Dimension(num_scale_groups), ov::Dimension(1)} : ov::PartialShape{ov::Dimension(p.num_total_experts), ov::Dimension(p.out_N), ov::Dimension(1)}; - auto scale_layout = layout{scale_shape, data_types::f16, format::bfyx}; + auto scale_format = num_scale_groups > 1 ? format::byfx : format::bfyx; + auto scale_layout = layout{scale_shape, data_types::f16, scale_format}; auto scale_mem = engine.allocate_memory(scale_layout); std::vector scales_data_transposed(scales_data.size()); transpose_weight_scales(scales_data, scales_data_transposed, p.num_total_experts, num_scale_groups, p.out_N); @@ -350,9 +350,11 @@ struct MoEGemmTest : public ::testing::TestWithParam { topo.add(moe_experts_prim); topo.add(moe_experts_scale_prim); if (!p.weight_symmetric_quant) { - auto zp_shape = num_scale_groups > 1 ? ov::PartialShape{ov::Dimension(p.num_total_experts), ov::Dimension(num_scale_groups), ov::Dimension(p.out_N), ov::Dimension(1)} : + // Match scale layout convention: [E, N, G, 1] byfx for grouped quantization. + auto zp_shape = num_scale_groups > 1 ? ov::PartialShape{ov::Dimension(p.num_total_experts), ov::Dimension(p.out_N), ov::Dimension(num_scale_groups), ov::Dimension(1)} : ov::PartialShape{ov::Dimension(p.num_total_experts), ov::Dimension(p.out_N), ov::Dimension(1)}; - auto zp_layout = layout{zp_shape, p.weight_dt, format::bfyx}; + auto zp_format = num_scale_groups > 1 ? format::byfx : format::bfyx; + auto zp_layout = layout{zp_shape, p.weight_dt, zp_format}; auto zp_mem = engine.allocate_memory(zp_layout); set_values(zp_mem, zp_data); auto moe_experts_zp_prim = data("moe_experts_zp", zp_mem); @@ -517,7 +519,6 @@ struct MoEGemmTest : public ::testing::TestWithParam { for (size_t i = 0; i < M * p.out_N; i++) { auto tolerance = std::max(std::abs(output_ref[i] * 0.01f), 0.1f); - // std::cout << "[" << i << "] " << output_ref[i] << " : " << output_ptr[i] << std::endl; ASSERT_NEAR(output_ptr[i], output_ref[i], tolerance); } } diff --git a/src/plugins/intel_gpu/tests/unit/test_cases/moe_router_fused_gpu_test.cpp b/src/plugins/intel_gpu/tests/unit/test_cases/moe_router_fused_gpu_test.cpp new file mode 100644 index 000000000000..0fd334c036f0 --- /dev/null +++ b/src/plugins/intel_gpu/tests/unit/test_cases/moe_router_fused_gpu_test.cpp @@ -0,0 +1,248 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../common/random_generator.hpp" +#include "../test_utils/test_utils.h" + +using namespace cldnn; +using namespace ::tests; + +struct MoeRouterTestParams { + size_t num_tokens; + size_t num_experts; + size_t top_k; +}; + +class moe_router_fused_softmax_gpu : public ::testing::TestWithParam {}; + +TEST_P(moe_router_fused_softmax_gpu, router_accuracy_test) { + auto param = GetParam(); + auto& engine = get_test_engine(); + if (!engine.get_device_info().supports_immad) { + GTEST_SKIP() << "No immad support"; + } + + tests::random_generator rg(GET_SUITE_NAME); + const size_t num_tokens = param.num_tokens; + const size_t num_experts = param.num_experts; + const size_t top_k = param.top_k; + + // Generate random logits + auto logits_data = rg.generate_random_1d(num_tokens * num_experts, -2.0f, 2.0f, 1000); + + // Create input memory + auto logits_mem = engine.allocate_memory(layout{ov::PartialShape{static_cast(num_tokens), static_cast(num_experts)}, + data_types::f16, format::bfyx}); + set_values(logits_mem, logits_data); + get_test_stream().finish(); + + // Build topology + topology topology; + topology.add(input_layout("logits", logits_mem->get_layout())); + + MoERouterFused::Config config; + config.num_expert = num_experts; + config.top_k = top_k; + config.routing_type = MoERouterFused::RoutingType::SOFTMAX; + + topology.add(moe_router_fused("router", {input_info("logits")}, config)); + topology.add(reorder("topk_weights", input_info("router", 0), format::bfyx, data_types::f16)); + topology.add(reorder("topk_indices", input_info("router", 1), format::bfyx, data_types::i32)); + + auto net_config = get_test_default_config(engine); + net_config.set_property(ov::intel_gpu::allow_new_shape_infer(true)); + network network(engine, topology, net_config); + network.set_input_data("logits", logits_mem); + + auto outputs = network.execute(); + get_test_stream().flush(); + + auto weights_mem = outputs.at("topk_weights").get_memory(); + auto indices_mem = outputs.at("topk_indices").get_memory(); + + cldnn::mem_lock weights_ptr(weights_mem, get_test_stream()); + cldnn::mem_lock indices_ptr(indices_mem, get_test_stream()); + + // Compute reference: softmax + top-k + normalize + for (size_t t = 0; t < num_tokens; ++t) { + // Softmax + std::vector logits(num_experts); + float max_val = std::numeric_limits::lowest(); + for (size_t e = 0; e < num_experts; ++e) { + logits[e] = static_cast(logits_data[t * num_experts + e]); + if (logits[e] > max_val) max_val = logits[e]; + } + float sum_exp = 0.0f; + for (size_t e = 0; e < num_experts; ++e) { + logits[e] = std::exp(logits[e] - max_val); + sum_exp += logits[e]; + } + for (size_t e = 0; e < num_experts; ++e) { + logits[e] /= sum_exp; + } + + // Top-k by sorting + std::vector> expert_weights; + for (size_t e = 0; e < num_experts; ++e) { + expert_weights.push_back({logits[e], e}); + } + std::partial_sort(expert_weights.begin(), + expert_weights.begin() + top_k, + expert_weights.end(), + [](const std::pair& a, const std::pair& b) { + return a.first > b.first; + }); + + // Normalize top-k weights + float sum_weights = 0.0f; + for (size_t k = 0; k < top_k; ++k) + sum_weights += expert_weights[k].first; + + // Compare + for (size_t k = 0; k < top_k; ++k) { + float ref_weight = expert_weights[k].first / sum_weights; + int32_t ref_index = static_cast(expert_weights[k].second); + + float actual_weight = static_cast(weights_ptr[t * top_k + k]); + int32_t actual_index = indices_ptr[t * top_k + k]; + + ASSERT_NEAR(actual_weight, ref_weight, 0.01f) + << "Token " << t << ", k=" << k << " weight mismatch"; + ASSERT_EQ(actual_index, ref_index) + << "Token " << t << ", k=" << k << " index mismatch"; + } + } +} + +class moe_router_fused_sigmoid_bias_gpu : public ::testing::TestWithParam {}; + +TEST_P(moe_router_fused_sigmoid_bias_gpu, router_accuracy_test) { + auto param = GetParam(); + auto& engine = get_test_engine(); + if (!engine.get_device_info().supports_immad) { + GTEST_SKIP() << "No immad support"; + } + + tests::random_generator rg(GET_SUITE_NAME); + const size_t num_tokens = param.num_tokens; + const size_t num_experts = param.num_experts; + const size_t top_k = param.top_k; + + // Generate random data + auto logits_data = rg.generate_random_1d(num_tokens * num_experts, -2.0f, 2.0f, 1000); + auto bias_data = rg.generate_random_1d(num_experts, -0.5f, 0.5f, 1000); + ov::float16 eps_val = ov::float16(1e-6f); + + // Create memories + auto logits_mem = engine.allocate_memory(layout{ov::PartialShape{static_cast(num_tokens), static_cast(num_experts)}, + data_types::f16, format::bfyx}); + set_values(logits_mem, logits_data); + get_test_stream().finish(); + + auto bias_mem = engine.allocate_memory(layout{ov::PartialShape{1, static_cast(num_experts)}, + data_types::f16, format::bfyx}); + set_values(bias_mem, bias_data); + get_test_stream().finish(); + + auto eps_mem = engine.allocate_memory({data_types::f16, format::bfyx, {1, 1, 1, 1}}); + set_values(eps_mem, {eps_val}); + get_test_stream().finish(); + + // Build topology + topology topology; + topology.add(input_layout("logits", logits_mem->get_layout())); + topology.add(data("bias", bias_mem)); + topology.add(data("eps", eps_mem)); + + MoERouterFused::Config config; + config.num_expert = num_experts; + config.top_k = top_k; + config.routing_type = MoERouterFused::RoutingType::SIGMOID_BIAS; + + topology.add(moe_router_fused("router", {input_info("logits"), input_info("bias"), input_info("eps")}, config)); + topology.add(reorder("topk_weights", input_info("router", 0), format::bfyx, data_types::f16)); + topology.add(reorder("topk_indices", input_info("router", 1), format::bfyx, data_types::i32)); + + auto net_config = get_test_default_config(engine); + net_config.set_property(ov::intel_gpu::allow_new_shape_infer(true)); + network network(engine, topology, net_config); + network.set_input_data("logits", logits_mem); + + auto outputs = network.execute(); + get_test_stream().flush(); + + auto weights_mem = outputs.at("topk_weights").get_memory(); + auto indices_mem = outputs.at("topk_indices").get_memory(); + + cldnn::mem_lock weights_ptr(weights_mem, get_test_stream()); + cldnn::mem_lock indices_ptr(indices_mem, get_test_stream()); + + // Compute reference: sigmoid + bias topk + normalize + for (size_t t = 0; t < num_tokens; ++t) { + // Sigmoid + std::vector sigmoid_scores(num_experts); + for (size_t e = 0; e < num_experts; ++e) { + float logit = static_cast(logits_data[t * num_experts + e]); + sigmoid_scores[e] = 1.0f / (1.0f + std::exp(-logit)); + } + + // Selection = sigmoid + bias + std::vector> expert_weights; + for (size_t e = 0; e < num_experts; ++e) { + float score = sigmoid_scores[e] + static_cast(bias_data[e]); + expert_weights.push_back({score, e}); + } + std::partial_sort(expert_weights.begin(), + expert_weights.begin() + top_k, + expert_weights.end(), + [](const std::pair& a, const std::pair& b) { + return a.first > b.first; + }); + + // Normalize using raw sigmoid values at top-k indices + eps + float sum_weights = 0.0f; + for (size_t k = 0; k < top_k; ++k) + sum_weights += sigmoid_scores[expert_weights[k].second]; + sum_weights += static_cast(eps_val); + + // Compare + for (size_t k = 0; k < top_k; ++k) { + float ref_weight = sigmoid_scores[expert_weights[k].second] / sum_weights; + int32_t ref_index = static_cast(expert_weights[k].second); + + float actual_weight = static_cast(weights_ptr[t * top_k + k]); + int32_t actual_index = indices_ptr[t * top_k + k]; + + ASSERT_NEAR(actual_weight, ref_weight, 0.02f) + << "Token " << t << ", k=" << k << " weight mismatch"; + ASSERT_EQ(actual_index, ref_index) + << "Token " << t << ", k=" << k << " index mismatch"; + } + } +} + +INSTANTIATE_TEST_SUITE_P(smoke, + moe_router_fused_softmax_gpu, + ::testing::Values(MoeRouterTestParams{1, 4, 2}, + MoeRouterTestParams{4, 4, 2}, + MoeRouterTestParams{16, 8, 2}, + MoeRouterTestParams{1, 8, 4})); + +INSTANTIATE_TEST_SUITE_P(smoke, + moe_router_fused_sigmoid_bias_gpu, + ::testing::Values(MoeRouterTestParams{1, 4, 2}, + MoeRouterTestParams{4, 4, 2}, + MoeRouterTestParams{16, 8, 2}, + MoeRouterTestParams{1, 8, 4})); diff --git a/src/plugins/intel_gpu/tests/unit/test_cases/moe_scatter_reduction_gpu_test.cpp b/src/plugins/intel_gpu/tests/unit/test_cases/moe_scatter_reduction_gpu_test.cpp index 2068a8ce53a2..fd03d7bdcfdd 100644 --- a/src/plugins/intel_gpu/tests/unit/test_cases/moe_scatter_reduction_gpu_test.cpp +++ b/src/plugins/intel_gpu/tests/unit/test_cases/moe_scatter_reduction_gpu_test.cpp @@ -10,7 +10,7 @@ #include #include #include -#include +#include "ov_ops/moe_compressed.hpp" using namespace cldnn; using namespace ov::intel_gpu; @@ -52,7 +52,7 @@ void test_moe_scatter_reduction(bool is_caching_test, size_t k) { size_t hidden_size = k; size_t num_active_experts_per_token = 2; - ov::intel_gpu::op::MOECompressed::Config moe_config; + ov::op::internal::MOECompressed::Config moe_config; moe_config.top_k = num_active_experts_per_token; moe_config.has_batch_dim = false; diff --git a/src/plugins/intel_gpu/tests/unit/test_cases/multiclass_nms_gpu_test.cpp b/src/plugins/intel_gpu/tests/unit/test_cases/multiclass_nms_gpu_test.cpp index cb2ac33f0c1c..554e952f2fea 100644 --- a/src/plugins/intel_gpu/tests/unit/test_cases/multiclass_nms_gpu_test.cpp +++ b/src/plugins/intel_gpu/tests/unit/test_cases/multiclass_nms_gpu_test.cpp @@ -186,7 +186,7 @@ struct multiclass_nms_test : public ::testing::TestWithParamexecute(); const auto output_boxes = outputs.at("multiclass_nms").get_memory(); - const cldnn::mem_lock output_boxes_ptr(output_boxes, get_test_stream()); + const cldnn::mem_lock output_boxes_ptr(output_boxes, get_test_stream()); ASSERT_EQ(output_boxes_ptr.size(), dim * 6) << "format=" << fmt_to_str(target_format); const auto get_plane_data = [&](const memory::ptr& mem, const data_types data_type, diff --git a/src/plugins/intel_gpu/tests/unit/test_cases/mvn_gpu_test.cpp b/src/plugins/intel_gpu/tests/unit/test_cases/mvn_gpu_test.cpp index 6b55c9f0c59f..d9a1cb69285e 100644 --- a/src/plugins/intel_gpu/tests/unit/test_cases/mvn_gpu_test.cpp +++ b/src/plugins/intel_gpu/tests/unit/test_cases/mvn_gpu_test.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -799,14 +800,6 @@ struct mvn_random_test_bsv32 : ::testing::TestWithParam { fill_data(mem, input_data); } - size_t get_x_pitch(layout& layout) { - auto tensor_x0 = tensor(batch(0), feature(0), spatial(0, 0, 0, 0)); - auto tensor_x1 = tensor(batch(0), feature(0), spatial(1, 0, 0, 0)); - auto x0 = layout.get_linear_offset(tensor_x0); - auto x1 = layout.get_linear_offset(tensor_x1); - return (x1 - x0); - } - template void compare_outputs(const cldnn::memory::ptr out_ref, const cldnn::memory::ptr out_opt) { auto output_lay = out_ref->get_layout(); @@ -816,22 +809,37 @@ struct mvn_random_test_bsv32 : ::testing::TestWithParam { size_t f = output_lay.feature(); size_t x = output_lay.spatial(0); size_t y = output_lay.spatial(1); + size_t z = output_lay.spatial(2); cldnn::mem_lock ref_ptr(out_ref, get_test_stream()); cldnn::mem_lock opt_ptr(out_opt, get_test_stream()); - auto ref_x_pitch = get_x_pitch(output_lay); - auto opt_x_pitch = get_x_pitch(opt_output_lay); + float tolerance = (output_lay.data_type == data_types::f16) ? 5.e-2f : 1.e-2f; + size_t err_count = 0; for (size_t bi = 0; bi < b; ++bi) { for (size_t fi = 0; fi < f; ++fi) { - for (size_t yi = 0; yi < y; ++yi) { - auto ref_out_coords = tensor(batch(bi), feature(fi), spatial(0, yi, 0, 0)); - auto ref_out_offset = output_lay.get_linear_offset(ref_out_coords); - auto opt_out_offset = opt_output_lay.get_linear_offset(ref_out_coords); - for (size_t xi = 0; xi < x; ++xi) { - auto ref_out_val = ref_ptr[ref_out_offset + xi * ref_x_pitch]; - auto opt_out_val = opt_ptr[opt_out_offset + xi * opt_x_pitch]; - ASSERT_NEAR(static_cast(opt_out_val), static_cast(ref_out_val), 1.e-1f); + for (size_t zi = 0; zi < z; ++zi) { + for (size_t yi = 0; yi < y; ++yi) { + for (size_t xi = 0; xi < x; ++xi) { + auto coords = tensor(batch(bi), feature(fi), spatial(xi, yi, zi, 0)); + auto ref_out_offset = output_lay.get_linear_offset(coords); + auto opt_out_offset = opt_output_lay.get_linear_offset(coords); + auto ref_out_val = ref_ptr[ref_out_offset]; + auto opt_out_val = opt_ptr[opt_out_offset]; + float diff = std::abs(static_cast(opt_out_val) - static_cast(ref_out_val)); + if (diff > tolerance && err_count < 5) { + std::cerr << "MISMATCH at b=" << bi << " f=" << fi << " z=" << zi + << " y=" << yi << " x=" << xi + << " ref=" << static_cast(ref_out_val) + << " opt=" << static_cast(opt_out_val) + << " diff=" << diff + << " ref_off=" << ref_out_offset + << " opt_off=" << opt_out_offset + << std::endl; + err_count++; + } + ASSERT_NEAR(static_cast(opt_out_val), static_cast(ref_out_val), tolerance); + } } } } @@ -842,13 +850,15 @@ struct mvn_random_test_bsv32 : ::testing::TestWithParam { auto& size = params.input_size; auto& output_pad = params.output_pad; auto& engine = get_test_engine(); - auto input = engine.allocate_memory({params.input_type, format::bfyx, params.input_size}); + bool is_5d = size.spatial[2] > 1; + auto ref_fmt = is_5d ? format::bfzyx : format::bfyx; + auto input = engine.allocate_memory({params.input_type, ref_fmt, params.input_size}); switch (params.input_type) { case data_types::f32: fill_random_data(input, -127, 127); break; case data_types::f16: - fill_random_data(input, -127, 127, 1); + fill_random_data(input, -10, 10, 1); break; case data_types::i8: fill_random_data(input, -127, 127, 1); @@ -860,7 +870,9 @@ struct mvn_random_test_bsv32 : ::testing::TestWithParam { break; } - auto axes = params.across_channels ? std::vector{1, 2, 3} : std::vector{2, 3}; + auto axes = params.across_channels + ? (is_5d ? std::vector{1, 2, 3, 4} : std::vector{1, 2, 3}) + : (is_5d ? std::vector{2, 3, 4} : std::vector{2, 3}); topology topo; topo.add(input_layout("input", input->get_layout())); auto prim = mvn("mvn", input_info("input"), params.normalize_variance, 1e-10f, false, axes); @@ -868,7 +880,7 @@ struct mvn_random_test_bsv32 : ::testing::TestWithParam { topo.add(prim); ExecutionConfig config = get_test_default_config(engine); config.set_property(ov::intel_gpu::custom_outputs(std::vector{"mvn"})); - config.set_property(ov::intel_gpu::force_implementations(ov::intel_gpu::ImplForcingMap{ {"mvn", {format::type::bfyx, "mvn_gpu_bfyx_opt"}} })); + config.set_property(ov::intel_gpu::force_implementations(ov::intel_gpu::ImplForcingMap{ {"mvn", {ref_fmt, "mvn_gpu_bfyx_opt"}} })); cldnn::network::ptr net = get_network(engine, topo, config, get_test_stream_ptr(), is_caching_test); @@ -885,7 +897,11 @@ struct mvn_random_test_bsv32 : ::testing::TestWithParam { topo_opt.add(prim_opt); ExecutionConfig config_opt = get_test_default_config(engine); config_opt.set_property(ov::intel_gpu::custom_outputs(std::vector{"mvn_opt", "input_to_target_layout"})); - config_opt.set_property(ov::intel_gpu::force_implementations(ov::intel_gpu::ImplForcingMap{ {"mvn_opt", {params.input_format, "mvn_gpu_b_fs_yx_fsv16_imad"}} })); + const auto opt_kernel = (params.input_format == format::bs_fs_yx_bsv32_fsv16 || + params.input_format == format::bs_fs_yx_bsv32_fsv32) + ? "mvn_gpu_b_fs_yx_bsv32" + : "mvn_gpu_b_fs_yx_fsv16"; + config_opt.set_property(ov::intel_gpu::force_implementations(ov::intel_gpu::ImplForcingMap{ {"mvn_opt", {params.input_format, opt_kernel}} })); cldnn::network::ptr net_opt = get_network(engine, topo_opt, config_opt, get_test_stream_ptr(), is_caching_test); @@ -931,6 +947,27 @@ struct mvn_test_case_generator_bsv32 : std::vector { push_back(mvn_basic_test_params{fmt, in_dt, {32, 32, 10, 10}, false, false, false, padding()}); return *this; } + + mvn_test_case_generator_bsv32& fsv_tests(format::type fmt, data_types in_dt) { + push_back(mvn_basic_test_params{fmt, in_dt, {2, 32, 17, 13}, false, false, false, padding()}); + push_back(mvn_basic_test_params{fmt, in_dt, {2, 32, 17, 13}, true, false, false, padding()}); + push_back(mvn_basic_test_params{fmt, in_dt, {2, 32, 17, 13}, false, true, false, padding()}); + push_back(mvn_basic_test_params{fmt, in_dt, {2, 32, 17, 13}, true, true, false, padding()}); + return *this; + } + + mvn_test_case_generator_bsv32& fsv_large_spatial_tests(format::type fmt, data_types in_dt) { + push_back(mvn_basic_test_params{fmt, in_dt, {1, 16, 257, 256}, true, false, false, padding()}); + return *this; + } + + mvn_test_case_generator_bsv32& fsv_zyx_tests(format::type fmt, data_types in_dt) { + push_back(mvn_basic_test_params{fmt, in_dt, {2, 32, 5, 13, 7}, false, false, false, padding()}); + push_back(mvn_basic_test_params{fmt, in_dt, {2, 32, 5, 13, 7}, true, false, false, padding()}); + push_back(mvn_basic_test_params{fmt, in_dt, {2, 32, 5, 13, 7}, false, true, false, padding()}); + push_back(mvn_basic_test_params{fmt, in_dt, {2, 32, 5, 13, 7}, true, true, false, padding()}); + return *this; + } }; INSTANTIATE_TEST_SUITE_P(mvn_bsv32_fsv32, @@ -949,6 +986,60 @@ INSTANTIATE_TEST_SUITE_P(mvn_fsv16, testing::ValuesIn(mvn_test_case_generator_bsv32() .bsv32_tests(format::b_fs_yx_fsv16, data_types::i8))); +// 4D fsv16 with float types (new: covers F16/F32 + mvn_gpu_b_fs_yx_fsv16 kernel path) +INSTANTIATE_TEST_SUITE_P(mvn_fsv16_f16, + mvn_random_test_bsv32, + testing::ValuesIn(mvn_test_case_generator_bsv32() + .fsv_tests(format::b_fs_yx_fsv16, data_types::f16))); + +INSTANTIATE_TEST_SUITE_P(mvn_fsv16_f16_large_spatial, + mvn_random_test_bsv32, + testing::ValuesIn(mvn_test_case_generator_bsv32() + .fsv_large_spatial_tests(format::b_fs_yx_fsv16, data_types::f16))); + +INSTANTIATE_TEST_SUITE_P(mvn_fsv16_f32, + mvn_random_test_bsv32, + testing::ValuesIn(mvn_test_case_generator_bsv32() + .fsv_tests(format::b_fs_yx_fsv16, data_types::f32))); + +// 4D fsv32 (new: covers INPUT_SLICE_PITCH=32 kernel path) +INSTANTIATE_TEST_SUITE_P(mvn_fsv32_f16, + mvn_random_test_bsv32, + testing::ValuesIn(mvn_test_case_generator_bsv32() + .fsv_tests(format::b_fs_yx_fsv32, data_types::f16))); + +INSTANTIATE_TEST_SUITE_P(mvn_fsv32_f32, + mvn_random_test_bsv32, + testing::ValuesIn(mvn_test_case_generator_bsv32() + .fsv_tests(format::b_fs_yx_fsv32, data_types::f32))); + +INSTANTIATE_TEST_SUITE_P(mvn_fsv32_i8, + mvn_random_test_bsv32, + testing::ValuesIn(mvn_test_case_generator_bsv32() + .fsv_tests(format::b_fs_yx_fsv32, data_types::i8))); + +// 5D zyx fsv16 (new: covers b_fs_zyx_fsv16 kernel path with float types) +INSTANTIATE_TEST_SUITE_P(mvn_zyx_fsv16_f16, + mvn_random_test_bsv32, + testing::ValuesIn(mvn_test_case_generator_bsv32() + .fsv_zyx_tests(format::b_fs_zyx_fsv16, data_types::f16))); + +INSTANTIATE_TEST_SUITE_P(mvn_zyx_fsv16_f32, + mvn_random_test_bsv32, + testing::ValuesIn(mvn_test_case_generator_bsv32() + .fsv_zyx_tests(format::b_fs_zyx_fsv16, data_types::f32))); + +// 5D zyx fsv32 (new: covers b_fs_zyx_fsv32 + INPUT_SLICE_PITCH=32 kernel path) +INSTANTIATE_TEST_SUITE_P(mvn_zyx_fsv32_f16, + mvn_random_test_bsv32, + testing::ValuesIn(mvn_test_case_generator_bsv32() + .fsv_zyx_tests(format::b_fs_zyx_fsv32, data_types::f16))); + +INSTANTIATE_TEST_SUITE_P(mvn_zyx_fsv32_f32, + mvn_random_test_bsv32, + testing::ValuesIn(mvn_test_case_generator_bsv32() + .fsv_zyx_tests(format::b_fs_zyx_fsv32, data_types::f32))); + TEST(mvn_gpu_test, mvn_test_across_channels_outside_sqrt_bfyx_cached) { test_mvn_test_across_channels_outside_sqrt_bfyx(true); } @@ -1045,3 +1136,81 @@ TEST(mvn_bfyx_opt_fused_ops, basic_fused) { } #endif + +// In-place crop feeding an MVN that requires alignment must materialize a contiguous +// buffer: MVN canonicalizes the shape and reads with contiguous pitches, so a strided +// sub-view from in-place crop would be read incorrectly (only row 0 was correct). +TEST(mvn_gpu_test, crop_then_mvn_last_axis_contiguous_input) { + auto& engine = get_test_engine(); + + // [3,4,5,64] -> crop -> [1,2,2,32] -> MVN(axis=-1). tensor ctor: (b, f, x, y). + const tensor src_size{3, 4, /*x=*/64, /*y=*/5}; + const tensor crop_size{1, 2, /*x=*/32, /*y=*/2}; + const tensor crop_offset{0, 0, 0, 0}; + + auto input = engine.allocate_memory({data_types::f32, format::bfyx, src_size}); + + tests::random_generator rg(GET_SUITE_NAME); + auto input_vec = rg.generate_random_1d(input->count(), -1.f, 1.f); + set_values(input, input_vec); + + topology topo; + topo.add(input_layout("input", input->get_layout())); + topo.add(crop("crop", input_info("input"), crop_size, crop_offset)); + topo.add(mvn("mvn", input_info("crop"), /*normalize_variance=*/true, + /*epsilon=*/0.f, /*eps_inside_sqrt=*/false, + /*reduction_axes=*/std::vector{3})); + + ExecutionConfig config = get_test_default_config(engine); + config.set_property(ov::intel_gpu::optimize_data(true)); + + network network(engine, topo, config); + network.set_input_data("input", input); + + auto outputs = network.execute(); + ASSERT_EQ(outputs.size(), size_t(1)); + auto output = outputs.at("mvn").get_memory(); + + const int64_t B = crop_size.batch[0]; + const int64_t F = crop_size.feature[0]; + const int64_t Y = crop_size.spatial[1]; + const int64_t X = crop_size.spatial[0]; + std::vector reference(B * F * Y * X); + + const int64_t SF = src_size.feature[0]; + const int64_t SY = src_size.spatial[1]; + const int64_t SX = src_size.spatial[0]; + + for (int64_t b = 0; b < B; ++b) { + for (int64_t f = 0; f < F; ++f) { + for (int64_t y = 0; y < Y; ++y) { + float mean = 0.f; + for (int64_t x = 0; x < X; ++x) { + int64_t src_idx = ((b * SF + f) * SY + y) * SX + x; + mean += input_vec[src_idx]; + } + mean /= static_cast(X); + float var = 0.f; + for (int64_t x = 0; x < X; ++x) { + int64_t src_idx = ((b * SF + f) * SY + y) * SX + x; + float v = input_vec[src_idx] - mean; + var += v * v; + } + var /= static_cast(X); + float inv_std = 1.f / std::sqrt(var); + for (int64_t x = 0; x < X; ++x) { + int64_t src_idx = ((b * SF + f) * SY + y) * SX + x; + int64_t dst_idx = ((b * F + f) * Y + y) * X + x; + reference[dst_idx] = (input_vec[src_idx] - mean) * inv_std; + } + } + } + } + + cldnn::mem_lock out_ptr(output, get_test_stream()); + ASSERT_EQ(out_ptr.size(), reference.size()); + const float tolerance = 1e-4f; + for (size_t i = 0; i < reference.size(); ++i) { + ASSERT_NEAR(out_ptr[i], reference[i], tolerance) << " mismatch at index " << i; + } +} diff --git a/src/plugins/intel_gpu/tests/unit/test_cases/non_max_suppression_test.cpp b/src/plugins/intel_gpu/tests/unit/test_cases/non_max_suppression_test.cpp index e4015edf47e1..70259edc989f 100644 --- a/src/plugins/intel_gpu/tests/unit/test_cases/non_max_suppression_test.cpp +++ b/src/plugins/intel_gpu/tests/unit/test_cases/non_max_suppression_test.cpp @@ -160,7 +160,7 @@ struct non_max_suppression_basic : public testing::Test { this->pad}; auto out_mem = result.at("plane_nms").get_memory(); - cldnn::mem_lock out_ptr(out_mem, get_test_stream()); + cldnn::mem_lock out_ptr(out_mem, get_test_stream()); ASSERT_EQ(expected_out.size(), out_ptr.size()); for (size_t i = 0; i < expected_out.size(); ++i) { @@ -217,7 +217,7 @@ struct non_max_suppression_basic : public testing::Test { }; auto out_mem = result.at("plane_nms").get_memory(); - cldnn::mem_lock out_ptr(out_mem, get_test_stream()); + cldnn::mem_lock out_ptr(out_mem, get_test_stream()); ASSERT_EQ(expected_out.size(), out_ptr.size()); for (size_t i = 0; i < expected_out.size(); ++i) { @@ -304,7 +304,7 @@ struct non_max_suppression_basic : public testing::Test { }; auto out_mem = result.at("plane_nms").get_memory(); - cldnn::mem_lock out_ptr(out_mem, get_test_stream()); + cldnn::mem_lock out_ptr(out_mem, get_test_stream()); ASSERT_EQ(expected_out.size(), out_ptr.size()); for (size_t i = 0; i < expected_out.size(); ++i) { @@ -325,7 +325,7 @@ struct non_max_suppression_basic : public testing::Test { auto second_output_result = second_output_net.execute(); auto plane_scores_mem = second_output_result.at("plane_scores").get_memory(); if (this->data_type == data_types::f32) { - cldnn::mem_lock second_output_ptr(plane_scores_mem, get_test_stream()); + cldnn::mem_lock second_output_ptr(plane_scores_mem, get_test_stream()); for (size_t i = 0; i < expected_second_out.size(); ++i) { ASSERT_FLOAT_EQ(expected_second_out[i], second_output_ptr[i]); @@ -423,7 +423,7 @@ struct non_max_suppression_basic : public testing::Test { }; auto out_mem = result.at("plane_nms").get_memory(); - cldnn::mem_lock out_ptr(out_mem, get_test_stream()); + cldnn::mem_lock out_ptr(out_mem, get_test_stream()); auto selected_scores_mem = result.at("plane_scores").get_memory(); auto valid_outputs_mem = result.at("plane_outputs").get_memory(); @@ -445,7 +445,7 @@ struct non_max_suppression_basic : public testing::Test { auto second_output_result = second_output_net.execute(); auto plane_scores_mem = second_output_result.at("plane_scores").get_memory(); if (this->data_type == data_types::f32) { - cldnn::mem_lock second_output_ptr(plane_scores_mem, get_test_stream()); + cldnn::mem_lock second_output_ptr(plane_scores_mem, get_test_stream()); for (size_t i = 0; i < expected_second_out.size(); ++i) { ASSERT_FLOAT_EQ(expected_second_out[i], second_output_ptr[i]); @@ -507,7 +507,7 @@ struct non_max_suppression_basic : public testing::Test { this->pad, this->pad, this->pad, this->pad, this->pad, this->pad, this->pad, this->pad, this->pad}; auto out_mem = result.at("plane_nms").get_memory(); - cldnn::mem_lock out_ptr(out_mem, get_test_stream()); + cldnn::mem_lock out_ptr(out_mem, get_test_stream()); ASSERT_EQ(expected_out.size(), out_ptr.size()); for (size_t i = 0; i < expected_out.size(); ++i) { @@ -564,7 +564,7 @@ struct non_max_suppression_basic : public testing::Test { this->pad, this->pad, this->pad, this->pad, this->pad, this->pad, this->pad, this->pad, this->pad}; auto out_mem = result.at("plane_nms").get_memory(); - cldnn::mem_lock out_ptr(out_mem, get_test_stream()); + cldnn::mem_lock out_ptr(out_mem, get_test_stream()); ASSERT_EQ(expected_out.size(), out_ptr.size()); for (size_t i = 0; i < expected_out.size(); ++i) { @@ -642,7 +642,7 @@ struct non_max_suppression_basic : public testing::Test { }; auto out_mem0 = result.at("plane_nms0").get_memory(); - cldnn::mem_lock out0_ptr(out_mem0, get_test_stream()); + cldnn::mem_lock out0_ptr(out_mem0, get_test_stream()); ASSERT_EQ(expected_out0.size(), out0_ptr.size()); for (size_t i = 0; i < out0_ptr.size(); ++i) { @@ -659,7 +659,7 @@ struct non_max_suppression_basic : public testing::Test { 1.0f, 0.0f, 0.5f }; auto out_mem1 = result.at("plane_nms1").get_memory(); - cldnn::mem_lock out1_ptr(out_mem1, get_test_stream()); + cldnn::mem_lock out1_ptr(out_mem1, get_test_stream()); ASSERT_EQ(expected_out1.size(), out1_ptr.size()); for (size_t i = 0; i < out1_ptr.size(); ++i) { @@ -674,7 +674,7 @@ struct non_max_suppression_basic : public testing::Test { 1.0f, 0.0f, 0.5f }; auto out_mem1 = result.at("plane_nms1").get_memory(); - cldnn::mem_lock out1_ptr(out_mem1, get_test_stream()); + cldnn::mem_lock out1_ptr(out_mem1, get_test_stream()); ASSERT_EQ(expected_out1.size(), out1_ptr.size()); for (size_t i = 0; i < out1_ptr.size(); ++i) { @@ -686,7 +686,7 @@ struct non_max_suppression_basic : public testing::Test { // output 2 auto out_mem2 = result.at("plane_nms2").get_memory(); - cldnn::mem_lock out2_ptr(out_mem2, get_test_stream()); + cldnn::mem_lock out2_ptr(out_mem2, get_test_stream()); ASSERT_EQ(1, out2_ptr.size()); ASSERT_EQ(5, out2_ptr[0]); } @@ -744,7 +744,7 @@ struct non_max_suppression_basic : public testing::Test { this->pad, this->pad, this->pad, this->pad, this->pad, this->pad, this->pad, this->pad, this->pad}; auto out_mem = result.at("plane_nms").get_memory(); - cldnn::mem_lock out_ptr(out_mem, get_test_stream()); + cldnn::mem_lock out_ptr(out_mem, get_test_stream()); std::vector score_indices; score_indices.resize(36); std::vector sel_scores(36); @@ -934,7 +934,7 @@ struct nms_rotated_test : public ::testing::TestWithParamset_input_data("scores", scores_mem); const auto result = net->execute(); const auto indices_mem = result.at("nms").get_memory(); - const cldnn::mem_lock indices_ptr(indices_mem, get_test_stream()); + const cldnn::mem_lock indices_ptr(indices_mem, get_test_stream()); const cldnn::mem_lock selected_scores_ptr(selected_scores_mem, get_test_stream()); const cldnn::mem_lock valid_outputs_ptr(valid_outputs_mem, get_test_stream()); diff --git a/src/plugins/intel_gpu/tests/unit/test_cases/non_zero_gpu_test.cpp b/src/plugins/intel_gpu/tests/unit/test_cases/non_zero_gpu_test.cpp index 03050957b9f0..fb72c5eefde2 100644 --- a/src/plugins/intel_gpu/tests/unit/test_cases/non_zero_gpu_test.cpp +++ b/src/plugins/intel_gpu/tests/unit/test_cases/non_zero_gpu_test.cpp @@ -56,7 +56,7 @@ void test_count_non_zero(layout in_layout, std::vector in_data) { auto outputs = network.execute(); auto output = outputs.at("count_nonzero").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); ASSERT_EQ(count_non_zero, output_ptr[0]); } @@ -152,7 +152,7 @@ TEST(test_count_non_zero, dynamic_2d_f32_bfyx) { ASSERT_EQ(outputs.size(), size_t(1)); auto output = outputs.at("count_nonzero").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); auto count_non_zero = ov::reference::non_zero_get_count(in_data.data(), in_layout.get_shape()); ASSERT_EQ(count_non_zero, output_ptr[0]); @@ -188,7 +188,7 @@ void test_gather_non_zero(layout in_layout, std::vector in_data) { network.set_input_data("InputData", input_mem); auto outputs = network.execute(); auto output = outputs.at("gather_nonzero").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); cldnn::mem_lock shape_ptr(output_shape_mem, get_test_stream()); for (size_t i = 0; i < expected_results.size(); ++i) { @@ -312,7 +312,7 @@ TEST(non_zero_gpu, dynamic) { auto outputs = network.execute(); auto output = outputs.at("gather_nonzero").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); ASSERT_EQ(output_ptr.size(), (uint32_t)8); for (uint32_t i = 0; i < out_data.size(); ++i) { @@ -343,7 +343,7 @@ void test_non_zero(layout in_layout, std::vector in_data) { network.set_input_data("InputData", input_mem); auto outputs = network.execute(); auto output = outputs.at("gather_nonzero").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (size_t i = 0; i < expected_results.size(); ++i) { ASSERT_EQ(expected_results[i], output_ptr[i]); @@ -476,7 +476,7 @@ TEST(test_gather_non_zero, not_use_local_mem) { auto outputs = network.execute(); auto output = outputs.at("gather_nonzero").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results(max_local_mem_size); ov::reference::non_zero(in_data.data(), expected_results.data(), in_layout.get_shape()); @@ -526,7 +526,7 @@ TEST(non_zero_gpu, const_input) { auto outputs = network.execute(); auto output = outputs.at("gather_nonzero").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); ASSERT_EQ(output_ptr.size(), (uint32_t)8); for (uint32_t i = 0; i < out_data.size(); ++i) { diff --git a/src/plugins/intel_gpu/tests/unit/test_cases/normalizel2_gpu_test.cpp b/src/plugins/intel_gpu/tests/unit/test_cases/normalizel2_gpu_test.cpp index f7f6ec91c2cc..5cd8f4fc59c9 100644 --- a/src/plugins/intel_gpu/tests/unit/test_cases/normalizel2_gpu_test.cpp +++ b/src/plugins/intel_gpu/tests/unit/test_cases/normalizel2_gpu_test.cpp @@ -89,7 +89,7 @@ struct normalize_basic : public testing::Test { auto output = outputs.at("plane_normalize2").get_memory(); if (this->data_type == data_types::f16) { - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); auto expected_results = this->get_expected_result(); for (size_t i = 0; i < expected_results.size(); ++i) { ASSERT_NEAR(expected_results[i], output_ptr[i], 0.001); diff --git a/src/plugins/intel_gpu/tests/unit/test_cases/one_hot_gpu_test.cpp b/src/plugins/intel_gpu/tests/unit/test_cases/one_hot_gpu_test.cpp index 1c66d13a990e..6d44830d2f69 100644 --- a/src/plugins/intel_gpu/tests/unit/test_cases/one_hot_gpu_test.cpp +++ b/src/plugins/intel_gpu/tests/unit/test_cases/one_hot_gpu_test.cpp @@ -94,7 +94,7 @@ void generic_one_hot_test_int(cldnn::format test_input_fmt, int input_b, int inp auto output_memory = outputs.at("output").get_memory(); auto output_layout = output_memory->get_layout(); - cldnn::mem_lock output_ptr(output_memory, get_test_stream()); + cldnn::mem_lock output_ptr(output_memory, get_test_stream()); VVVVF output_cpu = one_hot_cpu(input_rnd, one_hot_axis, one_hot_limit, input_padding_y, input_padding_x, output_padding_y, output_padding_x); ASSERT_EQ(output_layout.format.value, test_input_fmt.value); @@ -193,7 +193,7 @@ TEST(one_hot_gpu_i32, bfzyx_ax4) { auto output_memory = outputs.at("output").get_memory(); auto output_layout = output_memory->get_layout(); - cldnn::mem_lock output_ptr(output_memory, get_test_stream()); + cldnn::mem_lock output_ptr(output_memory, get_test_stream()); auto output_tensor = output_layout.get_padded_dims(); int z_size = output_tensor[2]; @@ -252,7 +252,7 @@ TEST(one_hot_gpu_i64, bfzyx_ax4) { auto output_memory = outputs.at("output").get_memory(); auto output_layout = output_memory->get_layout(); - cldnn::mem_lock output_ptr(output_memory, get_test_stream()); + cldnn::mem_lock output_ptr(output_memory, get_test_stream()); auto output_tensor = output_layout.get_padded_dims(); int z_size = output_tensor[2]; @@ -311,7 +311,7 @@ TEST(one_hot_gpu_i32_to_f32, bfyx_ax4) { auto output_memory = outputs.at("output").get_memory(); auto output_layout = output_memory->get_layout(); - cldnn::mem_lock output_ptr(output_memory, get_test_stream()); + cldnn::mem_lock output_ptr(output_memory, get_test_stream()); int z_size = output_layout.spatial(2); int y_size = output_layout.spatial(1); @@ -364,7 +364,7 @@ TEST(one_hot_gpu_i64_to_f32, bfyx_ax4) { auto output_memory = outputs.at("output").get_memory(); auto output_layout = output_memory->get_layout(); - cldnn::mem_lock output_ptr(output_memory, get_test_stream()); + cldnn::mem_lock output_ptr(output_memory, get_test_stream()); auto output_tensor = output_layout.get_padded_dims(); int z_size = output_tensor[2]; @@ -415,7 +415,7 @@ TEST(one_hot_gpu_i32, bfzyx_ax0) { auto output_memory = outputs.at("output").get_memory(); auto output_layout = output_memory->get_layout(); - cldnn::mem_lock output_ptr(output_memory, get_test_stream()); + cldnn::mem_lock output_ptr(output_memory, get_test_stream()); auto output_tensor = output_layout.get_padded_dims(); int z_size = output_tensor[2]; @@ -470,7 +470,7 @@ TEST(one_hot_gpu_i64, bfzyx_ax0) { auto output_memory = outputs.at("output").get_memory(); auto output_layout = output_memory->get_layout(); - cldnn::mem_lock output_ptr(output_memory, get_test_stream()); + cldnn::mem_lock output_ptr(output_memory, get_test_stream()); auto output_tensor = output_layout.get_padded_dims(); int z_size = output_tensor[2]; @@ -525,7 +525,7 @@ TEST(one_hot_gpu_i32, bfzyx_ax1) { auto output_memory = outputs.at("output").get_memory(); auto output_layout = output_memory->get_layout(); - cldnn::mem_lock output_ptr(output_memory, get_test_stream()); + cldnn::mem_lock output_ptr(output_memory, get_test_stream()); auto output_tensor = output_layout.get_padded_dims(); int z_size = output_tensor[2]; @@ -580,7 +580,7 @@ TEST(one_hot_gpu_i64, bfzyx_ax1) { auto output_memory = outputs.at("output").get_memory(); auto output_layout = output_memory->get_layout(); - cldnn::mem_lock output_ptr(output_memory, get_test_stream()); + cldnn::mem_lock output_ptr(output_memory, get_test_stream()); auto output_tensor = output_layout.get_padded_dims(); int z_size = output_tensor[2]; @@ -635,7 +635,7 @@ TEST(one_hot_gpu_i32, bfzyx_ax2) { auto output_memory = outputs.at("output").get_memory(); auto output_layout = output_memory->get_layout(); - cldnn::mem_lock output_ptr(output_memory, get_test_stream()); + cldnn::mem_lock output_ptr(output_memory, get_test_stream()); auto output_tensor = output_layout.get_padded_dims(); int z_size = output_tensor[2]; @@ -690,7 +690,7 @@ TEST(one_hot_gpu_i64, bfzyx_ax2) { auto output_memory = outputs.at("output").get_memory(); auto output_layout = output_memory->get_layout(); - cldnn::mem_lock output_ptr(output_memory, get_test_stream()); + cldnn::mem_lock output_ptr(output_memory, get_test_stream()); auto output_tensor = output_layout.get_padded_dims(); int z_size = output_tensor[2]; @@ -745,7 +745,7 @@ TEST(one_hot_gpu_i32, bfzyx_ax3) { auto output_memory = outputs.at("output").get_memory(); auto output_layout = output_memory->get_layout(); - cldnn::mem_lock output_ptr(output_memory, get_test_stream()); + cldnn::mem_lock output_ptr(output_memory, get_test_stream()); auto output_tensor = output_layout.get_padded_dims(); int z_size = output_tensor[2]; @@ -800,7 +800,7 @@ TEST(one_hot_gpu_i64, bfzyx_ax3) { auto output_memory = outputs.at("output").get_memory(); auto output_layout = output_memory->get_layout(); - cldnn::mem_lock output_ptr(output_memory, get_test_stream()); + cldnn::mem_lock output_ptr(output_memory, get_test_stream()); int z_size = output_layout.spatial(2); int y_size = output_layout.spatial(1); @@ -854,7 +854,7 @@ static void PerformNegativeIndicesModeTest(cldnn::engine& engine, const std::vec auto outputs = network.execute(); auto output = outputs.at("one_hot").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (size_t i = 0; i < expected.size(); ++i) { ASSERT_FLOAT_EQ(output_ptr[i], expected[i]) << "Mismatch at index " << i << ": expected " << expected[i] << ", got " << output_ptr[i] << std::endl; } diff --git a/src/plugins/intel_gpu/tests/unit/test_cases/pa_kv_reorder_gpu_test.cpp b/src/plugins/intel_gpu/tests/unit/test_cases/pa_kv_reorder_gpu_test.cpp index c487d146b2ef..18d92e55f578 100644 --- a/src/plugins/intel_gpu/tests/unit/test_cases/pa_kv_reorder_gpu_test.cpp +++ b/src/plugins/intel_gpu/tests/unit/test_cases/pa_kv_reorder_gpu_test.cpp @@ -2,35 +2,28 @@ // SPDX-License-Identifier: Apache-2.0 // -#include "test_utils.h" - +#include #include #include #include #include - -#include -#include #include +#include #include +#include "test_utils.h" + using namespace cldnn; using namespace ::tests; namespace { size_t key_offset(size_t block, size_t head, size_t k, size_t token, size_t kv_heads, size_t k_head_size, size_t block_size) { - return block * kv_heads * k_head_size * block_size + - head * k_head_size * block_size + - k * block_size + - token; + return block * kv_heads * k_head_size * block_size + head * k_head_size * block_size + k * block_size + token; } size_t value_offset(size_t block, size_t head, size_t token, size_t v, size_t kv_heads, size_t v_head_size, size_t block_size) { - return block * kv_heads * block_size * v_head_size + - head * block_size * v_head_size + - token * v_head_size + - v; + return block * kv_heads * block_size * v_head_size + head * block_size * v_head_size + token * v_head_size + v; } size_t key_comp_byte_offset(size_t block, @@ -42,8 +35,7 @@ size_t key_comp_byte_offset(size_t block, size_t k_head_size, size_t adjusted_k_head_size, size_t block_size) { - const size_t block_base = block * kv_heads * adjusted_k_head_size * block_size + - head * adjusted_k_head_size * block_size; + const size_t block_base = block * kv_heads * adjusted_k_head_size * block_size + head * adjusted_k_head_size * block_size; const size_t comp_base = block_base + k_head_size * block_size; const size_t token_base = (is_zp ? (block_size + token) : token) * sizeof(ov::float16); return comp_base + token_base + byte_in_fp16; @@ -58,8 +50,7 @@ size_t value_comp_byte_offset(size_t block, size_t v_head_size, size_t adjusted_v_head_size, size_t block_size) { - const size_t block_base = block * kv_heads * block_size * adjusted_v_head_size + - head * block_size * adjusted_v_head_size; + const size_t block_base = block * kv_heads * block_size * adjusted_v_head_size + head * block_size * adjusted_v_head_size; const size_t comp_base = block_base + v_head_size * block_size; const size_t token_base = (is_zp ? (block_size + token) : token) * sizeof(ov::float16); return comp_base + token_base + byte_in_fp16; @@ -73,11 +64,32 @@ size_t value_data_offset_compressed(size_t block, size_t v_head_size, size_t adjusted_v_head_size, size_t block_size) { - const size_t block_base = block * kv_heads * block_size * adjusted_v_head_size + - head * block_size * adjusted_v_head_size; + const size_t block_base = block * kv_heads * block_size * adjusted_v_head_size + head * block_size * adjusted_v_head_size; return block_base + token * v_head_size + v; } +// u4 V BY_TOKEN layout: each token row is `packed_v_head_size` data bytes followed inline +// by `[fp16 scale][fp16 zp]` (mirrors quantize_and_save_per_token in pa_kv_cache_update_ref.cl +// when out_data_pitch == 1). +size_t value_data_offset_int4_per_token(size_t block, size_t head, size_t token, size_t v, size_t kv_heads, size_t adjusted_v_head_size, size_t block_size) { + const size_t block_base = block * kv_heads * block_size * adjusted_v_head_size + head * block_size * adjusted_v_head_size; + return block_base + token * adjusted_v_head_size + v; +} + +size_t value_comp_byte_offset_int4_per_token(size_t block, + size_t head, + size_t token, + size_t byte_in_fp16, + bool is_zp, + size_t kv_heads, + size_t packed_v_head_size, + size_t adjusted_v_head_size, + size_t block_size) { + const size_t block_base = block * kv_heads * block_size * adjusted_v_head_size + head * block_size * adjusted_v_head_size; + const size_t row_base = block_base + token * adjusted_v_head_size + packed_v_head_size; + return row_base + (is_zp ? sizeof(ov::float16) : 0) + byte_in_fp16; +} + void run_copy_between_blocks_single_sequence_compressed_int4_test(data_types cache_dt) { auto& engine = get_test_engine(); @@ -119,10 +131,13 @@ void run_copy_between_blocks_single_sequence_compressed_int4_test(data_types cac set_values(block_indices_mem, {0, 1}); set_values(block_indices_begins_mem, {0, 2}); - set_values(block_update_indices_mem, { - 0, 17, - 15, 16, - }); + set_values(block_update_indices_mem, + { + 0, + 17, + 15, + 16, + }); set_values(block_update_indices_begins_mem, {0, 2}); topology topo; @@ -174,12 +189,12 @@ void run_copy_between_blocks_single_sequence_compressed_int4_test(data_types cac } for (size_t v = 0; v < packed_v_head_size; v++) { - const auto src0 = value_cache_ref[value_data_offset_compressed(0, 0, 0, v, kv_heads, packed_v_head_size, adjusted_v_head_size, block_size)]; - const auto dst17 = value_ptr[value_data_offset_compressed(1, 0, 1, v, kv_heads, packed_v_head_size, adjusted_v_head_size, block_size)]; + const auto src0 = value_cache_ref[value_data_offset_int4_per_token(0, 0, 0, v, kv_heads, adjusted_v_head_size, block_size)]; + const auto dst17 = value_ptr[value_data_offset_int4_per_token(1, 0, 1, v, kv_heads, adjusted_v_head_size, block_size)]; ASSERT_EQ(dst17, src0); - const auto src15 = value_cache_ref[value_data_offset_compressed(0, 0, 15, v, kv_heads, packed_v_head_size, adjusted_v_head_size, block_size)]; - const auto dst16 = value_ptr[value_data_offset_compressed(1, 0, 0, v, kv_heads, packed_v_head_size, adjusted_v_head_size, block_size)]; + const auto src15 = value_cache_ref[value_data_offset_int4_per_token(0, 0, 15, v, kv_heads, adjusted_v_head_size, block_size)]; + const auto dst16 = value_ptr[value_data_offset_int4_per_token(1, 0, 0, v, kv_heads, adjusted_v_head_size, block_size)]; ASSERT_EQ(dst16, src15); } @@ -189,10 +204,10 @@ void run_copy_between_blocks_single_sequence_compressed_int4_test(data_types cac ASSERT_EQ(key_ptr[key_comp_byte_offset(1, 0, 1, byte, true, kv_heads, packed_k_head_size, adjusted_k_head_size, block_size)], key_cache_ref[key_comp_byte_offset(0, 0, 0, byte, true, kv_heads, packed_k_head_size, adjusted_k_head_size, block_size)]); - ASSERT_EQ(value_ptr[value_comp_byte_offset(1, 0, 1, byte, false, kv_heads, packed_v_head_size, adjusted_v_head_size, block_size)], - value_cache_ref[value_comp_byte_offset(0, 0, 0, byte, false, kv_heads, packed_v_head_size, adjusted_v_head_size, block_size)]); - ASSERT_EQ(value_ptr[value_comp_byte_offset(1, 0, 1, byte, true, kv_heads, packed_v_head_size, adjusted_v_head_size, block_size)], - value_cache_ref[value_comp_byte_offset(0, 0, 0, byte, true, kv_heads, packed_v_head_size, adjusted_v_head_size, block_size)]); + ASSERT_EQ(value_ptr[value_comp_byte_offset_int4_per_token(1, 0, 1, byte, false, kv_heads, packed_v_head_size, adjusted_v_head_size, block_size)], + value_cache_ref[value_comp_byte_offset_int4_per_token(0, 0, 0, byte, false, kv_heads, packed_v_head_size, adjusted_v_head_size, block_size)]); + ASSERT_EQ(value_ptr[value_comp_byte_offset_int4_per_token(1, 0, 1, byte, true, kv_heads, packed_v_head_size, adjusted_v_head_size, block_size)], + value_cache_ref[value_comp_byte_offset_int4_per_token(0, 0, 0, byte, true, kv_heads, packed_v_head_size, adjusted_v_head_size, block_size)]); } } @@ -212,12 +227,7 @@ ov::float16 read_fp16_from_u8_vector(const std::vector& buffer, size_t return ov::float16::from_bits(bits); } -void fill_key_cache(memory::ptr key_cache_mem, - size_t blocks_num, - size_t kv_heads, - size_t k_head_size, - size_t block_size, - std::vector& values) { +void fill_key_cache(memory::ptr key_cache_mem, size_t blocks_num, size_t kv_heads, size_t k_head_size, size_t block_size, std::vector& values) { values.resize(key_cache_mem->count()); for (size_t b = 0; b < blocks_num; b++) { for (size_t h = 0; h < kv_heads; h++) { @@ -284,10 +294,13 @@ TEST(pa_kv_reorder_gpu, copy_between_blocks_single_sequence) { set_values(block_indices_mem, {0, 1}); set_values(block_indices_begins_mem, {0, 2}); - set_values(block_update_indices_mem, { - 0, 17, - 15, 16, - }); + set_values(block_update_indices_mem, + { + 0, + 17, + 15, + 16, + }); set_values(block_update_indices_begins_mem, {0, 2}); topology topo; @@ -347,8 +360,7 @@ TEST(pa_kv_reorder_gpu, copy_between_blocks_single_sequence) { ASSERT_EQ(dst16, src15); } - ASSERT_EQ(key_ptr[key_offset(0, 0, 0, 0, kv_heads, k_head_size, block_size)], - key_cache_ref[key_offset(0, 0, 0, 0, kv_heads, k_head_size, block_size)]); + ASSERT_EQ(key_ptr[key_offset(0, 0, 0, 0, kv_heads, k_head_size, block_size)], key_cache_ref[key_offset(0, 0, 0, 0, kv_heads, k_head_size, block_size)]); ASSERT_EQ(value_ptr[value_offset(0, 0, 0, 0, kv_heads, v_head_size, block_size)], value_cache_ref[value_offset(0, 0, 0, 0, kv_heads, v_head_size, block_size)]); } @@ -384,10 +396,13 @@ TEST(pa_kv_reorder_gpu, updates_are_scoped_per_sequence) { // Sequence 0 uses physical block 0, sequence 1 uses physical block 2. set_values(block_indices_mem, {0, 2}); set_values(block_indices_begins_mem, {0, 1, 2}); - set_values(block_update_indices_mem, { - 1, 3, // seq0: slot1 -> slot3 in block0 - 2, 4, // seq1: slot2 -> slot4 in block2 - }); + set_values(block_update_indices_mem, + { + 1, + 3, // seq0: slot1 -> slot3 in block0 + 2, + 4, // seq1: slot2 -> slot4 in block2 + }); set_values(block_update_indices_begins_mem, {0, 1, 2}); topology topo; @@ -428,14 +443,11 @@ TEST(pa_kv_reorder_gpu, updates_are_scoped_per_sequence) { cldnn::mem_lock value_ptr(value_cache_mem, network->get_stream()); for (size_t k = 0; k < k_head_size; k++) { - ASSERT_EQ(key_ptr[key_offset(0, 0, k, 3, kv_heads, k_head_size, block_size)], - key_cache_ref[key_offset(0, 0, k, 1, kv_heads, k_head_size, block_size)]); - ASSERT_EQ(key_ptr[key_offset(2, 0, k, 4, kv_heads, k_head_size, block_size)], - key_cache_ref[key_offset(2, 0, k, 2, kv_heads, k_head_size, block_size)]); + ASSERT_EQ(key_ptr[key_offset(0, 0, k, 3, kv_heads, k_head_size, block_size)], key_cache_ref[key_offset(0, 0, k, 1, kv_heads, k_head_size, block_size)]); + ASSERT_EQ(key_ptr[key_offset(2, 0, k, 4, kv_heads, k_head_size, block_size)], key_cache_ref[key_offset(2, 0, k, 2, kv_heads, k_head_size, block_size)]); // Unused middle block must stay untouched. - ASSERT_EQ(key_ptr[key_offset(1, 0, k, 4, kv_heads, k_head_size, block_size)], - key_cache_ref[key_offset(1, 0, k, 4, kv_heads, k_head_size, block_size)]); + ASSERT_EQ(key_ptr[key_offset(1, 0, k, 4, kv_heads, k_head_size, block_size)], key_cache_ref[key_offset(1, 0, k, 4, kv_heads, k_head_size, block_size)]); } for (size_t v = 0; v < v_head_size; v++) { @@ -488,10 +500,13 @@ TEST(pa_kv_reorder_gpu, copy_between_blocks_single_sequence_compressed) { set_values(block_indices_mem, {0, 1}); set_values(block_indices_begins_mem, {0, 2}); - set_values(block_update_indices_mem, { - 0, 17, - 15, 16, - }); + set_values(block_update_indices_mem, + { + 0, + 17, + 15, + 16, + }); set_values(block_update_indices_begins_mem, {0, 2}); topology topo; @@ -612,10 +627,13 @@ TEST(pa_kv_reorder_gpu, updates_are_scoped_per_sequence_compressed) { set_values(block_indices_mem, {0, 2}); set_values(block_indices_begins_mem, {0, 1, 2}); - set_values(block_update_indices_mem, { - 1, 3, // seq0: slot1 -> slot3 in block0 - 2, 4, // seq1: slot2 -> slot4 in block2 - }); + set_values(block_update_indices_mem, + { + 1, + 3, // seq0: slot1 -> slot3 in block0 + 2, + 4, // seq1: slot2 -> slot4 in block2 + }); set_values(block_update_indices_begins_mem, {0, 1, 2}); topology topo; @@ -762,24 +780,8 @@ TEST(pa_kv_reorder_gpu, copy_between_blocks_single_sequence_compressed_key_by_ch const ov::float16 scale = ov::float16(0.25f * static_cast(1 + ((b + t) % 3))); const ov::float16 zp = ov::float16(static_cast((static_cast(t) % 5) - 2)); for (size_t byte = 0; byte < sizeof(ov::float16); byte++) { - const auto scale_off = value_comp_byte_offset(b, - h, - t, - byte, - false, - kv_heads, - v_head_size, - adjusted_v_head_size, - block_size); - const auto zp_off = value_comp_byte_offset(b, - h, - t, - byte, - true, - kv_heads, - v_head_size, - adjusted_v_head_size, - block_size); + const auto scale_off = value_comp_byte_offset(b, h, t, byte, false, kv_heads, v_head_size, adjusted_v_head_size, block_size); + const auto zp_off = value_comp_byte_offset(b, h, t, byte, true, kv_heads, v_head_size, adjusted_v_head_size, block_size); value_cache_ref[scale_off] = reinterpret_cast(&scale)[byte]; value_cache_ref[zp_off] = reinterpret_cast(&zp)[byte]; } @@ -792,10 +794,13 @@ TEST(pa_kv_reorder_gpu, copy_between_blocks_single_sequence_compressed_key_by_ch set_values(block_indices_mem, {0, 1}); set_values(block_indices_begins_mem, {0, 2}); - set_values(block_update_indices_mem, { - 0, 17, - 15, 16, - }); + set_values(block_update_indices_mem, + { + 0, + 17, + 15, + 16, + }); set_values(block_update_indices_begins_mem, {0, 2}); topology topo; @@ -848,8 +853,10 @@ TEST(pa_kv_reorder_gpu, copy_between_blocks_single_sequence_compressed_key_by_ch const float dst_scale_inv = static_cast(read_fp16_from_byte_buffer(key_ptr, comp_scale_byte_offset)); const float dst_zp = static_cast(read_fp16_from_byte_buffer(key_ptr, comp_zp_byte_offset)); - const float dst17_dequant = (static_cast(key_ptr[key_offset(1, 0, k, 1, kv_heads, k_head_size, adjusted_paged_attention_block_size)]) - dst_zp) * dst_scale_inv; - const float dst16_dequant = (static_cast(key_ptr[key_offset(1, 0, k, 0, kv_heads, k_head_size, adjusted_paged_attention_block_size)]) - dst_zp) * dst_scale_inv; + const float dst17_dequant = + (static_cast(key_ptr[key_offset(1, 0, k, 1, kv_heads, k_head_size, adjusted_paged_attention_block_size)]) - dst_zp) * dst_scale_inv; + const float dst16_dequant = + (static_cast(key_ptr[key_offset(1, 0, k, 0, kv_heads, k_head_size, adjusted_paged_attention_block_size)]) - dst_zp) * dst_scale_inv; ASSERT_NEAR(dst17_dequant, static_cast(src0_q), 1.0f); ASSERT_NEAR(dst16_dequant, static_cast(src15_q), 1.0f); @@ -880,16 +887,20 @@ TEST(pa_kv_reorder_gpu, copy_between_blocks_single_sequence_compressed_u4_key_by constexpr size_t blocks_num = 2; constexpr size_t kv_heads = 1; constexpr size_t k_head_size = 32; - constexpr size_t packed_k_head_size = k_head_size / 2; constexpr size_t v_head_size = 16; constexpr size_t subgroup_size = 16; + constexpr size_t block_size = cldnn::paged_attention::block_size; + // u4 BY_CHANNEL key layout: each column = one head dim with 16 tokens packed as 8 bytes + // (lo nibble = token 2t, hi nibble = token 2t+1) followed by [scale_inv (f16)][zp (f16)] = 12 bytes/col. + // Number of columns = k_head_size (NOT halved, since BY_CHANNEL packs along the token axis). + constexpr size_t packed_block_size = block_size / 2; + constexpr size_t scales_zp_size = sizeof(ov::float16) * 2; + constexpr size_t adjusted_paged_attention_block_size = packed_block_size + scales_zp_size; + // u4 V is per-token inline: each token row = packed_v_head_size bytes + [scale][zp] (f16 each). constexpr size_t packed_v_head_size = ((v_head_size / 2 + subgroup_size - 1) / subgroup_size) * subgroup_size; - constexpr size_t scales_zp_size = sizeof(ov::float16) * 4; - constexpr size_t adjusted_paged_attention_block_size = cldnn::paged_attention::block_size + scales_zp_size; constexpr size_t adjusted_v_head_size = packed_v_head_size + scales_zp_size; - constexpr size_t block_size = cldnn::paged_attention::block_size; - auto key_cache_layout = layout{ov::PartialShape{blocks_num, kv_heads, packed_k_head_size, adjusted_paged_attention_block_size}, data_types::u8, format::bfyx}; + auto key_cache_layout = layout{ov::PartialShape{blocks_num, kv_heads, k_head_size, adjusted_paged_attention_block_size}, data_types::u8, format::bfyx}; auto value_cache_layout = layout{ov::PartialShape{blocks_num, kv_heads, block_size, adjusted_v_head_size}, data_types::u8, format::bfyx}; auto block_indices_layout = layout{ov::PartialShape{2}, data_types::i32, format::bfyx}; auto block_indices_begins_layout = layout{ov::PartialShape{2}, data_types::i32, format::bfyx}; @@ -912,58 +923,43 @@ TEST(pa_kv_reorder_gpu, copy_between_blocks_single_sequence_compressed_u4_key_by buffer[byte_offset + 1] = static_cast((bits >> 8) & 0xFF); }; - // Fill key cache data and per-packed-dim comp params. + // Fill key cache: each head dim h gets a column with 8 packed token bytes (16 tokens / 2) + // followed by [scale_inv (f16)][zp (f16)]. Within a packed byte, lo nibble = token 2t, hi = token 2t+1. for (size_t b = 0; b < blocks_num; b++) { - for (size_t p = 0; p < packed_k_head_size; p++) { - const float s0 = 0.10f + 0.01f * static_cast(p); - const float z0 = static_cast((static_cast(p) % 4) + 1); - const float s1 = 0.08f + 0.01f * static_cast(p); - const float z1 = static_cast((static_cast(p) % 5) + 2); - - const size_t comp_base = key_offset(b, 0, p, block_size, kv_heads, packed_k_head_size, adjusted_paged_attention_block_size); - write_fp16_bytes(key_cache_ref, comp_base + 0 * sizeof(ov::float16), ov::float16(s0)); - write_fp16_bytes(key_cache_ref, comp_base + 1 * sizeof(ov::float16), ov::float16(z0)); - write_fp16_bytes(key_cache_ref, comp_base + 2 * sizeof(ov::float16), ov::float16(s1)); - write_fp16_bytes(key_cache_ref, comp_base + 3 * sizeof(ov::float16), ov::float16(z1)); - - for (size_t t = 0; t < block_size; t++) { - const uint8_t q0 = static_cast((3 * t + p + b) & 0xF); - const uint8_t q1 = static_cast((2 * t + 2 * p + 1 + b) & 0xF); - key_cache_ref[key_offset(b, 0, p, t, kv_heads, packed_k_head_size, adjusted_paged_attention_block_size)] = - static_cast((q0 & 0xF) | ((q1 & 0xF) << 4)); + for (size_t h = 0; h < k_head_size; h++) { + const float scale_inv = 0.10f + 0.01f * static_cast(h); + const float zp = static_cast((static_cast(h) % 5) + 1); + + const size_t comp_base = key_offset(b, 0, h, packed_block_size, kv_heads, k_head_size, adjusted_paged_attention_block_size); + write_fp16_bytes(key_cache_ref, comp_base + 0 * sizeof(ov::float16), ov::float16(scale_inv)); + write_fp16_bytes(key_cache_ref, comp_base + 1 * sizeof(ov::float16), ov::float16(zp)); + + for (size_t byte_in_col = 0; byte_in_col < packed_block_size; byte_in_col++) { + const size_t t_lo = byte_in_col * 2; + const size_t t_hi = byte_in_col * 2 + 1; + const uint8_t q_lo = static_cast((3 * t_lo + h + b) & 0xF); + const uint8_t q_hi = static_cast((3 * t_hi + h + b) & 0xF); + key_cache_ref[key_offset(b, 0, h, byte_in_col, kv_heads, k_head_size, adjusted_paged_attention_block_size)] = + static_cast((q_lo & 0xF) | ((q_hi & 0xF) << 4)); } } } - // Fill value cache with packed BY_TOKEN style bytes and comp bytes. + // Fill value cache with u4 per-token-inline layout: each token row holds packed data + // followed by inline [scale][zp] (matches quantize_and_save_per_token, pitch == 1). for (size_t b = 0; b < blocks_num; b++) { for (size_t t = 0; t < block_size; t++) { for (size_t p = 0; p < packed_v_head_size; p++) { - value_cache_ref[value_data_offset_compressed(b, 0, t, p, kv_heads, packed_v_head_size, adjusted_v_head_size, block_size)] = + value_cache_ref[value_data_offset_int4_per_token(b, 0, t, p, kv_heads, adjusted_v_head_size, block_size)] = static_cast((7 * t + 5 * p + b) % 251); } const ov::float16 scale = ov::float16(0.125f * static_cast(1 + ((b + t) % 3))); const ov::float16 zp = ov::float16(static_cast((static_cast(t) % 7) - 3)); for (size_t byte = 0; byte < sizeof(ov::float16); byte++) { - const auto scale_off = value_comp_byte_offset(b, - 0, - t, - byte, - false, - kv_heads, - packed_v_head_size, - adjusted_v_head_size, - block_size); - const auto zp_off = value_comp_byte_offset(b, - 0, - t, - byte, - true, - kv_heads, - packed_v_head_size, - adjusted_v_head_size, - block_size); + const auto scale_off = + value_comp_byte_offset_int4_per_token(b, 0, t, byte, false, kv_heads, packed_v_head_size, adjusted_v_head_size, block_size); + const auto zp_off = value_comp_byte_offset_int4_per_token(b, 0, t, byte, true, kv_heads, packed_v_head_size, adjusted_v_head_size, block_size); value_cache_ref[scale_off] = reinterpret_cast(&scale)[byte]; value_cache_ref[zp_off] = reinterpret_cast(&zp)[byte]; } @@ -975,10 +971,13 @@ TEST(pa_kv_reorder_gpu, copy_between_blocks_single_sequence_compressed_u4_key_by set_values(block_indices_mem, {0, 1}); set_values(block_indices_begins_mem, {0, 2}); - set_values(block_update_indices_mem, { - 0, 17, - 15, 16, - }); + set_values(block_update_indices_mem, + { + 0, + 17, + 15, + 16, + }); set_values(block_update_indices_begins_mem, {0, 2}); topology topo; @@ -1020,72 +1019,60 @@ TEST(pa_kv_reorder_gpu, copy_between_blocks_single_sequence_compressed_u4_key_by cldnn::mem_lock key_ptr(key_cache_mem, network->get_stream()); cldnn::mem_lock value_ptr(value_cache_mem, network->get_stream()); - auto unpack_q_pair = [](uint8_t packed) { - return std::make_pair(static_cast(packed & 0xF), static_cast((packed >> 4) & 0xF)); + auto read_nibble_at_token = [&](const auto& buffer, size_t block, size_t h, size_t token) { + const size_t byte_off = key_offset(block, 0, h, token / 2, kv_heads, k_head_size, adjusted_paged_attention_block_size); + const uint8_t packed = buffer[byte_off]; + return (token % 2 == 0) ? static_cast(packed & 0xF) : static_cast((packed >> 4) & 0xF); }; - auto read_comp_output = [&](const cldnn::mem_lock& ptr, size_t block, size_t packed_k) { - const size_t comp_base = key_offset(block, 0, packed_k, block_size, kv_heads, packed_k_head_size, adjusted_paged_attention_block_size); - const float s0 = static_cast(read_fp16_from_byte_buffer(ptr, comp_base + 0 * sizeof(ov::float16))); - const float z0 = static_cast(read_fp16_from_byte_buffer(ptr, comp_base + 1 * sizeof(ov::float16))); - const float s1 = static_cast(read_fp16_from_byte_buffer(ptr, comp_base + 2 * sizeof(ov::float16))); - const float z1 = static_cast(read_fp16_from_byte_buffer(ptr, comp_base + 3 * sizeof(ov::float16))); - return std::make_tuple(s0, z0, s1, z1); + auto read_col_comp = [&](const auto& buffer, size_t block, size_t h) { + const size_t comp_base = key_offset(block, 0, h, packed_block_size, kv_heads, k_head_size, adjusted_paged_attention_block_size); + const float scale_inv = static_cast(read_fp16_from_byte_buffer(buffer, comp_base + 0 * sizeof(ov::float16))); + const float zp = static_cast(read_fp16_from_byte_buffer(buffer, comp_base + 1 * sizeof(ov::float16))); + return std::make_pair(scale_inv, zp); }; - auto read_comp_ref = [&](const std::vector& buffer, size_t block, size_t packed_k) { - const size_t comp_base = key_offset(block, 0, packed_k, block_size, kv_heads, packed_k_head_size, adjusted_paged_attention_block_size); - const float s0 = static_cast(read_fp16_from_u8_vector(buffer, comp_base + 0 * sizeof(ov::float16))); - const float z0 = static_cast(read_fp16_from_u8_vector(buffer, comp_base + 1 * sizeof(ov::float16))); - const float s1 = static_cast(read_fp16_from_u8_vector(buffer, comp_base + 2 * sizeof(ov::float16))); - const float z1 = static_cast(read_fp16_from_u8_vector(buffer, comp_base + 3 * sizeof(ov::float16))); - return std::make_tuple(s0, z0, s1, z1); + auto read_col_comp_ref = [&](const std::vector& buffer, size_t block, size_t h) { + const size_t comp_base = key_offset(block, 0, h, packed_block_size, kv_heads, k_head_size, adjusted_paged_attention_block_size); + const float scale_inv = static_cast(read_fp16_from_u8_vector(buffer, comp_base + 0 * sizeof(ov::float16))); + const float zp = static_cast(read_fp16_from_u8_vector(buffer, comp_base + 1 * sizeof(ov::float16))); + return std::make_pair(scale_inv, zp); }; - for (size_t p = 0; p < packed_k_head_size; p++) { - const uint8_t src_packed_0 = key_cache_ref[key_offset(0, 0, p, 0, kv_heads, packed_k_head_size, adjusted_paged_attention_block_size)]; - const uint8_t src_packed_15 = key_cache_ref[key_offset(0, 0, p, 15, kv_heads, packed_k_head_size, adjusted_paged_attention_block_size)]; - const auto [src_q0_0, src_q1_0] = unpack_q_pair(src_packed_0); - const auto [src_q0_15, src_q1_15] = unpack_q_pair(src_packed_15); - - const auto [src_s0, src_z0, src_s1, src_z1] = read_comp_ref(key_cache_ref, 0, p); - const auto [dst_s0, dst_z0, dst_s1, dst_z1] = read_comp_output(key_ptr, 1, p); - - const float src_val0_0 = (static_cast(src_q0_0) - src_z0) * src_s0; - const float src_val1_0 = (static_cast(src_q1_0) - src_z1) * src_s1; - const float src_val0_15 = (static_cast(src_q0_15) - src_z0) * src_s0; - const float src_val1_15 = (static_cast(src_q1_15) - src_z1) * src_s1; - - const uint8_t dst_packed_17 = key_ptr[key_offset(1, 0, p, 1, kv_heads, packed_k_head_size, adjusted_paged_attention_block_size)]; - const uint8_t dst_packed_16 = key_ptr[key_offset(1, 0, p, 0, kv_heads, packed_k_head_size, adjusted_paged_attention_block_size)]; - const auto [dst_q0_17, dst_q1_17] = unpack_q_pair(dst_packed_17); - const auto [dst_q0_16, dst_q1_16] = unpack_q_pair(dst_packed_16); - - const float dst_val0_17 = (static_cast(dst_q0_17) - dst_z0) * dst_s0; - const float dst_val1_17 = (static_cast(dst_q1_17) - dst_z1) * dst_s1; - const float dst_val0_16 = (static_cast(dst_q0_16) - dst_z0) * dst_s0; - const float dst_val1_16 = (static_cast(dst_q1_16) - dst_z1) * dst_s1; - - ASSERT_NEAR(dst_val0_17, src_val0_0, 1.0f); - ASSERT_NEAR(dst_val1_17, src_val1_0, 1.0f); - ASSERT_NEAR(dst_val0_16, src_val0_15, 1.0f); - ASSERT_NEAR(dst_val1_16, src_val1_15, 1.0f); + // Reorder maps src token 0 -> dst token 17 (block 1, slot 1) and src token 15 -> dst token 16 (block 1, slot 0). + // Compare in dequantized space because the dst column is requantized end-to-end on cross-block copies. + for (size_t h = 0; h < k_head_size; h++) { + const auto [src_scale_inv, src_zp] = read_col_comp_ref(key_cache_ref, 0, h); + const auto [dst_scale_inv, dst_zp] = read_col_comp(key_ptr, 1, h); + + const uint8_t src_q0 = read_nibble_at_token(key_cache_ref, 0, h, 0); + const uint8_t src_q15 = read_nibble_at_token(key_cache_ref, 0, h, 15); + const float src_val0 = (static_cast(src_q0) - src_zp) * src_scale_inv; + const float src_val15 = (static_cast(src_q15) - src_zp) * src_scale_inv; + + const uint8_t dst_q1 = read_nibble_at_token(key_ptr, 1, h, 1); + const uint8_t dst_q0 = read_nibble_at_token(key_ptr, 1, h, 0); + const float dst_val1 = (static_cast(dst_q1) - dst_zp) * dst_scale_inv; + const float dst_val0 = (static_cast(dst_q0) - dst_zp) * dst_scale_inv; + + ASSERT_NEAR(dst_val1, src_val0, 1.0f); + ASSERT_NEAR(dst_val0, src_val15, 1.0f); } for (size_t p = 0; p < packed_v_head_size; p++) { - const auto src0 = value_cache_ref[value_data_offset_compressed(0, 0, 0, p, kv_heads, packed_v_head_size, adjusted_v_head_size, block_size)]; - const auto dst17 = value_ptr[value_data_offset_compressed(1, 0, 1, p, kv_heads, packed_v_head_size, adjusted_v_head_size, block_size)]; + const auto src0 = value_cache_ref[value_data_offset_int4_per_token(0, 0, 0, p, kv_heads, adjusted_v_head_size, block_size)]; + const auto dst17 = value_ptr[value_data_offset_int4_per_token(1, 0, 1, p, kv_heads, adjusted_v_head_size, block_size)]; ASSERT_EQ(dst17, src0); - const auto src15 = value_cache_ref[value_data_offset_compressed(0, 0, 15, p, kv_heads, packed_v_head_size, adjusted_v_head_size, block_size)]; - const auto dst16 = value_ptr[value_data_offset_compressed(1, 0, 0, p, kv_heads, packed_v_head_size, adjusted_v_head_size, block_size)]; + const auto src15 = value_cache_ref[value_data_offset_int4_per_token(0, 0, 15, p, kv_heads, adjusted_v_head_size, block_size)]; + const auto dst16 = value_ptr[value_data_offset_int4_per_token(1, 0, 0, p, kv_heads, adjusted_v_head_size, block_size)]; ASSERT_EQ(dst16, src15); } for (size_t byte = 0; byte < sizeof(ov::float16); byte++) { - ASSERT_EQ(value_ptr[value_comp_byte_offset(1, 0, 1, byte, false, kv_heads, packed_v_head_size, adjusted_v_head_size, block_size)], - value_cache_ref[value_comp_byte_offset(0, 0, 0, byte, false, kv_heads, packed_v_head_size, adjusted_v_head_size, block_size)]); - ASSERT_EQ(value_ptr[value_comp_byte_offset(1, 0, 1, byte, true, kv_heads, packed_v_head_size, adjusted_v_head_size, block_size)], - value_cache_ref[value_comp_byte_offset(0, 0, 0, byte, true, kv_heads, packed_v_head_size, adjusted_v_head_size, block_size)]); + ASSERT_EQ(value_ptr[value_comp_byte_offset_int4_per_token(1, 0, 1, byte, false, kv_heads, packed_v_head_size, adjusted_v_head_size, block_size)], + value_cache_ref[value_comp_byte_offset_int4_per_token(0, 0, 0, byte, false, kv_heads, packed_v_head_size, adjusted_v_head_size, block_size)]); + ASSERT_EQ(value_ptr[value_comp_byte_offset_int4_per_token(1, 0, 1, byte, true, kv_heads, packed_v_head_size, adjusted_v_head_size, block_size)], + value_cache_ref[value_comp_byte_offset_int4_per_token(0, 0, 0, byte, true, kv_heads, packed_v_head_size, adjusted_v_head_size, block_size)]); } } diff --git a/src/plugins/intel_gpu/tests/unit/test_cases/paged_attention_gpu_test.cpp b/src/plugins/intel_gpu/tests/unit/test_cases/paged_attention_gpu_test.cpp index f1d37fc53734..719445cf8893 100644 --- a/src/plugins/intel_gpu/tests/unit/test_cases/paged_attention_gpu_test.cpp +++ b/src/plugins/intel_gpu/tests/unit/test_cases/paged_attention_gpu_test.cpp @@ -2,2305 +2,29 @@ // SPDX-License-Identifier: Apache-2.0 // -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include "openvino/runtime/properties.hpp" -#include "openvino/runtime/tensor.hpp" -#include "primitive_inst.h" -#include "random_generator.hpp" -#include "test_utils.h" - -#include - -using namespace cldnn; -using namespace ov::intel_gpu; -using namespace ::tests; - -/* - * PagedAttention inputs: - * [0]: query, shape: [batch_size_in_tokens, num_heads * head_size], type: f16 - * [1]: key, shape: [batch_size_in_tokens, num_kv_heads * head_size], type: f16 - * [2]: value, shape: [batch_size_in_tokens, num_kv_heads * head_size], type: f16 - * [3]: key_cache, shape: [num_blocks, num_kv_heads, head_size, block_size], type: f16 or i8 - * [4]: value_cache, shape: [num_blocks, num_kv_heads, block_size, head_size], type: f16 or i8 - * [5]: past_lens, shape: [batch_size_in_sequences], type: i32 - * [6]: subsequence_begins, shape: [batch_size_in_sequences + 1], type: i32 - * [7]: block_indices, shape: [num_blocks], type: i32 - * [8]: block_indices_begins, shape: [batch_size_in_sequences + 1], type: i32 - * [9]: scale, optional - * [10]: sliding_window, optional - * [11]: alibi_slopes, optional - * [12]: max_context_len, shape: [], type: i32 - * [13]: score_aggregation_window, optional, shape: [batch_size_in_sequences], type: i32 - * [14]: rotated_block_indices, optional, shape: [num_rotated_blocks], type: i32 - * [15]: rotation_deltas, optional, shape: [num_rotated_blocks, BLOCK_SIZE] or [num_rotated_blocks, 1], type: i32 - * [16]: rotation_trig_lut, optional, shape: [max_num_batched_tokens, head_size], type: f16 - * [17]: adaptive_rkv_start_size, optional, shape: [], type: i32 - * [18]: adaptive_rkv_evictable_sizes, optional, shape: [batch_size_in_sequences], type: i32 - * [19]: adaptive_rkv_diversity_block_set_indices, optional, shape: [total_blocks], type: i32 - * [20]: adaptive_rkv_diversity_block_set_indices_begins, optional, shape: [batch_size_in_sequences + 1], type: i32 - * [21]: qq_bias, optional, shape: [total_mask_size], type: u8 - * [22]: qq_bias_begins, optional, shape: [batch_size_in_sequences + 1], type: i32 - */ - -enum class ScoresMode { - DISABLED = 0, - LAST_TOKEN, - SNAPKV -}; - -struct SubsequenceDescriptor { - int num_tokens; - int past_len; -}; - -struct CacheRotationDescriptor { - bool apply_rotation; - // configures 2nd dimension of rotation_deltas - // if per_block is true, single value is used for all tokens inside the block - // otherwise, each token uses an independent value - bool per_block; -}; - -struct QueryToQueryAttentionDescriptor { - std::vector> qq_bias; - std::vector qq_bias_begins; -}; - -struct PagedAttentionManager { - int num_heads; - int num_kv_heads; - int k_head_size; - int v_head_size; - int block_size; - int sliding_window_size; - bool kv_cache_compression; - ov::internal::CacheQuantMode key_cache_quant_mode; - ov::element::Type kv_cache_precision = ov::element::dynamic; - bool has_score_aggregation; - CacheRotationDescriptor rotation_config; - std::vector subsequence_descs; - - // per-subsequence QKV inputs - std::vector> query_data; // {[1, num_tokens, num_heads, k_head_size], ..} - std::vector> key_data; // {[1, past_len + num_tokens, num_heads, k_head_size], ..} - std::vector> value_data; // {[1, past_len + num_tokens, num_heads, v_head_size], ..} - - // common PA inputs - std::vector past_lens; - std::vector subsequence_begins; - std::vector block_indices; - std::vector block_indices_begins; - std::vector max_context_len; - std::vector score_aggregation_window; - - // score aggregation related inputs - std::vector score_aggregation; - - // rotation related inputs - std::vector rotated_block_indices; - std::vector rotation_deltas; - std::vector rotation_trig_lut; - - // xattention related inputs - bool has_xattention; - std::vector xattention_threshold; - std::vector xattention_block_size; - std::vector xattention_stride; - - std::vector sinks; - - int adaptive_rkv_start_size = 0; - std::vector adaptive_rkv_evictable_sizes; - std::vector adaptive_rkv_diversity_block_set_indices; - std::vector adaptive_rkv_diversity_block_set_indices_begins; - - std::vector> qq_bias; - std::vector qq_bias_begins; - cldnn::engine& test_engine; - cldnn::stream& test_stream; - tests::random_generator& rg; - - PagedAttentionManager(tests::random_generator& rg, - cldnn::engine& engine, - cldnn::stream& stream, - const std::vector& subsequence_descs, - int num_heads, - int num_kv_heads, - int k_head_size, - int v_head_size, - int block_size, - int sliding_window_size, - bool kv_cache_compression, - ov::internal::CacheQuantMode key_cache_quant_mode, - bool has_score_aggregation, - bool has_xattention, - CacheRotationDescriptor rotation_config, - ov::element::Type kv_cache_precision = ov::element::dynamic) - : num_heads(num_heads) - , num_kv_heads(num_kv_heads) - , k_head_size(k_head_size) - , v_head_size(v_head_size) - , block_size(block_size) - , sliding_window_size(sliding_window_size) - , kv_cache_compression(kv_cache_compression) - , key_cache_quant_mode(key_cache_quant_mode) - , kv_cache_precision(kv_cache_precision) - , has_score_aggregation(has_score_aggregation) - , rotation_config(rotation_config) - , subsequence_descs(subsequence_descs) - , has_xattention(has_xattention) - , test_engine(engine) - , test_stream(stream) - , rg(rg) { - // init subsequence_begins and block_indices_begins - subsequence_begins.push_back(0); - block_indices_begins.push_back(0); - - int max_len = 0; - for (int i = 0; i < static_cast(subsequence_descs.size()); i++) { - const auto& subsequence_desc = subsequence_descs[i]; - max_len = std::max(max_len, subsequence_desc.num_tokens + subsequence_desc.past_len); - - query_data.push_back(generate_realistic_data(num_heads, subsequence_desc.num_tokens, k_head_size)); - key_data.push_back(generate_realistic_data(num_kv_heads, subsequence_desc.num_tokens + subsequence_desc.past_len, k_head_size)); - value_data.push_back(generate_realistic_data(num_kv_heads, subsequence_desc.num_tokens + subsequence_desc.past_len, v_head_size)); - - past_lens.push_back(subsequence_desc.past_len); - int subsequence_start_pos = subsequence_begins[i]; - int subsequence_end_pos = subsequence_start_pos + subsequence_desc.num_tokens; - subsequence_begins.push_back(subsequence_end_pos); - - int subsequence_length = subsequence_desc.num_tokens + subsequence_desc.past_len; - int required_blocks = ceil_div(subsequence_length, block_size); - int start_block_idx = block_indices.empty() ? 0 : block_indices.back() + 1; - int end_block_idx = start_block_idx + required_blocks; - for (int block_idx = start_block_idx; block_idx < end_block_idx; block_idx++) { - block_indices.push_back(block_idx); - } - - int block_indices_start_pos = block_indices_begins[i]; - int block_indices_end_pos = block_indices_start_pos + required_blocks; - block_indices_begins.push_back(block_indices_end_pos); - } - max_context_len.push_back(max_len); - - if (rotation_config.apply_rotation) { - // iterate over KV-cache blocks and apply cache rotation to every second - // fully occupied block - for (size_t i = 0; i < subsequence_descs.size(); i++) { - const auto& subsequence_desc = subsequence_descs[i]; - int past_len = subsequence_desc.past_len; - int start_block_idx = block_indices_begins[i]; - for (int block_idx = 1; block_idx < past_len / block_size; block_idx++) { - if (block_idx % 2 != 0) { - rotated_block_indices.push_back(start_block_idx + block_idx); - } - } - } - - if (!rotated_block_indices.empty()) { - rotation_deltas = generate_rotation_deltas_data(rg, - max_context_len[0], - rotated_block_indices.size(), - block_size, - rotation_config.per_block); - rotation_trig_lut = generate_rotation_trig_lut_data(rg, max_context_len[0], k_head_size); - } - } - - if (has_score_aggregation) { - for (const auto& subsequence_desc : subsequence_descs) { - const auto max_tokens = 10; - auto max_window_size = std::min(subsequence_desc.num_tokens, max_tokens); - auto window_size = rg.generate_random_val(1, max_window_size); - score_aggregation.push_back(window_size); - } - } - } - - memory::ptr get_query_memory() { - return get_QKV_memory(query_data, num_heads, k_head_size, false); - } - - memory::ptr get_key_memory() { - return get_QKV_memory(key_data, num_kv_heads, k_head_size, true); - } - - memory::ptr get_value_memory() { - return get_QKV_memory(value_data, num_kv_heads, v_head_size, true); - } - - memory::ptr get_key_cache_memory_cm() { - constexpr int kv_sub_block_size = 16; - auto key_cache_dt = kv_cache_compression ? data_types::i8 : data_types::f16; - const int head_size = k_head_size; - int adjusted_head_size = head_size; - int adjusted_block_size = block_size; - if (kv_cache_compression) { - if (key_cache_quant_mode == ov::internal::CacheQuantMode::BY_CHANNEL) { - OPENVINO_ASSERT(block_size % kv_sub_block_size == 0); - adjusted_block_size += block_size / kv_sub_block_size * 4; - } else { - adjusted_head_size += 4; - } - } - - const auto num_blocks = block_indices.back() + 1; - auto key_cache_shape = ov::PartialShape{static_cast(num_blocks), - static_cast(num_kv_heads), - static_cast(adjusted_block_size), - static_cast(adjusted_head_size)}; - auto key_cache_layout = layout{key_cache_shape, key_cache_dt, format::bfyx}; - auto memory = test_engine.allocate_memory(key_cache_layout); - - for (int i = 0; i < static_cast(subsequence_descs.size()); i++) { - const int past_len = subsequence_descs[i].past_len; - if (past_len == 0) - continue; - - const int blocks_num = ceil_div(past_len + 1, block_size); - const int start_block_idx = block_indices[block_indices_begins[i]]; - - for (int block_idx = 0; block_idx < blocks_num; block_idx++) { - const int last_token_idx = (block_idx == blocks_num - 1) ? (past_len - block_size * block_idx) : block_size; - - for (int token_idx = 0; token_idx < last_token_idx; token_idx++) { - for (int head_idx = 0; head_idx < num_kv_heads; head_idx++) { - const size_t input_token_offset = static_cast(block_idx) * block_size + token_idx; - ov::float16* src_ptr = - key_data[i].data() + input_token_offset * static_cast(num_kv_heads) * head_size + static_cast(head_idx) * head_size; - - if (!kv_cache_compression) { - const size_t base = (static_cast(start_block_idx + block_idx) * num_kv_heads * block_size * head_size) + - (static_cast(head_idx) * block_size * head_size); - const size_t off = base + static_cast(token_idx) * head_size; - set_values(test_stream, memory, src_ptr, head_size, off); - } else if (key_cache_quant_mode == ov::internal::CacheQuantMode::BY_TOKEN) { - // Compressed Key cache layout: - // logical shape: [num_blocks, num_kv_heads, block_size, adjusted_head_size], dt=i8 (adjusted_head_size=head_size+4). - // Per (block, head) region starts at block_base_i8, byte-packed as: - // data: block_base_i8 + t*head_size (u8 semantics), size=head_size bytes - // scale: scale_base_i8 + t*sizeof(fp16) (fp16), indexed as (scale_base_i8/2 + t) - // zp: zp_base_i8 + t*sizeof(fp16) (fp16), indexed as (zp_base_i8/2 + t) - // xattention quant: q∈[0..255], dequant x ≈ (q - zp) * scale, where scale=(max-min)/255, zp=(-min)*255/(max-min). - auto [qdata, scale, zp] = quantize_data(src_ptr, head_size, false, true); - int8_t* qptr = reinterpret_cast(qdata.data()); - - const size_t block_stride_i8 = static_cast(adjusted_head_size) * block_size; - const size_t block_base_i8 = (static_cast(start_block_idx + block_idx) * num_kv_heads + head_idx) * block_stride_i8; - - const size_t data_off_i8 = block_base_i8 + token_idx * head_size; - set_values(test_stream, memory, qptr, head_size, data_off_i8); - - const size_t scale_base_i8 = block_base_i8 + head_size * block_size; - const size_t zp_base_i8 = scale_base_i8 + block_size * sizeof(ov::float16); - - const size_t scale_off_f16 = scale_base_i8 / 2 + token_idx; - const size_t zp_off_f16 = zp_base_i8 / 2 + token_idx; - - set_values(test_stream, memory, &scale, 1, scale_off_f16); - set_values(test_stream, memory, &zp, 1, zp_off_f16); - } else { - // Compressed Key cache layout for BY_CHANNEL: - // shape: [num_blocks, num_kv_heads, adjusted_block_size, head_size], dt=i8. - // Per (block, head): - // data bytes region : [block_size * head_size] - // scale fp16 region per-subblock per-channel : [(block_size / sub_block) * head_size] - // zp fp16 region per-subblock per-channel : [(block_size / sub_block) * head_size] - const size_t block_stride_i8 = static_cast(adjusted_block_size) * static_cast(head_size); - const size_t block_base_i8 = (static_cast(start_block_idx + block_idx) * static_cast(num_kv_heads) + - static_cast(head_idx)) * - block_stride_i8; - - const int subblock_count = block_size / kv_sub_block_size; - const size_t scale_base_i8 = block_base_i8 + static_cast(block_size) * static_cast(head_size); - const size_t zp_base_i8 = scale_base_i8 + - static_cast(subblock_count) * static_cast(head_size) * sizeof(ov::float16); - - for (int channel = 0; channel < head_size; channel++) { - for (int sub_start = 0; sub_start < last_token_idx; sub_start += kv_sub_block_size) { - const int cur_sub_block_size = std::min(kv_sub_block_size, last_token_idx - sub_start); - std::vector token_block(cur_sub_block_size); - - for (int t = 0; t < cur_sub_block_size; t++) { - const size_t input_token_offset = static_cast(block_idx) * block_size + static_cast(sub_start + t); - token_block[t] = *(key_data[i].data() + input_token_offset * static_cast(num_kv_heads) * head_size + - static_cast(head_idx) * head_size + channel); - } - - auto [quantized_data, scale, zp] = quantize_data(token_block.data(), cur_sub_block_size, true, true); - - for (int t = 0; t < cur_sub_block_size; t++) { - const size_t data_off_i8 = block_base_i8 + static_cast(sub_start + t) * head_size + channel; - set_values(test_stream, memory, quantized_data.data() + t, 1, data_off_i8); - } - - const size_t sub_idx = static_cast(sub_start / kv_sub_block_size); - const size_t scale_off_f16 = scale_base_i8 / 2 + sub_idx * static_cast(head_size) + channel; - const size_t zp_off_f16 = zp_base_i8 / 2 + sub_idx * static_cast(head_size) + channel; - - set_values(test_stream, memory, &scale, 1, scale_off_f16); - set_values(test_stream, memory, &zp, 1, zp_off_f16); - } - } - } - } - } - } - } - return memory; - } - - bool is_int4_kv_cache() const { - return kv_cache_precision == ov::element::u4 || kv_cache_precision == ov::element::i4; - } - - memory::ptr get_key_cache_memory() { - auto key_cache_dt = data_types::f16; - auto adjusted_head_size = k_head_size; - auto adjusted_block_size = block_size; - if (kv_cache_compression) { - key_cache_dt = is_int4_kv_cache() ? data_types::u8 : data_types::i8; - const int scale_zp_bytes = is_int4_kv_cache() ? 8 : 4; - if (key_cache_quant_mode == ov::internal::CacheQuantMode::BY_CHANNEL) { - if (is_int4_kv_cache()) { - // u4/i4: stored as u8/i8 with head_size halved (2 u4 packed per byte). - // Scale/zp for BY_CHANNEL: 4 fp16 values = 8 bytes appended to block_size dim. - adjusted_head_size = k_head_size / 2; // pack 2 u4 into 1 u8 byte - } - adjusted_block_size += scale_zp_bytes; - } else { - if (is_int4_kv_cache()) { - // Scale/zp for BY_TOKEN: 2 fp16 values = 4 bytes appended to head_size dim. - adjusted_head_size = k_head_size / 2 + scale_zp_bytes; - } else { - adjusted_head_size += scale_zp_bytes; - } - } - } - - auto num_blocks = block_indices.back() + 1; - auto key_cache_shape = ov::PartialShape{ num_blocks, num_kv_heads, adjusted_head_size, adjusted_block_size }; - auto key_cache_layout = layout{ key_cache_shape, key_cache_dt, format::bfyx }; - auto memory = test_engine.allocate_memory(key_cache_layout); - for (int i = 0; i < static_cast(subsequence_descs.size()); i++) { - int past_len = subsequence_descs[i].past_len; - if (past_len != 0) { - int blocks_num = ceil_div(past_len + 1, block_size); - int start_block_idx = block_indices[block_indices_begins[i]]; - for (int block_idx = 0; block_idx < blocks_num; block_idx++) { - int last_token_idx = block_idx == blocks_num - 1 ? (past_len - block_size * block_idx) - : block_size; - // quantize by channel - if (kv_cache_compression && key_cache_quant_mode == ov::internal::CacheQuantMode::BY_CHANNEL) { - if (is_int4_kv_cache()) { - // INT4 BY_CHANNEL: packed layout [num_blocks, kv_heads, k_head_size/2, block_size+8] - // Each packed dim p corresponds to orig dims p*2 (even) and p*2+1 (odd). - // Each byte holds two u4 values: lower nibble = even dim, upper nibble = odd dim. - // Comp region at [block_size..block_size+7]: 4 fp16 = inv_scale0,zp0,inv_scale1,zp1 - const int packed_head_size = k_head_size / 2; - for (int head_idx = 0; head_idx < num_kv_heads; head_idx++) { - for (int p = 0; p < packed_head_size; p++) { - const int dim0 = p * 2; - const int dim1 = p * 2 + 1; - // Quantize both dims across tokens in this block - std::vector vals0(block_size, 0.f), vals1(block_size, 0.f); - for (int t = 0; t < last_token_idx; ++t) { - size_t in_off = (static_cast(block_idx) * block_size + t) * num_kv_heads * k_head_size - + head_idx * k_head_size; - vals0[t] = static_cast(key_data[i].data()[in_off + dim0]); - vals1[t] = static_cast(key_data[i].data()[in_off + dim1]); - } - auto quantize_u4 = [&](const std::vector& vals, int n_vals) { - float min_v = vals[0], max_v = vals[0]; - for (int t = 1; t < n_vals; ++t) { - min_v = std::min(min_v, vals[t]); - max_v = std::max(max_v, vals[t]); - } - float range = (max_v == min_v) ? 0.001f : (max_v - min_v); - const float min_range = std::abs(max_v) * 0.1f; - if (range <= min_range) range += std::max(1.0f, min_range); - float scale = 15.0f / range; - float zp = -min_v * scale; - std::vector q(n_vals); - for (int t = 0; t < n_vals; ++t) { - int v = static_cast(std::nearbyint(vals[t] * scale + zp)); - v = std::max(0, std::min(15, v)); - q[t] = static_cast(v); - } - return std::make_tuple(q, scale, zp); - }; - auto [q0, scale0, zp_val0] = quantize_u4(vals0, last_token_idx); - auto [q1, scale1, zp_val1] = quantize_u4(vals1, last_token_idx); - - const size_t output_block_offset = - static_cast(start_block_idx + block_idx) * num_kv_heads * packed_head_size * adjusted_block_size - + head_idx * packed_head_size * adjusted_block_size; - const size_t output_offset = output_block_offset + p * adjusted_block_size; - - // Pack and write u4 data - std::vector packed(last_token_idx); - for (int t = 0; t < last_token_idx; ++t) - packed[t] = static_cast((q0[t] & 0xFu) | ((q1[t] & 0xFu) << 4)); - set_values(test_stream, memory, packed.data(), static_cast(last_token_idx), output_offset); - - // Write 4 fp16 comp values: inv_scale0, zp0, inv_scale1, zp1 - // Comp region starts at output_offset + block_size (in u8 elements) - // Accessed via fp16 lock: divide u8 offset by 2 to get fp16 index - const size_t comp_offset_fp16 = (output_offset + block_size) / 2; - ov::float16 inv_scale0 = static_cast(1.0f / scale0); - ov::float16 fp16_zp0 = static_cast(zp_val0); - ov::float16 inv_scale1 = static_cast(1.0f / scale1); - ov::float16 fp16_zp1 = static_cast(zp_val1); - set_values(test_stream, memory, &inv_scale0, 1, comp_offset_fp16 + 0); - set_values(test_stream, memory, &fp16_zp0, 1, comp_offset_fp16 + 1); - set_values(test_stream, memory, &inv_scale1, 1, comp_offset_fp16 + 2); - set_values(test_stream, memory, &fp16_zp1, 1, comp_offset_fp16 + 3); - } - } - } else { - for (int head_idx = 0; head_idx < num_kv_heads; head_idx++) { - for (int k_head_size_idx = 0; k_head_size_idx < k_head_size; k_head_size_idx++) { - std::vector token_block(block_size); - for (int token_idx = 0; token_idx < last_token_idx; ++token_idx) { - size_t input_token_offset = block_idx * block_size + token_idx; - token_block[token_idx] = *(key_data[i].data() + input_token_offset * num_kv_heads * k_head_size + head_idx * k_head_size + k_head_size_idx); - } - auto [quantized_data, scale, zp] = quantize_data(token_block.data(), last_token_idx, true); - size_t output_block_offset = (start_block_idx + block_idx) * num_kv_heads * adjusted_head_size * adjusted_block_size + - head_idx * adjusted_head_size * adjusted_block_size; - size_t output_offset = output_block_offset + - k_head_size_idx * adjusted_block_size; - set_values(test_stream, memory, quantized_data.data(), last_token_idx, output_offset); - size_t comp_offset = (output_offset + block_size)/2; - set_values(test_stream, memory, &scale, 1, comp_offset); - set_values(test_stream, memory, &zp, 1, comp_offset + 1); - } - } - } - } - for (int token_idx = 0; token_idx < last_token_idx; token_idx++) { - for (int head_idx = 0; head_idx < num_kv_heads; head_idx++) { - if (kv_cache_compression) { - if (key_cache_quant_mode == ov::internal::CacheQuantMode::BY_TOKEN) { - // quantize by token - size_t input_token_offset = block_idx * block_size + token_idx; - ov::float16* data_ptr = key_data[i].data() + - input_token_offset * num_kv_heads * k_head_size + - head_idx * k_head_size; - // shape: [num_blocks, num_kv_heads, adjusted_head_size, block_size] - size_t output_block_offset = (start_block_idx + block_idx) * num_kv_heads * adjusted_head_size * block_size + - head_idx * adjusted_head_size * block_size; - - if (is_int4_kv_cache()) { - // INT4 BY_TOKEN: [num_blocks, kv_heads, k_head_size/2+8, block_size] u8 - // Kernel packing (SUBGROUP_SIZE=16): - // Y = pack_group*16 + sglid, where pack_group = d/(2*16), sglid = d%16 - // lower 4bit = q(dim[pack_group*32+sglid]) - // upper 4bit = q(dim[pack_group*32+sglid+16]) - // Scale/ZP: fp16 in comp region at Y=packed_head_size.. - // inv_scale[t] at fp16 idx (comp_base/2 + t) - // zp[t] at fp16 idx (comp_base/2 + block_size + t) - const int packed_head_size = k_head_size / 2; - constexpr int SG = 16; // SUBGROUP_SIZE - // Compute per-token min/max, then scale/zp in u4 range [0,15] - float min_v = std::numeric_limits::max(); - float max_v = -std::numeric_limits::max(); - for (int d = 0; d < k_head_size; d++) { - float v = static_cast(data_ptr[d]); - min_v = std::min(min_v, v); - max_v = std::max(max_v, v); - } - float range = (max_v == min_v) ? 0.001f : (max_v - min_v); - const float min_range = std::abs(max_v) * 0.1f; - if (range <= min_range) range += std::max(1.0f, min_range); - float token_scale = 15.0f / range; - float token_zp = -min_v * token_scale; - std::vector q(k_head_size); - for (int d = 0; d < k_head_size; d++) { - int v = static_cast(std::nearbyint(static_cast(data_ptr[d]) * token_scale + token_zp)); - q[d] = static_cast(std::max(0, std::min(15, v))); - } - // Pack and write: Y=pack_group*SG+sglid → (lower=q[d0], upper=q[d1]) - for (int y = 0; y < packed_head_size; y++) { - int sglid_val = y % SG; - int pack_group = y / SG; - int d0 = pack_group * 2 * SG + sglid_val; - int d1 = d0 + SG; - uint8_t packed_byte = q[d0] & 0xFu; - if (d1 < k_head_size) - packed_byte |= (q[d1] & 0xFu) << 4; - size_t offset = output_block_offset + static_cast(y) * block_size + token_idx; - set_values(test_stream, memory, &packed_byte, 1, offset); - } - // Write inv_scale and zp as fp16 in the comp region - size_t comp_offset_fp16 = (output_block_offset + static_cast(packed_head_size) * block_size) / 2; - ov::float16 fp16_inv_scale = static_cast(1.0f / token_scale); - ov::float16 fp16_zp = static_cast(token_zp); - set_values(test_stream, memory, &fp16_inv_scale, 1, comp_offset_fp16 + token_idx); - set_values(test_stream, memory, &fp16_zp, 1, comp_offset_fp16 + block_size + token_idx); - } else { - auto [quantized_data, scale, zp] = quantize_data(data_ptr, k_head_size); - for (int k_head_size_idx = 0; k_head_size_idx < k_head_size; k_head_size_idx++) { - auto quantized_data_ptr = quantized_data.data() + k_head_size_idx; - - size_t output_offset = output_block_offset + - k_head_size_idx * block_size + - token_idx; - - set_values(test_stream, memory, quantized_data_ptr, 1, output_offset); - } - size_t comp_offset = (output_block_offset + k_head_size * block_size) / 2; - set_values(test_stream, memory, &scale, 1, comp_offset + token_idx); - set_values(test_stream, memory, &zp, 1, comp_offset + block_size + token_idx); - } - } - } else { - for (int k_head_size_idx = 0; k_head_size_idx < k_head_size; k_head_size_idx++) { - size_t input_token_offset = block_idx * block_size + token_idx; - ov::float16* data_ptr = key_data[i].data() + - input_token_offset * num_kv_heads * k_head_size + - head_idx * k_head_size + k_head_size_idx; - - // shape: [num_blocks, num_kv_heads, k_head_size, block_size] - size_t output_offset = (start_block_idx + block_idx) * num_kv_heads * k_head_size * block_size + - head_idx * k_head_size * block_size + - k_head_size_idx * block_size + - token_idx; - - set_values(test_stream, memory, data_ptr, 1, output_offset); - } - } - } - } - } - } - } - - return memory; - } - - memory::ptr get_value_cache_memory() { - auto value_cache_dt = data_types::f16; - const int head_size = v_head_size; - int scale_zp_bytes = 0; - if (kv_cache_compression) { - value_cache_dt = is_int4_kv_cache() ? data_types::u8 : data_types::i8; - scale_zp_bytes = is_int4_kv_cache() ? 8 : 4; - } - - // For u4 (INT4), values are packed 2 per byte; the physical head size is halved. - // PACKED_ADJUSTED_V_HEAD_SIZE = v_head_size/2 + scales_zp_size = 64 + 8 = 72. - const int adjusted_head_size = is_int4_kv_cache() ? (head_size / 2 + scale_zp_bytes) : (head_size + scale_zp_bytes); - - const auto num_blocks = block_indices.back() + 1; - auto value_cache_shape = ov::PartialShape{static_cast(num_blocks), - static_cast(num_kv_heads), - static_cast(block_size), - static_cast(adjusted_head_size)}; - auto value_cache_layout = layout{value_cache_shape, value_cache_dt, format::bfyx}; - auto memory = test_engine.allocate_memory(value_cache_layout); - - for (int i = 0; i < static_cast(subsequence_descs.size()); i++) { - const int past_len = subsequence_descs[i].past_len; - if (past_len == 0) - continue; - - const int blocks_num = ceil_div(past_len + 1, block_size); - const int start_block_idx = block_indices[block_indices_begins[i]]; - - for (int block_idx = 0; block_idx < blocks_num; block_idx++) { - const int last_token_idx = (block_idx == blocks_num - 1) ? (past_len - block_size * block_idx) : block_size; - - for (int token_idx = 0; token_idx < last_token_idx; token_idx++) { - for (int head_idx = 0; head_idx < num_kv_heads; head_idx++) { - const size_t input_token_offset = static_cast(block_idx) * block_size + token_idx; - - ov::float16* src_ptr = value_data[i].data() + input_token_offset * static_cast(num_kv_heads) * head_size + - static_cast(head_idx) * head_size; - - if (!kv_cache_compression) { - const size_t base = (static_cast(start_block_idx + block_idx) * static_cast(num_kv_heads) * - static_cast(block_size) * static_cast(head_size)) + - (static_cast(head_idx) * static_cast(block_size) * static_cast(head_size)); - const size_t off = base + static_cast(token_idx) * static_cast(head_size); - set_values(test_stream, memory, src_ptr, head_size, off); - } else if (is_int4_kv_cache()) { - // INT4 (u4) BY_TOKEN value cache layout: - // [num_blocks, kv_heads, block_size, PACKED_ADJUSTED_V_HEAD_SIZE] u8 - // where PACKED_ADJUSTED_V_HEAD_SIZE = v_head_size/2 + scales_zp_size = 64 + 8 = 72. - // Data: block_base + token * packed_head_size + p (p = 0..packed_head_size-1) - // Each byte packs pair: byte = (q_even_group & 0xF) | ((q_odd_group & 0xF) << 4) - // Even group g*2 and odd group g*2+1 of 16 dims each → packed group g of 16 bytes. - // Comp: block_base + packed_head_size * block_size bytes: - // Scale (inv_scale = 1.0/scale): fp16[token] - // ZP: fp16[block_size + token] - const int packed_head_size = head_size / 2; // = 64 - - // Quantize entire token: one scale/zp for all head dims (BY_TOKEN) - float min_val = std::numeric_limits::max(); - float max_val = std::numeric_limits::lowest(); - for (int d = 0; d < head_size; d++) { - float v = static_cast(src_ptr[d]); - min_val = std::min(min_val, v); - max_val = std::max(max_val, v); - } - float diff = (max_val == min_val) ? 0.001f : (max_val - min_val); - // Expand range if too small (matches kernel logic) - float min_range = std::abs(max_val * 0.1f); - if (diff <= min_range) - diff += std::max(1.0f, min_range); - float scale_val = 15.0f / diff; // INT4_RANGE = 15 for u4 - float zp_val = -min_val * scale_val; - ov::float16 inv_scale_fp16 = ov::float16(1.0f / scale_val); - ov::float16 zp_fp16 = ov::float16(zp_val); - - const size_t block_stride = static_cast(adjusted_head_size) * static_cast(block_size); - const size_t block_base = - (static_cast(start_block_idx + block_idx) * static_cast(num_kv_heads) + static_cast(head_idx)) * - block_stride; - - // Pack pairs of groups: group (g*2) and (g*2+1), each 16 dims wide. - // Packed byte = (q0 & 0xF) | ((q1 & 0xF) << 4) - // Written at: block_base + token * packed_head_size + p, using u8 element offsets. - const int num_packed_groups = packed_head_size / 16; // = 4 - for (int g = 0; g < num_packed_groups; g++) { - for (int lane = 0; lane < 16; lane++) { - int dim_even = (g * 2) * 16 + lane; - int dim_odd = (g * 2 + 1) * 16 + lane; - int q0 = static_cast(std::nearbyint(static_cast(src_ptr[dim_even]) * scale_val + zp_val)); - int q1 = static_cast(std::nearbyint(static_cast(src_ptr[dim_odd]) * scale_val + zp_val)); - q0 = std::max(0, std::min(15, q0)); - q1 = std::max(0, std::min(15, q1)); - uint8_t packed_byte = static_cast((q0 & 0xFu) | (static_cast(q1 & 0xFu) << 4)); - const size_t packed_pos = g * 16 + lane; - const size_t byte_off = block_base + static_cast(token_idx) * packed_head_size + packed_pos; - // set_values for u8 memory: element index = byte offset - set_values(test_stream, memory, &packed_byte, 1, byte_off); - } - } - - // Write scale and zp into comp region - const size_t scale_base_bytes = block_base + static_cast(packed_head_size) * static_cast(block_size); - const size_t zp_base_bytes = scale_base_bytes + static_cast(block_size) * sizeof(ov::float16); - const size_t scale_off_f16 = (scale_base_bytes >> 1) + static_cast(token_idx); - const size_t zp_off_f16 = (zp_base_bytes >> 1) + static_cast(token_idx); - set_values(test_stream, memory, &inv_scale_fp16, 1, scale_off_f16); - set_values(test_stream, memory, &zp_fp16, 1, zp_off_f16); - } else { - // Compressed Value cache layout: - // logical shape: [num_blocks, num_kv_heads, block_size, adjusted_head_size], dt=i8 (adjusted_head_size=head_size+4). - // Per (block, head): data at block_base_i8 + t*head_size; scale/zp are fp16 arrays at scale_base_i8/zp_base_i8 - // (fp16 element offsets: scale_base_i8/2 + t, zp_base_i8/2 + t). - // has_xattention uses unsigned [0..255] quant; dequant x ≈ (q - zp) * scale, scale=(max-min)/255, zp=(-min)*255/(max-min). - auto [qdata, scale, zp] = quantize_data(src_ptr, head_size, false, has_xattention); - int8_t* qptr = reinterpret_cast(qdata.data()); - - const size_t block_stride_i8 = static_cast(adjusted_head_size) * static_cast(block_size); - const size_t block_base_i8 = - (static_cast(start_block_idx + block_idx) * static_cast(num_kv_heads) + static_cast(head_idx)) * - block_stride_i8; - - const size_t data_off_i8 = block_base_i8 + static_cast(token_idx) * static_cast(head_size); - set_values(test_stream, memory, qptr, head_size, data_off_i8); - - const size_t scale_base_i8 = block_base_i8 + static_cast(head_size) * static_cast(block_size); - const size_t zp_base_i8 = scale_base_i8 + static_cast(block_size) * sizeof(ov::float16); - - const size_t scale_off_f16 = (scale_base_i8 >> 1) + static_cast(token_idx); - const size_t zp_off_f16 = (zp_base_i8 >> 1) + static_cast(token_idx); - - set_values(test_stream, memory, &scale, 1, scale_off_f16); - set_values(test_stream, memory, &zp, 1, zp_off_f16); - } - } - } - } - } - return memory; - } - - memory::ptr get_past_lens_memory() { - return get_memory_from_vec(past_lens); - } - - memory::ptr get_subsequence_begins_memory() { - return get_memory_from_vec(subsequence_begins); - } - - memory::ptr get_block_indices_memory() { - return get_memory_from_vec(block_indices); - } - - memory::ptr get_block_indices_begins_memory() { - return get_memory_from_vec(block_indices_begins); - } - - memory::ptr get_scale_memory() { - std::vector scale = { ov::float16(get_default_scale()) }; - return get_memory_from_vec(scale); - } - - memory::ptr get_sliding_window_memory() { - std::vector sliding_window = { 0 }; - return get_memory_from_vec(sliding_window); - } - - memory::ptr get_alibi_memory() { - std::vector alibi; - return get_memory_from_vec(alibi); - } - - memory::ptr get_max_context_len_memory() { - return get_memory_from_vec(max_context_len); - } - - memory::ptr get_score_aggregation() { - return get_memory_from_vec(score_aggregation); - } - - memory::ptr get_rotated_block_indices_memory() { - return get_memory_from_vec(rotated_block_indices); - } - - memory::ptr get_rotation_deltas_memory() { - auto mem = get_memory_from_vec(rotation_deltas); - auto layout = mem->get_layout(); - auto last_dim = rotation_config.per_block ? 1 : block_size; - layout.set_partial_shape(ov::PartialShape{ static_cast(rotated_block_indices.size()), last_dim }); - - return test_engine.reinterpret_buffer(*mem, layout); - } - - memory::ptr get_rotation_trig_lut_memory() { - auto mem = get_memory_from_vec(rotation_trig_lut); - auto layout = mem->get_layout(); - layout.set_partial_shape(ov::PartialShape{ max_context_len[0], k_head_size }); - - if (rotated_block_indices.empty()) { - auto empty_layout = mem->get_layout(); - empty_layout.set_partial_shape(ov::PartialShape{ 0, k_head_size }); - return test_engine.reinterpret_buffer(*mem, empty_layout); - } - - return test_engine.reinterpret_buffer(*mem, layout); - } - - memory::ptr get_xattention_threshold_memory() { - return get_memory_from_vec(xattention_threshold); - } - - memory::ptr get_xattention_block_size_memory() { - return get_memory_from_vec(xattention_block_size); - } - - memory::ptr get_xattention_stride_memory() { - return get_memory_from_vec(xattention_stride); - } - - memory::ptr get_sinks_memory() { - auto mem = get_memory_from_vec(sinks); - auto layout = mem->get_layout(); - layout.set_partial_shape(ov::PartialShape{ 1, num_heads, 1, 1 }); - - if (sinks.empty()) { - auto empty_layout = mem->get_layout(); - empty_layout.set_partial_shape(ov::PartialShape{ 0, 0, 0, 0 }); - return test_engine.reinterpret_buffer(*mem, empty_layout); - } - - return test_engine.reinterpret_buffer(*mem, layout); - } - - memory::ptr get_adaptive_rkv_start_size_memory() { - auto mem = test_engine.allocate_memory({{}, data_types::i32, format::bfyx}); - mem_lock lock(mem, test_stream); - lock[0] = adaptive_rkv_start_size; - return mem; - } - - memory::ptr get_adaptive_rkv_evictable_sizes_memory() { - return get_memory_from_vec(adaptive_rkv_evictable_sizes); - } - - memory::ptr get_adaptive_rkv_diversity_block_set_indices_memory() { - return get_memory_from_vec(adaptive_rkv_diversity_block_set_indices); - } - - memory::ptr get_adaptive_rkv_diversity_block_set_indices_begins_memory() { - return get_memory_from_vec(adaptive_rkv_diversity_block_set_indices_begins); - } - - memory::ptr get_token_type_ids_memory() { - std::vector token_type_ids = { 0 }; - return get_memory_from_vec(token_type_ids); - } - - memory::ptr get_qq_bias_memory() { - std::vector flat_qq_bias; - for (const auto& matrix : qq_bias) { - for (bool val : matrix) { - flat_qq_bias.push_back(static_cast(val)); - } - } - return get_memory_from_vec(flat_qq_bias); - } - - memory::ptr get_qq_bias_begins_memory() { - return get_memory_from_vec(qq_bias_begins); - } - - float get_default_scale() { - return static_cast(1.f / std::sqrt(k_head_size)); - } - -private: - template - memory::ptr get_memory_from_vec(std::vector& input_data) { - auto data_size = input_data.empty() ? 1 : input_data.size(); - auto shape = ov::PartialShape{ static_cast(data_size) }; - auto layout = cldnn::layout{ shape, ov::element::from(), format::bfyx }; - auto memory = test_engine.allocate_memory(layout); - - if (input_data.empty()) { - auto shape = ov::PartialShape{0}; - auto layout = cldnn::layout{ shape, ov::element::from(), format::bfyx }; - return test_engine.reinterpret_buffer(*memory, layout); - } - - set_values(test_stream, memory, input_data.data(), input_data.size(), 0); - - return memory; - } - - memory::ptr get_QKV_memory(std::vector>& input_data, int num_heads, int head_size, bool skip_past_len) { - int total_tokens = 0; - for (const auto& subsequence_desc : subsequence_descs) - total_tokens += subsequence_desc.num_tokens; - - auto query_shape = ov::PartialShape{ total_tokens, num_heads * head_size }; - auto query_layout = layout{ query_shape, data_types::f16, format::bfyx }; - auto memory = test_engine.allocate_memory(query_layout); - - for (int subsequence_idx = 0; subsequence_idx < static_cast(subsequence_descs.size()); subsequence_idx++) { - for (int token_idx = 0; token_idx < subsequence_descs[subsequence_idx].num_tokens; token_idx++) { - for (int head_idx = 0; head_idx < num_heads; head_idx++) { - size_t input_token_offset = token_idx; - // as generated data stored in vectors includes past_len, ignore it for KV inputs - if (skip_past_len) - input_token_offset += subsequence_descs[subsequence_idx].past_len; - - ov::float16* data_ptr = input_data[subsequence_idx].data() + - input_token_offset * num_heads * head_size + - head_idx * head_size; - - size_t output_token_offset = subsequence_begins[subsequence_idx] + token_idx; - size_t output_offset = output_token_offset * num_heads * head_size + - head_idx * head_size; - - set_values(test_stream, memory, data_ptr, head_size, output_offset); - } - } - } - - return memory; - } - - template - static void set_values(stream& stream, memory::ptr mem, T* vals, size_t size, size_t dst_offset) { - mem_lock mem_ptr(mem, stream); - for (size_t i = 0; i < size; i++) { - mem_ptr[dst_offset + i] = vals[i]; - } - } - - static std::vector generate_input_data(tests::random_generator& rg, size_t num_heads, size_t tokens_num, size_t k_head_size) { - const size_t total_elements_num = tokens_num * num_heads * k_head_size; - auto data = rg.generate_random_1d(total_elements_num, -1, 1); - - return data; - } - - static std::vector generate_realistic_data(size_t num_heads, size_t tokens_num, size_t k_head_size) { - std::vector data(num_heads * tokens_num * k_head_size); - - std::mt19937 gen(1234); - std::normal_distribution dist(0.0f, 0.1f); - - for (size_t h = 0; h < num_heads; ++h) { - for (size_t t = 0; t < tokens_num; ++t) { - for (size_t d = 0; d < k_head_size; ++d) { - float val = dist(gen); - if (t > 0) - val = 0.8f * val + 0.2f * static_cast(data[h * tokens_num * k_head_size + (t - 1) * k_head_size + d]); - data[h * tokens_num * k_head_size + t * k_head_size + d] = static_cast(val); - } - } - } - - return data; - } - - static std::vector generate_rotation_deltas_data(tests::random_generator& rg, size_t max_tokens_num, size_t rotated_blocks_num, size_t block_size, bool per_block) { - const size_t total_elements_num = per_block ? rotated_blocks_num - : rotated_blocks_num * block_size; - auto data = rg.generate_random_1d(total_elements_num, 0, static_cast(max_tokens_num - 1)); - - return data; - } - - static std::vector generate_rotation_trig_lut_data(tests::random_generator& rg, size_t max_tokens_num, size_t k_head_size) { - const size_t total_elements_num = max_tokens_num * k_head_size; - auto data = rg.generate_random_1d(total_elements_num, -1, 1); - - return data; - } - - static std::tuple, ov::float16, ov::float16> quantize_data(ov::float16* data, - size_t size, - bool expand_range = false, - bool has_xattention = false) { - float min_value = std::numeric_limits::max(); - float max_value = std::numeric_limits::lowest(); - - for (size_t i = 0; i < size; i++) { - float v = static_cast(data[i]); - min_value = std::min(min_value, v); - max_value = std::max(max_value, v); - } - - if (has_xattention) { - if (max_value == min_value) { - std::vector qdata(size, 0); - return {qdata, ov::float16(0.0f), ov::float16(min_value)}; - } - - float diff_value = max_value - min_value; - if (expand_range && std::abs(diff_value) <= std::abs(max_value) * 0.1f) { - diff_value = (max_value - min_value) + std::max(1.0f, max_value * 0.1f); - } - - float scale_val = 255.0f / diff_value; - float zp_val = -min_value * scale_val; - - std::vector qdata(size); - for (size_t i = 0; i < size; i++) { - float q = data[i] * scale_val + zp_val; - int v = static_cast(std::nearbyint(q)); - if (v < 0) - v = 0; - if (v > 255) - v = 255; - qdata[i] = static_cast(v); - } - - ov::float16 scale = static_cast(diff_value / 255.0f); - ov::float16 zp = static_cast(zp_val); - return {qdata, scale, zp}; - } - - float diff_value = 0.001f; - if (max_value != min_value) - diff_value = max_value - min_value; - if (expand_range && std::abs(diff_value) <= std::abs(max_value) * 0.1f) { - diff_value = (max_value - min_value) + std::max(1.0f, max_value * 0.1f); - } - - float scale = (std::numeric_limits::max() - std::numeric_limits::lowest()) / diff_value; - float zp = -min_value * scale + std::numeric_limits::lowest(); - - std::vector qdata(size); - auto convert_char_rte = [](float val) { - float rounded = std::nearbyint(val); - if (rounded > 127.0f) - return static_cast(127); - if (rounded < -128.0f) - return static_cast(-128); - return static_cast(rounded); - }; - - for (size_t i = 0; i < size; i++) { - qdata[i] = convert_char_rte(data[i] * scale + zp); - } - - ov::float16 scale_out = static_cast(1.0f / scale); - ov::float16 zp_out = static_cast(zp); - return {qdata, scale_out, zp_out}; - } -}; - -namespace std { -template <> - struct hash { - uint64_t operator()(const ov::float16 __val) const { - return std::hash()(__val); - } -}; -} - -struct PagedAttentionReference { - PagedAttentionReference(PagedAttentionManager& pam) - : pam(pam) - , test_engine(pam.test_engine) - , test_stream(pam.test_stream) {} - - std::tuple, std::vector, std::vector> get_reference(memory::ptr key_cache_mem = nullptr) { - const bool has_xattention = pam.has_xattention; - if (has_xattention) { - const size_t total_iterations = pam.subsequence_descs.size(); - if (pam.xattention_threshold.size() != total_iterations) { - OPENVINO_THROW("xattention_threshold size (", pam.xattention_threshold.size(), - ") must match number of subsequences (", total_iterations, ")"); - } - if (pam.xattention_block_size.size() != total_iterations) { - OPENVINO_THROW("xattention_block_size size (", pam.xattention_block_size.size(), - ") must match number of subsequences (", total_iterations, ")"); - } - } - - std::vector ref_data_output; - std::vector ref_scores_output; - std::vector ref_diversity_output; - size_t qq_bias_offset = 0; - for (size_t i = 0; i < pam.subsequence_descs.size(); i++) { - const auto& subsequence_desc = pam.subsequence_descs[i]; - const auto kv_seq_len = subsequence_desc.num_tokens + subsequence_desc.past_len; - - auto key_data = pam.key_data[i]; - if (pam.rotation_config.apply_rotation) { - auto blocks_start = pam.block_indices_begins[i]; - auto blocks_end = pam.block_indices_begins[i + 1]; - - std::vector block_indices(pam.block_indices.begin() + blocks_start, - pam.block_indices.begin() + blocks_end); - - for (const auto& block_idx : block_indices) { - auto it = std::find(pam.rotated_block_indices.begin(), pam.rotated_block_indices.end(), block_idx); - if (it != pam.rotated_block_indices.end()) { - int index = std::distance(pam.rotated_block_indices.begin(), it); - int subsequence_rotated_block_idx = *it - blocks_start; - - rotate_block(key_data, - pam.rotation_deltas, - pam.rotation_trig_lut, - index, - subsequence_rotated_block_idx, - pam.num_kv_heads, - pam.k_head_size, - pam.block_size, - pam.rotation_config.per_block); - } - } - } - - auto window_size = pam.has_score_aggregation ? pam.score_aggregation[i] : 1; - - const std::vector* qq_bias_ptr = nullptr; - if (pam.qq_bias.size() > 0 && pam.subsequence_descs[i].past_len != 0) { - qq_bias_ptr = &pam.qq_bias[qq_bias_offset++]; - } - - double xattn_threshold = 1.0; - size_t xattn_block_size = 128; - if (has_xattention) { - xattn_threshold = static_cast(pam.xattention_threshold[i]); - - // reference path reflects runtime fallback/validation behavior for block size. - if (test_engine.get_device_info().arch < gpu_arch::xe2) { - xattn_block_size = 128; - } else { - const int user_value = pam.xattention_block_size[i]; - xattn_block_size = (user_value == 128 || user_value == 256) ? static_cast(user_value) : 256; - } - } - - auto subsequence_ref_results = run_reference(has_xattention, - pam.query_data[i], - key_data, - pam.value_data[i], - subsequence_desc.num_tokens, - kv_seq_len, - pam.num_heads, - pam.num_kv_heads, - pam.k_head_size, - pam.v_head_size, - window_size, - pam.sliding_window_size, - pam.get_default_scale(), - xattn_threshold, - xattn_block_size, - qq_bias_ptr); - - // concatenate all subsequences into one vector - ref_data_output.insert(ref_data_output.end(), - subsequence_ref_results.first.begin(), - subsequence_ref_results.first.end()); - ref_scores_output.insert(ref_scores_output.end(), - subsequence_ref_results.second.begin(), - subsequence_ref_results.second.end()); - } - - if (!pam.adaptive_rkv_evictable_sizes.empty()) { - ref_diversity_output = compute_diversity_reference(key_cache_mem); - } - - return { ref_data_output, ref_scores_output, ref_diversity_output }; - } - -private: - std::pair, std::vector> run_reference(bool has_xattention, - const std::vector& query_data, - const std::vector& key_data, - const std::vector& value_data, - int num_queries, - int num_keys, - int num_heads, - int num_kv_heads, - int k_head_size, - int v_head_size, - int window_size, - int sliding_window_size, - float scale, - double xattention_threshold, - size_t block_size, - const std::vector* qq_bias = nullptr, - size_t stride = 16) { - auto query_shape = ov::PartialShape{1, num_queries, num_heads, k_head_size}; - auto key_shape = ov::PartialShape{1, num_keys, num_kv_heads, k_head_size}; - auto value_shape = ov::PartialShape{1, num_keys, num_kv_heads, v_head_size}; - if (num_heads != num_kv_heads && !has_xattention) { - query_shape = ov::PartialShape{num_queries, num_kv_heads, (num_heads / num_kv_heads), k_head_size}; - key_shape = ov::PartialShape{num_keys, num_kv_heads, 1, k_head_size}; - value_shape = ov::PartialShape{num_keys, num_kv_heads, 1, v_head_size}; - } - bool do_gqa_expand = false; - std::vector expanded_key_data; - std::vector expanded_value_data; - if (has_xattention) { - // Grouped Query Attention - do_gqa_expand = (num_heads != num_kv_heads); - if (do_gqa_expand) { - const int group_size = num_heads / num_kv_heads; - - expanded_key_data.resize(static_cast(num_keys) * static_cast(num_heads) * static_cast(k_head_size)); - expanded_value_data.resize(static_cast(num_keys) * static_cast(num_heads) * static_cast(v_head_size)); - - for (int key_idx = 0; key_idx < num_keys; ++key_idx) { - for (int h = 0; h < num_heads; ++h) { - const int src_kv_head = h / group_size; - size_t src_key_off = (static_cast(key_idx) * static_cast(num_kv_heads) + static_cast(src_kv_head)) * static_cast(k_head_size); - size_t dst_key_off = (static_cast(key_idx) * static_cast(num_heads) + static_cast(h)) * static_cast(k_head_size); - for (int d = 0; d < k_head_size; ++d) - expanded_key_data[dst_key_off + static_cast(d)] = key_data[src_key_off + static_cast(d)]; - - size_t src_val_off = (static_cast(key_idx) * static_cast(num_kv_heads) + static_cast(src_kv_head)) * static_cast(v_head_size); - size_t dst_val_off = (static_cast(key_idx) * static_cast(num_heads) + static_cast(h)) * static_cast(v_head_size); - for (int d = 0; d < v_head_size; ++d) - expanded_value_data[dst_val_off + static_cast(d)] = value_data[src_val_off + static_cast(d)]; - } - } - - key_shape = ov::PartialShape{1, num_keys, num_heads, k_head_size}; - value_shape = ov::PartialShape{1, num_keys, num_heads, v_head_size}; - num_kv_heads = num_heads; - } - } - - auto query_layout = layout{query_shape, data_types::f16, format::bfyx}; - auto key_layout = layout{key_shape, data_types::f16, format::bfyx}; - auto value_layout = layout{value_shape, data_types::f16, format::bfyx}; - auto scale_layout = cldnn::layout({1}, data_types::f16, format::bfyx); - - OPENVINO_ASSERT(query_layout.count() == query_data.size()); - if (do_gqa_expand) { - OPENVINO_ASSERT(key_layout.count() == expanded_key_data.size()); - OPENVINO_ASSERT(value_layout.count() == expanded_value_data.size()); - } else { - OPENVINO_ASSERT(key_layout.count() == key_data.size()); - OPENVINO_ASSERT(value_layout.count() == value_data.size()); - } - - auto query_mem = test_engine.allocate_memory(query_layout); - auto key_mem = test_engine.allocate_memory(key_layout); - auto value_mem = test_engine.allocate_memory(value_layout); - auto scale_mem = test_engine.allocate_memory(scale_layout); - - set_values(query_mem, query_data); - if (do_gqa_expand) { - set_values(key_mem, expanded_key_data); - set_values(value_mem, expanded_value_data); - } else { - set_values(key_mem, key_data); - set_values(value_mem, value_data); - } - set_values(scale_mem, {static_cast(scale)}); - - ov::reference::XAttentionRetainedBlockIndicesForAllHeads retained_blocks; - if (num_queries >= static_cast(block_size) && has_xattention) { - auto reorder_qhk_to_hqd = [&](const std::vector& src, int outer_len, int num_heads, int head_dim) { - std::vector dst(num_heads * outer_len * head_dim); - for (int h = 0; h < num_heads; ++h) { - size_t dst_h_off = static_cast(h) * outer_len * head_dim; - for (int i = 0; i < outer_len; ++i) { - size_t src_off = static_cast(i) * num_heads * head_dim + static_cast(h) * head_dim; - std::copy_n(&src[src_off], head_dim, &dst[dst_h_off + static_cast(i) * head_dim]); - } - } - return dst; - }; - - const auto query_data_3d = reorder_qhk_to_hqd(query_data, num_queries, num_heads, k_head_size); - const auto key_data_3d = reorder_qhk_to_hqd(do_gqa_expand ? expanded_key_data : key_data, num_keys, num_heads, k_head_size); - const size_t padded_q = ((num_queries + block_size - 1) / block_size) * block_size; - const size_t padded_k = ((num_keys + block_size - 1) / block_size) * block_size; - - std::vector query_padded(num_heads * padded_q * k_head_size, 0.f); - std::vector key_padded(num_heads * padded_k * k_head_size, 0.f); - - for (int h = 0; h < num_heads; ++h) { - const auto* q_src = &query_data_3d[h * num_queries * k_head_size]; - const auto* k_src = &key_data_3d[h * num_keys * k_head_size]; - auto* q_dst = &query_padded[h * padded_q * k_head_size]; - auto* k_dst = &key_padded[h * padded_k * k_head_size]; - - std::transform(q_src, q_src + num_queries * k_head_size, q_dst, [](ov::float16 v) { - return static_cast(v); - }); - std::transform(k_src, k_src + num_keys * k_head_size, k_dst, [](ov::float16 v) { - return static_cast(v); - }); - } - ov::reference::XAttentionBlockSelector selector(xattention_threshold, block_size, stride); - retained_blocks = selector.select_blocks(query_padded.data(), - {static_cast(num_heads), padded_q, static_cast(k_head_size)}, - key_padded.data(), - {static_cast(num_heads), padded_k, static_cast(k_head_size)}); - } - auto mask_mem = get_mask_mem_combined_multi_head(num_queries, - num_keys, - num_heads, - num_kv_heads, - sliding_window_size, - retained_blocks, - static_cast(block_size), - qq_bias); - topology topology; - if (num_heads == num_kv_heads) { - topology.add(input_layout("query", query_layout), - input_layout("key", key_layout), - input_layout("value", value_layout), - data("mask", mask_mem), - data("scale", scale_mem), - permute("query_transposed", input_info("query"), {0, 2, 1, 3}), - permute("key_transposed", input_info("key"), {0, 2, 3, 1}), - permute("value_transposed", input_info("value"), {0, 2, 1, 3}), - gemm("qk_gemm", { input_info("query_transposed"), input_info("key_transposed") }, data_types::f16, false, false), - eltwise("scale_div", { input_info("qk_gemm"), input_info("scale") }, eltwise_mode::prod), - eltwise("eltwise", { input_info("scale_div"), input_info("mask") }, eltwise_mode::sum), - softmax("softmax", input_info("eltwise"), -1), - gemm("qkv_gemm", { input_info("softmax"), input_info("value_transposed") }, data_types::f16, false, false), - permute("qkv_gemm_transposed", input_info("qkv_gemm"), {0, 2, 1, 3}), - reorder("output_data", input_info("qkv_gemm_transposed"), format::bfyx, data_types::f16), - reorder("scores_data", input_info("softmax"), format::bfyx, data_types::f16) - ); - } else { - topology.add(input_layout("query", query_layout), - input_layout("key", key_layout), - input_layout("value", value_layout), - data("mask", mask_mem), - data("scale", scale_mem), - permute("query_transposed", input_info("query"), {1, 2, 0, 3}), - permute("key_transposed", input_info("key"), {1, 2, 3, 0}), - permute("value_transposed", input_info("value"), {1, 2, 0, 3}), - gemm("qk_gemm", { input_info("query_transposed"), input_info("key_transposed") }, data_types::f16, false, false), - eltwise("scale_div", { input_info("qk_gemm"), input_info("scale") }, eltwise_mode::prod), - eltwise("eltwise", { input_info("scale_div"), input_info("mask") }, eltwise_mode::sum), - softmax("softmax", input_info("eltwise"), -1), - gemm("qkv_gemm", { input_info("softmax"), input_info("value_transposed") }, data_types::f16, false, false), - reshape("qkv_gemm_reshape", input_info("qkv_gemm"), {1, num_heads, v_head_size, num_queries}), - permute("qkv_gemm_transposed", input_info("qkv_gemm_reshape"), {0, 2, 1, 3}), - reorder("output_data", input_info("qkv_gemm_transposed"), format::bfyx, data_types::f16), - reorder("scores_data", input_info("softmax"), format::bfyx, data_types::f16) - ); - } - - ExecutionConfig config = get_test_default_config(test_engine); - config.set_property(ov::intel_gpu::optimize_data(true)); - config.set_property(ov::intel_gpu::allow_new_shape_infer(true)); - - network::ptr network = get_network(test_engine, topology, config, get_test_stream_ptr(), false); - network->set_input_data("query", query_mem); - network->set_input_data("key", key_mem); - network->set_input_data("value", value_mem); - - auto outputs = network->execute(); - - auto output_data_mem = outputs.at("output_data").get_memory(); - auto output_scores_mem = outputs.at("scores_data").get_memory(); - - return { get_output_data_vec(output_data_mem, num_queries, v_head_size, num_heads), - get_output_scores_vec(output_scores_mem, window_size, num_queries, num_keys, num_heads) }; - } - - std::vector get_output_scores_vec(memory::ptr scores_output, - int window_size, - int num_queries, - int num_keys, - int num_heads) { - OPENVINO_ASSERT(scores_output->count() == static_cast(num_heads * num_queries * num_keys)); - - std::vector output_scores(num_keys, 0); - mem_lock mem_ptr(scores_output, test_stream); - for (int row_idx = 0; row_idx < window_size; row_idx++) { - for (int head_idx = 0; head_idx < num_heads; head_idx++) { - for (int score_idx = 0; score_idx < num_keys; score_idx++) { - auto scores_offset = head_idx * num_queries * num_keys + - (num_queries - window_size + row_idx) * num_keys + - score_idx; - output_scores[score_idx] += mem_ptr[scores_offset]; - } - } - } - - return output_scores; - } - - std::vector get_output_data_vec(memory::ptr data_output, - int num_queries, - int k_head_size, - int num_heads) { - OPENVINO_ASSERT(data_output->count() == static_cast(num_queries * num_heads * k_head_size)); - - std::vector output_data(data_output->count()); - mem_lock mem_ptr(data_output, test_stream); - for (size_t i = 0; i < data_output->count(); i++) - output_data[i] = mem_ptr[i]; - - return output_data; - } - - memory::ptr get_mask_mem_combined_multi_head(int num_queries, - int num_keys, - int num_heads, - int num_kv_heads, - int sliding_window_size, - const ov::reference::XAttentionRetainedBlockIndicesForAllHeads& retained_blocks, - int block_size, - const std::vector* qq_bias) { - int heads_per_kv = num_heads / num_kv_heads; - - ov::PartialShape mask_shape; - if (retained_blocks.empty()) { - mask_shape = ov::PartialShape{1, 1, num_queries, num_keys}; - } else if (num_heads == num_kv_heads) { - mask_shape = ov::PartialShape{1, num_heads, num_queries, num_keys}; - } else { - mask_shape = ov::PartialShape{num_kv_heads, heads_per_kv, num_queries, num_keys}; - } - - auto mask_layout = layout{mask_shape, data_types::f16, format::bfyx}; - auto mask_mem = test_engine.allocate_memory(mask_layout); - mem_lock mem_ptr(mask_mem, test_stream); - - size_t total_elems = mask_layout.count(); - for (size_t i = 0; i < total_elems; ++i) - mem_ptr[i] = std::numeric_limits::lowest(); - if (retained_blocks.empty()) { - if (sliding_window_size == 0) { - int past_len = num_keys - num_queries + 1; - for (int i = 0; i < num_queries; i++) { - for (int j = 0; j < num_keys; j++) { - mem_ptr[i * num_keys + j] = j >= past_len + i ? std::numeric_limits::lowest() : ov::float16(0.f); - } - } - } else { - int sliding_left = num_keys - num_queries - sliding_window_size + 1; - int past_len = num_keys - num_queries + 1; - - for (int i = 0; i < num_queries; i++) { - for (int j = 0; j < num_keys; j++) { - bool is_min; - if (num_queries == num_keys) { - is_min = (j >= sliding_left + i) && (j <= i) ? 0 : 1; - } else { - is_min = (j >= sliding_left + i) && (j < past_len + i) ? 0 : 1; - } - - mem_ptr[i * num_keys + j] = is_min ? std::numeric_limits::lowest() : ov::float16(0.f); - } - } - } - } else { - for (int h = 0; h < num_heads; ++h) { - int kv_idx = (num_heads == num_kv_heads) ? 0 : (h / heads_per_kv); - int head_in_kv = (num_heads == num_kv_heads) ? h : (h % heads_per_kv); - - size_t head_offset = (static_cast(kv_idx) * heads_per_kv + static_cast(head_in_kv)) * static_cast(num_queries) * - static_cast(num_keys); - - for (int i = 0; i < num_queries; i++) { - int left_idx = 0; - int right_idx = 0; - - if (sliding_window_size == 0) { - int past_len = num_keys - num_queries + 1; - right_idx = past_len + i - 1; - left_idx = 0; - } else { - int sliding_left = num_keys - num_queries - sliding_window_size + 1; - int past_len = num_keys - num_queries + 1; - if (num_queries == num_keys) { - left_idx = sliding_left + i; - right_idx = i; - } else { - left_idx = sliding_left + i; - right_idx = past_len + i - 1; - } - } - - left_idx = std::max(0, left_idx); - right_idx = std::min(num_keys - 1, right_idx); - - for (const auto& [q_block_idx, k_block_idx] : retained_blocks[h]) { - int q_start = q_block_idx * block_size; - int q_end = std::min(q_start + block_size, num_queries); - int k_start = k_block_idx * block_size; - int k_end = std::min(k_start + block_size, num_keys); - - if (i < q_start || i >= q_end) - continue; - - for (int j = k_start; j < k_end; j++) { - if (j >= left_idx && j <= right_idx) { - mem_ptr[head_offset + i * num_keys + j] = ov::float16(0.f); - } - } - } - } - } - } - - if (qq_bias && !qq_bias->empty()) { - OPENVINO_ASSERT(qq_bias->size() == static_cast(num_queries * num_queries)); - - auto apply_mask_for_head = [&](size_t head_offset) { - for (int i = 0; i < num_queries; i++) { - for (int j = 0; j < num_queries; j++) { - if (!(*qq_bias)[static_cast(i) * static_cast(num_queries) + static_cast(j)]) { - mem_ptr[head_offset + static_cast(i) * static_cast(num_keys) + (num_keys - num_queries) + static_cast(j)] = - std::numeric_limits::lowest(); - } - } - } - }; - - if (retained_blocks.empty()) { - apply_mask_for_head(0); - } else { - for (int h = 0; h < num_heads; ++h) { - int kv_idx = (num_heads == num_kv_heads) ? 0 : (h / heads_per_kv); - int head_in_kv = (num_heads == num_kv_heads) ? h : (h % heads_per_kv); - size_t head_offset = (static_cast(kv_idx) * static_cast(heads_per_kv) + static_cast(head_in_kv)) * - static_cast(num_queries) * static_cast(num_keys); - apply_mask_for_head(head_offset); - } - } - } - - return mask_mem; - } - - void rotate_block(std::vector& cache_data, - std::vector rotation_deltas, - std::vector rotation_trig_lut_mem, - int rotated_block_idx, - int subsequence_rotated_block_idx, - int num_heads, - int k_head_size, - int block_size, - bool per_block) { - // cache_data shape: [1, num_tokens, num_heads, k_head_size] - int start_token_idx = subsequence_rotated_block_idx * block_size; - - for (int token_idx = 0; token_idx < block_size; token_idx++) { - auto rotation_deltas_offset = per_block ? rotated_block_idx : rotated_block_idx * block_size + token_idx; - auto rotation_trig_lut_idx = rotation_deltas[rotation_deltas_offset]; - for (int head_idx = 0; head_idx < num_heads; head_idx++) { - for (int k_head_size_idx = 0; k_head_size_idx < k_head_size / 2; k_head_size_idx++) { - auto input_offset = (start_token_idx + token_idx) * num_heads * k_head_size + - head_idx * k_head_size + - k_head_size_idx; - - auto cache_value_0 = cache_data[input_offset]; - auto cache_value_1 = cache_data[input_offset + k_head_size / 2]; - - ov::float16 rotation_value_cos = rotation_trig_lut_mem[rotation_trig_lut_idx * k_head_size + k_head_size_idx]; - ov::float16 rotation_value_sin = rotation_trig_lut_mem[rotation_trig_lut_idx * k_head_size + k_head_size_idx + k_head_size / 2]; - - cache_data[input_offset] = cache_value_0 * rotation_value_cos - cache_value_1 * rotation_value_sin; - cache_data[input_offset + k_head_size / 2] = cache_value_0 * rotation_value_sin + cache_value_1 * rotation_value_cos; - } - } - } - } - - std::vector read_key_from_cache(memory::ptr key_cache_mem, size_t seq_idx, int total_tokens) { - // Read key vectors from key_cache memory - // key_cache layout: [num_blocks, num_kv_heads, head_size, block_size] - std::vector key_data(pam.num_kv_heads * total_tokens * pam.k_head_size); - - const int blocks_start = pam.block_indices_begins[seq_idx]; - const int blocks_end = pam.block_indices_begins[seq_idx + 1]; - const int num_blocks = blocks_end - blocks_start; - - const bool is_compressed = pam.kv_cache_compression; - - if (!is_compressed) { - // Uncompressed case: read as float16 - mem_lock cache_ptr(key_cache_mem, test_stream); - - for (int block_idx = 0; block_idx < num_blocks; block_idx++) { - const int physical_block = pam.block_indices[blocks_start + block_idx]; - const int tokens_in_block = std::min(pam.block_size, total_tokens - block_idx * pam.block_size); - - for (int token_offset = 0; token_offset < tokens_in_block; token_offset++) { - const int token_idx = block_idx * pam.block_size + token_offset; - - for (int head_idx = 0; head_idx < pam.num_kv_heads; head_idx++) { - const size_t cache_base = static_cast(physical_block) * pam.num_kv_heads * pam.k_head_size * pam.block_size + - static_cast(head_idx) * pam.k_head_size * pam.block_size; - - const size_t output_base = static_cast(head_idx) * total_tokens * pam.k_head_size + - static_cast(token_idx) * pam.k_head_size; - - for (int dim = 0; dim < pam.k_head_size; dim++) { - const size_t cache_offset = cache_base + static_cast(dim) * pam.block_size + token_offset; - key_data[output_base + dim] = cache_ptr[cache_offset]; - } - } - } - } - } else { - if (pam.key_cache_quant_mode == ov::internal::CacheQuantMode::BY_CHANNEL) { - if (pam.is_int4_kv_cache()) { - // INT4 BY_CHANNEL: [num_blocks, kv_heads, k_head_size/2, block_size+8] u8 - // Each packed dim p: byte holds two u4 values (lower=even dim, upper=odd dim). - // Comp at [p, block_size..block_size+7]: 4 fp16 = inv_scale0, zp0, inv_scale1, zp1 - mem_lock cache_ptr(key_cache_mem, test_stream); - const int packed_head_size = pam.k_head_size / 2; - const int adj_block_size = pam.block_size + 8; - - for (int block_idx = 0; block_idx < num_blocks; block_idx++) { - const int physical_block = pam.block_indices[blocks_start + block_idx]; - const int tokens_in_block = std::min(pam.block_size, total_tokens - block_idx * pam.block_size); - - for (int head_idx = 0; head_idx < pam.num_kv_heads; head_idx++) { - const size_t cache_base = static_cast(physical_block) * pam.num_kv_heads * packed_head_size * adj_block_size - + static_cast(head_idx) * packed_head_size * adj_block_size; - - for (int p = 0; p < packed_head_size; p++) { - const int dim0 = p * 2; - const int dim1 = p * 2 + 1; - // Read inv_scale and zp for both dims from comp region - const size_t comp_byte_off = cache_base + static_cast(p) * adj_block_size + pam.block_size; - const ov::float16* comp = reinterpret_cast(&cache_ptr[comp_byte_off]); - float inv_scale0 = static_cast(comp[0]); - float zp0 = static_cast(comp[1]); - float inv_scale1 = static_cast(comp[2]); - float zp1 = static_cast(comp[3]); - - for (int token_offset = 0; token_offset < tokens_in_block; token_offset++) { - const int token_idx = block_idx * pam.block_size + token_offset; - const size_t byte_off = cache_base + static_cast(p) * adj_block_size + token_offset; - uint8_t packed_byte = cache_ptr[byte_off]; - uint8_t q0 = packed_byte & 0xFu; - uint8_t q1 = (packed_byte >> 4) & 0xFu; - float dq0 = (static_cast(q0) - zp0) * inv_scale0; - float dq1 = (static_cast(q1) - zp1) * inv_scale1; - const size_t out_base = static_cast(head_idx) * total_tokens * pam.k_head_size - + static_cast(token_idx) * pam.k_head_size; - key_data[out_base + dim0] = ov::float16(dq0); - key_data[out_base + dim1] = ov::float16(dq1); - } - } - } - } - } else { - // I8/U8 BY_CHANNEL: [num_blocks, num_kv_heads, head_size, block_size+4] - // Each dimension quantized across all tokens in block - mem_lock cache_ptr(key_cache_mem, test_stream); - const int adj_block_size = pam.block_size + 4; - - for (int block_idx = 0; block_idx < num_blocks; block_idx++) { - const int physical_block = pam.block_indices[blocks_start + block_idx]; - const int tokens_in_block = std::min(pam.block_size, total_tokens - block_idx * pam.block_size); - - for (int head_idx = 0; head_idx < pam.num_kv_heads; head_idx++) { - const size_t cache_base = static_cast(physical_block) * pam.num_kv_heads * pam.k_head_size * adj_block_size + - static_cast(head_idx) * pam.k_head_size * adj_block_size; - - for (int dim = 0; dim < pam.k_head_size; dim++) { - // Read scale and zero-point for this dimension - const size_t scale_offset = cache_base + static_cast(dim) * adj_block_size + pam.block_size; - ov::float16 scale = *reinterpret_cast(&cache_ptr[scale_offset]); - ov::float16 zp = *reinterpret_cast(&cache_ptr[scale_offset + 2]); - - // Dequantize all tokens for this dimension - for (int token_offset = 0; token_offset < tokens_in_block; token_offset++) { - const int token_idx = block_idx * pam.block_size + token_offset; - const size_t cache_offset = cache_base + static_cast(dim) * adj_block_size + token_offset; - const size_t output_offset = static_cast(head_idx) * total_tokens * pam.k_head_size + - static_cast(token_idx) * pam.k_head_size + dim; - - int8_t quantized_value = cache_ptr[cache_offset]; - float dequantized = (static_cast(quantized_value) - static_cast(zp)) * static_cast(scale); - key_data[output_offset] = ov::float16(dequantized); - } - } - } - } - } - } else { - // BY_TOKEN - if (pam.is_int4_kv_cache()) { - // INT4 BY_TOKEN: [num_blocks, kv_heads, k_head_size/2+8, block_size] u8 - // Packing (SUBGROUP_SIZE=16): - // Y = pack_group*16 + sglid, where pack_group = d/(2*16), sglid = d%16 - // lower nibble = q(dim[pack_group*32 + sglid]) - // upper nibble = q(dim[pack_group*32 + sglid + 16]) - // Scale/ZP: fp16 in comp region at base + packed_head_size*block_size - // inv_scale[t] at comp_ptr[t], zp[t] at comp_ptr[block_size + t] - mem_lock cache_ptr(key_cache_mem, test_stream); - const int packed_head_size = pam.k_head_size / 2; - const int adj_head_size = packed_head_size + 8; - constexpr int SG = 16; - - for (int block_idx = 0; block_idx < num_blocks; block_idx++) { - const int physical_block = pam.block_indices[blocks_start + block_idx]; - const int tokens_in_block = std::min(pam.block_size, total_tokens - block_idx * pam.block_size); - - for (int head_idx = 0; head_idx < pam.num_kv_heads; head_idx++) { - const size_t cache_base = static_cast(physical_block) * pam.num_kv_heads * adj_head_size * pam.block_size + - static_cast(head_idx) * adj_head_size * pam.block_size; - // Scale and ZP are in the comp region after the packed data - const size_t comp_byte_base = cache_base + static_cast(packed_head_size) * pam.block_size; - const auto* inv_scale_arr = reinterpret_cast(&cache_ptr[comp_byte_base]); - const auto* zp_arr = reinterpret_cast(&cache_ptr[comp_byte_base + pam.block_size * 2]); - - for (int token_offset = 0; token_offset < tokens_in_block; token_offset++) { - const int token_idx = block_idx * pam.block_size + token_offset; - float inv_scale = static_cast(inv_scale_arr[token_offset]); - float zp_val = static_cast(zp_arr[token_offset]); - const size_t out_base = static_cast(head_idx) * total_tokens * pam.k_head_size + - static_cast(token_idx) * pam.k_head_size; - - for (int d = 0; d < pam.k_head_size; d++) { - int sglid_val = d % SG; - int pack_group = d / (2 * SG); - int group_in_pack = (d / SG) % 2; // 0=lower nibble, 1=upper nibble - int y = pack_group * SG + sglid_val; - const size_t byte_off = cache_base + static_cast(y) * pam.block_size + token_offset; - uint8_t packed_byte = cache_ptr[byte_off]; - uint8_t q_d = (group_in_pack == 0) ? (packed_byte & 0xFu) : ((packed_byte >> 4) & 0xFu); - key_data[out_base + d] = ov::float16((static_cast(q_d) - zp_val) * inv_scale); - } - } - } - } - - return key_data; - } - - // BY_TOKEN: [num_blocks, num_kv_heads, head_size+4, block_size] - // Token-wise quantization with shared scale/zp per token - // Layout: data rows [0..head_size-1], scale at [head_size], zp at [head_size+2] (fp16) - mem_lock cache_ptr(key_cache_mem, test_stream); - for (int block_idx = 0; block_idx < num_blocks; block_idx++) { - const int physical_block = pam.block_indices[blocks_start + block_idx]; - const int tokens_in_block = std::min(pam.block_size, total_tokens - block_idx * pam.block_size); - - for (int token_offset = 0; token_offset < tokens_in_block; token_offset++) { - const int token_idx = block_idx * pam.block_size + token_offset; - - for (int head_idx = 0; head_idx < pam.num_kv_heads; head_idx++) { - const size_t cache_base = static_cast(physical_block) * pam.num_kv_heads * (pam.k_head_size + 4) * pam.block_size + - static_cast(head_idx) * (pam.k_head_size + 4) * pam.block_size; - - // Read scale and zero-point for this token - // Scale is at [head_size][token], ZP is at [head_size+2][token] (2 rows below) - // token_offset * 2 because each half is 2 bytes (token 0 at offset 0-1, token 1 at offset 2-3, etc.) - const size_t scale_offset = cache_base + static_cast(pam.k_head_size) * pam.block_size + token_offset * 2; - const size_t zp_offset = scale_offset + 2 * pam.block_size; // ZP is 2 rows below scale - ov::float16 scale = *reinterpret_cast(&cache_ptr[scale_offset]); - ov::float16 zp = *reinterpret_cast(&cache_ptr[zp_offset]); - - const size_t output_base = static_cast(head_idx) * total_tokens * pam.k_head_size + - static_cast(token_idx) * pam.k_head_size; - - // Dequantize all dimensions for this token - for (int dim = 0; dim < pam.k_head_size; dim++) { - const size_t cache_offset = cache_base + static_cast(dim) * pam.block_size + token_offset; - - int8_t quantized_value = cache_ptr[cache_offset]; - float dequantized = (static_cast(quantized_value) - static_cast(zp)) * static_cast(scale); - - key_data[output_base + dim] = ov::float16(dequantized); - } - } - } - } - } // end else (i8 BY_TOKEN) - } // is_compressed - - return key_data; - } - - std::vector compute_diversity_reference(memory::ptr key_cache_mem) { - std::vector diversity_output; - - for (size_t seq_idx = 0; seq_idx < pam.subsequence_descs.size(); seq_idx++) { - const auto start_size = pam.adaptive_rkv_start_size; - const auto evictable_size = pam.adaptive_rkv_evictable_sizes[seq_idx]; - - // Read key data from key_cache instead of original key_data - const auto& subsequence_desc = pam.subsequence_descs[seq_idx]; - const auto total_tokens = subsequence_desc.num_tokens + subsequence_desc.past_len; - - // Extract key vectors from key_cache memory - std::vector key_data = read_key_from_cache(key_cache_mem, seq_idx, total_tokens); - - ov::Shape key_shape = {static_cast(pam.num_kv_heads), - static_cast(total_tokens), - static_cast(pam.k_head_size)}; - - // Use reference implementation - ov::reference::AdaptiveRKVDiversityCalculator calculator(start_size, evictable_size, pam.block_size); - - auto block_diversity = calculator.calculate_block_diversity(key_data.data(), key_shape); - - const size_t num_evictable_blocks = static_cast(evictable_size) / static_cast(pam.block_size); - // Flatten 2D to 1D: [num_evictable_blocks, evictable_size] -> [num_evictable_blocks * evictable_size] - for (size_t block_idx = 0; block_idx < num_evictable_blocks; block_idx++) { - for (size_t token_idx = 0; token_idx < static_cast(evictable_size); token_idx++) { - diversity_output.push_back(block_diversity[block_idx][token_idx]); - } - } - } - - return diversity_output; - } - - PagedAttentionManager& pam; - cldnn::engine& test_engine; - cldnn::stream& test_stream; -}; - -template -struct PagedAttentionTest : public ::testing::TestWithParam { -public: - random_generator rg; - cldnn::engine& engine = get_test_engine(); - float tolerance = 2e-3; - - void SetUp() override { - rg.set_seed(GET_SUITE_NAME); - } - - void execute(T& p, bool run_reference = true) { - ov::element::Type kv_cache_precision = p.kv_cache_precision; - PagedAttentionManager pam(rg, - get_test_engine(), - get_test_stream(), - p.subsequences, - p.num_heads, - p.num_kv_heads, - p.k_head_size, - p.v_head_size, - p.block_size, - p.sliding_window_size, - p.kv_cache_compression, - p.key_cache_quant_mode, - p.scores_mode == ScoresMode::SNAPKV, - p.has_xattention, - p.rotation_config, - kv_cache_precision); - if (p.has_xattention) { - pam.xattention_block_size.clear(); - if (p.xattention_block_size.has_value()) { - pam.xattention_block_size = p.xattention_block_size.value(); - } - pam.xattention_threshold.clear(); - if (p.xattention_threshold.has_value()) { - pam.xattention_threshold.reserve(p.xattention_threshold->size()); - for (const float t : p.xattention_threshold.value()) { - pam.xattention_threshold.emplace_back(static_cast(t)); - } - } - // Keep xattention_stride non-empty and per-sequence, reducing the mismatch risk with always-bound stride input. - pam.xattention_stride.assign(p.subsequences.size(), 16); - } - - if (p.has_adaptive_rkv) { - pam.adaptive_rkv_diversity_block_set_indices_begins.push_back(0); - pam.adaptive_rkv_start_size = p.start_size; - - for (size_t i = 0; i < p.subsequences.size(); i++) { - // Use per-sequence evictable_size if available, otherwise use first value - int evictable_size = i < p.evictable_sizes.size() ? p.evictable_sizes[i] : p.evictable_sizes[0]; - - pam.adaptive_rkv_evictable_sizes.push_back(evictable_size); - - int start_block = p.start_size / p.block_size; - int evictable_blocks = evictable_size / p.block_size; - int global_start_block_idx = pam.block_indices_begins[i] + start_block; - - for (int b = 0; b < evictable_blocks; b++) { - pam.adaptive_rkv_diversity_block_set_indices.push_back(global_start_block_idx + b); - } - - int prev_begin = pam.adaptive_rkv_diversity_block_set_indices_begins.back(); - pam.adaptive_rkv_diversity_block_set_indices_begins.push_back(prev_begin + evictable_blocks); - } - } - - if (p.has_qq_bias) { - pam.qq_bias = p.qq_bias_config.qq_bias; - pam.qq_bias_begins = p.qq_bias_config.qq_bias_begins; - } - - if (p.kv_cache_compression) - tolerance = 25e-3; - - auto query_mem = pam.get_query_memory(); - auto key_mem = pam.get_key_memory(); - auto value_mem = pam.get_value_memory(); - - memory::ptr key_cache_mem; - if (p.has_xattention) { - key_cache_mem = pam.get_key_cache_memory_cm(); - } else { - key_cache_mem = pam.get_key_cache_memory(); - } - auto value_cache_mem = pam.get_value_cache_memory(); - - auto past_lens_mem = pam.get_past_lens_memory(); - auto subsequence_begins_mem = pam.get_subsequence_begins_memory(); - auto block_indices_mem = pam.get_block_indices_memory(); - auto block_indices_begins_mem = pam.get_block_indices_begins_memory(); - - auto scale_mem = pam.get_scale_memory(); - auto sliding_window_mem = pam.get_sliding_window_memory(); - auto alibi_mem = pam.get_alibi_memory(); - auto max_context_len_mem = pam.get_max_context_len_memory(); - - // scores calculation related memory buffers - auto score_aggregation_mem = pam.get_score_aggregation(); - - // cache rotation related memory buffers - auto rotated_block_indices_mem = pam.get_rotated_block_indices_memory(); - auto rotation_deltas_mem = pam.get_rotation_deltas_memory(); - auto rotation_trig_lut_mem = pam.get_rotation_trig_lut_memory(); - - auto xattention_threshold_mem = pam.get_xattention_threshold_memory(); - auto xattention_block_size_mem = pam.get_xattention_block_size_memory(); - auto xattention_stride_mem = pam.get_xattention_stride_memory(); - auto sinks_mem = pam.get_sinks_memory(); - auto adaptive_rkv_start_size_mem = pam.get_adaptive_rkv_start_size_memory(); - auto adaptive_rkv_evictable_sizes_mem = pam.get_adaptive_rkv_evictable_sizes_memory(); - auto adaptive_rkv_diversity_block_set_indices_mem = pam.get_adaptive_rkv_diversity_block_set_indices_memory(); - auto adaptive_rkv_diversity_block_set_indices_begins_mem = pam.get_adaptive_rkv_diversity_block_set_indices_begins_memory(); - auto token_type_ids_mem = pam.get_token_type_ids_memory(); - - auto qq_bias = pam.get_qq_bias_memory(); - auto qq_bias_begins = pam.get_qq_bias_begins_memory(); - auto query_layout = query_mem->get_layout(); - auto key_layout = key_mem->get_layout(); - auto value_layout = value_mem->get_layout(); - auto key_cache_layout = key_cache_mem->get_layout(); - auto value_cache_layout = value_cache_mem->get_layout(); - auto past_lens_layout = past_lens_mem->get_layout(); - auto subsequence_begins_layout = subsequence_begins_mem->get_layout(); - auto block_indices_layout = block_indices_mem->get_layout(); - auto block_indices_begins_layout = block_indices_begins_mem->get_layout(); - auto scale_layout = scale_mem->get_layout(); - auto sliding_window_layout = sliding_window_mem->get_layout(); - auto alibi_layout = alibi_mem->get_layout(); - auto max_context_len_layout = max_context_len_mem->get_layout(); - auto score_aggregation_window_layout = score_aggregation_mem->get_layout(); - auto rotated_block_indices_layout = rotated_block_indices_mem->get_layout(); - auto rotation_deltas_layout = rotation_deltas_mem->get_layout(); - auto rotation_trig_lut_layout = rotation_trig_lut_mem->get_layout(); - auto xattention_threshold_layout = xattention_threshold_mem->get_layout(); - auto xattention_block_size_layout = xattention_block_size_mem->get_layout(); - auto xattention_stride_layout = xattention_stride_mem->get_layout(); - auto sinks_layout = sinks_mem->get_layout(); - auto adaptive_rkv_start_size_layout = adaptive_rkv_start_size_mem->get_layout(); - auto adaptive_rkv_evictable_sizes_layout = adaptive_rkv_evictable_sizes_mem->get_layout(); - auto adaptive_rkv_diversity_block_set_indices_layout = adaptive_rkv_diversity_block_set_indices_mem->get_layout(); - auto adaptive_rkv_diversity_block_set_indices_begins_layout = adaptive_rkv_diversity_block_set_indices_begins_mem->get_layout(); - auto token_type_ids_layout = token_type_ids_mem->get_layout(); - auto qq_bias_layout = qq_bias->get_layout(); - auto qq_bias_begins_layout = qq_bias_begins->get_layout(); - - // make layouts dynamic - query_layout.set_partial_shape(ov::PartialShape{ -1, p.num_heads * p.k_head_size }); - key_layout.set_partial_shape(ov::PartialShape{ -1, p.num_kv_heads * p.k_head_size }); - value_layout.set_partial_shape(ov::PartialShape{ -1, p.num_kv_heads * p.v_head_size }); - // key_cache_layout.set_partial_shape(ov::PartialShape{ -1, p.num_heads, p.k_head_size, p.block_size }); - { - auto pshape = key_cache_layout.get_partial_shape(); - pshape[0] = -1; - key_cache_layout.set_partial_shape(pshape); - } - // value_cache_layout.set_partial_shape(ov::PartialShape{ -1, p.num_heads, p.block_size, p.v_head_size }); - { - auto pshape = value_cache_layout.get_partial_shape(); - pshape[0] = -1; - value_cache_layout.set_partial_shape(pshape); - } - past_lens_layout.set_partial_shape(ov::PartialShape{ -1 }); - subsequence_begins_layout.set_partial_shape(ov::PartialShape{ -1 }); - block_indices_layout.set_partial_shape(ov::PartialShape{ -1 }); - block_indices_begins_layout.set_partial_shape(ov::PartialShape{ -1 }); - score_aggregation_window_layout.set_partial_shape(ov::PartialShape{ -1 }); - rotated_block_indices_layout.set_partial_shape(ov::PartialShape{ -1 }); - rotation_deltas_layout.set_partial_shape(ov::PartialShape{ -1, -1 }); - rotation_trig_lut_layout.set_partial_shape(ov::PartialShape{ -1, p.k_head_size }); - xattention_threshold_layout.set_partial_shape(ov::PartialShape{ -1 }); - adaptive_rkv_evictable_sizes_layout.set_partial_shape(ov::PartialShape{ -1 }); - adaptive_rkv_diversity_block_set_indices_layout.set_partial_shape(ov::PartialShape{ -1 }); - adaptive_rkv_diversity_block_set_indices_begins_layout.set_partial_shape(ov::PartialShape{ -1 }); - qq_bias_layout.set_partial_shape(ov::PartialShape{ -1 }); - qq_bias_begins_layout.set_partial_shape(ov::PartialShape{ -1 }); - - if (p.dynamic_paddings) { - const auto padding_axis = 1; - const auto pad_before = p.k_head_size; - const auto pad_after = p.k_head_size * 2; - - query_layout.data_padding._dynamic_dims_mask[padding_axis] = 1; - - auto query_data_layout = query_mem->get_layout(); - auto padded_query_data_layout = query_data_layout; - padded_query_data_layout.data_padding._lower_size[padding_axis] = pad_before; - padded_query_data_layout.data_padding._upper_size[padding_axis] = pad_after; - - auto new_query_memory = get_test_engine().allocate_memory(padded_query_data_layout, false); - - mem_lock query_mem_lock(query_mem, get_test_stream()); - mem_lock new_query_mem_lock(new_query_memory, get_test_stream()); - - auto query_data_shape = query_data_layout.get_shape(); - for (size_t b = 0; b < query_data_shape[0]; b++) { - for (size_t f = 0; f < query_data_shape[1]; f++) { - auto input_offset = - query_data_layout.get_linear_offset(cldnn::tensor(static_cast(b), static_cast(f), 0, 0, 0, 0)); - auto output_offset = - padded_query_data_layout.get_linear_offset(cldnn::tensor(static_cast(b), static_cast(f), 0, 0, 0, 0)); - - new_query_mem_lock[output_offset] = query_mem_lock[input_offset]; - } - } - query_mem = new_query_memory; - } - - std::vector pa_inputs = { - input_info("query"), - input_info("key"), - input_info("value"), - input_info("key_cache"), - input_info("value_cache"), - input_info("past_lens"), - input_info("subsequence_begins"), - input_info("block_indices"), - input_info("block_indices_begins"), - input_info("scale"), - input_info("sliding_window"), - input_info("alibi"), - input_info("max_context_len"), - input_info("score_aggregation_window"), - input_info("rotated_block_indices"), - input_info("rotation_deltas"), - input_info("rotation_trig_lut_modified"), - input_info("xattention_threshold"), - input_info("xattention_block_size"), - input_info("xattention_stride"), - input_info("sinks"), - input_info("adaptive_rkv_start_size"), - input_info("adaptive_rkv_evictable_sizes"), - input_info("adaptive_rkv_diversity_block_set_indices"), - input_info("adaptive_rkv_diversity_block_set_indices_begins"), - input_info("token_type_ids"), - input_info("qq_bias"), - input_info("qq_bias_begins") - }; - - auto pa_prim = paged_attention("paged_attention", pa_inputs); - - pa_prim.k_head_size = p.k_head_size; - pa_prim.v_head_size = p.v_head_size; - pa_prim.kv_heads_num = p.num_kv_heads; - pa_prim.heads_num = p.num_heads; - pa_prim.scale_val = pam.get_default_scale(); - pa_prim.has_alibi = false; - - int num_outputs = 1; - if (p.scores_mode != ScoresMode::DISABLED) num_outputs++; - if (p.has_adaptive_rkv) num_outputs++; - pa_prim.num_outputs = num_outputs; - pa_prim.has_rotated_blocks = p.rotation_config.apply_rotation; - pa_prim.has_score_aggregation = p.scores_mode == ScoresMode::SNAPKV; - pa_prim.has_adaptive_rkv = p.has_adaptive_rkv; - pa_prim.sliding_window = p.sliding_window_size; - pa_prim.is_key_by_channel = (p.key_cache_quant_mode == ov::internal::CacheQuantMode::BY_CHANNEL); - if (p.has_xattention) { - pa_prim.has_xattention = true; - } - - pa_prim.has_qq_bias = p.has_qq_bias; - - topology topology; - - topology.add( - input_layout("query", query_layout), - input_layout("key", key_layout), - input_layout("value", value_layout), - input_layout("key_cache", key_cache_layout), - input_layout("value_cache", value_cache_layout), - input_layout("past_lens", past_lens_layout), - input_layout("subsequence_begins", subsequence_begins_layout), - input_layout("block_indices", block_indices_layout), - input_layout("block_indices_begins", block_indices_begins_layout), - input_layout("scale", scale_layout), - input_layout("sliding_window", sliding_window_layout), - input_layout("alibi", alibi_layout), - input_layout("max_context_len", max_context_len_layout), - input_layout("score_aggregation_window", score_aggregation_window_layout), - pa_prim, - reorder("output_data", input_info("paged_attention", 0), format::bfyx, data_types::f16) - ); - - int output_idx = 1; - if (p.scores_mode != ScoresMode::DISABLED) { - topology.add(reorder("output_scores", input_info("paged_attention", output_idx), format::bfyx, data_types::f16)); - output_idx++; - } - if (p.has_adaptive_rkv) { - topology.add(reorder("output_diversity", input_info("paged_attention", output_idx), format::bfyx, data_types::f16)); - } - - { - topology.add(input_layout("rotated_block_indices", rotated_block_indices_layout)); - topology.add(input_layout("rotation_deltas", rotation_deltas_layout)); - topology.add(input_layout("rotation_trig_lut", rotation_trig_lut_layout)); - - // add dummy activation operation to simulate an empty PA `rotation_trig_lut` buffer for shapes like [0, k_head_size] - topology.add(activation("rotation_trig_lut_modified", input_info("rotation_trig_lut"), activation_func::none)); - - topology.add(input_layout("xattention_threshold", xattention_threshold_layout)); - topology.add(input_layout("xattention_block_size", xattention_block_size_layout)); - topology.add(input_layout("xattention_stride", xattention_stride_layout)); - topology.add(input_layout("sinks", sinks_layout)); - - topology.add(input_layout("adaptive_rkv_start_size", adaptive_rkv_start_size_layout)); - topology.add(input_layout("adaptive_rkv_evictable_sizes", adaptive_rkv_evictable_sizes_layout)); - topology.add(input_layout("adaptive_rkv_diversity_block_set_indices", adaptive_rkv_diversity_block_set_indices_layout)); - topology.add(input_layout("adaptive_rkv_diversity_block_set_indices_begins", adaptive_rkv_diversity_block_set_indices_begins_layout)); - topology.add(input_layout("token_type_ids", token_type_ids_layout)); - topology.add(input_layout("qq_bias", qq_bias_layout)); - topology.add(input_layout("qq_bias_begins", qq_bias_begins_layout)); - } - - ExecutionConfig config = get_test_default_config(get_test_engine()); - config.set_property(ov::intel_gpu::optimize_data(true)); - config.set_property(ov::intel_gpu::allow_new_shape_infer(true)); - // FlashAttn v1 or v2? - config.set_property(ov::intel_gpu::could_use_flashattn_v2(p.disable_flashattn_v2)); - config.set_property(ov::internal::key_cache_quant_mode(p.key_cache_quant_mode)); - if (kv_cache_precision != ov::element::dynamic) { - config.set_property(ov::hint::kv_cache_precision(kv_cache_precision)); - } - network::ptr network = get_network(get_test_engine(), topology, config, get_test_stream_ptr(), false); - network->set_input_data("query", query_mem); - network->set_input_data("key", key_mem); - network->set_input_data("value", value_mem); - network->set_input_data("key_cache", key_cache_mem); - network->set_input_data("value_cache", value_cache_mem); - network->set_input_data("past_lens", past_lens_mem); - network->set_input_data("subsequence_begins", subsequence_begins_mem); - network->set_input_data("block_indices", block_indices_mem); - network->set_input_data("block_indices_begins", block_indices_begins_mem); - network->set_input_data("scale", scale_mem); - network->set_input_data("sliding_window", sliding_window_mem); - network->set_input_data("alibi", alibi_mem); - network->set_input_data("max_context_len", max_context_len_mem); - network->set_input_data("score_aggregation_window", score_aggregation_mem); - network->set_input_data("rotated_block_indices", rotated_block_indices_mem); - network->set_input_data("rotation_deltas", rotation_deltas_mem); - network->set_input_data("rotation_trig_lut", rotation_trig_lut_mem); - network->set_input_data("xattention_threshold", xattention_threshold_mem); - network->set_input_data("xattention_block_size", xattention_block_size_mem); - - network->set_input_data("xattention_stride", xattention_stride_mem); - network->set_input_data("sinks", sinks_mem); - network->set_input_data("adaptive_rkv_start_size", adaptive_rkv_start_size_mem); - network->set_input_data("adaptive_rkv_evictable_sizes", adaptive_rkv_evictable_sizes_mem); - network->set_input_data("adaptive_rkv_diversity_block_set_indices", adaptive_rkv_diversity_block_set_indices_mem); - network->set_input_data("adaptive_rkv_diversity_block_set_indices_begins", adaptive_rkv_diversity_block_set_indices_begins_mem); - network->set_input_data("token_type_ids", token_type_ids_mem); - network->set_input_data("qq_bias", qq_bias); - network->set_input_data("qq_bias_begins", qq_bias_begins); - - auto outputs = network->execute(); - - if (!run_reference) { - return; - } - - cldnn::memory::ptr output_data_mem = nullptr; - cldnn::memory::ptr output_scores_mem = nullptr; - cldnn::memory::ptr output_diversity_mem = nullptr; - - output_data_mem = outputs.at("output_data").get_memory(); - if (p.scores_mode != ScoresMode::DISABLED) { - output_scores_mem = outputs.at("output_scores").get_memory(); - } - if (p.has_adaptive_rkv) { - output_diversity_mem = outputs.at("output_diversity").get_memory(); - } - auto ref_data = PagedAttentionReference(pam).get_reference(key_cache_mem); - if (p.has_xattention) { - compare_xattention(output_data_mem, output_scores_mem, ref_data); - } else { - compare(output_data_mem, output_scores_mem, output_diversity_mem, ref_data); - } - } - - void compare(memory::ptr data_output_mem, memory::ptr scores_output_mem, memory::ptr diversity_output_mem, std::tuple, std::vector, std::vector> ref_data) { - if (data_output_mem) { - ASSERT_EQ(data_output_mem->count(), std::get<0>(ref_data).size()); - mem_lock mem_ptr(data_output_mem, get_test_stream()); - for (size_t i = 0; i < data_output_mem->count(); i++) { - ASSERT_NEAR(mem_ptr[i], std::get<0>(ref_data)[i], tolerance) << " at index=" << i; - } - } - - if (scores_output_mem) { - ASSERT_EQ(scores_output_mem->count(), std::get<1>(ref_data).size()); - mem_lock mem_ptr(scores_output_mem, get_test_stream()); - for (size_t i = 0; i < scores_output_mem->count(); i++) { - ASSERT_NEAR(mem_ptr[i], std::get<1>(ref_data)[i], tolerance) << " at index=" << i; - } - } - - if (diversity_output_mem) { - ASSERT_EQ(diversity_output_mem->count(), std::get<2>(ref_data).size()); - mem_lock mem_ptr(diversity_output_mem, get_test_stream()); - // Relaxed tolerance due to float32 (GPU) vs float16 (reference) accumulator difference - float diversity_tolerance = tolerance * 10.0f; - for (size_t i = 0; i < diversity_output_mem->count(); i++) { - ASSERT_NEAR(mem_ptr[i], std::get<2>(ref_data)[i], diversity_tolerance) << " at index=" << i; - } - } - } - - void compare_xattention(memory::ptr data_output_mem, memory::ptr scores_output_mem, std::tuple, std::vector, std::vector> ref_data) { - if (data_output_mem) { - ASSERT_EQ(data_output_mem->count(), std::get<0>(ref_data).size()); - mem_lock mem_ptr(data_output_mem, get_test_stream()); - int mismatch_count = 0; - for (size_t i = 0; i < data_output_mem->count(); i++) { - if (std::fabs(static_cast(mem_ptr[i]) - static_cast(std::get<0>(ref_data)[i])) > tolerance) { - mismatch_count++; - } - } - EXPECT_LE(mismatch_count, int(data_output_mem->count() * 0.04)); - } - - if (scores_output_mem) { - ASSERT_EQ(scores_output_mem->count(), std::get<1>(ref_data).size()); - mem_lock mem_ptr(scores_output_mem, get_test_stream()); - int mismatch_count = 0; - for (size_t i = 0; i < scores_output_mem->count(); i++) { - if (std::fabs(static_cast(mem_ptr[i]) - static_cast(std::get<1>(ref_data)[i])) > tolerance) { - mismatch_count++; - } - } - EXPECT_LE(mismatch_count, int(scores_output_mem->count() * 0.04)); - } - } - - static bool check_cm_available() { - auto& engine = get_test_engine(); - ExecutionConfig config = get_test_default_config(engine); - return cldnn::check_cm_jit_support(engine, config) && - (engine.get_device_info().arch == gpu_arch::xe2 || engine.get_device_info().arch == gpu_arch::xe3); - } -}; - -struct paged_attention_test_params { - std::vector subsequences; - int num_heads; - int num_kv_heads; - int k_head_size; - int v_head_size; - int block_size; - int sliding_window_size; - bool kv_cache_compression; - ov::internal::CacheQuantMode key_cache_quant_mode; - bool dynamic_paddings; - ScoresMode scores_mode; - CacheRotationDescriptor rotation_config; - bool disable_flashattn_v2; - bool has_adaptive_rkv = false; - int start_size = 0; // Common start_size for all sequences - std::vector evictable_sizes; // Per-sequence evictable sizes - - // XAttention-related params are grouped below. - bool has_xattention = false; - std::optional> xattention_threshold = std::nullopt; - std::optional> xattention_block_size = std::nullopt; - - ov::element::Type kv_cache_precision = ov::element::dynamic; - - // test query-to-query attention bias - bool has_qq_bias = false; - QueryToQueryAttentionDescriptor qq_bias_config = {}; -}; +#include "paged_attention_gpu_test.h" class paged_attention_test : public PagedAttentionTest {}; TEST_P(paged_attention_test, basic) { auto p = GetParam(); - execute(p); + execute(p, p.run_reference); } class xattention_test : public PagedAttentionTest {}; TEST_P(xattention_test, basic) { - if (!check_cm_available()) - GTEST_SKIP(); auto p = GetParam(); + if (!check_cm_available()) + GTEST_SKIP() << "CM JIT support is required for XAttention tests, and the device must be Xe1 or later"; - execute(p); + execute(p, p.run_reference); } class xattention_invalid_test : public PagedAttentionTest {}; TEST_P(xattention_invalid_test, throws_on_invalid_xattention_inputs) { - if (!check_cm_available()) - GTEST_SKIP(); auto p = GetParam(); + if (!check_cm_available()) + GTEST_SKIP() << "CM JIT support is required for XAttention tests, and the device must be Xe1 or later"; std::string expected_error_substr; if (!p.xattention_block_size.has_value() || p.xattention_block_size->empty()) { @@ -2339,21 +63,81 @@ TEST_P(qq_bias_test, basic) { execute(p); } -const auto ENABLE_CACHE_COMPRESSION = true; -const auto DISABLE_CACHE_COMPRESSION = false; -const auto DISABLE_SCORES = ScoresMode::DISABLED; -const auto ENABLE_SCORES = ScoresMode::LAST_TOKEN; -const auto ENABLE_SCORES_SNAPKV = ScoresMode::SNAPKV; -const auto PER_BLOCK_ROTATION = CacheRotationDescriptor{ true, true }; -const auto PER_TOKEN_ROTATION = CacheRotationDescriptor{ true, false }; -const auto DISABLE_ROTATION = CacheRotationDescriptor{ false, false }; -const auto STATIC_INPUT_PAD = false; -const auto DYNAMIC_INPUT_PAD = true; -const auto ENABLE_FA_V2 = false; -const auto DISABLE_FA_V2 = true; -const auto ENABLE_DIVERSITY = true; -const auto DISABLE_DIVERSITY = false; -const auto ENABLE_QQ_BIAS = QueryToQueryAttentionDescriptor{{{{1, 0, 0, 0, 1, 1, 0, 0, 1, 0, 1, 0, 1, 0, 1, 1}}}, {0, 16}}; + +static paged_attention_test_params disable_reference_compare(paged_attention_test_params p) { + p.run_reference = false; + return p; +} + +static std::vector gen_tokens_ids_test_data(size_t seq_len, int num_images, size_t avg_img_len) { + std::vector v(seq_len, 0); + size_t gap = seq_len / (num_images + 1); + for (int img = 0; img < num_images; img++) { + size_t center = gap * (img + 1); + size_t half = avg_img_len / 2; + size_t start = (center > half) ? center - half : 0; + size_t end = std::min(seq_len, center + half); + for (size_t i = start; i < end; i++) + v[i] = 1; + } + + + return v; +} + + +// Test class to verify FlashAttnV2 multi-token sink correction is actually active. +// Runs the same prompt twice with FA_V2 enabled: once with non-zero sinks, once with zero sinks. +// Asserts outputs differ, proving the sink correction in sdpa_opt.cl executes. +// This test is platform-independent (works on any GPU with OpenCL support). +class paged_attention_sink_v2_effect_test : public PagedAttentionTest {}; + +TEST_P(paged_attention_sink_v2_effect_test, verify_sink_changes_v2_output) { + auto p = GetParam(); + p.has_sink_input = true; + p.force_flashattn_v2 = true; + + // Run FlashAttnV2 multi-token path with non-zero sinks + std::vector nonzero_sinks(p.num_heads); + for (int h = 0; h < p.num_heads; h++) + nonzero_sinks[h] = ov::float16(-1.0f + 0.3f * h); + + rg.set_seed(GET_SUITE_NAME); + p.sink_values = nonzero_sinks; + execute(p, false); + auto output_with_sinks = get_output_data(); + + // Run FlashAttnV2 multi-token path with zero sinks (sink code still runs but has no effect) + std::vector zero_sinks(p.num_heads, ov::float16(0.0f)); + + rg.set_seed(GET_SUITE_NAME); + p.sink_values = zero_sinks; + execute(p, false); + auto output_no_sinks = get_output_data(); + + ASSERT_EQ(output_with_sinks.size(), output_no_sinks.size()); + + // Verify outputs differ — proving the V2 sink correction code is active + bool found_difference = false; + for (size_t i = 0; i < output_with_sinks.size(); i++) { + if (std::abs(static_cast(output_with_sinks[i]) - static_cast(output_no_sinks[i])) > 1e-4f) { + found_difference = true; + break; + } + } + ASSERT_TRUE(found_difference) + << "FlashAttnV2 multi-token output is identical with and without sinks — sink code may not be executing"; +} + +INSTANTIATE_TEST_SUITE_P(smoke_paged_attention_sink_v2_effect, paged_attention_sink_v2_effect_test, ::testing::ValuesIn(std::vector{ + // Multi-token (prompt) cases that exercise the FlashAttnV2 online-softmax path with sinks. + // These directly test the HAS_SINK_INPUT code at line 2552 in sdpa_opt.cl. + // Platform-independent: forces FA_V2 regardless of HW micro-kernel availability. + paged_attention_test_params{ {{10, 0}}, 2, 2, 64, 64, 16, 0, DISABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, ENABLE_FA_V2, false, 0, {}, false }, + paged_attention_test_params{ {{128, 0}}, 4, 4, 64, 64, 16, 0, DISABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, ENABLE_FA_V2, false, 0, {}, false }, + // GQA prompt + paged_attention_test_params{ {{64, 0}}, 8, 2, 64, 64, 16, 0, DISABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, ENABLE_FA_V2, false, 0, {}, false }, +})); INSTANTIATE_TEST_SUITE_P(smoke_paged_attention, paged_attention_test, ::testing::ValuesIn(std::vector{ /* with scores output, use SnapKV */ @@ -2496,67 +280,228 @@ INSTANTIATE_TEST_SUITE_P(smoke_paged_attention, paged_attention_test, ::testing: paged_attention_test_params{ {{40, 96}, {30, 50}}, 2, 2, 64, 64, 16, 8, ENABLE_CACHE_COMPRESSION,ov::internal::CacheQuantMode::BY_CHANNEL, STATIC_INPUT_PAD, ENABLE_SCORES, PER_TOKEN_ROTATION, DISABLE_FA_V2, false, 0, {}, false }, // 2nd token, per token rotate paged_attention_test_params{ {{128, 130}, {256, 60}}, 2, 2, 48, 64, 16, 128, ENABLE_CACHE_COMPRESSION,ov::internal::CacheQuantMode::BY_CHANNEL, STATIC_INPUT_PAD, ENABLE_SCORES, PER_TOKEN_ROTATION, ENABLE_FA_V2, false, 0, {}, false }, // 2nd token, per token rotate - /* INT4 KV-cache compression (u4+BY_CHANNEL) */ - paged_attention_test_params{ {{10, 0}}, 2, 2, 64, 64, 16, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, DYNAMIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, false, {}, {}, ov::element::u4 }, // 1st token u4+BY_TOKEN - paged_attention_test_params{ {{256, 0}}, 2, 2, 64, 64, 16, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, DYNAMIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, false, {}, {}, ov::element::u4 }, // 1st token long u4+BY_TOKEN + /* INT4 KV-cache compression (u4+BY_CHANNEL only; BY_TOKEN is not supported for 4-bit) */ + paged_attention_test_params{ {{10, 0}}, 2, 2, 64, 64, 16, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_CHANNEL, DYNAMIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, false, {}, {}, ov::element::u4 }, // 1st token u4+BY_CHANNEL paged_attention_test_params{ {{256, 0}}, 2, 2, 64, 64, 16, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_CHANNEL, DYNAMIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, false, {}, {}, ov::element::u4 }, // 1st token long u4+BY_CHANNEL - paged_attention_test_params{ {{10, 0}, {30, 0}}, 2, 2, 64, 64, 16, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, DYNAMIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, false, {}, {}, ov::element::u4 }, // 1st token + 1st token u4+BY_TOKEN paged_attention_test_params{ {{10, 0}, {30, 0}}, 2, 2, 64, 64, 16, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_CHANNEL, DYNAMIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, false, {}, {}, ov::element::u4 }, // 1st token + 1st token u4+BY_CHANNEL - paged_attention_test_params{ {{1, 34}}, 2, 2, 64, 64, 16, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, DYNAMIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, false, {}, {}, ov::element::u4 }, // 2nd token u4+BY_TOKEN - paged_attention_test_params{ {{1, 34}, {1, 515}}, 2, 2, 64, 64, 16, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, DYNAMIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, false, {}, {}, ov::element::u4 }, // 2nd token + 2nd token u4+BY_TOKEN - paged_attention_test_params{ {{1, 300}}, 2, 2, 64, 64, 16, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, DYNAMIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, false, {}, {}, ov::element::u4 }, // 2nd token, past_len>partition_size u4+BY_TOKEN + paged_attention_test_params{ {{1, 34}}, 2, 2, 64, 64, 16, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_CHANNEL, DYNAMIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, false, {}, {}, ov::element::u4 }, // 2nd token u4+BY_CHANNEL + paged_attention_test_params{ {{1, 34}, {1, 515}}, 2, 2, 64, 64, 16, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_CHANNEL, DYNAMIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, false, {}, {}, ov::element::u4 }, // 2nd token + 2nd token u4+BY_CHANNEL paged_attention_test_params{ {{1, 300}}, 2, 2, 64, 64, 16, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_CHANNEL, DYNAMIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, false, {}, {}, ov::element::u4 }, // 2nd token, past_len>partition_size u4+BY_CHANNEL - /* u4 MIXED mode (generate+prefill in same batch) - covers pa_multi_token sink_ptr fix */ - paged_attention_test_params{ {{1, 34}, {25, 0}, {10, 34}}, 2, 2, 64, 64, 16, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, DYNAMIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, false, {}, {}, ov::element::u4 }, // mixed u4+BY_TOKEN + /* u4 MIXED mode (generate+prefill in same batch) */ paged_attention_test_params{ {{1, 34}, {25, 0}, {10, 34}}, 2, 2, 64, 64, 16, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_CHANNEL, DYNAMIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, false, {}, {}, ov::element::u4 }, // mixed u4+BY_CHANNEL /* u4 with larger head size */ - paged_attention_test_params{ {{1, 34}}, 2, 2, 128, 128, 16, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, DYNAMIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, false, {}, {}, ov::element::u4 }, // 2nd token, head_size=128 u4+BY_TOKEN paged_attention_test_params{ {{1, 34}}, 2, 2, 128, 128, 16, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_CHANNEL, DYNAMIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, false, {}, {}, ov::element::u4 }, // 2nd token, head_size=128 u4+BY_CHANNEL /* u4 with GQA (multi query groups) */ - paged_attention_test_params{ {{1, 34}}, 8, 2, 64, 64, 16, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, DYNAMIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, false, {}, {}, ov::element::u4 }, // 2nd token, GQA 8/2 u4+BY_TOKEN paged_attention_test_params{ {{1, 34}}, 8, 2, 64, 64, 16, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_CHANNEL, DYNAMIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, false, {}, {}, ov::element::u4 }, // 2nd token, GQA 8/2 u4+BY_CHANNEL + + paged_attention_test_params{ {{1, 4200}}, 8, 2, 64, 64, 16, 8, DISABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, ENABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, false }, // GQA 8/2 + scores + sw=8, kv_group_size=4 -> QUERIES_PER_WI=4 + paged_attention_test_params{ {{1, 4200}}, 4, 2, 64, 64, 16, 8, DISABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, ENABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, false }, // GQA 4/2 + scores + sw=8, kv_group_size=2 -> QUERIES_PER_WI=2 + paged_attention_test_params{ {{1, 5000}}, 16, 4, 128, 128, 16, 16, DISABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, ENABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, false }, // GQA 16/4 + scores + sw=16, k_head_size=128, kv_group_size=4 })); INSTANTIATE_TEST_SUITE_P(smoke_cm_xattention, xattention_test, ::testing::ValuesIn(std::vector{ - paged_attention_test_params{ {{32, 0}}, 2, 2, 64, 64, 256, 0, DISABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, ENABLE_FA_V2, false, 0, {}, true, std::vector{0.9f}, std::vector{128} }, // 1st token - paged_attention_test_params{ {{1024, 0}}, 2, 2, 64, 64, 256, 0, DISABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, ENABLE_FA_V2, false, 0, {}, true, std::vector{0.9f}, std::vector{128} }, // 1st token - paged_attention_test_params{ {{2048, 0}}, 2, 2, 64, 64, 256, 0, DISABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, ENABLE_FA_V2, false, 0, {}, true, std::vector{0.9f}, std::vector{128} }, // 1st token - paged_attention_test_params{ {{32, 0}}, 4, 2, 64, 64, 256, 0, DISABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, ENABLE_FA_V2, false, 0, {}, true, std::vector{0.9f}, std::vector{128} }, // 1st token - paged_attention_test_params{ {{1024, 0}}, 4, 2, 64, 64, 256, 0, DISABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, ENABLE_FA_V2, false, 0, {}, true, std::vector{0.9f}, std::vector{128} }, // 1st token - paged_attention_test_params{ {{2048, 0}}, 4, 2, 64, 64, 256, 0, DISABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, ENABLE_FA_V2, false, 0, {}, true, std::vector{0.9f}, std::vector{128} }, // 1st token - - paged_attention_test_params{ {{1, 31}}, 2, 2, 64, 64, 256, 0, DISABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, true, std::vector{0.9f}, std::vector{128} }, // 2nd token - paged_attention_test_params{ {{1, 32}}, 2, 2, 64, 64, 256, 0, DISABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, true, std::vector{0.9f}, std::vector{128} }, // 2nd token - paged_attention_test_params{ {{1, 1023}}, 2, 2, 64, 64, 256, 0, DISABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, true, std::vector{0.9f}, std::vector{128} }, // 2nd token - paged_attention_test_params{ {{1, 1024}}, 2, 2, 64, 64, 256, 0, DISABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, true, std::vector{0.9f}, std::vector{128} }, // 2nd token - paged_attention_test_params{ {{1, 127}}, 2, 2, 64, 64, 256, 0, DISABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, true, std::vector{0.9f}, std::vector{128} }, // 2nd token - paged_attention_test_params{ {{1, 128}}, 2, 2, 64, 64, 256, 0, DISABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, true, std::vector{0.9f}, std::vector{128} }, // 2nd token - paged_attention_test_params{ {{1, 129}}, 2, 2, 64, 64, 256, 0, DISABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, true, std::vector{0.9f}, std::vector{128} }, // 2nd token - paged_attention_test_params{ {{1, 32}}, 28, 28, 128, 128, 256, 0, DISABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, true, std::vector{0.9f}, std::vector{128} }, // 2nd token - - paged_attention_test_params{ {{32, 0}}, 2, 2, 64, 64, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, ENABLE_FA_V2, false, 0, {}, true, std::vector{0.9f}, std::vector{128} }, // 1st token - paged_attention_test_params{ {{1024, 0}}, 2, 2, 64, 64, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, ENABLE_FA_V2, false, 0, {}, true, std::vector{0.9f}, std::vector{128} }, // 1st token - paged_attention_test_params{ {{2048, 0}}, 2, 2, 64, 64, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, ENABLE_FA_V2, false, 0, {}, true, std::vector{0.9f}, std::vector{128} }, // 1st token - paged_attention_test_params{ {{32, 0}}, 4, 2, 64, 64, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, ENABLE_FA_V2, false, 0, {}, true, std::vector{0.9f}, std::vector{128} }, // 1st token - paged_attention_test_params{ {{1024, 0}}, 4, 2, 64, 64, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, ENABLE_FA_V2, false, 0, {}, true, std::vector{0.9f}, std::vector{128} }, // 1st token - paged_attention_test_params{ {{2048, 0}}, 4, 2, 64, 64, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, ENABLE_FA_V2, false, 0, {}, true, std::vector{0.9f}, std::vector{128} }, // 1st token - - paged_attention_test_params{ {{1, 31}}, 2, 2, 64, 64, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, true, std::vector{0.9f}, std::vector{128} }, // 2nd token - paged_attention_test_params{ {{1, 32}}, 2, 2, 64, 64, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, true, std::vector{0.9f}, std::vector{128} }, // 2nd toke - paged_attention_test_params{ {{1, 1023}}, 2, 2, 64, 64, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, true, std::vector{0.9f}, std::vector{128} }, // 2nd token - paged_attention_test_params{ {{1, 1024}}, 2, 2, 64, 64, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, true, std::vector{0.9f}, std::vector{128} }, // 2nd token - - paged_attention_test_params{ {{32, 0}}, 2, 2, 64, 64, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_CHANNEL, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, ENABLE_FA_V2, false, 0, {}, true, std::vector{0.9f}, std::vector{128} }, // 1st token - paged_attention_test_params{ {{1024, 0}}, 2, 2, 64, 64, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_CHANNEL, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, ENABLE_FA_V2, false, 0, {}, true, std::vector{0.9f}, std::vector{128} }, // 1st token - paged_attention_test_params{ {{2048, 0}}, 2, 2, 64, 64, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_CHANNEL, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, ENABLE_FA_V2, false, 0, {}, true, std::vector{0.9f}, std::vector{128} }, // 1st token - paged_attention_test_params{ {{32, 0}}, 4, 2, 64, 64, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_CHANNEL, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, ENABLE_FA_V2, false, 0, {}, true, std::vector{0.9f}, std::vector{128} }, // 1st token - paged_attention_test_params{ {{1024, 0}}, 4, 2, 64, 64, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_CHANNEL, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, ENABLE_FA_V2, false, 0, {}, true, std::vector{0.9f}, std::vector{128} }, // 1st token - paged_attention_test_params{ {{2048, 0}}, 4, 2, 64, 64, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_CHANNEL, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, ENABLE_FA_V2, false, 0, {}, true, std::vector{0.9f}, std::vector{128} }, // 1st token - - paged_attention_test_params{ {{1, 31}}, 2, 2, 64, 64, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_CHANNEL, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, true, std::vector{0.9f}, std::vector{128} }, // 2nd token - paged_attention_test_params{ {{1, 32}}, 2, 2, 64, 64, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_CHANNEL, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, true, std::vector{0.9f}, std::vector{128} }, // 2nd token - paged_attention_test_params{ {{1, 1023}}, 2, 2, 64, 64, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_CHANNEL, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, true, std::vector{0.9f}, std::vector{128} }, // 2nd token - paged_attention_test_params{ {{1, 1024}}, 2, 2, 64, 64, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_CHANNEL, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, true, std::vector{0.9f}, std::vector{128} }, // 2nd token + //////////////////////////////////////////////////////////////////////////////// single-seq + bypass xattention //////////////////////////////////////////////////////////////////////// + // single-seq prefill + uncompressed cache + paged_attention_test_params{ {{32, 0}}, 4, 2, 64, 64, 256, 0, DISABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, ENABLE_FA_V2, false, 0, {}, true, std::vector{100.0f}, std::vector{128} }, // 1st token [basic/0] + paged_attention_test_params{ {{1024, 0}}, 4, 2, 64, 64, 256, 0, DISABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, ENABLE_FA_V2, false, 0, {}, true, std::vector{100.0f}, std::vector{128} }, // 1st token [basic/1] + paged_attention_test_params{ {{2048, 0}}, 4, 2, 64, 64, 256, 0, DISABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, ENABLE_FA_V2, false, 0, {}, true, std::vector{100.0f}, std::vector{128} }, // 1st token [basic/2] + + // single-seq generate + uncompressed cache + paged_attention_test_params{ {{1, 31}}, 2, 2, 64, 64, 256, 0, DISABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, true, std::vector{100.0f}, std::vector{128} }, // 2nd token [basic/3] + paged_attention_test_params{ {{1, 32}}, 2, 2, 64, 64, 256, 0, DISABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, true, std::vector{100.0f}, std::vector{128} }, // 2nd token [basic/4] + paged_attention_test_params{ {{1, 10}}, 2, 2, 64, 64, 256, 0, DISABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, true, std::vector{100.0f}, std::vector{128} }, // generate_only [basic/5] + + paged_attention_test_params{ {{1, 300}}, 2, 2, 64, 64, 256, 0, DISABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, true, std::vector{100.0f}, std::vector{128} }, // generate_only, past_len>partition [basic/6] + paged_attention_test_params{ {{1, 1023}}, 2, 2, 64, 64, 256, 0, DISABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, true, std::vector{100.0f}, std::vector{128} }, // 2nd token [basic/7] + paged_attention_test_params{ {{1, 1024}}, 2, 2, 64, 64, 256, 0, DISABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, true, std::vector{100.0f}, std::vector{128} }, // 2nd token [basic/8] + paged_attention_test_params{ {{1, 127}}, 2, 2, 64, 64, 256, 0, DISABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, true, std::vector{100.0f}, std::vector{128} }, // 2nd token [basic/9] + paged_attention_test_params{ {{1, 128}}, 2, 2, 64, 64, 256, 0, DISABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, true, std::vector{100.0f}, std::vector{128} }, // 2nd token [basic/10] + paged_attention_test_params{ {{1, 129}}, 2, 2, 64, 64, 256, 0, DISABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, true, std::vector{100.0f}, std::vector{128} }, // 2nd token [basic/11] + paged_attention_test_params{ {{1, 32}}, 28, 28, 128, 128, 256, 0, DISABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, true, std::vector{100.0f}, std::vector{128} }, // 2nd token [basic/12] + + // single-seq prefill + by_token i8 cache + paged_attention_test_params{ {{32, 0}}, 2, 2, 64, 64, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, ENABLE_FA_V2, false, 0, {}, true, std::vector{100.0f}, std::vector{128} }, // 1st token [basic/13] + paged_attention_test_params{ {{1024, 0}}, 2, 2, 64, 64, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, ENABLE_FA_V2, false, 0, {}, true, std::vector{100.0f}, std::vector{128} }, // 1st token [basic/14] + paged_attention_test_params{ {{2048, 0}}, 2, 2, 64, 64, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, ENABLE_FA_V2, false, 0, {}, true, std::vector{100.0f}, std::vector{128} }, // 1st token [basic/15] + paged_attention_test_params{ {{32, 0}}, 4, 2, 64, 64, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, ENABLE_FA_V2, false, 0, {}, true, std::vector{100.0f}, std::vector{128} }, // 1st token [basic/16] + paged_attention_test_params{ {{1024, 0}}, 4, 2, 64, 64, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, ENABLE_FA_V2, false, 0, {}, true, std::vector{100.0f}, std::vector{128} }, // 1st token [basic/17] + paged_attention_test_params{ {{2048, 0}}, 4, 2, 64, 64, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, ENABLE_FA_V2, false, 0, {}, true, std::vector{100.0f}, std::vector{128} }, // 1st token [basic/18] + + // single-seq generate + by_token i8 cache + paged_attention_test_params{ {{1, 31}}, 2, 2, 64, 64, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, true, std::vector{100.0f}, std::vector{128} }, // 2nd token [basic/19] + paged_attention_test_params{ {{1, 32}}, 2, 2, 64, 64, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, true, std::vector{100.0f}, std::vector{128} }, // 2nd toke [basic/20] + paged_attention_test_params{ {{1, 10}}, 2, 2, 64, 64, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, true, std::vector{100.0f}, std::vector{128} }, // generate_only [basic/21] + paged_attention_test_params{ {{1, 300}}, 2, 2, 64, 64, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, true, std::vector{100.0f}, std::vector{128} }, // generate_only, past_len>partition [basic/22] + paged_attention_test_params{ {{1, 1023}}, 2, 2, 64, 64, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, true, std::vector{100.0f}, std::vector{128} }, // 2nd token [basic/23] + paged_attention_test_params{ {{1, 1024}}, 2, 2, 64, 64, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, true, std::vector{100.0f}, std::vector{128} }, // 2nd token [basic/24] + + // single-seq prefill + by_channel i8 cache + paged_attention_test_params{ {{32, 0}}, 4, 2, 64, 64, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_CHANNEL, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, ENABLE_FA_V2, false, 0, {}, true, std::vector{100.0f}, std::vector{128} }, // 1st token [basic/25] + paged_attention_test_params{ {{1024, 0}}, 4, 2, 64, 64, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_CHANNEL, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, ENABLE_FA_V2, false, 0, {}, true, std::vector{100.0f}, std::vector{128} }, // 1st token [basic/26] + paged_attention_test_params{ {{2048, 0}}, 4, 2, 64, 64, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_CHANNEL, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, ENABLE_FA_V2, false, 0, {}, true, std::vector{100.0f}, std::vector{128} }, // 1st token [basic/27] + + // single-seq generate + by_channel i8 cache + paged_attention_test_params{ {{1, 31}}, 2, 2, 64, 64, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_CHANNEL, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, true, std::vector{100.0f}, std::vector{128} }, // 2nd token [basic/28] + paged_attention_test_params{ {{1, 32}}, 2, 2, 64, 64, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_CHANNEL, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, true, std::vector{100.0f}, std::vector{128} }, // 2nd token [basic/29] + paged_attention_test_params{ {{1, 1023}}, 2, 2, 64, 64, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_CHANNEL, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, true, std::vector{100.0f}, std::vector{128} }, // 2nd token [basic/30] + paged_attention_test_params{ {{1, 1024}}, 2, 2, 64, 64, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_CHANNEL, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, true, std::vector{100.0f}, std::vector{128} }, // 2nd token [basic/31] + + //////////////////////////////////////////////////////////////////////////////// single-seq + xattention //////////////////////////////////////////////////////////////////////// + // single-seq prefill + uncompressed cache + paged_attention_test_params{ {{32, 0}}, 4, 2, 64, 64, 256, 0, DISABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, ENABLE_FA_V2, false, 0, {}, true, std::vector{0.9f}, std::vector{128} }, // 1st token [basic/32] + paged_attention_test_params{ {{1024, 0}}, 4, 2, 64, 64, 256, 0, DISABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, ENABLE_FA_V2, false, 0, {}, true, std::vector{0.9f}, std::vector{128} }, // 1st token [basic/33] - Skipped on Xe1, see TEST_P + paged_attention_test_params{ {{2048, 0}}, 4, 2, 64, 64, 256, 0, DISABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, ENABLE_FA_V2, false, 0, {}, true, std::vector{0.9f}, std::vector{128} }, // 1st token [basic/34] - Skipped on Xe1, see TEST_P + + // single-seq generate + uncompressed cache + paged_attention_test_params{ {{1, 31}}, 2, 2, 64, 64, 256, 0, DISABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, true, std::vector{0.9f}, std::vector{128} }, // 2nd token [basic/35] + paged_attention_test_params{ {{1, 32}}, 2, 2, 64, 64, 256, 0, DISABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, true, std::vector{0.9f}, std::vector{128} }, // 2nd token [basic/36] + paged_attention_test_params{ {{1, 10}}, 2, 2, 64, 64, 256, 0, DISABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, true, std::vector{0.9f}, std::vector{128} }, // generate_only [basic/37] + + paged_attention_test_params{ {{1, 300}}, 2, 2, 64, 64, 256, 0, DISABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, true, std::vector{0.9f}, std::vector{128} }, // generate_only, past_len>partition [basic/38] + paged_attention_test_params{ {{1, 1023}}, 2, 2, 64, 64, 256, 0, DISABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, true, std::vector{0.9f}, std::vector{128} }, // 2nd token [basic/39] + paged_attention_test_params{ {{1, 1024}}, 2, 2, 64, 64, 256, 0, DISABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, true, std::vector{0.9f}, std::vector{128} }, // 2nd token [basic/40] + paged_attention_test_params{ {{1, 127}}, 2, 2, 64, 64, 256, 0, DISABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, true, std::vector{0.9f}, std::vector{128} }, // 2nd token [basic/41] + paged_attention_test_params{ {{1, 128}}, 2, 2, 64, 64, 256, 0, DISABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, true, std::vector{0.9f}, std::vector{128} }, // 2nd token [basic/42] + paged_attention_test_params{ {{1, 129}}, 2, 2, 64, 64, 256, 0, DISABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, true, std::vector{0.9f}, std::vector{128} }, // 2nd token [basic/43] + paged_attention_test_params{ {{1, 32}}, 28, 28, 128, 128, 256, 0, DISABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, true, std::vector{0.9f}, std::vector{128} }, // 2nd token [basic/44] + + // single-seq prefill + by_token i8 cache + paged_attention_test_params{ {{32, 0}}, 2, 2, 64, 64, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, ENABLE_FA_V2, false, 0, {}, true, std::vector{0.9f}, std::vector{128} }, // 1st token [basic/45] + paged_attention_test_params{ {{1024, 0}}, 2, 2, 64, 64, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, ENABLE_FA_V2, false, 0, {}, true, std::vector{0.9f}, std::vector{128} }, // 1st token [basic/46] + paged_attention_test_params{ {{2048, 0}}, 2, 2, 64, 64, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, ENABLE_FA_V2, false, 0, {}, true, std::vector{0.9f}, std::vector{128} }, // 1st token [basic/47] + paged_attention_test_params{ {{32, 0}}, 4, 2, 64, 64, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, ENABLE_FA_V2, false, 0, {}, true, std::vector{0.9f}, std::vector{128} }, // 1st token [basic/48] + paged_attention_test_params{ {{1024, 0}}, 4, 2, 64, 64, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, ENABLE_FA_V2, false, 0, {}, true, std::vector{0.9f}, std::vector{128} }, // 1st token [basic/49] + paged_attention_test_params{ {{2048, 0}}, 4, 2, 64, 64, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, ENABLE_FA_V2, false, 0, {}, true, std::vector{0.9f}, std::vector{128} }, // 1st token [basic/50] + + // single-seq generate + by_token i8 cache + paged_attention_test_params{ {{1, 31}}, 2, 2, 64, 64, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, true, std::vector{0.9f}, std::vector{128} }, // 2nd token [basic/51] + paged_attention_test_params{ {{1, 32}}, 2, 2, 64, 64, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, true, std::vector{0.9f}, std::vector{128} }, // 2nd toke [basic/52] + paged_attention_test_params{ {{1, 10}}, 2, 2, 64, 64, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, true, std::vector{0.9f}, std::vector{128} }, // generate_only [basic/53] + paged_attention_test_params{ {{1, 300}}, 2, 2, 64, 64, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, true, std::vector{0.9f}, std::vector{128} }, // generate_only, past_len>partition [basic/54] + paged_attention_test_params{ {{1, 1023}}, 2, 2, 64, 64, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, true, std::vector{0.9f}, std::vector{128} }, // 2nd token [basic/55] + paged_attention_test_params{ {{1, 1024}}, 2, 2, 64, 64, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, true, std::vector{0.9f}, std::vector{128} }, // 2nd token [basic/56] + + // single-seq prefill + by_channel i8 cache + paged_attention_test_params{ {{32, 0}}, 4, 2, 64, 64, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_CHANNEL, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, ENABLE_FA_V2, false, 0, {}, true, std::vector{0.9f}, std::vector{128} }, // 1st token [basic/57] + paged_attention_test_params{ {{1024, 0}}, 4, 2, 64, 64, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_CHANNEL, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, ENABLE_FA_V2, false, 0, {}, true, std::vector{0.9f}, std::vector{128} }, // 1st token [basic/58] + paged_attention_test_params{ {{2048, 0}}, 4, 2, 64, 64, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_CHANNEL, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, ENABLE_FA_V2, false, 0, {}, true, std::vector{0.9f}, std::vector{128} }, // 1st token [basic/59] + + // single-seq generate + by_channel i8 cache + paged_attention_test_params{ {{1, 31}}, 2, 2, 64, 64, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_CHANNEL, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, true, std::vector{0.9f}, std::vector{128} }, // 2nd token [basic/60] + paged_attention_test_params{ {{1, 32}}, 2, 2, 64, 64, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_CHANNEL, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, true, std::vector{0.9f}, std::vector{128} }, // 2nd token [basic/61] + paged_attention_test_params{ {{1, 1023}}, 2, 2, 64, 64, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_CHANNEL, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, true, std::vector{0.9f}, std::vector{128} }, // 2nd token [basic/62] + paged_attention_test_params{ {{1, 1024}}, 2, 2, 64, 64, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_CHANNEL, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, true, std::vector{0.9f}, std::vector{128} }, // 2nd token [basic/63] + + //////////////////////////////////////////////////////////////////////////////// diversary head_size and head_ratio + bypass xattention //////////////////////////////////////////////////////////////////////// + /////////////////// head_size 96 as phi-3-mini-128k-instruct ////////////////////////////// + // single-seq prefill + uncompressed cache + paged_attention_test_params{ {{32, 0}}, 4, 2, 96, 96, 256, 0, DISABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, ENABLE_FA_V2, false, 0, {}, true, std::vector{100.0f}, std::vector{128} }, // 1st token [basic/64] + // single-seq generate + uncompressed cache + paged_attention_test_params{ {{1, 31}}, 2, 2, 96, 96, 256, 0, DISABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, true, std::vector{100.0f}, std::vector{128} }, // 2nd token [basic/65] + + // single-seq prefill + by_token i8 cache + paged_attention_test_params{ {{32, 0}}, 2, 2, 96, 96, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, ENABLE_FA_V2, false, 0, {}, true, std::vector{100.0f}, std::vector{128} }, // 1st token [basic/66] + // single-seq generate + by_token i8 cache + paged_attention_test_params{ {{1, 31}}, 2, 2, 96, 96, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, true, std::vector{100.0f}, std::vector{128} }, // 2nd token [basic/67] + + // single-seq prefill + by_channel i8 cache + paged_attention_test_params{ {{32, 0}}, 4, 2, 96, 96, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_CHANNEL, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, ENABLE_FA_V2, false, 0, {}, true, std::vector{100.0f}, std::vector{128} }, // 1st token [basic/68] + // single-seq generate + by_channel i8 cache + paged_attention_test_params{ {{1, 31}}, 2, 2, 96, 96, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_CHANNEL, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, true, std::vector{100.0f}, std::vector{128} }, // 2nd token [basic/69] + + /////////////////// head_ratio 16:1 as minicpm4 ////////////////////////////// + // single-seq prefill + uncompressed cache + paged_attention_test_params{ {{32, 0}}, 32, 2, 64, 64, 256, 0, DISABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, ENABLE_FA_V2, false, 0, {}, true, std::vector{100.0f}, std::vector{128} }, // 1st token [basic/70] + // single-seq generate + uncompressed cache + paged_attention_test_params{ {{1, 31}}, 32, 2, 64, 64, 256, 0, DISABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, true, std::vector{100.0f}, std::vector{128} }, // 2nd token [basic/71] + + // single-seq prefill + by_token i8 cache + paged_attention_test_params{ {{32, 0}}, 32, 2, 64, 64, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, ENABLE_FA_V2, false, 0, {}, true, std::vector{100.0f}, std::vector{128} }, // 1st token [basic/72] + // single-seq generate + by_token i8 cache + paged_attention_test_params{ {{1, 31}}, 32, 2, 64, 64, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, true, std::vector{100.0f}, std::vector{128} }, // 2nd token [basic/73] + + // single-seq prefill + by_channel i8 cache + paged_attention_test_params{ {{32, 0}}, 32, 2, 64, 64, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_CHANNEL, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, ENABLE_FA_V2, false, 0, {}, true, std::vector{100.0f}, std::vector{128} }, // 1st token [basic/74] + // single-seq generate + by_channel i8 cache + paged_attention_test_params{ {{1, 31}}, 32, 2, 64, 64, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_CHANNEL, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, true, std::vector{100.0f}, std::vector{128} }, // 2nd token [basic/75] + + //////////////////////////////////////////////////////////////////////////////// diversary head_size and head_ratio + enable xattention //////////////////////////////////////////////////////////////////////// + /////////////////// head_size 96 as phi-3-mini-128k-instruct ////////////////////////////// + // single-seq prefill + uncompressed cache + paged_attention_test_params{ {{32, 0}}, 4, 2, 96, 96, 256, 0, DISABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, ENABLE_FA_V2, false, 0, {}, true, std::vector{0.9f}, std::vector{128} }, // 1st token [basic/76] + // single-seq generate + uncompressed cache + paged_attention_test_params{ {{1, 31}}, 2, 2, 96, 96, 256, 0, DISABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, true, std::vector{0.9f}, std::vector{128} }, // 2nd token [basic/77] + + // single-seq prefill + by_token i8 cache + paged_attention_test_params{ {{32, 0}}, 2, 2, 96, 96, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, ENABLE_FA_V2, false, 0, {}, true, std::vector{0.9f}, std::vector{128} }, // 1st token [basic/78] + // single-seq generate + by_token i8 cache + paged_attention_test_params{ {{1, 31}}, 2, 2, 96, 96, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, true, std::vector{0.9f}, std::vector{128} }, // 2nd token [basic/79] + + // single-seq prefill + by_channel i8 cache + paged_attention_test_params{ {{32, 0}}, 4, 2, 96, 96, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_CHANNEL, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, ENABLE_FA_V2, false, 0, {}, true, std::vector{0.9f}, std::vector{128} }, // 1st token [basic/80] + // single-seq generate + by_channel i8 cache + paged_attention_test_params{ {{1, 31}}, 2, 2, 96, 96, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_CHANNEL, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, true, std::vector{0.9f}, std::vector{128} }, // 2nd token [basic/81] + + /////////////////// head_ratio 16:1 as minicpm4 ////////////////////////////// + // single-seq prefill + uncompressed cache + paged_attention_test_params{ {{32, 0}}, 32, 2, 64, 64, 256, 0, DISABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, ENABLE_FA_V2, false, 0, {}, true, std::vector{0.9f}, std::vector{128} }, // 1st token [basic/82] + // single-seq generate + uncompressed cache + paged_attention_test_params{ {{1, 31}}, 32, 2, 64, 64, 256, 0, DISABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, true, std::vector{0.9f}, std::vector{128} }, // 2nd token [basic/83] + + // single-seq prefill + by_token i8 cache + paged_attention_test_params{ {{32, 0}}, 32, 2, 64, 64, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, ENABLE_FA_V2, false, 0, {}, true, std::vector{0.9f}, std::vector{128} }, // 1st token [basic/84] + // single-seq generate + by_token i8 cache + paged_attention_test_params{ {{1, 31}}, 32, 2, 64, 64, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, true, std::vector{0.9f}, std::vector{128} }, // 2nd token [basic/85] + + // single-seq prefill + by_channel i8 cache + paged_attention_test_params{ {{32, 0}}, 32, 2, 64, 64, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_CHANNEL, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, ENABLE_FA_V2, false, 0, {}, true, std::vector{0.9f}, std::vector{128} }, // 1st token [basic/86] + // single-seq generate + by_channel i8 cache + paged_attention_test_params{ {{1, 31}}, 32, 2, 64, 64, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_CHANNEL, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, true, std::vector{0.9f}, std::vector{128} }, // 2nd token [basic/87] + + //////////////////////////////////////////////////////////////////////////////// multi-seq + bypass xattention //////////////////////////////////////////////////////////////////////// + // multi-seq prefill-only + paged_attention_test_params{ {{10, 0}, {30, 0}}, 4, 2, 64, 64, 256, 0, DISABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, true, std::vector{100.f, 100.f}, std::vector{128, 128} }, // multi-subsequence prefill [basic/88] + paged_attention_test_params{ {{10, 0}, {30, 0}}, 4, 2, 64, 64, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, true, std::vector{100.f, 100.f}, std::vector{128, 128} }, // multi-subsequence prefill [basic/89] + paged_attention_test_params{ {{10, 0}, {30, 0}}, 4, 2, 64, 64, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_CHANNEL, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, true, std::vector{100.f, 100.f}, std::vector{128, 128} }, // multi-subsequence prefill [basic/90] + + // multi-seq generate-only + paged_attention_test_params{ {{1, 34}, {1, 515}}, 4, 2, 64, 64, 256, 0, DISABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, true, std::vector{100.f, 100.f}, std::vector{128, 128} }, // multi-subsequence generate [basic/91] + paged_attention_test_params{ {{1, 34}, {1, 515}}, 4, 2, 64, 64, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, true, std::vector{100.f, 100.f}, std::vector{128, 128} }, // multi-subsequence generate [basic/92] + paged_attention_test_params{ {{1, 34}, {1, 515}}, 4, 2, 64, 64, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_CHANNEL, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, true, std::vector{100.f, 100.f}, std::vector{128, 128} }, // multi-subsequence generate [basic/93] + + // multi-seq mixed + paged_attention_test_params{ {{1, 34}, {25, 0}, {10, 34}}, 4, 2, 64, 64, 256, 0, DISABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, true, std::vector{100.f, 100.f, 100.f}, std::vector{128, 128, 128} }, // multi-subsequence mixed [basic/94] + paged_attention_test_params{ {{1, 34}, {25, 0}, {10, 34}}, 4, 2, 64, 64, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, true, std::vector{100.f, 100.f, 100.f}, std::vector{128, 128, 128} }, // multi-subsequence mixed [basic/95] + paged_attention_test_params{ {{1, 34}, {25, 0}, {10, 34}}, 4, 2, 64, 64, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_CHANNEL, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, true, std::vector{100.f, 100.f, 100.f}, std::vector{128, 128, 128} }, // multi-subsequence mixed [basic/96] + + paged_attention_test_params{ {{1, 1024 + 34}, {25, 0}, {10, 34}}, 4, 2, 64, 64, 256, 0, DISABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, true, std::vector{100.f, 100.f, 100.f}, std::vector{128, 128, 128} }, // multi-subsequence mixed [basic/97] + paged_attention_test_params{ {{1, 1024 + 34}, {25, 0}, {10, 34}}, 4, 2, 64, 64, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, true, std::vector{100.f, 100.f, 100.f}, std::vector{128, 128, 128} }, // multi-subsequence mixed [basic/98] + paged_attention_test_params{ {{1, 1024 + 34}, {25, 0}, {10, 34}}, 4, 2, 64, 64, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_CHANNEL, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, true, std::vector{100.f, 100.f, 100.f}, std::vector{128, 128, 128} }, // multi-subsequence mixed [basic/99] + + //////////////////////////////////////////////////////////////////////////////// multi-seq + enable xattention //////////////////////////////////////////////////////////////////////// + // multi-seq prefill + paged_attention_test_params{ {{10, 0}, {30, 0}}, 4, 2, 64, 64, 256, 0, DISABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, true, std::vector{0.9f, 0.9f}, std::vector{128, 128} }, // multi-subsequence generate [basic/100] + paged_attention_test_params{ {{10, 0}, {30, 0}}, 4, 2, 64, 64, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, true, std::vector{0.9f, 0.9f}, std::vector{128, 128} }, // multi-subsequence generate [basic/101] + paged_attention_test_params{ {{10, 0}, {30, 0}}, 4, 2, 64, 64, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_CHANNEL, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, true, std::vector{0.9f, 0.9f}, std::vector{128, 128} }, // multi-subsequence generate [basic/102] + + // multi-seq generate + paged_attention_test_params{ {{1, 34}, {1, 515}}, 4, 2, 64, 64, 256, 0, DISABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, true, std::vector{0.9f, 0.9f}, std::vector{128, 128} }, // multi-subsequence mixed [basic/103] + paged_attention_test_params{ {{1, 34}, {1, 515}}, 4, 2, 64, 64, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, true, std::vector{0.9f, 0.9f}, std::vector{128, 128} }, // multi-subsequence mixed [basic/104] + paged_attention_test_params{ {{1, 34}, {1, 515}}, 4, 2, 64, 64, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_CHANNEL, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, true, std::vector{0.9f, 0.9f}, std::vector{128, 128} }, // multi-subsequence mixed [basic/105] + + // multi-seq mixed + paged_attention_test_params{ {{1, 34}, {25, 0}, {10, 34}}, 4, 2, 64, 64, 256, 0, DISABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, true, std::vector{0.9f, 0.9f, 0.9f}, std::vector{128, 128, 128} }, // multi-subsequence mixed [basic/106] + paged_attention_test_params{ {{1, 34}, {25, 0}, {10, 34}}, 4, 2, 64, 64, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, true, std::vector{0.9f, 0.9f, 0.9f}, std::vector{128, 128, 128} }, // multi-subsequence mixed [basic/107] + paged_attention_test_params{ {{1, 34}, {25, 0}, {10, 34}}, 4, 2, 64, 64, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_CHANNEL, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, true, std::vector{0.9f, 0.9f, 0.9f}, std::vector{128, 128, 128} }, // multi-subsequence mixed [basic/108] + + paged_attention_test_params{ {{1, 1024 + 34}, {25, 0}, {10, 34}}, 4, 2, 64, 64, 256, 0, DISABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, true, std::vector{0.9f, 0.9f, 0.9f}, std::vector{128, 128, 128} }, // multi-subsequence mixed [basic/109] + paged_attention_test_params{ {{1, 1024 + 34}, {25, 0}, {10, 34}}, 4, 2, 64, 64, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, true, std::vector{0.9f, 0.9f, 0.9f}, std::vector{128, 128, 128} }, // multi-subsequence mixed [basic/110] + paged_attention_test_params{ {{1, 1024 + 34}, {25, 0}, {10, 34}}, 4, 2, 64, 64, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_CHANNEL, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, true, std::vector{0.9f, 0.9f, 0.9f}, std::vector{128, 128, 128} }, // multi-subsequence mixed [basic/111] + + // Option B: maximally-divergent past_tail to expose by-channel dispatch under-count (basic/112) + // past_len=3 => past_tail=3%16=3; cur_tokens 5,7,9 => sub-blocks ceil((3+5)/16)=1, ceil((3+7)/16)=1, ceil((3+9)/16)=1 = 3 sub-blocks + // Simple ceil_div(total_tokens=21, 16) would give 2, under-dispatching by 1 sub-block + paged_attention_test_params{ {{5, 3}, {7, 3}, {9, 3}}, 4, 2, 64, 64, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_CHANNEL, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, true, std::vector{0.9f, 0.9f, 0.9f}, std::vector{128, 128, 128} }, // multi-subsequence by-channel divergent [basic/112] })); INSTANTIATE_TEST_SUITE_P(smoke_cm_xattention_block_size, xattention_test, ::testing::ValuesIn(std::vector{ @@ -2572,6 +517,8 @@ INSTANTIATE_TEST_SUITE_P(smoke_cm_xattention_block_size_invalid, xattention_inva paged_attention_test_params{ {{1024, 0}}, 2, 2, 64, 64, 256, 0, DISABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, ENABLE_FA_V2, false, 0, {}, true, std::vector{0.9f}, std::nullopt }, // Abnormal: user provides empty xattention_block_size tensor paged_attention_test_params{ {{1024, 0}}, 2, 2, 64, 64, 256, 0, DISABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, ENABLE_FA_V2, false, 0, {}, true, std::vector{0.9f}, std::vector{} }, + // Abnormal: user provides invalid number of block sizes (should be 1 or equal to batch size) + paged_attention_test_params{ {{16, 0}, {16, 0}, {16, 0}}, 2, 2, 64, 64, 256, 0, DISABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, ENABLE_FA_V2, false, 0, {}, true, std::vector{0.9f}, std::vector{128, 128} }, })); INSTANTIATE_TEST_SUITE_P(smoke_cm_xattention_threshold_invalid, xattention_invalid_test, ::testing::ValuesIn(std::vector{ @@ -2579,6 +526,8 @@ INSTANTIATE_TEST_SUITE_P(smoke_cm_xattention_threshold_invalid, xattention_inval paged_attention_test_params{ {{1024, 0}}, 2, 2, 64, 64, 256, 0, DISABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, ENABLE_FA_V2, false, 0, {}, true, std::nullopt, std::vector{128} }, // Abnormal: user provides empty xattention_threshold tensor paged_attention_test_params{ {{1024, 0}}, 2, 2, 64, 64, 256, 0, DISABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, ENABLE_FA_V2, false, 0, {}, true, std::vector{}, std::vector{128} }, + // Abnormal: user provides invalid number of threshold values (should be 1 or equal to batch size) + paged_attention_test_params{ {{16, 0}, {16, 0}, {16, 0}}, 2, 2, 64, 64, 256, 0, DISABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, ENABLE_FA_V2, false, 0, {}, true, std::vector{0.9f, 0.9f}, std::vector{128, 128, 128} }, })); INSTANTIATE_TEST_SUITE_P(smoke_adaptive_rkv, adaptive_rkv_diversity_test, ::testing::ValuesIn(std::vector{ @@ -2638,4 +587,51 @@ INSTANTIATE_TEST_SUITE_P(smoke_qq_bias, qq_bias_test, ::testing::ValuesIn(std::v // multi sequence with different qq bias patterns paged_attention_test_params{ {{4, 20}, {2, 32}, {4, 25}}, 2, 2, 64, 64, 16, 0, DISABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, ENABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, DISABLE_DIVERSITY, 0, {}, false, std::nullopt, std::nullopt, ov::element::dynamic, true, QueryToQueryAttentionDescriptor{{{{1, 0, 0, 0, 1, 1, 0, 0, 1, 0, 1, 0, 1, 0, 1, 1}, {1, 0, 1, 1}, {1, 0, 0, 0, 1, 1, 0, 0, 1, 0, 1, 0, 1, 0, 1, 1}}}, {0, 16, 20, 36}} }, -})); \ No newline at end of file +})); + +// Performance-focused tests with larger sequence lengths, single/multi-subsequences, and CM v.s. OCL/micro path (which is triggered with xattention ON/OFF). +// These tests are not validating correctness (outputs are not checked), but are intended to be run in a performance benchmarking mode to evaluate kernel performance and behavior of the paged attention implementation. +INSTANTIATE_TEST_SUITE_P(DISABLED_smoke_paged_attention_perf_ocl, paged_attention_test, ::testing::ValuesIn(std::vector{ + // prefill-only + disable_reference_compare(paged_attention_test_params{ {{4096, 0}}, 32, 8, 128, 128, 16, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, DYNAMIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, false }), + disable_reference_compare(paged_attention_test_params{ {{4096, 4*1024}}, 32, 8, 128, 128, 16, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, DYNAMIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, false }), + disable_reference_compare(paged_attention_test_params{ {{4096, 7*1024}}, 32, 8, 128, 128, 16, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, DYNAMIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, false }), + disable_reference_compare(paged_attention_test_params{ {{4096, 15*1024}}, 32, 8, 128, 128, 16, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, DYNAMIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, false }), + disable_reference_compare(paged_attention_test_params{ {{4096, 23*1024}}, 32, 8, 128, 128, 16, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, DYNAMIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, false }), + disable_reference_compare(paged_attention_test_params{ {{4096, 31*1024}}, 32, 8, 128, 128, 16, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, DYNAMIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, false }), + + // generate-only + disable_reference_compare(paged_attention_test_params{ {{1, 4096}}, 32, 8, 128, 128, 16, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, DYNAMIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, false }), + disable_reference_compare(paged_attention_test_params{ {{1, 4*4096}}, 32, 8, 128, 128, 16, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, DYNAMIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, false }), + disable_reference_compare(paged_attention_test_params{ {{1, 8*4096}}, 32, 8, 128, 128, 16, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, DYNAMIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, false }), + disable_reference_compare(paged_attention_test_params{ {{1, 16*4096}}, 32, 8, 128, 128, 16, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, DYNAMIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, false }), + + // mixed prefill+generate + disable_reference_compare(paged_attention_test_params{ {{1, 1*4096}, {1, 1*1024}, {4096, 1024}}, 32, 8, 128, 128, 16, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, DYNAMIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, DISABLE_FA_V2, false, 0, {}, false }), +})); + +INSTANTIATE_TEST_SUITE_P(DISABLED_smoke_paged_attention_perf_cm, xattention_test, ::testing::ValuesIn(std::vector{ + // prefill-only + disable_reference_compare(paged_attention_test_params{ {{4096, 0}}, 32, 8, 128, 128, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, DYNAMIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, ENABLE_FA_V2, false, 0, {}, true, std::vector{100.0f}, std::vector{256} }), + disable_reference_compare(paged_attention_test_params{ {{4096, 4*1024}}, 32, 8, 128, 128, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, DYNAMIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, ENABLE_FA_V2, false, 0, {}, true, std::vector{100.0f}, std::vector{256} }), + disable_reference_compare(paged_attention_test_params{ {{4096, 7*1024}}, 32, 8, 128, 128, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, DYNAMIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, ENABLE_FA_V2, false, 0, {}, true, std::vector{100.0f}, std::vector{256} }), + disable_reference_compare(paged_attention_test_params{ {{4096, 15*1024}}, 32, 8, 128, 128, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, DYNAMIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, ENABLE_FA_V2, false, 0, {}, true, std::vector{100.0f}, std::vector{256} }), + disable_reference_compare(paged_attention_test_params{ {{4096, 23*1024}}, 32, 8, 128, 128, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, DYNAMIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, ENABLE_FA_V2, false, 0, {}, true, std::vector{100.0f}, std::vector{256} }), + disable_reference_compare(paged_attention_test_params{ {{4096, 31*1024}}, 32, 8, 128, 128, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, DYNAMIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, ENABLE_FA_V2, false, 0, {}, true, std::vector{100.0f}, std::vector{256} }), + + // generate-only + disable_reference_compare(paged_attention_test_params{ {{1, 4096}}, 32, 8, 128, 128, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, DYNAMIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, ENABLE_FA_V2, false, 0, {}, true, std::vector{100.0f}, std::vector{256} }), + disable_reference_compare(paged_attention_test_params{ {{1, 4*4096}}, 32, 8, 128, 128, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, DYNAMIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, ENABLE_FA_V2, false, 0, {}, true, std::vector{100.0f}, std::vector{256} }), + disable_reference_compare(paged_attention_test_params{ {{1, 8*4096}}, 32, 8, 128, 128, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, DYNAMIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, ENABLE_FA_V2, false, 0, {}, true, std::vector{100.0f}, std::vector{256} }), + disable_reference_compare(paged_attention_test_params{ {{1, 16*4096}}, 32, 8, 128, 128, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, DYNAMIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, ENABLE_FA_V2, false, 0, {}, true, std::vector{100.0f}, std::vector{256} }), + + // mixed prefill+generate + disable_reference_compare(paged_attention_test_params{ {{1, 1*4096}, {1, 1*1024}, {4096, 1024}}, 32, 8, 128, 128, 256, 0, ENABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, DYNAMIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, ENABLE_FA_V2, false, 0, {}, true, std::vector{100.0f, 100.0f, 100.0f}, std::vector{256, 256, 256} }), +})); + +static constexpr int TOKEN_IDS_SEQ_LEN = 8192; +INSTANTIATE_TEST_SUITE_P(DISABLED_smoke_paged_attention_perf_token_ids_ocl, paged_attention_test, ::testing::ValuesIn(std::vector{ + disable_reference_compare(paged_attention_test_params{ {{TOKEN_IDS_SEQ_LEN, 0}}, 1, 1, 128, 128, 16, 0, DISABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, ENABLE_FA_V2, false, 0, {}, false, std::nullopt, std::nullopt, ov::element::dynamic, false, {}, true, gen_tokens_ids_test_data(TOKEN_IDS_SEQ_LEN, 3, TOKEN_IDS_SEQ_LEN/4) }), + disable_reference_compare(paged_attention_test_params{ {{TOKEN_IDS_SEQ_LEN, 0}}, 1, 1, 128, 128, 16, 0, DISABLE_CACHE_COMPRESSION, ov::internal::CacheQuantMode::BY_TOKEN, STATIC_INPUT_PAD, DISABLE_SCORES, DISABLE_ROTATION, ENABLE_FA_V2, false, 0, {}, false, std::nullopt, std::nullopt, ov::element::dynamic, false, {}, true, gen_tokens_ids_test_data(TOKEN_IDS_SEQ_LEN, 1, TOKEN_IDS_SEQ_LEN) }), +})); + diff --git a/src/plugins/intel_gpu/tests/unit/test_cases/paged_attention_gpu_test.h b/src/plugins/intel_gpu/tests/unit/test_cases/paged_attention_gpu_test.h new file mode 100644 index 000000000000..a981d76a6caa --- /dev/null +++ b/src/plugins/intel_gpu/tests/unit/test_cases/paged_attention_gpu_test.h @@ -0,0 +1,2964 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "openvino/runtime/properties.hpp" +#include "openvino/runtime/tensor.hpp" +#include "primitive_inst.h" +#include "random_generator.hpp" +#include "test_utils.h" + +// Enable detailed xattention debugging (dumps, extra comparison info) +// Default: OFF (0). Set to 1 for investigation. +#ifndef XATTENTION_DEBUG_VERBOSE +# define XATTENTION_DEBUG_VERBOSE 0 +#endif + +/* + * PagedAttention inputs: + * [0]: query, shape: [batch_size_in_tokens, num_heads * head_size], type: f16 + * [1]: key, shape: [batch_size_in_tokens, num_kv_heads * head_size], type: f16 + * [2]: value, shape: [batch_size_in_tokens, num_kv_heads * head_size], type: f16 + * [3]: key_cache, shape: [num_blocks, num_kv_heads, head_size, block_size], type: f16 or i8 + * [4]: value_cache, shape: [num_blocks, num_kv_heads, block_size, head_size], type: f16 or i8 + * [5]: past_lens, shape: [batch_size_in_sequences], type: i32 + * [6]: subsequence_begins, shape: [batch_size_in_sequences + 1], type: i32 + * [7]: block_indices, shape: [num_blocks], type: i32 + * [8]: block_indices_begins, shape: [batch_size_in_sequences + 1], type: i32 + * [9]: scale, optional + * [10]: sliding_window, optional + * [11]: alibi_slopes, optional + * [12]: max_context_len, shape: [], type: i32 + * [13]: score_aggregation_window, optional, shape: [batch_size_in_sequences], type: i32 + * [14]: rotated_block_indices, optional, shape: [num_rotated_blocks], type: i32 + * [15]: rotation_deltas, optional, shape: [num_rotated_blocks, BLOCK_SIZE] or [num_rotated_blocks, 1], type: i32 + * [16]: rotation_trig_lut, optional, shape: [max_num_batched_tokens, head_size], type: f16 + * [17]: adaptive_rkv_start_size, optional, shape: [], type: i32 + * [18]: adaptive_rkv_evictable_sizes, optional, shape: [batch_size_in_sequences], type: i32 + * [19]: adaptive_rkv_diversity_block_set_indices, optional, shape: [total_blocks], type: i32 + * [20]: adaptive_rkv_diversity_block_set_indices_begins, optional, shape: [batch_size_in_sequences + 1], type: i32 + * [21]: qq_bias, optional, shape: [total_mask_size], type: u8 + * [22]: qq_bias_begins, optional, shape: [batch_size_in_sequences + 1], type: i32 + */ + +enum class ScoresMode { DISABLED = 0, LAST_TOKEN, SNAPKV }; + +struct SubsequenceDescriptor { + int num_tokens; + int past_len; +}; + +struct CacheRotationDescriptor { + bool apply_rotation; + // configures 2nd dimension of rotation_deltas + // if per_block is true, single value is used for all tokens inside the block + // otherwise, each token uses an independent value + bool per_block; +}; + +struct QueryToQueryAttentionDescriptor { + std::vector> qq_bias; + std::vector qq_bias_begins; +}; + +struct PagedAttentionManager { + int num_heads; + int num_kv_heads; + int k_head_size; + int v_head_size; + int block_size; + int sliding_window_size; + bool kv_cache_compression; + ov::internal::CacheQuantMode key_cache_quant_mode; + ov::element::Type kv_cache_precision = ov::element::dynamic; + bool has_score_aggregation; + CacheRotationDescriptor rotation_config; + std::vector subsequence_descs; + + // per-subsequence QKV inputs + std::vector> query_data; // {[1, num_tokens, num_heads, k_head_size], ..} + std::vector> key_data; // {[1, past_len + num_tokens, num_heads, k_head_size], ..} + std::vector> value_data; // {[1, past_len + num_tokens, num_heads, v_head_size], ..} + + // common PA inputs + std::vector past_lens; + std::vector subsequence_begins; + std::vector block_indices; + std::vector block_indices_begins; + std::vector max_context_len; + std::vector score_aggregation_window; + + // score aggregation related inputs + std::vector score_aggregation; + + // rotation related inputs + std::vector rotated_block_indices; + std::vector rotation_deltas; + std::vector rotation_trig_lut; + + // xattention related inputs + bool has_xattention; + std::vector xattention_threshold; + std::vector xattention_block_size; + std::vector xattention_stride; + + std::vector sinks; + + int adaptive_rkv_start_size = 0; + std::vector adaptive_rkv_evictable_sizes; + std::vector adaptive_rkv_diversity_block_set_indices; + std::vector adaptive_rkv_diversity_block_set_indices_begins; + + std::vector> qq_bias; + std::vector qq_bias_begins; + + // optional token_type_ids; when empty, a default single-element {0} buffer is used + std::vector token_type_ids; + cldnn::engine& test_engine; + cldnn::stream& test_stream; + tests::random_generator& rg; + + PagedAttentionManager(tests::random_generator& rg, + cldnn::engine& engine, + cldnn::stream& stream, + const std::vector& subsequence_descs, + int num_heads, + int num_kv_heads, + int k_head_size, + int v_head_size, + int block_size, + int sliding_window_size, + bool kv_cache_compression, + ov::internal::CacheQuantMode key_cache_quant_mode, + bool has_score_aggregation, + bool has_xattention, + CacheRotationDescriptor rotation_config, + ov::element::Type kv_cache_precision = ov::element::dynamic) + : num_heads(num_heads), + num_kv_heads(num_kv_heads), + k_head_size(k_head_size), + v_head_size(v_head_size), + block_size(block_size), + sliding_window_size(sliding_window_size), + kv_cache_compression(kv_cache_compression), + key_cache_quant_mode(key_cache_quant_mode), + kv_cache_precision(kv_cache_precision), + has_score_aggregation(has_score_aggregation), + rotation_config(rotation_config), + subsequence_descs(subsequence_descs), + has_xattention(has_xattention), + test_engine(engine), + test_stream(stream), + rg(rg) { + // init subsequence_begins and block_indices_begins + subsequence_begins.push_back(0); + block_indices_begins.push_back(0); + + int max_len = 0; + for (int i = 0; i < static_cast(subsequence_descs.size()); i++) { + const auto& subsequence_desc = subsequence_descs[i]; + max_len = std::max(max_len, subsequence_desc.num_tokens + subsequence_desc.past_len); + + query_data.push_back(generate_realistic_data(num_heads, subsequence_desc.num_tokens, k_head_size)); + key_data.push_back(generate_realistic_data(num_kv_heads, subsequence_desc.num_tokens + subsequence_desc.past_len, k_head_size)); + value_data.push_back(generate_realistic_data(num_kv_heads, subsequence_desc.num_tokens + subsequence_desc.past_len, v_head_size)); + + past_lens.push_back(subsequence_desc.past_len); + int subsequence_start_pos = subsequence_begins[i]; + int subsequence_end_pos = subsequence_start_pos + subsequence_desc.num_tokens; + subsequence_begins.push_back(subsequence_end_pos); + + int subsequence_length = subsequence_desc.num_tokens + subsequence_desc.past_len; + int required_blocks = cldnn::ceil_div(subsequence_length, block_size); + int start_block_idx = block_indices.empty() ? 0 : block_indices.back() + 1; + int end_block_idx = start_block_idx + required_blocks; + for (int block_idx = start_block_idx; block_idx < end_block_idx; block_idx++) { + block_indices.push_back(block_idx); + } + + int block_indices_start_pos = block_indices_begins[i]; + int block_indices_end_pos = block_indices_start_pos + required_blocks; + block_indices_begins.push_back(block_indices_end_pos); + } + max_context_len.push_back(max_len); + + if (rotation_config.apply_rotation) { + // iterate over KV-cache blocks and apply cache rotation to every second + // fully occupied block + for (size_t i = 0; i < subsequence_descs.size(); i++) { + const auto& subsequence_desc = subsequence_descs[i]; + int past_len = subsequence_desc.past_len; + int start_block_idx = block_indices_begins[i]; + for (int block_idx = 1; block_idx < past_len / block_size; block_idx++) { + if (block_idx % 2 != 0) { + rotated_block_indices.push_back(start_block_idx + block_idx); + } + } + } + + if (!rotated_block_indices.empty()) { + rotation_deltas = generate_rotation_deltas_data(rg, max_context_len[0], rotated_block_indices.size(), block_size, rotation_config.per_block); + rotation_trig_lut = generate_rotation_trig_lut_data(rg, max_context_len[0], k_head_size); + } + } + + if (has_score_aggregation) { + for (const auto& subsequence_desc : subsequence_descs) { + const auto max_tokens = 10; + auto max_window_size = std::min(subsequence_desc.num_tokens, max_tokens); + auto window_size = rg.generate_random_val(1, max_window_size); + score_aggregation.push_back(window_size); + } + } + } + + cldnn::memory::ptr get_query_memory() { + return get_QKV_memory(query_data, num_heads, k_head_size, false); + } + + cldnn::memory::ptr get_key_memory() { + return get_QKV_memory(key_data, num_kv_heads, k_head_size, true); + } + + cldnn::memory::ptr get_value_memory() { + return get_QKV_memory(value_data, num_kv_heads, v_head_size, true); + } + + cldnn::memory::ptr get_key_cache_memory_cm() { + constexpr int kv_sub_block_size = 16; + auto key_cache_dt = kv_cache_compression ? cldnn::data_types::i8 : cldnn::data_types::f16; + const int head_size = k_head_size; + int adjusted_head_size = head_size; + int adjusted_block_size = block_size; + if (kv_cache_compression) { + if (key_cache_quant_mode == ov::internal::CacheQuantMode::BY_CHANNEL) { + OPENVINO_ASSERT(block_size % kv_sub_block_size == 0); + adjusted_block_size += block_size / kv_sub_block_size * 4; + } else { + adjusted_head_size += 4; + } + } + + const auto num_blocks = block_indices.back() + 1; + auto key_cache_shape = ov::PartialShape{static_cast(num_blocks), + static_cast(num_kv_heads), + static_cast(adjusted_block_size), + static_cast(adjusted_head_size)}; + auto key_cache_layout = cldnn::layout{key_cache_shape, key_cache_dt, cldnn::format::bfyx}; + auto memory = test_engine.allocate_memory(key_cache_layout); + + for (int i = 0; i < static_cast(subsequence_descs.size()); i++) { + const int past_len = subsequence_descs[i].past_len; + if (past_len == 0) + continue; + + const int blocks_num = cldnn::ceil_div(past_len + 1, block_size); + const int start_block_idx = block_indices[block_indices_begins[i]]; + + for (int block_idx = 0; block_idx < blocks_num; block_idx++) { + const int last_token_idx = (block_idx == blocks_num - 1) ? (past_len - block_size * block_idx) : block_size; + + for (int token_idx = 0; token_idx < last_token_idx; token_idx++) { + for (int head_idx = 0; head_idx < num_kv_heads; head_idx++) { + const size_t input_token_offset = static_cast(block_idx) * block_size + token_idx; + ov::float16* src_ptr = + key_data[i].data() + input_token_offset * static_cast(num_kv_heads) * head_size + static_cast(head_idx) * head_size; + + if (!kv_cache_compression) { + const size_t base = (static_cast(start_block_idx + block_idx) * num_kv_heads * block_size * head_size) + + (static_cast(head_idx) * block_size * head_size); + const size_t off = base + static_cast(token_idx) * head_size; + set_values(test_stream, memory, src_ptr, head_size, off); + } else if (key_cache_quant_mode == ov::internal::CacheQuantMode::BY_TOKEN) { + // Compressed Key cache layout: + // logical shape: [num_blocks, num_kv_heads, block_size, adjusted_head_size], dt=i8 (adjusted_head_size=head_size+4). + // Per (block, head) region starts at block_base_i8, byte-packed as: + // data: block_base_i8 + t*head_size (u8 semantics), size=head_size bytes + // scale: scale_base_i8 + t*sizeof(fp16) (fp16), indexed as (scale_base_i8/2 + t) + // zp: zp_base_i8 + t*sizeof(fp16) (fp16), indexed as (zp_base_i8/2 + t) + // xattention quant: q∈[0..255], dequant x ≈ (q - zp) * scale, where scale=(max-min)/255, zp=(-min)*255/(max-min). + auto [qdata, scale, zp] = quantize_data(src_ptr, head_size, false, true); + int8_t* qptr = reinterpret_cast(qdata.data()); + + const size_t block_stride_i8 = static_cast(adjusted_head_size) * block_size; + const size_t block_base_i8 = (static_cast(start_block_idx + block_idx) * num_kv_heads + head_idx) * block_stride_i8; + + const size_t data_off_i8 = block_base_i8 + token_idx * head_size; + set_values(test_stream, memory, qptr, head_size, data_off_i8); + + const size_t scale_base_i8 = block_base_i8 + head_size * block_size; + const size_t zp_base_i8 = scale_base_i8 + block_size * sizeof(ov::float16); + + const size_t scale_off_f16 = scale_base_i8 / 2 + token_idx; + const size_t zp_off_f16 = zp_base_i8 / 2 + token_idx; + + set_values(test_stream, memory, &scale, 1, scale_off_f16); + set_values(test_stream, memory, &zp, 1, zp_off_f16); + } else { + // Compressed Key cache layout for BY_CHANNEL: + // shape: [num_blocks, num_kv_heads, adjusted_block_size, head_size], dt=i8. + // Per (block, head): + // data bytes region : [block_size * head_size] + // scale fp16 region per-subblock per-channel : [(block_size / sub_block) * head_size] + // zp fp16 region per-subblock per-channel : [(block_size / sub_block) * head_size] + const size_t block_stride_i8 = static_cast(adjusted_block_size) * static_cast(head_size); + const size_t block_base_i8 = + (static_cast(start_block_idx + block_idx) * static_cast(num_kv_heads) + static_cast(head_idx)) * + block_stride_i8; + + const int subblock_count = block_size / kv_sub_block_size; + const size_t scale_base_i8 = block_base_i8 + static_cast(block_size) * static_cast(head_size); + const size_t zp_base_i8 = + scale_base_i8 + static_cast(subblock_count) * static_cast(head_size) * sizeof(ov::float16); + + for (int channel = 0; channel < head_size; channel++) { + for (int sub_start = 0; sub_start < last_token_idx; sub_start += kv_sub_block_size) { + const int cur_sub_block_size = std::min(kv_sub_block_size, last_token_idx - sub_start); + std::vector token_block(cur_sub_block_size); + + for (int t = 0; t < cur_sub_block_size; t++) { + const size_t input_token_offset = static_cast(block_idx) * block_size + static_cast(sub_start + t); + token_block[t] = *(key_data[i].data() + input_token_offset * static_cast(num_kv_heads) * head_size + + static_cast(head_idx) * head_size + channel); + } + + auto [quantized_data, scale, zp] = quantize_data(token_block.data(), cur_sub_block_size, true, true); + + for (int t = 0; t < cur_sub_block_size; t++) { + const size_t data_off_i8 = block_base_i8 + static_cast(sub_start + t) * head_size + channel; + set_values(test_stream, memory, quantized_data.data() + t, 1, data_off_i8); + } + + const size_t sub_idx = static_cast(sub_start / kv_sub_block_size); + const size_t scale_off_f16 = scale_base_i8 / 2 + sub_idx * static_cast(head_size) + channel; + const size_t zp_off_f16 = zp_base_i8 / 2 + sub_idx * static_cast(head_size) + channel; + + set_values(test_stream, memory, &scale, 1, scale_off_f16); + set_values(test_stream, memory, &zp, 1, zp_off_f16); + } + } + } + } + } + } + } + return memory; + } + + bool is_int4_kv_cache() const { + return kv_cache_precision == ov::element::u4 || kv_cache_precision == ov::element::i4; + } + + cldnn::memory::ptr get_key_cache_memory() { + auto key_cache_dt = cldnn::data_types::f16; + auto adjusted_head_size = k_head_size; + auto adjusted_block_size = block_size; + if (kv_cache_compression) { + key_cache_dt = is_int4_kv_cache() ? cldnn::data_types::u8 : cldnn::data_types::i8; + const int scale_zp_bytes = 4; // 2 fp16 values (scale + zp) = 4 bytes + if (key_cache_quant_mode == ov::internal::CacheQuantMode::BY_CHANNEL) { + if (is_int4_kv_cache()) { + // u4/i4 BY_CHANNEL: block_size dim is packed (2 u4 tokens per byte) + scale/zp. + // Shape: [num_blocks, kv_heads, k_head_size, block_size/2 + 4] + // head_size is NOT packed (outer dim), block_size IS packed (inner dim). + adjusted_head_size = k_head_size; // NOT packed + adjusted_block_size = block_size / 2 + scale_zp_bytes; // packed + scale/zp + } else { + adjusted_block_size += scale_zp_bytes; + } + } else { + if (is_int4_kv_cache()) { + // Scale/zp for BY_TOKEN: 2 fp16 values = 4 bytes appended to head_size dim. + adjusted_head_size = k_head_size / 2 + scale_zp_bytes; + } else { + adjusted_head_size += scale_zp_bytes; + } + } + } + + auto num_blocks = block_indices.back() + 1; + auto key_cache_shape = ov::PartialShape{num_blocks, num_kv_heads, adjusted_head_size, adjusted_block_size}; + auto key_cache_layout = cldnn::layout{key_cache_shape, key_cache_dt, cldnn::format::bfyx}; + auto memory = test_engine.allocate_memory(key_cache_layout); + for (int i = 0; i < static_cast(subsequence_descs.size()); i++) { + int past_len = subsequence_descs[i].past_len; + if (past_len != 0) { + int blocks_num = cldnn::ceil_div(past_len + 1, block_size); + int start_block_idx = block_indices[block_indices_begins[i]]; + for (int block_idx = 0; block_idx < blocks_num; block_idx++) { + int last_token_idx = block_idx == blocks_num - 1 ? (past_len - block_size * block_idx) : block_size; + // quantize by channel + if (kv_cache_compression && key_cache_quant_mode == ov::internal::CacheQuantMode::BY_CHANNEL) { + if (is_int4_kv_cache()) { + // INT4 BY_CHANNEL: packed layout [num_blocks, kv_heads, k_head_size, block_size/2+4] + // block_size dim is packed: 2 u4 tokens per byte along innermost dim. + // Comp region at [d, block_size/2..block_size/2+3]: 2 fp16 = inv_scale, zp per head dim d. + const int packed_block = block_size / 2; + for (int head_idx = 0; head_idx < num_kv_heads; head_idx++) { + for (int d = 0; d < k_head_size; d++) { + // Gather values for this head dim across all tokens in this block + std::vector vals(block_size, 0.f); + for (int t = 0; t < last_token_idx; ++t) { + size_t in_off = (static_cast(block_idx) * block_size + t) * num_kv_heads * k_head_size + head_idx * k_head_size; + vals[t] = static_cast(key_data[i].data()[in_off + d]); + } + // Quantize to u4 + float min_v = vals[0], max_v = vals[0]; + for (int t = 1; t < last_token_idx; ++t) { + min_v = std::min(min_v, vals[t]); + max_v = std::max(max_v, vals[t]); + } + float range = (max_v == min_v) ? 0.001f : (max_v - min_v); + const float min_range = std::abs(max_v) * 0.1f; + if (range <= min_range) + range += std::max(1.0f, min_range); + float scale = 15.0f / range; + float zp_val = -min_v * scale; + std::vector q(last_token_idx); + for (int t = 0; t < last_token_idx; ++t) { + int v = static_cast(std::nearbyint(vals[t] * scale + zp_val)); + q[t] = static_cast(std::max(0, std::min(15, v))); + } + + const size_t block_offset = + static_cast(start_block_idx + block_idx) * num_kv_heads * k_head_size * adjusted_block_size + + head_idx * k_head_size * adjusted_block_size; + const size_t row_offset = block_offset + d * adjusted_block_size; + + // Pack 2 u4 tokens per byte: token t0 in lower nibble, t1 in upper nibble + std::vector packed_data(packed_block, 0); + for (int t = 0; t < last_token_idx; ++t) { + int byte_idx = t / 2; + if (t % 2 == 0) + packed_data[byte_idx] = q[t] & 0xFu; + else + packed_data[byte_idx] |= (q[t] & 0xFu) << 4; + } + set_values(test_stream, memory, packed_data.data(), static_cast(packed_block), row_offset); + + // Write comp: 2 fp16 (inv_scale, zp) at row_offset + packed_block + const size_t comp_offset_fp16 = (row_offset + packed_block) / 2; + ov::float16 inv_scale_val = static_cast(1.0f / scale); + ov::float16 fp16_zp = static_cast(zp_val); + set_values(test_stream, memory, &inv_scale_val, 1, comp_offset_fp16 + 0); + set_values(test_stream, memory, &fp16_zp, 1, comp_offset_fp16 + 1); + } + } + } else { + for (int head_idx = 0; head_idx < num_kv_heads; head_idx++) { + for (int k_head_size_idx = 0; k_head_size_idx < k_head_size; k_head_size_idx++) { + std::vector token_block(block_size); + for (int token_idx = 0; token_idx < last_token_idx; ++token_idx) { + size_t input_token_offset = block_idx * block_size + token_idx; + token_block[token_idx] = + *(key_data[i].data() + input_token_offset * num_kv_heads * k_head_size + head_idx * k_head_size + k_head_size_idx); + } + auto [quantized_data, scale, zp] = quantize_data(token_block.data(), last_token_idx, true); + size_t output_block_offset = (start_block_idx + block_idx) * num_kv_heads * adjusted_head_size * adjusted_block_size + + head_idx * adjusted_head_size * adjusted_block_size; + size_t output_offset = output_block_offset + k_head_size_idx * adjusted_block_size; + set_values(test_stream, memory, quantized_data.data(), last_token_idx, output_offset); + size_t comp_offset = (output_offset + block_size) / 2; + set_values(test_stream, memory, &scale, 1, comp_offset); + set_values(test_stream, memory, &zp, 1, comp_offset + 1); + } + } + } + } + for (int token_idx = 0; token_idx < last_token_idx; token_idx++) { + for (int head_idx = 0; head_idx < num_kv_heads; head_idx++) { + if (kv_cache_compression) { + if (key_cache_quant_mode == ov::internal::CacheQuantMode::BY_TOKEN) { + // quantize by token + size_t input_token_offset = block_idx * block_size + token_idx; + ov::float16* data_ptr = key_data[i].data() + input_token_offset * num_kv_heads * k_head_size + head_idx * k_head_size; + // shape: [num_blocks, num_kv_heads, adjusted_head_size, block_size] + size_t output_block_offset = (start_block_idx + block_idx) * num_kv_heads * adjusted_head_size * block_size + + head_idx * adjusted_head_size * block_size; + + if (is_int4_kv_cache()) { + // INT4 BY_TOKEN: [num_blocks, kv_heads, k_head_size/2+8, block_size] u8 + // Kernel packing (SUBGROUP_SIZE=16): + // Y = pack_group*16 + sglid, where pack_group = d/(2*16), sglid = d%16 + // lower 4bit = q(dim[pack_group*32+sglid]) + // upper 4bit = q(dim[pack_group*32+sglid+16]) + // Scale/ZP: fp16 in comp region at Y=packed_head_size.. + // inv_scale[t] at fp16 idx (comp_base/2 + t) + // zp[t] at fp16 idx (comp_base/2 + block_size + t) + const int packed_head_size = k_head_size / 2; + constexpr int SG = 16; // SUBGROUP_SIZE + // Compute per-token min/max, then scale/zp in u4 range [0,15] + float min_v = std::numeric_limits::max(); + float max_v = -std::numeric_limits::max(); + for (int d = 0; d < k_head_size; d++) { + float v = static_cast(data_ptr[d]); + min_v = std::min(min_v, v); + max_v = std::max(max_v, v); + } + float range = (max_v == min_v) ? 0.001f : (max_v - min_v); + const float min_range = std::abs(max_v) * 0.1f; + if (range <= min_range) + range += std::max(1.0f, min_range); + float token_scale = 15.0f / range; + float token_zp = -min_v * token_scale; + std::vector q(k_head_size); + for (int d = 0; d < k_head_size; d++) { + int v = static_cast(std::nearbyint(static_cast(data_ptr[d]) * token_scale + token_zp)); + q[d] = static_cast(std::max(0, std::min(15, v))); + } + // Pack and write: Y=pack_group*SG+sglid → (lower=q[d0], upper=q[d1]) + for (int y = 0; y < packed_head_size; y++) { + int sglid_val = y % SG; + int pack_group = y / SG; + int d0 = pack_group * 2 * SG + sglid_val; + int d1 = d0 + SG; + uint8_t packed_byte = q[d0] & 0xFu; + if (d1 < k_head_size) + packed_byte |= (q[d1] & 0xFu) << 4; + size_t offset = output_block_offset + static_cast(y) * block_size + token_idx; + set_values(test_stream, memory, &packed_byte, 1, offset); + } + // Write inv_scale and zp as fp16 in the comp region + size_t comp_offset_fp16 = (output_block_offset + static_cast(packed_head_size) * block_size) / 2; + ov::float16 fp16_inv_scale = static_cast(1.0f / token_scale); + ov::float16 fp16_zp = static_cast(token_zp); + set_values(test_stream, memory, &fp16_inv_scale, 1, comp_offset_fp16 + token_idx); + set_values(test_stream, memory, &fp16_zp, 1, comp_offset_fp16 + block_size + token_idx); + } else { + auto [quantized_data, scale, zp] = quantize_data(data_ptr, k_head_size); + for (int k_head_size_idx = 0; k_head_size_idx < k_head_size; k_head_size_idx++) { + auto quantized_data_ptr = quantized_data.data() + k_head_size_idx; + + size_t output_offset = output_block_offset + k_head_size_idx * block_size + token_idx; + + set_values(test_stream, memory, quantized_data_ptr, 1, output_offset); + } + size_t comp_offset = (output_block_offset + k_head_size * block_size) / 2; + set_values(test_stream, memory, &scale, 1, comp_offset + token_idx); + set_values(test_stream, memory, &zp, 1, comp_offset + block_size + token_idx); + } + } + } else { + for (int k_head_size_idx = 0; k_head_size_idx < k_head_size; k_head_size_idx++) { + size_t input_token_offset = block_idx * block_size + token_idx; + ov::float16* data_ptr = + key_data[i].data() + input_token_offset * num_kv_heads * k_head_size + head_idx * k_head_size + k_head_size_idx; + + // shape: [num_blocks, num_kv_heads, k_head_size, block_size] + size_t output_offset = (start_block_idx + block_idx) * num_kv_heads * k_head_size * block_size + + head_idx * k_head_size * block_size + k_head_size_idx * block_size + token_idx; + + set_values(test_stream, memory, data_ptr, 1, output_offset); + } + } + } + } + } + } + } + + return memory; + } + + cldnn::memory::ptr get_value_cache_memory() { + auto value_cache_dt = cldnn::data_types::f16; + const int head_size = v_head_size; + int scale_zp_bytes = 0; + if (kv_cache_compression) { + value_cache_dt = is_int4_kv_cache() ? cldnn::data_types::u8 : cldnn::data_types::i8; + scale_zp_bytes = 4; // 2 fp16 values (scale + zp) = 4 bytes + } + + // For u4 (INT4), values are packed 2 per byte; the physical head size is halved. + // PACKED_ADJUSTED_V_HEAD_SIZE = v_head_size/2 + scales_zp_size = 32 + 4 = 36. + const int adjusted_head_size = is_int4_kv_cache() ? (head_size / 2 + scale_zp_bytes) : (head_size + scale_zp_bytes); + + const auto num_blocks = block_indices.back() + 1; + auto value_cache_shape = ov::PartialShape{static_cast(num_blocks), + static_cast(num_kv_heads), + static_cast(block_size), + static_cast(adjusted_head_size)}; + auto value_cache_layout = cldnn::layout{value_cache_shape, value_cache_dt, cldnn::format::bfyx}; + auto memory = test_engine.allocate_memory(value_cache_layout); + + for (int i = 0; i < static_cast(subsequence_descs.size()); i++) { + const int past_len = subsequence_descs[i].past_len; + if (past_len == 0) + continue; + + const int blocks_num = cldnn::ceil_div(past_len + 1, block_size); + const int start_block_idx = block_indices[block_indices_begins[i]]; + + for (int block_idx = 0; block_idx < blocks_num; block_idx++) { + const int last_token_idx = (block_idx == blocks_num - 1) ? (past_len - block_size * block_idx) : block_size; + + for (int token_idx = 0; token_idx < last_token_idx; token_idx++) { + for (int head_idx = 0; head_idx < num_kv_heads; head_idx++) { + const size_t input_token_offset = static_cast(block_idx) * block_size + token_idx; + + ov::float16* src_ptr = value_data[i].data() + input_token_offset * static_cast(num_kv_heads) * head_size + + static_cast(head_idx) * head_size; + + if (!kv_cache_compression) { + const size_t base = (static_cast(start_block_idx + block_idx) * static_cast(num_kv_heads) * + static_cast(block_size) * static_cast(head_size)) + + (static_cast(head_idx) * static_cast(block_size) * static_cast(head_size)); + const size_t off = base + static_cast(token_idx) * static_cast(head_size); + set_values(test_stream, memory, src_ptr, head_size, off); + } else if (is_int4_kv_cache()) { + // INT4 (u4) BY_TOKEN value cache: inline per-token comp layout. + // [num_blocks, kv_heads, block_size, PACKED_ADJUSTED_V_HEAD_SIZE] u8 + // PACKED_ADJUSTED_V_HEAD_SIZE = v_head_size/2 + 4 = 36 (for v_head_size=64). + // Per token: [packed_data (32 bytes) | scale (fp16) | zp (fp16)] = 36 bytes. + const int packed_head_size = head_size / 2; + + // Quantize entire token: one scale/zp for all head dims (BY_TOKEN) + float min_val = std::numeric_limits::max(); + float max_val = std::numeric_limits::lowest(); + for (int d = 0; d < head_size; d++) { + float v = static_cast(src_ptr[d]); + min_val = std::min(min_val, v); + max_val = std::max(max_val, v); + } + float diff = (max_val == min_val) ? 0.001f : (max_val - min_val); + float min_range = std::abs(max_val * 0.1f); + if (diff <= min_range) + diff += std::max(1.0f, min_range); + float scale_val = 15.0f / diff; + float zp_val = -min_val * scale_val; + ov::float16 inv_scale_fp16 = ov::float16(1.0f / scale_val); + ov::float16 zp_fp16 = ov::float16(zp_val); + + const size_t block_stride = static_cast(adjusted_head_size) * static_cast(block_size); + const size_t block_base = + (static_cast(start_block_idx + block_idx) * static_cast(num_kv_heads) + static_cast(head_idx)) * + block_stride; + + // Token base: each token occupies adjusted_head_size bytes (inline comp) + const size_t token_base = block_base + static_cast(token_idx) * static_cast(adjusted_head_size); + + // Pack pairs of groups: group (g*2) and (g*2+1), each 16 dims wide. + const int num_packed_groups = packed_head_size / 16; + for (int g = 0; g < num_packed_groups; g++) { + for (int lane = 0; lane < 16; lane++) { + int dim_even = (g * 2) * 16 + lane; + int dim_odd = (g * 2 + 1) * 16 + lane; + int q0 = static_cast(std::nearbyint(static_cast(src_ptr[dim_even]) * scale_val + zp_val)); + int q1 = static_cast(std::nearbyint(static_cast(src_ptr[dim_odd]) * scale_val + zp_val)); + q0 = std::max(0, std::min(15, q0)); + q1 = std::max(0, std::min(15, q1)); + uint8_t packed_byte = static_cast((q0 & 0xFu) | (static_cast(q1 & 0xFu) << 4)); + const size_t packed_pos = g * 16 + lane; + set_values(test_stream, memory, &packed_byte, 1, token_base + packed_pos); + } + } + + // Write inline comp: scale (fp16) and zp (fp16) right after packed data + const size_t comp_byte_off = token_base + static_cast(packed_head_size); + const size_t comp_f16_off = comp_byte_off / 2; + set_values(test_stream, memory, &inv_scale_fp16, 1, comp_f16_off); + set_values(test_stream, memory, &zp_fp16, 1, comp_f16_off + 1); + } else { + // Compressed Value cache layout: + // logical shape: [num_blocks, num_kv_heads, block_size, adjusted_head_size], dt=i8 (adjusted_head_size=head_size+4). + // Per (block, head): data at block_base_i8 + t*head_size; scale/zp are fp16 arrays at scale_base_i8/zp_base_i8 + // (fp16 element offsets: scale_base_i8/2 + t, zp_base_i8/2 + t). + // has_xattention uses unsigned [0..255] quant; dequant x ≈ (q - zp) * scale, scale=(max-min)/255, zp=(-min)*255/(max-min). + auto [qdata, scale, zp] = quantize_data(src_ptr, head_size, false, has_xattention); + int8_t* qptr = reinterpret_cast(qdata.data()); + + const size_t block_stride_i8 = static_cast(adjusted_head_size) * static_cast(block_size); + const size_t block_base_i8 = + (static_cast(start_block_idx + block_idx) * static_cast(num_kv_heads) + static_cast(head_idx)) * + block_stride_i8; + + const size_t data_off_i8 = block_base_i8 + static_cast(token_idx) * static_cast(head_size); + set_values(test_stream, memory, qptr, head_size, data_off_i8); + + const size_t scale_base_i8 = block_base_i8 + static_cast(head_size) * static_cast(block_size); + const size_t zp_base_i8 = scale_base_i8 + static_cast(block_size) * sizeof(ov::float16); + + const size_t scale_off_f16 = (scale_base_i8 >> 1) + static_cast(token_idx); + const size_t zp_off_f16 = (zp_base_i8 >> 1) + static_cast(token_idx); + + set_values(test_stream, memory, &scale, 1, scale_off_f16); + set_values(test_stream, memory, &zp, 1, zp_off_f16); + } + } + } + } + } + return memory; + } + + cldnn::memory::ptr get_past_lens_memory() { + return get_memory_from_vec(past_lens); + } + + cldnn::memory::ptr get_subsequence_begins_memory() { + return get_memory_from_vec(subsequence_begins); + } + + cldnn::memory::ptr get_block_indices_memory() { + return get_memory_from_vec(block_indices); + } + + cldnn::memory::ptr get_block_indices_begins_memory() { + return get_memory_from_vec(block_indices_begins); + } + + cldnn::memory::ptr get_scale_memory() { + std::vector scale = {ov::float16(get_default_scale())}; + return get_memory_from_vec(scale); + } + + cldnn::memory::ptr get_sliding_window_memory() { + std::vector sliding_window = {0}; + return get_memory_from_vec(sliding_window); + } + + cldnn::memory::ptr get_alibi_memory() { + std::vector alibi; + return get_memory_from_vec(alibi); + } + + cldnn::memory::ptr get_max_context_len_memory() { + return get_memory_from_vec(max_context_len); + } + + cldnn::memory::ptr get_score_aggregation() { + return get_memory_from_vec(score_aggregation); + } + + cldnn::memory::ptr get_rotated_block_indices_memory() { + return get_memory_from_vec(rotated_block_indices); + } + + cldnn::memory::ptr get_rotation_deltas_memory() { + auto mem = get_memory_from_vec(rotation_deltas); + auto layout = mem->get_layout(); + auto last_dim = rotation_config.per_block ? 1 : block_size; + layout.set_partial_shape(ov::PartialShape{static_cast(rotated_block_indices.size()), last_dim}); + + return test_engine.reinterpret_buffer(*mem, layout); + } + + cldnn::memory::ptr get_rotation_trig_lut_memory() { + auto mem = get_memory_from_vec(rotation_trig_lut); + auto layout = mem->get_layout(); + layout.set_partial_shape(ov::PartialShape{max_context_len[0], k_head_size}); + + if (rotated_block_indices.empty()) { + auto empty_layout = mem->get_layout(); + empty_layout.set_partial_shape(ov::PartialShape{0, k_head_size}); + return test_engine.reinterpret_buffer(*mem, empty_layout); + } + + return test_engine.reinterpret_buffer(*mem, layout); + } + + cldnn::memory::ptr get_xattention_threshold_memory() { + return get_memory_from_vec(xattention_threshold); + } + + cldnn::memory::ptr get_xattention_block_size_memory() { + return get_memory_from_vec(xattention_block_size); + } + + cldnn::memory::ptr get_xattention_stride_memory() { + return get_memory_from_vec(xattention_stride); + } + + cldnn::memory::ptr get_sinks_memory() { + auto mem = get_memory_from_vec(sinks); + auto layout = mem->get_layout(); + layout.set_partial_shape(ov::PartialShape{1, num_heads, 1, 1}); + + if (sinks.empty()) { + auto empty_layout = mem->get_layout(); + empty_layout.set_partial_shape(ov::PartialShape{0, 0, 0, 0}); + return test_engine.reinterpret_buffer(*mem, empty_layout); + } + + return test_engine.reinterpret_buffer(*mem, layout); + } + + cldnn::memory::ptr get_adaptive_rkv_start_size_memory() { + auto mem = test_engine.allocate_memory({{}, cldnn::data_types::i32, cldnn::format::bfyx}); + cldnn::mem_lock lock(mem, test_stream); + lock[0] = adaptive_rkv_start_size; + return mem; + } + + cldnn::memory::ptr get_adaptive_rkv_evictable_sizes_memory() { + return get_memory_from_vec(adaptive_rkv_evictable_sizes); + } + + cldnn::memory::ptr get_adaptive_rkv_diversity_block_set_indices_memory() { + return get_memory_from_vec(adaptive_rkv_diversity_block_set_indices); + } + + cldnn::memory::ptr get_adaptive_rkv_diversity_block_set_indices_begins_memory() { + return get_memory_from_vec(adaptive_rkv_diversity_block_set_indices_begins); + } + + cldnn::memory::ptr get_token_type_ids_memory() { + if (!token_type_ids.empty()) { + return get_memory_from_vec(token_type_ids); + } + std::vector default_token_type_ids = {0}; + return get_memory_from_vec(default_token_type_ids); + } + + cldnn::memory::ptr get_qq_bias_memory() { + std::vector flat_qq_bias; + for (const auto& matrix : qq_bias) { + for (bool val : matrix) { + flat_qq_bias.push_back(static_cast(val)); + } + } + return get_memory_from_vec(flat_qq_bias); + } + + cldnn::memory::ptr get_qq_bias_begins_memory() { + return get_memory_from_vec(qq_bias_begins); + } + + float get_default_scale() { + return static_cast(1.f / std::sqrt(k_head_size)); + } + +private: + template + cldnn::memory::ptr get_memory_from_vec(std::vector& input_data) { + auto data_size = input_data.empty() ? 1 : input_data.size(); + auto shape = ov::PartialShape{static_cast(data_size)}; + auto layout = cldnn::layout{shape, ov::element::from(), cldnn::format::bfyx}; + auto memory = test_engine.allocate_memory(layout); + + if (input_data.empty()) { + auto shape = ov::PartialShape{0}; + auto layout = cldnn::layout{shape, ov::element::from(), cldnn::format::bfyx}; + return test_engine.reinterpret_buffer(*memory, layout); + } + + set_values(test_stream, memory, input_data.data(), input_data.size(), 0); + + return memory; + } + + cldnn::memory::ptr get_QKV_memory(std::vector>& input_data, int num_heads, int head_size, bool skip_past_len) { + int total_tokens = 0; + for (const auto& subsequence_desc : subsequence_descs) + total_tokens += subsequence_desc.num_tokens; + + auto query_shape = ov::PartialShape{total_tokens, num_heads * head_size}; + auto query_layout = cldnn::layout{query_shape, cldnn::data_types::f16, cldnn::format::bfyx}; + auto memory = test_engine.allocate_memory(query_layout); + + for (int subsequence_idx = 0; subsequence_idx < static_cast(subsequence_descs.size()); subsequence_idx++) { + for (int token_idx = 0; token_idx < subsequence_descs[subsequence_idx].num_tokens; token_idx++) { + for (int head_idx = 0; head_idx < num_heads; head_idx++) { + size_t input_token_offset = token_idx; + // as generated data stored in vectors includes past_len, ignore it for KV inputs + if (skip_past_len) + input_token_offset += subsequence_descs[subsequence_idx].past_len; + + ov::float16* data_ptr = input_data[subsequence_idx].data() + input_token_offset * num_heads * head_size + head_idx * head_size; + + size_t output_token_offset = subsequence_begins[subsequence_idx] + token_idx; + size_t output_offset = output_token_offset * num_heads * head_size + head_idx * head_size; + + set_values(test_stream, memory, data_ptr, head_size, output_offset); + } + } + } + + return memory; + } + + template + static void set_values(cldnn::stream& stream, cldnn::memory::ptr mem, T* vals, size_t size, size_t dst_offset) { + cldnn::mem_lock mem_ptr(mem, stream); + for (size_t i = 0; i < size; i++) { + mem_ptr[dst_offset + i] = vals[i]; + } + } + + static std::vector generate_input_data(tests::random_generator& rg, size_t num_heads, size_t tokens_num, size_t k_head_size) { + const size_t total_elements_num = tokens_num * num_heads * k_head_size; + auto data = rg.generate_random_1d(total_elements_num, -1, 1); + + return data; + } + + static std::vector generate_realistic_data(size_t num_heads, size_t tokens_num, size_t k_head_size) { + std::vector data(num_heads * tokens_num * k_head_size); + + std::mt19937 gen(1234); + std::normal_distribution dist(0.0f, 0.1f); + + for (size_t h = 0; h < num_heads; ++h) { + for (size_t t = 0; t < tokens_num; ++t) { + for (size_t d = 0; d < k_head_size; ++d) { + float val = dist(gen); + if (t > 0) + val = 0.8f * val + 0.2f * static_cast(data[h * tokens_num * k_head_size + (t - 1) * k_head_size + d]); + data[h * tokens_num * k_head_size + t * k_head_size + d] = static_cast(val); + } + } + } + + return data; + } + + static std::vector generate_rotation_deltas_data(tests::random_generator& rg, + size_t max_tokens_num, + size_t rotated_blocks_num, + size_t block_size, + bool per_block) { + const size_t total_elements_num = per_block ? rotated_blocks_num : rotated_blocks_num * block_size; + auto data = rg.generate_random_1d(total_elements_num, 0, static_cast(max_tokens_num - 1)); + + return data; + } + + static std::vector generate_rotation_trig_lut_data(tests::random_generator& rg, size_t max_tokens_num, size_t k_head_size) { + const size_t total_elements_num = max_tokens_num * k_head_size; + auto data = rg.generate_random_1d(total_elements_num, -1, 1); + + return data; + } + + static std::tuple, ov::float16, ov::float16> quantize_data(ov::float16* data, + size_t size, + bool expand_range = false, + bool has_xattention = false) { + float min_value = std::numeric_limits::max(); + float max_value = std::numeric_limits::lowest(); + + for (size_t i = 0; i < size; i++) { + float v = static_cast(data[i]); + min_value = std::min(min_value, v); + max_value = std::max(max_value, v); + } + + if (has_xattention) { + if (max_value == min_value) { + std::vector qdata(size, 0); + return {qdata, ov::float16(0.0f), ov::float16(min_value)}; + } + + float diff_value = max_value - min_value; + if (expand_range && std::abs(diff_value) <= std::abs(max_value) * 0.1f) { + diff_value = (max_value - min_value) + std::max(1.0f, max_value * 0.1f); + } + + float scale_val = 255.0f / diff_value; + float zp_val = -min_value * scale_val; + + std::vector qdata(size); + for (size_t i = 0; i < size; i++) { + float q = data[i] * scale_val + zp_val; + int v = static_cast(std::nearbyint(q)); + if (v < 0) + v = 0; + if (v > 255) + v = 255; + qdata[i] = static_cast(v); + } + + ov::float16 scale = static_cast(diff_value / 255.0f); + ov::float16 zp = static_cast(zp_val); + return {qdata, scale, zp}; + } + + float diff_value = 0.001f; + if (max_value != min_value) + diff_value = max_value - min_value; + if (expand_range && std::abs(diff_value) <= std::abs(max_value) * 0.1f) { + diff_value = (max_value - min_value) + std::max(1.0f, max_value * 0.1f); + } + + float scale = (std::numeric_limits::max() - std::numeric_limits::lowest()) / diff_value; + float zp = -min_value * scale + std::numeric_limits::lowest(); + + std::vector qdata(size); + auto convert_char_rte = [](float val) { + float rounded = std::nearbyint(val); + if (rounded > 127.0f) + return static_cast(127); + if (rounded < -128.0f) + return static_cast(-128); + return static_cast(rounded); + }; + + for (size_t i = 0; i < size; i++) { + qdata[i] = convert_char_rte(data[i] * scale + zp); + } + + ov::float16 scale_out = static_cast(1.0f / scale); + ov::float16 zp_out = static_cast(zp); + return {qdata, scale_out, zp_out}; + } +}; + +namespace std { +template <> +struct hash { + size_t operator()(const ov::float16& value) const noexcept { + return std::hash{}(static_cast(value)); + } +}; +} // namespace std + +struct PagedAttentionReference { + PagedAttentionReference(PagedAttentionManager& pam) : pam(pam), test_engine(pam.test_engine), test_stream(pam.test_stream) {} + + std::tuple, std::vector, std::vector> get_reference(cldnn::memory::ptr key_cache_mem = nullptr) { + const bool has_xattention = pam.has_xattention; + if (has_xattention) { + const size_t total_iterations = pam.subsequence_descs.size(); + if (pam.xattention_threshold.size() != total_iterations) { + OPENVINO_THROW("xattention_threshold size (", pam.xattention_threshold.size(), ") must match number of subsequences (", total_iterations, ")"); + } + if (pam.xattention_block_size.size() != total_iterations) { + OPENVINO_THROW("xattention_block_size size (", + pam.xattention_block_size.size(), + ") must match number of subsequences (", + total_iterations, + ")"); + } + } + + std::vector ref_data_output; + std::vector ref_scores_output; + std::vector ref_diversity_output; + size_t qq_bias_offset = 0; + for (size_t i = 0; i < pam.subsequence_descs.size(); i++) { + const auto& subsequence_desc = pam.subsequence_descs[i]; + const auto kv_seq_len = subsequence_desc.num_tokens + subsequence_desc.past_len; + + auto key_data = pam.key_data[i]; + if (pam.rotation_config.apply_rotation) { + auto blocks_start = pam.block_indices_begins[i]; + auto blocks_end = pam.block_indices_begins[i + 1]; + + std::vector block_indices(pam.block_indices.begin() + blocks_start, pam.block_indices.begin() + blocks_end); + + for (const auto& block_idx : block_indices) { + auto it = std::find(pam.rotated_block_indices.begin(), pam.rotated_block_indices.end(), block_idx); + if (it != pam.rotated_block_indices.end()) { + int index = std::distance(pam.rotated_block_indices.begin(), it); + int subsequence_rotated_block_idx = *it - blocks_start; + + rotate_block(key_data, + pam.rotation_deltas, + pam.rotation_trig_lut, + index, + subsequence_rotated_block_idx, + pam.num_kv_heads, + pam.k_head_size, + pam.block_size, + pam.rotation_config.per_block); + } + } + } + + auto window_size = pam.has_score_aggregation ? pam.score_aggregation[i] : 1; + + const std::vector* qq_bias_ptr = nullptr; + if (pam.qq_bias.size() > 0 && pam.subsequence_descs[i].past_len != 0) { + qq_bias_ptr = &pam.qq_bias[qq_bias_offset++]; + } + + double xattn_threshold = 1.0; + size_t xattn_block_size = 128; + if (has_xattention) { + xattn_threshold = static_cast(pam.xattention_threshold[i]); + + // reference path reflects runtime fallback/validation behavior for block size. + if (test_engine.get_device_info().arch < cldnn::gpu_arch::xe2) { + xattn_block_size = 128; + } else { + const int user_value = pam.xattention_block_size[i]; + xattn_block_size = (user_value == 128 || user_value == 256) ? static_cast(user_value) : 256; + } + } + + auto subsequence_ref_results = run_reference(has_xattention, + pam.query_data[i], + key_data, + pam.value_data[i], + subsequence_desc.num_tokens, + kv_seq_len, + pam.num_heads, + pam.num_kv_heads, + pam.k_head_size, + pam.v_head_size, + window_size, + pam.sliding_window_size, + pam.get_default_scale(), + xattn_threshold, + xattn_block_size, + qq_bias_ptr); + + // concatenate all subsequences into one vector + ref_data_output.insert(ref_data_output.end(), subsequence_ref_results.first.begin(), subsequence_ref_results.first.end()); + ref_scores_output.insert(ref_scores_output.end(), subsequence_ref_results.second.begin(), subsequence_ref_results.second.end()); + } + + if (!pam.adaptive_rkv_evictable_sizes.empty()) { + ref_diversity_output = compute_diversity_reference(key_cache_mem); + } + + return {ref_data_output, ref_scores_output, ref_diversity_output}; + } + +private: + std::pair, std::vector> run_reference(bool has_xattention, + const std::vector& query_data, + const std::vector& key_data, + const std::vector& value_data, + int num_queries, + int num_keys, + int num_heads, + int num_kv_heads, + int k_head_size, + int v_head_size, + int window_size, + int sliding_window_size, + float scale, + double xattention_threshold, + size_t block_size, + const std::vector* qq_bias = nullptr, + size_t stride = 16) { + auto query_shape = ov::PartialShape{1, num_queries, num_heads, k_head_size}; + auto key_shape = ov::PartialShape{1, num_keys, num_kv_heads, k_head_size}; + auto value_shape = ov::PartialShape{1, num_keys, num_kv_heads, v_head_size}; + if (num_heads != num_kv_heads && !has_xattention) { + query_shape = ov::PartialShape{num_queries, num_kv_heads, (num_heads / num_kv_heads), k_head_size}; + key_shape = ov::PartialShape{num_keys, num_kv_heads, 1, k_head_size}; + value_shape = ov::PartialShape{num_keys, num_kv_heads, 1, v_head_size}; + } + bool do_gqa_expand = false; + std::vector expanded_key_data; + std::vector expanded_value_data; + if (has_xattention) { + // Grouped Query Attention + do_gqa_expand = (num_heads != num_kv_heads); + if (do_gqa_expand) { + const int group_size = num_heads / num_kv_heads; + + expanded_key_data.resize(static_cast(num_keys) * static_cast(num_heads) * static_cast(k_head_size)); + expanded_value_data.resize(static_cast(num_keys) * static_cast(num_heads) * static_cast(v_head_size)); + + for (int key_idx = 0; key_idx < num_keys; ++key_idx) { + for (int h = 0; h < num_heads; ++h) { + const int src_kv_head = h / group_size; + size_t src_key_off = (static_cast(key_idx) * static_cast(num_kv_heads) + static_cast(src_kv_head)) * + static_cast(k_head_size); + size_t dst_key_off = + (static_cast(key_idx) * static_cast(num_heads) + static_cast(h)) * static_cast(k_head_size); + for (int d = 0; d < k_head_size; ++d) + expanded_key_data[dst_key_off + static_cast(d)] = key_data[src_key_off + static_cast(d)]; + + size_t src_val_off = (static_cast(key_idx) * static_cast(num_kv_heads) + static_cast(src_kv_head)) * + static_cast(v_head_size); + size_t dst_val_off = + (static_cast(key_idx) * static_cast(num_heads) + static_cast(h)) * static_cast(v_head_size); + for (int d = 0; d < v_head_size; ++d) + expanded_value_data[dst_val_off + static_cast(d)] = value_data[src_val_off + static_cast(d)]; + } + } + + key_shape = ov::PartialShape{1, num_keys, num_heads, k_head_size}; + value_shape = ov::PartialShape{1, num_keys, num_heads, v_head_size}; + num_kv_heads = num_heads; + } + } + + auto query_layout = cldnn::layout{query_shape, cldnn::data_types::f16, cldnn::format::bfyx}; + auto key_layout = cldnn::layout{key_shape, cldnn::data_types::f16, cldnn::format::bfyx}; + auto value_layout = cldnn::layout{value_shape, cldnn::data_types::f16, cldnn::format::bfyx}; + auto scale_layout = cldnn::layout({1}, cldnn::data_types::f16, cldnn::format::bfyx); + + OPENVINO_ASSERT(query_layout.count() == query_data.size()); + if (do_gqa_expand) { + OPENVINO_ASSERT(key_layout.count() == expanded_key_data.size()); + OPENVINO_ASSERT(value_layout.count() == expanded_value_data.size()); + } else { + OPENVINO_ASSERT(key_layout.count() == key_data.size()); + OPENVINO_ASSERT(value_layout.count() == value_data.size()); + } + + auto query_mem = test_engine.allocate_memory(query_layout); + auto key_mem = test_engine.allocate_memory(key_layout); + auto value_mem = test_engine.allocate_memory(value_layout); + auto scale_mem = test_engine.allocate_memory(scale_layout); + + tests::set_values(query_mem, query_data); + if (do_gqa_expand) { + tests::set_values(key_mem, expanded_key_data); + tests::set_values(value_mem, expanded_value_data); + } else { + tests::set_values(key_mem, key_data); + tests::set_values(value_mem, value_data); + } + tests::set_values(scale_mem, {static_cast(scale)}); + + ov::reference::XAttentionRetainedBlockIndicesForAllHeads retained_blocks; + if (num_queries >= static_cast(block_size) && has_xattention) { + auto reorder_qhk_to_hqd = [&](const std::vector& src, int outer_len, int num_heads, int head_dim) { + std::vector dst(num_heads * outer_len * head_dim); + for (int h = 0; h < num_heads; ++h) { + size_t dst_h_off = static_cast(h) * outer_len * head_dim; + for (int i = 0; i < outer_len; ++i) { + size_t src_off = static_cast(i) * num_heads * head_dim + static_cast(h) * head_dim; + std::copy_n(&src[src_off], head_dim, &dst[dst_h_off + static_cast(i) * head_dim]); + } + } + return dst; + }; + + const auto query_data_3d = reorder_qhk_to_hqd(query_data, num_queries, num_heads, k_head_size); + const auto key_data_3d = reorder_qhk_to_hqd(do_gqa_expand ? expanded_key_data : key_data, num_keys, num_heads, k_head_size); + const size_t padded_q = ((num_queries + block_size - 1) / block_size) * block_size; + const size_t padded_k = ((num_keys + block_size - 1) / block_size) * block_size; + + std::vector query_padded(num_heads * padded_q * k_head_size, 0.f); + std::vector key_padded(num_heads * padded_k * k_head_size, 0.f); + + for (int h = 0; h < num_heads; ++h) { + const auto* q_src = &query_data_3d[h * num_queries * k_head_size]; + const auto* k_src = &key_data_3d[h * num_keys * k_head_size]; + auto* q_dst = &query_padded[h * padded_q * k_head_size]; + auto* k_dst = &key_padded[h * padded_k * k_head_size]; + + std::transform(q_src, q_src + num_queries * k_head_size, q_dst, [](ov::float16 v) { + return static_cast(v); + }); + std::transform(k_src, k_src + num_keys * k_head_size, k_dst, [](ov::float16 v) { + return static_cast(v); + }); + } + ov::reference::XAttentionBlockSelector selector(xattention_threshold, block_size, stride); + retained_blocks = selector.select_blocks(query_padded.data(), + {static_cast(num_heads), padded_q, static_cast(k_head_size)}, + key_padded.data(), + {static_cast(num_heads), padded_k, static_cast(k_head_size)}); + } + auto mask_mem = get_mask_mem_combined_multi_head(num_queries, + num_keys, + num_heads, + num_kv_heads, + sliding_window_size, + retained_blocks, + static_cast(block_size), + qq_bias); + cldnn::topology topology; + if (num_heads == num_kv_heads) { + topology.add( + cldnn::input_layout("query", query_layout), + cldnn::input_layout("key", key_layout), + cldnn::input_layout("value", value_layout), + cldnn::data("mask", mask_mem), + cldnn::data("scale", scale_mem), + cldnn::permute("query_transposed", cldnn::input_info("query"), {0, 2, 1, 3}), + cldnn::permute("key_transposed", cldnn::input_info("key"), {0, 2, 3, 1}), + cldnn::permute("value_transposed", cldnn::input_info("value"), {0, 2, 1, 3}), + cldnn::gemm("qk_gemm", {cldnn::input_info("query_transposed"), cldnn::input_info("key_transposed")}, cldnn::data_types::f16, false, false), + cldnn::eltwise("scale_div", {cldnn::input_info("qk_gemm"), cldnn::input_info("scale")}, cldnn::eltwise_mode::prod), + cldnn::eltwise("eltwise", {cldnn::input_info("scale_div"), cldnn::input_info("mask")}, cldnn::eltwise_mode::sum), + cldnn::softmax("softmax", cldnn::input_info("eltwise"), -1), + cldnn::gemm("qkv_gemm", {cldnn::input_info("softmax"), cldnn::input_info("value_transposed")}, cldnn::data_types::f16, false, false), + cldnn::permute("qkv_gemm_transposed", cldnn::input_info("qkv_gemm"), {0, 2, 1, 3}), + cldnn::reorder("output_data", cldnn::input_info("qkv_gemm_transposed"), cldnn::format::bfyx, cldnn::data_types::f16), + cldnn::reorder("scores_data", cldnn::input_info("softmax"), cldnn::format::bfyx, cldnn::data_types::f16)); + } else { + topology.add( + cldnn::input_layout("query", query_layout), + cldnn::input_layout("key", key_layout), + cldnn::input_layout("value", value_layout), + cldnn::data("mask", mask_mem), + cldnn::data("scale", scale_mem), + cldnn::permute("query_transposed", cldnn::input_info("query"), {1, 2, 0, 3}), + cldnn::permute("key_transposed", cldnn::input_info("key"), {1, 2, 3, 0}), + cldnn::permute("value_transposed", cldnn::input_info("value"), {1, 2, 0, 3}), + cldnn::gemm("qk_gemm", {cldnn::input_info("query_transposed"), cldnn::input_info("key_transposed")}, cldnn::data_types::f16, false, false), + cldnn::eltwise("scale_div", {cldnn::input_info("qk_gemm"), cldnn::input_info("scale")}, cldnn::eltwise_mode::prod), + cldnn::eltwise("eltwise", {cldnn::input_info("scale_div"), cldnn::input_info("mask")}, cldnn::eltwise_mode::sum), + cldnn::softmax("softmax", cldnn::input_info("eltwise"), -1), + cldnn::gemm("qkv_gemm", {cldnn::input_info("softmax"), cldnn::input_info("value_transposed")}, cldnn::data_types::f16, false, false), + cldnn::reshape("qkv_gemm_reshape", cldnn::input_info("qkv_gemm"), {1, num_heads, v_head_size, num_queries}), + cldnn::permute("qkv_gemm_transposed", cldnn::input_info("qkv_gemm_reshape"), {0, 2, 1, 3}), + cldnn::reorder("output_data", cldnn::input_info("qkv_gemm_transposed"), cldnn::format::bfyx, cldnn::data_types::f16), + cldnn::reorder("scores_data", cldnn::input_info("softmax"), cldnn::format::bfyx, cldnn::data_types::f16)); + } + + ov::intel_gpu::ExecutionConfig config = tests::get_test_default_config(test_engine); + config.set_property(ov::intel_gpu::optimize_data(true)); + config.set_property(ov::intel_gpu::allow_new_shape_infer(true)); + + cldnn::network::ptr network = tests::get_network(test_engine, topology, config, tests::get_test_stream_ptr(), false); + network->set_input_data("query", query_mem); + network->set_input_data("key", key_mem); + network->set_input_data("value", value_mem); + + auto outputs = network->execute(); + + auto output_data_mem = outputs.at("output_data").get_memory(); + auto output_scores_mem = outputs.at("scores_data").get_memory(); + + return {get_output_data_vec(output_data_mem, num_queries, v_head_size, num_heads), + get_output_scores_vec(output_scores_mem, window_size, num_queries, num_keys, num_heads)}; + } + + std::vector get_output_scores_vec(cldnn::memory::ptr scores_output, int window_size, int num_queries, int num_keys, int num_heads) { + OPENVINO_ASSERT(scores_output->count() == static_cast(num_heads * num_queries * num_keys)); + + std::vector output_scores(num_keys, 0); + cldnn::mem_lock mem_ptr(scores_output, test_stream); + for (int row_idx = 0; row_idx < window_size; row_idx++) { + for (int head_idx = 0; head_idx < num_heads; head_idx++) { + for (int score_idx = 0; score_idx < num_keys; score_idx++) { + auto scores_offset = head_idx * num_queries * num_keys + (num_queries - window_size + row_idx) * num_keys + score_idx; + output_scores[score_idx] += mem_ptr[scores_offset]; + } + } + } + + return output_scores; + } + + std::vector get_output_data_vec(cldnn::memory::ptr data_output, int num_queries, int k_head_size, int num_heads) { + OPENVINO_ASSERT(data_output->count() == static_cast(num_queries * num_heads * k_head_size)); + + std::vector output_data(data_output->count()); + cldnn::mem_lock mem_ptr(data_output, test_stream); + for (size_t i = 0; i < data_output->count(); i++) + output_data[i] = mem_ptr[i]; + + return output_data; + } + + cldnn::memory::ptr get_mask_mem_combined_multi_head(int num_queries, + int num_keys, + int num_heads, + int num_kv_heads, + int sliding_window_size, + const ov::reference::XAttentionRetainedBlockIndicesForAllHeads& retained_blocks, + int block_size, + const std::vector* qq_bias) { + int heads_per_kv = num_heads / num_kv_heads; + + ov::PartialShape mask_shape; + if (retained_blocks.empty()) { + mask_shape = ov::PartialShape{1, 1, num_queries, num_keys}; + } else if (num_heads == num_kv_heads) { + mask_shape = ov::PartialShape{1, num_heads, num_queries, num_keys}; + } else { + mask_shape = ov::PartialShape{num_kv_heads, heads_per_kv, num_queries, num_keys}; + } + + auto mask_layout = cldnn::layout{mask_shape, cldnn::data_types::f16, cldnn::format::bfyx}; + auto mask_mem = test_engine.allocate_memory(mask_layout); + cldnn::mem_lock mem_ptr(mask_mem, test_stream); + + size_t total_elems = mask_layout.count(); + for (size_t i = 0; i < total_elems; ++i) + mem_ptr[i] = std::numeric_limits::lowest(); + if (retained_blocks.empty()) { + if (sliding_window_size == 0) { + int past_len = num_keys - num_queries + 1; + for (int i = 0; i < num_queries; i++) { + for (int j = 0; j < num_keys; j++) { + mem_ptr[i * num_keys + j] = j >= past_len + i ? std::numeric_limits::lowest() : ov::float16(0.f); + } + } + } else { + int sliding_left = num_keys - num_queries - sliding_window_size + 1; + int past_len = num_keys - num_queries + 1; + + for (int i = 0; i < num_queries; i++) { + for (int j = 0; j < num_keys; j++) { + bool is_min; + if (num_queries == num_keys) { + is_min = (j >= sliding_left + i) && (j <= i) ? 0 : 1; + } else { + is_min = (j >= sliding_left + i) && (j < past_len + i) ? 0 : 1; + } + + mem_ptr[i * num_keys + j] = is_min ? std::numeric_limits::lowest() : ov::float16(0.f); + } + } + } + } else { + for (int h = 0; h < num_heads; ++h) { + int kv_idx = (num_heads == num_kv_heads) ? 0 : (h / heads_per_kv); + int head_in_kv = (num_heads == num_kv_heads) ? h : (h % heads_per_kv); + + size_t head_offset = (static_cast(kv_idx) * heads_per_kv + static_cast(head_in_kv)) * static_cast(num_queries) * + static_cast(num_keys); + + for (int i = 0; i < num_queries; i++) { + int left_idx = 0; + int right_idx = 0; + + if (sliding_window_size == 0) { + int past_len = num_keys - num_queries + 1; + right_idx = past_len + i - 1; + left_idx = 0; + } else { + int sliding_left = num_keys - num_queries - sliding_window_size + 1; + int past_len = num_keys - num_queries + 1; + if (num_queries == num_keys) { + left_idx = sliding_left + i; + right_idx = i; + } else { + left_idx = sliding_left + i; + right_idx = past_len + i - 1; + } + } + + left_idx = std::max(0, left_idx); + right_idx = std::min(num_keys - 1, right_idx); + + for (const auto& [q_block_idx, k_block_idx] : retained_blocks[h]) { + int q_start = q_block_idx * block_size; + int q_end = std::min(q_start + block_size, num_queries); + int k_start = k_block_idx * block_size; + int k_end = std::min(k_start + block_size, num_keys); + + if (i < q_start || i >= q_end) + continue; + + for (int j = k_start; j < k_end; j++) { + if (j >= left_idx && j <= right_idx) { + mem_ptr[head_offset + i * num_keys + j] = ov::float16(0.f); + } + } + } + } + } + } + + if (qq_bias && !qq_bias->empty()) { + OPENVINO_ASSERT(qq_bias->size() == static_cast(num_queries * num_queries)); + + auto apply_mask_for_head = [&](size_t head_offset) { + for (int i = 0; i < num_queries; i++) { + for (int j = 0; j < num_queries; j++) { + if (!(*qq_bias)[static_cast(i) * static_cast(num_queries) + static_cast(j)]) { + mem_ptr[head_offset + static_cast(i) * static_cast(num_keys) + (num_keys - num_queries) + static_cast(j)] = + std::numeric_limits::lowest(); + } + } + } + }; + + if (retained_blocks.empty()) { + apply_mask_for_head(0); + } else { + for (int h = 0; h < num_heads; ++h) { + int kv_idx = (num_heads == num_kv_heads) ? 0 : (h / heads_per_kv); + int head_in_kv = (num_heads == num_kv_heads) ? h : (h % heads_per_kv); + size_t head_offset = (static_cast(kv_idx) * static_cast(heads_per_kv) + static_cast(head_in_kv)) * + static_cast(num_queries) * static_cast(num_keys); + apply_mask_for_head(head_offset); + } + } + } + + return mask_mem; + } + + void rotate_block(std::vector& cache_data, + std::vector rotation_deltas, + std::vector rotation_trig_lut_mem, + int rotated_block_idx, + int subsequence_rotated_block_idx, + int num_heads, + int k_head_size, + int block_size, + bool per_block) { + // cache_data shape: [1, num_tokens, num_heads, k_head_size] + int start_token_idx = subsequence_rotated_block_idx * block_size; + + for (int token_idx = 0; token_idx < block_size; token_idx++) { + auto rotation_deltas_offset = per_block ? rotated_block_idx : rotated_block_idx * block_size + token_idx; + auto rotation_trig_lut_idx = rotation_deltas[rotation_deltas_offset]; + for (int head_idx = 0; head_idx < num_heads; head_idx++) { + for (int k_head_size_idx = 0; k_head_size_idx < k_head_size / 2; k_head_size_idx++) { + auto input_offset = (start_token_idx + token_idx) * num_heads * k_head_size + head_idx * k_head_size + k_head_size_idx; + + auto cache_value_0 = cache_data[input_offset]; + auto cache_value_1 = cache_data[input_offset + k_head_size / 2]; + + ov::float16 rotation_value_cos = rotation_trig_lut_mem[rotation_trig_lut_idx * k_head_size + k_head_size_idx]; + ov::float16 rotation_value_sin = rotation_trig_lut_mem[rotation_trig_lut_idx * k_head_size + k_head_size_idx + k_head_size / 2]; + + cache_data[input_offset] = cache_value_0 * rotation_value_cos - cache_value_1 * rotation_value_sin; + cache_data[input_offset + k_head_size / 2] = cache_value_0 * rotation_value_sin + cache_value_1 * rotation_value_cos; + } + } + } + } + + std::vector read_key_from_cache(cldnn::memory::ptr key_cache_mem, size_t seq_idx, int total_tokens) { + // Read key vectors from key_cache memory + // key_cache layout: [num_blocks, num_kv_heads, head_size, block_size] + std::vector key_data(pam.num_kv_heads * total_tokens * pam.k_head_size); + + const int blocks_start = pam.block_indices_begins[seq_idx]; + const int blocks_end = pam.block_indices_begins[seq_idx + 1]; + const int num_blocks = blocks_end - blocks_start; + + const bool is_compressed = pam.kv_cache_compression; + + if (!is_compressed) { + // Uncompressed case: read as float16 + cldnn::mem_lock cache_ptr(key_cache_mem, test_stream); + + for (int block_idx = 0; block_idx < num_blocks; block_idx++) { + const int physical_block = pam.block_indices[blocks_start + block_idx]; + const int tokens_in_block = std::min(pam.block_size, total_tokens - block_idx * pam.block_size); + + for (int token_offset = 0; token_offset < tokens_in_block; token_offset++) { + const int token_idx = block_idx * pam.block_size + token_offset; + + for (int head_idx = 0; head_idx < pam.num_kv_heads; head_idx++) { + const size_t cache_base = static_cast(physical_block) * pam.num_kv_heads * pam.k_head_size * pam.block_size + + static_cast(head_idx) * pam.k_head_size * pam.block_size; + + const size_t output_base = + static_cast(head_idx) * total_tokens * pam.k_head_size + static_cast(token_idx) * pam.k_head_size; + + for (int dim = 0; dim < pam.k_head_size; dim++) { + const size_t cache_offset = cache_base + static_cast(dim) * pam.block_size + token_offset; + key_data[output_base + dim] = cache_ptr[cache_offset]; + } + } + } + } + } else { + if (pam.key_cache_quant_mode == ov::internal::CacheQuantMode::BY_CHANNEL) { + if (pam.is_int4_kv_cache()) { + // INT4 BY_CHANNEL: [num_blocks, kv_heads, k_head_size, block_size/2+4] u8 + // block_size dim is packed: 2 u4 tokens per byte. + // Comp at [d, packed_block..packed_block+3]: 2 fp16 = inv_scale, zp per head dim. + cldnn::mem_lock cache_ptr(key_cache_mem, test_stream); + const int packed_block = pam.block_size / 2; + const int adj_block_size = packed_block + 4; // block_size/2 + sizeof(fp16)*2 + + for (int block_idx = 0; block_idx < num_blocks; block_idx++) { + const int physical_block = pam.block_indices[blocks_start + block_idx]; + const int tokens_in_block = std::min(pam.block_size, total_tokens - block_idx * pam.block_size); + + for (int head_idx = 0; head_idx < pam.num_kv_heads; head_idx++) { + const size_t cache_base = static_cast(physical_block) * pam.num_kv_heads * pam.k_head_size * adj_block_size + + static_cast(head_idx) * pam.k_head_size * adj_block_size; + + for (int d = 0; d < pam.k_head_size; d++) { + // Read inv_scale and zp from comp region + const size_t comp_byte_off = cache_base + static_cast(d) * adj_block_size + packed_block; + const ov::float16* comp = reinterpret_cast(&cache_ptr[comp_byte_off]); + float inv_scale = static_cast(comp[0]); + float zp_val = static_cast(comp[1]); + + for (int token_offset = 0; token_offset < tokens_in_block; token_offset++) { + const int token_idx = block_idx * pam.block_size + token_offset; + const size_t byte_off = cache_base + static_cast(d) * adj_block_size + token_offset / 2; + uint8_t packed_byte = cache_ptr[byte_off]; + uint8_t q = (token_offset % 2 == 0) ? (packed_byte & 0xFu) : ((packed_byte >> 4) & 0xFu); + float dq = (static_cast(q) - zp_val) * inv_scale; + const size_t out_base = + static_cast(head_idx) * total_tokens * pam.k_head_size + static_cast(token_idx) * pam.k_head_size; + key_data[out_base + d] = ov::float16(dq); + } + } + } + } + } else { + // I8/U8 BY_CHANNEL: [num_blocks, num_kv_heads, head_size, block_size+4] + // Each dimension quantized across all tokens in block + cldnn::mem_lock cache_ptr(key_cache_mem, test_stream); + const int adj_block_size = pam.block_size + 4; + + for (int block_idx = 0; block_idx < num_blocks; block_idx++) { + const int physical_block = pam.block_indices[blocks_start + block_idx]; + const int tokens_in_block = std::min(pam.block_size, total_tokens - block_idx * pam.block_size); + + for (int head_idx = 0; head_idx < pam.num_kv_heads; head_idx++) { + const size_t cache_base = static_cast(physical_block) * pam.num_kv_heads * pam.k_head_size * adj_block_size + + static_cast(head_idx) * pam.k_head_size * adj_block_size; + + for (int dim = 0; dim < pam.k_head_size; dim++) { + // Read scale and zero-point for this dimension + const size_t scale_offset = cache_base + static_cast(dim) * adj_block_size + pam.block_size; + ov::float16 scale = *reinterpret_cast(&cache_ptr[scale_offset]); + ov::float16 zp = *reinterpret_cast(&cache_ptr[scale_offset + 2]); + + // Dequantize all tokens for this dimension + for (int token_offset = 0; token_offset < tokens_in_block; token_offset++) { + const int token_idx = block_idx * pam.block_size + token_offset; + const size_t cache_offset = cache_base + static_cast(dim) * adj_block_size + token_offset; + const size_t output_offset = + static_cast(head_idx) * total_tokens * pam.k_head_size + static_cast(token_idx) * pam.k_head_size + dim; + + int8_t quantized_value = cache_ptr[cache_offset]; + float dequantized = (static_cast(quantized_value) - static_cast(zp)) * static_cast(scale); + key_data[output_offset] = ov::float16(dequantized); + } + } + } + } + } + } else { + // BY_TOKEN + if (pam.is_int4_kv_cache()) { + // INT4 BY_TOKEN: [num_blocks, kv_heads, k_head_size/2+8, block_size] u8 + // Packing (SUBGROUP_SIZE=16): + // Y = pack_group*16 + sglid, where pack_group = d/(2*16), sglid = d%16 + // lower nibble = q(dim[pack_group*32 + sglid]) + // upper nibble = q(dim[pack_group*32 + sglid + 16]) + // Scale/ZP: fp16 in comp region at base + packed_head_size*block_size + // inv_scale[t] at comp_ptr[t], zp[t] at comp_ptr[block_size + t] + cldnn::mem_lock cache_ptr(key_cache_mem, test_stream); + const int packed_head_size = pam.k_head_size / 2; + const int adj_head_size = packed_head_size + 8; + constexpr int SG = 16; + + for (int block_idx = 0; block_idx < num_blocks; block_idx++) { + const int physical_block = pam.block_indices[blocks_start + block_idx]; + const int tokens_in_block = std::min(pam.block_size, total_tokens - block_idx * pam.block_size); + + for (int head_idx = 0; head_idx < pam.num_kv_heads; head_idx++) { + const size_t cache_base = static_cast(physical_block) * pam.num_kv_heads * adj_head_size * pam.block_size + + static_cast(head_idx) * adj_head_size * pam.block_size; + // Scale and ZP are in the comp region after the packed data + const size_t comp_byte_base = cache_base + static_cast(packed_head_size) * pam.block_size; + const auto* inv_scale_arr = reinterpret_cast(&cache_ptr[comp_byte_base]); + const auto* zp_arr = reinterpret_cast(&cache_ptr[comp_byte_base + pam.block_size * 2]); + + for (int token_offset = 0; token_offset < tokens_in_block; token_offset++) { + const int token_idx = block_idx * pam.block_size + token_offset; + float inv_scale = static_cast(inv_scale_arr[token_offset]); + float zp_val = static_cast(zp_arr[token_offset]); + const size_t out_base = + static_cast(head_idx) * total_tokens * pam.k_head_size + static_cast(token_idx) * pam.k_head_size; + + for (int d = 0; d < pam.k_head_size; d++) { + int sglid_val = d % SG; + int pack_group = d / (2 * SG); + int group_in_pack = (d / SG) % 2; // 0=lower nibble, 1=upper nibble + int y = pack_group * SG + sglid_val; + const size_t byte_off = cache_base + static_cast(y) * pam.block_size + token_offset; + uint8_t packed_byte = cache_ptr[byte_off]; + uint8_t q_d = (group_in_pack == 0) ? (packed_byte & 0xFu) : ((packed_byte >> 4) & 0xFu); + key_data[out_base + d] = ov::float16((static_cast(q_d) - zp_val) * inv_scale); + } + } + } + } + + return key_data; + } + + // BY_TOKEN: [num_blocks, num_kv_heads, head_size+4, block_size] + // Token-wise quantization with shared scale/zp per token + // Layout: data rows [0..head_size-1], scale at [head_size], zp at [head_size+2] (fp16) + cldnn::mem_lock cache_ptr(key_cache_mem, test_stream); + for (int block_idx = 0; block_idx < num_blocks; block_idx++) { + const int physical_block = pam.block_indices[blocks_start + block_idx]; + const int tokens_in_block = std::min(pam.block_size, total_tokens - block_idx * pam.block_size); + + for (int token_offset = 0; token_offset < tokens_in_block; token_offset++) { + const int token_idx = block_idx * pam.block_size + token_offset; + + for (int head_idx = 0; head_idx < pam.num_kv_heads; head_idx++) { + const size_t cache_base = static_cast(physical_block) * pam.num_kv_heads * (pam.k_head_size + 4) * pam.block_size + + static_cast(head_idx) * (pam.k_head_size + 4) * pam.block_size; + + // Read scale and zero-point for this token + // Scale is at [head_size][token], ZP is at [head_size+2][token] (2 rows below) + // token_offset * 2 because each half is 2 bytes (token 0 at offset 0-1, token 1 at offset 2-3, etc.) + const size_t scale_offset = cache_base + static_cast(pam.k_head_size) * pam.block_size + token_offset * 2; + const size_t zp_offset = scale_offset + 2 * pam.block_size; // ZP is 2 rows below scale + ov::float16 scale = *reinterpret_cast(&cache_ptr[scale_offset]); + ov::float16 zp = *reinterpret_cast(&cache_ptr[zp_offset]); + + const size_t output_base = + static_cast(head_idx) * total_tokens * pam.k_head_size + static_cast(token_idx) * pam.k_head_size; + + // Dequantize all dimensions for this token + for (int dim = 0; dim < pam.k_head_size; dim++) { + const size_t cache_offset = cache_base + static_cast(dim) * pam.block_size + token_offset; + + int8_t quantized_value = cache_ptr[cache_offset]; + float dequantized = (static_cast(quantized_value) - static_cast(zp)) * static_cast(scale); + + key_data[output_base + dim] = ov::float16(dequantized); + } + } + } + } + } // end else (i8 BY_TOKEN) + } // is_compressed + + return key_data; + } + + std::vector compute_diversity_reference(cldnn::memory::ptr key_cache_mem) { + std::vector diversity_output; + + for (size_t seq_idx = 0; seq_idx < pam.subsequence_descs.size(); seq_idx++) { + const auto start_size = pam.adaptive_rkv_start_size; + const auto evictable_size = pam.adaptive_rkv_evictable_sizes[seq_idx]; + + // Read key data from key_cache instead of original key_data + const auto& subsequence_desc = pam.subsequence_descs[seq_idx]; + const auto total_tokens = subsequence_desc.num_tokens + subsequence_desc.past_len; + + // Extract key vectors from key_cache memory + std::vector key_data = read_key_from_cache(key_cache_mem, seq_idx, total_tokens); + + ov::Shape key_shape = {static_cast(pam.num_kv_heads), static_cast(total_tokens), static_cast(pam.k_head_size)}; + + // Use reference implementation + ov::reference::AdaptiveRKVDiversityCalculator calculator(start_size, evictable_size, pam.block_size); + + auto block_diversity = calculator.calculate_block_diversity(key_data.data(), key_shape); + + const size_t num_evictable_blocks = static_cast(evictable_size) / static_cast(pam.block_size); + // Flatten 2D to 1D: [num_evictable_blocks, evictable_size] -> [num_evictable_blocks * evictable_size] + for (size_t block_idx = 0; block_idx < num_evictable_blocks; block_idx++) { + for (size_t token_idx = 0; token_idx < static_cast(evictable_size); token_idx++) { + diversity_output.push_back(block_diversity[block_idx][token_idx]); + } + } + } + + return diversity_output; + } + + PagedAttentionManager& pam; + cldnn::engine& test_engine; + cldnn::stream& test_stream; +}; + +template +struct PagedAttentionTest : public ::testing::TestWithParam { +public: + tests::random_generator rg; + cldnn::engine& engine = tests::get_test_engine(); + float tolerance = 2e-3; + cldnn::memory::ptr last_key_cache_mem = nullptr; + cldnn::memory::ptr last_output_data_mem = nullptr; + std::vector last_block_indices; + std::vector last_block_indices_begins; + std::optional pam; + + void SetUp() override { + rg.set_seed(GET_SUITE_NAME); + + auto p = this->GetParam(); + + pam.emplace(rg, + tests::get_test_engine(), + tests::get_test_stream(), + p.subsequences, + p.num_heads, + p.num_kv_heads, + p.k_head_size, + p.v_head_size, + p.block_size, + p.sliding_window_size, + p.kv_cache_compression, + p.key_cache_quant_mode, + p.scores_mode == ScoresMode::SNAPKV, + p.has_xattention, + p.rotation_config, + p.kv_cache_precision); + } + + std::vector get_output_data() { + OPENVINO_ASSERT(last_output_data_mem != nullptr, "No output data available"); + std::vector result(last_output_data_mem->count()); + cldnn::mem_lock mem_ptr(last_output_data_mem, tests::get_test_stream()); + for (size_t i = 0; i < last_output_data_mem->count(); i++) + result[i] = mem_ptr[i]; + return result; + } + + static std::vector to_float16(const std::vector& data) { + std::vector result(data.size()); + std::transform(data.begin(), data.end(), result.begin(), [](float value) { + return ov::float16(value); + }); + return result; + } + + struct gpu_outputs { + std::map outputs; + cldnn::memory::ptr key_cache_mem; + }; + + gpu_outputs run_gpu_inference(PagedAttentionManager& pam, T& p) { + gpu_outputs result; + + ov::element::Type kv_cache_precision = p.kv_cache_precision; + + if (p.has_xattention) { + pam.xattention_block_size.clear(); + if (p.xattention_block_size.has_value()) { + pam.xattention_block_size = p.xattention_block_size.value(); + } + pam.xattention_threshold.clear(); + if (p.xattention_threshold.has_value()) { + pam.xattention_threshold.reserve(p.xattention_threshold->size()); + for (const float t : p.xattention_threshold.value()) { + pam.xattention_threshold.emplace_back(static_cast(t)); + } + } + // Keep xattention_stride non-empty and per-sequence, reducing the mismatch risk with always-bound stride input. + pam.xattention_stride.assign(p.subsequences.size(), 16); + } + + if (p.has_adaptive_rkv) { + pam.adaptive_rkv_diversity_block_set_indices_begins.push_back(0); + pam.adaptive_rkv_start_size = p.start_size; + + for (size_t i = 0; i < p.subsequences.size(); i++) { + // Use per-sequence evictable_size if available, otherwise use first value + int evictable_size = i < p.evictable_sizes.size() ? p.evictable_sizes[i] : p.evictable_sizes[0]; + + pam.adaptive_rkv_evictable_sizes.push_back(evictable_size); + + int start_block = p.start_size / p.block_size; + int evictable_blocks = evictable_size / p.block_size; + int global_start_block_idx = pam.block_indices_begins[i] + start_block; + + for (int b = 0; b < evictable_blocks; b++) { + pam.adaptive_rkv_diversity_block_set_indices.push_back(global_start_block_idx + b); + } + + int prev_begin = pam.adaptive_rkv_diversity_block_set_indices_begins.back(); + pam.adaptive_rkv_diversity_block_set_indices_begins.push_back(prev_begin + evictable_blocks); + } + } + + if (p.has_qq_bias) { + pam.qq_bias = p.qq_bias_config.qq_bias; + pam.qq_bias_begins = p.qq_bias_config.qq_bias_begins; + } + + if (p.token_type_ids.has_value()) { + pam.token_type_ids = p.token_type_ids.value(); + EXPECT_EQ(pam.token_type_ids.size(), static_cast(pam.subsequence_descs.back().num_tokens + pam.subsequence_descs.back().past_len)); + } + + if (p.has_sink_input && p.sink_values.has_value()) { + pam.sinks = p.sink_values.value(); + } + + if (p.kv_cache_compression) { + // INT4 quantization has larger error than INT8 (~17x larger step size) + tolerance = (kv_cache_precision == ov::element::u4 || kv_cache_precision == ov::element::i4) ? 75e-3 : 25e-3; + } + + auto query_mem = pam.get_query_memory(); + auto key_mem = pam.get_key_memory(); + auto value_mem = pam.get_value_memory(); + + if (p.has_xattention) { + result.key_cache_mem = pam.get_key_cache_memory_cm(); + } else { + result.key_cache_mem = pam.get_key_cache_memory(); + } + auto value_cache_mem = pam.get_value_cache_memory(); + + auto past_lens_mem = pam.get_past_lens_memory(); + auto subsequence_begins_mem = pam.get_subsequence_begins_memory(); + auto block_indices_mem = pam.get_block_indices_memory(); + auto block_indices_begins_mem = pam.get_block_indices_begins_memory(); + + auto scale_mem = pam.get_scale_memory(); + auto sliding_window_mem = pam.get_sliding_window_memory(); + auto alibi_mem = pam.get_alibi_memory(); + auto max_context_len_mem = pam.get_max_context_len_memory(); + + // scores calculation related memory buffers + auto score_aggregation_mem = pam.get_score_aggregation(); + + // cache rotation related memory buffers + auto rotated_block_indices_mem = pam.get_rotated_block_indices_memory(); + auto rotation_deltas_mem = pam.get_rotation_deltas_memory(); + auto rotation_trig_lut_mem = pam.get_rotation_trig_lut_memory(); + + auto xattention_threshold_mem = pam.get_xattention_threshold_memory(); + auto xattention_block_size_mem = pam.get_xattention_block_size_memory(); + auto xattention_stride_mem = pam.get_xattention_stride_memory(); + auto sinks_mem = pam.get_sinks_memory(); + auto adaptive_rkv_start_size_mem = pam.get_adaptive_rkv_start_size_memory(); + auto adaptive_rkv_evictable_sizes_mem = pam.get_adaptive_rkv_evictable_sizes_memory(); + auto adaptive_rkv_diversity_block_set_indices_mem = pam.get_adaptive_rkv_diversity_block_set_indices_memory(); + auto adaptive_rkv_diversity_block_set_indices_begins_mem = pam.get_adaptive_rkv_diversity_block_set_indices_begins_memory(); + auto token_type_ids_mem = pam.get_token_type_ids_memory(); + + auto qq_bias = pam.get_qq_bias_memory(); + auto qq_bias_begins = pam.get_qq_bias_begins_memory(); + auto query_layout = query_mem->get_layout(); + auto key_layout = key_mem->get_layout(); + auto value_layout = value_mem->get_layout(); + auto key_cache_layout = result.key_cache_mem->get_layout(); + auto value_cache_layout = value_cache_mem->get_layout(); + auto past_lens_layout = past_lens_mem->get_layout(); + auto subsequence_begins_layout = subsequence_begins_mem->get_layout(); + auto block_indices_layout = block_indices_mem->get_layout(); + auto block_indices_begins_layout = block_indices_begins_mem->get_layout(); + auto scale_layout = scale_mem->get_layout(); + auto sliding_window_layout = sliding_window_mem->get_layout(); + auto alibi_layout = alibi_mem->get_layout(); + auto max_context_len_layout = max_context_len_mem->get_layout(); + auto score_aggregation_window_layout = score_aggregation_mem->get_layout(); + auto rotated_block_indices_layout = rotated_block_indices_mem->get_layout(); + auto rotation_deltas_layout = rotation_deltas_mem->get_layout(); + auto rotation_trig_lut_layout = rotation_trig_lut_mem->get_layout(); + auto xattention_threshold_layout = xattention_threshold_mem->get_layout(); + auto xattention_block_size_layout = xattention_block_size_mem->get_layout(); + auto xattention_stride_layout = xattention_stride_mem->get_layout(); + auto sinks_layout = sinks_mem->get_layout(); + auto adaptive_rkv_start_size_layout = adaptive_rkv_start_size_mem->get_layout(); + auto adaptive_rkv_evictable_sizes_layout = adaptive_rkv_evictable_sizes_mem->get_layout(); + auto adaptive_rkv_diversity_block_set_indices_layout = adaptive_rkv_diversity_block_set_indices_mem->get_layout(); + auto adaptive_rkv_diversity_block_set_indices_begins_layout = adaptive_rkv_diversity_block_set_indices_begins_mem->get_layout(); + auto token_type_ids_layout = token_type_ids_mem->get_layout(); + auto qq_bias_layout = qq_bias->get_layout(); + auto qq_bias_begins_layout = qq_bias_begins->get_layout(); + + // make layouts dynamic + query_layout.set_partial_shape(ov::PartialShape{-1, p.num_heads * p.k_head_size}); + key_layout.set_partial_shape(ov::PartialShape{-1, p.num_kv_heads * p.k_head_size}); + value_layout.set_partial_shape(ov::PartialShape{-1, p.num_kv_heads * p.v_head_size}); + // key_cache_layout.set_partial_shape(ov::PartialShape{ -1, p.num_heads, p.k_head_size, p.block_size }); + { + auto pshape = key_cache_layout.get_partial_shape(); + pshape[0] = -1; + key_cache_layout.set_partial_shape(pshape); + } + // value_cache_layout.set_partial_shape(ov::PartialShape{ -1, p.num_heads, p.block_size, p.v_head_size }); + { + auto pshape = value_cache_layout.get_partial_shape(); + pshape[0] = -1; + value_cache_layout.set_partial_shape(pshape); + } + past_lens_layout.set_partial_shape(ov::PartialShape{-1}); + subsequence_begins_layout.set_partial_shape(ov::PartialShape{-1}); + block_indices_layout.set_partial_shape(ov::PartialShape{-1}); + block_indices_begins_layout.set_partial_shape(ov::PartialShape{-1}); + score_aggregation_window_layout.set_partial_shape(ov::PartialShape{-1}); + rotated_block_indices_layout.set_partial_shape(ov::PartialShape{-1}); + rotation_deltas_layout.set_partial_shape(ov::PartialShape{-1, -1}); + rotation_trig_lut_layout.set_partial_shape(ov::PartialShape{-1, p.k_head_size}); + xattention_threshold_layout.set_partial_shape(ov::PartialShape{-1}); + adaptive_rkv_evictable_sizes_layout.set_partial_shape(ov::PartialShape{-1}); + adaptive_rkv_diversity_block_set_indices_layout.set_partial_shape(ov::PartialShape{-1}); + adaptive_rkv_diversity_block_set_indices_begins_layout.set_partial_shape(ov::PartialShape{-1}); + qq_bias_layout.set_partial_shape(ov::PartialShape{-1}); + qq_bias_begins_layout.set_partial_shape(ov::PartialShape{-1}); + + if (p.dynamic_paddings) { + const auto padding_axis = 1; + const auto pad_before = p.k_head_size; + const auto pad_after = p.k_head_size * 2; + + query_layout.data_padding._dynamic_dims_mask[padding_axis] = 1; + + auto query_data_layout = query_mem->get_layout(); + auto padded_query_data_layout = query_data_layout; + padded_query_data_layout.data_padding._lower_size[padding_axis] = pad_before; + padded_query_data_layout.data_padding._upper_size[padding_axis] = pad_after; + + auto new_query_memory = tests::get_test_engine().allocate_memory(padded_query_data_layout, false); + + cldnn::mem_lock query_mem_lock(query_mem, tests::get_test_stream()); + cldnn::mem_lock new_query_mem_lock(new_query_memory, tests::get_test_stream()); + + auto query_data_shape = query_data_layout.get_shape(); + for (size_t b = 0; b < query_data_shape[0]; b++) { + for (size_t f = 0; f < query_data_shape[1]; f++) { + auto input_offset = query_data_layout.get_linear_offset(cldnn::tensor(static_cast(b), static_cast(f), 0, 0, 0, 0)); + auto output_offset = + padded_query_data_layout.get_linear_offset(cldnn::tensor(static_cast(b), static_cast(f), 0, 0, 0, 0)); + + new_query_mem_lock[output_offset] = query_mem_lock[input_offset]; + } + } + query_mem = new_query_memory; + } + + std::vector pa_inputs = {cldnn::input_info("query"), + cldnn::input_info("key"), + cldnn::input_info("value"), + cldnn::input_info("key_cache"), + cldnn::input_info("value_cache"), + cldnn::input_info("past_lens"), + cldnn::input_info("subsequence_begins"), + cldnn::input_info("block_indices"), + cldnn::input_info("block_indices_begins"), + cldnn::input_info("scale"), + cldnn::input_info("sliding_window"), + cldnn::input_info("alibi"), + cldnn::input_info("max_context_len"), + cldnn::input_info("score_aggregation_window"), + cldnn::input_info("rotated_block_indices"), + cldnn::input_info("rotation_deltas"), + cldnn::input_info("rotation_trig_lut_modified"), + cldnn::input_info("xattention_threshold"), + cldnn::input_info("xattention_block_size"), + cldnn::input_info("xattention_stride"), + cldnn::input_info("sinks"), + cldnn::input_info("adaptive_rkv_start_size"), + cldnn::input_info("adaptive_rkv_evictable_sizes"), + cldnn::input_info("adaptive_rkv_diversity_block_set_indices"), + cldnn::input_info("adaptive_rkv_diversity_block_set_indices_begins"), + cldnn::input_info("token_type_ids"), + cldnn::input_info("qq_bias"), + cldnn::input_info("qq_bias_begins")}; + + auto pa_prim = cldnn::paged_attention("paged_attention", pa_inputs); + + pa_prim.k_head_size = p.k_head_size; + pa_prim.v_head_size = p.v_head_size; + pa_prim.kv_heads_num = p.num_kv_heads; + pa_prim.heads_num = p.num_heads; + pa_prim.scale_val = pam.get_default_scale(); + pa_prim.has_alibi = false; + pa_prim.has_token_type_ids = p.token_type_ids.has_value() || p.has_sink_input; + pa_prim.has_sink_input = p.has_sink_input; + + int num_outputs = 1; + if (p.scores_mode != ScoresMode::DISABLED) + num_outputs++; + if (p.has_adaptive_rkv) + num_outputs++; + pa_prim.num_outputs = num_outputs; + pa_prim.has_rotated_blocks = p.rotation_config.apply_rotation; + pa_prim.has_score_aggregation = p.scores_mode == ScoresMode::SNAPKV; + pa_prim.has_adaptive_rkv = p.has_adaptive_rkv; + pa_prim.sliding_window = p.sliding_window_size; + pa_prim.is_key_by_channel = (p.key_cache_quant_mode == ov::internal::CacheQuantMode::BY_CHANNEL); + if (p.has_xattention) { + pa_prim.has_xattention = true; + } + + pa_prim.has_qq_bias = p.has_qq_bias; + + cldnn::topology topology; + + topology.add(cldnn::input_layout("query", query_layout), + cldnn::input_layout("key", key_layout), + cldnn::input_layout("value", value_layout), + cldnn::input_layout("key_cache", key_cache_layout), + cldnn::input_layout("value_cache", value_cache_layout), + cldnn::input_layout("past_lens", past_lens_layout), + cldnn::input_layout("subsequence_begins", subsequence_begins_layout), + cldnn::input_layout("block_indices", block_indices_layout), + cldnn::input_layout("block_indices_begins", block_indices_begins_layout), + cldnn::input_layout("scale", scale_layout), + cldnn::input_layout("sliding_window", sliding_window_layout), + cldnn::input_layout("alibi", alibi_layout), + cldnn::input_layout("max_context_len", max_context_len_layout), + cldnn::input_layout("score_aggregation_window", score_aggregation_window_layout), + pa_prim, + cldnn::reorder("output_data", cldnn::input_info("paged_attention", 0), cldnn::format::bfyx, cldnn::data_types::f16)); + + int output_idx = 1; + if (p.scores_mode != ScoresMode::DISABLED) { + topology.add(cldnn::reorder("output_scores", cldnn::input_info("paged_attention", output_idx), cldnn::format::bfyx, cldnn::data_types::f16)); + output_idx++; + } + if (p.has_adaptive_rkv) { + topology.add(cldnn::reorder("output_diversity", cldnn::input_info("paged_attention", output_idx), cldnn::format::bfyx, cldnn::data_types::f16)); + } + + { + topology.add(cldnn::input_layout("rotated_block_indices", rotated_block_indices_layout)); + topology.add(cldnn::input_layout("rotation_deltas", rotation_deltas_layout)); + topology.add(cldnn::input_layout("rotation_trig_lut", rotation_trig_lut_layout)); + + // add dummy activation operation to simulate an empty PA `rotation_trig_lut` buffer for shapes like [0, k_head_size] + topology.add(cldnn::activation("rotation_trig_lut_modified", cldnn::input_info("rotation_trig_lut"), cldnn::activation_func::none)); + + topology.add(cldnn::input_layout("xattention_threshold", xattention_threshold_layout)); + topology.add(cldnn::input_layout("xattention_block_size", xattention_block_size_layout)); + topology.add(cldnn::input_layout("xattention_stride", xattention_stride_layout)); + topology.add(cldnn::input_layout("sinks", sinks_layout)); + + topology.add(cldnn::input_layout("adaptive_rkv_start_size", adaptive_rkv_start_size_layout)); + topology.add(cldnn::input_layout("adaptive_rkv_evictable_sizes", adaptive_rkv_evictable_sizes_layout)); + topology.add(cldnn::input_layout("adaptive_rkv_diversity_block_set_indices", adaptive_rkv_diversity_block_set_indices_layout)); + topology.add(cldnn::input_layout("adaptive_rkv_diversity_block_set_indices_begins", adaptive_rkv_diversity_block_set_indices_begins_layout)); + topology.add(cldnn::input_layout("token_type_ids", token_type_ids_layout)); + topology.add(cldnn::input_layout("qq_bias", qq_bias_layout)); + topology.add(cldnn::input_layout("qq_bias_begins", qq_bias_begins_layout)); + } + + ov::intel_gpu::ExecutionConfig config = tests::get_test_default_config(tests::get_test_engine()); + config.set_property(ov::intel_gpu::optimize_data(true)); + config.set_property(ov::intel_gpu::allow_new_shape_infer(true)); + // FlashAttn v1 or v2? + config.set_property(ov::intel_gpu::could_use_flashattn_v2(p.force_flashattn_v2 ? true : p.disable_flashattn_v2)); + config.set_property(ov::internal::key_cache_quant_mode(p.key_cache_quant_mode)); + if (kv_cache_precision != ov::element::dynamic) { + config.set_property(ov::hint::kv_cache_precision(kv_cache_precision)); + } + cldnn::network::ptr network = tests::get_network(tests::get_test_engine(), topology, config, tests::get_test_stream_ptr(), false); + network->set_input_data("query", query_mem); + network->set_input_data("key", key_mem); + network->set_input_data("value", value_mem); + network->set_input_data("key_cache", result.key_cache_mem); + network->set_input_data("value_cache", value_cache_mem); + network->set_input_data("past_lens", past_lens_mem); + network->set_input_data("subsequence_begins", subsequence_begins_mem); + network->set_input_data("block_indices", block_indices_mem); + network->set_input_data("block_indices_begins", block_indices_begins_mem); + network->set_input_data("scale", scale_mem); + network->set_input_data("sliding_window", sliding_window_mem); + network->set_input_data("alibi", alibi_mem); + network->set_input_data("max_context_len", max_context_len_mem); + network->set_input_data("score_aggregation_window", score_aggregation_mem); + network->set_input_data("rotated_block_indices", rotated_block_indices_mem); + network->set_input_data("rotation_deltas", rotation_deltas_mem); + network->set_input_data("rotation_trig_lut", rotation_trig_lut_mem); + network->set_input_data("xattention_threshold", xattention_threshold_mem); + network->set_input_data("xattention_block_size", xattention_block_size_mem); + + network->set_input_data("xattention_stride", xattention_stride_mem); + network->set_input_data("sinks", sinks_mem); + network->set_input_data("adaptive_rkv_start_size", adaptive_rkv_start_size_mem); + network->set_input_data("adaptive_rkv_evictable_sizes", adaptive_rkv_evictable_sizes_mem); + network->set_input_data("adaptive_rkv_diversity_block_set_indices", adaptive_rkv_diversity_block_set_indices_mem); + network->set_input_data("adaptive_rkv_diversity_block_set_indices_begins", adaptive_rkv_diversity_block_set_indices_begins_mem); + network->set_input_data("token_type_ids", token_type_ids_mem); + network->set_input_data("qq_bias", qq_bias); + network->set_input_data("qq_bias_begins", qq_bias_begins); + + last_key_cache_mem = result.key_cache_mem; + last_block_indices = pam.block_indices; + last_block_indices_begins = pam.block_indices_begins; + + result.outputs = network->execute(); + + last_output_data_mem = result.outputs.at("output_data").get_memory(); + + return result; + } + + void execute(T& p, bool run_reference = true) { + ASSERT_TRUE(this->pam.has_value()); + auto& pam = *this->pam; + + auto result = run_gpu_inference(pam, p); + + if (!run_reference) { + return; + } + + cldnn::memory::ptr output_data_mem = last_output_data_mem; + cldnn::memory::ptr output_scores_mem = nullptr; + cldnn::memory::ptr output_diversity_mem = nullptr; + + output_data_mem = result.outputs.at("output_data").get_memory(); + if (p.scores_mode != ScoresMode::DISABLED) { + output_scores_mem = result.outputs.at("output_scores").get_memory(); + } + if (p.has_adaptive_rkv) { + output_diversity_mem = result.outputs.at("output_diversity").get_memory(); + } + + // Verify KV cache was correctly written (CM PA path only) + // NOTE: This verification is specific to CM PA layout and should NOT run for OCL micro_sdpa + // because they use different layouts (key cache is [N,K,H,B] in OCL vs [N,K,B,H] in CM) + verify_cm_kv_cache_write(p); + + auto ref_data = PagedAttentionReference(pam).get_reference(result.key_cache_mem); + if (p.has_xattention) { + compare_xattention(output_data_mem, output_scores_mem, ref_data, p.num_heads, p.k_head_size); + } else { + compare(output_data_mem, output_scores_mem, output_diversity_mem, ref_data); + } + } + + void compare(cldnn::memory::ptr data_output_mem, + cldnn::memory::ptr scores_output_mem, + cldnn::memory::ptr diversity_output_mem, + std::tuple, std::vector, std::vector> ref_data) { + if (data_output_mem) { + ASSERT_EQ(data_output_mem->count(), std::get<0>(ref_data).size()); + cldnn::mem_lock mem_ptr(data_output_mem, tests::get_test_stream()); + for (size_t i = 0; i < data_output_mem->count(); i++) { + ASSERT_NEAR(mem_ptr[i], std::get<0>(ref_data)[i], tolerance) << " at index=" << i; + } + } + + if (scores_output_mem) { + ASSERT_EQ(scores_output_mem->count(), std::get<1>(ref_data).size()); + cldnn::mem_lock mem_ptr(scores_output_mem, tests::get_test_stream()); + for (size_t i = 0; i < scores_output_mem->count(); i++) { + ASSERT_NEAR(mem_ptr[i], std::get<1>(ref_data)[i], tolerance) << " at index=" << i; + } + } + + if (diversity_output_mem) { + ASSERT_EQ(diversity_output_mem->count(), std::get<2>(ref_data).size()); + cldnn::mem_lock mem_ptr(diversity_output_mem, tests::get_test_stream()); + // Relaxed tolerance due to float32 (GPU) vs float16 (reference) accumulator difference + float diversity_tolerance = tolerance * 10.0f; + for (size_t i = 0; i < diversity_output_mem->count(); i++) { + ASSERT_NEAR(mem_ptr[i], std::get<2>(ref_data)[i], diversity_tolerance) << " at index=" << i; + } + } + } + + void compare_xattention(cldnn::memory::ptr data_output_mem, + cldnn::memory::ptr scores_output_mem, + std::tuple, std::vector, std::vector> ref_data, + size_t num_heads, + size_t head_size) { + if (data_output_mem) { + ASSERT_EQ(data_output_mem->count(), std::get<0>(ref_data).size()); + cldnn::mem_lock mem_ptr(data_output_mem, tests::get_test_stream()); + int mismatch_count = 0; + +#if XATTENTION_DEBUG_VERBOSE + XAttentionErrorStats stats; + collect_xattention_error_stats(mem_ptr.data(), std::get<0>(ref_data), num_heads, head_size, tolerance, stats); + mismatch_count = stats.mismatch_count; + + // Print detailed statistics on failure + if (mismatch_count > int(data_output_mem->count() * 0.04)) { + print_xattention_error_details(stats, mem_ptr.data(), std::get<0>(ref_data), num_heads, head_size, tolerance); + } +#else + // Simple counting when verbose debug is disabled + for (size_t i = 0; i < data_output_mem->count(); i++) { + float actual = static_cast(mem_ptr[i]); + float expected = static_cast(std::get<0>(ref_data)[i]); + float error = std::fabs(actual - expected); + if (error > tolerance) { + mismatch_count++; + } + } +#endif + + EXPECT_LE(mismatch_count, int(data_output_mem->count() * 0.04)); + } + + if (scores_output_mem) { + ASSERT_EQ(scores_output_mem->count(), std::get<1>(ref_data).size()); + cldnn::mem_lock mem_ptr(scores_output_mem, tests::get_test_stream()); + int mismatch_count = 0; + for (size_t i = 0; i < scores_output_mem->count(); i++) { + if (std::fabs(static_cast(mem_ptr[i]) - static_cast(std::get<1>(ref_data)[i])) > tolerance) { + mismatch_count++; + } + } + EXPECT_LE(mismatch_count, int(scores_output_mem->count() * 0.04)); + } + } + +private: + // Helper: Verify CM PA KV cache was correctly written (CM path only) + void verify_cm_kv_cache_write(const T& p) { + if (last_key_cache_mem == nullptr || !p.has_xattention) + return; + + // Count total tokens for single-token BY_CHANNEL skip logic + int total_new_tokens = 0; + for (const auto& s : p.subsequences) + total_new_tokens += s.num_tokens; + + constexpr int kv_sub_block_size = 16; + const bool is_by_channel = p.kv_cache_compression && p.key_cache_quant_mode == ov::internal::CacheQuantMode::BY_CHANNEL; + const bool is_by_token = p.kv_cache_compression && p.key_cache_quant_mode == ov::internal::CacheQuantMode::BY_TOKEN; + + // CM cache layout: [num_blocks, num_kv_heads, adjusted_block_size, adjusted_head_size] + const int adj_head_size = is_by_token ? (p.k_head_size + 4) : p.k_head_size; + const int adj_block_size = is_by_channel ? (p.block_size + (p.block_size / kv_sub_block_size) * 4) : p.block_size; + const int elem_size = p.kv_cache_compression ? 1 : 2; + const size_t head_region_bytes = static_cast(adj_block_size) * adj_head_size * elem_size; + const size_t block_stride_bytes = static_cast(p.num_kv_heads) * head_region_bytes; + const int token_data_stride = p.k_head_size * elem_size; + + cldnn::mem_lock cache_lock(last_key_cache_mem, tests::get_test_stream()); + + int missing_count = 0, nan_count = 0, inf_count = 0, zero_scale_count = 0, out_of_range_zp_count = 0; + std::vector> nan_locations, inf_locations; + std::vector> zero_scale_locations, out_of_range_zp_locations; + + int total_tokens = 0; + for (int i = 0; i < static_cast(p.subsequences.size()); i++) { + const int past_len = p.subsequences[i].past_len; + const int num_tokens = p.subsequences[i].num_tokens; + for (int t = 0; t < num_tokens; t++) { + total_tokens++; + const int absolute_pos = past_len + t; + const int block_idx = absolute_pos / p.block_size; + const int token_in_block = absolute_pos % p.block_size; + const int physical_block = last_block_indices[last_block_indices_begins[i] + block_idx]; + const size_t block_base = static_cast(physical_block) * block_stride_bytes; + + for (int head = 0; head < p.num_kv_heads; head++) { + const size_t head_base = block_base + static_cast(head) * head_region_bytes; + const size_t token_offset = head_base + static_cast(token_in_block) * token_data_stride; + + // Check for missing token write (all-zero data) + bool skip_zero_check = is_by_channel && (total_new_tokens <= 1); + if (!skip_zero_check) { + bool all_zero = true; + for (int b = 0; b < token_data_stride; b++) { + if (cache_lock[token_offset + b] != 0) { + all_zero = false; + break; + } + } + if (all_zero && head == 0) { + missing_count++; + GPU_DEBUG_LOG << "KV cache update MISSING: seq=" << i << " token=" << t << " (absolute_pos=" << absolute_pos + << ") at byte_offset=" << token_offset << std::endl; + } + } + + // Check for NaN/INF in data or scale/zp + check_kv_cache_nan_inf(cache_lock.data(), + p, + is_by_token, + is_by_channel, + head_base, + token_in_block, + i, + t, + absolute_pos, + head, + nan_count, + inf_count, + zero_scale_count, + out_of_range_zp_count, + nan_locations, + inf_locations, + zero_scale_locations, + out_of_range_zp_locations); + } + } + } + + report_kv_cache_issues(nan_count, + inf_count, + zero_scale_count, + out_of_range_zp_count, + nan_locations, + inf_locations, + zero_scale_locations, + out_of_range_zp_locations, + total_tokens, + p, + is_by_token, + is_by_channel); + EXPECT_EQ(missing_count, 0) << missing_count << " out of " << total_tokens << " tokens were not written to key cache by KV update kernel"; + EXPECT_EQ(nan_count, 0) << "KV cache contains NaN values"; + EXPECT_EQ(inf_count, 0) << "KV cache contains INF values"; + EXPECT_EQ(zero_scale_count, 0) << "KV cache contains zero/near-zero scale values (causes division by zero in dequant)"; + // ZP can be outside [0, 255] legitimately - see pa-quantization skill for details + } + + // Helper: Check for NaN/INF in KV cache token + void check_kv_cache_nan_inf(const int8_t* cache_data, + const T& p, + bool is_by_token, + bool is_by_channel, + size_t head_base, + int token_in_block, + int seq_idx, + int token_idx, + int absolute_pos, + int head, + int& nan_count, + int& inf_count, + int& zero_scale_count, + int& out_of_range_zp_count, + std::vector>& nan_locations, + std::vector>& inf_locations, + std::vector>& zero_scale_locations, + std::vector>& out_of_range_zp_locations) { + constexpr int kv_sub_block_size = 16; + const size_t token_offset = head_base + static_cast(token_in_block) * p.k_head_size * (p.kv_cache_compression ? 1 : 2); + + if (!p.kv_cache_compression) { + // FP16 cache: check data for NaN/INF + const ov::float16* fp16_ptr = reinterpret_cast(cache_data + token_offset); + for (int dim = 0; dim < p.k_head_size; dim++) { + float val = static_cast(fp16_ptr[dim]); + if (std::isnan(val)) { + nan_count++; + if (nan_locations.size() < 10) { + nan_locations.push_back(std::make_tuple(seq_idx, token_idx, absolute_pos, head, dim)); + } + } + if (std::isinf(val)) { + inf_count++; + if (inf_locations.size() < 10) { + inf_locations.push_back(std::make_tuple(seq_idx, token_idx, absolute_pos, head, dim)); + } + } + } + } else if (is_by_token) { + // BY_TOKEN: check scale/zp + const size_t scale_offset = head_base + static_cast(p.block_size) * p.k_head_size + static_cast(token_in_block) * 2; + const size_t zp_offset = scale_offset + static_cast(p.block_size) * 2; + const ov::float16* scale_ptr = reinterpret_cast(cache_data + scale_offset); + const ov::float16* zp_ptr = reinterpret_cast(cache_data + zp_offset); + float scale = static_cast(*scale_ptr); + float zp = static_cast(*zp_ptr); + + check_scale_zp_validity(scale, + zp, + seq_idx, + token_idx, + absolute_pos, + head, + -1, + nan_count, + inf_count, + zero_scale_count, + out_of_range_zp_count, + nan_locations, + inf_locations, + zero_scale_locations, + out_of_range_zp_locations); + } else if (is_by_channel) { + // BY_CHANNEL: check scale/zp per channel (only first 4 channels to limit overhead) + int sub_block_idx = token_in_block / kv_sub_block_size; + int group_num = p.block_size / kv_sub_block_size; + for (int ch = 0; ch < std::min(p.k_head_size, 4); ch++) { + size_t scale_offset = + head_base + static_cast(p.block_size) * p.k_head_size + (static_cast(sub_block_idx) * p.k_head_size + ch) * 2; + size_t zp_offset = scale_offset + static_cast(group_num) * p.k_head_size * 2; + const ov::float16* scale_ptr = reinterpret_cast(cache_data + scale_offset); + const ov::float16* zp_ptr = reinterpret_cast(cache_data + zp_offset); + float scale = static_cast(*scale_ptr); + float zp = static_cast(*zp_ptr); + + check_scale_zp_validity(scale, + zp, + seq_idx, + token_idx, + absolute_pos, + head, + ch, + nan_count, + inf_count, + zero_scale_count, + out_of_range_zp_count, + nan_locations, + inf_locations, + zero_scale_locations, + out_of_range_zp_locations); + } + } + } + + // Helper: Check scale/zp for NaN/INF/zero + void check_scale_zp_validity(float scale, + float zp, + int seq_idx, + int token_idx, + int absolute_pos, + int head, + int dim, + int& nan_count, + int& inf_count, + int& zero_scale_count, + int& out_of_range_zp_count, + std::vector>& nan_locations, + std::vector>& inf_locations, + std::vector>& zero_scale_locations, + std::vector>& out_of_range_zp_locations) { + if (std::isnan(scale) || std::isnan(zp)) { + nan_count += (std::isnan(scale) ? 1 : 0) + (std::isnan(zp) ? 1 : 0); + if (nan_locations.size() < 10) { + nan_locations.push_back(std::make_tuple(seq_idx, token_idx, absolute_pos, head, dim)); + } + } + if (std::isinf(scale) || std::isinf(zp)) { + inf_count += (std::isinf(scale) ? 1 : 0) + (std::isinf(zp) ? 1 : 0); + if (inf_locations.size() < 10) { + inf_locations.push_back(std::make_tuple(seq_idx, token_idx, absolute_pos, head, dim)); + } + } + if (std::fabs(scale) < 1e-6f) { + zero_scale_count++; + if (zero_scale_locations.size() < 10) { + zero_scale_locations.push_back(std::make_tuple(seq_idx, token_idx, absolute_pos, head, scale, zp)); + } + } + if (zp < -1.0f || zp > 256.0f) { + out_of_range_zp_count++; + if (out_of_range_zp_locations.size() < 10) { + out_of_range_zp_locations.push_back(std::make_tuple(seq_idx, token_idx, absolute_pos, head, scale, zp)); + } + } + } + + // Helper: Report KV cache verification issues + void report_kv_cache_issues(int nan_count, + int inf_count, + int zero_scale_count, + int out_of_range_zp_count, + const std::vector>& nan_locations, + const std::vector>& inf_locations, + const std::vector>& zero_scale_locations, + const std::vector>& out_of_range_zp_locations, + int total_tokens, + const T& p, + bool is_by_token, + bool is_by_channel) { + if (nan_count > 0) { + GPU_DEBUG_LOG << "\nKV cache contains " << nan_count << " NaN values:" << std::endl; + for (const auto& [seq, tok, abs_pos, h, d] : nan_locations) { + GPU_DEBUG_LOG << " seq=" << seq << " token=" << tok << " (absolute_pos=" << abs_pos << ") head=" << h; + if (d == -1) { + GPU_DEBUG_LOG << " [BY_TOKEN scale/zp]"; + } else if (d >= 0) { + GPU_DEBUG_LOG << " [BY_CHANNEL scale/zp channel=" << d << "]"; + } else { + GPU_DEBUG_LOG << " [FP16 dim=" << d << "]"; + } + GPU_DEBUG_LOG << std::endl; + } + if (nan_locations.size() < static_cast(nan_count)) { + GPU_DEBUG_LOG << " ... and " << (nan_count - nan_locations.size()) << " more" << std::endl; + } + } + if (inf_count > 0) { + GPU_DEBUG_LOG << "\nKV cache contains " << inf_count << " INF values:" << std::endl; + for (const auto& [seq, tok, abs_pos, h, d] : inf_locations) { + GPU_DEBUG_LOG << " seq=" << seq << " token=" << tok << " (absolute_pos=" << abs_pos << ") head=" << h; + if (d == -1) { + GPU_DEBUG_LOG << " [BY_TOKEN scale/zp]"; + } else if (d >= 0) { + GPU_DEBUG_LOG << " [BY_CHANNEL scale/zp channel=" << d << "]"; + } else { + GPU_DEBUG_LOG << " [FP16 dim=" << d << "]"; + } + GPU_DEBUG_LOG << std::endl; + } + if (inf_locations.size() < static_cast(inf_count)) { + GPU_DEBUG_LOG << " ... and " << (inf_count - inf_locations.size()) << " more" << std::endl; + } + } + if (zero_scale_count > 0) { + GPU_DEBUG_LOG << "\nKV cache contains " << zero_scale_count << " zero/near-zero scale values:" << std::endl; + for (const auto& [seq, tok, abs_pos, h, scale, zp] : zero_scale_locations) { + GPU_DEBUG_LOG << " seq=" << seq << " token=" << tok << " (absolute_pos=" << abs_pos << ") head=" << h << " scale=" << scale << " zp=" << zp + << std::endl; + } + if (zero_scale_locations.size() < static_cast(zero_scale_count)) { + GPU_DEBUG_LOG << " ... and " << (zero_scale_count - zero_scale_locations.size()) << " more" << std::endl; + } + } + if (out_of_range_zp_count > 0) { + GPU_DEBUG_LOG << "\nKV cache contains " << out_of_range_zp_count + << " ZP values outside typical [0,255] range (NOTE: this is valid for shifted data distributions):" << std::endl; + for (const auto& [seq, tok, abs_pos, h, scale, zp] : out_of_range_zp_locations) { + GPU_DEBUG_LOG << " seq=" << seq << " token=" << tok << " (absolute_pos=" << abs_pos << ") head=" << h << " scale=" << scale << " zp=" << zp + << std::endl; + } + if (out_of_range_zp_locations.size() < static_cast(out_of_range_zp_count)) { + GPU_DEBUG_LOG << " ... and " << (out_of_range_zp_count - out_of_range_zp_locations.size()) << " more" << std::endl; + } + } + + if (nan_count == 0 && inf_count == 0 && zero_scale_count == 0 && out_of_range_zp_count == 0) { + GPU_DEBUG_LOG << "\nKV cache verification PASSED: " << total_tokens << " tokens checked"; + if (p.kv_cache_compression) { + GPU_DEBUG_LOG << " (compression mode: " << (is_by_token ? "BY_TOKEN" : (is_by_channel ? "BY_CHANNEL" : "UNKNOWN")) << ")"; + } else { + GPU_DEBUG_LOG << " (FP16 mode)"; + } + GPU_DEBUG_LOG << std::endl; + } + } + +private: + // Helper structure to hold XAttention error statistics + struct XAttentionErrorStats { + int mismatch_count = 0; + int catastrophic_count = 0; // NaN, Inf, or error > 1.0 + int large_error_count = 0; // error > 0.1 + float max_error = 0.0f; + float avg_error = 0.0f; + float avg_mismatch_error = 0.0f; + size_t first_mismatch_idx = 0; + float first_mismatch_actual = 0.0f; + float first_mismatch_expected = 0.0f; + bool found_first = false; + + // Separate counters for different abnormal value types + int actual_nan_count = 0; + int expected_nan_count = 0; + int actual_inf_count = 0; + int expected_inf_count = 0; + int actual_gt1_count = 0; + int expected_gt1_count = 0; + int error_gt1_count = 0; + + // Track coordinates of abnormal values + std::vector> actual_nan_coords; // (token, head, dim, value) + std::vector> expected_nan_coords; + std::vector> actual_inf_coords; + std::vector> expected_inf_coords; + std::vector> actual_gt1_coords; + std::vector> expected_gt1_coords; + }; + + // Collect XAttention error statistics by comparing GPU output with CPU reference + static void collect_xattention_error_stats(const ov::float16* mem_ptr, + const std::vector& ref_output, + size_t num_heads, + size_t head_size, + float tolerance, + XAttentionErrorStats& stats) { + size_t total_elements = ref_output.size(); + for (size_t i = 0; i < total_elements; i++) { + float actual = static_cast(mem_ptr[i]); + float expected = static_cast(ref_output[i]); + float error = std::fabs(actual - expected); + + stats.avg_error += error; + stats.max_error = std::max(stats.max_error, error); + + // Calculate coordinates: output layout is [T, Q * H] + size_t head_dim_flat = i % (num_heads * head_size); + size_t token_idx = i / (num_heads * head_size); + size_t head_idx = head_dim_flat / head_size; + size_t dim_idx = head_dim_flat % head_size; + + // Separate checks for actual vs expected with coordinate tracking + if (std::isnan(actual)) { + stats.actual_nan_count++; + stats.actual_nan_coords.push_back(std::make_tuple(token_idx, head_idx, dim_idx, actual)); + } + if (std::isnan(expected)) { + stats.expected_nan_count++; + stats.expected_nan_coords.push_back(std::make_tuple(token_idx, head_idx, dim_idx, expected)); + } + if (std::isinf(actual)) { + stats.actual_inf_count++; + stats.actual_inf_coords.push_back(std::make_tuple(token_idx, head_idx, dim_idx, actual)); + } + if (std::isinf(expected)) { + stats.expected_inf_count++; + stats.expected_inf_coords.push_back(std::make_tuple(token_idx, head_idx, dim_idx, expected)); + } + if (!std::isnan(actual) && !std::isinf(actual) && std::fabs(actual) > 1.0f) { + stats.actual_gt1_count++; + stats.actual_gt1_coords.push_back(std::make_tuple(token_idx, head_idx, dim_idx, actual)); + } + if (!std::isnan(expected) && !std::isinf(expected) && std::fabs(expected) > 1.0f) { + stats.expected_gt1_count++; + stats.expected_gt1_coords.push_back(std::make_tuple(token_idx, head_idx, dim_idx, expected)); + } + if (error > 1.0f) + stats.error_gt1_count++; + + if (std::isnan(actual) || std::isinf(actual) || error > 1.0f) { + stats.catastrophic_count++; + } + if (error > 0.1f) { + stats.large_error_count++; + } + + if (error > tolerance) { + if (!stats.found_first) { + stats.first_mismatch_idx = i; + stats.first_mismatch_actual = actual; + stats.first_mismatch_expected = expected; + stats.found_first = true; + } + stats.avg_mismatch_error += error; + stats.mismatch_count++; + } + } + + stats.avg_error /= total_elements; + if (stats.mismatch_count > 0) { + stats.avg_mismatch_error /= stats.mismatch_count; + } + } + + // Print coordinates of abnormal values (NaN/INF/large values) + static void print_abnormal_value_coords(const std::string& label, const std::vector>& coords, int count) { + GPU_DEBUG_LOG << " " << label << ": " << count << std::endl; + if (count > 0) { + GPU_DEBUG_LOG << " Coordinates (token, head, dim, value):" << std::endl; + size_t show_count = std::min(coords.size(), size_t(10)); + for (size_t j = 0; j < show_count; j++) { + auto [t, h, d, v] = coords[j]; + GPU_DEBUG_LOG << " [" << t << ", " << h << ", " << d << "] = " << v << std::endl; + } + if (coords.size() > 10) { + GPU_DEBUG_LOG << " ... and " << (coords.size() - 10) << " more" << std::endl; + } + } + } + + // Print catastrophic error locations with detailed tags + static void print_catastrophic_errors(const ov::float16* mem_ptr, + const std::vector& ref_output, + size_t num_heads, + size_t head_size, + int catastrophic_count) { + if (catastrophic_count > 0 && catastrophic_count <= 20) { + GPU_DEBUG_LOG << "\nCatastrophic error locations (NaN/Inf/error>1.0):" << std::endl; + size_t total_elements = ref_output.size(); + size_t total_tokens = total_elements / (num_heads * head_size); + + GPU_DEBUG_LOG << " Dimensions: tokens=" << total_tokens << " heads=" << num_heads << " head_size=" << head_size << " (total=" << total_elements + << ")" << std::endl; + + for (size_t i = 0; i < total_elements; i++) { + float actual = static_cast(mem_ptr[i]); + float expected = static_cast(ref_output[i]); + float error = std::fabs(actual - expected); + if (std::isnan(actual) || std::isnan(expected) || std::isinf(actual) || std::isinf(expected) || error > 1.0f) { + // Memory layout: [token][head][dim] with innermost dimension first + size_t elements_per_token = num_heads * head_size; + size_t token = i / elements_per_token; + size_t within_token = i % elements_per_token; + size_t head = within_token / head_size; + size_t dim = within_token % head_size; + + GPU_DEBUG_LOG << " Index[" << i << "]: token=" << token << " head=" << head << " dim=" << dim << " | actual=" << actual + << " expected=" << expected << " error=" << error; + + // Tag each type + std::vector tags; + if (std::isnan(actual)) + tags.push_back("ACTUAL_NaN"); + if (std::isnan(expected)) + tags.push_back("EXPECTED_NaN"); + if (std::isinf(actual)) + tags.push_back("ACTUAL_INF"); + if (std::isinf(expected)) + tags.push_back("EXPECTED_INF"); + if (!std::isnan(actual) && !std::isinf(actual) && std::fabs(actual) > 1.0f) + tags.push_back("ACTUAL_>1.0"); + if (!std::isnan(expected) && !std::isinf(expected) && std::fabs(expected) > 1.0f) + tags.push_back("EXPECTED_>1.0"); + if (error > 1.0f) + tags.push_back("ERROR_>1.0"); + + if (!tags.empty()) { + GPU_DEBUG_LOG << " ["; + for (size_t t = 0; t < tags.size(); t++) { + if (t > 0) { + GPU_DEBUG_LOG << ", "; + } + GPU_DEBUG_LOG << tags[t]; + } + GPU_DEBUG_LOG << "]"; + } + GPU_DEBUG_LOG << std::endl; + } + } + } + } + + // Print error distribution by 1024-element blocks + static void print_error_distribution(const ov::float16* mem_ptr, const std::vector& ref_output, float tolerance) { + GPU_DEBUG_LOG << "\nError distribution (by 1024-element blocks):" << std::endl; + size_t block_size = 1024; + size_t total_elements = ref_output.size(); + for (size_t block = 0; block < (total_elements + block_size - 1) / block_size && block < 10; block++) { + int block_mismatches = 0; + size_t start = block * block_size; + size_t end = std::min(start + block_size, total_elements); + for (size_t i = start; i < end; i++) { + float error = std::fabs(static_cast(mem_ptr[i]) - static_cast(ref_output[i])); + if (error > tolerance) + block_mismatches++; + } + GPU_DEBUG_LOG << " Block " << block << " [" << start << "-" << end << "): " << block_mismatches << "/" << (end - start) << " (" + << (100.0 * block_mismatches / (end - start)) << "%)" << std::endl; + } + } + + // Print detailed XAttention error analysis + static void print_xattention_error_details(const XAttentionErrorStats& stats, + const ov::float16* mem_ptr, + const std::vector& ref_output, + size_t num_heads, + size_t head_size, + float tolerance) { + auto& engine = tests::get_test_engine(); + auto arch = engine.get_device_info().arch; + std::string arch_name = (arch == cldnn::gpu_arch::xe2) ? "Xe2" : (arch == cldnn::gpu_arch::xe3) ? "Xe3" : "Xe1"; + size_t total_elements = ref_output.size(); + int allowed_mismatches = int(total_elements * 0.04); + + GPU_DEBUG_LOG << "\n=== XAttention Data Comparison Failed ===" << std::endl; + GPU_DEBUG_LOG << "GPU Architecture: " << arch_name << " (arch=" << static_cast(arch) << ")" << std::endl; + GPU_DEBUG_LOG << "Total elements: " << total_elements << std::endl; + GPU_DEBUG_LOG << "\nError Summary:" << std::endl; + GPU_DEBUG_LOG << " Mismatches (> tolerance): " << stats.mismatch_count << " (" << (100.0 * stats.mismatch_count / total_elements) << "%)" << std::endl; + GPU_DEBUG_LOG << " Allowed mismatches: " << allowed_mismatches << " (4%)" << std::endl; + GPU_DEBUG_LOG << " Large errors (> 0.1): " << stats.large_error_count << " (" << (100.0 * stats.large_error_count / total_elements) << "%)" + << std::endl; + GPU_DEBUG_LOG << " Catastrophic (NaN/Inf/>1.0): " << stats.catastrophic_count << std::endl; + + GPU_DEBUG_LOG << "\nAbnormal Value Analysis:" << std::endl; + GPU_DEBUG_LOG << " Actual output (GPU):" << std::endl; + print_abnormal_value_coords("NaN values", stats.actual_nan_coords, stats.actual_nan_count); + print_abnormal_value_coords("INF values", stats.actual_inf_coords, stats.actual_inf_count); + print_abnormal_value_coords("Values > 1.0", stats.actual_gt1_coords, stats.actual_gt1_count); + GPU_DEBUG_LOG << " Expected output (CPU reference):" << std::endl; + print_abnormal_value_coords("NaN values", stats.expected_nan_coords, stats.expected_nan_count); + print_abnormal_value_coords("INF values", stats.expected_inf_coords, stats.expected_inf_count); + print_abnormal_value_coords("Values > 1.0", stats.expected_gt1_coords, stats.expected_gt1_count); + GPU_DEBUG_LOG << " Error magnitude:" << std::endl; + GPU_DEBUG_LOG << " Errors > 1.0: " << stats.error_gt1_count << std::endl; + GPU_DEBUG_LOG << "\nError Magnitudes:" << std::endl; + GPU_DEBUG_LOG << " Tolerance threshold: " << tolerance << std::endl; + GPU_DEBUG_LOG << " Max error: " << stats.max_error << std::endl; + GPU_DEBUG_LOG << " Avg error (all): " << stats.avg_error << std::endl; + GPU_DEBUG_LOG << " Avg error (mismatches only): " << stats.avg_mismatch_error << std::endl; + GPU_DEBUG_LOG << "First mismatch at index " << stats.first_mismatch_idx << ":" << std::endl; + GPU_DEBUG_LOG << " Actual: " << stats.first_mismatch_actual << std::endl; + GPU_DEBUG_LOG << " Expected: " << stats.first_mismatch_expected << std::endl; + GPU_DEBUG_LOG << " Error: " << std::fabs(stats.first_mismatch_actual - stats.first_mismatch_expected) << std::endl; + + // Sample first 10 mismatches + GPU_DEBUG_LOG << "\nFirst 10 mismatches:" << std::endl; + int sample_count = 0; + for (size_t i = 0; i < total_elements && sample_count < 10; i++) { + float actual = static_cast(mem_ptr[i]); + float expected = static_cast(ref_output[i]); + float error = std::fabs(actual - expected); + if (error > tolerance) { + GPU_DEBUG_LOG << " [" << i << "] actual=" << actual << " expected=" << expected << " error=" << error << std::endl; + sample_count++; + } + } + + print_catastrophic_errors(mem_ptr, ref_output, num_heads, head_size, stats.catastrophic_count); + + // Analyze error type + GPU_DEBUG_LOG << "\nError Analysis:" << std::endl; + if (stats.catastrophic_count > 0) { + GPU_DEBUG_LOG << " ERROR TYPE: CATASTROPHIC - Contains NaN/Inf or very large errors" << std::endl; + GPU_DEBUG_LOG << " This indicates a serious bug, not just precision issues." << std::endl; + } else if (stats.large_error_count > stats.mismatch_count * 0.5) { + GPU_DEBUG_LOG << " ERROR TYPE: LARGE - Most mismatches have error > 0.1" << std::endl; + GPU_DEBUG_LOG << " This suggests wrong calculations, not precision drift." << std::endl; + } else { + GPU_DEBUG_LOG << " ERROR TYPE: PRECISION - Most errors are small" << std::endl; + GPU_DEBUG_LOG << " This suggests accumulated floating-point errors." << std::endl; + } + + // Comment on tolerance metric + GPU_DEBUG_LOG << "\nTolerance Metric Analysis:" << std::endl; + GPU_DEBUG_LOG << " Using: Absolute error with threshold " << tolerance << std::endl; + GPU_DEBUG_LOG << " Input range: Normal(0, 0.1), typically [-0.3, 0.3]" << std::endl; + GPU_DEBUG_LOG << " Relative error at typical values: ~" << (tolerance / 0.1 * 100) << "%" << std::endl; + GPU_DEBUG_LOG << " Verdict: Tolerance is reasonable for FP16 attention calculations" << std::endl; + + print_error_distribution(mem_ptr, ref_output, tolerance); + GPU_DEBUG_LOG << "========================================\n" << std::endl; + } + +public: + static bool check_cm_available() { + auto& engine = tests::get_test_engine(); + ov::intel_gpu::ExecutionConfig config = tests::get_test_default_config(engine); + return cldnn::check_cm_jit_support(engine, config) && engine.get_device_info().supports_immad; + } +}; + +struct paged_attention_test_params { + std::vector subsequences; + int num_heads; + int num_kv_heads; + int k_head_size; + int v_head_size; + int block_size; + int sliding_window_size; + bool kv_cache_compression; + ov::internal::CacheQuantMode key_cache_quant_mode; + bool dynamic_paddings; + ScoresMode scores_mode; + CacheRotationDescriptor rotation_config; + bool disable_flashattn_v2; + bool has_adaptive_rkv = false; + int start_size = 0; // Common start_size for all sequences + std::vector evictable_sizes; // Per-sequence evictable sizes + + // XAttention-related params are grouped below. + bool has_xattention = false; + std::optional> xattention_threshold = std::nullopt; + std::optional> xattention_block_size = std::nullopt; + + ov::element::Type kv_cache_precision = ov::element::dynamic; + + // test query-to-query attention bias + bool has_qq_bias = false; + QueryToQueryAttentionDescriptor qq_bias_config = {}; + bool run_reference = true; + + // optional token_type_ids passed to PagedAttention; if set (non-empty), it is forwarded + // to the op as the TOKEN_TYPE_IDS input. When std::nullopt, a default {0} buffer is used. + std::optional> token_type_ids = std::nullopt; + + // Sink input testing: when true, enables has_sink_input on the primitive and forces + // the sdpa_opt.cl path (by setting has_token_type_ids=true to disable micro-SDPA). + bool has_sink_input = false; + // When set, overrides the default sink values in PAM before memory allocation. + std::optional> sink_values = std::nullopt; + // When true, forces could_use_flashattn_v2(true) to guarantee the FA_V2 kernel path. + bool force_flashattn_v2 = false; +}; + +const auto ENABLE_CACHE_COMPRESSION = true; +const auto DISABLE_CACHE_COMPRESSION = false; +const auto DISABLE_SCORES = ScoresMode::DISABLED; +const auto ENABLE_SCORES = ScoresMode::LAST_TOKEN; +const auto ENABLE_SCORES_SNAPKV = ScoresMode::SNAPKV; +const auto PER_BLOCK_ROTATION = CacheRotationDescriptor{true, true}; +const auto PER_TOKEN_ROTATION = CacheRotationDescriptor{true, false}; +const auto DISABLE_ROTATION = CacheRotationDescriptor{false, false}; +const auto STATIC_INPUT_PAD = false; +const auto DYNAMIC_INPUT_PAD = true; +const auto ENABLE_FA_V2 = false; +const auto DISABLE_FA_V2 = true; +const auto ENABLE_DIVERSITY = true; +const auto DISABLE_DIVERSITY = false; +const auto ENABLE_QQ_BIAS = QueryToQueryAttentionDescriptor{{{{1, 0, 0, 0, 1, 1, 0, 0, 1, 0, 1, 0, 1, 0, 1, 1}}}, {0, 16}}; diff --git a/src/plugins/intel_gpu/tests/unit/test_cases/paged_attention_token_type_gpu_test.cpp b/src/plugins/intel_gpu/tests/unit/test_cases/paged_attention_token_type_gpu_test.cpp new file mode 100644 index 000000000000..0c7915a166bd --- /dev/null +++ b/src/plugins/intel_gpu/tests/unit/test_cases/paged_attention_token_type_gpu_test.cpp @@ -0,0 +1,100 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "paged_attention_gpu_test.h" +#include "test_utils/test_data/paged_attention_token_type_test_data.h" + +struct paged_attention_token_type_test_params : public paged_attention_test_params { + test::TestData token_type_test_data; +}; + +class paged_attention_token_type_test : public PagedAttentionTest { +public: + void apply_token_type_test_data(PagedAttentionManager& pam, const paged_attention_token_type_test_params& p, const test::TestData& data) { + ASSERT_EQ(p.subsequences.size(), 1); + ASSERT_EQ(p.subsequences[0].past_len, 0); + + const size_t seq_len = data.tokenTypes.size(); + const size_t hidden_dim = static_cast(p.num_heads) * static_cast(p.k_head_size); + ASSERT_EQ(static_cast(p.subsequences[0].num_tokens), seq_len); + ASSERT_EQ(data.qData.size(), seq_len * hidden_dim); + ASSERT_EQ(data.kData.size(), seq_len * hidden_dim); + ASSERT_EQ(data.vData.size(), seq_len * hidden_dim); + ASSERT_EQ(data.expectedOutput.size(), seq_len * hidden_dim); + + pam.query_data = {to_float16(data.qData)}; + pam.key_data = {to_float16(data.kData)}; + pam.value_data = {to_float16(data.vData)}; + pam.token_type_ids.assign(data.tokenTypes.begin(), data.tokenTypes.end()); + } + + void compare_token_type_output(cldnn::memory::ptr data_output_mem, const std::vector& expected_output) { + ASSERT_TRUE(data_output_mem); + ASSERT_EQ(data_output_mem->count(), expected_output.size()); + cldnn::mem_lock mem_ptr(data_output_mem, tests::get_test_stream()); + constexpr float token_type_tolerance = 1e-2f; + for (size_t i = 0; i < data_output_mem->count(); i++) { + ASSERT_NEAR(static_cast(mem_ptr[i]), expected_output[i], token_type_tolerance) << " at index=" << i; + } + } +}; +TEST_P(paged_attention_token_type_test, basic) { + auto p = GetParam(); + + ASSERT_TRUE(this->pam.has_value()); + auto& pam = *this->pam; + + apply_token_type_test_data(pam, p, p.token_type_test_data); + + auto result = run_gpu_inference(pam, p); + + cldnn::memory::ptr output_data_mem = nullptr; + cldnn::memory::ptr output_scores_mem = nullptr; + cldnn::memory::ptr output_diversity_mem = nullptr; + + output_data_mem = result.outputs.at("output_data").get_memory(); + + compare_token_type_output(output_data_mem, p.token_type_test_data.expectedOutput); +} + +static paged_attention_token_type_test_params make_token_type_test_param(const test::TestData& data, bool disable_flashattn_v2) { + paged_attention_token_type_test_params p; + p.subsequences = {{static_cast(data.tokenTypes.size()), 0}}; + p.num_heads = 1; + p.num_kv_heads = 1; + p.k_head_size = 32; + p.v_head_size = 32; + p.block_size = 16; + p.sliding_window_size = data.slidingWindowSize; + p.kv_cache_compression = DISABLE_CACHE_COMPRESSION; + p.key_cache_quant_mode = ov::internal::CacheQuantMode::BY_TOKEN; + p.dynamic_paddings = STATIC_INPUT_PAD; + p.scores_mode = DISABLE_SCORES; + p.rotation_config = DISABLE_ROTATION; + p.disable_flashattn_v2 = disable_flashattn_v2; + p.token_type_ids = std::vector(data.tokenTypes.begin(), data.tokenTypes.end()); + p.token_type_test_data = data; + return p; +} + +static std::vector make_token_type_test_params(const std::vector& test_data) { + std::vector params; + params.reserve(test_data.size() * 2); + for (const auto& data : test_data) { + params.push_back(make_token_type_test_param(data, ENABLE_FA_V2)); + params.push_back(make_token_type_test_param(data, DISABLE_FA_V2)); + } + return params; +} + +static std::string get_token_type_test_name(const testing::TestParamInfo& obj) { + const auto& p = obj.param; + return p.token_type_test_data.name + "_SW" + std::to_string(p.sliding_window_size) + + (p.disable_flashattn_v2 == DISABLE_FA_V2 ? "_FlashAttnV2Disabled" : "_FlashAttnV2Enabled"); +} + +INSTANTIATE_TEST_SUITE_P(smoke_paged_attention_token_type, + paged_attention_token_type_test, + ::testing::ValuesIn(make_token_type_test_params(test::PagedAttentionTokenTypeTestData::GetTestData())), + get_token_type_test_name); diff --git a/src/plugins/intel_gpu/tests/unit/test_cases/paged_causal_conv1d.cpp b/src/plugins/intel_gpu/tests/unit/test_cases/paged_causal_conv1d.cpp new file mode 100644 index 000000000000..f4728278344d --- /dev/null +++ b/src/plugins/intel_gpu/tests/unit/test_cases/paged_causal_conv1d.cpp @@ -0,0 +1,382 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include +#include +#include +#include +#include +#include + +#include "paged_causal_conv1d_inst.h" +#include "random_generator.hpp" +#include "test_utils.h" + +using namespace cldnn; +using namespace ::tests; + +namespace { + +struct paged_causal_conv1d_test_params { + int32_t tokens; + int32_t num_sequences; + int32_t hidden_size; + int32_t kernel_size; + ov::element::Type precision; + bool with_bias; +}; + +struct paged_causal_conv1d_gpu_test : public ::testing::TestWithParam { + tests::random_generator rg; + + struct paging_desc { + std::vector subsequence_begins; + std::vector block_indices; + std::vector block_indices_begins; + std::vector past_lens; + std::vector cache_interval; + int32_t num_blocks = 0; + }; + + template + static T to_data_type(float v) { + if constexpr (std::is_same::value) { + return ov::float16(v); + } + return static_cast(v); + } + + template + void load_input(cldnn::memory::ptr mem, const std::vector& input_data) { + set_values(mem, input_data); + } + + static paging_desc make_paging_desc(int32_t tokens, int32_t num_sequences) { + OPENVINO_ASSERT(num_sequences > 0, "num_sequences must be positive"); + OPENVINO_ASSERT(tokens >= num_sequences, "tokens must be >= num_sequences"); + + paging_desc d; + d.subsequence_begins.reserve(static_cast(num_sequences + 1)); + d.block_indices_begins.reserve(static_cast(num_sequences + 1)); + d.past_lens.reserve(static_cast(num_sequences)); + d.cache_interval.reserve(static_cast(num_sequences)); + + d.subsequence_begins.push_back(0); + d.block_indices_begins.push_back(0); + + int32_t consumed_tokens = 0; + int32_t total_blocks = 0; + for (int32_t seq = 0; seq < num_sequences; seq++) { + const int32_t rem_tokens = tokens - consumed_tokens; + const int32_t rem_seq = num_sequences - seq; + const int32_t seq_tokens = rem_tokens / rem_seq; + + consumed_tokens += seq_tokens; + d.subsequence_begins.push_back(consumed_tokens); + + const int32_t seq_past_len = 1 + (seq % 3); + const int32_t seq_interval = 2 + (seq % 2); + d.past_lens.push_back(seq_past_len); + d.cache_interval.push_back(seq_interval); + + const int32_t prev_nums = seq_past_len % seq_interval; + const int32_t total_cached = prev_nums + seq_tokens; + const int32_t max_slot = 1 + (total_cached - 1) / seq_interval; + const int32_t required_slots = 1 + std::max(1, max_slot); + for (int32_t i = 0; i < required_slots; i++) { + d.block_indices.push_back(total_blocks + i); + } + total_blocks += required_slots; + d.block_indices_begins.push_back(total_blocks); + } + + d.num_blocks = total_blocks; + return d; + } + + template + static void run_reference(const std::vector& input_embeds, + const std::vector& conv_weight, + const std::vector& conv_bias, + std::vector& conv_state_table, + const std::vector& subsequence_begins, + const std::vector& block_indices, + const std::vector& block_indices_begins, + const std::vector& past_lens, + const std::vector& cache_interval, + int32_t hidden_size, + int32_t kernel_size, + std::vector& output_embeds) { + const int32_t token_count = static_cast(input_embeds.size()) / hidden_size; + output_embeds.resize(static_cast(token_count) * hidden_size); + + auto state_off = [hidden_size, kernel_size](int32_t block, int32_t h, int32_t k) { + return (block * hidden_size + h) * kernel_size + k; + }; + + for (int32_t seq = 0; seq < static_cast(subsequence_begins.size()) - 1; seq++) { + const int32_t token_begin = subsequence_begins[seq]; + const int32_t token_end = subsequence_begins[seq + 1]; + const int32_t blk_begin = block_indices_begins[seq]; + const int32_t blk_end = block_indices_begins[seq + 1]; + + if (token_begin < 0 || token_end < token_begin || blk_end <= blk_begin) { + continue; + } + + const int32_t block_span = blk_end - blk_begin; + if (block_span <= 1) { + continue; + } + + const int32_t seq_interval = cache_interval[seq]; + const int32_t prev_nums = (seq_interval > 0) ? (past_lens[seq] % seq_interval) : 0; + const int32_t seq_tokens = token_end - token_begin; + const int32_t read_physical_block = block_indices[blk_begin]; + + for (int32_t h = 0; h < hidden_size; h++) { + std::vector state(static_cast(kernel_size), 0.0f); + for (int32_t k = 0; k < kernel_size; k++) { + state[k] = static_cast(conv_state_table[state_off(read_physical_block, h, k)]); + } + + const float bias_val = conv_bias.empty() ? 0.0f : static_cast(conv_bias[h]); + + for (int32_t t = 0; t < seq_tokens; t++) { + for (int32_t k = 0; k + 1 < kernel_size; k++) { + state[k] = state[k + 1]; + } + + const int32_t token_idx = token_begin + t; + state[kernel_size - 1] = static_cast(input_embeds[token_idx * hidden_size + h]); + + float sum = bias_val; + const int32_t w_base = h * kernel_size; + for (int32_t k = 0; k < kernel_size; k++) { + sum = std::fma(state[k], static_cast(conv_weight[w_base + k]), sum); + } + + output_embeds[token_idx * hidden_size + h] = to_data_type(sum); + + const int32_t cached_tokens = prev_nums + (t + 1); + const bool interval_hit = (seq_interval > 0) && ((cached_tokens % seq_interval) == 0); + const bool is_last_token = (t == seq_tokens - 1); + if (interval_hit || is_last_token) { + const int32_t slot = (seq_interval > 0) ? (1 + (cached_tokens - 1) / seq_interval) : 1; + if (slot >= 1 && slot < block_span) { + const int32_t physical_block = block_indices[blk_begin + slot]; + for (int32_t k = 0; k < kernel_size; k++) { + conv_state_table[state_off(physical_block, h, k)] = to_data_type(state[k]); + } + } + } + } + } + } + } + + topology create_topology(layout input_data_layout, + layout state_data_layout, + layout weight_data_layout, + layout bias_data_layout, + layout subseq_data_layout, + layout block_idx_data_layout, + layout block_idx_begins_data_layout, + layout past_lens_data_layout, + layout cache_interval_data_layout, + data_types output_dt) { + topology topo; + + topo.add(input_layout("input_embeds", input_data_layout)); + topo.add(input_layout("conv_state_table", state_data_layout)); + topo.add(input_layout("conv_weight", weight_data_layout)); + topo.add(input_layout("conv_bias", bias_data_layout)); + topo.add(input_layout("subsequence_begins", subseq_data_layout)); + topo.add(input_layout("block_indices", block_idx_data_layout)); + topo.add(input_layout("block_indices_begins", block_idx_begins_data_layout)); + topo.add(input_layout("past_lens", past_lens_data_layout)); + topo.add(input_layout("cache_interval", cache_interval_data_layout)); + + topo.add(paged_causal_conv1d("paged_causal_conv1d", + {input_info("input_embeds"), + input_info("conv_state_table"), + input_info("conv_weight"), + input_info("conv_bias"), + input_info("subsequence_begins"), + input_info("block_indices"), + input_info("block_indices_begins"), + input_info("past_lens"), + input_info("cache_interval")})); + + topo.add(reorder("output", input_info("paged_causal_conv1d"), format::bfyx, output_dt)); + return topo; + } + + std::tuple run_network(topology& topo, + cldnn::memory::ptr input_mem, + cldnn::memory::ptr state_mem, + cldnn::memory::ptr weight_mem, + cldnn::memory::ptr bias_mem, + cldnn::memory::ptr subseq_mem, + cldnn::memory::ptr block_idx_mem, + cldnn::memory::ptr block_idx_begins_mem, + cldnn::memory::ptr past_lens_mem, + cldnn::memory::ptr cache_interval_mem) { + auto& engine = get_test_engine(); + + ExecutionConfig config = get_test_default_config(engine); + config.set_property(ov::intel_gpu::allow_new_shape_infer(true)); + + cldnn::network::ptr net = get_network(engine, topo, config, get_test_stream_ptr(), false); + + net->set_input_data("input_embeds", input_mem); + net->set_input_data("conv_state_table", state_mem); + net->set_input_data("conv_weight", weight_mem); + net->set_input_data("conv_bias", bias_mem); + net->set_input_data("subsequence_begins", subseq_mem); + net->set_input_data("block_indices", block_idx_mem); + net->set_input_data("block_indices_begins", block_idx_begins_mem); + net->set_input_data("past_lens", past_lens_mem); + net->set_input_data("cache_interval", cache_interval_mem); + + auto outputs = net->execute(); + auto output = outputs.at("output").get_memory(); + + return std::make_tuple(output, net); + } + + template + void execute_t(const paged_causal_conv1d_test_params& p, data_types data_type, float output_tolerance, float state_tolerance) { + auto& engine = get_test_engine(); + + const auto page = make_paging_desc(p.tokens, p.num_sequences); + + const layout input_layout({p.tokens, p.hidden_size}, data_type, format::bfyx); + const layout state_layout({page.num_blocks, p.hidden_size, p.kernel_size}, data_type, format::bfyx); + const layout weight_layout({p.hidden_size, 1, p.kernel_size}, data_type, format::bfyx); + const layout bias_layout({p.with_bias ? p.hidden_size : 0}, data_type, format::bfyx); + + const layout subseq_layout({static_cast(page.subsequence_begins.size())}, data_types::i32, format::bfyx); + const layout block_idx_layout({static_cast(page.block_indices.size())}, data_types::i32, format::bfyx); + const layout block_idx_begins_layout({static_cast(page.block_indices_begins.size())}, data_types::i32, format::bfyx); + const layout past_lens_layout({static_cast(page.past_lens.size())}, data_types::i32, format::bfyx); + const layout cache_interval_layout({static_cast(page.cache_interval.size())}, data_types::i32, format::bfyx); + + auto input_mem = engine.allocate_memory(input_layout); + auto state_mem = engine.allocate_memory(state_layout); + auto weight_mem = engine.allocate_memory(weight_layout); + auto bias_mem = engine.allocate_memory(bias_layout); + auto subseq_mem = engine.allocate_memory(subseq_layout); + auto block_idx_mem = engine.allocate_memory(block_idx_layout); + auto block_idx_begins_mem = engine.allocate_memory(block_idx_begins_layout); + auto past_lens_mem = engine.allocate_memory(past_lens_layout); + auto cache_interval_mem = engine.allocate_memory(cache_interval_layout); + + auto input_embeds = rg.generate_random_1d(ov::shape_size(input_layout.get_shape()), -1.0f, 1.0f, 256); + auto conv_state_table = rg.generate_random_1d(ov::shape_size(state_layout.get_shape()), -1.0f, 1.0f, 256); + auto conv_weight = rg.generate_random_1d(ov::shape_size(weight_layout.get_shape()), -1.0f, 1.0f, 256); + std::vector conv_bias; + if (p.with_bias) { + conv_bias = rg.generate_random_1d(ov::shape_size(bias_layout.get_shape()), -0.5f, 0.5f, 256); + } + + auto ref_state = conv_state_table; + std::vector ref_output; + run_reference(input_embeds, + conv_weight, + conv_bias, + ref_state, + page.subsequence_begins, + page.block_indices, + page.block_indices_begins, + page.past_lens, + page.cache_interval, + p.hidden_size, + p.kernel_size, + ref_output); + + load_input(input_mem, input_embeds); + load_input(state_mem, conv_state_table); + load_input(weight_mem, conv_weight); + if (!conv_bias.empty()) { + load_input(bias_mem, conv_bias); + } + load_input(subseq_mem, page.subsequence_begins); + load_input(block_idx_mem, page.block_indices); + load_input(block_idx_begins_mem, page.block_indices_begins); + load_input(past_lens_mem, page.past_lens); + load_input(cache_interval_mem, page.cache_interval); + + auto topo = create_topology(input_layout, + state_layout, + weight_layout, + bias_layout, + subseq_layout, + block_idx_layout, + block_idx_begins_layout, + past_lens_layout, + cache_interval_layout, + data_type); + + auto [out_mem, net] = + run_network(topo, input_mem, state_mem, weight_mem, bias_mem, subseq_mem, block_idx_mem, block_idx_begins_mem, past_lens_mem, cache_interval_mem); + + ASSERT_TRUE(out_mem != nullptr); + ASSERT_EQ(out_mem->count(), ref_output.size()); + + { + cldnn::mem_lock out_data(out_mem, get_test_stream()); + for (size_t i = 0; i < ref_output.size(); i++) { + ASSERT_NEAR(static_cast(out_data[i]), static_cast(ref_output[i]), output_tolerance) << " at index=" << i; + } + } + + ASSERT_EQ(state_mem->count(), ref_state.size()); + { + cldnn::mem_lock state_data(state_mem, get_test_stream()); + for (size_t i = 0; i < ref_state.size(); i++) { + ASSERT_NEAR(static_cast(state_data[i]), static_cast(ref_state[i]), state_tolerance) << " at index=" << i; + } + } + } + + void execute(const paged_causal_conv1d_test_params& p) { + const auto cldnn_precision = cldnn::element_type_to_data_type(p.precision); + + if (p.precision == ov::element::f16) { + execute_t(p, cldnn_precision, 0.03f, 0.03f); + return; + } + + if (p.precision == ov::element::f32) { + execute_t(p, cldnn_precision, 1e-4f, 1e-4f); + return; + } + + FAIL() << "Unsupported precision for paged_causal_conv1d test"; + } + + static std::string PrintToStringParamName(const testing::TestParamInfo& info) { + const auto& p = info.param; + return "paged_causal_conv1d_gpu_test_" + p.precision.to_string() + "_tokens_" + std::to_string(p.tokens) + "_seq_" + std::to_string(p.num_sequences) + + "_hidden_" + std::to_string(p.hidden_size) + "_kernel_" + std::to_string(p.kernel_size) + (p.with_bias ? "_bias" : "_no_bias"); + } +}; + +TEST_P(paged_causal_conv1d_gpu_test, basic) { + const auto p = GetParam(); + execute(p); +} + +INSTANTIATE_TEST_SUITE_P(smoke_paged_causal_conv1d_gpu_test, + paged_causal_conv1d_gpu_test, + ::testing::Values(paged_causal_conv1d_test_params{8, 2, 16, 4, ov::element::f16, true}, + paged_causal_conv1d_test_params{12, 3, 32, 5, ov::element::f16, true}, + paged_causal_conv1d_test_params{8, 2, 16, 4, ov::element::f32, true}, + paged_causal_conv1d_test_params{12, 3, 32, 5, ov::element::f32, true}, + paged_causal_conv1d_test_params{8, 2, 16, 4, ov::element::f16, false}, + paged_causal_conv1d_test_params{8, 2, 16, 4, ov::element::f32, false}), + paged_causal_conv1d_gpu_test::PrintToStringParamName); + +} // namespace diff --git a/src/plugins/intel_gpu/tests/unit/test_cases/paged_gated_delta_net.cpp b/src/plugins/intel_gpu/tests/unit/test_cases/paged_gated_delta_net.cpp new file mode 100644 index 000000000000..ca9d73d29858 --- /dev/null +++ b/src/plugins/intel_gpu/tests/unit/test_cases/paged_gated_delta_net.cpp @@ -0,0 +1,449 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include +#include +#include +#include +#include +#include +#include + +#include "paged_gated_delta_net_inst.h" +#include "random_generator.hpp" +#include "test_utils.h" + +using namespace cldnn; +using namespace ::tests; + +namespace { + +struct paged_gated_delta_net_test_params { + std::vector subsequence_tokens; // Explicit token count for each subsequence + std::vector past_lens; // Explicit past length for each subsequence + std::vector cache_intervals; // Explicit cache interval for each subsequence + int32_t qk_heads; // Number of query/key heads + int32_t v_heads; // Number of value heads + int32_t head_size; // Per-head hidden size (K and V dims in this test) + ov::element::Type precision; // Data precision for query/key/value/state tensors +}; + +struct paged_gated_delta_net_gpu_test : public ::testing::TestWithParam { + tests::random_generator rg; + + template + static void normalize_and_scale(const T* src, size_t n, float scale, std::vector& dst) { + dst.resize(n); + float sum = 0.0f; + for (size_t i = 0; i < n; i++) { + float v = static_cast(src[i]); + dst[i] = v; + sum += v * v; + } + float inv = 1.0f / std::sqrt(sum + 1e-6f); + for (size_t i = 0; i < n; i++) { + dst[i] *= inv * scale; + } + } + + template + static void run_reference(const std::vector& query, + const std::vector& key, + const std::vector& value, + const std::vector& gate, + const std::vector& beta, + std::vector& recurrent_state_table, + const std::vector& subsequence_begins, + const std::vector& block_indices, + const std::vector& block_indices_begins, + const std::vector& past_lens, + const std::vector& cache_interval, + int32_t qk_heads, + int32_t v_heads, + int32_t qk_head_size, + int32_t v_head_size, + std::vector& output) { + const int32_t tokens = static_cast(query.size()) / (qk_heads * qk_head_size); + + OPENVINO_ASSERT(static_cast(key.size()) == tokens * qk_heads * qk_head_size, "Key tensor size does not match inferred qk head size"); + OPENVINO_ASSERT(static_cast(value.size()) == tokens * v_heads * v_head_size, "Value tensor size does not match provided v head size"); + OPENVINO_ASSERT(static_cast(gate.size()) == tokens * v_heads, "Gate tensor size does not match inferred token/head dimensions"); + OPENVINO_ASSERT(static_cast(beta.size()) == tokens * v_heads, "Beta tensor size does not match inferred token/head dimensions"); + + output.resize(static_cast(tokens) * v_heads * v_head_size); + + const auto state_off = [=](int32_t block, int32_t h, int32_t k_idx, int32_t v_idx) { + return ((block * v_heads + h) * v_head_size + v_idx) * qk_head_size + k_idx; + }; + + const float attn_scale = 1.0f / std::sqrt(static_cast(qk_head_size)); + const int32_t num_sequences = static_cast(subsequence_begins.size()) - 1; + const int32_t group_size = v_heads / qk_heads; + + OPENVINO_ASSERT(static_cast(recurrent_state_table.size()) % (v_heads * qk_head_size * v_head_size) == 0, + "State table size is inconsistent with inferred qk/v head sizes"); + + for (int32_t seq = 0; seq < num_sequences; seq++) { + const int32_t token_begin = subsequence_begins[seq]; + const int32_t token_end = subsequence_begins[seq + 1]; + const int32_t block_begin = block_indices_begins[seq]; + const int32_t block_end = block_indices_begins[seq + 1]; + const int32_t seq_blocks = std::max(block_end - block_begin, 0); + const int32_t past_len = past_lens[seq]; + const int32_t interval = cache_interval[seq]; + const int32_t prev_nums = (interval > 0) ? (past_len % interval) : 0; + + for (int32_t h = 0; h < v_heads; h++) { + const int32_t hk = h / group_size; + std::vector state(static_cast(qk_head_size) * v_head_size, 0.0f); + + const int32_t block_id = block_indices[block_begin]; + + for (int32_t k_idx = 0; k_idx < qk_head_size; k_idx++) { + for (int32_t v_idx = 0; v_idx < v_head_size; v_idx++) { + state[k_idx * v_head_size + v_idx] = static_cast(recurrent_state_table[state_off(block_id, h, k_idx, v_idx)]); + } + } + + for (int32_t token = token_begin; token < token_end; token++) { + const auto q_ptr = query.data() + (token * qk_heads + hk) * qk_head_size; + const auto k_ptr = key.data() + (token * qk_heads + hk) * qk_head_size; + + std::vector q_norm; + std::vector k_norm; + normalize_and_scale(q_ptr, qk_head_size, attn_scale, q_norm); + normalize_and_scale(k_ptr, qk_head_size, 1.0f, k_norm); + + const float b_g = std::exp(static_cast(gate[token * v_heads + h])); + const float b_beta = static_cast(beta[token * v_heads + h]); + + for (int32_t v_idx = 0; v_idx < v_head_size; v_idx++) { + const float b_v = static_cast(value[(token * v_heads + h) * v_head_size + v_idx]); + + float h_k = 0.0f; + for (int32_t k_idx = 0; k_idx < qk_head_size; k_idx++) { + auto& s = state[k_idx * v_head_size + v_idx]; + s *= b_g; + h_k += s * k_norm[k_idx]; + } + + const float update = (b_v - h_k) * b_beta; + float out_v = 0.0f; + for (int32_t k_idx = 0; k_idx < qk_head_size; k_idx++) { + auto& s = state[k_idx * v_head_size + v_idx]; + s += k_norm[k_idx] * update; + out_v += s * q_norm[k_idx]; + } + + output[(token * v_heads + h) * v_head_size + v_idx] = static_cast(out_v); + } + + const int32_t processed_tokens = (token - token_begin) + 1; + const int32_t cached_tokens = prev_nums + processed_tokens; + const bool reached_interval_boundary = (interval > 0) && ((cached_tokens % interval) == 0); + const bool reached_sequence_end = (token == token_end - 1); + if (reached_interval_boundary || reached_sequence_end) { + const int32_t slot = interval > 0 ? (1 + (cached_tokens - 1) / interval) : 1; + if (slot < seq_blocks) { + const int32_t block_id = block_indices[block_begin + slot]; + for (int32_t k_idx = 0; k_idx < qk_head_size; k_idx++) { + for (int32_t v_idx = 0; v_idx < v_head_size; v_idx++) { + recurrent_state_table[state_off(block_id, h, k_idx, v_idx)] = static_cast(state[k_idx * v_head_size + v_idx]); + } + } + } + } + } + } + } + } + + template + void execute_t(const paged_gated_delta_net_test_params& p) { + auto& engine = get_test_engine(); + + const auto& subseq_tokens = p.subsequence_tokens; + const auto& param_past_lens = p.past_lens; + const auto& param_cache_intervals = p.cache_intervals; + const int32_t num_sequences = static_cast(subseq_tokens.size()); + const int32_t tokens = static_cast(std::accumulate(subseq_tokens.begin(), subseq_tokens.end(), size_t{0})); + const int32_t qk_heads = p.qk_heads; + const int32_t v_heads = p.v_heads; + const int32_t head_size = p.head_size; + + OPENVINO_ASSERT(num_sequences > 1, "Test should cover num_sequences > 1"); + OPENVINO_ASSERT(tokens >= num_sequences, "tokens should be >= num_sequences"); + OPENVINO_ASSERT(static_cast(param_past_lens.size()) == num_sequences, "past_lens size must match number of subsequences"); + OPENVINO_ASSERT(static_cast(param_cache_intervals.size()) == num_sequences, "cache_intervals size must match number of subsequences"); + + std::vector subsequence_begins; + std::vector past_lens; + std::vector cache_interval; + std::vector block_indices; + std::vector block_indices_begins; + + subsequence_begins.reserve(static_cast(num_sequences + 1)); + block_indices_begins.reserve(static_cast(num_sequences + 1)); + past_lens.reserve(static_cast(num_sequences)); + cache_interval.reserve(static_cast(num_sequences)); + + subsequence_begins.push_back(0); + block_indices_begins.push_back(0); + + int32_t acc_tokens = 0; + int32_t total_blocks = 0; + for (int32_t seq = 0; seq < num_sequences; seq++) { + const int32_t seq_tokens = static_cast(subseq_tokens[seq]); + OPENVINO_ASSERT(seq_tokens > 0, "Each subsequence must contain at least one token"); + acc_tokens += seq_tokens; + subsequence_begins.push_back(acc_tokens); + + const int32_t seq_past_len = param_past_lens[seq]; + const int32_t seq_interval = param_cache_intervals[seq]; + OPENVINO_ASSERT(seq_past_len >= 0, "past_len must be non-negative"); + OPENVINO_ASSERT(seq_interval >= 0, "cache_interval must be >= 0"); + past_lens.push_back(seq_past_len); + cache_interval.push_back(seq_interval); + + const int32_t required_slots = [&]() { + if (seq_interval == 0) { + return 2; + } + + const int32_t prev_nums = seq_past_len % seq_interval; + const int32_t write_blocks = (prev_nums + seq_tokens + seq_interval - 1) / seq_interval; + return 1 + write_blocks; + }(); + for (int32_t i = 0; i < required_slots; i++) { + block_indices.push_back(total_blocks + i); + } + total_blocks += required_slots; + block_indices_begins.push_back(total_blocks); + } + + layout q_layout({tokens, qk_heads, head_size}, p.precision, format::bfyx); + layout k_layout({tokens, qk_heads, head_size}, p.precision, format::bfyx); + layout v_layout({tokens, v_heads, head_size}, p.precision, format::bfyx); + layout state_layout({static_cast(block_indices.size()), v_heads, head_size, head_size}, p.precision, format::bfyx); + layout gate_layout({tokens, v_heads}, p.precision, format::bfyx); + layout beta_layout({tokens, v_heads}, p.precision, format::bfyx); + layout seq_begins_layout({static_cast(subsequence_begins.size())}, data_types::i32, format::bfyx); + layout block_indices_layout({static_cast(block_indices.size())}, data_types::i32, format::bfyx); + layout block_indices_begins_layout({static_cast(block_indices_begins.size())}, data_types::i32, format::bfyx); + layout past_lens_layout({static_cast(past_lens.size())}, data_types::i32, format::bfyx); + layout cache_interval_layout({static_cast(cache_interval.size())}, data_types::i32, format::bfyx); + + auto q_mem = engine.allocate_memory(q_layout); + auto k_mem = engine.allocate_memory(k_layout); + auto v_mem = engine.allocate_memory(v_layout); + auto state_mem = engine.allocate_memory(state_layout); + auto gate_mem = engine.allocate_memory(gate_layout); + auto beta_mem = engine.allocate_memory(beta_layout); + auto seq_begins_mem = engine.allocate_memory(seq_begins_layout); + auto block_indices_mem = engine.allocate_memory(block_indices_layout); + auto block_indices_begins_mem = engine.allocate_memory(block_indices_begins_layout); + auto past_lens_mem = engine.allocate_memory(past_lens_layout); + auto cache_interval_mem = engine.allocate_memory(cache_interval_layout); + + auto query = rg.generate_random_1d(q_mem->count(), -1, 1, 1024); + auto key = rg.generate_random_1d(k_mem->count(), -1, 1, 256); + auto value = rg.generate_random_1d(v_mem->count(), -1, 1, 256); + auto gate = rg.generate_random_1d(gate_mem->count(), -1, 1, 256); + auto beta = rg.generate_random_1d(beta_mem->count(), 0, 1, 256); + auto state = rg.generate_random_1d(state_mem->count(), -1, 1, 256); + + set_values(q_mem, query); + set_values(k_mem, key); + set_values(v_mem, value); + set_values(gate_mem, gate); + set_values(beta_mem, beta); + set_values(state_mem, state); + set_values(seq_begins_mem, subsequence_begins); + set_values(block_indices_mem, block_indices); + set_values(block_indices_begins_mem, block_indices_begins); + set_values(past_lens_mem, past_lens); + set_values(cache_interval_mem, cache_interval); + + topology topo; + topo.add(input_layout("q", q_layout)); + topo.add(input_layout("k", k_layout)); + topo.add(input_layout("v", v_layout)); + topo.add(input_layout("state", state_layout)); + topo.add(input_layout("g", gate_layout)); + topo.add(input_layout("beta", beta_layout)); + topo.add(input_layout("subsequence_begins", seq_begins_layout)); + topo.add(input_layout("block_indices", block_indices_layout)); + topo.add(input_layout("block_indices_begins", block_indices_begins_layout)); + topo.add(input_layout("past_lens", past_lens_layout)); + topo.add(input_layout("cache_interval", cache_interval_layout)); + topo.add(paged_gated_delta_net("paged_gdn", + {input_info("q"), + input_info("k"), + input_info("v"), + input_info("state"), + input_info("g"), + input_info("beta"), + input_info("subsequence_begins"), + input_info("block_indices"), + input_info("block_indices_begins"), + input_info("past_lens"), + input_info("cache_interval")}, + true, + 1e-6f, + 1e-6f)); + + ExecutionConfig config = get_test_default_config(engine); + config.set_property(ov::intel_gpu::allow_new_shape_infer(true)); + auto network = get_network(engine, topo, config, get_test_stream_ptr(), false); + + network->set_input_data("q", q_mem); + network->set_input_data("k", k_mem); + network->set_input_data("v", v_mem); + network->set_input_data("state", state_mem); + network->set_input_data("g", gate_mem); + network->set_input_data("beta", beta_mem); + network->set_input_data("subsequence_begins", seq_begins_mem); + network->set_input_data("block_indices", block_indices_mem); + network->set_input_data("block_indices_begins", block_indices_begins_mem); + network->set_input_data("past_lens", past_lens_mem); + network->set_input_data("cache_interval", cache_interval_mem); + + auto outputs = network->execute(); + auto out_mem = outputs.at("paged_gdn").get_memory(); + + std::vector ref_output; + auto ref_state = state; + run_reference(query, + key, + value, + gate, + beta, + ref_state, + subsequence_begins, + block_indices, + block_indices_begins, + past_lens, + cache_interval, + qk_heads, + v_heads, + head_size, + head_size, + ref_output); + + const float atol = std::is_same::value ? 4e-2f : 1e-3f; + const float rtol = std::is_same::value ? 5e-4f : 1e-5f; + + { + cldnn::mem_lock out_lock(out_mem, get_test_stream()); + ASSERT_EQ(out_mem->count(), ref_output.size()); + for (size_t i = 0; i < ref_output.size(); i++) { + const float actual = static_cast(out_lock[i]); + const float expected = static_cast(ref_output[i]); + const float tol = atol + rtol * std::abs(expected); + ASSERT_LE(std::abs(actual - expected), tol) << " at output index=" << i; + } + } + + { + cldnn::mem_lock state_lock(state_mem, get_test_stream()); + ASSERT_EQ(state_mem->count(), ref_state.size()); + for (size_t i = 0; i < ref_state.size(); i++) { + const float actual = static_cast(state_lock[i]); + const float expected = static_cast(ref_state[i]); + const float tol = atol + rtol * std::abs(expected); + ASSERT_LE(std::abs(actual - expected), tol) << " at state index=" << i; + } + } + } + + void execute(const paged_gated_delta_net_test_params& p) { + if (p.precision == ov::element::f16) { + execute_t(p); + return; + } + + if (p.precision == ov::element::f32) { + execute_t(p); + return; + } + + FAIL() << "Unsupported precision for paged_gated_delta_net test"; + } + + static std::string PrintToStringParamName(const testing::TestParamInfo& info) { + std::string subseq_tokens_str; + for (size_t i = 0; i < info.param.subsequence_tokens.size(); i++) { + if (i > 0) + subseq_tokens_str += "-"; + subseq_tokens_str += std::to_string(info.param.subsequence_tokens[i]); + } + + std::string past_lens_str; + for (size_t i = 0; i < info.param.past_lens.size(); i++) { + if (i > 0) + past_lens_str += "-"; + past_lens_str += std::to_string(info.param.past_lens[i]); + } + + std::string cache_intervals_str; + for (size_t i = 0; i < info.param.cache_intervals.size(); i++) { + if (i > 0) + cache_intervals_str += "-"; + cache_intervals_str += std::to_string(info.param.cache_intervals[i]); + } + + std::string result = "paged_gated_delta_net_gpu_test_" + info.param.precision.to_string() + "_" + subseq_tokens_str + "_" + past_lens_str + "_" + + cache_intervals_str + "_" + std::to_string(info.param.qk_heads) + "_" + std::to_string(info.param.v_heads) + "_" + + std::to_string(info.param.head_size); + return result; + } +}; + +TEST_P(paged_gated_delta_net_gpu_test, basic) { + execute(GetParam()); +} + +INSTANTIATE_TEST_SUITE_P(smoke_paged_gated_delta_net_gpu_test, + paged_gated_delta_net_gpu_test, + // Parameter order: + // {subsequence_tokens, past_lens, cache_intervals, qk_heads, v_heads, head_size, precision} + ::testing::Values( + // f16: decode stage (past_len > 0), head_size 16/64/128, different sequence counts. + paged_gated_delta_net_test_params{{3, 3}, {2, 3}, {2, 3}, 2, 2, 16, ov::element::f16}, + paged_gated_delta_net_test_params{{2, 3, 2}, {1, 2, 4}, {2, 3, 2}, 2, 2, 64, ov::element::f16}, + paged_gated_delta_net_test_params{{1, 2, 3, 2}, {1, 3, 2, 4}, {2, 2, 3, 4}, 2, 2, 128, ov::element::f16}, + // f16: prefill stage (all past_len = 0), head_size 16/64/128, different sequence counts. + paged_gated_delta_net_test_params{{3, 2}, {0, 0}, {2, 2}, 2, 2, 16, ov::element::f16}, + paged_gated_delta_net_test_params{{2, 2, 2}, {0, 0, 0}, {2, 2, 2}, 2, 2, 64, ov::element::f16}, + paged_gated_delta_net_test_params{{1, 2, 2, 3}, {0, 0, 0, 0}, {2, 2, 3, 2}, 2, 2, 128, ov::element::f16}, + // f16: mixed stage (some past_len = 0, some past_len > 0), head_size 16/64/128. + paged_gated_delta_net_test_params{{4, 2}, {0, 3}, {2, 2}, 2, 2, 16, ov::element::f16}, + paged_gated_delta_net_test_params{{1, 4, 2}, {0, 2, 0}, {2, 3, 2}, 2, 2, 64, ov::element::f16}, + paged_gated_delta_net_test_params{{2, 1, 3, 2}, {0, 4, 0, 1}, {2, 2, 3, 2}, 2, 2, 128, ov::element::f16}, + // f16: blocking stress (cache_interval=16), seq lengths occupy 1/2/3/4 blocks. + // fully occupied past blocks + paged_gated_delta_net_test_params{{16, 32, 48, 64}, {16, 32, 48, 64}, {16, 16, 16, 16}, 2, 2, 64, ov::element::f16}, + // partially occupied past blocks + paged_gated_delta_net_test_params{{16, 32, 48, 64}, {1, 17, 33, 49}, {16, 16, 16, 16}, 2, 2, 64, ov::element::f16}, + + // f32: decode stage (past_len > 0), head_size 16/64/128, different sequence counts. + paged_gated_delta_net_test_params{{3, 3}, {2, 3}, {2, 3}, 2, 2, 16, ov::element::f32}, + paged_gated_delta_net_test_params{{2, 3, 2}, {1, 2, 4}, {2, 3, 2}, 2, 2, 64, ov::element::f32}, + paged_gated_delta_net_test_params{{1, 2, 3, 2}, {1, 3, 2, 4}, {2, 2, 3, 4}, 2, 2, 128, ov::element::f32}, + // f32: prefill stage (all past_len = 0), head_size 16/64/128, different sequence counts. + paged_gated_delta_net_test_params{{3, 2}, {0, 0}, {2, 2}, 2, 2, 16, ov::element::f32}, + paged_gated_delta_net_test_params{{2, 2, 2}, {0, 0, 0}, {2, 2, 2}, 2, 2, 64, ov::element::f32}, + paged_gated_delta_net_test_params{{1, 2, 2, 3}, {0, 0, 0, 0}, {2, 2, 3, 2}, 2, 2, 128, ov::element::f32}, + // f32: mixed stage (some past_len = 0, some past_len > 0), head_size 16/64/128. + paged_gated_delta_net_test_params{{4, 2}, {0, 3}, {2, 2}, 2, 2, 16, ov::element::f32}, + paged_gated_delta_net_test_params{{1, 4, 2}, {0, 2, 0}, {2, 3, 2}, 2, 2, 64, ov::element::f32}, + paged_gated_delta_net_test_params{{2, 1, 3, 2}, {0, 4, 0, 1}, {2, 2, 3, 2}, 2, 2, 128, ov::element::f32}, + // f32: blocking stress (cache_interval=16), seq lengths occupy 1/2/3/4 blocks. + // fully occupied past blocks + paged_gated_delta_net_test_params{{16, 32, 48, 64}, {16, 32, 48, 64}, {16, 16, 16, 16}, 2, 2, 64, ov::element::f32}, + // partially occupied past blocks + paged_gated_delta_net_test_params{{16, 32, 48, 64}, {1, 17, 33, 49}, {16, 16, 16, 16}, 2, 2, 64, ov::element::f32}), + paged_gated_delta_net_gpu_test::PrintToStringParamName); + +} // namespace diff --git a/src/plugins/intel_gpu/tests/unit/test_cases/pooling_gpu_test.cpp b/src/plugins/intel_gpu/tests/unit/test_cases/pooling_gpu_test.cpp index 7ab45a7e9ece..396ea57093e0 100644 --- a/src/plugins/intel_gpu/tests/unit/test_cases/pooling_gpu_test.cpp +++ b/src/plugins/intel_gpu/tests/unit/test_cases/pooling_gpu_test.cpp @@ -315,7 +315,7 @@ TEST(pooling_forward_gpu, basic_max_pooling_int8) { auto outputs = network.execute(); auto interm = outputs.at("reorder2").get_memory(); - cldnn::mem_lock interm_ptr(interm, get_test_stream()); + cldnn::mem_lock interm_ptr(interm, get_test_stream()); unsigned int cntr = 0; for (const auto& exp : final_results) { @@ -364,7 +364,7 @@ TEST(pooling_forward_gpu, basic_avg_pooling_int8) { auto outputs = network.execute(); auto interm = outputs.at("reorder2").get_memory(); - cldnn::mem_lock interm_ptr(interm, get_test_stream()); + cldnn::mem_lock interm_ptr(interm, get_test_stream()); ASSERT_EQ(final_result, interm_ptr[0]); } @@ -603,7 +603,7 @@ TEST(pooling_forward_gpu, offsets_max_yxfb_f32_wsiz2x2_wstr2x2_i3x3x1x1_zeropad) auto output_prim = outputs.begin()->second.get_memory(); ASSERT_EQ((int)output_prim->get_layout().count(), 4); - cldnn::mem_lock output_ptr(output_prim, get_test_stream()); + cldnn::mem_lock output_ptr(output_prim, get_test_stream()); ASSERT_EQ(1.5f, output_ptr[0]); ASSERT_EQ(-0.5f, output_ptr[1]); ASSERT_EQ(1.0f, output_ptr[2]); @@ -1254,7 +1254,7 @@ static void generic_average_wo_padding_test(format fmt, tensor output, tensor in ASSERT_EQ(output_mem->count(), expected_output.size()); ASSERT_EQ(output_mem->get_layout().get_tensor(), output); - cldnn::mem_lock out_ptr(output_mem, get_test_stream()); + cldnn::mem_lock out_ptr(output_mem, get_test_stream()); for (size_t i = 0; i < expected_output.size(); ++i) ASSERT_FLOAT_EQ(out_ptr[i], expected_output[i]); @@ -1560,7 +1560,7 @@ TEST(pooling_forward_gpu, fs_b_yx_fsv32_avg_3x3_input_2x2_pool_1x1_stride_2x2_ou auto output_prim = outputs.begin()->second.get_memory(); - cldnn::mem_lock output_ptr(output_prim, get_test_stream()); + cldnn::mem_lock output_ptr(output_prim, get_test_stream()); ASSERT_EQ(1.0f, float(output_ptr[0])); ASSERT_EQ(0.625f, float(output_ptr[1])); @@ -1611,7 +1611,7 @@ TEST(pooling_forward_gpu, fs_b_yx_fsv32_avg_3x3_input_2x2_pool_2x2_stride) ASSERT_EQ(outputs.begin()->first, "reorder_after_pooling"); auto output_prim = outputs.begin()->second.get_memory(); - cldnn::mem_lock output_ptr(output_prim, get_test_stream()); + cldnn::mem_lock output_ptr(output_prim, get_test_stream()); ASSERT_EQ(1.0f, float(output_ptr[0])); ASSERT_EQ(0.f, float(output_ptr[1])); @@ -1681,7 +1681,7 @@ TEST(pooling_forward_gpu, fs_b_yx_fsv32_avg_2x2x3x3_input_2x2_pool_2x2_stride) auto output_prim = outputs.begin()->second.get_memory(); - cldnn::mem_lock output_ptr(output_prim, get_test_stream()); + cldnn::mem_lock output_ptr(output_prim, get_test_stream()); ASSERT_EQ((int)output_ptr.size(), batch_count * features_count*out_x*out_y); @@ -1766,7 +1766,7 @@ TEST(pooling_forward_gpu, fs_b_yx_fsv32_max_1x1x3x3_input_2x2_pool_2x2_stride_2x ASSERT_EQ((int)output_prim->get_layout().count(), 4); ASSERT_EQ((int)output_prim->get_layout().get_linear_size(), 16); - cldnn::mem_lock output_ptr(output_prim, get_test_stream()); + cldnn::mem_lock output_ptr(output_prim, get_test_stream()); for (size_t i = 0; i < expected.size(); ++i) { ASSERT_EQ(expected[i], float(output_ptr[i])); @@ -1844,7 +1844,7 @@ TEST(pooling_forward_gpu, fs_b_yx_fsv32_max_1x1x5x5_input_2x2_pool_2x2_stride_2x ASSERT_EQ((int)output_prim->get_layout().count(), 9); ASSERT_EQ((int)output_prim->get_layout().get_linear_size(), 25); - cldnn::mem_lock output_ptr(output_prim, get_test_stream()); + cldnn::mem_lock output_ptr(output_prim, get_test_stream()); for (size_t i = 0; i < expected.size(); ++i) { ASSERT_EQ(expected[i], float(output_ptr[i])); @@ -1990,7 +1990,7 @@ class pooling_test_base { auto result = net->execute(); auto out_mem = result.at(output_id()).get_memory(); auto out_lay = out_mem->get_layout(); - cldnn::mem_lock out_ptr(out_mem, get_test_stream()); + cldnn::mem_lock out_ptr(out_mem, get_test_stream()); if (!is_caching_test) { std::string kernel; @@ -3333,7 +3333,7 @@ TEST(pooling_forward_gpu_onednn, basic_max_pooling_int8) { auto outputs = network.execute(); auto interm = outputs.at("reorder2").get_memory(); - cldnn::mem_lock interm_ptr(interm, get_test_stream()); + cldnn::mem_lock interm_ptr(interm, get_test_stream()); unsigned int cntr = 0; for (const auto& exp : final_results) { ASSERT_EQ(exp, interm_ptr[cntr++]); diff --git a/src/plugins/intel_gpu/tests/unit/test_cases/prior_box_gpu_test.cpp b/src/plugins/intel_gpu/tests/unit/test_cases/prior_box_gpu_test.cpp index f997054601da..d61e34d3a4b5 100644 --- a/src/plugins/intel_gpu/tests/unit/test_cases/prior_box_gpu_test.cpp +++ b/src/plugins/intel_gpu/tests/unit/test_cases/prior_box_gpu_test.cpp @@ -1,5 +1,6 @@ // Copyright (C) 2018-2026 Intel Corporation // SPDX-License-Identifier: Apache-2.0 +// #include #include @@ -98,7 +99,7 @@ class PriorBoxGPUTest : public ::testing::TestWithParamexecute(); const auto output = outputs.at("prior_box").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); ASSERT_EQ(output_ptr.size(), expected_values.size()); for (size_t i = 0; i < output_ptr.size(); ++i) { diff --git a/src/plugins/intel_gpu/tests/unit/test_cases/propagate_constants_gpu_test.cpp b/src/plugins/intel_gpu/tests/unit/test_cases/propagate_constants_gpu_test.cpp index dee226c26bf3..43d9b4647604 100644 --- a/src/plugins/intel_gpu/tests/unit/test_cases/propagate_constants_gpu_test.cpp +++ b/src/plugins/intel_gpu/tests/unit/test_cases/propagate_constants_gpu_test.cpp @@ -104,7 +104,7 @@ TEST(propagate_constants, permute_1_0_reorder_fc) { auto outputs = network.execute(); auto output = outputs.at("fc1").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); ExecutionConfig config_ref = get_test_default_config(engine); config_ref.set_property(ov::intel_gpu::optimize_data(false)); @@ -121,7 +121,7 @@ TEST(propagate_constants, permute_1_0_reorder_fc) { auto outputs_ref = network_ref.execute(); auto output_ref = outputs_ref.at("fc1").get_memory(); - cldnn::mem_lock output_ref_ptr(output_ref, get_test_stream()); + cldnn::mem_lock output_ref_ptr(output_ref, get_test_stream()); for (size_t i = 0; i < output_ref_ptr.size(); ++i) { ASSERT_EQ(output_ptr[i], output_ref_ptr[i]); diff --git a/src/plugins/intel_gpu/tests/unit/test_cases/quantize_gpu_test.cpp b/src/plugins/intel_gpu/tests/unit/test_cases/quantize_gpu_test.cpp index efebd09d1adc..b2ca1d78e280 100644 --- a/src/plugins/intel_gpu/tests/unit/test_cases/quantize_gpu_test.cpp +++ b/src/plugins/intel_gpu/tests/unit/test_cases/quantize_gpu_test.cpp @@ -90,7 +90,7 @@ TEST(quantize_gpu, quantize_levels_2_output_broadcast_inputs_1) { auto outputs = network.execute(); auto output = outputs.at("quantize").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); // Check that layout and memory contains logical size of tensor ASSERT_EQ(output->count(), (size_t)64); @@ -154,7 +154,7 @@ TEST(quantize_gpu, quantize_levels_2_output_broadcast_inputs_1_ch8) { auto outputs = network.execute(); auto output = outputs.at("quantize").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); // Check that layout and memory contains logical size of tensor ASSERT_EQ(output->count(), (size_t)32); @@ -232,7 +232,7 @@ TEST(quantize_gpu, quantize_levels_2_output_broadcast_inputs_2) { auto outputs = network.execute(); auto output = outputs.at("quantize").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); // Check that layout and memory contains logical size of tensor ASSERT_EQ(output->count(), (size_t)64); @@ -321,7 +321,7 @@ TEST(quantize_gpu, quantize_levels_3) { auto outputs = network.execute(); auto output = outputs.at("quantize").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); // Check that layout and memory contains logical size of tensor ASSERT_EQ(output->count(), ref_data.size()); @@ -412,7 +412,7 @@ TEST(quantize_gpu, quantize_levels_256_2d_unsigned) { auto outputs = network.execute(); auto output = outputs.at("quantize").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); // Check that layout and memory contains logical size of tensor ASSERT_EQ(output->count(), ref_data.size()); @@ -503,7 +503,7 @@ TEST(quantize_gpu, quantize_levels_256_2d_unsigned_const_input) { auto outputs = network.execute(); auto output = outputs.at("quantize").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); // Check that layout and memory contains logical size of tensor ASSERT_EQ(output->count(), ref_data.size()); @@ -595,7 +595,7 @@ TEST(quantize_gpu, quantize_levels_256_3d_unsigned) { auto outputs = network.execute(); auto output = outputs.at("out").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); // Check that layout and memory contains logical size of tensor ASSERT_EQ(output->count(), ref_data.size()); @@ -725,7 +725,7 @@ TEST(quantize_gpu, eltwise_quantize_fs_b_yx_fsv32) { auto outputs = network.execute(); auto output = outputs.at("reorder").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); // Check that layout and memory contains logical size of tensor ASSERT_EQ(output->count(), (size_t)64); @@ -827,7 +827,7 @@ TEST(quantize_gpu, dynamic) { auto outputs = network.execute(); auto output = outputs.at("quantize").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); // Check that layout and memory contains logical size of tensor ASSERT_EQ(output->count(), (size_t)64); @@ -930,7 +930,7 @@ TEST(quantize_gpu, dynamic_fsv16) { auto outputs = network.execute(); auto output = outputs.at("output_reorder").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); // Check that layout and memory contains logical size of tensor ASSERT_EQ(output->count(), (size_t)64); diff --git a/src/plugins/intel_gpu/tests/unit/test_cases/random_uniform_gpu_test.cpp b/src/plugins/intel_gpu/tests/unit/test_cases/random_uniform_gpu_test.cpp index 4fadee61bbc4..c90b13f439be 100644 --- a/src/plugins/intel_gpu/tests/unit/test_cases/random_uniform_gpu_test.cpp +++ b/src/plugins/intel_gpu/tests/unit/test_cases/random_uniform_gpu_test.cpp @@ -68,7 +68,7 @@ struct random_uniform_gpu_test : public ::testing::TestWithParamexecute(); auto out_mem = result.at("random_uniform").get_memory(); - cldnn::mem_lock out_ptr(out_mem, get_test_stream()); + cldnn::mem_lock out_ptr(out_mem, get_test_stream()); ASSERT_EQ(params.expected_out.size(), out_ptr.size()); for (size_t i = 0; i < params.expected_out.size(); ++i) { diff --git a/src/plugins/intel_gpu/tests/unit/test_cases/reduce_gpu_test.cpp b/src/plugins/intel_gpu/tests/unit/test_cases/reduce_gpu_test.cpp index acdc6e1ec565..5193f7453a2a 100644 --- a/src/plugins/intel_gpu/tests/unit/test_cases/reduce_gpu_test.cpp +++ b/src/plugins/intel_gpu/tests/unit/test_cases/reduce_gpu_test.cpp @@ -541,7 +541,7 @@ class ReduceTestBase : public ::testing::TestWithParamexecute(); auto out_mem = outputs.at("reduce").get_memory(); - cldnn::mem_lock out_ptr(out_mem, get_test_stream()); + cldnn::mem_lock out_ptr(out_mem, get_test_stream()); auto out_lay = out_mem->get_layout(); ASSERT_EQ(out_lay.get_tensor().sizes()[0], reference_result.size()); // b @@ -798,7 +798,7 @@ void test_common_bfyx(bool is_caching_test) { std::vector ref_data = {1.0f}; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (size_t i = 0; i < ref_data.size(); ++i) { ASSERT_TRUE(are_equal(ref_data[i], output_ptr[i])); @@ -832,7 +832,7 @@ TEST(reduce_gpu, common_bfyx_keepdims) { std::vector ref_data = {6.0f, 22.0f, 38.0f}; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (size_t i = 0; i < ref_data.size(); ++i) { ASSERT_TRUE(are_equal(ref_data[i], output_ptr[i])); @@ -862,7 +862,7 @@ TEST(reduce_gpu, regr_bfyx_keepdims) { std::vector ref_data = { 1.0f, 5.0f, 9.0f, 13.0f, 17.0f, 21.0f }; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (size_t i = 0; i < ref_data.size(); ++i) { ASSERT_TRUE(are_equal(ref_data[i], output_ptr[i])); @@ -892,7 +892,7 @@ TEST(reduce_gpu, common_bfzyx) { std::vector ref_data = {1.0f}; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (size_t i = 0; i < ref_data.size(); ++i) { ASSERT_TRUE(are_equal(ref_data[i], output_ptr[i])); @@ -922,7 +922,7 @@ TEST(reduce_gpu, common_bfzyx_keepdims) { std::vector ref_data = {1.0f}; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (size_t i = 0; i < ref_data.size(); ++i) { ASSERT_TRUE(are_equal(ref_data[i], output_ptr[i])); @@ -952,7 +952,7 @@ TEST(reduce_gpu, common_bfwzyx) { std::vector ref_data = {6.0f, 22.0f, 38.0f}; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (size_t i = 0; i < ref_data.size(); ++i) { ASSERT_TRUE(are_equal(ref_data[i], output_ptr[i])); @@ -982,7 +982,7 @@ TEST(reduce_gpu, common_bfwzyx_keepdims) { std::vector ref_data = {66.0f}; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (size_t i = 0; i < ref_data.size(); ++i) { ASSERT_TRUE(are_equal(ref_data[i], output_ptr[i])); @@ -1013,7 +1013,7 @@ TEST(reduce_gpu, common_bfwzyx_max_keepdims) { std::vector ref_data = {20.0f, 21.0f, 22.0f, 23.0f}; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (size_t i = 0; i < ref_data.size(); ++i) { ASSERT_TRUE(are_equal(ref_data[i], output_ptr[i])); @@ -1043,7 +1043,7 @@ TEST(reduce_gpu, common_bfwzyx_min) { std::vector ref_data = {0.0f, 3.0f}; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (size_t i = 0; i < ref_data.size(); ++i) { ASSERT_TRUE(are_equal(ref_data[i], output_ptr[i])); @@ -1073,7 +1073,7 @@ TEST(reduce_gpu, common_bfwzyx_min_keepdims) { std::vector ref_data = {0.0f, 3.0f}; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (size_t i = 0; i < ref_data.size(); ++i) { ASSERT_TRUE(are_equal(ref_data[i], output_ptr[i])); @@ -1103,7 +1103,7 @@ TEST(reduce_gpu, common_bfwzyx_mean) { std::vector ref_data = {1.0f, 4.0f}; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (size_t i = 0; i < ref_data.size(); ++i) { ASSERT_TRUE(are_equal(ref_data[i], output_ptr[i])); @@ -1133,7 +1133,7 @@ TEST(reduce_gpu, common_bfwzyx_mean_keepdims) { std::vector ref_data = {1.0f, 4.0f}; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (size_t i = 0; i < ref_data.size(); ++i) { ASSERT_TRUE(are_equal(ref_data[i], output_ptr[i])); @@ -1163,7 +1163,7 @@ TEST(reduce_gpu, common_bfwzyx_prod) { std::vector ref_data = {0.0f, 60.0f}; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (size_t i = 0; i < ref_data.size(); ++i) { ASSERT_TRUE(are_equal(ref_data[i], output_ptr[i])); @@ -1193,7 +1193,7 @@ TEST(reduce_gpu, common_bfwzyx_prod_keepdims) { std::vector ref_data = {0.0f, 60.0f}; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (size_t i = 0; i < ref_data.size(); ++i) { ASSERT_TRUE(are_equal(ref_data[i], output_ptr[i])); @@ -1224,7 +1224,7 @@ TEST(reduce_gpu, common_bfwzyx_sum_keepdims) { std::vector ref_data = {60.0f, 66.0f, 72.0f, 78.0f}; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (size_t i = 0; i < ref_data.size(); ++i) { ASSERT_TRUE(are_equal(ref_data[i], output_ptr[i])); @@ -1254,7 +1254,7 @@ TEST(reduce_gpu, common_bfwzyx_logical_and) { std::vector ref_data = {0, 1}; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (size_t i = 0; i < ref_data.size(); ++i) { ASSERT_TRUE(are_equal(ref_data[i], output_ptr[i])); @@ -1284,7 +1284,7 @@ TEST(reduce_gpu, common_bfwzyx_logical_and_keepdims) { std::vector ref_data = {0, 1}; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (size_t i = 0; i < ref_data.size(); ++i) { ASSERT_TRUE(are_equal(ref_data[i], output_ptr[i])); @@ -1314,7 +1314,7 @@ TEST(reduce_gpu, common_bfwzyx_logical_or) { std::vector ref_data = {1, 1}; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (size_t i = 0; i < ref_data.size(); ++i) { ASSERT_TRUE(are_equal(ref_data[i], output_ptr[i])); @@ -1344,7 +1344,7 @@ TEST(reduce_gpu, common_bfwzyx_logical_or_keepdims) { std::vector ref_data = {1, 1}; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (size_t i = 0; i < ref_data.size(); ++i) { ASSERT_TRUE(are_equal(ref_data[i], output_ptr[i])); @@ -1374,7 +1374,7 @@ TEST(reduce_gpu, common_bfwzyx_sum_square) { std::vector ref_data = {5.0f, 50.0f}; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (size_t i = 0; i < ref_data.size(); ++i) { ASSERT_TRUE(are_equal(ref_data[i], output_ptr[i])); @@ -1404,7 +1404,7 @@ TEST(reduce_gpu, common_bfwzyx_sum_square_keepdims) { std::vector ref_data = {5.0f, 50.0f}; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (size_t i = 0; i < ref_data.size(); ++i) { ASSERT_TRUE(are_equal(ref_data[i], output_ptr[i])); @@ -1434,7 +1434,7 @@ TEST(reduce_gpu, common_bfwzyx_l1) { std::vector ref_data = {3.0f, 12.0f}; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (size_t i = 0; i < ref_data.size(); ++i) { ASSERT_TRUE(are_equal(ref_data[i], output_ptr[i])); @@ -1464,7 +1464,7 @@ TEST(reduce_gpu, common_bfwzyx_l1_keepdims) { std::vector ref_data = {3.0f, 12.0f}; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (size_t i = 0; i < ref_data.size(); ++i) { ASSERT_TRUE(are_equal(ref_data[i], output_ptr[i])); @@ -1494,7 +1494,7 @@ TEST(reduce_gpu, common_bfwzyx_l2) { std::vector ref_data = {2.236067977f, 7.071067812f}; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (size_t i = 0; i < ref_data.size(); ++i) { ASSERT_TRUE(are_equal(ref_data[i], output_ptr[i])); @@ -1524,7 +1524,7 @@ TEST(reduce_gpu, common_bfwzyx_l2_keepdims) { std::vector ref_data = {2.236067977f, 7.071067812f}; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (size_t i = 0; i < ref_data.size(); ++i) { ASSERT_TRUE(are_equal(ref_data[i], output_ptr[i])); @@ -1554,7 +1554,7 @@ TEST(reduce_gpu, common_bfwzyx_log_sum) { std::vector ref_data = {1.0986122887f, 2.4849066498f}; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (size_t i = 0; i < ref_data.size(); ++i) { ASSERT_TRUE(are_equal(ref_data[i], output_ptr[i])); @@ -1584,7 +1584,7 @@ TEST(reduce_gpu, common_bfwzyx_log_sum_keepdims) { std::vector ref_data = {1.0986122887f, 2.4849066498f}; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (size_t i = 0; i < ref_data.size(); ++i) { ASSERT_TRUE(are_equal(ref_data[i], output_ptr[i])); @@ -1614,7 +1614,7 @@ TEST(reduce_gpu, common_bfwzyx_log_sum_exp) { std::vector ref_data = {2.407605964f, 5.407605964f}; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (size_t i = 0; i < ref_data.size(); ++i) { ASSERT_TRUE(are_equal(ref_data[i], output_ptr[i])); @@ -1644,7 +1644,7 @@ TEST(reduce_gpu, common_bfwzyx_log_sum_exp_keepdims) { std::vector ref_data = {2.407605964f, 5.407605964f}; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (size_t i = 0; i < ref_data.size(); ++i) { ASSERT_TRUE(are_equal(ref_data[i], output_ptr[i])); @@ -1676,7 +1676,7 @@ TEST(reduce_gpu, cpu_impl_int32) { std::vector ref_data = {24}; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (size_t i = 0; i < ref_data.size(); ++i) { ASSERT_EQ(ref_data[i], output_ptr[i]); @@ -1713,7 +1713,7 @@ TEST(reduce_gpu, dynamic) { auto output = outputs.at("reduce").get_memory(); std::vector ref_data = {0.0f, 60.0f}; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (size_t i = 0; i < ref_data.size(); ++i) { ASSERT_TRUE(are_equal(ref_data[i], output_ptr[i])); @@ -1770,7 +1770,7 @@ TEST(reduce_gpu, b_fs_yx_fsv16_min_dynamic) { std::vector ref_data = {1.0f, -9.0f}; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (size_t i = 0; i < ref_data.size(); ++i) { ASSERT_TRUE(are_equal(ref_data[i], output_ptr[i])); @@ -1827,7 +1827,7 @@ TEST(reduce_gpu, b_fs_yx_fsv16_max_dynamic) { std::vector ref_data = {9.0f, -1.0f}; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (size_t i = 0; i < ref_data.size(); ++i) { ASSERT_TRUE(are_equal(ref_data[i], output_ptr[i])); @@ -1979,7 +1979,7 @@ class ReduceXYWithBigTensorTestBase : public ::testing::TestWithParamexecute(); auto out_mem = outputs.at("reduce").get_memory(); - cldnn::mem_lock out_ptr(out_mem, get_test_stream()); + cldnn::mem_lock out_ptr(out_mem, get_test_stream()); auto out_lay = out_mem->get_layout(); ASSERT_EQ(out_lay.get_tensor().sizes()[0], reference_result.size()); // b @@ -2139,7 +2139,7 @@ class ReduceOnednnTestBase : public ::testing::TestWithParam out_ptr(out_mem, get_test_stream()); + cldnn::mem_lock out_ptr(out_mem, get_test_stream()); auto out_lay = out_mem->get_layout(); ASSERT_EQ(out_lay.get_tensor().sizes()[0], reference_result.size()); // b diff --git a/src/plugins/intel_gpu/tests/unit/test_cases/region_yolo_gpu_test.cpp b/src/plugins/intel_gpu/tests/unit/test_cases/region_yolo_gpu_test.cpp index 7038b1ad30b0..1ce9d3f8b100 100644 --- a/src/plugins/intel_gpu/tests/unit/test_cases/region_yolo_gpu_test.cpp +++ b/src/plugins/intel_gpu/tests/unit/test_cases/region_yolo_gpu_test.cpp @@ -191,7 +191,7 @@ void runRegionTest(region_yolo_test_params& params, bool is_caching_test = false auto outputs = network->execute(); auto output = outputs.at("reorder_post").get_memory(); - cldnn::mem_lock outputData(output, get_test_stream()); + cldnn::mem_lock outputData(output, get_test_stream()); /// reference value std::vector refOutputData(inputData.size()); diff --git a/src/plugins/intel_gpu/tests/unit/test_cases/removing_output_node_test.cpp b/src/plugins/intel_gpu/tests/unit/test_cases/removing_output_node_test.cpp index 017fdcdbe908..494762a9a1b8 100644 --- a/src/plugins/intel_gpu/tests/unit/test_cases/removing_output_node_test.cpp +++ b/src/plugins/intel_gpu/tests/unit/test_cases/removing_output_node_test.cpp @@ -71,7 +71,7 @@ void test_multiple_outputs(bool is_caching_test) { auto outputs = network->execute(); auto output = outputs.at("reshape").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); ASSERT_TRUE(output->get_layout().get_tensor() == after_reshape); @@ -141,7 +141,7 @@ void test_output_node_optimization(bool is_caching_test) { auto output_memory = outputs.at("relu").get_memory(); auto output_layout = output_memory->get_layout(); - cldnn::mem_lock output_ptr(output_memory, get_test_stream()); + cldnn::mem_lock output_ptr(output_memory, get_test_stream()); int y_size = output_layout.spatial(1); int x_size = output_layout.spatial(0); diff --git a/src/plugins/intel_gpu/tests/unit/test_cases/reorder_gpu_test.cpp b/src/plugins/intel_gpu/tests/unit/test_cases/reorder_gpu_test.cpp index f38162dbb6f8..141e40b37dac 100644 --- a/src/plugins/intel_gpu/tests/unit/test_cases/reorder_gpu_test.cpp +++ b/src/plugins/intel_gpu/tests/unit/test_cases/reorder_gpu_test.cpp @@ -19,6 +19,7 @@ #include #include +#include using namespace cldnn; using namespace ::tests; @@ -269,6 +270,554 @@ TEST(reorder_gpu_optimization, compare_with_ref__bfyx_to_blocked_format_differen compare_bfyx2blocked_with_ref("reorder_data_bfyx_to_blocked_format", data_types::i64, data_types::f32, format::bfyx, format::b_fs_yx_fsv16, 3, 32 + 4, 16 + 7, 2, 0, 0, false); } +static void compare_bfyx2blocked_with_ref_dynamic(const std::string& kernel_name, + const data_types input_data_type, const data_types output_data_type, + cldnn::format input_format, cldnn::format output_format, + int32_t b_in, int32_t f_in, int32_t x_in, int32_t y_in, int32_t z_in, int32_t w_in) { + auto& engine = get_test_engine(); + if (engine.get_device_info().supports_immad) { + return; + } + + tensor ts; + if (input_format.dimension() == 4) { + ts = { b_in, f_in, x_in, y_in }; + } else if (input_format.dimension() == 5) { + ts = { b_in, f_in, x_in, y_in, z_in }; + } else { + ts = { b_in, f_in, x_in, y_in, z_in, w_in }; + } + + auto input = engine.allocate_memory({ input_data_type, input_format, ts }); + layout output_layout(output_data_type, output_format, ts); + + { + auto stream = std::shared_ptr(engine.create_stream(get_test_default_config(engine))); + if (input_data_type == data_types::i8 || input_data_type == data_types::u8) { + mem_lock input_ptr{input, *stream}; + unsigned char i = 1; + for (auto it = input_ptr.begin(); it != input_ptr.end(); ++it) { + *it = (i++); + if (i > 100) { i = 1; } + } + } else if (input_data_type == data_types::f16) { + mem_lock input_ptr{input, *stream}; + ov::float16 i = ov::float16(1.0f); + for (auto it = input_ptr.begin(); it != input_ptr.end(); ++it) { + *it = i; + i = ov::float16(static_cast(i) + 1.0f); + } + } else { + mem_lock input_ptr{input, *stream}; + float i = 1.f; + for (auto it = input_ptr.begin(); it != input_ptr.end(); ++it) { + *it = (i); + i += 1.f; + } + } + } + + // Reference: static shape with reorder_data (ref kernel) + topology topo_ref( + input_layout("input", input->get_layout()), + reorder("reorder", input_info("input"), output_layout)); + + ExecutionConfig config_ref = get_test_default_config(engine); + ov::intel_gpu::ImplementationDesc reorder_ref = { output_format, "reorder_data" }; + config_ref.set_property(ov::intel_gpu::force_implementations(ov::intel_gpu::ImplForcingMap{ {"reorder", reorder_ref} })); + + cldnn::network network_ref(engine, topo_ref, config_ref); + network_ref.set_input_data("input", input); + auto outputs_ref = network_ref.execute(); + + // Dynamic: shape-agnostic with optimized kernel + layout in_dynamic_layout{ov::PartialShape::dynamic(input_format.dimension()), input_data_type, input_format}; + + topology topo_dyn( + input_layout("input", in_dynamic_layout), + reorder("reorder", input_info("input"), output_format, output_data_type)); + + ExecutionConfig config_dyn = get_test_default_config(engine); + ov::intel_gpu::ImplementationDesc reorder_opt = { output_format, kernel_name }; + config_dyn.set_property(ov::intel_gpu::force_implementations(ov::intel_gpu::ImplForcingMap{ {"reorder", reorder_opt} })); + config_dyn.set_property(ov::intel_gpu::allow_new_shape_infer(true)); + + cldnn::network network_dyn(engine, topo_dyn, config_dyn); + + network_dyn.set_input_data("input", input); + auto outputs_dyn = network_dyn.execute(); + + auto reorder_inst = network_dyn.get_primitive("reorder"); + auto reorder_impl = reorder_inst->get_impl(); + ASSERT_TRUE(reorder_impl != nullptr); + ASSERT_NE(reorder_impl->get_kernel_name().find(kernel_name), std::string::npos); + + // Compare ref vs dynamic + if (output_data_type == data_types::i8) + compare_result(outputs_ref, outputs_dyn); + else if (output_data_type == data_types::u8) + compare_result(outputs_ref, outputs_dyn); + else if (output_data_type == data_types::i32) + compare_result(outputs_ref, outputs_dyn); + else if (output_data_type == data_types::i64) + compare_result(outputs_ref, outputs_dyn); + else if (output_data_type == data_types::f16) + compare_result(outputs_ref, outputs_dyn); + else if (output_data_type == data_types::f32) + compare_result(outputs_ref, outputs_dyn); +} + +static void compare_fsv_reorder_with_ref_output_padding(const data_types input_data_type, + const data_types output_data_type, + cldnn::format input_format, + cldnn::format output_format, + int32_t b_in, + int32_t f_in, + int32_t x_in, + int32_t y_in, + int32_t z_in, + int32_t w_in, + const padding& output_padding) { + auto& engine = get_test_engine(); + if (engine.get_device_info().supports_immad) { + return; + } + + tensor ts; + if (input_format.dimension() == 4) { + ts = { b_in, f_in, x_in, y_in }; + } else if (input_format.dimension() == 5) { + ts = { b_in, f_in, x_in, y_in, z_in }; + } else { + ts = { b_in, f_in, x_in, y_in, z_in, w_in }; + } + + auto input = engine.allocate_memory({ input_data_type, input_format, ts }); + layout output_layout(output_data_type, output_format, ts, output_padding); + + { + auto stream = std::shared_ptr(engine.create_stream(get_test_default_config(engine))); + if (input_data_type == data_types::i8 || input_data_type == data_types::u8) { + mem_lock input_ptr{input, *stream}; + unsigned char i = 1; + for (auto it = input_ptr.begin(); it != input_ptr.end(); ++it) { + *it = (i++); + if (i > 100) { + i = 1; + } + } + } else if (input_data_type == data_types::f16) { + mem_lock input_ptr{input, *stream}; + ov::float16 i = ov::float16(1.0f); + for (auto it = input_ptr.begin(); it != input_ptr.end(); ++it) { + *it = i; + i = ov::float16(static_cast(i) + 1.0f); + } + } else { + mem_lock input_ptr{input, *stream}; + float i = 1.f; + for (auto it = input_ptr.begin(); it != input_ptr.end(); ++it) { + *it = i; + i += 1.f; + } + } + } + + topology topo_ref( + input_layout("input", input->get_layout()), + reorder("reorder", input_info("input"), output_layout)); + + ExecutionConfig config_ref = get_test_default_config(engine); + ov::intel_gpu::ImplementationDesc reorder_ref = { output_format, "reorder_data" }; + config_ref.set_property(ov::intel_gpu::force_implementations(ov::intel_gpu::ImplForcingMap{{"reorder", reorder_ref}})); + + cldnn::network network_ref(engine, topo_ref, config_ref); + network_ref.set_input_data("input", input); + auto outputs_ref = network_ref.execute(); + + ExecutionConfig config_opt = get_test_default_config(engine); + ov::intel_gpu::ImplementationDesc reorder_opt = { output_format, "reorder_data_fsv" }; + config_opt.set_property(ov::intel_gpu::force_implementations(ov::intel_gpu::ImplForcingMap{{"reorder", reorder_opt}})); + + cldnn::network network_opt(engine, topo_ref, config_opt); + network_opt.set_input_data("input", input); + auto outputs_opt = network_opt.execute(); + + auto reorder_inst = network_opt.get_primitive("reorder"); + auto reorder_impl = reorder_inst->get_impl(); + ASSERT_TRUE(reorder_impl != nullptr); + ASSERT_NE(reorder_impl->get_kernel_name().find("reorder_data_fsv"), std::string::npos); + + if (output_data_type == data_types::i8) + compare_result(outputs_ref, outputs_opt); + else if (output_data_type == data_types::u8) + compare_result(outputs_ref, outputs_opt); + else if (output_data_type == data_types::i32) + compare_result(outputs_ref, outputs_opt); + else if (output_data_type == data_types::i64) + compare_result(outputs_ref, outputs_opt); + else if (output_data_type == data_types::f16) + compare_result(outputs_ref, outputs_opt); + else if (output_data_type == data_types::f32) + compare_result(outputs_ref, outputs_opt); +} + +template +static void fill_fsv_reorder_input(const memory::ptr& input, + const layout& input_layout, + int32_t b_in, + int32_t f_in, + int32_t x_in, + int32_t y_in, + int32_t z_in, + int32_t w_in) { + mem_lock input_ptr{input, get_test_stream()}; + std::fill(input_ptr.begin(), input_ptr.end(), T{}); + + int32_t value = 1; + for (int32_t b = 0; b < b_in; ++b) { + for (int32_t f = 0; f < f_in; ++f) { + for (int32_t w = 0; w < w_in; ++w) { + for (int32_t z = 0; z < z_in; ++z) { + for (int32_t y = 0; y < y_in; ++y) { + for (int32_t x = 0; x < x_in; ++x) { + const auto offset = input_layout.get_linear_offset({b, f, x, y, z, w}); + input_ptr[offset] = static_cast(value); + value = value == 100 ? 1 : value + 1; + } + } + } + } + } + } +} + +static void compare_fsv_reorder_with_ref_padding(const data_types input_data_type, + const data_types output_data_type, + cldnn::format input_format, + cldnn::format output_format, + int32_t b_in, + int32_t f_in, + int32_t x_in, + int32_t y_in, + int32_t z_in, + int32_t w_in, + const padding& input_padding, + const padding& output_padding) { + auto& engine = get_test_engine(); + if (engine.get_device_info().supports_immad) { + return; + } + + tensor ts; + if (input_format.dimension() == 4) { + ts = { b_in, f_in, x_in, y_in }; + } else if (input_format.dimension() == 5) { + ts = { b_in, f_in, x_in, y_in, z_in }; + } else { + ts = { b_in, f_in, x_in, y_in, z_in, w_in }; + } + + layout padded_input_layout(input_data_type, input_format, ts, input_padding); + auto input = engine.allocate_memory(padded_input_layout); + layout output_layout(output_data_type, output_format, ts, output_padding); + + if (input_data_type == data_types::i8) { + fill_fsv_reorder_input(input, padded_input_layout, b_in, f_in, x_in, y_in, z_in, w_in); + } else if (input_data_type == data_types::u8) { + fill_fsv_reorder_input(input, padded_input_layout, b_in, f_in, x_in, y_in, z_in, w_in); + } else if (input_data_type == data_types::f16) { + fill_fsv_reorder_input(input, padded_input_layout, b_in, f_in, x_in, y_in, z_in, w_in); + } else { + fill_fsv_reorder_input(input, padded_input_layout, b_in, f_in, x_in, y_in, z_in, w_in); + } + + topology topo_ref( + input_layout("input", input->get_layout()), + reorder("reorder", input_info("input"), output_layout)); + + ExecutionConfig config_ref = get_test_default_config(engine); + ov::intel_gpu::ImplementationDesc reorder_ref = { output_format, "reorder_data" }; + config_ref.set_property(ov::intel_gpu::force_implementations(ov::intel_gpu::ImplForcingMap{{"reorder", reorder_ref}})); + + cldnn::network network_ref(engine, topo_ref, config_ref); + network_ref.set_input_data("input", input); + auto outputs_ref = network_ref.execute(); + + ExecutionConfig config_opt = get_test_default_config(engine); + ov::intel_gpu::ImplementationDesc reorder_opt = { output_format, "reorder_data_fsv" }; + config_opt.set_property(ov::intel_gpu::force_implementations(ov::intel_gpu::ImplForcingMap{{"reorder", reorder_opt}})); + + cldnn::network network_opt(engine, topo_ref, config_opt); + network_opt.set_input_data("input", input); + auto outputs_opt = network_opt.execute(); + + auto reorder_inst = network_opt.get_primitive("reorder"); + auto reorder_impl = reorder_inst->get_impl(); + ASSERT_TRUE(reorder_impl != nullptr); + ASSERT_NE(reorder_impl->get_kernel_name().find("reorder_data_fsv"), std::string::npos); + + if (output_data_type == data_types::i8) + compare_result(outputs_ref, outputs_opt); + else if (output_data_type == data_types::u8) + compare_result(outputs_ref, outputs_opt); + else if (output_data_type == data_types::i32) + compare_result(outputs_ref, outputs_opt); + else if (output_data_type == data_types::i64) + compare_result(outputs_ref, outputs_opt); + else if (output_data_type == data_types::f16) + compare_result(outputs_ref, outputs_opt); + else if (output_data_type == data_types::f32) + compare_result(outputs_ref, outputs_opt); +} + +static void compare_fsv_reorder_with_ref_input_padding(const data_types input_data_type, + const data_types output_data_type, + cldnn::format input_format, + cldnn::format output_format, + int32_t b_in, + int32_t f_in, + int32_t x_in, + int32_t y_in, + int32_t z_in, + int32_t w_in, + const padding& input_padding) { + compare_fsv_reorder_with_ref_padding(input_data_type, + output_data_type, + input_format, + output_format, + b_in, + f_in, + x_in, + y_in, + z_in, + w_in, + input_padding, + padding()); +} + +TEST(reorder_gpu_optimization, dynamic_bfyx_to_blocked_f32) { + // bfzyx -> b_fs_zyx_fsv32 (nnUNet pattern) + compare_bfyx2blocked_with_ref_dynamic("reorder_data_bfyx_to_blocked_format", data_types::f32, data_types::f32, format::bfzyx, format::b_fs_zyx_fsv32, 1, 320, 8, 8, 8, 0); + compare_bfyx2blocked_with_ref_dynamic("reorder_data_bfyx_to_blocked_format", data_types::f32, data_types::f32, format::bfzyx, format::b_fs_zyx_fsv32, 2, 64, 16, 16, 4, 0); + // Remainder shapes: F%tile!=0, X%tile!=0 + compare_bfyx2blocked_with_ref_dynamic("reorder_data_bfyx_to_blocked_format", data_types::f32, data_types::f32, format::bfzyx, format::b_fs_zyx_fsv32, 1, 37, 13, 8, 4, 0); + compare_bfyx2blocked_with_ref_dynamic("reorder_data_bfyx_to_blocked_format", data_types::f32, data_types::f32, format::bfyx, format::b_fs_yx_fsv32, 2, 37, 13, 4, 0, 0); + // Multiple blocked output layouts + compare_bfyx2blocked_with_ref_dynamic("reorder_data_bfyx_to_blocked_format", data_types::f32, data_types::f32, format::bfyx, format::b_fs_yx_fsv16, 2, 48, 16, 4, 0, 0); + compare_bfyx2blocked_with_ref_dynamic("reorder_data_bfyx_to_blocked_format", data_types::f32, data_types::f32, format::bfzyx, format::b_fs_zyx_fsv16, 1, 64, 8, 8, 4, 0); + // b_fs_yx_fsv4 + compare_bfyx2blocked_with_ref_dynamic("reorder_data_bfyx_to_blocked_format", data_types::f32, data_types::f32, format::bfyx, format::b_fs_yx_fsv4, 4, 32, 16, 4, 0, 0); +} + +TEST(reorder_gpu_optimization, dynamic_bfyx_to_blocked_f16) { + compare_bfyx2blocked_with_ref_dynamic("reorder_data_bfyx_to_blocked_format", data_types::f16, data_types::f16, format::bfzyx, format::b_fs_zyx_fsv32, 1, 320, 8, 8, 8, 0); + compare_bfyx2blocked_with_ref_dynamic("reorder_data_bfyx_to_blocked_format", data_types::f16, data_types::f16, format::bfyx, format::b_fs_yx_fsv16, 2, 37, 13, 4, 0, 0); +} + +TEST(reorder_gpu_optimization, dynamic_bfyx_to_blocked_u8) { + // nnUNet actual pattern: u8:bfzyx -> u8:b_fs_zyx_fsv32 + compare_bfyx2blocked_with_ref_dynamic("reorder_data_bfyx_to_blocked_format", data_types::u8, data_types::u8, format::bfzyx, format::b_fs_zyx_fsv32, 1, 320, 8, 8, 8, 0); + // u8 with remainder + compare_bfyx2blocked_with_ref_dynamic("reorder_data_bfyx_to_blocked_format", data_types::u8, data_types::u8, format::bfyx, format::b_fs_yx_fsv16, 2, 37, 13, 4, 0, 0); +} + +TEST(reorder_gpu_optimization, dynamic_bfyx_to_double_blocked) { + // bs_fs_yx_bsv16_fsv16 (double-blocked) + compare_bfyx2blocked_with_ref_dynamic("reorder_data_bfyx_to_blocked_format", data_types::f32, data_types::f32, format::bfyx, format::bs_fs_yx_bsv16_fsv16, 32, 48, 8, 4, 0, 0); + // bs_fs_yx_bsv16_fsv16 with remainder + compare_bfyx2blocked_with_ref_dynamic("reorder_data_bfyx_to_blocked_format", data_types::f32, data_types::f32, format::bfyx, format::bs_fs_yx_bsv16_fsv16, 34, 37, 13, 4, 0, 0); + // bs_fs_zyx_bsv16_fsv32 (5D double-blocked) + compare_bfyx2blocked_with_ref_dynamic("reorder_data_bfyx_to_blocked_format", data_types::f32, data_types::f32, format::bfzyx, format::bs_fs_zyx_bsv16_fsv32, 32, 48, 8, 4, 4, 0); +} + +TEST(reorder_gpu_optimization, dynamic_fsv_reorder_f32) { + // fsv16 -> fsv32 (nnUNet bottleneck pattern: b_fs_zyx_fsv16 -> b_fs_zyx_fsv32) + compare_bfyx2blocked_with_ref_dynamic("reorder_data_fsv", data_types::f32, data_types::f32, format::b_fs_zyx_fsv16, format::b_fs_zyx_fsv32, 1, 32, 8, 8, 8, 0); + compare_bfyx2blocked_with_ref_dynamic("reorder_data_fsv", data_types::f32, data_types::f32, format::b_fs_zyx_fsv16, format::b_fs_zyx_fsv32, 2, 64, 16, 16, 4, 0); + // Remainder: F not aligned to output fsv + compare_bfyx2blocked_with_ref_dynamic("reorder_data_fsv", data_types::f32, data_types::f32, format::b_fs_zyx_fsv16, format::b_fs_zyx_fsv32, 1, 48, 8, 8, 4, 0); + // 4D: fsv16 -> fsv32 + compare_bfyx2blocked_with_ref_dynamic("reorder_data_fsv", data_types::f32, data_types::f32, format::b_fs_yx_fsv16, format::b_fs_yx_fsv32, 2, 64, 16, 8, 0, 0); + // fsv32 -> fsv16 (reverse direction) + compare_bfyx2blocked_with_ref_dynamic("reorder_data_fsv", data_types::f32, data_types::f32, format::b_fs_zyx_fsv32, format::b_fs_zyx_fsv16, 1, 64, 8, 8, 4, 0); + compare_bfyx2blocked_with_ref_dynamic("reorder_data_fsv", data_types::f32, data_types::f32, format::b_fs_yx_fsv32, format::b_fs_yx_fsv16, 2, 48, 16, 8, 0, 0); +} + +TEST(reorder_gpu_optimization, dynamic_fsv_reorder_u8) { + // u8: nnUNet actual bottleneck pattern + compare_bfyx2blocked_with_ref_dynamic("reorder_data_fsv", data_types::u8, data_types::u8, format::b_fs_zyx_fsv16, format::b_fs_zyx_fsv32, 1, 32, 8, 8, 8, 0); + // u8 with feature remainder + compare_bfyx2blocked_with_ref_dynamic("reorder_data_fsv", data_types::u8, data_types::u8, format::b_fs_zyx_fsv16, format::b_fs_zyx_fsv32, 1, 48, 8, 8, 4, 0); + // u8 reverse + compare_bfyx2blocked_with_ref_dynamic("reorder_data_fsv", data_types::u8, data_types::u8, format::b_fs_zyx_fsv32, format::b_fs_zyx_fsv16, 1, 64, 8, 8, 4, 0); +} + +TEST(reorder_gpu_optimization, dynamic_fsv_reorder_f16) { + compare_bfyx2blocked_with_ref_dynamic("reorder_data_fsv", data_types::f16, data_types::f16, format::b_fs_zyx_fsv16, format::b_fs_zyx_fsv32, 1, 32, 8, 8, 8, 0); + compare_bfyx2blocked_with_ref_dynamic("reorder_data_fsv", data_types::f16, data_types::f16, format::b_fs_yx_fsv32, format::b_fs_yx_fsv16, 2, 64, 16, 8, 0, 0); +} + +TEST(reorder_gpu_optimization, dynamic_fsv_reorder_cross_type) { + // Cross-type: u8 -> f32 with format change + compare_bfyx2blocked_with_ref_dynamic("reorder_data_fsv", data_types::u8, data_types::f32, format::b_fs_zyx_fsv16, format::b_fs_zyx_fsv32, 1, 32, 8, 8, 4, 0); + // Cross-type: f16 -> f32 with format change + compare_bfyx2blocked_with_ref_dynamic("reorder_data_fsv", data_types::f16, data_types::f32, format::b_fs_yx_fsv32, format::b_fs_yx_fsv16, 2, 48, 16, 8, 0, 0); +} + +TEST(reorder_gpu_optimization, fsv_reorder_output_padding_feature_axis) { + compare_fsv_reorder_with_ref_output_padding(data_types::f32, data_types::f32, + format::b_fs_yx_fsv16, format::b_fs_yx_fsv32, + 2, 48, 13, 5, 0, 0, + padding({0, 32, 0, 0}, {0, 16, 0, 0})); + compare_fsv_reorder_with_ref_output_padding(data_types::f32, data_types::f32, + format::b_fs_yx_fsv32, format::b_fs_yx_fsv16, + 2, 64, 11, 7, 0, 0, + padding({0, 16, 0, 0}, {0, 8, 0, 0})); + compare_fsv_reorder_with_ref_output_padding(data_types::f32, data_types::f32, + format::b_fs_zyx_fsv16, format::b_fs_zyx_fsv32, + 1, 48, 7, 5, 3, 0, + padding({0, 32, 0, 0, 0}, {0, 16, 0, 0, 0})); + compare_fsv_reorder_with_ref_output_padding(data_types::f32, data_types::f32, + format::b_fs_zyx_fsv32, format::b_fs_zyx_fsv16, + 1, 64, 9, 4, 2, 0, + padding({0, 16, 0, 0, 0}, {0, 8, 0, 0, 0})); +} + +TEST(reorder_gpu_optimization, fsv_reorder_output_padding_spatial_axis) { + compare_fsv_reorder_with_ref_output_padding(data_types::f32, data_types::f32, + format::b_fs_yx_fsv16, format::b_fs_yx_fsv32, + 2, 48, 13, 5, 0, 0, + padding({0, 0, 2, 3}, {0, 0, 1, 4})); + compare_fsv_reorder_with_ref_output_padding(data_types::f32, data_types::f32, + format::b_fs_yx_fsv32, format::b_fs_yx_fsv16, + 2, 64, 11, 7, 0, 0, + padding({0, 0, 1, 2}, {0, 0, 3, 1})); + compare_fsv_reorder_with_ref_output_padding(data_types::f32, data_types::f32, + format::b_fs_zyx_fsv16, format::b_fs_zyx_fsv32, + 1, 48, 7, 5, 3, 0, + padding({0, 0, 2, 2, 2}, {0, 0, 1, 1, 1})); + compare_fsv_reorder_with_ref_output_padding(data_types::f32, data_types::f32, + format::b_fs_zyx_fsv32, format::b_fs_zyx_fsv16, + 1, 64, 9, 4, 2, 0, + padding({0, 0, 1, 1, 1}, {0, 0, 2, 2, 2})); +} + +TEST(reorder_gpu_optimization, fsv_reorder_output_padding_batch_axis) { + compare_fsv_reorder_with_ref_output_padding(data_types::f32, data_types::f32, + format::b_fs_yx_fsv16, format::b_fs_yx_fsv32, + 2, 48, 13, 5, 0, 0, + padding({1, 0, 0, 0}, {2, 0, 0, 0})); + compare_fsv_reorder_with_ref_output_padding(data_types::f32, data_types::f32, + format::b_fs_yx_fsv32, format::b_fs_yx_fsv16, + 3, 64, 11, 7, 0, 0, + padding({2, 0, 0, 0}, {1, 0, 0, 0})); + compare_fsv_reorder_with_ref_output_padding(data_types::f32, data_types::f32, + format::b_fs_zyx_fsv16, format::b_fs_zyx_fsv32, + 2, 48, 7, 5, 3, 0, + padding({1, 0, 0, 0, 0}, {2, 0, 0, 0, 0})); + compare_fsv_reorder_with_ref_output_padding(data_types::f32, data_types::f32, + format::b_fs_zyx_fsv32, format::b_fs_zyx_fsv16, + 3, 64, 9, 4, 2, 0, + padding({2, 0, 0, 0, 0}, {1, 0, 0, 0, 0})); +} + +TEST(reorder_gpu_optimization, fsv_reorder_input_padding_feature_axis) { + compare_fsv_reorder_with_ref_input_padding(data_types::f32, data_types::f32, + format::b_fs_yx_fsv16, format::b_fs_yx_fsv32, + 2, 48, 13, 5, 0, 0, + padding({0, 32, 0, 0}, {0, 16, 0, 0})); + compare_fsv_reorder_with_ref_input_padding(data_types::f32, data_types::f32, + format::b_fs_yx_fsv32, format::b_fs_yx_fsv16, + 2, 64, 11, 7, 0, 0, + padding({0, 16, 0, 0}, {0, 8, 0, 0})); + compare_fsv_reorder_with_ref_input_padding(data_types::f32, data_types::f32, + format::b_fs_zyx_fsv16, format::b_fs_zyx_fsv32, + 1, 48, 7, 5, 3, 0, + padding({0, 32, 0, 0, 0}, {0, 16, 0, 0, 0})); + compare_fsv_reorder_with_ref_input_padding(data_types::f32, data_types::f32, + format::b_fs_zyx_fsv32, format::b_fs_zyx_fsv16, + 1, 64, 9, 4, 2, 0, + padding({0, 16, 0, 0, 0}, {0, 8, 0, 0, 0})); +} + +TEST(reorder_gpu_optimization, fsv_reorder_input_padding_spatial_axis) { + compare_fsv_reorder_with_ref_input_padding(data_types::f32, data_types::f32, + format::b_fs_yx_fsv16, format::b_fs_yx_fsv32, + 2, 48, 13, 5, 0, 0, + padding({0, 0, 2, 3}, {0, 0, 1, 4})); + compare_fsv_reorder_with_ref_input_padding(data_types::f32, data_types::f32, + format::b_fs_yx_fsv32, format::b_fs_yx_fsv16, + 2, 64, 11, 7, 0, 0, + padding({0, 0, 1, 2}, {0, 0, 3, 1})); + compare_fsv_reorder_with_ref_input_padding(data_types::f32, data_types::f32, + format::b_fs_zyx_fsv16, format::b_fs_zyx_fsv32, + 1, 48, 7, 5, 3, 0, + padding({0, 0, 2, 2, 2}, {0, 0, 1, 1, 1})); + compare_fsv_reorder_with_ref_input_padding(data_types::f32, data_types::f32, + format::b_fs_zyx_fsv32, format::b_fs_zyx_fsv16, + 1, 64, 9, 4, 2, 0, + padding({0, 0, 1, 1, 1}, {0, 0, 2, 2, 2})); +} + +TEST(reorder_gpu_optimization, fsv_reorder_input_padding_batch_axis) { + compare_fsv_reorder_with_ref_input_padding(data_types::f32, data_types::f32, + format::b_fs_yx_fsv16, format::b_fs_yx_fsv32, + 2, 48, 13, 5, 0, 0, + padding({1, 0, 0, 0}, {2, 0, 0, 0})); + compare_fsv_reorder_with_ref_input_padding(data_types::f32, data_types::f32, + format::b_fs_yx_fsv32, format::b_fs_yx_fsv16, + 3, 64, 11, 7, 0, 0, + padding({2, 0, 0, 0}, {1, 0, 0, 0})); + compare_fsv_reorder_with_ref_input_padding(data_types::f32, data_types::f32, + format::b_fs_zyx_fsv16, format::b_fs_zyx_fsv32, + 2, 48, 7, 5, 3, 0, + padding({1, 0, 0, 0, 0}, {2, 0, 0, 0, 0})); + compare_fsv_reorder_with_ref_input_padding(data_types::f32, data_types::f32, + format::b_fs_zyx_fsv32, format::b_fs_zyx_fsv16, + 3, 64, 9, 4, 2, 0, + padding({2, 0, 0, 0, 0}, {1, 0, 0, 0, 0})); +} + +TEST(reorder_gpu_optimization, fsv_reorder_input_output_padding_feature_axis) { + compare_fsv_reorder_with_ref_padding(data_types::f32, data_types::f32, + format::b_fs_yx_fsv16, format::b_fs_yx_fsv32, + 2, 48, 13, 5, 0, 0, + padding({0, 32, 0, 0}, {0, 16, 0, 0}), + padding({0, 16, 0, 0}, {0, 32, 0, 0})); + compare_fsv_reorder_with_ref_padding(data_types::f32, data_types::f32, + format::b_fs_zyx_fsv32, format::b_fs_zyx_fsv16, + 2, 64, 9, 4, 2, 0, + padding({0, 16, 0, 0, 0}, {0, 8, 0, 0, 0}), + padding({0, 32, 0, 0, 0}, {0, 16, 0, 0, 0})); +} + +TEST(reorder_gpu_optimization, fsv_reorder_input_output_padding_spatial_axis) { + compare_fsv_reorder_with_ref_padding(data_types::f32, data_types::f32, + format::b_fs_yx_fsv16, format::b_fs_yx_fsv32, + 2, 48, 13, 5, 0, 0, + padding({0, 0, 2, 3}, {0, 0, 1, 4}), + padding({0, 0, 1, 2}, {0, 0, 3, 1})); + compare_fsv_reorder_with_ref_padding(data_types::f32, data_types::f32, + format::b_fs_zyx_fsv32, format::b_fs_zyx_fsv16, + 1, 64, 9, 4, 2, 0, + padding({0, 0, 1, 1, 1}, {0, 0, 2, 2, 2}), + padding({0, 0, 2, 2, 2}, {0, 0, 1, 1, 1})); +} + +TEST(reorder_gpu_optimization, fsv_reorder_input_output_padding_batch_axis) { + compare_fsv_reorder_with_ref_padding(data_types::f32, data_types::f32, + format::b_fs_yx_fsv16, format::b_fs_yx_fsv32, + 2, 48, 13, 5, 0, 0, + padding({1, 0, 0, 0}, {2, 0, 0, 0}), + padding({2, 0, 0, 0}, {1, 0, 0, 0})); + compare_fsv_reorder_with_ref_padding(data_types::f32, data_types::f32, + format::b_fs_zyx_fsv32, format::b_fs_zyx_fsv16, + 3, 64, 9, 4, 2, 0, + padding({2, 0, 0, 0, 0}, {1, 0, 0, 0, 0}), + padding({1, 0, 0, 0, 0}, {2, 0, 0, 0, 0})); +} + TEST(reorder_gpu_optimization, bfyx_to_fsv16_without_f_remainder) { auto& engine = get_test_engine(); const int32_t b_in = 1; @@ -666,9 +1215,7 @@ TEST(reorder_gpu_f16, basic_subtract_f32_output_f32) { if (!engine.get_device_info().supports_fp16) { - std::cout << "[ SKIPPED ] The test is skipped (cl_khr_fp16 is not supported)." << std::endl; - ASSERT_EQ(1, 1); - return; + GTEST_SKIP() << "The test is skipped (cl_khr_fp16 is not supported)."; } auto input = engine.allocate_memory({ data_types::f16, format::yxfb, { 2, 2, 2, 2 } }); @@ -758,11 +1305,8 @@ TEST(reorder_gpu_f16, basic_subtract_value) { // auto& engine = get_test_engine(); - if (!engine.get_device_info().supports_fp16) - { - std::cout << "[ SKIPPED ] The test is skipped (cl_khr_fp16 is not supported)." << std::endl; - ASSERT_EQ(1, 1); - return; + if (!engine.get_device_info().supports_fp16) { + GTEST_SKIP() << "The test is skipped (cl_khr_fp16 is not supported)."; } auto input = engine.allocate_memory({ data_types::f16, format::yxfb, { 2, 2, 2, 2 } }); @@ -830,9 +1374,7 @@ TEST(reorder_gpu, basic_convert_f16_f32_f16) { if (!engine.get_device_info().supports_fp16) { - std::cout << "[ SKIPPED ] The test is skipped (cl_khr_fp16 is not supported)." << std::endl; - ASSERT_EQ(1, 1); - return; + GTEST_SKIP() << "The test is skipped (cl_khr_fp16 is not supported)."; } std::vector expected_values; @@ -872,7 +1414,7 @@ TEST(reorder_gpu, basic_convert_f16_f32_f16) { ASSERT_TRUE(outputs.find("reorder_f32_f16") != outputs.end()); auto interm = outputs.at("reorder_f16_f32").get_memory(); - cldnn::mem_lock interm_ptr(interm, get_test_stream()); + cldnn::mem_lock interm_ptr(interm, get_test_stream()); // Sample positive. ASSERT_TRUE(are_equal(interm_ptr[0x3400], 0.25f)); @@ -893,7 +1435,7 @@ TEST(reorder_gpu, basic_convert_f16_f32_f16) { ASSERT_TRUE(std::isnan(interm_ptr[0xF803])); auto output = outputs.at("reorder_f32_f16").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (int i = 0; i < 0xF802; ++i) // NOTE: do not test for possibly ambiguous values of floating point (-0, NaNs). { ASSERT_TRUE(are_equal(static_cast(expected_values[i]), static_cast(output_ptr[i]))); @@ -943,7 +1485,7 @@ TEST(reorder_gpu, basic_convert_int8) { auto outputs = network.execute(); auto interm = outputs.at("reorder2").get_memory(); - cldnn::mem_lock interm_ptr(interm, get_test_stream()); + cldnn::mem_lock interm_ptr(interm, get_test_stream()); unsigned int cntr = 0; for (const auto& exp : final_results) { @@ -993,7 +1535,7 @@ TEST(reorder_gpu, basic_convert_uint8) { auto outputs = network.execute(); auto interm = outputs.at("reorder2").get_memory(); - cldnn::mem_lock interm_ptr(interm, get_test_stream()); + cldnn::mem_lock interm_ptr(interm, get_test_stream()); unsigned int cntr = 0; for (const auto& exp : final_results) { EXPECT_EQ(exp, interm_ptr[cntr++]); @@ -1015,9 +1557,7 @@ TEST(reorder_gpu, basic_convert_uint8rgbabyxf_to_fp32_bfyx) { if (!engine.get_device_info().supports_fp16) { - std::cout << "[ SKIPPED ] The test is skipped (cl_khr_fp16 is not supported)." << std::endl; - ASSERT_EQ(1, 1); - return; + GTEST_SKIP() << "The test is skipped (cl_khr_fp16 is not supported)."; } std::initializer_list input_i8 = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, @@ -1073,7 +1613,7 @@ TEST(reorder_gpu, basic_convert_uint8rgbabyxf_to_fp32_bfyx) { ASSERT_TRUE(outputs.find("crop") != outputs.end()); auto interm = outputs.at("reorder_input").get_memory(); - cldnn::mem_lock interm_ptr(interm, get_test_stream()); + cldnn::mem_lock interm_ptr(interm, get_test_stream()); auto interm_size = outputs.at("reorder_input").get_memory()->count(); ASSERT_EQ(interm_size,(size_t) (1*feature_size*kernel_size*kernel_size)); @@ -1096,7 +1636,7 @@ TEST(reorder_gpu, basic_convert_uint8rgbabyxf_to_fp32_bfyx) { } auto output = outputs.at("crop").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); auto output_size = output->count(); ASSERT_EQ(output_size,(size_t) (1 * (feature_size-1)*kernel_size*kernel_size)); @@ -1390,7 +1930,7 @@ TEST(reorder_gpu_f32, dynamic_bfyx_to_bfyx_dynamic_padding_x) { 1.f, 2.f, 3.f, 4.f, 5.f, 6.f, 7.f, 8.f }; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (int i = 0; i < 8; i++) { ASSERT_NEAR(answer[i], output_ptr[i], 1e-2f); } @@ -1451,7 +1991,7 @@ TEST(reorder_gpu_f32, dynamic_bfyx_to_bfyx_dynamic_padding_f) { 11.f, 22.f, 33.f, 44.f, 55.f, 66.f }; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (int i = 0; i < 12; i++) { ASSERT_NEAR(answer[i], output_ptr[i], 1e-2f); } @@ -1597,6 +2137,57 @@ TEST(reorder_gpu_f32, dynamic_bfyx_to_fsv16) { } } +// Test: dynamic reorder from b_fs_yx_fsv16 (non-simple) to bfyx (simple) +// should select OCL dynamic impl even when allow_new_shape_infer is enabled +// This verifies the fix where OCL dynamic reorder is not incorrectly blocked +TEST(reorder_gpu_f32, dynamic_fsv16_to_bfyx_executes_without_error) { + auto& engine = get_test_engine(); + + ov::Shape in_shape{ 1, 16, 4, 4 }; + size_t element_count = 1; + for (const auto dim : in_shape) + element_count *= dim; + + // Dynamic input in b_fs_yx_fsv16 format + layout in_layout{ov::PartialShape::dynamic(in_shape.size()), data_types::f32, format::b_fs_yx_fsv16}; + + // Prepare properly formatted fsv16 memory via helper network + auto input_bfyx = engine.allocate_memory({ov::PartialShape(in_shape), data_types::f32, format::bfyx}); + std::vector input_data(element_count); + std::iota(input_data.begin(), input_data.end(), 1.f); + set_values(input_bfyx, input_data); + + topology prep_topology( + input_layout("prep_in", layout{ov::PartialShape(in_shape), data_types::f32, format::bfyx}), + reorder("prep_out", input_info("prep_in"), format::b_fs_yx_fsv16, data_types::f32)); + network prep_net(engine, prep_topology, get_test_default_config(engine)); + prep_net.set_input_data("prep_in", input_bfyx); + auto prep_outputs = prep_net.execute(); + auto input_fsv16 = prep_outputs.at("prep_out").get_memory(); + + // Main topology: dynamic fsv16 -> reorder to bfyx + topology topology( + input_layout("input", in_layout), + reorder("output", input_info("input"), format::bfyx, data_types::f32)); + + ExecutionConfig config = get_test_default_config(engine); + config.set_property(ov::intel_gpu::allow_new_shape_infer(true)); + network network(engine, topology, config); + network.set_input_data("input", input_fsv16); + + // Must not throw + auto outputs = network.execute(); + ASSERT_EQ(outputs.size(), size_t(1)); + + auto output = outputs.at("output").get_memory(); + ASSERT_EQ(output->get_layout().format, format::bfyx); + + cldnn::mem_lock output_ptr(output, get_test_stream()); + for (size_t i = 0; i < input_data.size(); i++) { + ASSERT_NEAR(input_data[i], output_ptr[i], 1e-5f); + } +} + TEST(reorder_gpu_f32, basic_yxfb_to_bfzyx) { // Input : yxfb:2x2x2x2 @@ -1951,7 +2542,7 @@ TEST(reorder_gpu_opt, mean_mul) auto outputs = net.execute(); auto output = outputs.begin()->second.get_memory(); - cldnn::mem_lock ptr(output, get_test_stream()); + cldnn::mem_lock ptr(output, get_test_stream()); float* a_ptr = answers; for (auto& val : ptr) ASSERT_FLOAT_EQ(*(a_ptr++), val); @@ -1986,7 +2577,7 @@ TEST(reorder_gpu_opt, mean_div) auto outputs = net.execute(); auto output = outputs.begin()->second.get_memory(); - cldnn::mem_lock ptr(output, get_test_stream()); + cldnn::mem_lock ptr(output, get_test_stream()); float* a_ptr = answers; for (auto& val : ptr) ASSERT_FLOAT_EQ(*(a_ptr++), val); @@ -2017,7 +2608,7 @@ TEST(reorder_gpu_opt, mean_mul_val) auto outputs = net.execute(); auto output = outputs.begin()->second.get_memory(); - cldnn::mem_lock ptr(output, get_test_stream()); + cldnn::mem_lock ptr(output, get_test_stream()); float* a_ptr = answers; for (auto& val : ptr) ASSERT_FLOAT_EQ(*(a_ptr++), val); @@ -2047,7 +2638,7 @@ TEST(reorder_gpu_opt, mean_mul_val_float_to_int) auto outputs = net.execute(); auto output = outputs.begin()->second.get_memory(); - cldnn::mem_lock ptr(output, get_test_stream()); + cldnn::mem_lock ptr(output, get_test_stream()); char* a_ptr = answers; for (auto& val : ptr) ASSERT_EQ(*(a_ptr++), val); @@ -2135,7 +2726,7 @@ TEST(reorder_weights_gpu_i32, reorder_weights) }; auto output = outputs.begin()->second.get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); ASSERT_EQ(output_ptr.size(), ref_output.size()); for (size_t i = 0; i < ref_output.size(); ++i) { @@ -2277,7 +2868,7 @@ TEST(reorder_weights_gpu_i32, reorder_weights_opt) }; auto output = outputs.begin()->second.get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); ASSERT_EQ(output_ptr.size(), ref_output.size()); for (size_t i = 0; i < ref_output.size(); ++i) { @@ -2690,7 +3281,7 @@ TEST(reorder_gpu_f32, b_fs_yx_fsv16_to_bfyx_opt_allowed) 16.f, 17.f, 18.f, 19.f, 20.f, 21.f, 22.f, 23.f, 24.f, 25.f, 26.f, 27.f, }; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); ASSERT_EQ(output_ptr.size(), 24); for (size_t i = 0; i < output_ptr.size(); i++) { @@ -2734,7 +3325,7 @@ TEST(reorder_gpu_f32, b_fs_yx_fsv16_to_bfyx_opt_not_allowed) float answers[1] = { 28.f }; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (int i = 0; i < 1; i++) { ASSERT_FLOAT_EQ(answers[i], output_ptr[i]) << "i=" << i; @@ -2793,7 +3384,7 @@ TEST(reorder_gpu_f32, b_fs_yx_fsv16_to_bfyx_opt_padded) 16.f, 17.f, 18.f, 19.f, }; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); ASSERT_EQ(output_ptr.size(), 8); for (size_t i = 0; i < output_ptr.size(); i++) { ASSERT_FLOAT_EQ(answers[i], output_ptr[i]) << "i=" << i; @@ -2818,7 +3409,7 @@ TEST(reorder_gpu, any_format) { auto outputs = net.execute(); auto out_mem = outputs.at("out").get_memory(); - cldnn::mem_lock output(out_mem, get_test_stream()); + cldnn::mem_lock output(out_mem, get_test_stream()); for (size_t i = 0; i < data.size(); ++i) { ASSERT_EQ(output[i], data[i]) << "i = " << i; @@ -2846,7 +3437,7 @@ void reorder_cpu_any_format(bool disable_usm) { auto outputs = net.execute(); auto out_mem = outputs.at("reorder").get_memory(); - cldnn::mem_lock output(out_mem, get_test_stream()); + cldnn::mem_lock output(out_mem, get_test_stream()); for (size_t i = 0; i < data.size(); ++i) { ASSERT_EQ(output[i], data[i]) << "i = " << i; @@ -3478,7 +4069,7 @@ TEST(reorder_onednn_gpu, basic_convert_int8) { auto outputs = network.execute(); auto interm = outputs.at("reorder2").get_memory(); - cldnn::mem_lock interm_ptr(interm, get_test_stream()); + cldnn::mem_lock interm_ptr(interm, get_test_stream()); unsigned int cntr = 0; for (const auto& exp : final_results) { @@ -3697,7 +4288,7 @@ TEST(reorder_gpu_fp32, test_needs_completion_events) { auto outputs = network.execute(); auto output = outputs.at("reorder2").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (size_t i = 0; i < expected_results.size(); ++i) { ASSERT_EQ(expected_results[i], output_ptr[i]); @@ -3872,7 +4463,7 @@ static void run_reorder_int4(const ov::Shape in_shape) { auto output = outputs.begin()->second.get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); ASSERT_EQ(expected_data.size(), output_ptr.size()); for (size_t idx = 0; idx < output_ptr.size(); idx++) @@ -3906,7 +4497,7 @@ void run_reorder_test_i4(data_types input_type, data_types output_type, const st auto outputs = network.execute(); auto output_mem = outputs.at("reorder").get_memory(); - cldnn::mem_lock output_ptr(output_mem, get_test_stream()); + cldnn::mem_lock output_ptr(output_mem, get_test_stream()); for (size_t i = 0; i < expected.size(); ++i) { ASSERT_EQ(expected[i], output_ptr[i]); diff --git a/src/plugins/intel_gpu/tests/unit/test_cases/reorg_yolo_gpu_test.cpp b/src/plugins/intel_gpu/tests/unit/test_cases/reorg_yolo_gpu_test.cpp index 5b033b8e4093..2f4f3bc15970 100644 --- a/src/plugins/intel_gpu/tests/unit/test_cases/reorg_yolo_gpu_test.cpp +++ b/src/plugins/intel_gpu/tests/unit/test_cases/reorg_yolo_gpu_test.cpp @@ -320,7 +320,7 @@ struct reorg_yolo_test const auto result = network->execute(); auto out_mem = result.at("reorg_yolo_reordered").get_memory(); - cldnn::mem_lock out_ptr(out_mem, get_test_stream()); + cldnn::mem_lock out_ptr(out_mem, get_test_stream()); ASSERT_EQ(params.expected.size(), out_ptr.size()); for (size_t i = 0; i < params.expected.size(); ++i) { diff --git a/src/plugins/intel_gpu/tests/unit/test_cases/resample_gpu_test.cpp b/src/plugins/intel_gpu/tests/unit/test_cases/resample_gpu_test.cpp index 8fb601edbaac..af4eeaf64686 100644 --- a/src/plugins/intel_gpu/tests/unit/test_cases/resample_gpu_test.cpp +++ b/src/plugins/intel_gpu/tests/unit/test_cases/resample_gpu_test.cpp @@ -55,7 +55,7 @@ void test_basic_in2x3x2x2_nearest(bool is_caching_test) { auto outputs = net->execute(); auto output = outputs.at("upsampling").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); T answers[96] = { 1.f, 1.f, 2.f, 2.f, -10.f, -10.f, @@ -124,7 +124,7 @@ TEST(resample_gpu, basic_in2x3x2x2_bilinear) { auto outputs = net.execute(); auto output = outputs.at("upsampling").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); ASSERT_EQ(output->get_layout().get_linear_size(), (size_t) 16); @@ -175,7 +175,7 @@ TEST(resample_gpu, nearest_asymmetric) { auto outputs = net.execute(); auto output = outputs.at("upsampling").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); ASSERT_EQ(output->get_layout().get_linear_size(), (size_t)20); @@ -226,7 +226,7 @@ TEST(resample_gpu, nearest_asymmetric_i8) { auto outputs = net.execute(); auto output = outputs.at("upsampling").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); ASSERT_EQ(output->get_layout().get_linear_size(), (size_t)20); @@ -277,7 +277,7 @@ TEST(resample_gpu, bilinear_asymmetric) { auto outputs = net.execute(); auto output = outputs.at("upsampling").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); ASSERT_EQ(output->get_layout().get_linear_size(), (size_t)24); @@ -610,8 +610,8 @@ struct caffe_resample_random_test : testing::TestWithParam ref_ptr(out_ref, get_test_stream()); - cldnn::mem_lock opt_ptr(out_opt, get_test_stream()); + cldnn::mem_lock ref_ptr(out_ref, get_test_stream()); + cldnn::mem_lock opt_ptr(out_opt, get_test_stream()); for (size_t bi = 0; bi < b; ++bi) { for (size_t fi = 0; fi < f; ++fi) { for (size_t yi = 0; yi < y; ++yi) { @@ -779,7 +779,7 @@ TEST(resample_gpu, interpolate_in2x2x3x2_nearest1) { auto outputs = net.execute(); auto output = outputs.at("interpolate").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); float answers[96] = { 0.f, 1.f, 1.f, 1.f, @@ -869,7 +869,7 @@ TEST(resample_gpu, interpolate_in2x2x3x2_nearest2) { auto outputs = net.execute(); auto output = outputs.at("interpolate").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); float answers[96] = { 0.f, 0.f, 1.f, 1.f, @@ -959,7 +959,7 @@ TEST(resample_gpu, interpolate_in2x2x3x2_nearest3) { auto outputs = net.execute(); auto output = outputs.at("interpolate").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); float answers[96] = { 0.f, 0.f, 1.f, 1.f, @@ -1049,7 +1049,7 @@ TEST(resample_gpu, interpolate_in2x2x3x2_nearest4) { auto outputs = net.execute(); auto output = outputs.at("interpolate").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); float answers[96] = { 0.f, 0.f, 0.f, 1.f, @@ -1139,7 +1139,7 @@ TEST(resample_gpu, interpolate_in2x2x3x2_nearest5) { auto outputs = net.execute(); auto output = outputs.at("interpolate").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); float answers[96] = { 0.f, 0.f, 0.f, 1.f, @@ -1231,7 +1231,7 @@ TEST(resample_gpu, interpolate_in2x2x3x2_coord_transform_mode1) { auto outputs = net.execute(); auto output = outputs.at("interpolate").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector answers = { 0.f, 0.f, 1.f, @@ -1301,7 +1301,7 @@ TEST(resample_gpu, interpolate_in2x2x3x2_coord_transform_mode2) { auto outputs = net.execute(); auto output = outputs.at("interpolate").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector answers = { 0.f, 0.f, 1.f, @@ -1365,7 +1365,7 @@ TEST(resample_gpu, interpolate_in2x2x3x2_coord_transform_mode3) { auto outputs = net.execute(); auto output = outputs.at("interpolate").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector answers = { 0.f, 1.f, 1.f, @@ -1435,7 +1435,7 @@ TEST(resample_gpu, interpolate_in2x2x3x2_coord_transform_mode4) { auto outputs = net.execute(); auto output = outputs.at("interpolate").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector answers = { 2.f, 3.f, 3.f, @@ -1505,7 +1505,7 @@ TEST(resample_gpu, interpolate_in2x2x3x2_coord_transform_mode5) { auto outputs = net.execute(); auto output = outputs.at("interpolate").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector answers = { 0.f, 0.f, 1.f, @@ -1573,7 +1573,7 @@ TEST(resample_gpu, interpolate_in2x2x3x2_cubic) { auto outputs = net.execute(); auto output = outputs.at("interpolate").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector answers = { 0.29600694f, 0.8828125f, 1.46961806f, @@ -1634,7 +1634,7 @@ TEST(resample_gpu, interpolate_in2x2x3x2_cubic2) { auto outputs = net.execute(); auto output = outputs.at("interpolate").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector answers = { 5.34722222f, 3.f, 0.65277778f, @@ -1694,7 +1694,7 @@ TEST(resample_gpu, interpolate_in2x2x3x2_linear) { auto outputs = net.execute(); auto output = outputs.at("interpolate").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector answers = { 0.5f, 1.f, 1.5f, @@ -1863,7 +1863,7 @@ TYPED_TEST(onnx_5d_format, interpolate_linear_onnx5d) auto outputs = net.execute(); auto output = outputs.at("output").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); ASSERT_EQ(this->expected_results[i].size(), output_ptr.size()); for (size_t j = 0; j < this->expected_results[i].size(); ++j) { @@ -1916,7 +1916,7 @@ TEST(resample_gpu, interpolate_in1x1x2x4_linear_scale) { auto outputs = net.execute(); auto output = outputs.at("interpolate").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector answers = { 2.6666665f, 4.3333331f @@ -1956,7 +1956,7 @@ TEST(resample_gpu, downsampling_u8) { auto outputs = net.execute(); auto output = outputs.at("resample").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector ref_out = { 124, 114, 104, 112, 143, 79, 78, 72, 74, 75, 72, 79, 77, 73, 74 }; @@ -2533,7 +2533,7 @@ TEST(resample_gpu, interpolate_in2x2x3x2_cubic_opt) { auto outputs = net.execute(); auto output = outputs.at("interpolate").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector answers = { 0.29600694f, 0.8828125f, 1.46961806f, @@ -2594,7 +2594,7 @@ TEST(resample_gpu, interpolate_in1x1x3x2_cubic_opt) { auto outputs = net.execute(); auto output = outputs.at("interpolate").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector answers = { 5.34722222f, 3.f, 0.65277778f, diff --git a/src/plugins/intel_gpu/tests/unit/test_cases/reshape_gpu_test.cpp b/src/plugins/intel_gpu/tests/unit/test_cases/reshape_gpu_test.cpp index 9384e41ab83b..7db53bc041ac 100644 --- a/src/plugins/intel_gpu/tests/unit/test_cases/reshape_gpu_test.cpp +++ b/src/plugins/intel_gpu/tests/unit/test_cases/reshape_gpu_test.cpp @@ -479,13 +479,13 @@ void test_multiple_users_with_reorder(bool is_caching_test) { auto outputs = network->execute(); auto output = outputs.at("relu1").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (size_t i = 0; i < out1.size(); i++) ASSERT_EQ(output_ptr[i], out1[i]); auto output_2 = outputs.at("relu2").get_memory(); - cldnn::mem_lock output_ptr_2(output_2, get_test_stream()); + cldnn::mem_lock output_ptr_2(output_2, get_test_stream()); for (size_t i = 0; i < out2.size(); i++) ASSERT_EQ(output_ptr_2[i], out2[i]); @@ -673,7 +673,7 @@ void test_basic_bfwzyx(bool is_caching_test) { ASSERT_EQ(output->get_layout().data_type, input->get_layout().data_type); ASSERT_EQ(output->get_layout().format, input->get_layout().format); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); ASSERT_EQ(output_ptr.size(), expected_out.size()); for (size_t i = 0; i < expected_out.size(); i++) { @@ -724,7 +724,7 @@ void test_shrink_chain_partial(bool is_caching_test) { auto outputs = network->execute(); auto output = outputs.at("out_reorder").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (size_t i = 0; i < out.size(); i++) ASSERT_EQ(output_ptr[i], out[i]) << " i=" << i; @@ -769,7 +769,7 @@ void test_shrink_chain_full(bool is_caching_test) { auto outputs = network.execute(); auto output = outputs.at("out_reorder").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (size_t i = 0; i < out.size(); i++) ASSERT_EQ(output_ptr[i], out[i]) << " i=" << i; @@ -809,7 +809,7 @@ void test_shrink_chain_out(bool is_caching_test) { auto outputs = network->execute(); auto output = outputs.at("reshape1").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (size_t i = 0; i < out.size(); i++) ASSERT_EQ(output_ptr[i], out[i]) << " i=" << i; @@ -858,7 +858,7 @@ void test_shrink_chain_partial_reorder_truncate(bool is_caching_test) { auto outputs = network->execute(); auto output = outputs.at("out_reorder").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (size_t i = 0; i < out.size(); i++) ASSERT_EQ(output_ptr[i], out[i]) << " i=" << i; @@ -908,7 +908,7 @@ TEST(reshape_gpu_f32, basic_runtime_static_shape) { ASSERT_EQ(output->get_layout().data_type, input->get_layout().data_type); ASSERT_EQ(output->get_layout().format, format::bfyx); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); ASSERT_EQ(output_ptr.size(), input_data.size()); for (size_t i = 0; i < input_data.size(); i++) { @@ -957,7 +957,7 @@ TEST(reshape_gpu_f32, basic_runtime_dynamic_shape) { ASSERT_EQ(output->get_layout().data_type, input->get_layout().data_type); ASSERT_EQ(output->get_layout().format, format::bfyx); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); ASSERT_EQ(output_ptr.size(), input_data.size()); for (size_t i = 0; i < input_data.size(); i++) { @@ -1013,7 +1013,7 @@ TEST(reshape_gpu_f32, basic_runtime_dynamic_shape_with_const) { ov::PartialShape ref_pshape = {12, 3}; ASSERT_EQ(output->get_layout().get_partial_shape(), ref_pshape); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); ASSERT_EQ(output_ptr.size(), input_data.size()); for (size_t i = 0; i < input_data.size(); i++) { @@ -1070,7 +1070,7 @@ TEST(reshape_gpu_f32, basic_runtime_dynamic_shape_with_const_optimized_out) { ov::PartialShape ref_pshape = {12, 3}; ASSERT_EQ(output->get_layout().get_partial_shape(), ref_pshape); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); ASSERT_EQ(output_ptr.size(), input_data.size()); for (size_t i = 0; i < input_data.size(); i++) { @@ -1116,7 +1116,7 @@ TEST(reshape_gpu_f32, basic_dynamic_shape_to_static_optimized_out) { ov::PartialShape expected_shape = {2, 1}; ASSERT_EQ(output->get_layout().get_partial_shape(), expected_shape); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_res = {9.f, 9.f}; ASSERT_EQ(output_ptr.size(), expected_res.size()); @@ -1166,7 +1166,7 @@ TEST(reshape_gpu_f32, basic_dynamic_shape_to_static_optimized_out_static_optimiz ov::PartialShape expected_shape = {2, 1}; ASSERT_EQ(output->get_layout().get_partial_shape(), expected_shape); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_res = {9.f, 9.f}; ASSERT_EQ(output_ptr.size(), expected_res.size()); @@ -1216,7 +1216,7 @@ TEST(reshape_gpu_f32, basic_runtime_dynamic_shape_activation_fusion) { ASSERT_EQ(output->get_layout().data_type, input->get_layout().data_type); ASSERT_EQ(output->get_layout().format, format::bfyx); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); ASSERT_EQ(output_ptr.size(), input_data.size()); for (size_t i = 0; i < input_data.size(); i++) { @@ -1278,7 +1278,7 @@ TEST(reshape_gpu_f32, reshape_reorder_trucation_mode) auto output = outputs.begin()->second.get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (size_t i = 0; i < output_ptr.size(); ++i) { @@ -1701,7 +1701,7 @@ TEST(reshape_gpu_f32, followed_by_convolution_dynamic) { auto output_memory = outputs.at("conv").get_memory(); auto output_layout = output_memory->get_layout(); - cldnn::mem_lock output_ptr(output_memory, get_test_stream()); + cldnn::mem_lock output_ptr(output_memory, get_test_stream()); int y_size = output_layout.spatial(1); int x_size = output_layout.spatial(0); @@ -1749,7 +1749,7 @@ TEST(reshape_gpu_f32, followed_by_convolution_dynamic) { auto output_memory = outputs.at("conv").get_memory(); auto output_layout = output_memory->get_layout(); - cldnn::mem_lock output_ptr(output_memory, get_test_stream()); + cldnn::mem_lock output_ptr(output_memory, get_test_stream()); int y_size = output_layout.spatial(1); int x_size = output_layout.spatial(0); @@ -1820,7 +1820,7 @@ TEST(reshape_gpu_f32, followed_by_convolution_dynamic_w_pad) { // check 'conv' auto output_memory = outputs.at("conv").get_memory(); auto output_layout = output_memory->get_layout(); - cldnn::mem_lock output_ptr(output_memory, get_test_stream()); + cldnn::mem_lock output_ptr(output_memory, get_test_stream()); int y_size = output_layout.spatial(1); int x_size = output_layout.spatial(0); diff --git a/src/plugins/intel_gpu/tests/unit/test_cases/reverse_gpu_test.cpp b/src/plugins/intel_gpu/tests/unit/test_cases/reverse_gpu_test.cpp index 2fec9f78791f..687a9abf7265 100644 --- a/src/plugins/intel_gpu/tests/unit/test_cases/reverse_gpu_test.cpp +++ b/src/plugins/intel_gpu/tests/unit/test_cases/reverse_gpu_test.cpp @@ -82,7 +82,7 @@ struct reverse_gpu_test : public ::testing::TestWithParam auto result = network->execute(); auto out_mem = result.at(ouput_op_name).get_memory(); - cldnn::mem_lock out_ptr(out_mem, get_test_stream()); + cldnn::mem_lock out_ptr(out_mem, get_test_stream()); ASSERT_EQ(params.expected_out.size(), out_ptr.size()); for (size_t i = 0; i < params.expected_out.size(); ++i) { diff --git a/src/plugins/intel_gpu/tests/unit/test_cases/reverse_sequence_gpu_test.cpp b/src/plugins/intel_gpu/tests/unit/test_cases/reverse_sequence_gpu_test.cpp index 8b79304466da..0c5ee6854326 100644 --- a/src/plugins/intel_gpu/tests/unit/test_cases/reverse_sequence_gpu_test.cpp +++ b/src/plugins/intel_gpu/tests/unit/test_cases/reverse_sequence_gpu_test.cpp @@ -44,7 +44,7 @@ void test_fp32_d2_2_ba1_sa0(bool is_caching_test) { auto outputs = network->execute(); auto output = outputs.at("reverse_sequence").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = { 0.0f, 3.0f, 2.0f, 1.0f @@ -93,7 +93,7 @@ void test_fp32_d3_3_3_ba0_sa1(bool is_caching_test) { auto outputs = network->execute(); auto output = outputs.at("reverse_sequence").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = { 3.0f, 4.0f, 5.0f, 0.0f, 1.0f, 2.0f, 6.0f, 7.0f, 8.0f, @@ -143,7 +143,7 @@ TEST(reverese_sequence_gpu_test, fp32_d3_3_3_ba2_sa0) { auto outputs = network.execute(); auto output = outputs.at("reverse_sequence").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = { 9.0f, 10.0f, 11.0f, 12.0f, 13.0f, 14.0f, 15.0f, 16.0f, 17.0f, @@ -189,7 +189,7 @@ TEST(reverese_sequence_gpu_test, fp32_d2_2_3_2ba0_sa3) { auto outputs = network.execute(); auto output = outputs.at("reverse_sequence").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = { 0.0f, 1.0f, 2.0f, 3.0f, 4.0f, 5.0f, @@ -236,7 +236,7 @@ TEST(reverese_sequence_gpu_test, fp32_d2_2_3_2ba0_sa2) { auto outputs = network.execute(); auto output = outputs.at("reverse_sequence").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = { 2.0f, 3.0f, 0.0f, 1.0f, 4.0f, 5.0f, @@ -283,7 +283,7 @@ TEST(reverese_sequence_gpu_test, fp32_d2_2_3_2ba2_sa0) { auto outputs = network.execute(); auto output = outputs.at("reverse_sequence").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = { 0.0f, 1.0f, 2.0f, 3.0f, 16.0f, 17.0f, @@ -328,7 +328,7 @@ TEST(reverese_sequence_gpu_test, fp16_d2_2_ba1_sa0) { auto outputs = network.execute(); auto output = outputs.at("reverse_sequence").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = { 0.0f, 3.0f, 2.0f, 1.0f @@ -370,7 +370,7 @@ TEST(reverese_sequence_gpu_test, fp16x2_d2_2_ba1_sa0) { auto outputs = network.execute(); auto output = outputs.at("reverse_sequence").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = { 0.0f, 3.0f, 2.0f, 1.0f @@ -414,7 +414,7 @@ TEST(reverese_sequence_gpu_test, fp16_d3_3_3_ba0_sa1) { auto outputs = network.execute(); auto output = outputs.at("reverse_sequence").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = { 3.0f, 4.0f, 5.0f, 0.0f, 1.0f, 2.0f, 6.0f, 7.0f, 8.0f, @@ -460,7 +460,7 @@ TEST(reverese_sequence_gpu_test, fp16_d3_3_3_ba2_sa0) { auto outputs = network.execute(); auto output = outputs.at("reverse_sequence").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = { 9.0f, 10.0f, 11.0f, 12.0f, 13.0f, 14.0f, 15.0f, 16.0f, 17.0f, @@ -506,7 +506,7 @@ TEST(reverese_sequence_gpu_test, fp16_d2_2_3_2ba0_sa3) { auto outputs = network.execute(); auto output = outputs.at("reverse_sequence").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = { 0.0f, 1.0f, 2.0f, 3.0f, 4.0f, 5.0f, @@ -553,7 +553,7 @@ TEST(reverese_sequence_gpu_test, fp16_d2_2_3_2ba0_sa2) { auto outputs = network.execute(); auto output = outputs.at("reverse_sequence").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = { 2.0f, 3.0f, 0.0f, 1.0f, 4.0f, 5.0f, @@ -600,7 +600,7 @@ TEST(reverese_sequence_gpu_test, fp16_d2_2_3_2ba2_sa0) { auto outputs = network.execute(); auto output = outputs.at("reverse_sequence").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = { 0.0f, 1.0f, 2.0f, 3.0f, 16.0f, 17.0f, diff --git a/src/plugins/intel_gpu/tests/unit/test_cases/rms_gpu_test.cpp b/src/plugins/intel_gpu/tests/unit/test_cases/rms_gpu_test.cpp index 490db4b4a790..c650a8770689 100644 --- a/src/plugins/intel_gpu/tests/unit/test_cases/rms_gpu_test.cpp +++ b/src/plugins/intel_gpu/tests/unit/test_cases/rms_gpu_test.cpp @@ -4,7 +4,9 @@ #include "test_utils.h" +#include #include +#include #include #include "rms_inst.h" @@ -30,36 +32,36 @@ void rms_ref(const memory::ptr input, const memory::ptr gamma, memory::ptr outpu weight = std::make_unique>(gamma, get_test_stream()); } + // RMS normalization across the last (innermost populated) dimension. + // For bfyx with x_size > 1 (rank 4): normalize across X per (b, f, y). + // For bfyx with x_size == 1 (rank 3 mapped to bfyx): normalize across Y per (b, f). + // This matches the kernel behavior based on ov_input_rank. + bool norm_over_x = (x_size > 1); + uint32_t outer_y = norm_over_x ? y_size : 1; + uint32_t norm_size = norm_over_x ? x_size : y_size; + for (uint32_t b = 0; b < batch_size; ++b) { for (uint32_t f = 0; f < feature_size; ++f) { - float rms = 0.f; - for (uint32_t y = 0; y < y_size; ++y) { - for (uint32_t x = 0; x < x_size; ++x) { - auto tensor_src = tensor(batch(b), feature(f), spatial(x, y, 0, 0)); - size_t src_offset = input_layout.get_linear_offset(tensor_src); - rms += std::pow(static_cast(src[src_offset]), 2); + for (uint32_t oy = 0; oy < outer_y; ++oy) { + float rms = 0.f; + for (uint32_t n = 0; n < norm_size; ++n) { + uint32_t y = norm_over_x ? oy : n; + uint32_t x = norm_over_x ? n : 0; + auto t = tensor(batch(b), feature(f), spatial(x, y, 0, 0)); + rms += std::pow(static_cast(src[input_layout.get_linear_offset(t)]), 2); } - } - rms /= y_size * x_size; - rms += epsilon; - rms = std::pow(std::sqrt(rms), -1); - - for (uint32_t y = 0; y < y_size; ++y) { - for (uint32_t x = 0; x < x_size; ++x) { - auto tensor_src = tensor(batch(b), feature(f), spatial(x, y, 0, 0)); - auto tensor_dst = tensor(batch(b), feature(f), spatial(x, y, 0, 0)); - size_t src_offset = input_layout.get_linear_offset(tensor_src); - size_t dst_offset = input_layout.get_linear_offset(tensor_dst); - - float gamma_val = 1.0f; - if (weight) { - auto tensor_weight = tensor(batch(0), feature(0), spatial(x, y, 0, 0)); - size_t weight_offset = input_layout.get_linear_offset(tensor_weight); - gamma_val = static_cast((*weight)[weight_offset]); - } - - float result = rms * static_cast(src[src_offset]) * gamma_val; - dst[dst_offset] = static_cast(result); + rms /= norm_size; + rms += epsilon; + rms = std::pow(std::sqrt(rms), -1); + + for (uint32_t n = 0; n < norm_size; ++n) { + uint32_t y = norm_over_x ? oy : n; + uint32_t x = norm_over_x ? n : 0; + auto t = tensor(batch(b), feature(f), spatial(x, y, 0, 0)); + size_t offset = input_layout.get_linear_offset(t); + + float gamma_val = weight ? static_cast((*weight)[n]) : 1.0f; + dst[offset] = static_cast(rms * static_cast(src[offset]) * gamma_val); } } } @@ -98,7 +100,7 @@ TEST(rms_gpu_test, rms_test_bfyx_ref) { ASSERT_EQ(outputs.begin()->first, "rms"); auto output = outputs.begin()->second.get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); cldnn::mem_lock output_ref_ptr(output_ref, get_test_stream()); for (unsigned int i = 0; i < output_ref->count(); ++i) { @@ -141,7 +143,7 @@ TEST(rms_gpu_test, rms_test_bfyx_opt) { ASSERT_EQ(outputs.begin()->first, "rms"); auto output = outputs.begin()->second.get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); cldnn::mem_lock output_ref_ptr(output_ref, get_test_stream()); for (unsigned int i = 0; i < output_ref->count(); ++i) { @@ -184,7 +186,7 @@ TEST(rms_gpu_test, rms_test_bfyx_opt_leftovers) { ASSERT_EQ(outputs.begin()->first, "rms"); auto output = outputs.begin()->second.get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); cldnn::mem_lock output_ref_ptr(output_ref, get_test_stream()); for (unsigned int i = 0; i < output_ref->count(); ++i) { @@ -229,7 +231,7 @@ TEST(rms_gpu_test, rms_test_bfyx_opt_dyn) { ASSERT_EQ(outputs.begin()->first, "rms"); auto output = outputs.begin()->second.get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); cldnn::mem_lock output_ref_ptr(output_ref, get_test_stream()); for (unsigned int i = 0; i < output_ref->count(); ++i) { @@ -274,7 +276,7 @@ TEST(rms_gpu_test, rms_test_bfyx_opt_all_dims_dyn) { ASSERT_EQ(outputs.begin()->first, "rms"); auto output = outputs.begin()->second.get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); cldnn::mem_lock output_ref_ptr(output_ref, get_test_stream()); for (unsigned int i = 0; i < output_ref->count(); ++i) { @@ -319,7 +321,7 @@ TEST(rms_gpu_test, rms_test_bfyx_opt_leftovers_dyn) { ASSERT_EQ(outputs.begin()->first, "rms"); auto output = outputs.begin()->second.get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); cldnn::mem_lock output_ref_ptr(output_ref, get_test_stream()); for (unsigned int i = 0; i < output_ref->count(); ++i) { @@ -364,7 +366,7 @@ TEST(rms_gpu_test, rms_test_bfyx_opt_unaligned_dyn) { ASSERT_EQ(outputs.begin()->first, "rms"); auto output = outputs.begin()->second.get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); cldnn::mem_lock output_ref_ptr(output_ref, get_test_stream()); for (unsigned int i = 0; i < output_ref->count(); ++i) { @@ -420,7 +422,7 @@ TEST(rms_gpu_test, rms_test_bfyx_opt_padding) { ASSERT_EQ(outputs.begin()->first, "rms"); auto output = outputs.begin()->second.get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); cldnn::mem_lock output_ref_ptr(output_ref, get_test_stream()); for (unsigned int i = 0; i < output_ref->count(); ++i) { @@ -454,7 +456,7 @@ TEST(rms_gpu_test, rms_test_without_gamma_bfyx_ref) { ASSERT_EQ(outputs.begin()->first, "rms"); auto output = outputs.begin()->second.get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); cldnn::mem_lock output_ref_ptr(output_ref, get_test_stream()); for (unsigned int i = 0; i < output_ref->count(); ++i) { @@ -490,7 +492,7 @@ TEST(rms_gpu_test, rms_test_without_gamma_bfyx_opt) { ASSERT_EQ(outputs.begin()->first, "rms"); auto output = outputs.begin()->second.get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); cldnn::mem_lock output_ref_ptr(output_ref, get_test_stream()); for (unsigned int i = 0; i < output_ref->count(); ++i) { @@ -531,10 +533,115 @@ TEST(rms_gpu_test, rms_test_without_gamma_dyn) { ASSERT_EQ(outputs.begin()->first, "rms"); auto output = outputs.begin()->second.get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); cldnn::mem_lock output_ref_ptr(output_ref, get_test_stream()); for (unsigned int i = 0; i < output_ref->count(); ++i) { EXPECT_NEAR(output_ptr[i], output_ref_ptr[i], 1e-3); } } + +// Regression test for in-place crop on spatial axis followed by RMS normalization. +// RMS kernels may not correctly handle spatial padding introduced by in-place crop. +TEST(rms_gpu_test, in_place_crop_rms_spatial_split) { + auto& engine = get_test_engine(); + + // Input [1,2,3,16] bfyx. VariadicSplit on axis 2 (spatial Y) into [2, 1]. + // crop_0 covers y=[0..1] (size 2), crop_1 covers y=2 (size 1). + // Each crop feeds RMS normalization. + // dim_x = 16 is the minimum for the bfyx_opt kernel (requires gamma >= subgroup_size(16)). + // force_implementations ensures bfyx_opt is selected regardless of other heuristics. + const int64_t dim_b = 1, dim_f = 2, dim_y = 3, dim_x = 16; + const int64_t split0 = 2, split1 = 1; + const int64_t off_y0 = 0, off_y1 = 2; + const float epsilon = 1e-6f; + + auto input_mem = engine.allocate_memory({ov::PartialShape{dim_b, dim_f, dim_y, dim_x}, + data_types::f32, format::bfyx}); + auto axis_mem = engine.allocate_memory({ov::PartialShape{}, data_types::i64, format::bfyx}); + auto splits_length_mem = engine.allocate_memory({ov::PartialShape{2}, data_types::i64, format::bfyx}); + // Gamma weights for RMS: shape [1, 1, 1, dim_x] — per-element scale on X axis + auto gamma_mem = engine.allocate_memory({ov::PartialShape{1, 1, 1, dim_x}, data_types::f32, format::bfyx}); + + const int64_t axis = 2; + const size_t total = static_cast(dim_b * dim_f * dim_y * dim_x); + + // Deterministic input with small values + std::vector input_data(total); + for (size_t i = 0; i < total; i++) + input_data[i] = static_cast(i + 1); + set_values(input_mem, input_data); + set_values(axis_mem, {axis}); + set_values(splits_length_mem, {split0, split1}); + // Gamma = 1.0 (identity scale) for easy reference computation + std::vector gamma_data(dim_x, 1.0f); + set_values(gamma_mem, gamma_data); + + // Prepare crop slices and compute reference outputs using rms_ref + auto make_crop_ref = [&](int64_t split_size, int64_t y_offset) { + auto crop_mem = engine.allocate_memory({ov::PartialShape{dim_b, dim_f, split_size, dim_x}, data_types::f32, format::bfyx}); + auto out_mem = engine.allocate_memory({ov::PartialShape{dim_b, dim_f, split_size, dim_x}, data_types::f32, format::bfyx}); + std::vector crop_data; + for (int64_t f = 0; f < dim_f; f++) + for (int64_t y = 0; y < split_size; y++) + for (int64_t x = 0; x < dim_x; x++) + crop_data.push_back(input_data[static_cast(f * dim_y * dim_x + (y_offset + y) * dim_x + x)]); + set_values(crop_mem, crop_data); + rms_ref(crop_mem, gamma_mem, out_mem, epsilon); + return out_mem; + }; + auto output_ref_0 = make_crop_ref(split0, off_y0); + auto output_ref_1 = make_crop_ref(split1, off_y1); + + cldnn::crop_ngraph_op_mode op_mode = cldnn::crop_ngraph_op_mode::variadic_split; + topology topology; + topology.add(input_layout("input", input_mem->get_layout())); + topology.add(data("axis", axis_mem)); + topology.add(data("splits_length", splits_length_mem)); + topology.add(data("gamma", gamma_mem)); + // Branch 0: crop [1,2,2,16] -> RMS normalization + topology.add(crop("crop_0", {input_info("input"), input_info("axis"), input_info("splits_length")}, + tensor(1), tensor(0, 0, 0, off_y0), op_mode, 0, axis)); + topology.add(rms("rms_0", input_info("crop_0"), input_info("gamma"), epsilon)); + topology.add(reorder("output_0", input_info("rms_0"), format::bfyx, data_types::f32, + std::vector(), reorder_mean_mode::subtract, padding(), true)); + // Branch 1: crop [1,2,1,16] -> RMS normalization + topology.add(crop("crop_1", {input_info("input"), input_info("axis"), input_info("splits_length")}, + tensor(1), tensor(0, 0, 0, off_y1), op_mode, 1, axis)); + topology.add(rms("rms_1", input_info("crop_1"), input_info("gamma"), epsilon)); + topology.add(reorder("output_1", input_info("rms_1"), format::bfyx, data_types::f32, + std::vector(), reorder_mean_mode::subtract, padding(), true)); + + auto config = get_test_default_config(engine); + config.set_property(ov::intel_gpu::allow_new_shape_infer(true)); + config.set_property(ov::intel_gpu::optimize_data(true)); + config.set_property(ov::intel_gpu::force_implementations(ov::intel_gpu::ImplForcingMap{ + {"rms_0", {format::bfyx, "rms_gpu_bfyx_opt"}}, + {"rms_1", {format::bfyx, "rms_gpu_bfyx_opt"}} + })); + network network(engine, topology, config); + network.set_input_data("input", input_mem); + + auto outputs = network.execute(); + + ASSERT_TRUE(network.get_primitive("crop_0")->can_be_optimized()); + ASSERT_TRUE(network.get_primitive("crop_1")->can_be_optimized()); + + // Verify branch 0: crop [1,2,2,16], y-offset = 0 + auto out0_mem = outputs.at("output_0").get_memory(); + cldnn::mem_lock out0(out0_mem, get_test_stream()); + cldnn::mem_lock ref0(output_ref_0, get_test_stream()); + ASSERT_EQ(out0.size(), ref0.size()); + for (size_t i = 0; i < ref0.size(); i++) { + ASSERT_NEAR(out0[i], ref0[i], 1e-4f) << "Branch 0 mismatch at index=" << i; + } + + // Verify branch 1: crop [1,2,1,16], y-offset = 2 + auto out1_mem = outputs.at("output_1").get_memory(); + cldnn::mem_lock out1(out1_mem, get_test_stream()); + cldnn::mem_lock ref1(output_ref_1, get_test_stream()); + ASSERT_EQ(out1.size(), ref1.size()); + for (size_t i = 0; i < ref1.size(); i++) { + ASSERT_NEAR(out1[i], ref1[i], 1e-4f) << "Branch 1 mismatch at index=" << i; + } +} diff --git a/src/plugins/intel_gpu/tests/unit/test_cases/roi_align_gpu_test.cpp b/src/plugins/intel_gpu/tests/unit/test_cases/roi_align_gpu_test.cpp index dd0694ba76de..fc515d952fd1 100644 --- a/src/plugins/intel_gpu/tests/unit/test_cases/roi_align_gpu_test.cpp +++ b/src/plugins/intel_gpu/tests/unit/test_cases/roi_align_gpu_test.cpp @@ -101,7 +101,7 @@ struct roi_align_test : public testing::Test { auto outputs = network->execute(); auto output = outputs.at("out").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); ASSERT_EQ(output_ptr.size(), expected_output.size()); for (uint32_t i = 0; i < expected_output.size(); ++i) { @@ -260,7 +260,7 @@ TEST(roi_align_gpu_fp32, bfyx_inpad_1x1) { 3.f, 3.75f, 4.75f, 5.f, 3.f, 5.5f, 2.75f, 3.75f }; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); ASSERT_EQ(output_ptr.size(), expected_output.size()); for (uint32_t i = 0; i < expected_output.size(); ++i) { diff --git a/src/plugins/intel_gpu/tests/unit/test_cases/roi_align_rotated_gpu_test.cpp b/src/plugins/intel_gpu/tests/unit/test_cases/roi_align_rotated_gpu_test.cpp index ddfe2ea82682..d8bdaec30ad8 100644 --- a/src/plugins/intel_gpu/tests/unit/test_cases/roi_align_rotated_gpu_test.cpp +++ b/src/plugins/intel_gpu/tests/unit/test_cases/roi_align_rotated_gpu_test.cpp @@ -132,7 +132,7 @@ class roi_align_rotated_test : public ::testing::TestWithParamexecute(); auto output = outputs.at("out").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); cldnn::mem_lock wanted_output_ptr(params.expectedOutput, get_test_stream()); ASSERT_EQ(output->get_layout(), params.expectedOutput->get_layout()); diff --git a/src/plugins/intel_gpu/tests/unit/test_cases/roi_pooling_gpu_test.cpp b/src/plugins/intel_gpu/tests/unit/test_cases/roi_pooling_gpu_test.cpp index 432028293e80..cd68a9a33db1 100644 --- a/src/plugins/intel_gpu/tests/unit/test_cases/roi_pooling_gpu_test.cpp +++ b/src/plugins/intel_gpu/tests/unit/test_cases/roi_pooling_gpu_test.cpp @@ -190,7 +190,7 @@ struct roi_pooling_gpu_test : public testing::TestWithParamfirst, "reordered_roi_pooling"); auto output = outputs.at("reordered_roi_pooling").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); ASSERT_EQ(output_ptr.size(), p.output_values.size()); for (size_t i = 0; i < output_ptr.size(); ++i) { diff --git a/src/plugins/intel_gpu/tests/unit/test_cases/roll_gpu_test.cpp b/src/plugins/intel_gpu/tests/unit/test_cases/roll_gpu_test.cpp index 8469e93af975..9d532640b831 100644 --- a/src/plugins/intel_gpu/tests/unit/test_cases/roll_gpu_test.cpp +++ b/src/plugins/intel_gpu/tests/unit/test_cases/roll_gpu_test.cpp @@ -45,7 +45,7 @@ struct roll_test : testing::TestWithParam> { const auto outputs = network->execute(); auto output = outputs.at("reordered_roll").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); ASSERT_EQ(output_ptr.size(), p.expected_values.size()); for (size_t i = 0; i < output_ptr.size(); ++i) { diff --git a/src/plugins/intel_gpu/tests/unit/test_cases/scatter_elements_update_gpu_test.cpp b/src/plugins/intel_gpu/tests/unit/test_cases/scatter_elements_update_gpu_test.cpp index 3d1911ad26bb..604309370b99 100644 --- a/src/plugins/intel_gpu/tests/unit/test_cases/scatter_elements_update_gpu_test.cpp +++ b/src/plugins/intel_gpu/tests/unit/test_cases/scatter_elements_update_gpu_test.cpp @@ -82,7 +82,7 @@ void test_d2411_axisF(bool is_caching_test) { auto outputs = network->execute(); auto output = outputs.at("scatter_elements_update").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = { 10.f, 11.f, 5.f, 4.f, @@ -340,7 +340,7 @@ struct scatter_elements_update_gpu_formats_test const auto outputs = network->execute(); const auto output = outputs.at("ScatterEelementsUpdatePlain").get_memory(); - const cldnn::mem_lock output_ptr(output, get_test_stream()); + const cldnn::mem_lock output_ptr(output, get_test_stream()); ASSERT_EQ(params.data.size(), output_ptr.size()); ASSERT_EQ(expected.size(), output_ptr.size()); @@ -428,7 +428,7 @@ struct scatter_elements_update_gpu_reduction_test const auto outputs = network->execute(); const auto output = outputs.at("ScatterElementsUpdate").get_memory(); - const cldnn::mem_lock output_ptr(output, get_test_stream()); + const cldnn::mem_lock output_ptr(output, get_test_stream()); ASSERT_EQ(params.data.size(), output_ptr.size()); ASSERT_EQ(expected.size(), output_ptr.size()); @@ -740,7 +740,7 @@ TEST(scatter_elements_update_gpu_fp32, smoke_multiple_indices_mean_big_1d_dynami auto outputs = network.execute(); auto output = outputs.at("scatter_elements_update").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results(num, 0); expected_results.front() = 1; @@ -797,7 +797,7 @@ TEST(scatter_elements_update_gpu_fp32, smoke_multiple_indices_sum_big_1d_dynamic auto outputs = network.execute(); auto output = outputs.at("scatter_elements_update").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results(num, 0); for (auto pos : target_update_positions) { diff --git a/src/plugins/intel_gpu/tests/unit/test_cases/scatter_nd_update_gpu_test.cpp b/src/plugins/intel_gpu/tests/unit/test_cases/scatter_nd_update_gpu_test.cpp index 442850cc38d0..905e3b9bc39c 100644 --- a/src/plugins/intel_gpu/tests/unit/test_cases/scatter_nd_update_gpu_test.cpp +++ b/src/plugins/intel_gpu/tests/unit/test_cases/scatter_nd_update_gpu_test.cpp @@ -150,7 +150,7 @@ struct scatter_nd_update_random_test : testing::TestWithParamexecute(); auto output = outputs.at("out").get_memory(); - cldnn::mem_lock outputs_ptr(output, get_test_stream()); + cldnn::mem_lock outputs_ptr(output, get_test_stream()); auto outputs_ref = std::vector(params.input_size.count()); ov::reference::scatterNdUpdate(input_data.data(), @@ -220,7 +220,7 @@ struct scatter_nd_update_random_test : testing::TestWithParamexecute(); auto output = outputs.at("out").get_memory(); - cldnn::mem_lock outputs_ptr(output, get_test_stream()); + cldnn::mem_lock outputs_ptr(output, get_test_stream()); auto outputs_ref = std::vector(params.input_size.count()); ov::reference::scatterNdUpdate(input_data.data(), @@ -559,7 +559,7 @@ TEST(scatter_nd_update_gpu_fp16_test16, data3_indice4_update3_dynamic) { auto outputs = network.execute(); auto output = outputs.at("scatter_nd_update").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (size_t i = 0; i < expected_results.size(); ++i) { ASSERT_EQ(expected_results[i], half_to_float(output_ptr[i])); @@ -654,7 +654,7 @@ TEST(scatter_nd_update_gpu_fp16_test15, data5_indice3_update5) { auto outputs = network.execute(); auto output = outputs.at("scatter_nd_update").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (size_t i = 0; i < expected_results.size(); ++i) { ASSERT_EQ(expected_results[i], half_to_float(output_ptr[i])); @@ -738,7 +738,7 @@ TEST(scatter_nd_update_gpu_fp16_test14, data5_indice2_update3) { auto outputs = network.execute(); auto output = outputs.at("scatter_nd_update").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (size_t i = 0; i < expected_results.size(); ++i) { ASSERT_EQ(expected_results[i], half_to_float(output_ptr[i])); @@ -802,7 +802,7 @@ TEST(scatter_nd_update_gpu_fp16_test13, data4_indice2_update2) { auto outputs = network.execute(); auto output = outputs.at("scatter_nd_update").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (size_t i = 0; i < expected_results.size(); ++i) { ASSERT_EQ(expected_results[i], half_to_float(output_ptr[i])); @@ -873,7 +873,7 @@ TEST(scatter_nd_update_gpu_fp16_test12, data3_indice3_update1) { auto outputs = network.execute(); auto output = outputs.at("scatter_nd_update").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (size_t i = 0; i < expected_results.size(); ++i) { ASSERT_EQ(expected_results[i], half_to_float(output_ptr[i])); @@ -1003,7 +1003,7 @@ TEST(scatter_nd_update_gpu_fp16_test11, data6_indice1_update6) { auto outputs = network.execute(); auto output = outputs.at("scatter_nd_update").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (size_t i = 0; i < expected_results.size(); ++i) { ASSERT_EQ(expected_results[i], half_to_float(output_ptr[i])); @@ -1099,7 +1099,7 @@ TEST(scatter_nd_update_gpu_fp16_test10, data5_indice1_update5) { auto outputs = network.execute(); auto output = outputs.at("scatter_nd_update").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (size_t i = 0; i < expected_results.size(); ++i) { ASSERT_EQ(expected_results[i], half_to_float(output_ptr[i])); @@ -1177,7 +1177,7 @@ TEST(scatter_nd_update_gpu_fp16_test9, data4_indice1_update4) { auto outputs = network.execute(); auto output = outputs.at("scatter_nd_update").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (size_t i = 0; i < expected_results.size(); ++i) { ASSERT_EQ(expected_results[i], half_to_float(output_ptr[i])); @@ -1275,7 +1275,7 @@ TEST(scatter_nd_update_gpu_fp16_test8, data6_indice2_update5) { auto outputs = network.execute(); auto output = outputs.at("scatter_nd_update").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (size_t i = 0; i < expected_results.size(); ++i) { ASSERT_EQ(expected_results[i], half_to_float(output_ptr[i])); @@ -1343,7 +1343,7 @@ TEST(scatter_nd_update_gpu_fp16_test7, data5_indice2_update4) { auto outputs = network.execute(); auto output = outputs.at("scatter_nd_update").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (size_t i = 0; i < expected_results.size(); ++i) { ASSERT_EQ(expected_results[i], half_to_float(output_ptr[i])); @@ -1409,7 +1409,7 @@ TEST(scatter_nd_update_gpu_fp16_test6, data4_indice2_update3) { auto outputs = network.execute(); auto output = outputs.at("scatter_nd_update").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (size_t i = 0; i < expected_results.size(); ++i) { ASSERT_EQ(expected_results[i], half_to_float(output_ptr[i])); @@ -1474,7 +1474,7 @@ TEST(scatter_nd_update_gpu_fp16_test5, data3_indice2_update2) { auto outputs = network.execute(); auto output = outputs.at("scatter_nd_update").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (size_t i = 0; i < expected_results.size(); ++i) { ASSERT_EQ(expected_results[i], half_to_float(output_ptr[i])); @@ -1529,7 +1529,7 @@ TEST(scatter_nd_update_gpu_fp16_test4, data2_indice2_update1) { auto outputs = network.execute(); auto output = outputs.at("scatter_nd_update").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (size_t i = 0; i < expected_results.size(); ++i) { ASSERT_EQ(expected_results[i], half_to_float(output_ptr[i])); @@ -1604,7 +1604,7 @@ TEST(scatter_nd_update_gpu_fp16_test3, data3_indice1_update3) { auto outputs = network.execute(); auto output = outputs.at("scatter_nd_update").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (size_t i = 0; i < expected_results.size(); ++i) { ASSERT_EQ(expected_results[i], half_to_float(output_ptr[i])); @@ -1659,7 +1659,7 @@ TEST(scatter_nd_update_gpu_fp16_test2, data2_indice1_update2) { auto outputs = network.execute(); auto output = outputs.at("scatter_nd_update").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (size_t i = 0; i < expected_results.size(); ++i) { ASSERT_EQ(expected_results[i], half_to_float(output_ptr[i])); @@ -1708,7 +1708,7 @@ TEST(scatter_nd_update_gpu_fp16_test1, data1_indice1_update1) { auto outputs = network.execute(); auto output = outputs.at("scatter_nd_update").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (size_t i = 0; i < expected_results.size(); ++i) { ASSERT_EQ(expected_results[i], half_to_float(output_ptr[i])); @@ -1804,7 +1804,7 @@ TEST(scatter_nd_update_gpu_fp16, d6661_i2311) { auto output = outputs.at("scatter_nd_update").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = { 100.f, 101.f, 102.f, 103.f, 104.f, 105.f, @@ -1943,7 +1943,7 @@ TEST(scatter_nd_update_gpu_fp16, d6661_i2211) { auto output = outputs.at("scatter_nd_update").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = { 100.f, 101.f, 102.f, 103.f, 104.f, 105.f, @@ -2092,7 +2092,7 @@ TEST(scatter_nd_update_gpu_fp16, d6661_i2111) { auto output = outputs.at("scatter_nd_update").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = { 777.f, 999.f, 999.f, 999.f, 999.f, 999.f, @@ -2213,7 +2213,7 @@ TEST(scatter_nd_update_gpu_fp16, d3232_i2411) { auto output = outputs.at("scatter_nd_update").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = { 100.f, 101.f, @@ -2316,7 +2316,7 @@ TEST(scatter_nd_update_gpu_fp16, d3232_i2311) { auto output = outputs.at("scatter_nd_update").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = { 100.f, 101.f, @@ -2425,7 +2425,7 @@ TEST(scatter_nd_update_gpu_fp16, d3232_i2211) { auto output = outputs.at("scatter_nd_update").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = { 100.f, 101.f, @@ -2542,7 +2542,7 @@ TEST(scatter_nd_update_gpu_fp16, d3232_i2111) { auto output = outputs.at("scatter_nd_update").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = { 666.f, 666.f, @@ -2676,7 +2676,7 @@ TEST(scatter_nd_update_gpu_fp16, d32323_i25111) { auto output = outputs.at("scatter_nd_update").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = { 100.f, 101.f, 102.f, @@ -2844,7 +2844,7 @@ TEST(scatter_nd_update_gpu_fp16, d32323_i24111) { auto output = outputs.at("scatter_nd_update").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = { 100.f, 101.f, 102.f, @@ -3015,7 +3015,7 @@ TEST(scatter_nd_update_gpu_fp16, d32323_i23111) { auto output = outputs.at("scatter_nd_update").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = { 100.f, 101.f, 102.f, @@ -3198,7 +3198,7 @@ TEST(scatter_nd_update_gpu_fp16, d32323_i22111) { auto output = outputs.at("scatter_nd_update").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = { 100.f, 101.f, 102.f, @@ -3398,7 +3398,7 @@ TEST(scatter_nd_update_gpu_fp16, d32323_i21111) { auto output = outputs.at("scatter_nd_update").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = { 555.f, 555.f, 555.f, @@ -3558,7 +3558,7 @@ TEST(scatter_nd_update_gpu_fp16, d222222_i261111) { auto output = outputs.at("scatter_nd_update").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = { 100.f, 101.f, @@ -3711,7 +3711,7 @@ TEST(scatter_nd_update_gpu_fp16, d222222_i251111) { auto output = outputs.at("scatter_nd_update").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = { 100.f, 101.f, @@ -3867,7 +3867,7 @@ TEST(scatter_nd_update_gpu_fp16, d222222_i241111) { auto output = outputs.at("scatter_nd_update").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = { 100.f, 101.f, @@ -4030,7 +4030,7 @@ TEST(scatter_nd_update_gpu_fp16, d222222_i231111) { auto output = outputs.at("scatter_nd_update").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = { 100.f, 101.f, @@ -4204,7 +4204,7 @@ TEST(scatter_nd_update_gpu_fp16, d222222_i221111) { auto output = outputs.at("scatter_nd_update").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = { 100.f, 101.f, @@ -4401,7 +4401,7 @@ void test_d222222_i211111(bool is_caching_test) { auto output = outputs.at("scatter_nd_update").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = { 777.f, 777.f, @@ -4519,7 +4519,7 @@ TEST(scatter_nd_update_gpu, dynamic) { auto outputs = network.execute(); auto output = outputs.at("scatter_nd_update").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = { 0.f, 1.f, 2.f, 3.f, 4.f, 5.f, 6.f, 7.f, @@ -4580,7 +4580,7 @@ TEST(scatter_nd_update_gpu, dynamic_padded_output) { auto outputs = network.execute(); auto output = outputs.at("scatter_nd_update").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = { 0.f, 0.f, 0.f, 0.f, 0.f, 0.f, 0.f, 0.f, 0.f, 0.f, @@ -4699,7 +4699,7 @@ TEST(scatter_nd_update_gpu, dynamic_5d) { auto output = outputs.at("scatter_nd_update").get_memory(); ASSERT_EQ(output->get_layout().get_partial_shape(), input1->get_layout().get_partial_shape()); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (size_t i = 0; i < expected_res.size(); ++i) { ASSERT_EQ(expected_res[i], output_ptr[i]) << " i = " << i; @@ -4773,7 +4773,7 @@ TEST(scatter_nd_update_gpu, subgraph_input_changed) { auto outputs = network.execute(); auto output = outputs.at("scatter_nd_update").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (size_t i = 0; i < 2; ++i) { ASSERT_EQ(expected_results[i], output_ptr[i]); diff --git a/src/plugins/intel_gpu/tests/unit/test_cases/scatter_update_gpu_test.cpp b/src/plugins/intel_gpu/tests/unit/test_cases/scatter_update_gpu_test.cpp index 5114da7c20b7..3a1d4a3e7e9a 100644 --- a/src/plugins/intel_gpu/tests/unit/test_cases/scatter_update_gpu_test.cpp +++ b/src/plugins/intel_gpu/tests/unit/test_cases/scatter_update_gpu_test.cpp @@ -105,7 +105,7 @@ void test_d2411_axisB(bool is_caching_test) { auto output = outputs.at("out").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = { 3.f, 6.f, 5.f, 4.f, @@ -186,7 +186,7 @@ TEST(scatter_update_gpu_fp32, d8111_axisB) { auto outputs = network.execute(); auto output = outputs.at("out").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = { 1.f, 11.f, 3.f, 10.f, 9.f, 6.f, 7.f, 12.f @@ -279,7 +279,7 @@ TEST(scatter_update_gpu_fp16, d4311_axisB) { auto outputs = network.execute(); auto output = outputs.at("out").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = { 9.f, 10.f, 11.f, @@ -406,7 +406,7 @@ TEST(scatter_update_gpu_fp16, d2521_axisF) { auto outputs = network.execute(); auto output = outputs.at("out").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = { 21.f, 31.f, @@ -519,7 +519,7 @@ TEST(scatter_update_gpu_fp16, d2241_axisY) { auto outputs = network.execute(); auto output = outputs.at("out").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = { 0.f, 40.f, 20.f, 30.f, @@ -680,7 +680,7 @@ TEST(scatter_update_gpu_fp16, d8x2x20x1_axisB) { auto outputs = network.execute(); auto output = outputs.at("out").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = { 0.f, 0.f, 0.f, 0.f, 0.f, 0.f, 0.f, 0.f, 0.f, 0.f, 0.f, 0.f, 0.f, 0.f, 0.f, 0.f, 0.f, 0.f, 0.f, 0.f, @@ -806,7 +806,7 @@ TEST(scatter_update_gpu_fp32, d2214_axisX) { auto outputs = network.execute(); auto output = outputs.at("out").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = { 30.f, 1.f, 20.f, 40.f, @@ -908,7 +908,7 @@ TEST(scatter_update_gpu_int32, d6211_axisB) { auto outputs = network.execute(); auto output = outputs.at("out").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = { 1, 2, @@ -1007,7 +1007,7 @@ TEST(scatter_update_gpu_int32, d3151_axisY) { auto outputs = network.execute(); auto output = outputs.at("out").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = { 30, 1, 20, 200, 40, @@ -1091,7 +1091,7 @@ TEST(scatter_update_gpu_fp32, d24111_axisF_bfzyx) { auto outputs = network.execute(); auto output = outputs.at("out").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = { 2.f, 0.f, 1.f, 0.f, @@ -1197,7 +1197,7 @@ TEST(scatter_update_gpu_int32, d121251_bfwzyx_axisB) { auto outputs = network.execute(); auto output = outputs.at("scatter_update").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = { 40, 30, 20, 3, 50, @@ -1289,7 +1289,7 @@ TEST(scatter_update_gpu_fp32, d21511_bfzyx_axisX) { auto outputs = network.execute(); auto output = outputs.at("out").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = { 30.f, 40.f, 2.f, 10.f, 20.f, @@ -1394,7 +1394,7 @@ TEST(scatter_update_gpu_fp32, d1252_axisY_bfwzyx) { auto outputs = network.execute(); auto output = outputs.at("out").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = { 40.f, 50.f, 2.f, 3.f, 20.f, 30.f, 60.f, 70.f, 80.f, 90.f, @@ -1484,7 +1484,7 @@ TEST(scatter_update_gpu_int32, d2115_axisX_bfwzyx) { auto outputs = network.execute(); auto output = outputs.at("out").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = { 0, 30, 20, 50, 40, @@ -1578,7 +1578,7 @@ void test_d21214_bfzyx_axisX_bfwzyx(bool is_caching_test) { auto outputs = network->execute(); auto output = outputs.at("out").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = { 0.f, 40.f, 30.f, 20.f, @@ -1669,6 +1669,253 @@ TEST(scatter_update_gpu_fp32, dynamic) { ASSERT_TRUE(impl != nullptr); ASSERT_TRUE(impl->is_dynamic()); + auto outputs = network.execute(); + auto output = outputs.at("out").get_memory(); + cldnn::mem_lock output_ptr(output, get_test_stream()); + + std::vector expected_results = { + 40.f, 50.f, 2.f, 3.f, 20.f, 30.f, 60.f, 70.f, 80.f, 90.f, + 120.f, 130.f, 12.f, 13.f, 100.f, 110.f, 140.f, 150.f, 160.f, 170.f + }; + + for (size_t i = 0; i < expected_results.size(); ++i) { + ASSERT_EQ(expected_results[i], output_ptr[i]); + } +} + +TEST(scatter_update_gpu_fp32, static_input_padding) { + // Dictionary : 1x2x5x2 + // Indexes : 2x1x2x1 + // Updates : 1x2x2x1x2x2 + // Axis : 2 + // Output : 1x2x5x2 + // Input values in fp32 + + auto& engine = get_test_engine(); + + auto input1_layout = layout{ov::PartialShape{1, 2, 5, 2}, data_types::f32, format::bfyx}; + auto input2_layout = layout{ov::PartialShape{2, 1, 2, 1}, data_types::f32, format::bfyx}; + auto input3_layout = layout{ov::PartialShape{1, 2, 2, 1, 2, 2}, data_types::f32, format::bfwzyx, padding({0, 0, 0, 0, 0, 1}, {0, 0, 0, 0, 0, 0})}; + + auto input1 = engine.allocate_memory({{1, 2, 5, 2}, data_types::f32, format::bfyx}); // Dictionary + auto input2 = engine.allocate_memory({{2, 1, 2, 1}, data_types::f32, format::bfyx}); // Indices + auto input3 = engine.allocate_memory({{1, 2, 2, 1, 2, 2}, data_types::f32, format::bfwzyx, padding({0, 0, 0, 0, 0, 1}, {0, 0, 0, 0, 0, 0})}); // Updates + auto axis = 2; + + set_values(input1, { + 0.f, 1.f, 2.f, 3.f, 4.f, 5.f, 6.f, 7.f, 8.f, 9.f, + 10.f, 11.f, 12.f, 13.f, 14.f, 15.f, 16.f, 17.f, 18.f, 19.f + }); + + set_values(input2, { + 2.f, 0.f, + 3.f, 4.f + }); + + set_values(input3, { + 0.f, 20.f, 30.f, + 0.f, 40.f, 50.f, + 0.f, 60.f, 70.f, + 0.f, 80.f, 90.f, + 0.f, 100.f, 110.f, + 0.f, 120.f, 130.f, + 0.f, 140.f, 150.f, + 0.f, 160.f, 170.f + }); + + topology topology; + topology.add(input_layout("InputDictionary", input1_layout)); + topology.add(input_layout("InputText", input2_layout)); + topology.add(input_layout("InputUpdates", input3_layout)); + + topology.add(reorder("DictionaryReordered", input_info("InputDictionary"), format::bfyx, data_types::f32)); + topology.add(reorder("TextReordered", input_info("InputText"), format::bfyx, data_types::f32)); + topology.add(scatter_update("scatter_update", + input_info("DictionaryReordered"), + input_info("TextReordered"), + input_info("InputUpdates"), + axis) + ); + topology.add(reorder("out", input_info("scatter_update"), format::bfyx, data_types::f32)); + + ExecutionConfig config = get_test_default_config(engine); + config.set_property(ov::intel_gpu::allow_new_shape_infer(true)); + network network(engine, topology, config); + + network.set_input_data("InputDictionary", input1); + network.set_input_data("InputText", input2); + network.set_input_data("InputUpdates", input3); + + auto inst = network.get_primitive("scatter_update"); + auto impl = inst->get_impl(); + ASSERT_TRUE(impl != nullptr); + + auto outputs = network.execute(); + auto output = outputs.at("out").get_memory(); + cldnn::mem_lock output_ptr(output, get_test_stream()); + + std::vector expected_results = { + 40.f, 50.f, 2.f, 3.f, 20.f, 30.f, 60.f, 70.f, 80.f, 90.f, + 120.f, 130.f, 12.f, 13.f, 100.f, 110.f, 140.f, 150.f, 160.f, 170.f + }; + + for (size_t i = 0; i < expected_results.size(); ++i) { + ASSERT_EQ(expected_results[i], output_ptr[i]); + } +} + +TEST(scatter_update_gpu_fp32, dynamic_with_static_input_padding) { + // Dictionary : 1x2x5x2 + // Indexes : 2x1x2x1 + // Updates : 1x2x2x1x2x2 + // Axis : 2 + // Output : 1x2x5x2 + // Input values in fp32 + + auto& engine = get_test_engine(); + + auto input1_layout = layout{ ov::PartialShape::dynamic(4), data_types::f32, format::bfyx }; + auto input2_layout = layout{ ov::PartialShape::dynamic(4), data_types::f32, format::bfyx }; + auto input3_layout = layout{ov::PartialShape::dynamic(6), data_types::f32, format::bfwzyx, padding({0, 0, 0, 0, 0, 1}, {0, 0, 0, 0, 0, 0})}; + + auto input1 = engine.allocate_memory({{1, 2, 5, 2}, data_types::f32, format::bfyx}); // Dictionary + auto input2 = engine.allocate_memory({{2, 1, 2, 1}, data_types::f32, format::bfyx}); // Indices + auto input3 = engine.allocate_memory({{1, 2, 2, 1, 2, 2}, data_types::f32, format::bfwzyx, padding({0, 0, 0, 0, 0, 1}, {0, 0, 0, 0, 0, 0})}); // Updates + auto axis = 2; + + set_values(input1, { + 0.f, 1.f, 2.f, 3.f, 4.f, 5.f, 6.f, 7.f, 8.f, 9.f, + 10.f, 11.f, 12.f, 13.f, 14.f, 15.f, 16.f, 17.f, 18.f, 19.f + }); + + set_values(input2, { + 2.f, 0.f, + 3.f, 4.f + }); + + set_values(input3, { + 0.f, 20.f, 30.f, + 0.f, 40.f, 50.f, + 0.f, 60.f, 70.f, + 0.f, 80.f, 90.f, + 0.f, 100.f, 110.f, + 0.f, 120.f, 130.f, + 0.f, 140.f, 150.f, + 0.f, 160.f, 170.f + }); + + topology topology; + topology.add(input_layout("InputDictionary", input1_layout)); + topology.add(input_layout("InputText", input2_layout)); + topology.add(input_layout("InputUpdates", input3_layout)); + + topology.add(reorder("DictionaryReordered", input_info("InputDictionary"), format::bfyx, data_types::f32)); + topology.add(reorder("TextReordered", input_info("InputText"), format::bfyx, data_types::f32)); + topology.add(scatter_update("scatter_update", + input_info("DictionaryReordered"), + input_info("TextReordered"), + input_info("InputUpdates"), + axis) + ); + topology.add(reorder("out", input_info("scatter_update"), format::bfyx, data_types::f32)); + + ExecutionConfig config = get_test_default_config(engine); + config.set_property(ov::intel_gpu::allow_new_shape_infer(true)); + network network(engine, topology, config); + + network.set_input_data("InputDictionary", input1); + network.set_input_data("InputText", input2); + network.set_input_data("InputUpdates", input3); + + auto inst = network.get_primitive("scatter_update"); + auto impl = inst->get_impl(); + ASSERT_TRUE(impl != nullptr); + ASSERT_TRUE(impl->is_dynamic()); + + auto outputs = network.execute(); + auto output = outputs.at("out").get_memory(); + cldnn::mem_lock output_ptr(output, get_test_stream()); + + std::vector expected_results = { + 40.f, 50.f, 2.f, 3.f, 20.f, 30.f, 60.f, 70.f, 80.f, 90.f, + 120.f, 130.f, 12.f, 13.f, 100.f, 110.f, 140.f, 150.f, 160.f, 170.f + }; + + for (size_t i = 0; i < expected_results.size(); ++i) { + ASSERT_EQ(expected_results[i], output_ptr[i]); + } +} + +TEST(scatter_update_gpu_fp32, dynamic_with_dynamic_input_padding) { + // Dictionary : 1x2x5x2 + // Indexes : 2x1x2x1 + // Updates : 1x2x2x1x2x2 + // Axis : 2 + // Output : 1x2x5x2 + // Input values in fp32 + + auto& engine = get_test_engine(); + + auto input1_layout = layout{ ov::PartialShape::dynamic(4), data_types::f32, format::bfyx }; + auto input2_layout = layout{ ov::PartialShape::dynamic(4), data_types::f32, format::bfyx }; + padding::DynamicDimsMask mask; + mask.set(5); + auto input3_layout = layout{ov::PartialShape::dynamic(6), data_types::f32, format::bfwzyx, padding({}, mask)}; + + auto input1 = engine.allocate_memory({{1, 2, 5, 2}, data_types::f32, format::bfyx}); // Dictionary + auto input2 = engine.allocate_memory({{2, 1, 2, 1}, data_types::f32, format::bfyx}); // Indices + auto input3 = engine.allocate_memory({{1, 2, 2, 1, 2, 2}, data_types::f32, format::bfwzyx, padding({0, 0, 0, 0, 0, 1}, {0, 0, 0, 0, 0, 0}, mask)}); // Updates + auto axis = 2; + + set_values(input1, { + 0.f, 1.f, 2.f, 3.f, 4.f, 5.f, 6.f, 7.f, 8.f, 9.f, + 10.f, 11.f, 12.f, 13.f, 14.f, 15.f, 16.f, 17.f, 18.f, 19.f + }); + + set_values(input2, { + 2.f, 0.f, + 3.f, 4.f + }); + + set_values(input3, { + 0.f, 20.f, 30.f, + 0.f, 40.f, 50.f, + 0.f, 60.f, 70.f, + 0.f, 80.f, 90.f, + 0.f, 100.f, 110.f, + 0.f, 120.f, 130.f, + 0.f, 140.f, 150.f, + 0.f, 160.f, 170.f + }); + + topology topology; + topology.add(input_layout("InputDictionary", input1_layout)); + topology.add(input_layout("InputText", input2_layout)); + topology.add(input_layout("InputUpdates", input3_layout)); + + topology.add(reorder("DictionaryReordered", input_info("InputDictionary"), format::bfyx, data_types::f32)); + topology.add(reorder("TextReordered", input_info("InputText"), format::bfyx, data_types::f32)); + topology.add(scatter_update("scatter_update", + input_info("DictionaryReordered"), + input_info("TextReordered"), + input_info("InputUpdates"), + axis) + ); + topology.add(reorder("out", input_info("scatter_update"), format::bfyx, data_types::f32)); + + ExecutionConfig config = get_test_default_config(engine); + config.set_property(ov::intel_gpu::allow_new_shape_infer(true)); + network network(engine, topology, config); + + network.set_input_data("InputDictionary", input1); + network.set_input_data("InputText", input2); + network.set_input_data("InputUpdates", input3); + + auto inst = network.get_primitive("scatter_update"); + auto impl = inst->get_impl(); + ASSERT_TRUE(impl != nullptr); + ASSERT_TRUE(impl->is_dynamic()); + auto outputs = network.execute(); auto output = outputs.at("out").get_memory(); cldnn::mem_lock output_ptr(output, get_test_stream()); @@ -1753,7 +2000,7 @@ TEST(scatter_update_gpu_fp32, mixed_input_with_dynamic_static) { auto outputs = network.execute(); auto output = outputs.at("out").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = { 40.f, 50.f, 2.f, 3.f, 20.f, 30.f, 60.f, 70.f, 80.f, 90.f, @@ -1836,7 +2083,7 @@ TEST(scatter_update_cpu_impl_fp32, dynamic) { auto outputs = network.execute(); auto output = outputs.at("out").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = { 40.f, 50.f, 2.f, 3.f, 20.f, 30.f, 60.f, 70.f, 80.f, 90.f, @@ -1938,7 +2185,7 @@ TEST(scatter_update_gpu_fp32, output_padding) { auto outputs = network.execute(); auto output = outputs.at("out").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = { 30.f, 1.f, 20.f, 40.f, @@ -2028,7 +2275,7 @@ TEST(scatter_update_gpu_fp32, d8111_axisB_first_iteration_kernel_check) { auto output = outputs.at("out").get_memory(); ASSERT_TRUE(engine.is_the_same_buffer(*output_mem, *output)); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = { 1.0f, 2.0f, 3.0f, 4.0f, 9.0f, 6.0f, 7.0f, 8.0f diff --git a/src/plugins/intel_gpu/tests/unit/test_cases/sdpa_gpu_test.cpp b/src/plugins/intel_gpu/tests/unit/test_cases/sdpa_gpu_test.cpp index e747b4e56f79..d5dbe1a7bad7 100644 --- a/src/plugins/intel_gpu/tests/unit/test_cases/sdpa_gpu_test.cpp +++ b/src/plugins/intel_gpu/tests/unit/test_cases/sdpa_gpu_test.cpp @@ -267,7 +267,8 @@ INSTANTIATE_TEST_SUITE_P( sdpa_test_params{64, 10, 77, 77, 1, true}, // two ranks mask sdpa_test_params{64, 10, 77, 77, 1, false}, // two ranks mask sdpa_test_params{64, 32, 128, 128, 2, true, 0.125f, false, 0.0f}, // scale_val only - sdpa_test_params{64, 32, 128, 128, 2, false, 1.0f, true, 0.5f} // attn_mask only + sdpa_test_params{64, 32, 128, 128, 2, false, 1.0f, true, 0.5f}, // attn_mask only + sdpa_test_params{512, 8, 1, 1024, 2, true} ), sdpa_gpu_test::PrintToStringParamName ); @@ -360,4 +361,95 @@ TEST(sdpa_gpu_custom, single_token_cond_attn_mask_clamp) { ASSERT_NEAR(static_cast(ref_ptr[hs]), static_cast(output_ptr[hs]), 1e-2f); } } + +TEST(sdpa_gpu_custom, scalar_placeholder_mask_matches_scale_only) { + tests::random_generator rg; + rg.set_seed(GET_SUITE_NAME); + auto& engine = get_test_engine(); + + const int batch = 1; + const int seq_length_q = 4; + const int seq_length_kv = 6; + const int num_heads = 2; + const int head_size = 32; + const float scale_val = 0.35f; + + const layout q_layout({batch, seq_length_q, num_heads, head_size}, data_types::f16, format::bfyx); + const layout k_layout({batch, seq_length_kv, num_heads, head_size}, data_types::f16, format::bfyx); + const layout v_layout({batch, seq_length_kv, num_heads, head_size}, data_types::f16, format::bfyx); + const layout scalar_mask_layout{ov::PartialShape{}, data_types::f16, format::bfyx}; + + auto q_mem = engine.allocate_memory(q_layout); + auto k_mem = engine.allocate_memory(k_layout); + auto v_mem = engine.allocate_memory(v_layout); + auto scalar_mask_mem = engine.allocate_memory(scalar_mask_layout); + + auto fill_random = [&](const memory::ptr& mem) { + const auto shape = mem->get_layout().get_shape(); + const size_t elements_num = ov::shape_size(shape); + auto data = rg.generate_random_1d(elements_num, -1.0f, 1.0f); + set_values(mem, data); + }; + + fill_random(q_mem); + fill_random(k_mem); + fill_random(v_mem); + set_values(scalar_mask_mem, {ov::float16(1.0f)}); + + auto run_sdpa = [&](bool use_placeholder_mask) { + topology topo; + topo.add(input_layout("q", q_layout)); + topo.add(input_layout("k", k_layout)); + topo.add(input_layout("v", v_layout)); + std::vector inputs = {input_info("q"), input_info("k"), input_info("v")}; + if (use_placeholder_mask) { + topo.add(input_layout("mask", scalar_mask_layout)); + inputs.push_back(input_info("mask")); + } + + auto sdpa_prim = scaled_dot_product_attention("sdpa", + inputs, + false, + -1, + {0, 2, 1, 3}, + {0, 2, 1, 3}, + {0, 2, 1, 3}, + {0, 1, 2, 3}, + {}, + false); + sdpa_prim.scale_val = scale_val; + + topo.add(sdpa_prim); + topo.add(reorder("result", input_info("sdpa"), format::bfyx, data_types::f16)); + + ExecutionConfig cfg = get_test_default_config(engine); + cfg.set_property(ov::intel_gpu::allow_new_shape_infer(true)); + cfg.set_property(ov::intel_gpu::force_implementations(ov::intel_gpu::ImplForcingMap{{"sdpa", {format::type::bfyx, "sdpa_opt"}}})); + + auto network = get_network(engine, topo, cfg, get_test_stream_ptr(), false); + network->set_input_data("q", q_mem); + network->set_input_data("k", k_mem); + network->set_input_data("v", v_mem); + if (use_placeholder_mask) { + network->set_input_data("mask", scalar_mask_mem); + } + + return network->execute().at("result").get_memory(); + }; + + auto output_without_mask = run_sdpa(false); + auto output_with_placeholder_mask = run_sdpa(true); + + cldnn::mem_lock without_mask_ptr(output_without_mask, get_test_stream()); + cldnn::mem_lock with_placeholder_mask_ptr(output_with_placeholder_mask, get_test_stream()); + + ASSERT_EQ(without_mask_ptr.size(), with_placeholder_mask_ptr.size()); + for (size_t i = 0; i < without_mask_ptr.size(); ++i) { + ASSERT_NEAR(static_cast(without_mask_ptr[i]), static_cast(with_placeholder_mask_ptr[i]), 1e-3f) + << "Mismatch at index " << i + << ", Expected : " << static_cast(without_mask_ptr[i]) + << " actual : " << static_cast(with_placeholder_mask_ptr[i]) + << std::endl; + } +} } // namespace diff --git a/src/plugins/intel_gpu/tests/unit/test_cases/search_sorted_gpu_test.cpp b/src/plugins/intel_gpu/tests/unit/test_cases/search_sorted_gpu_test.cpp index c27eeff58b9c..478236efbf18 100644 --- a/src/plugins/intel_gpu/tests/unit/test_cases/search_sorted_gpu_test.cpp +++ b/src/plugins/intel_gpu/tests/unit/test_cases/search_sorted_gpu_test.cpp @@ -103,7 +103,7 @@ class search_sorted_test : public ::testing::TestWithParamexecute(); auto output = outputs.at("search_sorted").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); cldnn::mem_lock wanted_output_ptr(params.expectedOutput, get_test_stream()); ASSERT_EQ(output->get_layout(), params.expectedOutput->get_layout()); diff --git a/src/plugins/intel_gpu/tests/unit/test_cases/select_gpu_test.cpp b/src/plugins/intel_gpu/tests/unit/test_cases/select_gpu_test.cpp index 1e4a9616b842..0d4f87c2e8ee 100644 --- a/src/plugins/intel_gpu/tests/unit/test_cases/select_gpu_test.cpp +++ b/src/plugins/intel_gpu/tests/unit/test_cases/select_gpu_test.cpp @@ -59,7 +59,7 @@ void test_select_basic(bool is_caching_test) { 15.f, 0.5f, 8.f, 12.f, 4.f, 6.5f, 8.f, -2.5f }; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (int i = 0; i < 16; i++) { @@ -117,7 +117,7 @@ TEST(select_gpu_f32, select_basic_negative) { 15.f, 0.5f, 8.f, 12.f, 4.f, 6.5f, 8.f, -2.5f }; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (int i = 0; i < 16; i++) { @@ -1209,7 +1209,7 @@ TEST(select_gpu_f32, select_basic_comma) { 15.f, 0.5f, 8.f, 12.f, 4.f, 6.5f, 8.f, -2.5f }; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (int i = 0; i < 16; i++) { @@ -1310,7 +1310,7 @@ TEST(select_gpu_f32, select_basic_byxf) { 15.f, 0.5f, 8.f, 12.f, 4.f, 6.5f, 8.f, -2.5f }; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (int i = 0; i < 16; i++) { @@ -1364,7 +1364,7 @@ TEST(select_gpu_f32, select_basic_mask_f16) { 15.f, 0.5f, 8.f, 12.f, 4.f, 6.5f, 8.f, -2.5f }; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (int i = 0; i < 16; i++) { @@ -1418,7 +1418,7 @@ TEST(select_gpu_f32, select_basic_mask_i8) { 15.f, 0.5f, 8.f, 12.f, 4.f, 6.5f, 8.f, -2.5f }; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (int i = 0; i < 16; i++) { @@ -1472,7 +1472,7 @@ TEST(select_gpu_f32, select_basic_mask_u8) { 15.f, 0.5f, 8.f, 12.f, 4.f, 6.5f, 8.f, -2.5f }; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (int i = 0; i < 16; i++) { @@ -1518,7 +1518,7 @@ TEST(select_gpu_f32, select_basic_1x1x2x2) { 0.5f, 2.5f, 2.f, 0.f }; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (int i = 0; i < 4; i++) { @@ -1568,7 +1568,7 @@ TEST(select_gpu_f32, select_basic_bfyx_1x1x2x2) { 2.f, 0.f }; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (int i = 0; i < 4; i++) { @@ -1618,7 +1618,7 @@ TEST(select_gpu_f32, select_basic_byxf_1x1x2x2) { 2.f, 0.f }; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (int i = 0; i < 4; i++) { @@ -1670,7 +1670,7 @@ void test_f16_select_basic_1x1x2x2(bool is_caching_test) { 2, 0 }; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (int i = 0; i < 4; i++) { @@ -1724,7 +1724,7 @@ TEST(select_gpu_f16, select_basic_mask_f32_1x1x2x2) { 2, 0 }; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (int i = 0; i < 4; i++) { @@ -1774,7 +1774,7 @@ TEST(select_gpu_f16, select_basic_mask_i8_1x1x2x2) { 2, 0 }; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (int i = 0; i < 4; i++) { @@ -1824,7 +1824,7 @@ TEST(select_gpu_f16, select_basic_mask_u8_1x1x2x2) { 2, 0 }; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (int i = 0; i < 4; i++) { @@ -1876,7 +1876,7 @@ void test_i8_select_basic_1x1x2x2(bool is_caching_test) { 2, 0 }; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (int i = 0; i < 4; i++) { @@ -1930,7 +1930,7 @@ TEST(select_gpu_i8, select_basic_mask_f32_1x1x2x2) { 2, 0 }; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (int i = 0; i < 4; i++) { @@ -1980,7 +1980,7 @@ TEST(select_gpu_i8, select_basic_mask_f16_1x1x2x2) { 2, 0 }; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (int i = 0; i < 4; i++) { @@ -2030,7 +2030,7 @@ TEST(select_gpu_i8, select_basic_mask_u8_1x1x2x2) { 2, 0 }; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (int i = 0; i < 4; i++) { @@ -2082,7 +2082,7 @@ void test_u8_select_basic_1x1x2x2(bool is_caching_test) { 255, 0 }; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (int i = 0; i < 4; i++) { @@ -2136,7 +2136,7 @@ TEST(select_gpu_u8, select_basic_mask_f32_1x1x2x2) { 255, 0 }; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (int i = 0; i < 4; i++) { @@ -2186,7 +2186,7 @@ TEST(select_gpu_u8, select_basic_mask_f16_1x1x2x2) { 255, 0 }; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (int i = 0; i < 4; i++) { @@ -2236,7 +2236,7 @@ TEST(select_gpu_u8, select_basic_mask_i8_1x1x2x2) { 255, 0 }; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (int i = 0; i < 4; i++) { diff --git a/src/plugins/intel_gpu/tests/unit/test_cases/set_output_memory_gpu_test.cpp b/src/plugins/intel_gpu/tests/unit/test_cases/set_output_memory_gpu_test.cpp index bd845c23682f..c1b3870da6b4 100644 --- a/src/plugins/intel_gpu/tests/unit/test_cases/set_output_memory_gpu_test.cpp +++ b/src/plugins/intel_gpu/tests/unit/test_cases/set_output_memory_gpu_test.cpp @@ -56,7 +56,7 @@ void test_basic(bool is_caching_test) { auto output = outputs.at("reorder").get_memory(); ASSERT_TRUE(engine.is_the_same_buffer(*output_mem, *output)); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (size_t i = 0; i < inputVals.size(); ++i) { ASSERT_TRUE(are_equal(inputVals[i], output_ptr[i])) << i; @@ -106,8 +106,8 @@ TEST(set_output_memory_gpu, basic_const) { auto output_const = outputs.at("reorder_const").get_memory(); ASSERT_TRUE(engine.is_the_same_buffer(*output_mem, *output_dyn)); - cldnn::mem_lock output_dyn_ptr(output_dyn, get_test_stream()); - cldnn::mem_lock output_const_ptr(output_const, get_test_stream()); + cldnn::mem_lock output_dyn_ptr(output_dyn, get_test_stream()); + cldnn::mem_lock output_const_ptr(output_const, get_test_stream()); for (size_t i = 0; i < inputVals.size(); ++i) { ASSERT_TRUE(are_equal(inputVals[i], output_dyn_ptr[i])) << i; @@ -156,7 +156,7 @@ TEST(set_output_memory_gpu, basic_mutable) { ASSERT_TRUE(engine.is_the_same_buffer(*output_mem, *output_dyn)); ASSERT_TRUE(engine.is_the_same_buffer(*output_mutable_mem, *output_mutable)); - cldnn::mem_lock output_dyn_ptr(output_dyn, get_test_stream()); + cldnn::mem_lock output_dyn_ptr(output_dyn, get_test_stream()); cldnn::mem_lock output_mutable_mem_ptr(output_mutable_mem, get_test_stream()); for (size_t i = 0; i < inputVals.size(); ++i) { @@ -205,7 +205,7 @@ TEST(set_output_memory_gpu, top_k1) { auto output = outputs.at("reorder").get_memory(); ASSERT_TRUE(engine.is_the_same_buffer(*output_mem, *output)); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); cldnn::mem_lock output_mem_ptr(output_mem, get_test_stream()); for (size_t i = 0; i < output_ptr.size(); ++i) { @@ -251,7 +251,7 @@ TEST(set_output_memory_gpu, top_k2) { auto output = outputs.at("reorder").get_memory(); ASSERT_TRUE(engine.is_the_same_buffer(*second_output_mem, *output)); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); cldnn::mem_lock output_mem_ptr(second_output_mem, get_test_stream()); for (size_t i = 0; i < output_ptr.size(); ++i) { diff --git a/src/plugins/intel_gpu/tests/unit/test_cases/shape_of_gpu_test.cpp b/src/plugins/intel_gpu/tests/unit/test_cases/shape_of_gpu_test.cpp index fdca64ecd2b4..86d7bd5ff449 100644 --- a/src/plugins/intel_gpu/tests/unit/test_cases/shape_of_gpu_test.cpp +++ b/src/plugins/intel_gpu/tests/unit/test_cases/shape_of_gpu_test.cpp @@ -35,7 +35,7 @@ TEST(shape_of_gpu, bfyx) { auto outputs = network.execute(); auto output = outputs.at("shape_of").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = {1, 2, 3, 3}; @@ -60,7 +60,7 @@ TEST(shape_of_gpu, bfyx_i64) { auto outputs = network.execute(); auto output = outputs.at("shape_of").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = {1, 2, 3, 3}; @@ -89,7 +89,7 @@ void shape_of_cpu_impl_bfyx_i64(bool disable_usm) { auto outputs = network.execute(); auto output = outputs.at("shape_of").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = {1, 2, 3, 3}; @@ -122,7 +122,7 @@ TEST(shape_of_gpu, yxfb) { auto outputs = network.execute(); auto output = outputs.at("shape_of").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = {1, 2, 3, 3}; @@ -147,7 +147,7 @@ TEST(shape_of_gpu, bfzyx) { auto outputs = network.execute(); auto output = outputs.at("shape_of").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = {1, 2, 4, 3, 3}; @@ -184,7 +184,7 @@ TEST(shape_of_gpu, dynamic) { auto outputs = network.execute(); auto output = outputs.at("shape_of").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = {1, 2, 3, 4}; @@ -199,7 +199,7 @@ TEST(shape_of_gpu, dynamic) { auto outputs = network.execute(); auto output = outputs.at("shape_of").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = {4, 3, 2, 1}; @@ -239,7 +239,7 @@ TEST(shape_of_gpu, shape_infer_optimization_dynamic) { auto outputs = network.execute(); auto output = outputs.at("shape_of").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (size_t i = 0; i < input.size(); ++i) { ASSERT_EQ(input[i], output_ptr[i]); @@ -308,7 +308,7 @@ TEST_P(smoke_shape_of_test, basic) { auto outputs = network.execute(); auto output = outputs.at("shape_of").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (size_t i = 0; i < input.size(); ++i) { ASSERT_EQ(input[i], output_ptr[i]); diff --git a/src/plugins/intel_gpu/tests/unit/test_cases/shuffle_channels_test.cpp b/src/plugins/intel_gpu/tests/unit/test_cases/shuffle_channels_test.cpp index 1e2285b1053f..5ec85480da21 100644 --- a/src/plugins/intel_gpu/tests/unit/test_cases/shuffle_channels_test.cpp +++ b/src/plugins/intel_gpu/tests/unit/test_cases/shuffle_channels_test.cpp @@ -42,7 +42,7 @@ void test_d1_15_2_2_ax1_g5(bool is_caching_test) { auto outputs = network->execute(); auto output = outputs.at("shuffle_channels").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = { 0.f, 1.f, 2.f, 3.f, 12.f, 13.f, 14.f, 15.f, 24.f, 25.f, 26.f, 27.f, 36.f, 37.f, 38.f, 39.f, 48.f, 49.f, 50.f, 51.f, @@ -88,7 +88,7 @@ TEST(shuffle_channels_fp32_gpu, d1_15_2_2_axm3_g5) { auto outputs = network.execute(); auto output = outputs.at("shuffle_channels").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = { 0.f, 1.f, 2.f, 3.f, 12.f, 13.f, 14.f, 15.f, 24.f, 25.f, 26.f, 27.f, 36.f, 37.f, 38.f, 39.f, 48.f, 49.f, 50.f, 51.f, @@ -130,7 +130,7 @@ TEST(shuffle_channels_fp32_gpu, d15_2_2_ax0_g5) { auto outputs = network.execute(); auto output = outputs.at("shuffle_channels").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = { 0.f, 1.f, 2.f, 3.f, 12.f, 13.f, 14.f, 15.f, 24.f, 25.f, 26.f, 27.f, 36.f, 37.f, 38.f, 39.f, 48.f, 49.f, 50.f, 51.f, @@ -172,7 +172,7 @@ TEST(shuffle_channels_fp32_gpu, d15_2_2_axm4_g5) { auto outputs = network.execute(); auto output = outputs.at("shuffle_channels").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = { 0.f, 1.f, 2.f, 3.f, 12.f, 13.f, 14.f, 15.f, 24.f, 25.f, 26.f, 27.f, 36.f, 37.f, 38.f, 39.f, 48.f, 49.f, 50.f, 51.f, @@ -211,7 +211,7 @@ TEST(shuffle_channels_fp32_gpu, d2_2_6_axm2_g3) { auto outputs = network.execute(); auto output = outputs.at("shuffle_channels").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = { 0.f, 2.f, 4.f, 1.f, 3.f, 5.f, 6.f, 8.f, 10.f, 7.f, 9.f, 11.f, @@ -249,7 +249,7 @@ TEST(shuffle_channels_fp32_gpu, d2_6_2_axm3_g3) { auto outputs = network.execute(); auto output = outputs.at("shuffle_channels").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = { 0.f, 1.f, 4.f, 5.f, 8.f, 9.f, 2.f, 3.f, 6.f, 7.f, 10.f, 11.f, @@ -287,7 +287,7 @@ TEST(shuffle_channels_fp32_gpu, d2_2_6_axm2_g2) { auto outputs = network.execute(); auto output = outputs.at("shuffle_channels").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = { 0.f, 3.f, 1.f, 4.f, 2.f, 5.f, 6.f, 9.f, 7.f, 10.f, 8.f, 11.f, @@ -325,7 +325,7 @@ TEST(shuffle_channels_fp32_gpu, d2_6_2_axm3_g2) { auto outputs = network.execute(); auto output = outputs.at("shuffle_channels").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = { 0.f, 1.f, 6.f, 7.f, 2.f, 3.f, 8.f, 9.f, 4.f, 5.f, 10.f, 11.f, @@ -361,7 +361,7 @@ TEST(shuffle_channels_fp32_gpu, d6_axm0_g2) { auto outputs = network.execute(); auto output = outputs.at("shuffle_channels").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = { 0.f, 3.f, 1.f, 4.f, 2.f, 5.f diff --git a/src/plugins/intel_gpu/tests/unit/test_cases/slice_gpu_test.cpp b/src/plugins/intel_gpu/tests/unit/test_cases/slice_gpu_test.cpp index 54247966b886..19bbff588698 100644 --- a/src/plugins/intel_gpu/tests/unit/test_cases/slice_gpu_test.cpp +++ b/src/plugins/intel_gpu/tests/unit/test_cases/slice_gpu_test.cpp @@ -159,7 +159,7 @@ class SliceTest : public ::testing::Test { auto output = outputs.at("slice").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); cldnn::mem_lock wanted_output_ptr(params.wanted_output, get_test_stream()); ASSERT_EQ(output->get_layout(), params.wanted_output->get_layout()); diff --git a/src/plugins/intel_gpu/tests/unit/test_cases/slice_scatter_gpu_test.cpp b/src/plugins/intel_gpu/tests/unit/test_cases/slice_scatter_gpu_test.cpp index 49d154973155..1c948c88e3c6 100644 --- a/src/plugins/intel_gpu/tests/unit/test_cases/slice_scatter_gpu_test.cpp +++ b/src/plugins/intel_gpu/tests/unit/test_cases/slice_scatter_gpu_test.cpp @@ -414,7 +414,7 @@ class SliceScatterTest : public ::testing::Test { auto output = outputs.at("slice_scatter").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); cldnn::mem_lock wanted_output_ptr(params.wanted_output, get_test_stream()); ASSERT_EQ(output->get_layout().get_shape(), params.wanted_output->get_layout().get_shape()); @@ -720,7 +720,7 @@ class SliceScatterPerfTest : public ::testing::Test { auto outputs = network->execute(); // Force sync auto out = outputs.at("slice_scatter").get_memory(); - cldnn::mem_lock lock(out, get_test_stream()); + cldnn::mem_lock lock(out, get_test_stream()); (void)lock[0]; } @@ -731,7 +731,7 @@ class SliceScatterPerfTest : public ::testing::Test { network->set_input_data("updates", updates_mem); auto outputs = network->execute(); auto out = outputs.at("slice_scatter").get_memory(); - cldnn::mem_lock lock(out, get_test_stream()); + cldnn::mem_lock lock(out, get_test_stream()); (void)lock[0]; } auto t1 = std::chrono::high_resolution_clock::now(); diff --git a/src/plugins/intel_gpu/tests/unit/test_cases/softmax_gpu_test.cpp b/src/plugins/intel_gpu/tests/unit/test_cases/softmax_gpu_test.cpp index 3deac638e316..dc788abcefc0 100644 --- a/src/plugins/intel_gpu/tests/unit/test_cases/softmax_gpu_test.cpp +++ b/src/plugins/intel_gpu/tests/unit/test_cases/softmax_gpu_test.cpp @@ -86,7 +86,7 @@ class softmax_gpu_xb_f32_test_fixture: public ::testing::Test { auto output_prim = outputs.begin()->second.get_memory(); - cldnn::mem_lock output_ptr(output_prim, get_test_stream()); + cldnn::mem_lock output_ptr(output_prim, get_test_stream()); for (uint32_t i = 0; i < out_size; i++) { out_buffer[i] = output_ptr[i]; } @@ -120,7 +120,7 @@ class softmax_gpu_xb_f32_test_fixture: public ::testing::Test { auto output_prim = outputs.begin()->second.get_memory(); - cldnn::mem_lock output_ptr(output_prim, get_test_stream()); + cldnn::mem_lock output_ptr(output_prim, get_test_stream()); for (uint32_t i = 0; i < out_size; i++) { out_buffer[i] = output_ptr[i]; } @@ -178,7 +178,7 @@ class softmax_gpu_xb_f32_test_fixture: public ::testing::Test { auto output_prim = outputs.begin()->second.get_memory(); - cldnn::mem_lock output_ptr(output_prim, get_test_stream()); + cldnn::mem_lock output_ptr(output_prim, get_test_stream()); for (uint32_t i = 0; i < out_size; i++) { out_buffer[i] = output_ptr[i]; } @@ -250,7 +250,7 @@ TEST(softmax_gpu_bfyx_f32, normalize_y) { ASSERT_EQ(outputs.begin()->first, "softmax"); auto output = outputs.at("softmax").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); float out_buffer[buf_size]; for (uint32_t i = 0; i < buf_size; i++) { out_buffer[i] = output_ptr[i]; @@ -330,10 +330,7 @@ TEST(softmax_gpu_bfyx_f32, normalize_f) { ASSERT_EQ(outputs.begin()->first, "softmax"); auto output = outputs.at("softmax").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); - for (size_t i = 0; i < output->count(); i++) { - std::cerr << "i = " << i << " v = " << output_ptr[i] << std::endl; - } + cldnn::mem_lock output_ptr(output, get_test_stream()); float out_buffer[buf_size]; for (uint32_t i = 0; i < buf_size; i++) { @@ -415,7 +412,7 @@ TEST(softmax_gpu_bfzyx_f32, normalize_z) { ASSERT_EQ(outputs.begin()->first, "softmax"); auto output = outputs.at("softmax").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); float out_buffer[buf_size]; for (uint32_t i = 0; i < buf_size; i++) { out_buffer[i] = output_ptr[i]; @@ -498,7 +495,7 @@ TEST(softmax_gpu_bfyx_f32, normalize_b) { ASSERT_EQ(outputs.begin()->first, "softmax"); auto output = outputs.at("softmax").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); float out_buffer[buf_size]; for (uint32_t i = 0; i < buf_size; i++) { out_buffer[i] = output_ptr[i]; @@ -951,7 +948,7 @@ struct softmax_gpu_formats_test network->set_input_data("input", input); const auto outputs = network->execute(); const auto output = outputs.at("softmax").get_memory(); - const cldnn::mem_lock output_ptr(output, get_test_stream()); + const cldnn::mem_lock output_ptr(output, get_test_stream()); ASSERT_EQ(params.input_tensor.count(), output_ptr.size()); for (uint32_t i = 0; i < output_ptr.size(); i++) { @@ -1061,7 +1058,7 @@ TEST(softmax_gpu_bfyx_f32, normalize_f_dynamic) { ASSERT_EQ(outputs.begin()->first, "softmax"); auto output = outputs.at("softmax").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); float out_buffer[buf_size]; for (uint32_t i = 0; i < buf_size; i++) { out_buffer[i] = output_ptr[i]; @@ -1166,7 +1163,7 @@ TEST(softmax_gpu_bfyx_f32, bf_opt_normalize_f_dynamic) { ASSERT_EQ(outputs.begin()->first, "softmax"); auto output = outputs.at("softmax").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); float out_buffer[buf_size]; for (uint32_t i = 0; i < buf_size; i++) { out_buffer[i] = output_ptr[i]; diff --git a/src/plugins/intel_gpu/tests/unit/test_cases/space_to_batch_gpu_test.cpp b/src/plugins/intel_gpu/tests/unit/test_cases/space_to_batch_gpu_test.cpp index 6da63cea4a77..24d0a84ca584 100644 --- a/src/plugins/intel_gpu/tests/unit/test_cases/space_to_batch_gpu_test.cpp +++ b/src/plugins/intel_gpu/tests/unit/test_cases/space_to_batch_gpu_test.cpp @@ -47,7 +47,7 @@ class space_to_batch_fp16_gpu: public ::testing::Test { auto outputs = network->execute(); auto output = outputs.at("space_to_batch").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = { 0.f, 1.f, 2.f, 3.f, 4.f, 5.f, 6.f, 7.f @@ -91,7 +91,7 @@ class space_to_batch_fp16_gpu: public ::testing::Test { auto outputs = network->execute(); auto output = outputs.at("space_to_batch").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = { 0.f, 0.f, 0.f, 1.f, 4.f, 5.f, @@ -137,7 +137,7 @@ class space_to_batch_fp16_gpu: public ::testing::Test { auto outputs = network->execute(); auto output = outputs.at("space_to_batch").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = { 0.f, 2.f, 0.f, 8.f, 0.f, 3.f, 0.f, 9.f, @@ -183,7 +183,7 @@ class space_to_batch_fp16_gpu: public ::testing::Test { auto outputs = network->execute(); auto output = outputs.at("space_to_batch").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = { 0.f, 2.f, 0.f, 3.f, 0.f, 4.f, 1.f, 5.f, @@ -231,7 +231,7 @@ class space_to_batch_fp16_gpu: public ::testing::Test { auto outputs = network->execute(); auto output = outputs.at("space_to_batch").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = { 0.f, 0.f, 0.f, 0.f, 0.f, 0.f, 0.f, 0.f, @@ -284,7 +284,7 @@ class space_to_batch_fp16_gpu: public ::testing::Test { auto outputs = network->execute(); auto output = outputs.at("stb_to_bfyx").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = { 0.f, 0.f, 0.f, 0.f, 0.f, 0.f, 0.f, 0.f, @@ -337,7 +337,7 @@ class space_to_batch_fp16_gpu: public ::testing::Test { auto outputs = network->execute(); auto output = outputs.at("stb_to_bfyx").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = { 0.f, 0.f, 0.f, 0.f, 0.f, 0.f, 0.f, 0.f, 0.f, 0.f, @@ -413,7 +413,7 @@ class space_to_batch_fp32_gpu: public ::testing::Test { auto outputs = network->execute(); auto output = outputs.at("space_to_batch").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = { 0.f, 1.f, 2.f, 3.f, 4.f, 5.f, 6.f, 7.f @@ -457,7 +457,7 @@ class space_to_batch_fp32_gpu: public ::testing::Test { auto outputs = network->execute(); auto output = outputs.at("space_to_batch").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = { 0.f, 0.f, 0.f, 1.f, 4.f, 5.f, @@ -503,7 +503,7 @@ class space_to_batch_fp32_gpu: public ::testing::Test { auto outputs = network->execute(); auto output = outputs.at("space_to_batch").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = { 0.f, 2.f, 0.f, 8.f, 0.f, 3.f, 0.f, 9.f, @@ -549,7 +549,7 @@ class space_to_batch_fp32_gpu: public ::testing::Test { auto outputs = network->execute(); auto output = outputs.at("space_to_batch").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = { 0.f, 2.f, 0.f, 3.f, 0.f, 4.f, 1.f, 5.f, @@ -595,7 +595,7 @@ class space_to_batch_fp32_gpu: public ::testing::Test { auto outputs = network->execute(); auto output = outputs.at("space_to_batch").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = { 0.f, 0.f, 0.f, 0.f, 0.f, 0.f, 0.f, 0.f, @@ -652,7 +652,7 @@ class space_to_batch_fp32_gpu: public ::testing::Test { auto outputs = network->execute(); auto output = outputs.at("stb_to_bfyx").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = { 0.f, 1.f, 16.f, 17.f, 32.f, 33.f, 48.f, 49.f, @@ -706,7 +706,7 @@ class space_to_batch_fp32_gpu: public ::testing::Test { auto outputs = network->execute(); auto output = outputs.at("stb_to_bfyx").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = { 0.f, 1.f, 0.f, 4.f, 0.f, 19.f, 0.f, 22.f, diff --git a/src/plugins/intel_gpu/tests/unit/test_cases/space_to_depth_gpu_test.cpp b/src/plugins/intel_gpu/tests/unit/test_cases/space_to_depth_gpu_test.cpp index ac7e261465ad..6b00edb3d3fd 100644 --- a/src/plugins/intel_gpu/tests/unit/test_cases/space_to_depth_gpu_test.cpp +++ b/src/plugins/intel_gpu/tests/unit/test_cases/space_to_depth_gpu_test.cpp @@ -44,7 +44,7 @@ class space_to_depth_fp16_gpu: public ::testing::Test { auto outputs = network->execute(); auto output = outputs.at("space_to_depth").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = { 0.f, 1.f, 2.f, 3.f @@ -86,7 +86,7 @@ class space_to_depth_fp16_gpu: public ::testing::Test { auto outputs = network->execute(); auto output = outputs.at("space_to_depth").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = { 0.0f, 4.0f, 1.0f, 5.0f, 2.0f, 6.0f, 3.0f, 7.0f @@ -134,7 +134,7 @@ class space_to_depth_fp16_gpu: public ::testing::Test { auto outputs = network->execute(); auto output = outputs.at("space_to_depth").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = { 0.0f, 2.0f, 8.0f, 10.0f, 16.0f, 18.0f, @@ -196,7 +196,7 @@ class space_to_depth_fp16_gpu: public ::testing::Test { auto outputs = network->execute(); auto output = outputs.at("space_to_depth").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = { 0.0f, 3.0f, 6.0f, 27.0f, 30.0f, 33.0f, 54.0f, 57.0f, 60.0f, 1.0f, @@ -244,7 +244,7 @@ class space_to_depth_fp16_gpu: public ::testing::Test { auto outputs = network->execute(); auto output = outputs.at("space_to_depth").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = { 0.f, 1.f, 2.f, 3.f @@ -286,7 +286,7 @@ class space_to_depth_fp16_gpu: public ::testing::Test { auto outputs = network->execute(); auto output = outputs.at("space_to_depth").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = { 0.0f, 4.0f, 1.0f, 5.0f, 2.0f, 6.0f, 3.0f, 7.0f @@ -334,7 +334,7 @@ class space_to_depth_fp16_gpu: public ::testing::Test { auto outputs = network->execute(); auto output = outputs.at("space_to_depth").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = { 0.0f, 2.0f, 8.0f, 10.0f, 16.0f, 18.0f, @@ -396,7 +396,7 @@ class space_to_depth_fp16_gpu: public ::testing::Test { auto outputs = network->execute(); auto output = outputs.at("space_to_depth").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = { 0.0f, 3.0f, 6.0f, 27.0f, 30.0f, 33.0f, 54.0f, 57.0f, 60.0f, 1.0f, @@ -446,7 +446,7 @@ class space_to_depth_fp32_gpu: public ::testing::Test { auto outputs = network->execute(); auto output = outputs.at("space_to_depth").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = { 0.f, 1.f, 2.f, 3.f @@ -485,7 +485,7 @@ class space_to_depth_fp32_gpu: public ::testing::Test { auto outputs = network->execute(); auto output = outputs.at("space_to_depth").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = { 0.0f, 4.0f, 1.0f, 5.0f, 2.0f, 6.0f, 3.0f, 7.0f @@ -533,7 +533,7 @@ class space_to_depth_fp32_gpu: public ::testing::Test { auto outputs = network->execute(); auto output = outputs.at("space_to_depth").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = { 0.0f, 2.0f, 8.0f, 10.0f, 16.0f, 18.0f, @@ -587,7 +587,7 @@ class space_to_depth_fp32_gpu: public ::testing::Test { auto outputs = network->execute(); auto output = outputs.at("space_to_depth").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = { 0.0f, 3.0f, 6.0f, 27.0f, 30.0f, 33.0f, 54.0f, 57.0f, 60.0f, 1.0f, @@ -634,7 +634,7 @@ class space_to_depth_fp32_gpu: public ::testing::Test { auto outputs = network->execute(); auto output = outputs.at("space_to_depth").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = { 0.f, 1.f, 2.f, 3.f @@ -673,7 +673,7 @@ class space_to_depth_fp32_gpu: public ::testing::Test { auto outputs = network->execute(); auto output = outputs.at("space_to_depth").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = { 0.0f, 4.0f, 1.0f, 5.0f, 2.0f, 6.0f, 3.0f, 7.0f @@ -721,7 +721,7 @@ class space_to_depth_fp32_gpu: public ::testing::Test { auto outputs = network->execute(); auto output = outputs.at("space_to_depth").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = { 0.0f, 2.0f, 8.0f, 10.0f, 16.0f, 18.0f, @@ -775,7 +775,7 @@ class space_to_depth_fp32_gpu: public ::testing::Test { auto outputs = network->execute(); auto output = outputs.at("space_to_depth").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = { 0.0f, 3.0f, 6.0f, 27.0f, 30.0f, 33.0f, 54.0f, 57.0f, 60.0f, 1.0f, @@ -830,7 +830,7 @@ class space_to_depth_fp32_gpu: public ::testing::Test { auto outputs = network->execute(); auto output = outputs.at("reorder_out").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = { 0.0f, 3.0f, 6.0f, 27.0f, 30.0f, 33.0f, 54.0f, 57.0f, 60.0f, 1.0f, @@ -885,7 +885,7 @@ class space_to_depth_fp32_gpu: public ::testing::Test { auto outputs = network->execute(); auto output = outputs.at("reorder_out").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector expected_results = { 0.0f, 3.0f, 6.0f, 27.0f, 30.0f, 33.0f, 54.0f, 57.0f, 60.0f, 1.0f, diff --git a/src/plugins/intel_gpu/tests/unit/test_cases/streams_test.cpp b/src/plugins/intel_gpu/tests/unit/test_cases/streams_test.cpp index 54eebe68534d..d42430266e01 100644 --- a/src/plugins/intel_gpu/tests/unit/test_cases/streams_test.cpp +++ b/src/plugins/intel_gpu/tests/unit/test_cases/streams_test.cpp @@ -40,7 +40,7 @@ class gpu_streams: public ::testing::Test { auto output_memory = outputs.at("relu").get_memory(); auto output_layout = output_memory->get_layout(); - cldnn::mem_lock output_ptr(output_memory, get_test_stream()); + cldnn::mem_lock output_ptr(output_memory, get_test_stream()); int y_size = output_layout.spatial(1); int x_size = output_layout.spatial(0); @@ -118,8 +118,8 @@ class gpu_streams: public ::testing::Test { auto output_memory0 = outputs0.at("conv").get_memory(); auto output_memory1 = outputs1.at("conv").get_memory(); auto output_layout = output_memory0->get_layout(); - cldnn::mem_lock output_ptr0(output_memory0, get_test_stream()); - cldnn::mem_lock output_ptr1(output_memory1, get_test_stream()); + cldnn::mem_lock output_ptr0(output_memory0, get_test_stream()); + cldnn::mem_lock output_ptr1(output_memory1, get_test_stream()); auto wmem0 = network0->get_output_memory("weights"); auto wmem1 = network1->get_output_memory("weights"); @@ -206,8 +206,8 @@ class gpu_streams: public ::testing::Test { auto output_memory0 = outputs0.at("conv").get_memory(); auto output_memory1 = outputs1.at("conv").get_memory(); auto output_layout = output_memory0->get_layout(); - cldnn::mem_lock output_ptr0(output_memory0, get_test_stream()); - cldnn::mem_lock output_ptr1(output_memory1, get_test_stream()); + cldnn::mem_lock output_ptr0(output_memory0, get_test_stream()); + cldnn::mem_lock output_ptr1(output_memory1, get_test_stream()); auto wmem0 = network0->get_output_memory("weights"); auto wmem1 = network1->get_output_memory("weights"); diff --git a/src/plugins/intel_gpu/tests/unit/test_cases/strided_slice_gpu_test.cpp b/src/plugins/intel_gpu/tests/unit/test_cases/strided_slice_gpu_test.cpp index 079c1d22c575..ef8bbe37d76d 100644 --- a/src/plugins/intel_gpu/tests/unit/test_cases/strided_slice_gpu_test.cpp +++ b/src/plugins/intel_gpu/tests/unit/test_cases/strided_slice_gpu_test.cpp @@ -3,6 +3,8 @@ // #include "test_utils.h" +#include "program_wrapper.h" +#include "pass_manager.h" #include #include @@ -64,7 +66,7 @@ class strided_slice_gpu: public ::testing::Test { std::vector answers = { 0.f, 1.f, 2.f, 3.f, 4.f, 5.f, 6.f, 7.f, 8.f, 9.f, 10.f, 11.f, 12.f, 13.f, 14.f, 15.f }; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); ASSERT_EQ(output_ptr.size(), answers.size()); for (size_t i = 0; i < answers.size(); ++i) @@ -109,7 +111,7 @@ class strided_slice_gpu: public ::testing::Test { std::vector answers = { 0.f, 1.f, 2.f, 3.f, 4.f, 5.f, 6.f, 7.f, 8.f, 9.f, 10.f, 11.f, 12.f, 13.f, 14.f, 15.f }; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); ASSERT_EQ(output_ptr.size(), answers.size()); for (size_t i = 0; i < answers.size(); ++i) @@ -272,7 +274,7 @@ class strided_slice_gpu: public ::testing::Test { std::vector answers = { 0.f, 1.f, 2.f, 3.f, 4.f, 5.f, 6.f, 7.f, 8.f, 9.f, 10.f, 11.f, 12.f, 13.f, 14.f, 15.f }; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); ASSERT_EQ(output_ptr.size(), answers.size()); for (size_t i = 0; i < answers.size(); ++i) @@ -319,7 +321,7 @@ class strided_slice_gpu: public ::testing::Test { 9.0f, 10.0f, 11.0f, 12.0f, 13.0f, 14.0f, 15.0f }; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); ASSERT_EQ(output_ptr.size(), answers.size()); for (size_t i = 0; i < answers.size(); ++i) @@ -367,7 +369,7 @@ class strided_slice_gpu: public ::testing::Test { std::vector answers = { 15.f }; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); ASSERT_EQ(output_ptr.size(), answers.size()); for (size_t i = 0; i < answers.size(); ++i) @@ -418,7 +420,7 @@ class strided_slice_gpu: public ::testing::Test { 24.f, 25.f, 26.f, 30.f, 31.f, 32.f, 36.f, 37.f, 38.f, 42.f, 43.f, 44.f }; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); ASSERT_EQ(output_ptr.size(), answers.size()); for (size_t i = 0; i < answers.size(); ++i) @@ -534,7 +536,7 @@ class strided_slice_gpu: public ::testing::Test { 10.0f, 11.0f, 12.0f, 13.0f, 14.0f, 15.0f }; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (size_t i = 0; i < answers.size(); ++i) { @@ -576,7 +578,7 @@ class strided_slice_gpu: public ::testing::Test { 0.0f, 1.0f, 2.0f, 3.0f }; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (size_t i = 0; i < answers.size(); ++i) { @@ -621,7 +623,7 @@ class strided_slice_gpu: public ::testing::Test { 0.0f, 1.0f, 2.0f, 3.0f }; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (size_t i = 0; i < answers.size(); ++i) { @@ -662,7 +664,7 @@ class strided_slice_gpu: public ::testing::Test { 0.0f, 1.0f, 2.0f, 3.0f }; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (size_t i = 0; i < answers.size(); ++i) { @@ -707,7 +709,7 @@ class strided_slice_gpu: public ::testing::Test { 0.0f, 4.0f }; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (size_t i = 0; i < answers.size(); ++i) { @@ -749,7 +751,7 @@ class strided_slice_gpu: public ::testing::Test { 0.0f, 1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, }; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (size_t i = 0; i < answers.size(); ++i) { @@ -795,7 +797,7 @@ class strided_slice_gpu: public ::testing::Test { 0.0f, 8.0f, }; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (size_t i = 0; i < answers.size(); ++i) { @@ -839,7 +841,7 @@ class strided_slice_gpu: public ::testing::Test { std::vector answers = { 12.f, 13.f, 14.f, 15.f, 8.f, 9.f, 10.f, 11.f, 4.f, 5.f, 6.f, 7.f, 0.f, 1.f, 2.f, 3.f }; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); ASSERT_EQ(output_ptr.size(), answers.size()); for (size_t i = 0; i < answers.size(); ++i) @@ -888,7 +890,7 @@ class strided_slice_gpu: public ::testing::Test { std::vector answers = { 12.f, 13.f, 14.f, 15.f, 8.f, 9.f, 10.f, 11.f, 4.f, 5.f, 6.f, 7.f, 0.f, 1.f, 2.f, 3.f }; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (size_t i = 0; i < answers.size(); ++i) { @@ -929,7 +931,7 @@ class strided_slice_gpu: public ::testing::Test { 0.0f, 4.0f }; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (size_t i = 0; i < answers.size(); ++i) { @@ -985,7 +987,7 @@ class strided_slice_gpu: public ::testing::Test { 0.0f, 4.0f }; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (size_t i = 0; i < answers.size(); ++i) { @@ -1093,7 +1095,7 @@ class strided_slice_gpu: public ::testing::Test { 0.0f, 4.0f }; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (size_t i = 0; i < answers.size(); ++i) { @@ -1223,7 +1225,7 @@ class strided_slice_gpu_constants: public ::testing::Test { std::vector answers = { 0.f, 1.f, 2.f, 3.f, 4.f, 5.f, 6.f, 7.f, 8.f, 9.f, 10.f, 11.f, 12.f, 13.f, 14.f, 15.f }; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); ASSERT_EQ(output_ptr.size(), answers.size()); for (size_t i = 0; i < answers.size(); ++i) @@ -1282,7 +1284,7 @@ class strided_slice_gpu_constants: public ::testing::Test { 9.0f, 10.0f, 11.0f, 12.0f, 13.0f, 14.0f, 15.0f }; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); ASSERT_EQ(output_ptr.size(), answers.size()); for (size_t i = 0; i < answers.size(); ++i) @@ -1342,7 +1344,7 @@ class strided_slice_gpu_constants: public ::testing::Test { std::vector answers = { 15.f }; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); ASSERT_EQ(output_ptr.size(), answers.size()); for (size_t i = 0; i < answers.size(); ++i) @@ -1409,7 +1411,7 @@ class strided_slice_gpu_constants: public ::testing::Test { 24.f, 25.f, 26.f, 30.f, 31.f, 32.f, 36.f, 37.f, 38.f, 42.f, 43.f, 44.f }; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); ASSERT_EQ(output_ptr.size(), answers.size()); for (size_t i = 0; i < answers.size(); ++i) @@ -1556,7 +1558,7 @@ class strided_slice_gpu_constants: public ::testing::Test { 10.0f, 11.0f, 12.0f, 13.0f, 14.0f, 15.0f }; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (size_t i = 0; i < answers.size(); ++i) { @@ -1610,7 +1612,7 @@ class strided_slice_gpu_constants: public ::testing::Test { 0.0f, 1.0f, 2.0f, 3.0f }; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (size_t i = 0; i < answers.size(); ++i) { @@ -1664,7 +1666,7 @@ class strided_slice_gpu_constants: public ::testing::Test { 0.0f, 1.0f, 2.0f, 3.0f }; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (size_t i = 0; i < answers.size(); ++i) { @@ -1717,7 +1719,7 @@ class strided_slice_gpu_constants: public ::testing::Test { 0.0f, 1.0f, 2.0f, 3.0f }; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (size_t i = 0; i < answers.size(); ++i) { @@ -1770,7 +1772,7 @@ class strided_slice_gpu_constants: public ::testing::Test { 0.0f, 4.0f }; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (size_t i = 0; i < answers.size(); ++i) { @@ -1826,7 +1828,7 @@ class strided_slice_gpu_constants: public ::testing::Test { std::vector answers = { 12.f, 13.f, 14.f, 15.f, 8.f, 9.f, 10.f, 11.f, 4.f, 5.f, 6.f, 7.f, 0.f, 1.f, 2.f, 3.f }; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); ASSERT_EQ(output_ptr.size(), answers.size()); for (size_t i = 0; i < answers.size(); ++i) @@ -1877,7 +1879,7 @@ class strided_slice_gpu_constants: public ::testing::Test { std::vector answers = { 4.f, 5.f, 6.f, 7.f, 0.f, 1.f, 2.f, 3.f }; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); ASSERT_EQ(output_ptr.size(), answers.size()); for (size_t i = 0; i < answers.size(); ++i) @@ -1928,7 +1930,7 @@ class strided_slice_gpu_constants: public ::testing::Test { std::vector answers = { 4.f, 5.f, 6.f, 7.f, 0.f, 1.f, 2.f, 3.f }; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); ASSERT_EQ(output_ptr.size(), answers.size()); for (size_t i = 0; i < answers.size(); ++i) @@ -1979,7 +1981,7 @@ class strided_slice_gpu_constants: public ::testing::Test { std::vector answers = { 4.f, 5.f, 6.f, 7.f, 0.f, 1.f, 2.f, 3.f }; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); ASSERT_EQ(output_ptr.size(), answers.size()); for (size_t i = 0; i < answers.size(); ++i) @@ -2033,7 +2035,7 @@ class strided_slice_gpu_constants: public ::testing::Test { 0.0f, 4.0f }; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (size_t i = 0; i < answers.size(); ++i) { @@ -2088,7 +2090,7 @@ class strided_slice_gpu_constants: public ::testing::Test { 6.0f, 7.0f, 8.0f, }; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (size_t i = 0; i < answers.size(); ++i) { @@ -2141,7 +2143,7 @@ class strided_slice_gpu_constants: public ::testing::Test { 6.0f, 7.0f, 8.0f, }; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (size_t i = 0; i < answers.size(); ++i) { @@ -2198,7 +2200,7 @@ class strided_slice_gpu_constants: public ::testing::Test { std::vector answers = { 12.f, 13.f, 14.f, 15.f, 8.f, 9.f, 10.f, 11.f, 4.f, 5.f, 6.f, 7.f, 0.f, 1.f, 2.f, 3.f }; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); ASSERT_EQ(output_ptr.size(), answers.size()); for (size_t i = 0; i < answers.size(); ++i) @@ -2253,7 +2255,7 @@ class strided_slice_gpu_constants: public ::testing::Test { std::vector answers = { 5.0f, 3.0f }; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); ASSERT_EQ(output_ptr.size(), answers.size()); for (size_t i = 0; i < answers.size(); ++i) @@ -2308,7 +2310,7 @@ class strided_slice_gpu_constants: public ::testing::Test { std::vector answers = { 5.0f, 3.0f }; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); ASSERT_EQ(output_ptr.size(), answers.size()); for (size_t i = 0; i < answers.size(); ++i) @@ -2371,7 +2373,7 @@ class strided_slice_gpu_four_inputs: public ::testing::Test { 10.0f, 11.0f, 12.0f, 13.0f, 14.0f, 15.0f }; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (size_t i = 0; i < answers.size(); ++i) { @@ -2428,7 +2430,7 @@ class strided_slice_gpu_four_inputs: public ::testing::Test { 0.0f, 1.0f, 2.0f, 3.0f }; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (size_t i = 0; i < answers.size(); ++i) { @@ -2472,7 +2474,7 @@ class strided_slice_gpu_i8: public ::testing::Test { 0, 1, 2, 3 }; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (size_t i = 0; i < answers.size(); ++i) { ASSERT_TRUE(are_equal(answers[i], output_ptr[i])); @@ -2513,7 +2515,7 @@ class strided_slice_gpu_i8: public ::testing::Test { 0, 1, 2, 3, 4, 5, 6, 7, }; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (size_t i = 0; i < answers.size(); ++i) { ASSERT_TRUE(are_equal(answers[i], output_ptr[i])); @@ -2568,7 +2570,7 @@ class strided_slice_gpu_f32_i32: public ::testing::Test { 0.0f, 1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f }; - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); for (size_t i = 0; i < answers.size(); ++i) { @@ -2953,3 +2955,70 @@ TEST_F(strided_slice_gpu_constants, test_1x1x1x10_pos_begin_end_neg_stride2) { TEST_F(strided_slice_gpu_constants, test_1x1x1x10_neg_begin_end_neg_stride2) { this->test_1x1x1x10_neg_begin_end_neg_stride2(false); } + +// Verify that strided_slice with partial end_mask is NOT marked as runtime skippable. +// Regression test: previously, the dimension validation loop did not break on the first +// non-full-slice dimension, allowing a later full-slice dim to overwrite is_valid to true. +TEST(strided_slice_gpu_mark_skippable, partial_end_mask_not_skippable) { + auto& engine = get_test_engine(); + // Static 4D input: shape {1, 2, 8, 4} + auto in_layout = layout{ov::PartialShape{1, 2, 8, 4}, data_types::f32, format::bfyx}; + + // end_mask = {1, 1, 0, 1}: dims 0,1,3 are full-slice via mask, dim 2 has end=4 < 8 (partial) + std::vector begin_data = {0, 0, 0, 0}; + std::vector end_data = {0, 0, 4, 0}; + std::vector strides_data = {1, 1, 1, 1}; + std::vector begin_mask = {1, 1, 0, 1}; + std::vector end_mask = {1, 1, 0, 1}; + + topology topology; + topology.add(input_layout("input", in_layout)); + topology.add(strided_slice("strided_slice", input_info("input"), + begin_data, end_data, strides_data, + begin_mask, end_mask, {}, {}, {}, {1, 2, 4, 4})); + topology.add(reorder("output", input_info("strided_slice"), format::bfyx, data_types::f32)); + + ExecutionConfig config = get_test_default_config(engine); + config.set_property(ov::intel_gpu::optimize_data(true)); + config.set_property(ov::intel_gpu::allow_new_shape_infer(true)); + + auto prog = program::build_program(engine, topology, config, false, true); + ASSERT_NE(prog, nullptr); + program_wrapper::apply_opt_pass(*prog); + + auto& ss_node = prog->get_node("strided_slice"); + // The strided_slice slices dim 2 partially, so it must NOT be runtime skippable + ASSERT_FALSE(ss_node.is_runtime_skippable()); +} + +// Verify that strided_slice with all-ones end_mask IS marked as runtime skippable. +TEST(strided_slice_gpu_mark_skippable, full_end_mask_is_skippable) { + auto& engine = get_test_engine(); + auto in_layout = layout{ov::PartialShape{1, 2, 8, 4}, data_types::f32, format::bfyx}; + + // end_mask = {1, 1, 1, 1}: all dims are full-slice via mask + std::vector begin_data = {0, 0, 0, 0}; + std::vector end_data = {0, 0, 0, 0}; + std::vector strides_data = {1, 1, 1, 1}; + std::vector begin_mask = {1, 1, 1, 1}; + std::vector end_mask = {1, 1, 1, 1}; + + topology topology; + topology.add(input_layout("input", in_layout)); + topology.add(strided_slice("strided_slice", input_info("input"), + begin_data, end_data, strides_data, + begin_mask, end_mask, {}, {}, {}, {1, 2, 8, 4})); + topology.add(reorder("output", input_info("strided_slice"), format::bfyx, data_types::f32)); + + ExecutionConfig config = get_test_default_config(engine); + config.set_property(ov::intel_gpu::optimize_data(true)); + config.set_property(ov::intel_gpu::allow_new_shape_infer(true)); + + auto prog = program::build_program(engine, topology, config, false, true); + ASSERT_NE(prog, nullptr); + program_wrapper::apply_opt_pass(*prog); + + auto& ss_node = prog->get_node("strided_slice"); + // All dims are full-slice, so it should be runtime skippable + ASSERT_TRUE(ss_node.is_runtime_skippable()); +} diff --git a/src/plugins/intel_gpu/tests/unit/test_cases/swiglu_gpu_test.cpp b/src/plugins/intel_gpu/tests/unit/test_cases/swiglu_gpu_test.cpp index 858e1c717eb8..b6483623b2d1 100644 --- a/src/plugins/intel_gpu/tests/unit/test_cases/swiglu_gpu_test.cpp +++ b/src/plugins/intel_gpu/tests/unit/test_cases/swiglu_gpu_test.cpp @@ -110,7 +110,7 @@ TEST(swiglu_gpu_test, swiglu_test_bfyx_dyn) { ASSERT_EQ(outputs.begin()->first, "swiglu"); auto output = outputs.begin()->second.get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); cldnn::mem_lock output_ref_ptr(output_ref, get_test_stream()); for (unsigned int i = 0; i < output_ref->count(); ++i) { @@ -153,7 +153,7 @@ TEST(swiglu_gpu_test, swiglu_test_bfyx_dyn_clamp) { ASSERT_EQ(outputs.begin()->first, "swiglu"); auto output = outputs.begin()->second.get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); cldnn::mem_lock output_ref_ptr(output_ref, get_test_stream()); for (unsigned int i = 0; i < output_ref->count(); ++i) { @@ -200,6 +200,59 @@ TEST(swiglu_gpu_test, swiglu_test_bfyx_dyn_clamp_swish_beta_up_add_val) { ASSERT_EQ(outputs.size(), size_t(1)); ASSERT_EQ(outputs.begin()->first, "swiglu"); + auto output = outputs.begin()->second.get_memory(); + cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ref_ptr(output_ref, get_test_stream()); + + for (unsigned int i = 0; i < output_ref->count(); ++i) { + EXPECT_NEAR(output_ptr[i], output_ref_ptr[i], 1e-3); + } +} + +// Regression: stride==2 + gate_idx=1 (GPT-OSS). OPT kernel previously read input[y+GLU_STRIDE] +// for gate instead of input[y+1], causing OOB on the last x. Static shape forces OPT. +TEST(swiglu_gpu_test, swiglu_test_bfyx_static_clamp_swish_beta_up_add_val_gate_idx_1) { + auto& engine = get_test_engine(); + auto input_layout_static = layout{ov::PartialShape{2, 1, 12}, data_types::f32, format::bfyx}; + auto input_mem = engine.allocate_memory({ov::PartialShape{2, 1, 12}, data_types::f32, format::bfyx}); + auto output_ref = engine.allocate_memory({ov::PartialShape{2, 1, 6}, data_types::f32, format::bfyx}); + + auto clamp_min = -0.7f; + auto clamp_max = 7.0f; + + int32_t gate_idx = 1; + int32_t glu_stride = 2; + float swish_beta = 1.2f; + float up_add_val = 1.0f; + + // Sequential values keep input[y+1] vs input[y+2] swish results distinguishable. + set_values(input_mem, { + 0.0f, 1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f, 9.0f, 10.0f, 11.0f, + 0.5f, 1.5f, 2.5f, 3.5f, 4.5f, 5.5f, 6.5f, 7.5f, 8.5f, 9.5f, 10.5f, 11.5f, + }); + + swiglu_ref(input_mem, output_ref, 2, gate_idx, glu_stride, clamp_min, clamp_max, swish_beta, up_add_val); + + topology topology; + topology.add(input_layout("input", input_layout_static)); + // bfyx tensor for {2, 1, 6} -> b=2, f=1, x=6, y=1 (cldnn::tensor takes b, f, x, y) + topology.add(swiglu("swiglu", input_info("input"), -1, glu_stride, ov::op::internal::GLU::GluType::Swish, gate_idx, clamp_min, clamp_max, swish_beta, up_add_val, cldnn::tensor(2, 1, 6, 1))); + + ExecutionConfig config = get_test_default_config(engine); + // Bug lives in OPT kernel; default selector picks REF for these shapes. + ov::intel_gpu::ImplForcingMap forced{ + {"swiglu", ov::intel_gpu::ImplementationDesc{format::bfyx, "swiglu_gpu_opt"}}, + }; + config.set_property(ov::intel_gpu::force_implementations(forced)); + + network network(engine, topology, config); + + network.set_input_data("input", input_mem); + + auto outputs = network.execute(); + ASSERT_EQ(outputs.size(), size_t(1)); + ASSERT_EQ(outputs.begin()->first, "swiglu"); + auto output = outputs.begin()->second.get_memory(); cldnn::mem_lock output_ptr(output, get_test_stream()); cldnn::mem_lock output_ref_ptr(output_ref, get_test_stream()); diff --git a/src/plugins/intel_gpu/tests/unit/test_cases/test_device_mem_usage_estimation.cpp b/src/plugins/intel_gpu/tests/unit/test_cases/test_device_mem_usage_estimation.cpp index 0bbd4e0afd07..8a5ca43d26a0 100644 --- a/src/plugins/intel_gpu/tests/unit/test_cases/test_device_mem_usage_estimation.cpp +++ b/src/plugins/intel_gpu/tests/unit/test_cases/test_device_mem_usage_estimation.cpp @@ -3,6 +3,7 @@ // #include +#include #include "test_utils.h" #include @@ -61,6 +62,21 @@ class test_device_mem_usage_estimation: public ::testing::Test { void get_max_batch_size() { ov::Core ie; + // This test uses ov::Core to compile a model and query max_batch_size, + // which requires the GPU plugin to be registered. The unit test binary + // may not have the plugin discoverable — skip gracefully in that case. + std::vector available_devices; + try { + available_devices = ie.get_available_devices(); + } catch (...) {} + + const bool has_gpu = std::any_of(available_devices.begin(), available_devices.end(), [](const std::string& device) { + return device.rfind("GPU", 0) == 0; + }); + if (!has_gpu) { + GTEST_SKIP() << "GPU plugin is not available in ov::Core (unit test binary may lack plugin discovery)."; + } + auto& engine = get_test_engine(); uint32_t batch_size = 0, batch_size_native = 0; uint32_t n_streams = 1; diff --git a/src/plugins/intel_gpu/tests/unit/test_cases/tile_gpu_test.cpp b/src/plugins/intel_gpu/tests/unit/test_cases/tile_gpu_test.cpp index d68608737895..ef20e6ac8165 100644 --- a/src/plugins/intel_gpu/tests/unit/test_cases/tile_gpu_test.cpp +++ b/src/plugins/intel_gpu/tests/unit/test_cases/tile_gpu_test.cpp @@ -82,7 +82,7 @@ class tile_gpu: public ::testing::Test { auto outputs = network->execute(); auto output = outputs.at("tile").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); cldnn::mem_lock output_ref_ptr(output_ref, get_test_stream()); for (unsigned int i = 0; i < output_ref->count(); ++i) { @@ -118,7 +118,7 @@ class tile_gpu: public ::testing::Test { auto outputs = network->execute(); auto output = outputs.at("tile").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); cldnn::mem_lock output_ref_ptr(output_ref, get_test_stream()); for (unsigned int i = 0; i < output_ref->count(); ++i) { @@ -158,7 +158,7 @@ class tile_gpu: public ::testing::Test { auto outputs = network->execute(); auto output = outputs.at("tile").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); cldnn::mem_lock output_ref_ptr(output_ref, get_test_stream()); for (unsigned int i = 0; i < output_ref->count(); ++i) { @@ -194,7 +194,7 @@ class tile_gpu: public ::testing::Test { auto outputs = network->execute(); auto output = outputs.at("tile").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); cldnn::mem_lock output_ref_ptr(output_ref, get_test_stream()); for (unsigned int i = 0; i < output_ref->count(); ++i) { @@ -226,7 +226,7 @@ class tile_gpu: public ::testing::Test { auto outputs = network->execute(); auto output = outputs.at("tile").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); cldnn::mem_lock output_ref_ptr(output_ref, get_test_stream()); for (unsigned int i = 0; i < output_ref->count(); ++i) { @@ -267,7 +267,7 @@ class tile_gpu: public ::testing::Test { auto outputs = network->execute(); auto output = outputs.at("tile").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); cldnn::mem_lock output_ref_ptr(output_ref, get_test_stream()); for (unsigned int i = 0; i < output_ref->count(); ++i) { @@ -307,7 +307,7 @@ class tile_gpu: public ::testing::Test { auto outputs = network.execute(); auto output = outputs.at("tile").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); std::vector ref_data = { 1.f, 0.f, 5.f, 1.5f, @@ -414,7 +414,7 @@ TEST(tile_cpu_imp_test, disable_usm) { auto outputs = network->execute(); auto output = outputs.at("tile").get_memory(); - cldnn::mem_lock output_ptr(output, get_test_stream()); + cldnn::mem_lock output_ptr(output, get_test_stream()); cldnn::mem_lock output_ref_ptr(output_ref, get_test_stream()); for (unsigned int i = 0; i < output_ref->count(); ++i) { @@ -774,7 +774,7 @@ struct tile_test auto result = network->execute(); auto out_mem = result.at(result_id).get_memory(); - cldnn::mem_lock out_ptr(out_mem, get_test_stream()); + cldnn::mem_lock out_ptr(out_mem, get_test_stream()); ASSERT_EQ(params.output_tensor.count(), out_ptr.size()); diff --git a/src/plugins/intel_gpu/tests/unit/test_cases/unique_gpu_test.cpp b/src/plugins/intel_gpu/tests/unit/test_cases/unique_gpu_test.cpp index 150c3f69a658..ef7b4591fd78 100644 --- a/src/plugins/intel_gpu/tests/unit/test_cases/unique_gpu_test.cpp +++ b/src/plugins/intel_gpu/tests/unit/test_cases/unique_gpu_test.cpp @@ -69,28 +69,28 @@ struct unique_gpu_test : public testing::TestWithParam expected_unique_values_ptr(expected_unique_values, get_test_stream()); + cldnn::mem_lock expected_unique_values_ptr(expected_unique_values, get_test_stream()); ASSERT_EQ(expected_unique_values_ptr.size(), p.expected_unique_values.size()); for (auto i = 0U; i < expected_unique_values_ptr.size(); ++i) { ASSERT_EQ(expected_unique_values_ptr[i], p.expected_unique_values[i]); } const auto expected_indices = outputs.at("expected_indices").get_memory(); - cldnn::mem_lock expected_indices_ptr(expected_indices, get_test_stream()); + cldnn::mem_lock expected_indices_ptr(expected_indices, get_test_stream()); ASSERT_EQ(expected_indices_ptr.size(), p.expected_indices.size()); for (auto i = 0U; i < expected_indices_ptr.size(); ++i) { ASSERT_EQ(expected_indices_ptr[i], p.expected_indices[i]); } const auto expected_rev_indices = outputs.at("expected_rev_indices").get_memory(); - cldnn::mem_lock expected_rev_indices_ptr(expected_rev_indices, get_test_stream()); + cldnn::mem_lock expected_rev_indices_ptr(expected_rev_indices, get_test_stream()); ASSERT_EQ(expected_rev_indices_ptr.size(), p.expected_rev_indices.size()); for (auto i = 0U; i < expected_rev_indices_ptr.size(); ++i) { ASSERT_EQ(expected_rev_indices_ptr[i], p.expected_rev_indices[i]); } const auto expected_counts = outputs.at("expected_counts").get_memory(); - cldnn::mem_lock expected_counts_ptr(expected_counts, get_test_stream()); + cldnn::mem_lock expected_counts_ptr(expected_counts, get_test_stream()); ASSERT_EQ(expected_counts_ptr.size(), p.expected_counts.size()); for (auto i = 0U; i < expected_counts_ptr.size(); ++i) { ASSERT_EQ(expected_counts_ptr[i], p.expected_counts[i]); diff --git a/src/plugins/intel_gpu/tests/unit/test_cases/variable.cpp b/src/plugins/intel_gpu/tests/unit/test_cases/variable.cpp index 6b624c9232cc..5e6f975a129e 100644 --- a/src/plugins/intel_gpu/tests/unit/test_cases/variable.cpp +++ b/src/plugins/intel_gpu/tests/unit/test_cases/variable.cpp @@ -214,10 +214,23 @@ void test_variable_copy_from_fake_aligned_fc(bool is_caching_test) { auto is_4bit = weights_layout_dt == data_types::i4 || weights_layout_dt == data_types::u4; auto is_8bit = weights_layout_dt == data_types::i8 || weights_layout_dt == data_types::u8; auto is_extra_alignment_needed = input_b >= 256; - auto fake_align_base = (is_4bit || is_8bit) && is_extra_alignment_needed ? 64 : 16; + // Match runtime logic in fully_connected_inst::get_fake_aligned_params(): + // iGPU uses base 16 (or 64 for quantized + large batch), dGPU uses base 8. + size_t fake_align_base = 8; + if (engine.get_device_info().dev_type == cldnn::device_type::integrated_gpu) { + fake_align_base = (is_4bit || is_8bit) && is_extra_alignment_needed ? 64 : 16; + } - const auto fc_output_batch_size_aligned = align_to(input_b * input_f, fake_align_base); - ASSERT_EQ(fc_output_batch_size, fc_output_batch_size_aligned); + const size_t batch_size = static_cast(input_b * input_f); + const auto fc_output_batch_size_aligned = align_to(batch_size, fake_align_base); + // On Xe2+ and IMMAD iGPUs the runtime may roll back fake alignment when + // the predecessor buffer is too small (see get_fake_aligned_params_if_possible). + // Accept either the aligned or the original unaligned batch size. + ASSERT_TRUE(fc_output_batch_size == fc_output_batch_size_aligned || + fc_output_batch_size == batch_size) + << "fc_output_batch_size=" << fc_output_batch_size + << " expected aligned=" << fc_output_batch_size_aligned + << " or unaligned=" << batch_size; // read_value ASSERT_EQ(outputs.size(), size_t(1)); diff --git a/src/plugins/intel_gpu/tests/unit/test_utils/parallel_mem_streambuf_test.cpp b/src/plugins/intel_gpu/tests/unit/test_utils/parallel_mem_streambuf_test.cpp new file mode 100644 index 000000000000..7c02b8a71529 --- /dev/null +++ b/src/plugins/intel_gpu/tests/unit/test_utils/parallel_mem_streambuf_test.cpp @@ -0,0 +1,497 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "common_utils/parallel_mem_streambuf.hpp" + +#include + +#include +#include +#include +#include +#include + +#ifdef _WIN32 +# ifndef NOMINMAX +# define NOMINMAX +# endif +# include +#else +# include +# include +# include +#endif + +namespace { + +void fill_pattern(std::vector& buf, size_t start_index = 0) { + for (size_t i = 0; i < buf.size(); ++i) { + buf[i] = static_cast((start_index + i) % 251u); + } +} + +} // namespace + +// 1. Small read – threshold=SIZE_MAX forces the single memcpy path. +TEST(ParallelMemStreamBufTest, FullReadSingleMemcpyPath) { + constexpr size_t k_size = 4 * 1024; + std::vector src(k_size); + fill_pattern(src); + + ov::intel_gpu::ParallelMemStreamBuf buf(src.data(), k_size, /*threshold=*/SIZE_MAX); + std::istream stream(&buf); + + std::vector got(k_size); + ASSERT_TRUE(stream.read((got.data()), static_cast(k_size))); + EXPECT_EQ(got, src); +} + +// 2. threshold=1 forces parallel_copy() to be called on every bulk read. +TEST(ParallelMemStreamBufTest, FullReadParallelMemcpyPath) { + constexpr size_t k_size = 8 * 1024; + std::vector src(k_size); + fill_pattern(src); + + ov::intel_gpu::ParallelMemStreamBuf buf(src.data(), k_size, /*threshold=*/1); + std::istream stream(&buf); + + std::vector got(k_size); + ASSERT_TRUE(stream.read((got.data()), static_cast(k_size))); + EXPECT_EQ(got, src); +} + +// 3. Non-zero logical start: construct on a sub-span of a larger allocation. +TEST(ParallelMemStreamBufTest, NonZeroPointerOffset) { + constexpr size_t k_prefix_size = 512; + constexpr size_t k_payload_size = 2 * 1024; + std::vector backing(k_prefix_size + k_payload_size); + std::fill(backing.begin(), backing.begin() + k_prefix_size, 0xFFu); + std::vector payload(k_payload_size); + fill_pattern(payload); + std::memcpy(backing.data() + k_prefix_size, payload.data(), k_payload_size); + + const char* payload_ptr = (backing.data() + k_prefix_size); + ov::intel_gpu::ParallelMemStreamBuf buf(payload_ptr, k_payload_size, /*threshold=*/1); + std::istream stream(&buf); + + std::vector got(k_payload_size); + ASSERT_TRUE(stream.read((got.data()), static_cast(k_payload_size))); + EXPECT_EQ(got, payload); +} + +// 4. Multiple consecutive partial reads consume bytes in order. +TEST(ParallelMemStreamBufTest, ChunkedReads) { + constexpr size_t k_size = 8 * 1024; + constexpr size_t k_chunk = 1000; + std::vector src(k_size); + fill_pattern(src); + + ov::intel_gpu::ParallelMemStreamBuf buf(src.data(), k_size, /*threshold=*/1); + std::istream stream(&buf); + + std::vector got(k_size); + size_t offset = 0; + while (offset < k_size) { + const size_t n = std::min(k_chunk, k_size - offset); + ASSERT_TRUE(stream.read((got.data() + offset), static_cast(n))); + offset += n; + } + EXPECT_EQ(got, src); +} + +// 5. underflow() + uflow() – char-by-char consumption via stream.get(). +TEST(ParallelMemStreamBufTest, CharByCharRead) { + constexpr size_t k_size = 200; + std::vector src(k_size); + fill_pattern(src); + + ov::intel_gpu::ParallelMemStreamBuf buf(src.data(), k_size, /*threshold=*/SIZE_MAX); + std::istream stream(&buf); + + std::vector got; + got.reserve(k_size); + int ch; + while ((ch = stream.get()) != std::char_traits::eof()) { + got.push_back(static_cast(ch)); + } + ASSERT_EQ(got.size(), k_size); + EXPECT_EQ(got, src); +} + +// 6. seekg(pos, beg) then read returns bytes at that logical position. +TEST(ParallelMemStreamBufTest, SeekFromBeginning) { + constexpr size_t k_size = 1024; + std::vector src(k_size); + fill_pattern(src); + + ov::intel_gpu::ParallelMemStreamBuf buf(src.data(), k_size, /*threshold=*/1); + std::istream stream(&buf); + + constexpr std::streamoff k_seek_pos = 300; + constexpr size_t k_read_len = 20; + stream.seekg(k_seek_pos, std::ios::beg); + ASSERT_TRUE(stream.good()); + + std::vector got(k_read_len); + ASSERT_TRUE(stream.read((got.data()), static_cast(k_read_len))); + + std::vector expected(src.begin() + k_seek_pos, src.begin() + k_seek_pos + k_read_len); + EXPECT_EQ(got, expected); +} + +// 7. seekg(off, cur) – relative forward seek after an initial read. +TEST(ParallelMemStreamBufTest, SeekFromCurrent) { + constexpr size_t k_size = 1024; + std::vector src(k_size); + fill_pattern(src); + + ov::intel_gpu::ParallelMemStreamBuf buf(src.data(), k_size, /*threshold=*/1); + std::istream stream(&buf); + + constexpr size_t k_first_read = 100; + constexpr std::streamoff k_skip = 150; + constexpr size_t k_second_read = 30; + + std::vector first(k_first_read); + ASSERT_TRUE(stream.read((first.data()), static_cast(k_first_read))); + EXPECT_EQ(first, std::vector(src.begin(), src.begin() + k_first_read)); + + stream.seekg(k_skip, std::ios::cur); + ASSERT_TRUE(stream.good()); + + std::vector second(k_second_read); + ASSERT_TRUE(stream.read((second.data()), static_cast(k_second_read))); + + const size_t expected_start = k_first_read + static_cast(k_skip); + std::vector expected_slice(src.begin() + expected_start, src.begin() + expected_start + k_second_read); + EXPECT_EQ(second, expected_slice); +} + +// 8. seekg(off, end) – backward seek from the end. +TEST(ParallelMemStreamBufTest, SeekFromEnd) { + constexpr size_t k_size = 1024; + std::vector src(k_size); + fill_pattern(src); + + ov::intel_gpu::ParallelMemStreamBuf buf(src.data(), k_size, /*threshold=*/1); + std::istream stream(&buf); + + constexpr std::streamoff k_from_end = 48; + stream.seekg(-k_from_end, std::ios::end); + ASSERT_TRUE(stream.good()); + + std::vector got(static_cast(k_from_end)); + ASSERT_TRUE(stream.read((got.data()), k_from_end)); + + std::vector expected(src.end() - k_from_end, src.end()); + EXPECT_EQ(got, expected); +} + +// 9. seekg(0, end) then tellg() must equal the buffer size. +TEST(ParallelMemStreamBufTest, TellgAtEnd) { + constexpr size_t k_size = 512; + std::vector src(k_size, 0xAAu); + + ov::intel_gpu::ParallelMemStreamBuf buf(src.data(), k_size, /*threshold=*/1); + std::istream stream(&buf); + + stream.seekg(0, std::ios::end); + ASSERT_TRUE(stream.good()); + EXPECT_EQ(static_cast(stream.tellg()), k_size); +} + +// 10. tellg() reflects the current position accurately after mixed reads/seeks. +TEST(ParallelMemStreamBufTest, TellgIsConsistent) { + constexpr size_t k_size = 512; + std::vector src(k_size, 0xBBu); + + ov::intel_gpu::ParallelMemStreamBuf buf(src.data(), k_size, /*threshold=*/SIZE_MAX); + std::istream stream(&buf); + + EXPECT_EQ(stream.tellg(), std::streampos(0)); + + std::vector tmp(100); + stream.read(tmp.data(), 100); + EXPECT_EQ(stream.tellg(), std::streampos(100)); + + for (int i = 0; i < 10; ++i) { + stream.get(); + } + EXPECT_EQ(stream.tellg(), std::streampos(110)); + + stream.seekg(200, std::ios::beg); + EXPECT_EQ(stream.tellg(), std::streampos(200)); +} + +// 11. Out-of-range seek returns pos_type(-1). +TEST(ParallelMemStreamBufTest, OutOfRangeSeekFails) { + constexpr size_t k_size = 64; + std::vector src(k_size, 0x55u); + + ov::intel_gpu::ParallelMemStreamBuf buf(src.data(), k_size, /*threshold=*/1); + std::istream stream(&buf); + + const auto pos = stream.seekg(-1, std::ios::beg).tellg(); + EXPECT_EQ(pos, std::streampos(-1)); +} + +// 12. Partial read at EOF. +TEST(ParallelMemStreamBufTest, ReadAtEof) { + constexpr size_t k_size = 80; + std::vector src(k_size); + fill_pattern(src); + + ov::intel_gpu::ParallelMemStreamBuf buf(src.data(), k_size, /*threshold=*/SIZE_MAX); + std::istream stream(&buf); + + std::vector discard(k_size - 10); + ASSERT_TRUE(stream.read((discard.data()), static_cast(k_size - 10))); + + std::vector tail(20, 0xFFu); + const bool ok = static_cast(stream.read((tail.data()), 20)); + EXPECT_FALSE(ok); + EXPECT_TRUE(stream.eof()); + ASSERT_EQ(stream.gcount(), 10); + EXPECT_TRUE(std::equal(tail.begin(), tail.begin() + 10, src.end() - 10)); +} + +// 13. showmanyc() / in_avail(). +TEST(ParallelMemStreamBufTest, ShowmanycReflectsRemainingBytes) { + constexpr size_t k_size = 256; + std::vector src(k_size, 0x77u); + + ov::intel_gpu::ParallelMemStreamBuf buf(src.data(), k_size, /*threshold=*/SIZE_MAX); + std::istream stream(&buf); + + EXPECT_EQ(stream.rdbuf()->in_avail(), static_cast(k_size)); + + std::vector tmp(100); + stream.read(tmp.data(), 100); + EXPECT_EQ(stream.rdbuf()->in_avail(), static_cast(k_size - 100)); + + std::vector rest(k_size - 100); + stream.read(rest.data(), static_cast(k_size - 100)); + EXPECT_EQ(stream.rdbuf()->in_avail(), -1); +} + +// 14. Mixed underflow + bulk read. +TEST(ParallelMemStreamBufTest, MixedCharAndBulkRead) { + constexpr size_t k_size = 1024; + std::vector src(k_size); + fill_pattern(src); + + ov::intel_gpu::ParallelMemStreamBuf buf(src.data(), k_size, /*threshold=*/SIZE_MAX); + std::istream stream(&buf); + + for (int i = 0; i < 8; ++i) { + const int ch = stream.get(); + ASSERT_NE(ch, std::char_traits::eof()); + EXPECT_EQ(static_cast(ch), src[static_cast(i)]); + } + + std::vector rest(k_size - 8); + ASSERT_TRUE(stream.read((rest.data()), static_cast(k_size - 8))); + EXPECT_EQ(rest, std::vector(src.begin() + 8, src.end())); +} + +// 15. Seek back to position 0 and re-read. +TEST(ParallelMemStreamBufTest, SeekToZeroAndReread) { + constexpr size_t k_size = 1024; + std::vector src(k_size); + fill_pattern(src); + + ov::intel_gpu::ParallelMemStreamBuf buf(src.data(), k_size, /*threshold=*/1); + std::istream stream(&buf); + + std::vector first(k_size); + ASSERT_TRUE(stream.read((first.data()), static_cast(k_size))); + EXPECT_EQ(first, src); + + stream.clear(); + stream.seekg(0, std::ios::beg); + ASSERT_TRUE(stream.good()); + + std::vector second(k_size); + ASSERT_TRUE(stream.read((second.data()), static_cast(k_size))); + EXPECT_EQ(second, src); +} + +// 16. Parallel path correctness with large buffer. +TEST(ParallelMemStreamBufTest, ParallelDispatchFullReadCorrectness) { + const size_t k_max_hw = 16; + const size_t hw_raw = std::max(size_t{2}, static_cast(std::thread::hardware_concurrency())); + const size_t hw = hw_raw > k_max_hw ? k_max_hw : hw_raw; + const size_t k_size = 2u * hw * 2u * 1024u * 1024u + 1u; + + std::vector src(k_size); + fill_pattern(src); + + ov::intel_gpu::ParallelMemStreamBuf buf(src.data(), k_size, /*threshold=*/1); + std::istream stream(&buf); + + std::vector got(k_size); + ASSERT_TRUE(stream.read((got.data()), static_cast(k_size))); + EXPECT_EQ(got, src) << "Parallel memcpy produced incorrect data"; +} + +// 17. Parallel path with mid-stream seek. +TEST(ParallelMemStreamBufTest, ParallelDispatchSeekAndReread) { + const size_t k_max_hw = 16; + const size_t hw_raw = std::max(size_t{2}, static_cast(std::thread::hardware_concurrency())); + const size_t hw = hw_raw > k_max_hw ? k_max_hw : hw_raw; + const size_t k_size = 2u * hw * 2u * 1024u * 1024u; + + std::vector src(k_size); + fill_pattern(src); + + ov::intel_gpu::ParallelMemStreamBuf buf(src.data(), k_size, /*threshold=*/1); + std::istream stream(&buf); + + const size_t k_half = k_size / 2; + std::vector first_half(k_half); + ASSERT_TRUE(stream.read((first_half.data()), static_cast(k_half))); + EXPECT_TRUE(std::equal(first_half.begin(), first_half.end(), src.begin())); + + stream.seekg(0, std::ios::beg); + ASSERT_TRUE(stream.good()); + + std::vector full(k_size); + ASSERT_TRUE(stream.read((full.data()), static_cast(k_size))); + EXPECT_EQ(full, src) << "Full read after seek produced incorrect data"; +} + +// 18. File-backed mmap detection: create a file, mmap it, pass the mmap +// pointer to ParallelMemStreamBuf, and verify correct data is returned. +// This exercises the get_mmap_file_info() detection + delegation to +// ParallelReadStreamBuf that is otherwise untested by in-memory tests. +#ifndef _WIN32 +TEST(ParallelMemStreamBufTest, FileBackedMmapDetection) { + constexpr size_t k_size = 16 * 1024; // 16 KB + std::vector expected(k_size); + fill_pattern(expected); + + auto tmp_path = std::filesystem::temp_directory_path() / "par_mem_mmap_test.bin"; + { + std::ofstream ofs(tmp_path, std::ios::binary | std::ios::trunc); + ASSERT_TRUE(ofs.is_open()) << "Cannot create temp file: " << tmp_path; + ofs.write(expected.data(), static_cast(k_size)); + } + + int fd = ::open(tmp_path.c_str(), O_RDONLY); + ASSERT_NE(fd, -1) << "Cannot open temp file for mmap"; + void* mapped = ::mmap(nullptr, k_size, PROT_READ, MAP_PRIVATE, fd, 0); + ::close(fd); + ASSERT_NE(mapped, MAP_FAILED) << "mmap failed"; + + { + // threshold=1 so the constructor attempts mmap file detection on any size + ov::intel_gpu::ParallelMemStreamBuf buf(mapped, k_size, /*threshold=*/1); + std::istream stream(&buf); + + std::vector got(k_size); + ASSERT_TRUE(stream.read(got.data(), static_cast(k_size))); + EXPECT_EQ(got, expected); + + // Verify seek + re-read works through the delegated ParallelReadStreamBuf + stream.clear(); + stream.seekg(0, std::ios::beg); + ASSERT_TRUE(stream.good()); + + std::vector got2(k_size); + ASSERT_TRUE(stream.read(got2.data(), static_cast(k_size))); + EXPECT_EQ(got2, expected); + } + + ::munmap(mapped, k_size); + std::filesystem::remove(tmp_path); +} + +// 19. File-backed mmap with non-zero offset: mmap a file at a page-aligned +// offset and verify the detection correctly computes the file offset. +TEST(ParallelMemStreamBufTest, FileBackedMmapNonZeroOffset) { + const size_t page_size = static_cast(sysconf(_SC_PAGESIZE)); + const size_t k_prefix = page_size; // one page of prefix + const size_t k_payload = 8 * 1024; + const size_t k_total = k_prefix + k_payload; + + std::vector file_content(k_total); + // Fill prefix with 0xFF, payload with deterministic pattern + std::memset(file_content.data(), 0xFF, k_prefix); + for (size_t i = 0; i < k_payload; ++i) { + file_content[k_prefix + i] = static_cast((i) % 251u); + } + + auto tmp_path = std::filesystem::temp_directory_path() / "par_mem_mmap_offset_test.bin"; + { + std::ofstream ofs(tmp_path, std::ios::binary | std::ios::trunc); + ASSERT_TRUE(ofs.is_open()); + ofs.write(file_content.data(), static_cast(k_total)); + } + + int fd = ::open(tmp_path.c_str(), O_RDONLY); + ASSERT_NE(fd, -1); + // mmap the entire file, then pass a pointer into the payload region + void* mapped = ::mmap(nullptr, k_total, PROT_READ, MAP_PRIVATE, fd, 0); + ::close(fd); + ASSERT_NE(mapped, MAP_FAILED); + + const void* payload_ptr = static_cast(mapped) + k_prefix; + + { + ov::intel_gpu::ParallelMemStreamBuf buf(payload_ptr, k_payload, /*threshold=*/1); + std::istream stream(&buf); + + std::vector got(k_payload); + ASSERT_TRUE(stream.read(got.data(), static_cast(k_payload))); + // Verify we got the payload, not the prefix + std::vector expected_payload(file_content.begin() + k_prefix, file_content.end()); + EXPECT_EQ(got, expected_payload); + } + + ::munmap(mapped, k_total); + std::filesystem::remove(tmp_path); +} +#else // _WIN32 +TEST(ParallelMemStreamBufTest, FileBackedMmapDetection) { + constexpr size_t k_size = 16 * 1024; + std::vector expected(k_size); + fill_pattern(expected); + + auto tmp_path = std::filesystem::temp_directory_path() / L"par_mem_mmap_test.bin"; + { + std::ofstream ofs(tmp_path, std::ios::binary | std::ios::trunc); + ASSERT_TRUE(ofs.is_open()) << "Cannot create temp file"; + ofs.write(expected.data(), static_cast(k_size)); + } + + HANDLE hFile = CreateFileW(tmp_path.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); + ASSERT_NE(hFile, INVALID_HANDLE_VALUE) << "Cannot open temp file"; + HANDLE hMapping = CreateFileMappingW(hFile, nullptr, PAGE_READONLY, 0, 0, nullptr); + ASSERT_NE(hMapping, nullptr) << "CreateFileMapping failed"; + void* mapped = MapViewOfFile(hMapping, FILE_MAP_READ, 0, 0, 0); + ASSERT_NE(mapped, nullptr) << "MapViewOfFile failed"; + + { + ov::intel_gpu::ParallelMemStreamBuf buf(mapped, k_size, /*threshold=*/1); + std::istream stream(&buf); + + std::vector got(k_size); + ASSERT_TRUE(stream.read(got.data(), static_cast(k_size))); + EXPECT_EQ(got, expected); + + stream.clear(); + stream.seekg(0, std::ios::beg); + ASSERT_TRUE(stream.good()); + + std::vector got2(k_size); + ASSERT_TRUE(stream.read(got2.data(), static_cast(k_size))); + EXPECT_EQ(got2, expected); + } + + UnmapViewOfFile(mapped); + CloseHandle(hMapping); + CloseHandle(hFile); + std::filesystem::remove(tmp_path); +} +#endif diff --git a/src/tests/functional/plugin/shared/src/single_op/paged_attention_token_type_test_data.cpp b/src/plugins/intel_gpu/tests/unit/test_utils/test_data/paged_attention_token_type_test_data.cpp similarity index 99% rename from src/tests/functional/plugin/shared/src/single_op/paged_attention_token_type_test_data.cpp rename to src/plugins/intel_gpu/tests/unit/test_utils/test_data/paged_attention_token_type_test_data.cpp index 90c144c67aa4..7bf37f70fe48 100644 --- a/src/tests/functional/plugin/shared/src/single_op/paged_attention_token_type_test_data.cpp +++ b/src/plugins/intel_gpu/tests/unit/test_utils/test_data/paged_attention_token_type_test_data.cpp @@ -2,15 +2,13 @@ // SPDX-License-Identifier: Apache-2.0 // -#include "shared_test_classes/single_op/paged_attention_token_type.hpp" +#include "paged_attention_token_type_test_data.h" -using namespace ov::op; - -namespace ov { namespace test { -std::vector PagedAttentionTokenTypeTest::GetTestDataForHeadSize32HeadNum1() { +static std::vector GetTestDataForHeadSize32HeadNum1() { std::vector testData; + const int sliding_window_size = 0; { const std::string name = "plain_casual"; @@ -163,7 +161,7 @@ std::vector PagedAttentionTokenTypeTest::GetTestDataForHeadSize32HeadN -0.308003, 0.027901, -0.193572, 0.030041, -0.003273, -0.011198, 0.11971, -0.113571, 0.096788, 0.267463, 0.206419, -0.269637, 0.570271, -0.336993, 0.193851, 0.012757, 0.050464, 0.260114, -0.194947, -0.03562, -0.219055, 0.196855, 0.047158}; - testData.push_back({name, token_types, q_data, k_data, v_data, wanted_output}); + testData.push_back({name, token_types, q_data, k_data, v_data, wanted_output, sliding_window_size}); } { @@ -317,7 +315,7 @@ std::vector PagedAttentionTokenTypeTest::GetTestDataForHeadSize32HeadN -0.308003, 0.027901, -0.193572, 0.030041, -0.003273, -0.011198, 0.11971, -0.113571, 0.096788, 0.267463, 0.206419, -0.269637, 0.570271, -0.336993, 0.193851, 0.012757, 0.050464, 0.260114, -0.194947, -0.03562, -0.219055, 0.196855, 0.047158}; - testData.push_back({name, token_types, q_data, k_data, v_data, wanted_output}); + testData.push_back({name, token_types, q_data, k_data, v_data, wanted_output, sliding_window_size}); } { @@ -555,7 +553,7 @@ std::vector PagedAttentionTokenTypeTest::GetTestDataForHeadSize32HeadN 0.124981, 0.204197, -0.058794, 0.098339, -0.011927, 0.211357, -0.041711, 0.098862, 0.119303, 0.067251, -0.152045, 0.27265, -0.10156, 0.138649, -0.130894, -0.155933, -0.095668, -0.104816, 0.008138, 0.12806, 0.215425, -0.036746, 0.117335, 0.172523, 0.036049, 0.197724}; - testData.push_back({name, token_types, q_data, k_data, v_data, wanted_output}); + testData.push_back({name, token_types, q_data, k_data, v_data, wanted_output, sliding_window_size}); } { @@ -793,7 +791,7 @@ std::vector PagedAttentionTokenTypeTest::GetTestDataForHeadSize32HeadN 0.124981, 0.204197, -0.058794, 0.098339, -0.011927, 0.211357, -0.041711, 0.098862, 0.119303, 0.067251, -0.152045, 0.27265, -0.10156, 0.138649, -0.130894, -0.155933, -0.095668, -0.104816, 0.008138, 0.12806, 0.215425, -0.036746, 0.117335, 0.172523, 0.036049, 0.197724}; - testData.push_back({name, token_types, q_data, k_data, v_data, wanted_output}); + testData.push_back({name, token_types, q_data, k_data, v_data, wanted_output, sliding_window_size}); } { @@ -2699,7 +2697,7 @@ std::vector PagedAttentionTokenTypeTest::GetTestDataForHeadSize32HeadN 0.036639, -0.014603, -0.107071, -0.001154, -0.071759, 0.043341, 0.019358, -0.01315, -0.004735, -0.001274, 0.064109, 0.022396, 0.038025, 0.100135, -0.015358, -0.094546, 0.048992, -0.004058, 0.018969, -0.068489, -0.095415, 0.03516, -0.006241, 0.009353, 0.028247, 0.030636}; - testData.push_back({name, token_types, q_data, k_data, v_data, wanted_output}); + testData.push_back({name, token_types, q_data, k_data, v_data, wanted_output, sliding_window_size}); } { @@ -5162,7 +5160,7 @@ std::vector PagedAttentionTokenTypeTest::GetTestDataForHeadSize32HeadN 0.001951, -0.042289, 0.042452, -0.085086, -0.078698, 0.024427, -0.019972, 0.114998, -0.011411, -0.010484, 0.010148, 0.034235, -0.080249, 0.027983, -0.013981, -0.010044, 0.025208, -0.022628, -0.036155, -0.05289, -0.010923, 0.014724, 0.046534}; - testData.push_back({name, token_types, q_data, k_data, v_data, wanted_output}); + testData.push_back({name, token_types, q_data, k_data, v_data, wanted_output, sliding_window_size}); } { @@ -6315,14 +6313,15 @@ std::vector PagedAttentionTokenTypeTest::GetTestDataForHeadSize32HeadN -0.069667, -0.015423, 0.040691, -0.106843, 0.04387, 0.006794, 0.070803, 0.032478, 0.066869, -0.090667, -0.090796, -0.106503, 0.138459, -0.064017, 0.1206, 0.045981, 0.110657, -0.049117, -0.013569, -0.019554, 0.062624, 0.01151}; - testData.push_back({name, token_types, q_data, k_data, v_data, wanted_output}); + testData.push_back({name, token_types, q_data, k_data, v_data, wanted_output, sliding_window_size}); } return testData; } -std::vector PagedAttentionTokenTypeTest::GetTestDataForHeadSize32HeadNum1SlidingWindowSize5() { +static std::vector GetTestDataForHeadSize32HeadNum1SlidingWindowSize5() { std::vector testData; + const int sliding_window_size = 5; { const std::string name = "only_sliding_window"; const std::vector token_types{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; @@ -6546,7 +6545,7 @@ std::vector PagedAttentionTokenTypeTest::GetTestDataForHeadSize32HeadN 0.248161, -0.120971, -0.02436, 0.157455, 0.04565, -0.20967, -0.238891, -0.251281, 0.091486, 0.107387, -0.101826, -0.146468, 0.008208, 0.217812, 0.369781, -0.020876, 0.074199, -0.056155, -0.200816, -0.087326, 0.350625}; - testData.push_back({name, token_types, q_data, k_data, v_data, wanted_output}); + testData.push_back({name, token_types, q_data, k_data, v_data, wanted_output, sliding_window_size}); } { @@ -6772,7 +6771,7 @@ std::vector PagedAttentionTokenTypeTest::GetTestDataForHeadSize32HeadN 0.248161, -0.120971, -0.02436, 0.157455, 0.04565, -0.20967, -0.238891, -0.251281, 0.091486, 0.107387, -0.101826, -0.146468, 0.008208, 0.217812, 0.369781, -0.020876, 0.074199, -0.056155, -0.200816, -0.087326, 0.350625}; - testData.push_back({name, token_types, q_data, k_data, v_data, wanted_output}); + testData.push_back({name, token_types, q_data, k_data, v_data, wanted_output, sliding_window_size}); } { @@ -7155,7 +7154,7 @@ std::vector PagedAttentionTokenTypeTest::GetTestDataForHeadSize32HeadN -0.275347, -0.133337, 0.05895, -0.296771, -0.223556, -0.203761, -0.224432, 0.138675, 0.369687, 0.076605, 0.125778, -0.257924, -0.156964, 0.152194, 0.259834, -0.205936, -0.243631, -0.354199, 0.140052, 0.231083, 0.026353, -0.162425}; - testData.push_back({name, token_types, q_data, k_data, v_data, wanted_output}); + testData.push_back({name, token_types, q_data, k_data, v_data, wanted_output, sliding_window_size}); } { @@ -9903,7 +9902,7 @@ std::vector PagedAttentionTokenTypeTest::GetTestDataForHeadSize32HeadN 0.030964, 0.268971, 0.186284, -0.003771, -0.295209, -0.084093, 0.117445, 0.245304, 0.089725, -0.329711, -0.453279, 0.080331, 0.152678, -0.458175, -0.187473, -0.402553, 0.225154, -0.069841, -0.423506, 0.367267, -0.40356, 0.16414, -0.655346, -0.295128}; - testData.push_back({name, token_types, q_data, k_data, v_data, wanted_output}); + testData.push_back({name, token_types, q_data, k_data, v_data, wanted_output, sliding_window_size}); } { @@ -13336,7 +13335,7 @@ std::vector PagedAttentionTokenTypeTest::GetTestDataForHeadSize32HeadN 0.251237, 0.237103, 0.358358, -0.762111, -0.364944, -0.04633, 0.120133, -0.249979, 0.265841, -0.181785, -0.328781, 0.336781, -0.117505, -0.034779, 0.00199, -0.349486, 0.606767, 0.237495, 0.311784, -0.089736, -0.162392}; - testData.push_back({name, token_types, q_data, k_data, v_data, wanted_output}); + testData.push_back({name, token_types, q_data, k_data, v_data, wanted_output, sliding_window_size}); } { @@ -16056,11 +16055,19 @@ std::vector PagedAttentionTokenTypeTest::GetTestDataForHeadSize32HeadN -0.000925, -0.053269, -0.01973, -0.07028, -0.054812, 0.035725, 0.01787, 0.05495, -0.014351, -0.023094, -0.001283, 0.077208, -0.046138, 0.013706, -0.027749, 0.034011, 0.089154, -0.015186, -0.021743, -0.018396, 0.010127, 0.005655, 0.074629}; - testData.push_back({name, token_types, q_data, k_data, v_data, wanted_output}); + testData.push_back({name, token_types, q_data, k_data, v_data, wanted_output, sliding_window_size}); } return testData; } -} // namespace test -} // namespace ov \ No newline at end of file +std::vector PagedAttentionTokenTypeTestData::GetTestData() { + std::vector testData; + auto data1 = GetTestDataForHeadSize32HeadNum1(); + testData.insert(testData.end(), data1.begin(), data1.end()); + auto data2 = GetTestDataForHeadSize32HeadNum1SlidingWindowSize5(); + testData.insert(testData.end(), data2.begin(), data2.end()); + return testData; +} + +} // namespace test \ No newline at end of file diff --git a/src/plugins/intel_gpu/tests/unit/test_utils/test_data/paged_attention_token_type_test_data.h b/src/plugins/intel_gpu/tests/unit/test_utils/test_data/paged_attention_token_type_test_data.h new file mode 100644 index 000000000000..1aed4bdb9629 --- /dev/null +++ b/src/plugins/intel_gpu/tests/unit/test_utils/test_data/paged_attention_token_type_test_data.h @@ -0,0 +1,28 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#pragma once + +#include +#include +#include + +namespace test { + +struct TestData { + std::string name; + std::vector tokenTypes; + std::vector qData; + std::vector kData; + std::vector vData; + std::vector expectedOutput; + int slidingWindowSize; +}; + +class PagedAttentionTokenTypeTestData { +public: + static std::vector GetTestData(); +}; + +} // namespace test \ No newline at end of file diff --git a/src/plugins/intel_gpu/tests/unit/test_utils/test_utils.h b/src/plugins/intel_gpu/tests/unit/test_utils/test_utils.h index 63325714a989..3ec78d72b5ad 100644 --- a/src/plugins/intel_gpu/tests/unit/test_utils/test_utils.h +++ b/src/plugins/intel_gpu/tests/unit/test_utils/test_utils.h @@ -251,7 +251,7 @@ template T get_value(T* ptr, uint32_t index) { return ptr[index]; } template void set_values(cldnn::memory::ptr mem, std::initializer_list args) { - cldnn::mem_lock ptr(mem, get_test_stream()); + cldnn::mem_lock ptr(mem, get_test_stream()); auto it = ptr.begin(); for(auto x : args) @@ -260,7 +260,7 @@ void set_values(cldnn::memory::ptr mem, std::initializer_list args) { template void set_values(cldnn::memory::ptr mem, std::vector args) { - cldnn::mem_lock ptr(mem, get_test_stream()); + cldnn::mem_lock ptr(mem, get_test_stream()); auto it = ptr.begin(); for (auto x : args) @@ -269,7 +269,7 @@ void set_values(cldnn::memory::ptr mem, std::vector args) { template void set_values_per_batch_and_feature(cldnn::memory::ptr mem, std::vector args) { - cldnn::mem_lock mem_ptr(mem, get_test_stream()); + cldnn::mem_lock mem_ptr(mem, get_test_stream()); auto&& pitches = mem->get_layout().get_pitches(); auto&& l = mem->get_layout(); for (cldnn::tensor::value_type b = 0; b < l.batch(); ++b) { @@ -289,7 +289,7 @@ template::value || std::is_same::value>::type* = nullptr> void set_random_values(cldnn::memory::ptr mem, bool sign = false, unsigned significand_bit = 8, unsigned scale = 1) { - cldnn::mem_lock ptr(mem, get_test_stream()); + cldnn::mem_lock ptr(mem, get_test_stream()); std::mt19937 gen; for (auto it = ptr.begin(); it != ptr.end(); ++it) { @@ -303,7 +303,7 @@ void set_random_values(cldnn::memory::ptr mem) using T1 = typename std::conditional::value, int, T>::type; using T2 = typename std::conditional::value, unsigned int, T1>::type; - cldnn::mem_lock ptr(mem, get_test_stream()); + cldnn::mem_lock ptr(mem, get_test_stream()); std::mt19937 gen; static std::uniform_int_distribution uid(std::numeric_limits::min(), std::numeric_limits::max()); diff --git a/src/plugins/intel_gpu/tests/unit/transformations/convert_fc_to_compressed_test.cpp b/src/plugins/intel_gpu/tests/unit/transformations/convert_fc_to_compressed_test.cpp index 26f96ed84b7d..24b185ac16c9 100644 --- a/src/plugins/intel_gpu/tests/unit/transformations/convert_fc_to_compressed_test.cpp +++ b/src/plugins/intel_gpu/tests/unit/transformations/convert_fc_to_compressed_test.cpp @@ -662,6 +662,70 @@ TEST_F(TransformationTestsF, ConvertFCToCompressed16_i8ZpNotConvertedToU8) { } } +// 4D non-grouped weight constant: leading dims are unit, only last two are +// real (K, N). is_weight_3d is false, so the reshape branch must collapse +// dims [d0, d1, d2, d3] -> [d0*d1*d2, d3] for weight/scale/zp. +TEST_F(TransformationTestsF, ConvertFCToCompressed17_4dNonGrouped) { + { + auto input1 = std::make_shared(ov::element::f16, ov::PartialShape{ -1, 16 }); + auto weights_const = ov::op::v0::Constant::create(ov::element::u8, ov::Shape{ 1, 1, 32, 16 }, { 1 }); + auto convert = std::make_shared(weights_const, ov::element::f16); + auto zp_const = ov::op::v0::Constant::create(ov::element::u8, ov::Shape{ 1, 1, 32, 1 }, { 1 }); + auto zp_convert = std::make_shared(zp_const, ov::element::f16); + auto sub = std::make_shared(convert, zp_convert); + auto scale_const = ov::op::v0::Constant::create(ov::element::f16, ov::Shape{ 1, 1, 32, 1 }, { 1 }); + auto scale = std::make_shared(sub, scale_const); + auto no_bias = std::make_shared(); + auto fc = std::make_shared(input1, scale, no_bias); + + model = std::make_shared(ov::OutputVector{fc}, ov::ParameterVector{input1}); + manager.register_pass(); + } + { + auto input1 = std::make_shared(ov::element::f16, ov::PartialShape{ -1, 16 }); + auto weights_const = ov::op::v0::Constant::create(ov::element::u8, ov::Shape{ 32, 16 }, { 1 }); + auto no_bias = std::make_shared(); + auto scale_const = ov::op::v0::Constant::create(ov::element::f16, ov::Shape{ 32, 1 }, { 1 }); + auto zp_const = ov::op::v0::Constant::create(ov::element::u8, ov::Shape{ 32, 1 }, { 1 }); + auto fc_compressed = std::make_shared(input1, weights_const, no_bias, scale_const, zp_const); + + model_ref = std::make_shared(ov::OutputVector{fc_compressed}, ov::ParameterVector{input1}); + } +} + +// 4D grouped weight/scale/zp constants with is_weight_3d=true: the FC sees +// a 3D weight (after 4D->3D reshape), so the reshape_const lambda must +// collapse 4D constants [d0, d1, d2, d3] -> [d0, d1, d2*d3] (grouped path). +TEST_F(TransformationTestsF, ConvertFCToCompressed18_4dGroupedWith3dWeight) { + { + auto input1 = std::make_shared(ov::element::f16, ov::PartialShape{ -1, 8 }); + auto weights_const = ov::op::v0::Constant::create(ov::element::u4, ov::Shape{ 2, 4, 2, 4 }, { 1 }); + auto convert = std::make_shared(weights_const, ov::element::f16); + auto zp_const = ov::op::v0::Constant::create(ov::element::u8, ov::Shape{ 2, 4, 2, 1 }, { 1 }); + auto zp_convert = std::make_shared(zp_const, ov::element::f16); + auto sub = std::make_shared(convert, zp_convert); + auto scale_const = ov::op::v0::Constant::create(ov::element::f16, ov::Shape{ 2, 4, 2, 1 }, { 1 }); + auto scale = std::make_shared(sub, scale_const); + auto reshape_const = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{ 3 }, { 2, 4, -1 }); + auto reshape = std::make_shared(scale, reshape_const, false); + auto no_bias = std::make_shared(); + auto fc = std::make_shared(input1, reshape, no_bias); + + model = std::make_shared(ov::OutputVector{fc}, ov::ParameterVector{input1}); + manager.register_pass(); + } + { + auto input1 = std::make_shared(ov::element::f16, ov::PartialShape{ -1, 8 }); + auto weights_const = ov::op::v0::Constant::create(ov::element::u4, ov::Shape{ 2, 4, 8 }, { 1 }); + auto no_bias = std::make_shared(); + auto scale_const = ov::op::v0::Constant::create(ov::element::f16, ov::Shape{ 2, 4, 2 }, { 1 }); + auto zp_const = ov::op::v0::Constant::create(ov::element::u8, ov::Shape{ 2, 4, 2 }, { 1 }); + auto fc_compressed = std::make_shared(input1, weights_const, no_bias, scale_const, zp_const); + + model_ref = std::make_shared(ov::OutputVector{fc_compressed}, ov::ParameterVector{input1}); + } +} + } // namespace intel_gpu } // namespace test } // namespace ov diff --git a/src/plugins/intel_gpu/tests/unit/transformations/convert_matmul_to_fc_test.cpp b/src/plugins/intel_gpu/tests/unit/transformations/convert_matmul_to_fc_test.cpp index 30f71d07ff3b..87a72daeac86 100644 --- a/src/plugins/intel_gpu/tests/unit/transformations/convert_matmul_to_fc_test.cpp +++ b/src/plugins/intel_gpu/tests/unit/transformations/convert_matmul_to_fc_test.cpp @@ -474,6 +474,72 @@ TEST_F(TransformationTestsF, ConvertMatMulToFullyConnectedTest_compressed_u8_par } } +TEST_F(TransformationTestsF, ConvertMatMulToFullyConnectedTest_compressed_u8_weights_extra_multiply) { + { + auto data = std::make_shared(ov::element::f16, ov::Shape{1, 4, 2}); + auto weights = ov::opset1::Constant::create(ov::element::u8, ov::Shape{1, 2, 32}, {1}); + auto convert = std::make_shared(weights, ov::element::f16); + auto mul_const = ov::opset1::Constant::create(ov::element::f16, ov::Shape{1, 1, 32}, {1}); + auto mul = std::make_shared(convert, mul_const); + auto reshape_const = ov::opset1::Constant::create(ov::element::i32, ov::Shape{2}, {2, 32}); + auto reshape = std::make_shared(mul, reshape_const, false); + auto extra_mul = std::make_shared(reshape, mul_const); + auto matmul = std::make_shared(data, extra_mul); + + model = std::make_shared(ov::OutputVector{matmul}, ov::ParameterVector{data}); + bool support_immad = true; + manager.register_pass(support_immad); + } + { + auto data = std::make_shared(ov::element::f16, ov::Shape{1, 4, 2}); + auto weights = ov::opset1::Constant::create(ov::element::u8, ov::Shape{1, 2, 32}, {1}); + auto convert = std::make_shared(weights, ov::element::f16); + auto mul_const = ov::opset1::Constant::create(ov::element::f16, ov::Shape{1, 1, 32}, {1}); + auto mul = std::make_shared(convert, mul_const); + auto reshape_const = ov::opset1::Constant::create(ov::element::i32, ov::Shape{2}, {2, 32}); + auto reshape = std::make_shared(mul, reshape_const, false); + auto extra_mul = std::make_shared(reshape, mul_const); + + auto transpose_const = ov::opset1::Constant::create(ov::element::i32, {3}, {0, 2, 1}); + auto transpose = std::make_shared(extra_mul, transpose_const); + auto no_bias = std::make_shared(); + auto matmul = std::make_shared(data, transpose, no_bias); + + model_ref = std::make_shared(ov::OutputVector{matmul}, ov::ParameterVector{data}); + } +} + +TEST_F(TransformationTestsF, ConvertMatMulToFullyConnectedTest_compressed_u8_weights_extra_multiply_non_trivial_batch_broadcast) { + { + auto data = std::make_shared(ov::element::f16, ov::Shape{1, 4, 16}); + auto weights = ov::opset1::Constant::create(ov::element::u8, ov::Shape{8, 2, 32}, {1}); + auto convert = std::make_shared(weights, ov::element::f16); + auto mul_const = ov::opset1::Constant::create(ov::element::f16, ov::Shape{8, 1, 32}, {1}); + auto mul = std::make_shared(convert, mul_const); + auto reshape_const = ov::opset1::Constant::create(ov::element::i32, ov::Shape{2}, {16, 32}); + auto reshape = std::make_shared(mul, reshape_const, false); + auto extra_mul = std::make_shared(reshape, mul_const); + auto matmul = std::make_shared(data, extra_mul); + + model = std::make_shared(ov::OutputVector{matmul}, ov::ParameterVector{data}); + bool support_immad = true; + manager.register_pass(support_immad); + } + { + auto data = std::make_shared(ov::element::f16, ov::Shape{1, 4, 16}); + auto weights = ov::opset1::Constant::create(ov::element::u8, ov::Shape{8, 2, 32}, {1}); + auto convert = std::make_shared(weights, ov::element::f16); + auto mul_const = ov::opset1::Constant::create(ov::element::f16, ov::Shape{8, 1, 32}, {1}); + auto mul = std::make_shared(convert, mul_const); + auto reshape_const = ov::opset1::Constant::create(ov::element::i32, ov::Shape{2}, {16, 32}); + auto reshape = std::make_shared(mul, reshape_const, false); + auto extra_mul = std::make_shared(reshape, mul_const); + auto matmul = std::make_shared(data, extra_mul); + + model_ref = std::make_shared(ov::OutputVector{matmul}, ov::ParameterVector{data}); + } +} + TEST_F(TransformationTestsF, ConvertMatMulToFullyConnectedTest_compressed_u4_weights_3D) { { auto data = std::make_shared(ov::element::f16, ov::Shape{3, 2, 2}); @@ -700,6 +766,40 @@ TEST(TransformationTests, ConvertMatMulToFullyConnectedExceptionTest_sibling_mat ASSERT_TRUE(success == false); } +TEST(TransformationTests, ConvertMatMulToFullyConnected_clone_shared_convert) { + auto input1 = std::make_shared(ov::element::f32, ov::Shape{1, 10, 32}); + auto input2 = std::make_shared(ov::element::f32, ov::Shape{1, 10, 64}); + auto weights = ov::opset1::Constant::create(ov::element::f16, ov::Shape{64, 32}, {1}); + auto convert = std::make_shared(weights, ov::element::f32); + ov::mark_as_decompression(convert); + + auto no_bias = std::make_shared(); + auto fc1 = std::make_shared(input1, convert, no_bias); + auto matmul2 = std::make_shared(input2, convert, false, false); + + auto model = std::make_shared(ov::OutputVector{fc1, matmul2}, + ov::ParameterVector{input1, input2}); + + ov::pass::Manager manager; + manager.register_pass(); + // Without the fix, the pass mutates the shared Convert in-place and breaks fc1's + // weight shape, causing validate_and_infer_types to throw inside run_passes. + ASSERT_NO_THROW(manager.run_passes(model)); + + size_t fc_count = 0; + size_t matmul_count = 0; + for (auto& op : model->get_ops()) { + std::string type_name(op->get_type_name()); + if (type_name.find("FullyConnected") != std::string::npos) { + ++fc_count; + } else if (type_name == "MatMul") { + ++matmul_count; + } + } + ASSERT_EQ(fc_count, 2u); + ASSERT_EQ(matmul_count, 0u); +} + TEST_F(TransformationTestsF, ConvertMatMulToFullyConnectedExceptionTest) { { auto input1 = std::make_shared(ov::element::f32, ov::Shape{1, 10, 64}); @@ -728,3 +828,22 @@ TEST_F(TransformationTestsF, ConvertMatMulToFullyConnectedExceptionTest) { model_ref = std::make_shared(ov::OutputVector{matmul1, matmul2}, ov::ParameterVector{input1}); } } + +TEST_F(TransformationTestsF, ConvertMatMulToFullyConnectedTest_rank_a_less_than_rank_b) { + { + auto activation = std::make_shared(ov::element::f32, ov::Shape{1, 128}); + auto weights = ov::opset1::Constant::create(ov::element::f32, ov::Shape{1, 1, 256, 128}, {1.0f}); + auto matmul = std::make_shared(activation, weights, false, true); + + model = std::make_shared(ov::OutputVector{matmul}, ov::ParameterVector{activation}); + manager.register_pass(true); // supports_immad=true + } + { + // Expected: MatMul remains unchanged (rank_a=2 < rank_b=4 rejected by rank check) + auto activation = std::make_shared(ov::element::f32, ov::Shape{1, 128}); + auto weights = ov::opset1::Constant::create(ov::element::f32, ov::Shape{1, 1, 256, 128}, {1.0f}); + auto matmul = std::make_shared(activation, weights, false, true); + + model_ref = std::make_shared(ov::OutputVector{matmul}, ov::ParameterVector{activation}); + } +} diff --git a/src/plugins/intel_gpu/tests/unit/transformations/convert_moe_to_compressed_test.cpp b/src/plugins/intel_gpu/tests/unit/transformations/convert_moe_to_compressed_test.cpp deleted file mode 100644 index bb45e051a24f..000000000000 --- a/src/plugins/intel_gpu/tests/unit/transformations/convert_moe_to_compressed_test.cpp +++ /dev/null @@ -1,936 +0,0 @@ -// Copyright (C) 2018-2026 Intel Corporation -// SPDX-License-Identifier: Apache-2.0 -// - -#include - -#include "common_test_utils/ov_test_utils.hpp" -#include "intel_gpu/op/moe_compressed.hpp" -#include "intel_gpu/op/placeholder.hpp" -#include "openvino/op/constant.hpp" -#include "openvino/op/convert.hpp" -#include "openvino/op/moe.hpp" -#include "openvino/op/multiply.hpp" -#include "openvino/op/reshape.hpp" -#include "openvino/op/subtract.hpp" -#include "openvino/op/transpose.hpp" -#include "plugin/transformations/convert_moe_to_compressed.hpp" - -using namespace testing; -using namespace ov::intel_gpu; - -namespace ov { -namespace test { -namespace intel_gpu { -class ConvertMOEToMOE3GemmCompressedTest : public TransformationTestsF, public WithParamInterface { -public: - static std::string get_test_case_name(testing::TestParamInfo obj) { - ov::element::Type element_type = obj.param; - std::ostringstream result; - result << "ElementType=" << element_type.get_type_name(); - return result.str(); - } -}; - -TEST_P(ConvertMOEToMOE3GemmCompressedTest, GEMM3SwiGLU) { - disable_rt_info_check(); - const auto& data_type = GetParam(); - { - // tokens:32, hidden_size:2048, iter_size:768, experts:128, topk:8 - auto hidden_states = std::make_shared(data_type, Shape{32, 2048}); - auto routing_weights = std::make_shared(data_type, Shape{128, 1, 32, 1}); - auto routing_idx = std::make_shared(element::i32, Shape{32, 8}); - - // Gate projection - auto wei_gate = op::v0::Constant::create(element::u4, Shape{128, 768, 16, 128}, {1}); - auto zp_gate = op::v0::Constant::create(element::u4, Shape{128, 768, 16, 1}, {0}); - auto scale_gate = op::v0::Constant::create(element::f16, Shape{128, 768, 16, 1}, {0.01f}); - auto reshape_const_gate = op::v0::Constant::create(element::i32, Shape{3}, {128, 768, 2048}); - - auto w_gate_f16 = std::make_shared(wei_gate, element::f16); - auto zp_gate_f16 = std::make_shared(zp_gate, element::f16); - auto sub_gate = std::make_shared(w_gate_f16, zp_gate_f16); - auto mul_gate = std::make_shared(sub_gate, scale_gate); - auto reshape_gate = std::make_shared(mul_gate, reshape_const_gate, false); - auto convert_gate = std::make_shared(reshape_gate, element::f32); - - // Up projection - auto wei_up = op::v0::Constant::create(element::u4, Shape{128, 768, 16, 128}, {1}); - auto zp_up = op::v0::Constant::create(element::u4, Shape{128, 768, 16, 1}, {0}); - auto scale_up = op::v0::Constant::create(element::f16, Shape{128, 768, 16, 1}, {0.01f}); - auto reshape_const_up = op::v0::Constant::create(element::i32, Shape{3}, {128, 768, 2048}); - - auto w_up_f16 = std::make_shared(wei_up, element::f16); - auto zp_up_f16 = std::make_shared(zp_up, element::f16); - auto sub_up = std::make_shared(w_up_f16, zp_up_f16); - auto mul_up = std::make_shared(sub_up, scale_up); - auto reshape_up = std::make_shared(mul_up, reshape_const_up, false); - auto convert_up = std::make_shared(reshape_up, element::f32); - - // Down projection - auto wei_down = op::v0::Constant::create(element::u4, Shape{128, 2048, 6, 128}, {1}); - auto zp_down = op::v0::Constant::create(element::u4, Shape{128, 2048, 6, 1}, {0}); - auto scale_down = op::v0::Constant::create(element::f16, Shape{128, 2048, 6, 1}, {0.01f}); - auto reshape_const_down = op::v0::Constant::create(element::i32, Shape{3}, {128, 2048, 768}); - - auto wei_down_f16 = std::make_shared(wei_down, element::f16); - auto zp_down_f16 = std::make_shared(zp_down, element::f16); - auto sub_down = std::make_shared(wei_down_f16, zp_down_f16); - auto mul_down = std::make_shared(sub_down, scale_down); - auto reshape_down = std::make_shared(mul_down, reshape_const_down, false); - auto convert_down = std::make_shared(reshape_down, element::f32); - - // Construct MOE node - ov::op::internal::MOE::Config config; - config.expert_type = ov::op::internal::MOE::Expert_type::GEMM3_SWIGLU; - auto moe = std::make_shared( - ov::OutputVector{hidden_states, routing_weights, routing_idx, convert_gate, convert_up, convert_down}, config); - model = std::make_shared(moe, ov::ParameterVector{hidden_states, routing_weights, routing_idx}); - manager.register_pass(0); - } - { - // Inputs - auto hidden_states = std::make_shared(data_type, Shape{32, 2048}); - auto routing_weights = std::make_shared(data_type, Shape{128, 1, 32, 1}); - auto routing_idx = std::make_shared(element::i32, Shape{32, 8}); - - // Gate and up projection - auto reshape_const_gate_up = op::v0::Constant::create(element::i32, Shape{3}, {128, 768, 16}); - auto transpose_const_gate_up = op::v0::Constant::create(element::i32, Shape{3}, {0, 2, 1}); - - auto wei_gate = op::v0::Constant::create(element::u4, Shape{128, 768, 16, 128}, {1}); - auto zp_gate = op::v0::Constant::create(element::u4, Shape{128, 768, 16, 1}, {0}); - auto scale_gate = op::v0::Constant::create(element::f16, Shape{128, 768, 16, 1}, {0.01f}); - auto zp_reshape_gate = std::make_shared(zp_gate, reshape_const_gate_up, false); - auto zp_transpose_gate = std::make_shared(zp_reshape_gate, transpose_const_gate_up); - auto scale_reshape_gate = std::make_shared(scale_gate, reshape_const_gate_up, false); - auto scale_transpose_gate = std::make_shared(scale_reshape_gate, transpose_const_gate_up); - - auto wei_up = op::v0::Constant::create(element::u4, Shape{128, 768, 16, 128}, {1}); - auto zp_up = op::v0::Constant::create(element::u4, Shape{128, 768, 16, 1}, {0}); - auto scale_up = op::v0::Constant::create(element::f16, Shape{128, 768, 16, 1}, {0.01f}); - auto zp_reshape_up = std::make_shared(zp_up, reshape_const_gate_up, false); - auto zp_transpose_up = std::make_shared(zp_reshape_up, transpose_const_gate_up); - auto scale_reshape_up = std::make_shared(scale_up, reshape_const_gate_up, false); - auto scale_transpose_up = std::make_shared(scale_reshape_up, transpose_const_gate_up); - - // Down projection - auto wei_down = op::v0::Constant::create(element::u4, Shape{128, 2048, 6, 128}, {1}); - auto zp_down = op::v0::Constant::create(element::u4, Shape{128, 2048, 6, 1}, {0}); - auto scale_down = op::v0::Constant::create(element::f16, Shape{128, 2048, 6, 1}, {0.01f}); - auto reshape_const_down = op::v0::Constant::create(element::i32, Shape{3}, {128, 2048, 6}); - auto transpose_const_down = op::v0::Constant::create(element::i32, Shape{3}, {0, 2, 1}); - auto zp_reshape_down = std::make_shared(zp_down, reshape_const_down, false); - auto zp_transpose_down = std::make_shared(zp_reshape_down, transpose_const_down); - auto scale_reshape_down = std::make_shared(scale_down, reshape_const_down, false); - auto scale_transpose_down = std::make_shared(scale_reshape_down, transpose_const_down); - - ov::intel_gpu::op::MOECompressed::Config config; - config.hidden_size = 2048; - config.inter_size = 768; - config.num_expert = 128; - config.top_k = 8; - config.group_size = 128; - config.out_type = ov::element::f16; - std::shared_ptr moe_compressed = std::make_shared( - ov::OutputVector{hidden_states, routing_weights, routing_idx, - wei_gate, scale_transpose_gate, zp_transpose_gate, - wei_up, scale_transpose_up, zp_transpose_up, - wei_down, scale_transpose_down, zp_transpose_down}, config); - if (config.out_type != data_type) { - moe_compressed = std::make_shared(moe_compressed, data_type); - } - model_ref = std::make_shared(moe_compressed, ov::ParameterVector{hidden_states, routing_weights, routing_idx}); - } -} - -INSTANTIATE_TEST_SUITE_P(smoke, - ConvertMOEToMOE3GemmCompressedTest, - Values(element::f16, element::f32), - ConvertMOEToMOE3GemmCompressedTest::get_test_case_name); - -// Test symmetric weight compression (i4 weights, scale-only, no zero points) -class ConvertMOEToMOE3GemmSymmetricCompressedTest : public TransformationTestsF, public WithParamInterface { -public: - static std::string get_test_case_name(testing::TestParamInfo obj) { - ov::element::Type element_type = obj.param; - std::ostringstream result; - result << "ElementType=" << element_type.get_type_name(); - return result.str(); - } -}; - -TEST_P(ConvertMOEToMOE3GemmSymmetricCompressedTest, GEMM3SwiGLUSymmetric) { - disable_rt_info_check(); - const auto& data_type = GetParam(); - { - // tokens:32, hidden_size:2048, iter_size:768, experts:128, topk:8 - auto hidden_states = std::make_shared(data_type, Shape{32, 2048}); - auto routing_weights = std::make_shared(data_type, Shape{128, 1, 32, 1}); - auto routing_idx = std::make_shared(element::i32, Shape{32, 8}); - - // Gate projection (symmetric: Convert -> Multiply, no Subtract) - auto wei_gate = op::v0::Constant::create(element::i4, Shape{128, 768, 16, 128}, {1}); - auto scale_gate = op::v0::Constant::create(element::f16, Shape{128, 768, 16, 1}, {0.01f}); - auto reshape_const_gate = op::v0::Constant::create(element::i32, Shape{3}, {128, 768, 2048}); - - auto w_gate_f16 = std::make_shared(wei_gate, element::f16); - auto mul_gate = std::make_shared(w_gate_f16, scale_gate); - auto reshape_gate = std::make_shared(mul_gate, reshape_const_gate, false); - auto convert_gate = std::make_shared(reshape_gate, element::f32); - - // Up projection (symmetric: Convert -> Multiply, no Subtract) - auto wei_up = op::v0::Constant::create(element::i4, Shape{128, 768, 16, 128}, {1}); - auto scale_up = op::v0::Constant::create(element::f16, Shape{128, 768, 16, 1}, {0.01f}); - auto reshape_const_up = op::v0::Constant::create(element::i32, Shape{3}, {128, 768, 2048}); - - auto w_up_f16 = std::make_shared(wei_up, element::f16); - auto mul_up = std::make_shared(w_up_f16, scale_up); - auto reshape_up = std::make_shared(mul_up, reshape_const_up, false); - auto convert_up = std::make_shared(reshape_up, element::f32); - - // Down projection (symmetric: Convert -> Multiply, no Subtract) - auto wei_down = op::v0::Constant::create(element::i4, Shape{128, 2048, 6, 128}, {1}); - auto scale_down = op::v0::Constant::create(element::f16, Shape{128, 2048, 6, 1}, {0.01f}); - auto reshape_const_down = op::v0::Constant::create(element::i32, Shape{3}, {128, 2048, 768}); - - auto wei_down_f16 = std::make_shared(wei_down, element::f16); - auto mul_down = std::make_shared(wei_down_f16, scale_down); - auto reshape_down = std::make_shared(mul_down, reshape_const_down, false); - auto convert_down = std::make_shared(reshape_down, element::f32); - - // Construct MOE node - ov::op::internal::MOE::Config config; - config.expert_type = ov::op::internal::MOE::Expert_type::GEMM3_SWIGLU; - auto moe = - std::make_shared(ov::OutputVector{hidden_states, routing_weights, routing_idx, convert_gate, convert_up, convert_down}, - config); - model = std::make_shared(moe, ov::ParameterVector{hidden_states, routing_weights, routing_idx}); - manager.register_pass(0); - } - { - // Inputs - auto hidden_states = std::make_shared(data_type, Shape{32, 2048}); - auto routing_weights = std::make_shared(data_type, Shape{128, 1, 32, 1}); - auto routing_idx = std::make_shared(element::i32, Shape{32, 8}); - - // Gate and up projection - auto reshape_const_gate_up = op::v0::Constant::create(element::i32, Shape{3}, {128, 768, 16}); - auto transpose_const_gate_up = op::v0::Constant::create(element::i32, Shape{3}, {0, 2, 1}); - - auto wei_gate = op::v0::Constant::create(element::i4, Shape{128, 768, 16, 128}, {1}); - auto scale_gate = op::v0::Constant::create(element::f16, Shape{128, 768, 16, 1}, {0.01f}); - auto scale_reshape_gate = std::make_shared(scale_gate, reshape_const_gate_up, false); - auto scale_transpose_gate = std::make_shared(scale_reshape_gate, transpose_const_gate_up); - // Scalar dummy ZP for symmetric (has_zp=false) — minimizes memory allocation - auto zp_gate_dummy = op::v0::Constant::create(element::i4, Shape{1}, {0}); - - auto wei_up = op::v0::Constant::create(element::i4, Shape{128, 768, 16, 128}, {1}); - auto scale_up = op::v0::Constant::create(element::f16, Shape{128, 768, 16, 1}, {0.01f}); - auto scale_reshape_up = std::make_shared(scale_up, reshape_const_gate_up, false); - auto scale_transpose_up = std::make_shared(scale_reshape_up, transpose_const_gate_up); - // Scalar dummy ZP for symmetric (has_zp=false) — minimizes memory allocation - auto zp_up_dummy = op::v0::Constant::create(element::i4, Shape{1}, {0}); - - // Down projection - auto wei_down = op::v0::Constant::create(element::i4, Shape{128, 2048, 6, 128}, {1}); - auto scale_down = op::v0::Constant::create(element::f16, Shape{128, 2048, 6, 1}, {0.01f}); - auto reshape_const_down = op::v0::Constant::create(element::i32, Shape{3}, {128, 2048, 6}); - auto transpose_const_down = op::v0::Constant::create(element::i32, Shape{3}, {0, 2, 1}); - auto scale_reshape_down = std::make_shared(scale_down, reshape_const_down, false); - auto scale_transpose_down = std::make_shared(scale_reshape_down, transpose_const_down); - // Scalar dummy ZP for symmetric (has_zp=false) — minimizes memory allocation - auto zp_down_dummy = op::v0::Constant::create(element::i4, Shape{1}, {0}); - - ov::intel_gpu::op::MOECompressed::Config config; - config.hidden_size = 2048; - config.inter_size = 768; - config.num_expert = 128; - config.top_k = 8; - config.group_size = 128; - config.out_type = ov::element::f16; - config.has_zp = false; - std::shared_ptr moe_compressed = std::make_shared(ov::OutputVector{hidden_states, - routing_weights, - routing_idx, - wei_gate, - scale_transpose_gate, - zp_gate_dummy, - wei_up, - scale_transpose_up, - zp_up_dummy, - wei_down, - scale_transpose_down, - zp_down_dummy}, - config); - if (config.out_type != data_type) { - moe_compressed = std::make_shared(moe_compressed, data_type); - } - model_ref = std::make_shared(moe_compressed, ov::ParameterVector{hidden_states, routing_weights, routing_idx}); - } -} - -INSTANTIATE_TEST_SUITE_P(smoke, - ConvertMOEToMOE3GemmSymmetricCompressedTest, - Values(element::f16, element::f32), - ConvertMOEToMOE3GemmSymmetricCompressedTest::get_test_case_name); - -TEST_F(TransformationTestsF, ConvertMOEToMOE3GemmSharedExpertCompressedTest) { - disable_rt_info_check(); - { - // Input: 10-input MOE (shared expert weights already absorbed by FuseMOESharedExpert) - // tokens:32, hidden_size:2048, inter_size:768, experts:128, topk:8 - auto hidden_states = std::make_shared(element::f16, Shape{32, 2048}); - auto routing_weights = std::make_shared(element::f16, Shape{128, 1, 32, 1}); - auto routing_idx = std::make_shared(element::i32, Shape{32, 8}); - - // Gate projection decompression chain - auto wei_gate = op::v0::Constant::create(element::u4, Shape{128, 768, 16, 128}, {1}); - auto zp_gate = op::v0::Constant::create(element::u4, Shape{128, 768, 16, 1}, {0}); - auto scale_gate = op::v0::Constant::create(element::f16, Shape{128, 768, 16, 1}, {0.01f}); - auto reshape_const_gate = op::v0::Constant::create(element::i32, Shape{3}, {128, 768, 2048}); - auto w_gate_f16 = std::make_shared(wei_gate, element::f16); - auto zp_gate_f16 = std::make_shared(zp_gate, element::f16); - auto sub_gate = std::make_shared(w_gate_f16, zp_gate_f16); - auto mul_gate = std::make_shared(sub_gate, scale_gate); - auto reshape_gate = std::make_shared(mul_gate, reshape_const_gate, false); - auto convert_gate = std::make_shared(reshape_gate, element::f32); - - // Up projection decompression chain - auto wei_up = op::v0::Constant::create(element::u4, Shape{128, 768, 16, 128}, {1}); - auto zp_up = op::v0::Constant::create(element::u4, Shape{128, 768, 16, 1}, {0}); - auto scale_up = op::v0::Constant::create(element::f16, Shape{128, 768, 16, 1}, {0.01f}); - auto reshape_const_up = op::v0::Constant::create(element::i32, Shape{3}, {128, 768, 2048}); - auto w_up_f16 = std::make_shared(wei_up, element::f16); - auto zp_up_f16 = std::make_shared(zp_up, element::f16); - auto sub_up = std::make_shared(w_up_f16, zp_up_f16); - auto mul_up = std::make_shared(sub_up, scale_up); - auto reshape_up = std::make_shared(mul_up, reshape_const_up, false); - auto convert_up = std::make_shared(reshape_up, element::f32); - - // Down projection decompression chain - auto wei_down = op::v0::Constant::create(element::u4, Shape{128, 2048, 6, 128}, {1}); - auto zp_down = op::v0::Constant::create(element::u4, Shape{128, 2048, 6, 1}, {0}); - auto scale_down = op::v0::Constant::create(element::f16, Shape{128, 2048, 6, 1}, {0.01f}); - auto reshape_const_down = op::v0::Constant::create(element::i32, Shape{3}, {128, 2048, 768}); - auto wei_down_f16 = std::make_shared(wei_down, element::f16); - auto zp_down_f16 = std::make_shared(zp_down, element::f16); - auto sub_down = std::make_shared(wei_down_f16, zp_down_f16); - auto mul_down = std::make_shared(sub_down, scale_down); - auto reshape_down = std::make_shared(mul_down, reshape_const_down, false); - auto convert_down = std::make_shared(reshape_down, element::f32); - - // Shared expert decompression chains (3D without num_experts dim) - auto sh_wei_gate = op::v0::Constant::create(element::u4, Shape{768, 16, 128}, {2}); - auto sh_zp_gate = op::v0::Constant::create(element::u4, Shape{768, 16, 1}, {1}); - auto sh_scale_gate = op::v0::Constant::create(element::f16, Shape{768, 16, 1}, {0.02f}); - auto sh_reshape_const_gate = op::v0::Constant::create(element::i32, Shape{2}, {768, 2048}); - auto sh_w_gate_f16 = std::make_shared(sh_wei_gate, element::f16); - auto sh_zp_gate_f16 = std::make_shared(sh_zp_gate, element::f16); - auto sh_sub_gate = std::make_shared(sh_w_gate_f16, sh_zp_gate_f16); - auto sh_mul_gate = std::make_shared(sh_sub_gate, sh_scale_gate); - auto sh_reshape_gate = std::make_shared(sh_mul_gate, sh_reshape_const_gate, false); - auto sh_convert_gate = std::make_shared(sh_reshape_gate, element::f32); - - auto sh_wei_up = op::v0::Constant::create(element::u4, Shape{768, 16, 128}, {2}); - auto sh_zp_up = op::v0::Constant::create(element::u4, Shape{768, 16, 1}, {1}); - auto sh_scale_up = op::v0::Constant::create(element::f16, Shape{768, 16, 1}, {0.02f}); - auto sh_reshape_const_up = op::v0::Constant::create(element::i32, Shape{2}, {768, 2048}); - auto sh_w_up_f16 = std::make_shared(sh_wei_up, element::f16); - auto sh_zp_up_f16 = std::make_shared(sh_zp_up, element::f16); - auto sh_sub_up = std::make_shared(sh_w_up_f16, sh_zp_up_f16); - auto sh_mul_up = std::make_shared(sh_sub_up, sh_scale_up); - auto sh_reshape_up = std::make_shared(sh_mul_up, sh_reshape_const_up, false); - auto sh_convert_up = std::make_shared(sh_reshape_up, element::f32); - - auto sh_wei_down = op::v0::Constant::create(element::u4, Shape{2048, 6, 128}, {2}); - auto sh_zp_down = op::v0::Constant::create(element::u4, Shape{2048, 6, 1}, {1}); - auto sh_scale_down = op::v0::Constant::create(element::f16, Shape{2048, 6, 1}, {0.02f}); - auto sh_reshape_const_down = op::v0::Constant::create(element::i32, Shape{2}, {2048, 768}); - auto sh_w_down_f16 = std::make_shared(sh_wei_down, element::f16); - auto sh_zp_down_f16 = std::make_shared(sh_zp_down, element::f16); - auto sh_sub_down = std::make_shared(sh_w_down_f16, sh_zp_down_f16); - auto sh_mul_down = std::make_shared(sh_sub_down, sh_scale_down); - auto sh_reshape_down = std::make_shared(sh_mul_down, sh_reshape_const_down, false); - auto sh_convert_down = std::make_shared(sh_reshape_down, element::f32); - - // Gate_gate weight (passed through as-is by FuseMOESharedExpert) - auto shared_gate_gate_wei_m = std::make_shared(element::f32, Shape{2048, 1}, std::vector(2048, 1.0f)); - - // Construct 10-input MOE node (as produced by FuseMOESharedExpert) - ov::op::internal::MOE::Config config; - config.expert_type = ov::op::internal::MOE::Expert_type::GEMM3_SWIGLU; - auto hidden_states_f32 = std::make_shared(hidden_states, element::f32); - auto moe = std::make_shared( - ov::OutputVector{hidden_states_f32, routing_weights, routing_idx, - convert_gate, convert_up, convert_down, - sh_convert_gate, sh_convert_up, sh_convert_down, - shared_gate_gate_wei_m}, config); - model = std::make_shared(moe, ov::ParameterVector{hidden_states, routing_weights, routing_idx}); - manager.register_pass(0); - } - { - // Inputs - auto hidden_states = std::make_shared(element::f16, Shape{32, 2048}); - auto routing_weights = std::make_shared(element::f16, Shape{128, 1, 32, 1}); - auto routing_idx = std::make_shared(element::i32, Shape{32, 8}); - - // Gate and up projection - auto reshape_const_gate_up = op::v0::Constant::create(element::i32, Shape{3}, {128, 768, 16}); - auto transpose_const_gate_up = op::v0::Constant::create(element::i32, Shape{3}, {0, 2, 1}); - - auto wei_gate = op::v0::Constant::create(element::u4, Shape{128, 768, 16, 128}, {1}); - auto zp_gate = op::v0::Constant::create(element::u4, Shape{128, 768, 16, 1}, {0}); - auto scale_gate = op::v0::Constant::create(element::f16, Shape{128, 768, 16, 1}, {0.01f}); - auto zp_reshape_gate = std::make_shared(zp_gate, reshape_const_gate_up, false); - auto zp_transpose_gate = std::make_shared(zp_reshape_gate, transpose_const_gate_up); - auto scale_reshape_gate = std::make_shared(scale_gate, reshape_const_gate_up, false); - auto scale_transpose_gate = std::make_shared(scale_reshape_gate, transpose_const_gate_up); - - auto wei_up = op::v0::Constant::create(element::u4, Shape{128, 768, 16, 128}, {1}); - auto zp_up = op::v0::Constant::create(element::u4, Shape{128, 768, 16, 1}, {0}); - auto scale_up = op::v0::Constant::create(element::f16, Shape{128, 768, 16, 1}, {0.01f}); - auto zp_reshape_up = std::make_shared(zp_up, reshape_const_gate_up, false); - auto zp_transpose_up = std::make_shared(zp_reshape_up, transpose_const_gate_up); - auto scale_reshape_up = std::make_shared(scale_up, reshape_const_gate_up, false); - auto scale_transpose_up = std::make_shared(scale_reshape_up, transpose_const_gate_up); - - // Down projection - auto wei_down = op::v0::Constant::create(element::u4, Shape{128, 2048, 6, 128}, {1}); - auto zp_down = op::v0::Constant::create(element::u4, Shape{128, 2048, 6, 1}, {0}); - auto scale_down = op::v0::Constant::create(element::f16, Shape{128, 2048, 6, 1}, {0.01f}); - auto reshape_const_down = op::v0::Constant::create(element::i32, Shape{3}, {128, 2048, 6}); - auto transpose_const_down = op::v0::Constant::create(element::i32, Shape{3}, {0, 2, 1}); - auto zp_reshape_down = std::make_shared(zp_down, reshape_const_down, false); - auto zp_transpose_down = std::make_shared(zp_reshape_down, transpose_const_down); - auto scale_reshape_down = std::make_shared(scale_down, reshape_const_down, false); - auto scale_transpose_down = std::make_shared(scale_reshape_down, transpose_const_down); - - // Shared expert weights (3D without num_experts dim, separate from MOE expert weights) - auto sh_reshape_const_gate_up = op::v0::Constant::create(element::i32, Shape{2}, {768, 16}); - auto sh_transpose_const_gate_up = op::v0::Constant::create(element::i32, Shape{2}, {1, 0}); - - auto sh_wei_gate = op::v0::Constant::create(element::u4, Shape{768, 16, 128}, {2}); - auto sh_zp_gate = op::v0::Constant::create(element::u4, Shape{768, 16, 1}, {1}); - auto sh_scale_gate = op::v0::Constant::create(element::f16, Shape{768, 16, 1}, {0.02f}); - auto sh_zp_reshape_gate = std::make_shared(sh_zp_gate, sh_reshape_const_gate_up, false); - auto sh_zp_transpose_gate = std::make_shared(sh_zp_reshape_gate, sh_transpose_const_gate_up); - auto sh_scale_reshape_gate = std::make_shared(sh_scale_gate, sh_reshape_const_gate_up, false); - auto sh_scale_transpose_gate = std::make_shared(sh_scale_reshape_gate, sh_transpose_const_gate_up); - - auto sh_wei_up = op::v0::Constant::create(element::u4, Shape{768, 16, 128}, {2}); - auto sh_zp_up = op::v0::Constant::create(element::u4, Shape{768, 16, 1}, {1}); - auto sh_scale_up = op::v0::Constant::create(element::f16, Shape{768, 16, 1}, {0.02f}); - auto sh_zp_reshape_up = std::make_shared(sh_zp_up, sh_reshape_const_gate_up, false); - auto sh_zp_transpose_up = std::make_shared(sh_zp_reshape_up, sh_transpose_const_gate_up); - auto sh_scale_reshape_up = std::make_shared(sh_scale_up, sh_reshape_const_gate_up, false); - auto sh_scale_transpose_up = std::make_shared(sh_scale_reshape_up, sh_transpose_const_gate_up); - - auto sh_reshape_const_down = op::v0::Constant::create(element::i32, Shape{2}, {2048, 6}); - auto sh_transpose_const_down = op::v0::Constant::create(element::i32, Shape{2}, {1, 0}); - auto sh_wei_down = op::v0::Constant::create(element::u4, Shape{2048, 6, 128}, {2}); - auto sh_zp_down = op::v0::Constant::create(element::u4, Shape{2048, 6, 1}, {1}); - auto sh_scale_down = op::v0::Constant::create(element::f16, Shape{2048, 6, 1}, {0.02f}); - auto sh_zp_reshape_down = std::make_shared(sh_zp_down, sh_reshape_const_down, false); - auto sh_zp_transpose_down = std::make_shared(sh_zp_reshape_down, sh_transpose_const_down); - auto sh_scale_reshape_down = std::make_shared(sh_scale_down, sh_reshape_const_down, false); - auto sh_scale_transpose_down = std::make_shared(sh_scale_reshape_down, sh_transpose_const_down); - - ov::intel_gpu::op::MOECompressed::Config config; - config.hidden_size = 2048; - config.inter_size = 768; - config.num_expert = 128; - config.num_shared_expert = 1; - config.top_k = 8; - config.group_size = 128; - config.out_type = ov::element::f16; - auto hidden_states_f32 = std::make_shared(hidden_states, element::f32); - auto shared_gate_gate_wei_m = std::make_shared(element::f32, Shape{2048, 1}, std::vector(2048, 1.0f)); - std::shared_ptr moe_compressed = std::make_shared( - ov::OutputVector{hidden_states_f32, routing_weights, routing_idx, - wei_gate, scale_transpose_gate, zp_transpose_gate, - wei_up, scale_transpose_up, zp_transpose_up, - wei_down, scale_transpose_down, zp_transpose_down, - sh_wei_gate, sh_scale_transpose_gate, sh_zp_transpose_gate, - sh_wei_up, sh_scale_transpose_up, sh_zp_transpose_up, - sh_wei_down, sh_scale_transpose_down, sh_zp_transpose_down, - shared_gate_gate_wei_m}, config); - // MOECompressed outputs f16, but the input MOE outputs f32, - // so the transformation inserts a Convert(f16->f32) to preserve the original output type. - moe_compressed = std::make_shared(moe_compressed, element::f32); - model_ref = std::make_shared(moe_compressed, ov::ParameterVector{hidden_states, routing_weights, routing_idx}); - } -} - -// Test asymmetric weight compression with u8 weights (Convert -> Subtract(zp) -> Multiply(scale)) -class ConvertMOEToMOE3GemmAsymmetricU8CompressedTest : public TransformationTestsF, public WithParamInterface { -public: - static std::string get_test_case_name(testing::TestParamInfo obj) { - ov::element::Type element_type = obj.param; - std::ostringstream result; - result << "ElementType=" << element_type.get_type_name(); - return result.str(); - } -}; - -TEST_P(ConvertMOEToMOE3GemmAsymmetricU8CompressedTest, GEMM3SwiGLUAsymU8) { - disable_rt_info_check(); - const auto& data_type = GetParam(); - { - // tokens:32, hidden_size:2048, iter_size:768, experts:128, topk:8, group_size:128 - auto hidden_states = std::make_shared(data_type, Shape{32, 2048}); - auto routing_weights = std::make_shared(data_type, Shape{128, 1, 32, 1}); - auto routing_idx = std::make_shared(element::i32, Shape{32, 8}); - - // Gate projection (asymmetric u8: Convert -> Subtract -> Multiply) - auto wei_gate = op::v0::Constant::create(element::u8, Shape{128, 768, 16, 128}, {1}); - auto zp_gate = op::v0::Constant::create(element::u8, Shape{128, 768, 16, 1}, {0}); - auto scale_gate = op::v0::Constant::create(element::f16, Shape{128, 768, 16, 1}, {0.01f}); - auto reshape_const_gate = op::v0::Constant::create(element::i32, Shape{3}, {128, 768, 2048}); - - auto w_gate_f16 = std::make_shared(wei_gate, element::f16); - auto zp_gate_f16 = std::make_shared(zp_gate, element::f16); - auto sub_gate = std::make_shared(w_gate_f16, zp_gate_f16); - auto mul_gate = std::make_shared(sub_gate, scale_gate); - auto reshape_gate = std::make_shared(mul_gate, reshape_const_gate, false); - auto convert_gate = std::make_shared(reshape_gate, element::f32); - - // Up projection (asymmetric u8) - auto wei_up = op::v0::Constant::create(element::u8, Shape{128, 768, 16, 128}, {1}); - auto zp_up = op::v0::Constant::create(element::u8, Shape{128, 768, 16, 1}, {0}); - auto scale_up = op::v0::Constant::create(element::f16, Shape{128, 768, 16, 1}, {0.01f}); - auto reshape_const_up = op::v0::Constant::create(element::i32, Shape{3}, {128, 768, 2048}); - - auto w_up_f16 = std::make_shared(wei_up, element::f16); - auto zp_up_f16 = std::make_shared(zp_up, element::f16); - auto sub_up = std::make_shared(w_up_f16, zp_up_f16); - auto mul_up = std::make_shared(sub_up, scale_up); - auto reshape_up = std::make_shared(mul_up, reshape_const_up, false); - auto convert_up = std::make_shared(reshape_up, element::f32); - - // Down projection (asymmetric u8) - auto wei_down = op::v0::Constant::create(element::u8, Shape{128, 2048, 6, 128}, {1}); - auto zp_down = op::v0::Constant::create(element::u8, Shape{128, 2048, 6, 1}, {0}); - auto scale_down = op::v0::Constant::create(element::f16, Shape{128, 2048, 6, 1}, {0.01f}); - auto reshape_const_down = op::v0::Constant::create(element::i32, Shape{3}, {128, 2048, 768}); - - auto wei_down_f16 = std::make_shared(wei_down, element::f16); - auto zp_down_f16 = std::make_shared(zp_down, element::f16); - auto sub_down = std::make_shared(wei_down_f16, zp_down_f16); - auto mul_down = std::make_shared(sub_down, scale_down); - auto reshape_down = std::make_shared(mul_down, reshape_const_down, false); - auto convert_down = std::make_shared(reshape_down, element::f32); - - ov::op::internal::MOE::Config config; - config.expert_type = ov::op::internal::MOE::Expert_type::GEMM3_SWIGLU; - auto moe = - std::make_shared(ov::OutputVector{hidden_states, routing_weights, routing_idx, convert_gate, convert_up, convert_down}, - config); - model = std::make_shared(moe, ov::ParameterVector{hidden_states, routing_weights, routing_idx}); - manager.register_pass(0); - } - { - auto hidden_states = std::make_shared(data_type, Shape{32, 2048}); - auto routing_weights = std::make_shared(data_type, Shape{128, 1, 32, 1}); - auto routing_idx = std::make_shared(element::i32, Shape{32, 8}); - - auto reshape_const_gate_up = op::v0::Constant::create(element::i32, Shape{3}, {128, 768, 16}); - auto transpose_const_gate_up = op::v0::Constant::create(element::i32, Shape{3}, {0, 2, 1}); - - auto wei_gate = op::v0::Constant::create(element::u8, Shape{128, 768, 16, 128}, {1}); - auto zp_gate = op::v0::Constant::create(element::u8, Shape{128, 768, 16, 1}, {0}); - auto scale_gate = op::v0::Constant::create(element::f16, Shape{128, 768, 16, 1}, {0.01f}); - auto zp_reshape_gate = std::make_shared(zp_gate, reshape_const_gate_up, false); - auto zp_transpose_gate = std::make_shared(zp_reshape_gate, transpose_const_gate_up); - auto scale_reshape_gate = std::make_shared(scale_gate, reshape_const_gate_up, false); - auto scale_transpose_gate = std::make_shared(scale_reshape_gate, transpose_const_gate_up); - - auto wei_up = op::v0::Constant::create(element::u8, Shape{128, 768, 16, 128}, {1}); - auto zp_up = op::v0::Constant::create(element::u8, Shape{128, 768, 16, 1}, {0}); - auto scale_up = op::v0::Constant::create(element::f16, Shape{128, 768, 16, 1}, {0.01f}); - auto zp_reshape_up = std::make_shared(zp_up, reshape_const_gate_up, false); - auto zp_transpose_up = std::make_shared(zp_reshape_up, transpose_const_gate_up); - auto scale_reshape_up = std::make_shared(scale_up, reshape_const_gate_up, false); - auto scale_transpose_up = std::make_shared(scale_reshape_up, transpose_const_gate_up); - - auto wei_down = op::v0::Constant::create(element::u8, Shape{128, 2048, 6, 128}, {1}); - auto zp_down = op::v0::Constant::create(element::u8, Shape{128, 2048, 6, 1}, {0}); - auto scale_down = op::v0::Constant::create(element::f16, Shape{128, 2048, 6, 1}, {0.01f}); - auto reshape_const_down = op::v0::Constant::create(element::i32, Shape{3}, {128, 2048, 6}); - auto transpose_const_down = op::v0::Constant::create(element::i32, Shape{3}, {0, 2, 1}); - auto zp_reshape_down = std::make_shared(zp_down, reshape_const_down, false); - auto zp_transpose_down = std::make_shared(zp_reshape_down, transpose_const_down); - auto scale_reshape_down = std::make_shared(scale_down, reshape_const_down, false); - auto scale_transpose_down = std::make_shared(scale_reshape_down, transpose_const_down); - - ov::intel_gpu::op::MOECompressed::Config config; - config.hidden_size = 2048; - config.inter_size = 768; - config.num_expert = 128; - config.top_k = 8; - config.group_size = 128; - config.out_type = ov::element::f16; - std::shared_ptr moe_compressed = std::make_shared(ov::OutputVector{hidden_states, - routing_weights, - routing_idx, - wei_gate, - scale_transpose_gate, - zp_transpose_gate, - wei_up, - scale_transpose_up, - zp_transpose_up, - wei_down, - scale_transpose_down, - zp_transpose_down}, - config); - if (config.out_type != data_type) { - moe_compressed = std::make_shared(moe_compressed, data_type); - } - model_ref = std::make_shared(moe_compressed, ov::ParameterVector{hidden_states, routing_weights, routing_idx}); - } -} - -INSTANTIATE_TEST_SUITE_P(smoke, - ConvertMOEToMOE3GemmAsymmetricU8CompressedTest, - Values(element::f16, element::f32), - ConvertMOEToMOE3GemmAsymmetricU8CompressedTest::get_test_case_name); - -// Test asymmetric u8 weight compression with shared expert (10-input MOE, post-FuseMOESharedExpert) -TEST_F(TransformationTestsF, ConvertMOEToMOE3GemmSharedExpertAsymU8CompressedTest) { - disable_rt_info_check(); - { - // Input: 10-input MOE (shared expert weights already absorbed by FuseMOESharedExpert) - auto hidden_states = std::make_shared(element::f16, Shape{32, 2048}); - auto routing_weights = std::make_shared(element::f16, Shape{128, 1, 32, 1}); - auto routing_idx = std::make_shared(element::i32, Shape{32, 8}); - - // Expert gate projection (asymmetric u8) - auto wei_gate = op::v0::Constant::create(element::u8, Shape{128, 768, 16, 128}, {1}); - auto zp_gate = op::v0::Constant::create(element::u8, Shape{128, 768, 16, 1}, {0}); - auto scale_gate = op::v0::Constant::create(element::f16, Shape{128, 768, 16, 1}, {0.01f}); - auto reshape_const_gate = op::v0::Constant::create(element::i32, Shape{3}, {128, 768, 2048}); - auto w_gate_f16 = std::make_shared(wei_gate, element::f16); - auto zp_gate_f16 = std::make_shared(zp_gate, element::f16); - auto sub_gate = std::make_shared(w_gate_f16, zp_gate_f16); - auto mul_gate = std::make_shared(sub_gate, scale_gate); - auto reshape_gate = std::make_shared(mul_gate, reshape_const_gate, false); - auto convert_gate = std::make_shared(reshape_gate, element::f32); - - // Expert up projection (asymmetric u8) - auto wei_up = op::v0::Constant::create(element::u8, Shape{128, 768, 16, 128}, {1}); - auto zp_up = op::v0::Constant::create(element::u8, Shape{128, 768, 16, 1}, {0}); - auto scale_up = op::v0::Constant::create(element::f16, Shape{128, 768, 16, 1}, {0.01f}); - auto reshape_const_up = op::v0::Constant::create(element::i32, Shape{3}, {128, 768, 2048}); - auto w_up_f16 = std::make_shared(wei_up, element::f16); - auto zp_up_f16 = std::make_shared(zp_up, element::f16); - auto sub_up = std::make_shared(w_up_f16, zp_up_f16); - auto mul_up = std::make_shared(sub_up, scale_up); - auto reshape_up = std::make_shared(mul_up, reshape_const_up, false); - auto convert_up = std::make_shared(reshape_up, element::f32); - - // Expert down projection (asymmetric u8) - auto wei_down = op::v0::Constant::create(element::u8, Shape{128, 2048, 6, 128}, {1}); - auto zp_down = op::v0::Constant::create(element::u8, Shape{128, 2048, 6, 1}, {0}); - auto scale_down = op::v0::Constant::create(element::f16, Shape{128, 2048, 6, 1}, {0.01f}); - auto reshape_const_down = op::v0::Constant::create(element::i32, Shape{3}, {128, 2048, 768}); - auto wei_down_f16 = std::make_shared(wei_down, element::f16); - auto zp_down_f16 = std::make_shared(zp_down, element::f16); - auto sub_down = std::make_shared(wei_down_f16, zp_down_f16); - auto mul_down = std::make_shared(sub_down, scale_down); - auto reshape_down = std::make_shared(mul_down, reshape_const_down, false); - auto convert_down = std::make_shared(reshape_down, element::f32); - - // Shared expert decompression chains (asymmetric u8, 3D without num_experts dim) - auto sh_wei_gate = op::v0::Constant::create(element::u8, Shape{768, 16, 128}, {2}); - auto sh_zp_gate = op::v0::Constant::create(element::u8, Shape{768, 16, 1}, {1}); - auto sh_scale_gate = op::v0::Constant::create(element::f16, Shape{768, 16, 1}, {0.02f}); - auto sh_reshape_const_gate = op::v0::Constant::create(element::i32, Shape{2}, {768, 2048}); - auto sh_w_gate_f16 = std::make_shared(sh_wei_gate, element::f16); - auto sh_zp_gate_f16 = std::make_shared(sh_zp_gate, element::f16); - auto sh_sub_gate = std::make_shared(sh_w_gate_f16, sh_zp_gate_f16); - auto sh_mul_gate = std::make_shared(sh_sub_gate, sh_scale_gate); - auto sh_reshape_gate = std::make_shared(sh_mul_gate, sh_reshape_const_gate, false); - auto sh_convert_gate = std::make_shared(sh_reshape_gate, element::f32); - - auto sh_wei_up = op::v0::Constant::create(element::u8, Shape{768, 16, 128}, {2}); - auto sh_zp_up = op::v0::Constant::create(element::u8, Shape{768, 16, 1}, {1}); - auto sh_scale_up = op::v0::Constant::create(element::f16, Shape{768, 16, 1}, {0.02f}); - auto sh_reshape_const_up = op::v0::Constant::create(element::i32, Shape{2}, {768, 2048}); - auto sh_w_up_f16 = std::make_shared(sh_wei_up, element::f16); - auto sh_zp_up_f16 = std::make_shared(sh_zp_up, element::f16); - auto sh_sub_up = std::make_shared(sh_w_up_f16, sh_zp_up_f16); - auto sh_mul_up = std::make_shared(sh_sub_up, sh_scale_up); - auto sh_reshape_up = std::make_shared(sh_mul_up, sh_reshape_const_up, false); - auto sh_convert_up = std::make_shared(sh_reshape_up, element::f32); - - auto sh_wei_down = op::v0::Constant::create(element::u8, Shape{2048, 6, 128}, {2}); - auto sh_zp_down = op::v0::Constant::create(element::u8, Shape{2048, 6, 1}, {1}); - auto sh_scale_down = op::v0::Constant::create(element::f16, Shape{2048, 6, 1}, {0.02f}); - auto sh_reshape_const_down = op::v0::Constant::create(element::i32, Shape{2}, {2048, 768}); - auto sh_w_down_f16 = std::make_shared(sh_wei_down, element::f16); - auto sh_zp_down_f16 = std::make_shared(sh_zp_down, element::f16); - auto sh_sub_down = std::make_shared(sh_w_down_f16, sh_zp_down_f16); - auto sh_mul_down = std::make_shared(sh_sub_down, sh_scale_down); - auto sh_reshape_down = std::make_shared(sh_mul_down, sh_reshape_const_down, false); - auto sh_convert_down = std::make_shared(sh_reshape_down, element::f32); - - // Gate_gate weight - auto shared_gate_gate_wei_m = std::make_shared(element::f32, Shape{2048, 1}, std::vector(2048, 1.0f)); - - // Construct 10-input MOE node (as produced by FuseMOESharedExpert) - ov::op::internal::MOE::Config config; - config.expert_type = ov::op::internal::MOE::Expert_type::GEMM3_SWIGLU; - auto hidden_states_f32 = std::make_shared(hidden_states, element::f32); - auto moe = std::make_shared( - ov::OutputVector{hidden_states_f32, routing_weights, routing_idx, - convert_gate, convert_up, convert_down, - sh_convert_gate, sh_convert_up, sh_convert_down, - shared_gate_gate_wei_m}, config); - model = std::make_shared(moe, ov::ParameterVector{hidden_states, routing_weights, routing_idx}); - manager.register_pass(0); - } - { - auto hidden_states = std::make_shared(element::f16, Shape{32, 2048}); - auto routing_weights = std::make_shared(element::f16, Shape{128, 1, 32, 1}); - auto routing_idx = std::make_shared(element::i32, Shape{32, 8}); - - auto reshape_const_gate_up = op::v0::Constant::create(element::i32, Shape{3}, {128, 768, 16}); - auto transpose_const_gate_up = op::v0::Constant::create(element::i32, Shape{3}, {0, 2, 1}); - - auto wei_gate = op::v0::Constant::create(element::u8, Shape{128, 768, 16, 128}, {1}); - auto zp_gate = op::v0::Constant::create(element::u8, Shape{128, 768, 16, 1}, {0}); - auto scale_gate = op::v0::Constant::create(element::f16, Shape{128, 768, 16, 1}, {0.01f}); - auto zp_reshape_gate = std::make_shared(zp_gate, reshape_const_gate_up, false); - auto zp_transpose_gate = std::make_shared(zp_reshape_gate, transpose_const_gate_up); - auto scale_reshape_gate = std::make_shared(scale_gate, reshape_const_gate_up, false); - auto scale_transpose_gate = std::make_shared(scale_reshape_gate, transpose_const_gate_up); - - auto wei_up = op::v0::Constant::create(element::u8, Shape{128, 768, 16, 128}, {1}); - auto zp_up = op::v0::Constant::create(element::u8, Shape{128, 768, 16, 1}, {0}); - auto scale_up = op::v0::Constant::create(element::f16, Shape{128, 768, 16, 1}, {0.01f}); - auto zp_reshape_up = std::make_shared(zp_up, reshape_const_gate_up, false); - auto zp_transpose_up = std::make_shared(zp_reshape_up, transpose_const_gate_up); - auto scale_reshape_up = std::make_shared(scale_up, reshape_const_gate_up, false); - auto scale_transpose_up = std::make_shared(scale_reshape_up, transpose_const_gate_up); - - auto wei_down = op::v0::Constant::create(element::u8, Shape{128, 2048, 6, 128}, {1}); - auto zp_down = op::v0::Constant::create(element::u8, Shape{128, 2048, 6, 1}, {0}); - auto scale_down = op::v0::Constant::create(element::f16, Shape{128, 2048, 6, 1}, {0.01f}); - auto reshape_const_down = op::v0::Constant::create(element::i32, Shape{3}, {128, 2048, 6}); - auto transpose_const_down = op::v0::Constant::create(element::i32, Shape{3}, {0, 2, 1}); - auto zp_reshape_down = std::make_shared(zp_down, reshape_const_down, false); - auto zp_transpose_down = std::make_shared(zp_reshape_down, transpose_const_down); - auto scale_reshape_down = std::make_shared(scale_down, reshape_const_down, false); - auto scale_transpose_down = std::make_shared(scale_reshape_down, transpose_const_down); - - // Shared expert weights (3D) - auto sh_reshape_const_gate_up = op::v0::Constant::create(element::i32, Shape{2}, {768, 16}); - auto sh_transpose_const_gate_up = op::v0::Constant::create(element::i32, Shape{2}, {1, 0}); - - auto sh_wei_gate = op::v0::Constant::create(element::u8, Shape{768, 16, 128}, {2}); - auto sh_zp_gate = op::v0::Constant::create(element::u8, Shape{768, 16, 1}, {1}); - auto sh_scale_gate = op::v0::Constant::create(element::f16, Shape{768, 16, 1}, {0.02f}); - auto sh_zp_reshape_gate = std::make_shared(sh_zp_gate, sh_reshape_const_gate_up, false); - auto sh_zp_transpose_gate = std::make_shared(sh_zp_reshape_gate, sh_transpose_const_gate_up); - auto sh_scale_reshape_gate = std::make_shared(sh_scale_gate, sh_reshape_const_gate_up, false); - auto sh_scale_transpose_gate = std::make_shared(sh_scale_reshape_gate, sh_transpose_const_gate_up); - - auto sh_wei_up = op::v0::Constant::create(element::u8, Shape{768, 16, 128}, {2}); - auto sh_zp_up = op::v0::Constant::create(element::u8, Shape{768, 16, 1}, {1}); - auto sh_scale_up = op::v0::Constant::create(element::f16, Shape{768, 16, 1}, {0.02f}); - auto sh_zp_reshape_up = std::make_shared(sh_zp_up, sh_reshape_const_gate_up, false); - auto sh_zp_transpose_up = std::make_shared(sh_zp_reshape_up, sh_transpose_const_gate_up); - auto sh_scale_reshape_up = std::make_shared(sh_scale_up, sh_reshape_const_gate_up, false); - auto sh_scale_transpose_up = std::make_shared(sh_scale_reshape_up, sh_transpose_const_gate_up); - - auto sh_reshape_const_down = op::v0::Constant::create(element::i32, Shape{2}, {2048, 6}); - auto sh_transpose_const_down = op::v0::Constant::create(element::i32, Shape{2}, {1, 0}); - auto sh_wei_down = op::v0::Constant::create(element::u8, Shape{2048, 6, 128}, {2}); - auto sh_zp_down = op::v0::Constant::create(element::u8, Shape{2048, 6, 1}, {1}); - auto sh_scale_down = op::v0::Constant::create(element::f16, Shape{2048, 6, 1}, {0.02f}); - auto sh_zp_reshape_down = std::make_shared(sh_zp_down, sh_reshape_const_down, false); - auto sh_zp_transpose_down = std::make_shared(sh_zp_reshape_down, sh_transpose_const_down); - auto sh_scale_reshape_down = std::make_shared(sh_scale_down, sh_reshape_const_down, false); - auto sh_scale_transpose_down = std::make_shared(sh_scale_reshape_down, sh_transpose_const_down); - - ov::intel_gpu::op::MOECompressed::Config config; - config.hidden_size = 2048; - config.inter_size = 768; - config.num_expert = 128; - config.num_shared_expert = 1; - config.top_k = 8; - config.group_size = 128; - config.out_type = ov::element::f16; - auto hidden_states_f32 = std::make_shared(hidden_states, element::f32); - auto shared_gate_gate_wei_m = std::make_shared(element::f32, Shape{2048, 1}, std::vector(2048, 1.0f)); - std::shared_ptr moe_compressed = std::make_shared( - ov::OutputVector{hidden_states_f32, routing_weights, routing_idx, - wei_gate, scale_transpose_gate, zp_transpose_gate, - wei_up, scale_transpose_up, zp_transpose_up, - wei_down, scale_transpose_down, zp_transpose_down, - sh_wei_gate, sh_scale_transpose_gate, sh_zp_transpose_gate, - sh_wei_up, sh_scale_transpose_up, sh_zp_transpose_up, - sh_wei_down, sh_scale_transpose_down, sh_zp_transpose_down, - shared_gate_gate_wei_m}, config); - moe_compressed = std::make_shared(moe_compressed, element::f32); - model_ref = std::make_shared(moe_compressed, ov::ParameterVector{hidden_states, routing_weights, routing_idx}); - } -} - -// Test symmetric i4 weight compression with shared expert (10-input MOE, post-FuseMOESharedExpert) -TEST_F(TransformationTestsF, ConvertMOEToMOE3GemmSharedExpertSymmetricCompressedTest) { - disable_rt_info_check(); - { - // Input: 10-input MOE with sym-quant expert weights (Convert -> Mul, no Subtract) - auto hidden_states = std::make_shared(element::f16, Shape{32, 2048}); - auto routing_weights = std::make_shared(element::f16, Shape{128, 1, 32, 1}); - auto routing_idx = std::make_shared(element::i32, Shape{32, 8}); - - // Expert gate (symmetric i4: Convert -> Multiply(scale), no Subtract) - auto wei_gate = op::v0::Constant::create(element::i4, Shape{128, 768, 16, 128}, {1}); - auto scale_gate = op::v0::Constant::create(element::f16, Shape{128, 768, 16, 1}, {0.01f}); - auto reshape_const_gate = op::v0::Constant::create(element::i32, Shape{3}, {128, 768, 2048}); - auto w_gate_f16 = std::make_shared(wei_gate, element::f16); - auto mul_gate = std::make_shared(w_gate_f16, scale_gate); - auto reshape_gate = std::make_shared(mul_gate, reshape_const_gate, false); - auto convert_gate = std::make_shared(reshape_gate, element::f32); - - // Expert up (symmetric i4) - auto wei_up = op::v0::Constant::create(element::i4, Shape{128, 768, 16, 128}, {1}); - auto scale_up = op::v0::Constant::create(element::f16, Shape{128, 768, 16, 1}, {0.01f}); - auto reshape_const_up = op::v0::Constant::create(element::i32, Shape{3}, {128, 768, 2048}); - auto w_up_f16 = std::make_shared(wei_up, element::f16); - auto mul_up = std::make_shared(w_up_f16, scale_up); - auto reshape_up = std::make_shared(mul_up, reshape_const_up, false); - auto convert_up = std::make_shared(reshape_up, element::f32); - - // Expert down (symmetric i4) - auto wei_down = op::v0::Constant::create(element::i4, Shape{128, 2048, 6, 128}, {1}); - auto scale_down = op::v0::Constant::create(element::f16, Shape{128, 2048, 6, 1}, {0.01f}); - auto reshape_const_down = op::v0::Constant::create(element::i32, Shape{3}, {128, 2048, 768}); - auto wei_down_f16 = std::make_shared(wei_down, element::f16); - auto mul_down = std::make_shared(wei_down_f16, scale_down); - auto reshape_down = std::make_shared(mul_down, reshape_const_down, false); - auto convert_down = std::make_shared(reshape_down, element::f32); - - // Shared expert (symmetric i4, 3D without num_experts dim) - auto sh_wei_gate = op::v0::Constant::create(element::i4, Shape{768, 16, 128}, {2}); - auto sh_scale_gate = op::v0::Constant::create(element::f16, Shape{768, 16, 1}, {0.02f}); - auto sh_reshape_const_gate = op::v0::Constant::create(element::i32, Shape{2}, {768, 2048}); - auto sh_w_gate_f16 = std::make_shared(sh_wei_gate, element::f16); - auto sh_mul_gate = std::make_shared(sh_w_gate_f16, sh_scale_gate); - auto sh_reshape_gate = std::make_shared(sh_mul_gate, sh_reshape_const_gate, false); - auto sh_convert_gate = std::make_shared(sh_reshape_gate, element::f32); - - auto sh_wei_up = op::v0::Constant::create(element::i4, Shape{768, 16, 128}, {2}); - auto sh_scale_up = op::v0::Constant::create(element::f16, Shape{768, 16, 1}, {0.02f}); - auto sh_reshape_const_up = op::v0::Constant::create(element::i32, Shape{2}, {768, 2048}); - auto sh_w_up_f16 = std::make_shared(sh_wei_up, element::f16); - auto sh_mul_up = std::make_shared(sh_w_up_f16, sh_scale_up); - auto sh_reshape_up = std::make_shared(sh_mul_up, sh_reshape_const_up, false); - auto sh_convert_up = std::make_shared(sh_reshape_up, element::f32); - - auto sh_wei_down = op::v0::Constant::create(element::i4, Shape{2048, 6, 128}, {2}); - auto sh_scale_down = op::v0::Constant::create(element::f16, Shape{2048, 6, 1}, {0.02f}); - auto sh_reshape_const_down = op::v0::Constant::create(element::i32, Shape{2}, {2048, 768}); - auto sh_w_down_f16 = std::make_shared(sh_wei_down, element::f16); - auto sh_mul_down = std::make_shared(sh_w_down_f16, sh_scale_down); - auto sh_reshape_down = std::make_shared(sh_mul_down, sh_reshape_const_down, false); - auto sh_convert_down = std::make_shared(sh_reshape_down, element::f32); - - // Gate_gate weight - auto shared_gate_gate_wei_m = std::make_shared(element::f32, Shape{2048, 1}, std::vector(2048, 1.0f)); - - // Construct 10-input MOE node - ov::op::internal::MOE::Config config; - config.expert_type = ov::op::internal::MOE::Expert_type::GEMM3_SWIGLU; - auto hidden_states_f32 = std::make_shared(hidden_states, element::f32); - auto moe = std::make_shared( - ov::OutputVector{hidden_states_f32, routing_weights, routing_idx, - convert_gate, convert_up, convert_down, - sh_convert_gate, sh_convert_up, sh_convert_down, - shared_gate_gate_wei_m}, config); - model = std::make_shared(moe, ov::ParameterVector{hidden_states, routing_weights, routing_idx}); - manager.register_pass(0); - } - { - auto hidden_states = std::make_shared(element::f16, Shape{32, 2048}); - auto routing_weights = std::make_shared(element::f16, Shape{128, 1, 32, 1}); - auto routing_idx = std::make_shared(element::i32, Shape{32, 8}); - - auto reshape_const_gate_up = op::v0::Constant::create(element::i32, Shape{3}, {128, 768, 16}); - auto transpose_const_gate_up = op::v0::Constant::create(element::i32, Shape{3}, {0, 2, 1}); - - // Expert weights (sym: no ZP, dummy scalar ZP inserted) - auto wei_gate = op::v0::Constant::create(element::i4, Shape{128, 768, 16, 128}, {1}); - auto scale_gate = op::v0::Constant::create(element::f16, Shape{128, 768, 16, 1}, {0.01f}); - auto scale_reshape_gate = std::make_shared(scale_gate, reshape_const_gate_up, false); - auto scale_transpose_gate = std::make_shared(scale_reshape_gate, transpose_const_gate_up); - auto zp_gate_dummy = op::v0::Constant::create(element::i4, Shape{1}, {0}); - - auto wei_up = op::v0::Constant::create(element::i4, Shape{128, 768, 16, 128}, {1}); - auto scale_up = op::v0::Constant::create(element::f16, Shape{128, 768, 16, 1}, {0.01f}); - auto scale_reshape_up = std::make_shared(scale_up, reshape_const_gate_up, false); - auto scale_transpose_up = std::make_shared(scale_reshape_up, transpose_const_gate_up); - auto zp_up_dummy = op::v0::Constant::create(element::i4, Shape{1}, {0}); - - auto wei_down = op::v0::Constant::create(element::i4, Shape{128, 2048, 6, 128}, {1}); - auto scale_down = op::v0::Constant::create(element::f16, Shape{128, 2048, 6, 1}, {0.01f}); - auto reshape_const_down = op::v0::Constant::create(element::i32, Shape{3}, {128, 2048, 6}); - auto transpose_const_down = op::v0::Constant::create(element::i32, Shape{3}, {0, 2, 1}); - auto scale_reshape_down = std::make_shared(scale_down, reshape_const_down, false); - auto scale_transpose_down = std::make_shared(scale_reshape_down, transpose_const_down); - auto zp_down_dummy = op::v0::Constant::create(element::i4, Shape{1}, {0}); - - // Shared expert weights (sym, 3D) - auto sh_reshape_const_gate_up = op::v0::Constant::create(element::i32, Shape{2}, {768, 16}); - auto sh_transpose_const_gate_up = op::v0::Constant::create(element::i32, Shape{2}, {1, 0}); - - auto sh_wei_gate = op::v0::Constant::create(element::i4, Shape{768, 16, 128}, {2}); - auto sh_scale_gate = op::v0::Constant::create(element::f16, Shape{768, 16, 1}, {0.02f}); - auto sh_scale_reshape_gate = std::make_shared(sh_scale_gate, sh_reshape_const_gate_up, false); - auto sh_scale_transpose_gate = std::make_shared(sh_scale_reshape_gate, sh_transpose_const_gate_up); - auto sh_zp_gate_dummy = op::v0::Constant::create(element::i4, Shape{1}, {0}); - - auto sh_wei_up = op::v0::Constant::create(element::i4, Shape{768, 16, 128}, {2}); - auto sh_scale_up = op::v0::Constant::create(element::f16, Shape{768, 16, 1}, {0.02f}); - auto sh_scale_reshape_up = std::make_shared(sh_scale_up, sh_reshape_const_gate_up, false); - auto sh_scale_transpose_up = std::make_shared(sh_scale_reshape_up, sh_transpose_const_gate_up); - auto sh_zp_up_dummy = op::v0::Constant::create(element::i4, Shape{1}, {0}); - - auto sh_reshape_const_down = op::v0::Constant::create(element::i32, Shape{2}, {2048, 6}); - auto sh_transpose_const_down = op::v0::Constant::create(element::i32, Shape{2}, {1, 0}); - auto sh_wei_down = op::v0::Constant::create(element::i4, Shape{2048, 6, 128}, {2}); - auto sh_scale_down = op::v0::Constant::create(element::f16, Shape{2048, 6, 1}, {0.02f}); - auto sh_scale_reshape_down = std::make_shared(sh_scale_down, sh_reshape_const_down, false); - auto sh_scale_transpose_down = std::make_shared(sh_scale_reshape_down, sh_transpose_const_down); - auto sh_zp_down_dummy = op::v0::Constant::create(element::i4, Shape{1}, {0}); - - ov::intel_gpu::op::MOECompressed::Config config; - config.hidden_size = 2048; - config.inter_size = 768; - config.num_expert = 128; - config.num_shared_expert = 1; - config.top_k = 8; - config.group_size = 128; - config.out_type = ov::element::f16; - config.has_zp = false; - auto hidden_states_f32 = std::make_shared(hidden_states, element::f32); - auto shared_gate_gate_wei_m = std::make_shared(element::f32, Shape{2048, 1}, std::vector(2048, 1.0f)); - std::shared_ptr moe_compressed = std::make_shared( - ov::OutputVector{hidden_states_f32, routing_weights, routing_idx, - wei_gate, scale_transpose_gate, zp_gate_dummy, - wei_up, scale_transpose_up, zp_up_dummy, - wei_down, scale_transpose_down, zp_down_dummy, - sh_wei_gate, sh_scale_transpose_gate, sh_zp_gate_dummy, - sh_wei_up, sh_scale_transpose_up, sh_zp_up_dummy, - sh_wei_down, sh_scale_transpose_down, sh_zp_down_dummy, - shared_gate_gate_wei_m}, config); - moe_compressed = std::make_shared(moe_compressed, element::f32); - model_ref = std::make_shared(moe_compressed, ov::ParameterVector{hidden_states, routing_weights, routing_idx}); - } -} - -} // namespace intel_gpu -} // namespace test -} // namespace ov diff --git a/src/plugins/intel_gpu/tests/unit/transformations/disable_fp16_compression_cumsum_sin_gen_test.cpp b/src/plugins/intel_gpu/tests/unit/transformations/disable_fp16_compression_cumsum_sin_gen_test.cpp new file mode 100644 index 000000000000..b2e536785b30 --- /dev/null +++ b/src/plugins/intel_gpu/tests/unit/transformations/disable_fp16_compression_cumsum_sin_gen_test.cpp @@ -0,0 +1,199 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "openvino/op/constant.hpp" +#include "openvino/op/cum_sum.hpp" +#include "openvino/op/interpolate.hpp" +#include "openvino/op/multiply.hpp" +#include "openvino/op/parameter.hpp" +#include "openvino/op/sin.hpp" +#include "openvino/op/transpose.hpp" +#include "plugin/transformations/disable_fp16_comp_cumsum_sin_gen.hpp" + +using namespace testing; +using namespace ov::intel_gpu; + +// Friendly names used to look up the matched nodes after the pass has run. +namespace { +const std::string name_cumsum_input = "cumsum_input"; +const std::string name_cumsum = "cumsum"; +const std::string name_mul1 = "mul1"; +const std::string name_transpose2 = "transpose2"; +const std::string name_mul2 = "mul2"; +const std::string name_interpolate = "interpolate"; +const std::string name_transpose3 = "transpose3"; +const std::string name_sin = "sin"; + +// Build the full chain matched by DisableFP16CompCumSumSinGen: +// producer -> CumSum -> Multiply -> Transpose -> Multiply -> Interpolate -> Transpose -> Sin +std::shared_ptr create_model_to_match() { + auto input = std::make_shared(ov::element::f32, ov::PartialShape{1, 32}); + + // Extra Multiply so the test can verify the CumSum producer gets marked. + auto producer_const = ov::op::v0::Constant::create(ov::element::f32, ov::Shape{1, 32}, {1.0f}); + auto cumsum_input = std::make_shared(input, producer_const); + cumsum_input->set_friendly_name(name_cumsum_input); + + auto axis = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{}, {1}); + auto cumsum = std::make_shared(cumsum_input, axis); + cumsum->set_friendly_name(name_cumsum); + + auto mul1_const = ov::op::v0::Constant::create(ov::element::f32, ov::Shape{1, 32}, {6.2832f}); + auto mul1 = std::make_shared(cumsum, mul1_const); + mul1->set_friendly_name(name_mul1); + + auto order2 = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{2}, {1, 0}); + auto transpose2 = std::make_shared(mul1, order2); + transpose2->set_friendly_name(name_transpose2); + + auto mul2_const = ov::op::v0::Constant::create(ov::element::f32, ov::Shape{32, 1}, {1.0f}); + auto mul2 = std::make_shared(transpose2, mul2_const); + mul2->set_friendly_name(name_mul2); + + ov::op::v4::Interpolate::InterpolateAttrs attrs; + attrs.mode = ov::op::v4::Interpolate::InterpolateMode::NEAREST; + attrs.shape_calculation_mode = ov::op::v4::Interpolate::ShapeCalcMode::SCALES; + attrs.nearest_mode = ov::op::v4::Interpolate::NearestMode::ROUND_PREFER_FLOOR; + auto target_shape = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{2}, {64, 1}); + auto scales = ov::op::v0::Constant::create(ov::element::f32, ov::Shape{2}, {2.0f, 1.0f}); + auto interpolate = std::make_shared(mul2, target_shape, scales, attrs); + interpolate->set_friendly_name(name_interpolate); + + auto order3 = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{2}, {1, 0}); + auto transpose3 = std::make_shared(interpolate, order3); + transpose3->set_friendly_name(name_transpose3); + + auto sin = std::make_shared(transpose3); + sin->set_friendly_name(name_sin); + + return std::make_shared(ov::OutputVector{sin}, ov::ParameterVector{input}); +} + +// A bare Sin (no upstream CumSum) — must not be matched. +std::shared_ptr create_model_not_to_match() { + auto input = std::make_shared(ov::element::f32, ov::PartialShape{1, 32}); + + auto mul_const = ov::op::v0::Constant::create(ov::element::f32, ov::Shape{1, 32}, {1.0f}); + auto mul = std::make_shared(input, mul_const); + mul->set_friendly_name(name_mul1); + + auto sin = std::make_shared(mul); + sin->set_friendly_name(name_sin); + + return std::make_shared(ov::OutputVector{sin}, ov::ParameterVector{input}); +} + +// Same chain but without the second Multiply between Transpose_2 and +// Interpolate — must not match. +std::shared_ptr create_model_missing_mul2() { + auto input = std::make_shared(ov::element::f32, ov::PartialShape{1, 32}); + + auto axis = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{}, {1}); + auto cumsum = std::make_shared(input, axis); + cumsum->set_friendly_name(name_cumsum); + + auto mul1_const = ov::op::v0::Constant::create(ov::element::f32, ov::Shape{1, 32}, {6.2832f}); + auto mul1 = std::make_shared(cumsum, mul1_const); + mul1->set_friendly_name(name_mul1); + + auto order2 = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{2}, {1, 0}); + auto transpose2 = std::make_shared(mul1, order2); + transpose2->set_friendly_name(name_transpose2); + + // NOTE: no second Multiply here. + + ov::op::v4::Interpolate::InterpolateAttrs attrs; + attrs.mode = ov::op::v4::Interpolate::InterpolateMode::NEAREST; + attrs.shape_calculation_mode = ov::op::v4::Interpolate::ShapeCalcMode::SCALES; + attrs.nearest_mode = ov::op::v4::Interpolate::NearestMode::ROUND_PREFER_FLOOR; + auto target_shape = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{2}, {64, 1}); + auto scales = ov::op::v0::Constant::create(ov::element::f32, ov::Shape{2}, {2.0f, 1.0f}); + auto interpolate = std::make_shared(transpose2, target_shape, scales, attrs); + interpolate->set_friendly_name(name_interpolate); + + auto order3 = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{2}, {1, 0}); + auto transpose3 = std::make_shared(interpolate, order3); + transpose3->set_friendly_name(name_transpose3); + + auto sin = std::make_shared(transpose3); + sin->set_friendly_name(name_sin); + + return std::make_shared(ov::OutputVector{sin}, ov::ParameterVector{input}); +} + +void run_test(const std::shared_ptr& model, + const std::unordered_map& expected_fp16_disabled_status) { + ov::pass::Manager manager; + manager.register_pass(); + + precisions_map fp_convert_precision_map = {{ov::element::f32, ov::element::f16}}; + manager.register_pass(fp_convert_precision_map); + + manager.run_passes(model); + + for (const auto& op : model->get_ops()) { + auto it = expected_fp16_disabled_status.find(op->get_friendly_name()); + if (it == expected_fp16_disabled_status.end()) + continue; + if (it->second) { + ASSERT_TRUE(ov::is_conversion_disabled(op, ov::element::f16)) + << "FP16 compression is not disabled for node: " << op->get_friendly_name(); + } else { + ASSERT_FALSE(ov::is_conversion_disabled(op, ov::element::f16)) + << "FP16 compression is unexpectedly disabled for node: " << op->get_friendly_name(); + } + } +} +} // namespace + +TEST(TransformationTests, DisableFP16CompCumSumSinGen_Positive) { + auto model = create_model_to_match(); + // The pass marks the full 7-node chain plus the producer feeding CumSum. + std::unordered_map expected_status = { + {name_cumsum_input, true}, + {name_cumsum, true}, + {name_mul1, true}, + {name_transpose2, true}, + {name_mul2, true}, + {name_interpolate, true}, + {name_transpose3, true}, + {name_sin, true}, + }; + run_test(model, expected_status); +} + +TEST(TransformationTests, DisableFP16CompCumSumSinGen_NoCumSumUpstream_NoOp) { + auto model = create_model_not_to_match(); + std::unordered_map expected_status = { + {name_mul1, false}, + {name_sin, false}, + }; + run_test(model, expected_status); +} + +TEST(TransformationTests, DisableFP16CompCumSumSinGen_MissingSecondMultiply_NoOp) { + auto model = create_model_missing_mul2(); + // No second Multiply — pattern must not match. + std::unordered_map expected_status = { + {name_cumsum, false}, + {name_mul1, false}, + {name_transpose2, false}, + {name_interpolate, false}, + {name_transpose3, false}, + {name_sin, false}, + }; + run_test(model, expected_status); +} diff --git a/src/plugins/intel_gpu/tests/unit/transformations/disable_fp16_compression_rms_test.cpp b/src/plugins/intel_gpu/tests/unit/transformations/disable_fp16_compression_rms_test.cpp index 6746383360be..454b7085015e 100644 --- a/src/plugins/intel_gpu/tests/unit/transformations/disable_fp16_compression_rms_test.cpp +++ b/src/plugins/intel_gpu/tests/unit/transformations/disable_fp16_compression_rms_test.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include "plugin/transformations/disable_fp16_comp_rms.hpp" #include "ov_ops/rms.hpp" @@ -96,10 +97,10 @@ static void run_test(std::shared_ptr model, if (it != expected_fp16_disabled_status.end()) { bool expected_status = it->second; if (expected_status) { - ASSERT_TRUE(ov::fp16_compression_is_disabled(op)) + ASSERT_TRUE(ov::is_conversion_disabled(op, ov::element::f16)) << "FP16 compression is not disabled for node: " << op->get_friendly_name(); } else { - ASSERT_FALSE(ov::fp16_compression_is_disabled(op)) + ASSERT_FALSE(ov::is_conversion_disabled(op, ov::element::f16)) << "FP16 compression is unexpectedly disabled for node: " << op->get_friendly_name(); } } diff --git a/src/plugins/intel_gpu/tests/unit/transformations/disable_fp16_compression_sin_gen_test.cpp b/src/plugins/intel_gpu/tests/unit/transformations/disable_fp16_compression_sin_gen_test.cpp index 0fd0c4b48df7..8dece3ca3093 100644 --- a/src/plugins/intel_gpu/tests/unit/transformations/disable_fp16_compression_sin_gen_test.cpp +++ b/src/plugins/intel_gpu/tests/unit/transformations/disable_fp16_compression_sin_gen_test.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include "plugin/transformations/disable_fp16_comp_sin_gen.hpp" #include "openvino/op/parameter.hpp" @@ -95,10 +96,10 @@ static void run_test(std::shared_ptr model, if (it != expected_fp16_disabled_status.end()) { bool expected_status = it->second; if (expected_status) { - ASSERT_TRUE(ov::fp16_compression_is_disabled(op)) + ASSERT_TRUE(ov::is_conversion_disabled(op, ov::element::f16)) << "FP16 compression is not disabled for node: " << op->get_friendly_name(); } else { - ASSERT_FALSE(ov::fp16_compression_is_disabled(op)) + ASSERT_FALSE(ov::is_conversion_disabled(op, ov::element::f16)) << "FP16 compression is unexpectedly disabled for node: " << op->get_friendly_name(); } } diff --git a/src/plugins/intel_gpu/tests/unit/transformations/fold_activation_transpose_test.cpp b/src/plugins/intel_gpu/tests/unit/transformations/fold_activation_transpose_test.cpp new file mode 100644 index 000000000000..b5b234f759af --- /dev/null +++ b/src/plugins/intel_gpu/tests/unit/transformations/fold_activation_transpose_test.cpp @@ -0,0 +1,169 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "common_test_utils/ov_test_utils.hpp" + +#include "openvino/core/model.hpp" +#include "openvino/op/constant.hpp" +#include "openvino/op/multiply.hpp" +#include "openvino/op/parameter.hpp" +#include "openvino/op/swish.hpp" +#include "openvino/op/transpose.hpp" +#include "openvino/pass/manager.hpp" + +#include "plugin/transformations/fold_activation_transpose.hpp" + +using namespace testing; +using namespace ov::intel_gpu; + +namespace ov { +namespace test { +namespace intel_gpu { + +// Valid case +TEST_F(TransformationTestsF, FoldActivationTranspose1) { + { + auto input1 = std::make_shared(ov::element::f32, ov::PartialShape{-1, 16, 3, 7}); + auto order1 = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{4}, {0, 3, 1, 2}); + auto transpose1 = std::make_shared(input1, order1); + auto swish = std::make_shared(transpose1); + auto input2 = std::make_shared(ov::element::f32, ov::PartialShape{-1, 16, 3, 7}); + auto order2 = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{4}, {0, 3, 1, 2}); + auto transpose2 = std::make_shared(input2, order2); + auto mul = std::make_shared(swish, transpose2); + auto order3 = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{4}, {0, 2, 3, 1}); + auto transpose3 = std::make_shared(mul, order3); + transpose3->set_friendly_name("Multiply_transpose"); + + model = std::make_shared(ov::OutputVector{transpose3}, ov::ParameterVector{input1, input2}); + manager.register_pass(); + } + { + auto input1 = std::make_shared(ov::element::f32, ov::PartialShape{-1, 16, 3, 7}); + auto swish = std::make_shared(input1); + auto input2 = std::make_shared(ov::element::f32, ov::PartialShape{-1, 16, 3, 7}); + auto mul = std::make_shared(swish, input2); + mul->set_friendly_name("Multiply_transpose"); + + model_ref = std::make_shared(ov::OutputVector{mul}, ov::ParameterVector{input1, input2}); + } +} + +// Mismatching output transpose order +TEST_F(TransformationTestsF, FoldActivationTranspose2) { + { + auto input1 = std::make_shared(ov::element::f32, ov::PartialShape{-1, 16, 3, 7}); + auto order1 = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{4}, {0, 3, 1, 2}); + auto transpose1 = std::make_shared(input1, order1); + auto swish = std::make_shared(transpose1); + auto input2 = std::make_shared(ov::element::f32, ov::PartialShape{-1, 16, 3, 7}); + auto order2 = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{4}, {0, 3, 1, 2}); + auto transpose2 = std::make_shared(input2, order2); + auto mul = std::make_shared(swish, transpose2); + auto order3 = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{4}, {0, 1, 3, 2}); + auto transpose3 = std::make_shared(mul, order3); + + model = std::make_shared(ov::OutputVector{transpose3}, ov::ParameterVector{input1, input2}); + manager.register_pass(); + } + { + model_ref = model->clone(); + } +} + +// Mismatching input1 layout +TEST_F(TransformationTestsF, FoldActivationTranspose3) { + { + auto input1 = std::make_shared(ov::element::f32, ov::PartialShape{-1, 16, 7, 3}); + auto order1 = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{4}, {0, 2, 1, 3}); + auto transpose1 = std::make_shared(input1, order1); + auto swish = std::make_shared(transpose1); + auto input2 = std::make_shared(ov::element::f32, ov::PartialShape{-1, 16, 3, 7}); + auto order2 = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{4}, {0, 3, 1, 2}); + auto transpose2 = std::make_shared(input2, order2); + auto mul = std::make_shared(swish, transpose2); + auto order3 = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{4}, {0, 2, 3, 1}); + auto transpose3 = std::make_shared(mul, order3); + + model = std::make_shared(ov::OutputVector{transpose3}, ov::ParameterVector{input1, input2}); + manager.register_pass(); + } + { + model_ref = model->clone(); + } +} + +// Mismatching input2 layout +TEST_F(TransformationTestsF, FoldActivationTranspose4) { + { + auto input1 = std::make_shared(ov::element::f32, ov::PartialShape{-1, 16, 3, 7}); + auto order1 = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{4}, {0, 3, 1, 2}); + auto transpose1 = std::make_shared(input1, order1); + auto swish = std::make_shared(transpose1); + auto input2 = std::make_shared(ov::element::f32, ov::PartialShape{-1, 16, 7, 3}); + auto order2 = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{4}, {0, 2, 1, 3}); + auto transpose2 = std::make_shared(input2, order2); + auto mul = std::make_shared(swish, transpose2); + auto order3 = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{4}, {0, 2, 3, 1}); + auto transpose3 = std::make_shared(mul, order3); + + model = std::make_shared(ov::OutputVector{transpose3}, ov::ParameterVector{input1, input2}); + manager.register_pass(); + } + { + model_ref = model->clone(); + } +} + +// Swish extra consumer +TEST_F(TransformationTestsF, FoldActivationTranspose5) { + { + auto input1 = std::make_shared(ov::element::f32, ov::PartialShape{-1, 16, 3, 7}); + auto order1 = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{4}, {0, 3, 1, 2}); + auto transpose1 = std::make_shared(input1, order1); + auto swish = std::make_shared(transpose1); + auto square = std::make_shared(swish, swish); + auto input2 = std::make_shared(ov::element::f32, ov::PartialShape{-1, 16, 3, 7}); + auto order2 = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{4}, {0, 3, 1, 2}); + auto transpose2 = std::make_shared(input2, order2); + auto mul = std::make_shared(swish, transpose2); + auto order3 = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{4}, {0, 2, 3, 1}); + auto transpose3 = std::make_shared(mul, order3); + transpose3->set_friendly_name("Multiply_transpose"); + + model = std::make_shared(ov::OutputVector{transpose3, square}, ov::ParameterVector{input1, input2}); + manager.register_pass(); + } + { + model_ref = model->clone(); + } +} + +// Mul extra consumer +TEST_F(TransformationTestsF, FoldActivationTranspose6) { + { + auto input1 = std::make_shared(ov::element::f32, ov::PartialShape{-1, 16, 3, 7}); + auto order1 = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{4}, {0, 3, 1, 2}); + auto transpose1 = std::make_shared(input1, order1); + auto swish = std::make_shared(transpose1); + auto input2 = std::make_shared(ov::element::f32, ov::PartialShape{-1, 16, 3, 7}); + auto order2 = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{4}, {0, 3, 1, 2}); + auto transpose2 = std::make_shared(input2, order2); + auto mul = std::make_shared(swish, transpose2); + auto square = std::make_shared(mul, mul); + auto order3 = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{4}, {0, 2, 3, 1}); + auto transpose3 = std::make_shared(mul, order3); + transpose3->set_friendly_name("Multiply_transpose"); + + model = std::make_shared(ov::OutputVector{transpose3, square}, ov::ParameterVector{input1, input2}); + manager.register_pass(); + } + { + model_ref = model->clone(); + } +} + +} // namespace intel_gpu +} // namespace test +} // namespace ov diff --git a/src/plugins/intel_gpu/tests/unit/transformations/fuse_atan2_decomposed_test.cpp b/src/plugins/intel_gpu/tests/unit/transformations/fuse_atan2_decomposed_test.cpp new file mode 100644 index 000000000000..23d4706874f0 --- /dev/null +++ b/src/plugins/intel_gpu/tests/unit/transformations/fuse_atan2_decomposed_test.cpp @@ -0,0 +1,146 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include + +#include +#include + +#include +#include +#include "openvino/op/add.hpp" +#include "openvino/op/atan.hpp" +#include "openvino/op/constant.hpp" +#include "openvino/op/divide.hpp" +#include "openvino/op/equal.hpp" +#include "openvino/op/greater.hpp" +#include "openvino/op/greater_eq.hpp" +#include "openvino/op/less.hpp" +#include "openvino/op/logical_and.hpp" +#include "openvino/op/logical_or.hpp" +#include "openvino/op/multiply.hpp" +#include "openvino/op/parameter.hpp" +#include "openvino/op/power.hpp" +#include "openvino/op/result.hpp" +#include "openvino/op/select.hpp" + +#include +#include + +#include "common_test_utils/ov_test_utils.hpp" + +using namespace testing; +using namespace ov::intel_gpu; + +namespace { + +// Constructs the post-ConvertDivide form of the frontend atan2 decomposition, +// matching what FuseAtan2Decomposed sees in the GPU pipeline. +struct Atan2DecomposedModel { + std::shared_ptr y; // lhs (imag) + std::shared_ptr x; // rhs (real) + std::shared_ptr root; // Sel3 + std::shared_ptr model; +}; + +Atan2DecomposedModel build_decomposed_atan2(ov::element::Type et, bool use_divide) { + Atan2DecomposedModel m; + m.y = std::make_shared(et, ov::PartialShape{-1, -1}); + m.x = std::make_shared(et, ov::PartialShape{-1, -1}); + + std::shared_ptr div_out; + if (use_divide) { + div_out = std::make_shared(m.y, m.x); + } else { + auto neg_one = std::make_shared(et, ov::Shape{}, std::vector{-1.0f}); + auto pow = std::make_shared(m.x, neg_one); + div_out = std::make_shared(m.y, pow); + } + auto atan = std::make_shared(div_out); + + auto pi = std::make_shared(et, ov::Shape{}, std::vector{3.14159265f}); + auto neg_pi = std::make_shared(et, ov::Shape{}, std::vector{-3.14159265f}); + auto half_pi = std::make_shared(et, ov::Shape{}, std::vector{1.57079633f}); + auto neg_half_pi = std::make_shared(et, ov::Shape{}, std::vector{-1.57079633f}); + auto zero = std::make_shared(et, ov::Shape{}, std::vector{0.0f}); + + auto atan_plus_pi = std::make_shared(atan, pi); + auto atan_minus_pi = std::make_shared(atan, neg_pi); + + auto x_lt = std::make_shared(m.x, zero); + auto y_ge = std::make_shared(m.y, zero); + auto add_pi_cond = std::make_shared(x_lt, y_ge); + auto sel1 = std::make_shared(add_pi_cond, atan_plus_pi, atan_minus_pi); + + auto x_gt = std::make_shared(m.x, zero); + auto sel2 = std::make_shared(x_gt, atan, sel1); + + auto x_eq = std::make_shared(m.x, zero); + auto y_gt = std::make_shared(m.y, zero); + auto y_lt = std::make_shared(m.y, zero); + auto half_pi_cond = std::make_shared(x_eq, y_gt); + auto neg_half_pi_cond = std::make_shared(x_eq, y_lt); + auto special_cond = std::make_shared(half_pi_cond, neg_half_pi_cond); + auto special_select = std::make_shared(half_pi_cond, half_pi, neg_half_pi); + + m.root = std::make_shared(special_cond, special_select, sel2); + m.model = std::make_shared(ov::OutputVector{m.root}, ov::ParameterVector{m.y, m.x}); + return m; +} + +bool model_contains_atan2(const std::shared_ptr& m) { + for (const auto& node : m->get_ordered_ops()) { + if (ov::as_type_ptr(node)) + return true; + } + return false; +} + +} // namespace + +TEST(FuseAtan2DecomposedTest, FusesPostConvertDivideForm) { + auto m = build_decomposed_atan2(ov::element::f16, /*use_divide=*/false); + + ov::pass::Manager manager; + manager.register_pass(); + manager.run_passes(m.model); + + ASSERT_TRUE(model_contains_atan2(m.model)) << "Atan2 op should be inserted"; + + // Result should now consume the Atan2 directly. + ASSERT_EQ(m.model->get_results().size(), 1u); + auto result_input = m.model->get_results()[0]->get_input_node_shared_ptr(0); + auto atan2 = ov::as_type_ptr(result_input); + ASSERT_TRUE(atan2 != nullptr) << "Result must be fed by Atan2"; + + // input0 = y, input1 = x. + EXPECT_EQ(atan2->get_input_node_shared_ptr(0).get(), m.y.get()); + EXPECT_EQ(atan2->get_input_node_shared_ptr(1).get(), m.x.get()); +} + +TEST(FuseAtan2DecomposedTest, FusesDivideForm) { + auto m = build_decomposed_atan2(ov::element::f32, /*use_divide=*/true); + + ov::pass::Manager manager; + manager.register_pass(); + manager.run_passes(m.model); + + ASSERT_TRUE(model_contains_atan2(m.model)); +} + +TEST(FuseAtan2DecomposedTest, DoesNotFuseUnrelatedSelect) { + // A bare Select(cond, x, y) without the surrounding atan structure must + // not be touched. + auto cond = std::make_shared(ov::element::boolean, ov::PartialShape{-1}); + auto a = std::make_shared(ov::element::f32, ov::PartialShape{-1}); + auto b = std::make_shared(ov::element::f32, ov::PartialShape{-1}); + auto sel = std::make_shared(cond, a, b); + auto model = std::make_shared(ov::OutputVector{sel}, ov::ParameterVector{cond, a, b}); + + ov::pass::Manager manager; + manager.register_pass(); + manager.run_passes(model); + + EXPECT_FALSE(model_contains_atan2(model)); +} diff --git a/src/plugins/intel_gpu/tests/unit/transformations/fuse_moe_3gemm_compressed_test.cpp b/src/plugins/intel_gpu/tests/unit/transformations/fuse_moe_3gemm_compressed_test.cpp deleted file mode 100644 index ed908864f365..000000000000 --- a/src/plugins/intel_gpu/tests/unit/transformations/fuse_moe_3gemm_compressed_test.cpp +++ /dev/null @@ -1,261 +0,0 @@ -// Copyright (C) 2018-2026 Intel Corporation -// SPDX-License-Identifier: Apache-2.0 -// - -#include - -#include "common_test_utils/ov_test_utils.hpp" -#include "intel_gpu/op/moe_compressed.hpp" -#include "intel_gpu/op/moe_3gemm_fused_compressed.hpp" -#include "openvino/op/constant.hpp" -#include "openvino/op/matmul.hpp" -#include "openvino/op/reshape.hpp" -#include "openvino/op/shape_of.hpp" -#include "plugin/transformations/fuse_moe_3gemm_compressed.hpp" -#include "common_test_utils/node_builders/moe_builders.hpp" - -using namespace testing; -using namespace ov::intel_gpu; - -namespace ov { -namespace test { -namespace intel_gpu { - -using namespace ov::test; - -using FuseMOE3GemmCompressedTestParams = std::tuple; - -class FuseMOE3GemmCompressedTest : public TransformationTestsF, - public ::testing::WithParamInterface { -public: - static std::string get_test_case_name(const ::testing::TestParamInfo& info) { - std::string name; - switch (std::get<0>(info.param)) { - case MoERoutingType::SOFTMAX: - name = "Softmax"; - break; - case MoERoutingType::SIGMOID_BIAS: - name = "SigmoidBias"; - break; - default: - OPENVINO_THROW("Unsupported routing type"); - } - if (std::get<1>(info.param)) - name += "_ReshapeOnMoeInput"; - return name; - } -}; - -TEST_P(FuseMOE3GemmCompressedTest, CompareFunctions) { - const auto& [routing_type, reshape_on_moe_input] = GetParam(); - { - // tokens:32, hidden_size:2048, inter_size:768, experts:128, topk:8 - auto hidden_states = std::make_shared(element::f16, Shape{4, 8, 2048}); - auto flatten_shape = op::v0::Constant::create(element::i32, Shape{2}, {32, 2048}); - auto hidden_states_reshape = std::make_shared(hidden_states, flatten_shape, false); - auto routers = op::v0::Constant::create(element::f16, Shape{2048, 128}, {0.2}); - auto routing_weights = std::make_shared(hidden_states_reshape, routers); - - // tokens:32, num_experts:128, topk:8 - auto routing_pair = routing_type == MoERoutingType::SOFTMAX - ? build_softmax_routing_subgraph(routing_weights, 128, 8) - : build_sigmoid_bias_routing_subgraph(routing_weights, element::f16, 128, 8); - auto unsqueeze_moe = routing_pair.first; - auto topk_indices = routing_pair.second; - - auto wei_gate = op::v0::Constant::create(element::u4, Shape{128, 768, 16, 128}, {1}); - auto scale_gate = op::v0::Constant::create(element::f16, Shape{128, 16, 768}, {0.01f}); - auto zp_gate = op::v0::Constant::create(element::u4, Shape{128, 16, 768}, {0}); - auto wei_up = op::v0::Constant::create(element::u4, Shape{128, 768, 16, 128}, {1}); - auto scale_up = op::v0::Constant::create(element::f16, Shape{128, 16, 768}, {0.01f}); - auto zp_up = op::v0::Constant::create(element::u4, Shape{128, 16, 768, 16}, {0}); - auto wei_down = op::v0::Constant::create(element::u4, Shape{128, 2048, 6, 128}, {1}); - auto scale_down = op::v0::Constant::create(element::f16, Shape{128, 6, 2048}, {0.01f}); - auto zp_down = op::v0::Constant::create(element::u4, Shape{128, 6, 2048}, {0}); - - ov::intel_gpu::op::MOECompressed::Config config; - config.hidden_size = 2048; - config.inter_size = 768; - config.num_expert = 128; - config.group_size = 128; - config.top_k = 8; - config.out_type = ov::element::f16; - - // When reshape_on_moe_input is true, MOECompressed gets the flattened 2D hidden_states_reshape; - // otherwise it gets the original 3D hidden_states. - auto moe_input_0 = reshape_on_moe_input - ? ov::Output(hidden_states_reshape) - : ov::Output(hidden_states); - auto moe_compressed = std::make_shared( - ov::OutputVector{moe_input_0, unsqueeze_moe, topk_indices, - wei_gate, scale_gate, zp_gate, wei_up, scale_up, zp_up, wei_down, scale_down, zp_down}, config); - model = std::make_shared(moe_compressed, ov::ParameterVector{hidden_states}); - } - manager.register_pass(); - { - // tokens:32, hidden_size:2048, inter_size:768, experts:128, topk:8 - auto hidden_states = std::make_shared(element::f16, Shape{4, 8, 2048}); - auto flatten_shape = op::v0::Constant::create(element::i32, Shape{2}, {32, 2048}); - auto hidden_states_reshape = std::make_shared(hidden_states, flatten_shape, false); - auto routers = op::v0::Constant::create(element::f16, Shape{2048, 128}, {0.2}); - auto routing_weights = std::make_shared(hidden_states_reshape, routers); - - auto wei_gate = op::v0::Constant::create(element::u4, Shape{128, 768, 16, 128}, {1}); - auto scale_gate = op::v0::Constant::create(element::f16, Shape{128, 16, 768}, {0.01f}); - auto zp_gate = op::v0::Constant::create(element::u4, Shape{128, 16, 768}, {0}); - auto wei_up = op::v0::Constant::create(element::u4, Shape{128, 768, 16, 128}, {1}); - auto scale_up = op::v0::Constant::create(element::f16, Shape{128, 16, 768}, {0.01f}); - auto zp_up = op::v0::Constant::create(element::u4, Shape{128, 16, 768, 16}, {0}); - auto wei_down = op::v0::Constant::create(element::u4, Shape{128, 2048, 6, 128}, {1}); - auto scale_down = op::v0::Constant::create(element::f16, Shape{128, 6, 2048}, {0.01f}); - auto zp_down = op::v0::Constant::create(element::u4, Shape{128, 6, 2048}, {0}); - - ov::intel_gpu::op::MOECompressed::Config config; - config.hidden_size = 2048; - config.inter_size = 768; - config.num_expert = 128; - config.group_size = 128; - config.top_k = 8; - config.out_type = ov::element::f16; - if (routing_type == MoERoutingType::SOFTMAX) { - config.routing_type = ov::intel_gpu::op::MOECompressed::RoutingType::SOFTMAX; - } else if (routing_type == MoERoutingType::SIGMOID_BIAS) { - config.routing_type = ov::intel_gpu::op::MOECompressed::RoutingType::SIGMOID_BIAS; - } else { - OPENVINO_THROW("Unsupported routing type"); - } - - ov::OutputVector args{hidden_states_reshape, routing_weights, - wei_gate, scale_gate, zp_gate, wei_up, scale_up, zp_up, wei_down, scale_down, zp_down}; - if (routing_type == MoERoutingType::SIGMOID_BIAS) { - auto routing_bias = op::v0::Constant::create(element::f16, Shape{1, 128}, {0.1f}); - args.push_back(routing_bias); - auto routing_eps = op::v0::Constant::create(element::f16, Shape{1, 1}, {1e-6f}); - args.push_back(routing_eps); - } - - std::shared_ptr result = std::make_shared(args, config); - - // When reshape_on_moe_input is false, MOE3GemmFusedCompressed takes reshaped input from routing subgraph, - // so the transformation inserts a reshape-back to restore the original shape. - if (!reshape_on_moe_input) { - auto hidden_state_shape = std::make_shared(hidden_states); - result = std::make_shared(result, hidden_state_shape, false); - } - - model_ref = std::make_shared(result, ov::ParameterVector{hidden_states}); - } -} - -INSTANTIATE_TEST_SUITE_P(smoke, - FuseMOE3GemmCompressedTest, - ::testing::Combine( - ::testing::Values(MoERoutingType::SOFTMAX, MoERoutingType::SIGMOID_BIAS), - ::testing::Values(false, true)), - FuseMOE3GemmCompressedTest::get_test_case_name); - -TEST_F(TransformationTestsF, FuseMOE3GemmSharedExpertCompressedTest) { - { - // tokens:32, hidden_size:2048, iter_size:768, experts:128, topk:8 - auto hidden_states = std::make_shared(element::f16, Shape{32, 2048}); - auto routers = op::v0::Constant::create(element::f16, Shape{2048, 128}, {0.2}); - auto routing_weights = std::make_shared(hidden_states, routers); - - auto routing_pair = build_softmax_routing_subgraph(routing_weights, 128, 8); - auto unsqueeze_moe = routing_pair.first; - auto topk_indices = routing_pair.second; - - // weight - auto wei_gate = op::v0::Constant::create(element::u4, Shape{128, 768, 16, 128}, {1}); - auto scale_gate = op::v0::Constant::create(element::f16, Shape{128, 16, 768}, {0.01f}); - auto zp_gate = op::v0::Constant::create(element::u4, Shape{128, 16, 768}, {0}); - auto wei_up = op::v0::Constant::create(element::u4, Shape{128, 768, 16, 128}, {1}); - auto scale_up = op::v0::Constant::create(element::f16, Shape{128, 16, 768}, {0.01f}); - auto zp_up = op::v0::Constant::create(element::u4, Shape{128, 16, 768, 16}, {0}); - auto wei_down = op::v0::Constant::create(element::u4, Shape{128, 2048, 6, 128}, {1}); - auto scale_down = op::v0::Constant::create(element::f16, Shape{128, 6, 2048}, {0.01f}); - auto zp_down = op::v0::Constant::create(element::u4, Shape{128, 6, 2048}, {0}); - - // Shared expert weights (single shared expert: leading dimension 1) - auto sh_wei_gate = op::v0::Constant::create(element::u4, Shape{1, 768, 16, 128}, {2}); - auto sh_scale_gate = op::v0::Constant::create(element::f16, Shape{1, 16, 768}, {0.02f}); - auto sh_zp_gate = op::v0::Constant::create(element::u4, Shape{1, 16, 768}, {0}); - auto sh_wei_up = op::v0::Constant::create(element::u4, Shape{1, 768, 16, 128}, {2}); - auto sh_scale_up = op::v0::Constant::create(element::f16, Shape{1, 16, 768}, {0.02f}); - auto sh_zp_up = op::v0::Constant::create(element::u4, Shape{1, 16, 768, 16}, {0}); - auto sh_wei_down = op::v0::Constant::create(element::u4, Shape{1, 2048, 6, 128}, {2}); - auto sh_scale_down = op::v0::Constant::create(element::f16, Shape{1, 6, 2048}, {0.02f}); - auto sh_zp_down = op::v0::Constant::create(element::u4, Shape{1, 6, 2048}, {0}); - auto sh_gate_gate_wei = op::v0::Constant::create(element::f16, Shape{2048, 1}, {0.5f}); - - ov::intel_gpu::op::MOECompressed::Config config; - config.hidden_size = 2048; - config.inter_size = 768; - config.num_expert = 128; - config.num_shared_expert = 1; - config.group_size = 128; - config.top_k = 8; - config.out_type = ov::element::f16; - auto moe_compressed = std::make_shared( - ov::OutputVector{hidden_states, unsqueeze_moe, topk_indices, - wei_gate, scale_gate, zp_gate, wei_up, scale_up, zp_up, wei_down, scale_down, zp_down, - sh_wei_gate, sh_scale_gate, sh_zp_gate, sh_wei_up, sh_scale_up, sh_zp_up, - sh_wei_down, sh_scale_down, sh_zp_down, sh_gate_gate_wei}, config); - model = std::make_shared(moe_compressed, ov::ParameterVector{hidden_states}); - manager.register_pass(); - } - { - // tokens:32, hidden_size:2048, iter_size:768, experts:128, topk:8 - auto hidden_states = std::make_shared(element::f16, Shape{32, 2048}); - auto routers = op::v0::Constant::create(element::f16, Shape{2048, 128}, {0.2}); - auto routing_weights = std::make_shared(hidden_states, routers); - - // MOE expert weights - auto wei_gate = op::v0::Constant::create(element::u4, Shape{128, 768, 16, 128}, {1}); - auto scale_gate = op::v0::Constant::create(element::f16, Shape{128, 16, 768}, {0.01f}); - auto zp_gate = op::v0::Constant::create(element::u4, Shape{128, 16, 768}, {0}); - auto wei_up = op::v0::Constant::create(element::u4, Shape{128, 768, 16, 128}, {1}); - auto scale_up = op::v0::Constant::create(element::f16, Shape{128, 16, 768}, {0.01f}); - auto zp_up = op::v0::Constant::create(element::u4, Shape{128, 16, 768, 16}, {0}); - auto wei_down = op::v0::Constant::create(element::u4, Shape{128, 2048, 6, 128}, {1}); - auto scale_down = op::v0::Constant::create(element::f16, Shape{128, 6, 2048}, {0.01f}); - auto zp_down = op::v0::Constant::create(element::u4, Shape{128, 6, 2048}, {0}); - - // Shared expert weights (single shared expert: leading dimension 1) - auto sh_wei_gate = op::v0::Constant::create(element::u4, Shape{1, 768, 16, 128}, {2}); - auto sh_scale_gate = op::v0::Constant::create(element::f16, Shape{1, 16, 768}, {0.02f}); - auto sh_zp_gate = op::v0::Constant::create(element::u4, Shape{1, 16, 768}, {0}); - auto sh_wei_up = op::v0::Constant::create(element::u4, Shape{1, 768, 16, 128}, {2}); - auto sh_scale_up = op::v0::Constant::create(element::f16, Shape{1, 16, 768}, {0.02f}); - auto sh_zp_up = op::v0::Constant::create(element::u4, Shape{1, 16, 768, 16}, {0}); - auto sh_wei_down = op::v0::Constant::create(element::u4, Shape{1, 2048, 6, 128}, {2}); - auto sh_scale_down = op::v0::Constant::create(element::f16, Shape{1, 6, 2048}, {0.02f}); - auto sh_zp_down = op::v0::Constant::create(element::u4, Shape{1, 6, 2048}, {0}); - auto sh_gate_gate_wei = op::v0::Constant::create(element::f16, Shape{2048, 1}, {0.5f}); - - // Dummy placeholders for SOFTMAX + shared expert (indices 11-12) - auto dummy_bias = op::v0::Constant::create(element::f16, Shape{1}, {0.0f}); - auto dummy_eps = op::v0::Constant::create(element::f16, Shape{1}, {0.0f}); - - ov::intel_gpu::op::MOECompressed::Config config; - config.hidden_size = 2048; - config.inter_size = 768; - config.num_expert = 128; - config.num_shared_expert = 1; - config.group_size = 128; - config.top_k = 8; - config.out_type = ov::element::f16; - auto moe_3gemm_fused_compressed = std::make_shared( - ov::OutputVector{hidden_states, routing_weights, - wei_gate, scale_gate, zp_gate, wei_up, scale_up, zp_up, wei_down, scale_down, zp_down, - dummy_bias, dummy_eps, - sh_wei_gate, sh_scale_gate, sh_zp_gate, sh_wei_up, sh_scale_up, sh_zp_up, - sh_wei_down, sh_scale_down, sh_zp_down, sh_gate_gate_wei}, config); - - model_ref = std::make_shared(moe_3gemm_fused_compressed, ov::ParameterVector{hidden_states}); - } -} - -} // namespace intel_gpu -} // namespace test -} // namespace ov diff --git a/src/plugins/intel_gpu/tests/unit/transformations/fuse_moe_router_scale_test.cpp b/src/plugins/intel_gpu/tests/unit/transformations/fuse_moe_router_scale_test.cpp new file mode 100644 index 000000000000..c7b90998543f --- /dev/null +++ b/src/plugins/intel_gpu/tests/unit/transformations/fuse_moe_router_scale_test.cpp @@ -0,0 +1,147 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "plugin/transformations/fuse_moe_router_scale.hpp" + +#include + +#include + +#include "common_test_utils/ov_test_utils.hpp" +#include "openvino/op/constant.hpp" +#include "openvino/op/gather.hpp" +#include "openvino/op/multiply.hpp" +#include "openvino/op/parameter.hpp" +#include "ov_ops/moe_compressed.hpp" + +using namespace testing; +using namespace ov::intel_gpu; + +namespace ov::test::intel_gpu { + +namespace { + +using MOECompressed = ov::op::internal::MOECompressed; + +// Returns {gate_w, gate_scale, gate_zp, up_w, up_scale, up_zp, down_w, down_scale, down_zp} +ov::OutputVector gemm3_weights(size_t num_experts, size_t hidden_size, size_t inter_size, float down_scale_val = 1.0f) { + using C = ov::op::v0::Constant; + const size_t gate_w = num_experts * inter_size * hidden_size; + const size_t gate_s = num_experts * inter_size; + const size_t down_w = num_experts * hidden_size * inter_size; + const size_t down_s = num_experts * hidden_size; + return { + C::create(ov::element::u8, {num_experts, inter_size, hidden_size}, std::vector(gate_w, 1)), + C::create(ov::element::f16, {num_experts, inter_size, 1}, std::vector(gate_s, 1.0f)), + C::create(ov::element::u8, {num_experts, inter_size, 1}, std::vector(gate_s, 0)), + C::create(ov::element::u8, {num_experts, inter_size, hidden_size}, std::vector(gate_w, 1)), + C::create(ov::element::f16, {num_experts, inter_size, 1}, std::vector(gate_s, 1.0f)), + C::create(ov::element::u8, {num_experts, inter_size, 1}, std::vector(gate_s, 0)), + C::create(ov::element::u8, {num_experts, hidden_size, inter_size}, std::vector(down_w, 1)), + C::create(ov::element::f16, {num_experts, hidden_size, 1}, std::vector(down_s, down_scale_val)), + C::create(ov::element::u8, {num_experts, hidden_size, 1}, std::vector(down_s, 0)), + }; +} + +MOECompressed::Config gemm3_config(size_t num_experts, size_t hidden_size, size_t inter_size, size_t top_k) { + MOECompressed::Config c; + c.expert_type = ov::op::internal::MOE::Expert_type::GEMM3_SWIGLU; + c.activation_type = ov::op::internal::MOE::Activation_type::SWIGLU; + c.hidden_size = hidden_size; + c.inter_size = inter_size; + c.num_expert = num_experts; + c.num_shared_expert = 0; + c.top_k = top_k; + c.group_size = std::numeric_limits::max(); + c.has_zp = true; + c.out_type = ov::element::f16; + return c; +} + +} // namespace + +TEST_F(TransformationTestsF, FuseMoEPerExpertScale) { + const size_t num_experts = 4, hidden_size = 8, inter_size = 4, top_k = 2; + const std::vector per_expert_scales = {1.0f, 2.0f, 1.0f, 0.5f}; + + { + auto hidden = std::make_shared(ov::element::f16, ov::PartialShape{-1, hidden_size}); + auto routing = std::make_shared(ov::element::f16, ov::PartialShape{-1, top_k}); + auto topk_idx = std::make_shared(ov::element::i32, ov::PartialShape{-1, top_k}); + + auto pes_const = ov::op::v0::Constant::create(ov::element::f16, {num_experts}, per_expert_scales); + auto axis = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{}, {0}); + auto gathered = std::make_shared(pes_const, topk_idx, axis); + auto scaled = std::make_shared(routing, gathered); + + auto ws = gemm3_weights(num_experts, hidden_size, inter_size); + auto moe = std::make_shared( + ov::OutputVector{hidden, scaled, topk_idx, ws[0], ws[1], ws[2], ws[3], ws[4], ws[5], ws[6], ws[7], ws[8]}, + gemm3_config(num_experts, hidden_size, inter_size, top_k)); + + model = std::make_shared(ov::OutputVector{moe}, ov::ParameterVector{hidden, routing, topk_idx}); + manager.register_pass(); + comparator.enable(FunctionsComparator::CmpValues::CONST_VALUES); + } + + { + auto hidden = std::make_shared(ov::element::f16, ov::PartialShape{-1, hidden_size}); + auto routing = std::make_shared(ov::element::f16, ov::PartialShape{-1, top_k}); + auto topk_idx = std::make_shared(ov::element::i32, ov::PartialShape{-1, top_k}); + + std::vector folded_down_scale(num_experts * hidden_size); + for (size_t n = 0; n < num_experts; ++n) + for (size_t h = 0; h < hidden_size; ++h) + folded_down_scale[n * hidden_size + h] = per_expert_scales[n]; + + auto ws = gemm3_weights(num_experts, hidden_size, inter_size); + auto down_scale = ov::op::v0::Constant::create(ov::element::f16, {num_experts, hidden_size, 1}, folded_down_scale); + auto moe = std::make_shared( + ov::OutputVector{hidden, routing, topk_idx, ws[0], ws[1], ws[2], ws[3], ws[4], ws[5], ws[6], down_scale, ws[8]}, + gemm3_config(num_experts, hidden_size, inter_size, top_k)); + + model_ref = std::make_shared(ov::OutputVector{moe}, ov::ParameterVector{hidden, routing, topk_idx}); + } +} + +TEST_F(TransformationTestsF, FuseMoEScalarScale) { + const size_t num_experts = 4, hidden_size = 8, inter_size = 4, top_k = 2; + const float scale_val = 2.0f; + + { + auto hidden = std::make_shared(ov::element::f16, ov::PartialShape{-1, hidden_size}); + auto routing = std::make_shared(ov::element::f16, ov::PartialShape{-1, top_k}); + auto topk_idx = std::make_shared(ov::element::i32, ov::PartialShape{-1, top_k}); + + auto scalar = ov::op::v0::Constant::create(ov::element::f16, ov::Shape{1}, {scale_val}); + auto scaled = std::make_shared(routing, scalar); + + auto ws = gemm3_weights(num_experts, hidden_size, inter_size); + auto moe = std::make_shared( + ov::OutputVector{hidden, scaled, topk_idx, ws[0], ws[1], ws[2], ws[3], ws[4], ws[5], ws[6], ws[7], ws[8]}, + gemm3_config(num_experts, hidden_size, inter_size, top_k)); + + model = std::make_shared(ov::OutputVector{moe}, ov::ParameterVector{hidden, routing, topk_idx}); + manager.register_pass(); + comparator.enable(FunctionsComparator::CmpValues::CONST_VALUES); + } + + { + auto hidden = std::make_shared(ov::element::f16, ov::PartialShape{-1, hidden_size}); + auto routing = std::make_shared(ov::element::f16, ov::PartialShape{-1, top_k}); + auto topk_idx = std::make_shared(ov::element::i32, ov::PartialShape{-1, top_k}); + + auto ws = gemm3_weights(num_experts, hidden_size, inter_size); + auto down_scale = ov::op::v0::Constant::create(ov::element::f16, + {num_experts, hidden_size, 1}, + std::vector(num_experts * hidden_size, scale_val)); + auto moe = std::make_shared( + ov::OutputVector{hidden, routing, topk_idx, ws[0], ws[1], ws[2], ws[3], ws[4], ws[5], ws[6], down_scale, ws[8]}, + gemm3_config(num_experts, hidden_size, inter_size, top_k)); + + model_ref = std::make_shared(ov::OutputVector{moe}, ov::ParameterVector{hidden, routing, topk_idx}); + } +} + +} // namespace ov::test::intel_gpu diff --git a/src/plugins/intel_gpu/tests/unit/transformations/fuse_moe_router_test.cpp b/src/plugins/intel_gpu/tests/unit/transformations/fuse_moe_router_test.cpp new file mode 100644 index 000000000000..492f7aa03c88 --- /dev/null +++ b/src/plugins/intel_gpu/tests/unit/transformations/fuse_moe_router_test.cpp @@ -0,0 +1,153 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "plugin/transformations/fuse_moe_router.hpp" + +#include + +#include "common_test_utils/node_builders/moe_builders.hpp" +#include "common_test_utils/ov_test_utils.hpp" +#include "intel_gpu/op/moe_router_fused.hpp" +#include "openvino/op/add.hpp" +#include "openvino/op/constant.hpp" +#include "openvino/op/convert.hpp" +#include "openvino/op/divide.hpp" +#include "openvino/op/gather_elements.hpp" +#include "openvino/op/matmul.hpp" +#include "openvino/op/reduce_sum.hpp" +#include "openvino/op/reshape.hpp" +#include "openvino/op/sigmoid.hpp" +#include "openvino/op/softmax.hpp" +#include "openvino/op/topk.hpp" +#include "openvino/op/unsqueeze.hpp" + +using namespace testing; +using namespace ov::intel_gpu; + +namespace ov { +namespace test { +namespace intel_gpu { + +using namespace ov::test; + +using TestParams = std::tuple; // routing_type, with_convert_on_indices, num_expert, top_k + +class FuseMoERouterTest : public TransformationTestsF, public ::testing::WithParamInterface { +public: + static std::string get_test_case_name(const ::testing::TestParamInfo& info) { + const auto routing_type = std::get<0>(info.param); + const bool with_convert = std::get<1>(info.param); + const size_t num_expert = std::get<2>(info.param); + const size_t top_k = std::get<3>(info.param); + std::string name; + switch (routing_type) { + case MoERoutingType::SOFTMAX: + name = "Softmax"; + break; + case MoERoutingType::SIGMOID_BIAS: + name = "SigmoidBias"; + break; + default: + OPENVINO_THROW("Unsupported routing type"); + } + name += with_convert ? "_WithConvert" : "_NoConvert"; + name += "_E" + std::to_string(num_expert) + "_K" + std::to_string(top_k); + return name; + } +}; + +static std::pair, ov::Output> build_softmax_routing_for_fuse_test(const ov::Output& routing_weights, + size_t topk, + bool with_convert_on_indices) { + auto softmax = std::make_shared(routing_weights, 1); + const auto index_type = with_convert_on_indices ? ov::element::i64 : ov::element::i32; + auto k = op::v0::Constant::create(element::i32, Shape{}, {static_cast(topk)}); + auto topk_node = std::make_shared(softmax, k, 1, ov::op::v11::TopK::Mode::MAX, ov::op::v11::TopK::SortType::SORT_VALUES, index_type); + auto reduce_axis = op::v0::Constant::create(element::i64, Shape{1}, {1}); + auto reduce_sum = std::make_shared(topk_node->output(0), reduce_axis, true); + ov::Output norm = std::make_shared(topk_node->output(0), reduce_sum); + ov::Output indices = topk_node->output(1); + if (with_convert_on_indices) + indices = std::make_shared(indices, ov::element::i32); + return {norm, indices}; +} + +static std::pair, ov::Output> build_sigmoid_routing_for_fuse_test(const ov::Output& routing_weights, + ov::element::Type data_precision, + size_t number_of_experts, + size_t topk, + bool with_convert_on_indices) { + auto sigmoid = std::make_shared(routing_weights); + auto bias = op::v0::Constant::create(data_precision, Shape{1, number_of_experts}, {0.1f}); + auto sig_add = std::make_shared(sigmoid, bias); + const auto index_type = with_convert_on_indices ? ov::element::i64 : ov::element::i32; + auto k = op::v0::Constant::create(element::i64, Shape{}, {static_cast(topk)}); + auto topk_node = std::make_shared(sig_add, k, -1, ov::op::v11::TopK::Mode::MAX, ov::op::v11::TopK::SortType::SORT_VALUES, index_type); + ov::Output indices = topk_node->output(1); + if (with_convert_on_indices) + indices = std::make_shared(indices, ov::element::i32); + auto gather_el = std::make_shared(sigmoid, indices, 1); + auto reduce_axis = op::v0::Constant::create(element::i64, Shape{1}, {1}); + auto reduce_sum = std::make_shared(gather_el, reduce_axis, true); + auto eps = op::v0::Constant::create(data_precision, Shape{1, 1}, {1e-6f}); + auto add_eps = std::make_shared(reduce_sum, eps); + ov::Output norm = std::make_shared(gather_el, add_eps); + return {norm, indices}; +} + +TEST_P(FuseMoERouterTest, CompareFunctions) { + const auto [routing_type, with_convert_on_indices, num_expert, top_k] = GetParam(); + { + auto hidden_states = std::make_shared(element::f16, Shape{4, 8, 2048}); + auto flatten_shape = op::v0::Constant::create(element::i32, Shape{2}, {32, 2048}); + auto hidden_states_reshape = std::make_shared(hidden_states, flatten_shape, false); + auto routers = op::v0::Constant::create(element::f16, Shape{2048, num_expert}, {0.2}); + auto routing_weights = std::make_shared(hidden_states_reshape, routers); + + const auto [routing_out, topk_indices] = + routing_type == MoERoutingType::SOFTMAX + ? build_softmax_routing_for_fuse_test(routing_weights, top_k, with_convert_on_indices) + : build_sigmoid_routing_for_fuse_test(routing_weights, element::f16, num_expert, top_k, with_convert_on_indices); + + // Wrap outputs to avoid feeding Result nodes directly (required by replace_output_update_name) + auto weights_out = std::make_shared(routing_out, ov::op::v0::Constant::create(element::i32, Shape{1}, {0})); + auto indices_out = std::make_shared(topk_indices, ov::op::v0::Constant::create(element::i32, Shape{1}, {0})); + model = std::make_shared(ov::OutputVector{weights_out, indices_out}, ov::ParameterVector{hidden_states}); + } + manager.register_pass(); + { + auto hidden_states = std::make_shared(element::f16, Shape{4, 8, 2048}); + auto flatten_shape = op::v0::Constant::create(element::i32, Shape{2}, {32, 2048}); + auto hidden_states_reshape = std::make_shared(hidden_states, flatten_shape, false); + auto routers = op::v0::Constant::create(element::f16, Shape{2048, num_expert}, {0.2}); + auto routing_weights = std::make_shared(hidden_states_reshape, routers); + + ov::intel_gpu::op::MoERouterFused::Config router_config; + router_config.num_expert = num_expert; + router_config.top_k = top_k; + ov::OutputVector router_args{routing_weights}; + if (routing_type == MoERoutingType::SIGMOID_BIAS) { + router_config.routing_type = ov::intel_gpu::op::MoERouterFused::RoutingType::SIGMOID_BIAS; + router_args.push_back(op::v0::Constant::create(element::f16, Shape{1, num_expert}, {0.1f})); + router_args.push_back(op::v0::Constant::create(element::f16, Shape{1, 1}, {1e-6f})); + } + auto router_node = std::make_shared(router_args, router_config); + + auto weights_out = std::make_shared(router_node->output(0), ov::op::v0::Constant::create(element::i32, Shape{1}, {0})); + auto indices_out = std::make_shared(router_node->output(1), ov::op::v0::Constant::create(element::i32, Shape{1}, {0})); + model_ref = std::make_shared(ov::OutputVector{weights_out, indices_out}, ov::ParameterVector{hidden_states}); + } +} + +INSTANTIATE_TEST_SUITE_P(smoke, + FuseMoERouterTest, + ::testing::Combine(::testing::Values(MoERoutingType::SOFTMAX, MoERoutingType::SIGMOID_BIAS), + ::testing::Values(true, false), + ::testing::Values(128, 512), + ::testing::Values(8, 10)), + FuseMoERouterTest::get_test_case_name); + +} // namespace intel_gpu +} // namespace test +} // namespace ov diff --git a/src/plugins/intel_gpu/tests/unit/transformations/fuse_moe_shared_expert_test.cpp b/src/plugins/intel_gpu/tests/unit/transformations/fuse_moe_shared_expert_test.cpp index fc70a12cda63..c6b3676771ad 100644 --- a/src/plugins/intel_gpu/tests/unit/transformations/fuse_moe_shared_expert_test.cpp +++ b/src/plugins/intel_gpu/tests/unit/transformations/fuse_moe_shared_expert_test.cpp @@ -15,6 +15,7 @@ #include "openvino/op/sigmoid.hpp" #include "openvino/op/subtract.hpp" #include "openvino/op/swish.hpp" +#include "ov_ops/moe_compressed.hpp" #include "plugin/transformations/fuse_moe_shared_expert.hpp" using namespace testing; @@ -72,7 +73,7 @@ TEST_F(TransformationTestsF, FuseMOESharedExpertWithSigmoidGating) { { // tokens:32, hidden_size:2048, inter_size:768, experts:128, topk:8 auto hidden_states = std::make_shared(element::f16, Shape{32, 2048}); - auto routing_weights = std::make_shared(element::f16, Shape{128, 1, 32, 1}); + auto routing_weights = std::make_shared(element::f16, Shape{32, 8}); auto routing_idx = std::make_shared(element::i32, Shape{32, 8}); // MOE expert decompression chains @@ -125,7 +126,7 @@ TEST_F(TransformationTestsF, FuseMOESharedExpertWithSigmoidGating) { { // Expected: MOE with 10 inputs (shared expert weights absorbed) auto hidden_states = std::make_shared(element::f16, Shape{32, 2048}); - auto routing_weights = std::make_shared(element::f16, Shape{128, 1, 32, 1}); + auto routing_weights = std::make_shared(element::f16, Shape{32, 8}); auto routing_idx = std::make_shared(element::i32, Shape{32, 8}); auto hidden_states_f32 = std::make_shared(hidden_states, element::f32); @@ -164,7 +165,7 @@ TEST_F(TransformationTestsF, FuseMOESharedExpertWithoutGating) { { // Same as above but without sigmoid gating (shared_down output goes through Add directly) auto hidden_states = std::make_shared(element::f16, Shape{32, 2048}); - auto routing_weights = std::make_shared(element::f16, Shape{128, 1, 32, 1}); + auto routing_weights = std::make_shared(element::f16, Shape{32, 8}); auto routing_idx = std::make_shared(element::i32, Shape{32, 8}); auto gate_decompressed = create_decompression_chain( @@ -205,7 +206,7 @@ TEST_F(TransformationTestsF, FuseMOESharedExpertWithoutGating) { } { auto hidden_states = std::make_shared(element::f16, Shape{32, 2048}); - auto routing_weights = std::make_shared(element::f16, Shape{128, 1, 32, 1}); + auto routing_weights = std::make_shared(element::f16, Shape{32, 8}); auto routing_idx = std::make_shared(element::i32, Shape{32, 8}); auto hidden_states_f32 = std::make_shared(hidden_states, element::f32); @@ -244,7 +245,7 @@ TEST_F(TransformationTestsF, FuseMOESharedExpertSymmetricWithGating) { disable_rt_info_check(); { auto hidden_states = std::make_shared(element::f16, Shape{32, 2048}); - auto routing_weights = std::make_shared(element::f16, Shape{128, 1, 32, 1}); + auto routing_weights = std::make_shared(element::f16, Shape{32, 8}); auto routing_idx = std::make_shared(element::i32, Shape{32, 8}); // MOE expert sym-quant decompression chains (no Subtract) @@ -294,7 +295,7 @@ TEST_F(TransformationTestsF, FuseMOESharedExpertSymmetricWithGating) { } { auto hidden_states = std::make_shared(element::f16, Shape{32, 2048}); - auto routing_weights = std::make_shared(element::f16, Shape{128, 1, 32, 1}); + auto routing_weights = std::make_shared(element::f16, Shape{32, 8}); auto routing_idx = std::make_shared(element::i32, Shape{32, 8}); auto hidden_states_f32 = std::make_shared(hidden_states, element::f32); @@ -327,6 +328,116 @@ TEST_F(TransformationTestsF, FuseMOESharedExpertSymmetricWithGating) { } } +// Test FuseMOESharedExpert with MOECompressed input (sigmoid gating) +TEST_F(TransformationTestsF, FuseMOECompressedSharedExpertWithSigmoidGating) { + disable_rt_info_check(); + { + // tokens:32, hidden_size:2048, inter_size:768, experts:128, topk:8 + auto hidden_states = std::make_shared(element::f16, Shape{32, 2048}); + auto routing_weights = std::make_shared(element::f16, Shape{32, 8}); + auto routing_idx = std::make_shared(element::i32, Shape{32, 8}); + + // MOE compressed weights (no decompression chains) + auto wei_gate = op::v0::Constant::create(element::u4, Shape{128, 768, 16, 128}, {1}); + auto scale_gate = op::v0::Constant::create(element::f16, Shape{128, 768, 16}, {0.01f}); + auto zp_gate = op::v0::Constant::create(element::u4, Shape{128, 768, 16}, {0}); + auto wei_up = op::v0::Constant::create(element::u4, Shape{128, 768, 16, 128}, {1}); + auto scale_up = op::v0::Constant::create(element::f16, Shape{128, 768, 16}, {0.01f}); + auto zp_up = op::v0::Constant::create(element::u4, Shape{128, 768, 16}, {0}); + auto wei_down = op::v0::Constant::create(element::u4, Shape{128, 2048, 6, 128}, {1}); + auto scale_down = op::v0::Constant::create(element::f16, Shape{128, 2048, 6}, {0.01f}); + auto zp_down = op::v0::Constant::create(element::u4, Shape{128, 2048, 6}, {0}); + + ov::op::internal::MOECompressed::Config config; + config.expert_type = ov::op::internal::MOE::Expert_type::GEMM3_SWIGLU; + config.has_zp = true; + config.hidden_size = 2048; + config.inter_size = 768; + config.num_expert = 128; + config.group_size = 128; + config.top_k = 8; + config.out_type = ov::element::f32; + auto moe = std::make_shared( + ov::OutputVector{hidden_states, routing_weights, routing_idx, + wei_gate, scale_gate, zp_gate, wei_up, scale_up, zp_up, + wei_down, scale_down, zp_down}, config); + + // Shared expert subgraph with decompression chains (outputs f32) + auto hidden_states_f32 = std::make_shared(hidden_states, element::f32); + auto reshape_const_hs = op::v0::Constant::create(element::i64, Shape{2}, {32, 2048}); + auto hidden_states_reshaped = std::make_shared(hidden_states_f32, reshape_const_hs, false); + + auto sh_gate_decompressed = create_decompression_chain( + element::u4, {768, 16, 128}, {768, 16, 1}, {768, 16, 1}, {768, 2048}, 2.0f, 1.0f, 0.02f); + auto sh_up_decompressed = create_decompression_chain( + element::u4, {768, 16, 128}, {768, 16, 1}, {768, 16, 1}, {768, 2048}, 2.0f, 1.0f, 0.02f); + auto sh_down_decompressed = create_decompression_chain( + element::u4, {2048, 6, 128}, {2048, 6, 1}, {2048, 6, 1}, {2048, 768}, 2.0f, 1.0f, 0.02f); + + auto shared_gate_m = std::make_shared(hidden_states_reshaped, sh_gate_decompressed, false, true); + auto shared_swish_m = std::make_shared(shared_gate_m); + auto shared_up_m = std::make_shared(hidden_states_reshaped, sh_up_decompressed, false, true); + auto shared_mul_m = std::make_shared(shared_swish_m, shared_up_m); + auto shared_down_m = std::make_shared(shared_mul_m, sh_down_decompressed, false, true); + + // Sigmoid gating + auto gate_gate_wei = std::make_shared(element::f32, Shape{2048, 1}, std::vector(2048, 1.0f)); + auto gate_gate_mm = std::make_shared(hidden_states_reshaped, gate_gate_wei); + auto gate_sigmoid = std::make_shared(gate_gate_mm); + auto shared_gated = std::make_shared(gate_sigmoid, shared_down_m); + + auto reshape_const_output = op::v0::Constant::create(element::i64, Shape{2}, {32, 2048}); + auto shared_reshaped = std::make_shared(shared_gated, reshape_const_output, false); + auto add_m = std::make_shared(shared_reshaped, moe); + + model = std::make_shared(add_m, ov::ParameterVector{hidden_states, routing_weights, routing_idx}); + manager.register_pass(); + } + { + // Expected: MOECompressed with shared expert weights absorbed + auto hidden_states = std::make_shared(element::f16, Shape{32, 2048}); + auto routing_weights = std::make_shared(element::f16, Shape{32, 8}); + auto routing_idx = std::make_shared(element::i32, Shape{32, 8}); + + auto wei_gate = op::v0::Constant::create(element::u4, Shape{128, 768, 16, 128}, {1}); + auto scale_gate = op::v0::Constant::create(element::f16, Shape{128, 768, 16}, {0.01f}); + auto zp_gate = op::v0::Constant::create(element::u4, Shape{128, 768, 16}, {0}); + auto wei_up = op::v0::Constant::create(element::u4, Shape{128, 768, 16, 128}, {1}); + auto scale_up = op::v0::Constant::create(element::f16, Shape{128, 768, 16}, {0.01f}); + auto zp_up = op::v0::Constant::create(element::u4, Shape{128, 768, 16}, {0}); + auto wei_down = op::v0::Constant::create(element::u4, Shape{128, 2048, 6, 128}, {1}); + auto scale_down = op::v0::Constant::create(element::f16, Shape{128, 2048, 6}, {0.01f}); + auto zp_down = op::v0::Constant::create(element::u4, Shape{128, 2048, 6}, {0}); + + auto sh_gate_decompressed = create_decompression_chain( + element::u4, {768, 16, 128}, {768, 16, 1}, {768, 16, 1}, {768, 2048}, 2.0f, 1.0f, 0.02f); + auto sh_up_decompressed = create_decompression_chain( + element::u4, {768, 16, 128}, {768, 16, 1}, {768, 16, 1}, {768, 2048}, 2.0f, 1.0f, 0.02f); + auto sh_down_decompressed = create_decompression_chain( + element::u4, {2048, 6, 128}, {2048, 6, 1}, {2048, 6, 1}, {2048, 768}, 2.0f, 1.0f, 0.02f); + + auto gate_gate_wei = std::make_shared(element::f32, Shape{2048, 1}, std::vector(2048, 1.0f)); + + ov::op::internal::MOECompressed::Config config; + config.expert_type = ov::op::internal::MOE::Expert_type::GEMM3_SWIGLU; + config.has_zp = true; + config.hidden_size = 2048; + config.inter_size = 768; + config.num_expert = 128; + config.group_size = 128; + config.top_k = 8; + config.out_type = ov::element::f32; + auto moe_expected = std::make_shared( + ov::OutputVector{hidden_states, routing_weights, routing_idx, + wei_gate, scale_gate, zp_gate, wei_up, scale_up, zp_up, + wei_down, scale_down, zp_down, + sh_gate_decompressed, sh_up_decompressed, sh_down_decompressed, + gate_gate_wei}, config); + + model_ref = std::make_shared(moe_expected, ov::ParameterVector{hidden_states, routing_weights, routing_idx}); + } +} + } // namespace intel_gpu } // namespace test } // namespace ov diff --git a/src/plugins/intel_gpu/tests/unit/transformations/increase_precision_test.cpp b/src/plugins/intel_gpu/tests/unit/transformations/increase_precision_test.cpp index 68b01c589fcb..1f1ff2a48602 100644 --- a/src/plugins/intel_gpu/tests/unit/transformations/increase_precision_test.cpp +++ b/src/plugins/intel_gpu/tests/unit/transformations/increase_precision_test.cpp @@ -26,6 +26,7 @@ #include "openvino/op/transpose.hpp" #include "openvino/op/add.hpp" #include "openvino/op/gather.hpp" +#include "openvino/op/gather_nd.hpp" #include "openvino/op/strided_slice.hpp" #include "openvino/op/shape_of.hpp" #include "openvino/op/broadcast.hpp" @@ -376,186 +377,236 @@ TEST_F(TransformationTestsF, IncreasePositionIdsSliceGatherUnsqueezeRoPE) { comparator.enable(FunctionsComparator::CmpValues::ATTRIBUTES); } -TEST_F(TransformationTestsF, IncreasePositionIdsLTXVideo) { +static void test_IncreasePositionIdsLTXVideo(std::shared_ptr& model, + std::shared_ptr& model_ref, + ov::pass::Manager& manager, + FunctionsComparator& comparator, + bool is_ltx_video) { + // Test graph matches the post-RoPE-fusion pattern: + // Multiply → Add(Const) → Transpose → Reshape → Sin/Cos → T → U → B → GND → T → Concat → RoPE { - auto input_1 = std::make_shared(ov::element::f16, ov::PartialShape{ -1, 3, -1 }); - auto input_2 = std::make_shared(ov::element::f16, ov::PartialShape{ -1, -1, 2048 }); - - auto constant_01 = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{ 3 }, { 0, 2, 1 }); - auto transpose = std::make_shared(input_1, constant_01); - auto constant_02 = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{ 1 }, { -1 }); - auto unsqueeze_1 = std::make_shared(transpose, constant_02); - auto constant_03 = ov::op::v0::Constant::create(ov::element::f16, ov::Shape{ 1, 1, 1, 341 }, { 3.14f }); - auto multiply = std::make_shared(unsqueeze_1, constant_03); - - auto constant_04 = ov::op::v0::Constant::create(ov::element::f16, ov::Shape{ 1, 1, 1, 341 }, { 1e-6 }); - auto add = std::make_shared(multiply, constant_04); - - auto constant_05 = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{ 4 }, { 0, 1, 3, 2 }); - auto transpose_1 = std::make_shared(add, constant_05); - - auto constant_06 = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{ 3 }, { 0, 0, 1023 }); - auto reshape_1 = std::make_shared(transpose_1, constant_06, true); - - auto cos = std::make_shared(reshape_1); - auto sin = std::make_shared(reshape_1); - - auto constant_07 = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{ 2046 }, { 0 }); - auto constant_08 = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{ 2046 }, { 0 }); - auto constant_09 = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{ }, { -1 }); - auto constant_10 = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{ }, { -1 }); - - auto gather_1 = std::make_shared(cos, constant_07, constant_09); - auto gather_3 = std::make_shared(sin, constant_08, constant_10); - - auto constant_11 = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{ 3 }, { 0, 0, 0 }); - auto constant_12 = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{ 3 }, { 0, 0, 2 }); - auto constant_13 = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{ 3 }, { 1, 1, 1 }); - auto slice = std::make_shared(gather_1, constant_11, constant_12, constant_13, std::vector{}, std::vector{}); - - auto shape_of = std::make_shared(slice); - auto constant_14 = ov::op::v0::Constant::create(ov::element::f16, ov::Shape{ }, { 1 }); - auto constant_15 = ov::op::v0::Constant::create(ov::element::f16, ov::Shape{ }, { 0 }); - auto broadcast_ones_like = std::make_shared(constant_14, shape_of); - auto broadcast_zeros_like = std::make_shared(constant_15, shape_of); - - auto concat = std::make_shared(ov::OutputVector{broadcast_ones_like, gather_1}, -1); - auto concat_1 = std::make_shared(ov::OutputVector{broadcast_zeros_like, gather_3}, -1); - - auto constant_16 = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{ 1, 1, 2048 }, { 0 }); - auto rms = std::make_shared(input_2, constant_16, 1e-19); - - auto constant_17 = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{ 4 }, { 0, 0, 1024, 2 }); - auto reshape_2 = std::make_shared(rms, constant_17, true); - - auto constant_18 = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{ }, { -1 }); - auto split_1 = std::make_shared(reshape_2, constant_18, 2); - - auto constant_19 = ov::op::v0::Constant::create(ov::element::f16, ov::Shape{ }, { -1 }); - auto multiply_2 = std::make_shared(split_1->output(0), constant_19); - - auto constant_20 = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{ }, { -1 }); - auto squeeze_1 = std::make_shared(multiply_2, constant_20); - - auto constant_21 = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{ }, { -1 }); - auto unsqueeze_2 = std::make_shared(squeeze_1, constant_21); - - auto concat_stack_1 = std::make_shared(ov::OutputVector{unsqueeze_2, split_1->output(1)}, -1); - auto constant_22 = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{ 3 }, { 0, 0, 2048 }); - auto reshape_3 = std::make_shared(concat_stack_1, constant_22, true); - - auto multiply_3 = std::make_shared(rms, concat); - auto multiply_4 = std::make_shared(reshape_3, concat_1); - - auto add_1 = std::make_shared(multiply_3, multiply_4); - auto constant_23 = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{ 4 }, { 0, 0, 32, 64 }); - auto reshape_4 = std::make_shared(add_1, constant_23, true); - auto constant_24 = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{ 4 }, { 0, 2, 1, 3 }); - auto transpose_3 = std::make_shared(reshape_4, constant_24); - - auto input_3 = std::make_shared(ov::element::f16, ov::PartialShape{ -1, -1, 32, 64 }); - auto input_4 = std::make_shared(ov::element::f16, ov::PartialShape{ -1, -1, 32, 64 }); - auto transpose_2 = std::make_shared(input_3, constant_24); - auto transpose_4 = std::make_shared(input_4, constant_24); - auto sdpa = std::make_shared(transpose_2, transpose_3, transpose_4, true); - - model = std::make_shared(ov::OutputVector{sdpa}, ov::ParameterVector{input_1, input_2, input_3, input_4}); + auto param_mul_in = std::make_shared(ov::element::f16, ov::PartialShape{1, 3, 64}); + auto param_x = std::make_shared(ov::element::f16, ov::PartialShape{1, 64, 16}); + + // Upstream: Multiply → Add(Constant) → Transpose → Reshape + auto mul_const = ov::op::v0::Constant::create(ov::element::f16, ov::Shape{1, 1, 64}, {3.14f}); + auto multiply = std::make_shared(param_mul_in, mul_const); + + auto add_const = ov::op::v0::Constant::create(ov::element::f16, ov::Shape{1, 1, 64}, {1e-6f}); + auto add = std::make_shared(multiply, add_const); + + auto transpose_order = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{3}, {0, 2, 1}); + auto transpose_up = std::make_shared(add, transpose_order); + + auto reshape_shape = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{3}, {1, 64, 3}); + auto reshape = std::make_shared(transpose_up, reshape_shape, false); + + // Sin path: Sin → Transpose → Unsqueeze → Broadcast → GatherND → Transpose → Concat + auto sin_node = std::make_shared(reshape); + auto sin_t_order = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{3}, {0, 2, 1}); + auto sin_transpose = std::make_shared(sin_node, sin_t_order); + auto sin_unsq_axis = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{1}, {0}); + auto sin_unsqueeze = std::make_shared(sin_transpose, sin_unsq_axis); + auto sin_bcast_shape = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{4}, {2, 1, 3, 64}); + auto sin_broadcast = std::make_shared(sin_unsqueeze, sin_bcast_shape); + auto sin_gnd_indices = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{1, 8, 3}, {0}); + auto sin_gathernd = std::make_shared(sin_broadcast, sin_gnd_indices); + auto sin_t2_order = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{3}, {0, 2, 1}); + auto sin_transpose2 = std::make_shared(sin_gathernd, sin_t2_order); + auto sin_concat_other = ov::op::v0::Constant::create(ov::element::f16, ov::Shape{1, 64, 8}, {0.0f}); + auto sin_concat = std::make_shared(ov::OutputVector{sin_concat_other, sin_transpose2}, -1); + + // Cos path: Cos → Transpose → Unsqueeze → Broadcast → GatherND → Transpose → Concat + auto cos_node = std::make_shared(reshape); + auto cos_t_order = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{3}, {0, 2, 1}); + auto cos_transpose = std::make_shared(cos_node, cos_t_order); + auto cos_unsq_axis = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{1}, {0}); + auto cos_unsqueeze = std::make_shared(cos_transpose, cos_unsq_axis); + auto cos_bcast_shape = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{4}, {2, 1, 3, 64}); + auto cos_broadcast = std::make_shared(cos_unsqueeze, cos_bcast_shape); + auto cos_gnd_indices = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{1, 8, 3}, {0}); + auto cos_gathernd = std::make_shared(cos_broadcast, cos_gnd_indices); + auto cos_t2_order = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{3}, {0, 2, 1}); + auto cos_transpose2 = std::make_shared(cos_gathernd, cos_t2_order); + auto cos_concat_other = ov::op::v0::Constant::create(ov::element::f16, ov::Shape{1, 64, 8}, {1.0f}); + auto cos_concat = std::make_shared(ov::OutputVector{cos_concat_other, cos_transpose2}, -1); + + ov::op::internal::RoPE::Config rope_config; + rope_config.is_ltx_video = is_ltx_video; + rope_config.rotary_ndims = 64; + auto rope = std::make_shared(ov::OutputVector{param_x, cos_concat, sin_concat}, rope_config); + + model = std::make_shared(ov::OutputVector{rope}, ov::ParameterVector{param_mul_in, param_x}); manager.register_pass(); } - { - auto input_1 = std::make_shared(ov::element::f16, ov::PartialShape{ -1, 3, -1 }); - auto input_2 = std::make_shared(ov::element::f16, ov::PartialShape{ -1, -1, 2048 }); - - auto constant_01 = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{ 3 }, { 0, 2, 1 }); - auto transpose = std::make_shared(input_1, constant_01); - auto constant_02 = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{ 1 }, { -1 }); - auto unsqueeze_1 = std::make_shared(transpose, constant_02); - auto constant_03 = ov::op::v0::Constant::create(ov::element::f16, ov::Shape{ 1, 1, 1, 341 }, { 3.14f }); - - auto convert_1 = std::make_shared(unsqueeze_1, ov::element::f32); - auto convert_2 = std::make_shared(constant_03, ov::element::f32); - auto multiply = std::make_shared(convert_1, convert_2); - - auto constant_04 = ov::op::v0::Constant::create(ov::element::f16, ov::Shape{ 1, 1, 1, 341 }, { 1e-6 }); - auto convert_3 = std::make_shared(constant_04, ov::element::f32); - auto add = std::make_shared(multiply, convert_3); - - auto constant_05 = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{ 4 }, { 0, 1, 3, 2 }); - auto transpose_1 = std::make_shared(add, constant_05); - - auto constant_06 = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{ 3 }, { 0, 0, 1023 }); - auto reshape_1 = std::make_shared(transpose_1, constant_06, true); - - auto cos = std::make_shared(reshape_1); - auto sin = std::make_shared(reshape_1); - - auto convert_4 = std::make_shared(cos, ov::element::f16); - auto convert_5 = std::make_shared(sin, ov::element::f16); - - auto constant_07 = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{ 2046 }, { 0 }); - auto constant_08 = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{ 2046 }, { 0 }); - auto constant_09 = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{ }, { -1 }); - auto constant_10 = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{ }, { -1 }); - - auto gather_1 = std::make_shared(convert_4, constant_07, constant_09); - auto gather_3 = std::make_shared(convert_5, constant_08, constant_10); - - auto constant_11 = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{ 3 }, { 0, 0, 0 }); - auto constant_12 = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{ 3 }, { 0, 0, 2 }); - auto constant_13 = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{ 3 }, { 1, 1, 1 }); - auto slice = std::make_shared(gather_1, constant_11, constant_12, constant_13, std::vector{}, std::vector{}); - - auto shape_of = std::make_shared(slice); - auto constant_14 = ov::op::v0::Constant::create(ov::element::f16, ov::Shape{ }, { 1 }); - auto constant_15 = ov::op::v0::Constant::create(ov::element::f16, ov::Shape{ }, { 0 }); - auto broadcast_ones_like = std::make_shared(constant_14, shape_of); - auto broadcast_zeros_like = std::make_shared(constant_15, shape_of); - - auto concat = std::make_shared(ov::OutputVector{broadcast_ones_like, gather_1}, -1); - auto concat_1 = std::make_shared(ov::OutputVector{broadcast_zeros_like, gather_3}, -1); - - auto constant_16 = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{ 1, 1, 2048 }, { 0 }); - auto rms = std::make_shared(input_2, constant_16, 1e-19); - - auto constant_17 = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{ 4 }, { 0, 0, 1024, 2 }); - auto reshape_2 = std::make_shared(rms, constant_17, true); - - auto constant_18 = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{ }, { -1 }); - auto split_1 = std::make_shared(reshape_2, constant_18, 2); - - auto constant_19 = ov::op::v0::Constant::create(ov::element::f16, ov::Shape{ }, { -1 }); - auto multiply_2 = std::make_shared(split_1->output(0), constant_19); - - auto constant_20 = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{ }, { -1 }); - auto squeeze_1 = std::make_shared(multiply_2, constant_20); - - auto constant_21 = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{ }, { -1 }); - auto unsqueeze_2 = std::make_shared(squeeze_1, constant_21); - - auto concat_stack_1 = std::make_shared(ov::OutputVector{unsqueeze_2, split_1->output(1)}, -1); - auto constant_22 = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{ 3 }, { 0, 0, 2048 }); - auto reshape_3 = std::make_shared(concat_stack_1, constant_22, true); - - auto multiply_3 = std::make_shared(rms, concat); - auto multiply_4 = std::make_shared(reshape_3, concat_1); + if (is_ltx_video) { + auto param_mul_in = std::make_shared(ov::element::f16, ov::PartialShape{1, 3, 64}); + auto param_x = std::make_shared(ov::element::f16, ov::PartialShape{1, 64, 16}); + + // Upstream with converts: Convert(f16→f32) before Multiply inputs, Convert on Add constant + auto convert_mul_in = std::make_shared(param_mul_in, ov::element::f32); + auto mul_const = ov::op::v0::Constant::create(ov::element::f16, ov::Shape{1, 1, 64}, {3.14f}); + auto convert_mul_const = std::make_shared(mul_const, ov::element::f32); + auto multiply = std::make_shared(convert_mul_in, convert_mul_const); + + auto add_const = ov::op::v0::Constant::create(ov::element::f16, ov::Shape{1, 1, 64}, {1e-6f}); + auto convert_add_const = std::make_shared(add_const, ov::element::f32); + auto add = std::make_shared(multiply, convert_add_const); + + auto transpose_order = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{3}, {0, 2, 1}); + auto transpose_up = std::make_shared(add, transpose_order); + + auto reshape_shape = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{3}, {1, 64, 3}); + auto reshape = std::make_shared(transpose_up, reshape_shape, false); + + // Sin path with Convert(f32→f16) after Sin + auto sin_node = std::make_shared(reshape); + auto convert_sin = std::make_shared(sin_node, ov::element::f16); + auto sin_t_order = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{3}, {0, 2, 1}); + auto sin_transpose = std::make_shared(convert_sin, sin_t_order); + auto sin_unsq_axis = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{1}, {0}); + auto sin_unsqueeze = std::make_shared(sin_transpose, sin_unsq_axis); + auto sin_bcast_shape = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{4}, {2, 1, 3, 64}); + auto sin_broadcast = std::make_shared(sin_unsqueeze, sin_bcast_shape); + auto sin_gnd_indices = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{1, 8, 3}, {0}); + auto sin_gathernd = std::make_shared(sin_broadcast, sin_gnd_indices); + auto sin_t2_order = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{3}, {0, 2, 1}); + auto sin_transpose2 = std::make_shared(sin_gathernd, sin_t2_order); + auto sin_concat_other = ov::op::v0::Constant::create(ov::element::f16, ov::Shape{1, 64, 8}, {0.0f}); + auto sin_concat = std::make_shared(ov::OutputVector{sin_concat_other, sin_transpose2}, -1); + + // Cos path with Convert(f32→f16) after Cos + auto cos_node = std::make_shared(reshape); + auto convert_cos = std::make_shared(cos_node, ov::element::f16); + auto cos_t_order = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{3}, {0, 2, 1}); + auto cos_transpose = std::make_shared(convert_cos, cos_t_order); + auto cos_unsq_axis = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{1}, {0}); + auto cos_unsqueeze = std::make_shared(cos_transpose, cos_unsq_axis); + auto cos_bcast_shape = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{4}, {2, 1, 3, 64}); + auto cos_broadcast = std::make_shared(cos_unsqueeze, cos_bcast_shape); + auto cos_gnd_indices = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{1, 8, 3}, {0}); + auto cos_gathernd = std::make_shared(cos_broadcast, cos_gnd_indices); + auto cos_t2_order = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{3}, {0, 2, 1}); + auto cos_transpose2 = std::make_shared(cos_gathernd, cos_t2_order); + auto cos_concat_other = ov::op::v0::Constant::create(ov::element::f16, ov::Shape{1, 64, 8}, {1.0f}); + auto cos_concat = std::make_shared(ov::OutputVector{cos_concat_other, cos_transpose2}, -1); + + ov::op::internal::RoPE::Config rope_config; + rope_config.is_ltx_video = true; + rope_config.rotary_ndims = 64; + auto rope = std::make_shared(ov::OutputVector{param_x, cos_concat, sin_concat}, rope_config); + + model_ref = std::make_shared(ov::OutputVector{rope}, ov::ParameterVector{param_mul_in, param_x}); + } + comparator.enable(FunctionsComparator::CmpValues::ATTRIBUTES); +} - auto add_1 = std::make_shared(multiply_3, multiply_4); - auto constant_23 = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{ 4 }, { 0, 0, 32, 64 }); - auto reshape_4 = std::make_shared(add_1, constant_23, true); - auto constant_24 = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{ 4 }, { 0, 2, 1, 3 }); - auto transpose_3 = std::make_shared(reshape_4, constant_24); +TEST_F(TransformationTestsF, IncreasePositionIdsLTXVideo_T) { + test_IncreasePositionIdsLTXVideo(model, model_ref, manager, comparator, true); +} - auto input_3 = std::make_shared(ov::element::f16, ov::PartialShape{ -1, -1, 32, 64 }); - auto input_4 = std::make_shared(ov::element::f16, ov::PartialShape{ -1, -1, 32, 64 }); - auto transpose_2 = std::make_shared(input_3, constant_24); - auto transpose_4 = std::make_shared(input_4, constant_24); - auto sdpa = std::make_shared(transpose_2, transpose_3, transpose_4, true); +TEST_F(TransformationTestsF, IncreasePositionIdsLTXVideo_F) { + test_IncreasePositionIdsLTXVideo(model, model_ref, manager, comparator, false); +} - model_ref = std::make_shared(ov::OutputVector{sdpa}, ov::ParameterVector{input_1, input_2, input_3, input_4}); +// model2 topology: Sin/Cos → Gather(repeat_interleave) → Concat +// Replaces the GatherND→Transpose chain used in model1. +static void test_IncreasePositionIdsLTXVideoGather(std::shared_ptr& model, + std::shared_ptr& model_ref, + ov::pass::Manager& manager, + FunctionsComparator& comparator, + bool is_ltx_video) { + { + auto param_mul_in = std::make_shared(ov::element::f16, ov::PartialShape{1, 3, 64}); + auto param_x = std::make_shared(ov::element::f16, ov::PartialShape{1, 64, 16}); + + // Upstream: Multiply → Add(Constant) → Transpose → Reshape + auto mul_const = ov::op::v0::Constant::create(ov::element::f16, ov::Shape{1, 1, 64}, {3.14f}); + auto multiply = std::make_shared(param_mul_in, mul_const); + auto add_const = ov::op::v0::Constant::create(ov::element::f16, ov::Shape{1, 1, 64}, {1e-6f}); + auto add = std::make_shared(multiply, add_const); + auto transpose_order = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{3}, {0, 2, 1}); + auto transpose_up = std::make_shared(add, transpose_order); + auto reshape_shape = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{3}, {1, 64, 3}); + auto reshape = std::make_shared(transpose_up, reshape_shape, false); + + // Sin path: Sin → Gather → Concat(Broadcast, Gather) + auto sin_node = std::make_shared(reshape); + auto sin_gather_indices = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{64}, {0}); + auto sin_gather_axis = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{}, {-1}); + auto sin_gather = std::make_shared(sin_node, sin_gather_indices, sin_gather_axis); + auto sin_concat_other = ov::op::v0::Constant::create(ov::element::f16, ov::Shape{1, 64, 8}, {0.0f}); + auto sin_concat = std::make_shared(ov::OutputVector{sin_concat_other, sin_gather}, -1); + + // Cos path: Cos → Gather → Concat(Broadcast, Gather) + auto cos_node = std::make_shared(reshape); + auto cos_gather_indices = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{64}, {0}); + auto cos_gather_axis = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{}, {-1}); + auto cos_gather = std::make_shared(cos_node, cos_gather_indices, cos_gather_axis); + auto cos_concat_other = ov::op::v0::Constant::create(ov::element::f16, ov::Shape{1, 64, 8}, {1.0f}); + auto cos_concat = std::make_shared(ov::OutputVector{cos_concat_other, cos_gather}, -1); + + ov::op::internal::RoPE::Config rope_config; + rope_config.is_ltx_video = is_ltx_video; + rope_config.rotary_ndims = 64; + auto rope = std::make_shared(ov::OutputVector{param_x, cos_concat, sin_concat}, rope_config); + + model = std::make_shared(ov::OutputVector{rope}, ov::ParameterVector{param_mul_in, param_x}); + manager.register_pass(); + } + if (is_ltx_video) { + auto param_mul_in = std::make_shared(ov::element::f16, ov::PartialShape{1, 3, 64}); + auto param_x = std::make_shared(ov::element::f16, ov::PartialShape{1, 64, 16}); + + // Upstream with f32 converts inserted by the pass + auto convert_mul_in = std::make_shared(param_mul_in, ov::element::f32); + auto mul_const = ov::op::v0::Constant::create(ov::element::f16, ov::Shape{1, 1, 64}, {3.14f}); + auto convert_mul_const = std::make_shared(mul_const, ov::element::f32); + auto multiply = std::make_shared(convert_mul_in, convert_mul_const); + auto add_const = ov::op::v0::Constant::create(ov::element::f16, ov::Shape{1, 1, 64}, {1e-6f}); + auto convert_add_const = std::make_shared(add_const, ov::element::f32); + auto add = std::make_shared(multiply, convert_add_const); + auto transpose_order = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{3}, {0, 2, 1}); + auto transpose_up = std::make_shared(add, transpose_order); + auto reshape_shape = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{3}, {1, 64, 3}); + auto reshape = std::make_shared(transpose_up, reshape_shape, false); + + // Sin path with Convert(f32→f16) after Sin + auto sin_node = std::make_shared(reshape); + auto convert_sin = std::make_shared(sin_node, ov::element::f16); + auto sin_gather_indices = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{64}, {0}); + auto sin_gather_axis = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{}, {-1}); + auto sin_gather = std::make_shared(convert_sin, sin_gather_indices, sin_gather_axis); + auto sin_concat_other = ov::op::v0::Constant::create(ov::element::f16, ov::Shape{1, 64, 8}, {0.0f}); + auto sin_concat = std::make_shared(ov::OutputVector{sin_concat_other, sin_gather}, -1); + + // Cos path with Convert(f32→f16) after Cos + auto cos_node = std::make_shared(reshape); + auto convert_cos = std::make_shared(cos_node, ov::element::f16); + auto cos_gather_indices = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{64}, {0}); + auto cos_gather_axis = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{}, {-1}); + auto cos_gather = std::make_shared(convert_cos, cos_gather_indices, cos_gather_axis); + auto cos_concat_other = ov::op::v0::Constant::create(ov::element::f16, ov::Shape{1, 64, 8}, {1.0f}); + auto cos_concat = std::make_shared(ov::OutputVector{cos_concat_other, cos_gather}, -1); + + ov::op::internal::RoPE::Config rope_config; + rope_config.is_ltx_video = true; + rope_config.rotary_ndims = 64; + auto rope = std::make_shared(ov::OutputVector{param_x, cos_concat, sin_concat}, rope_config); + + model_ref = std::make_shared(ov::OutputVector{rope}, ov::ParameterVector{param_mul_in, param_x}); } comparator.enable(FunctionsComparator::CmpValues::ATTRIBUTES); } +TEST_F(TransformationTestsF, IncreasePositionIdsLTXVideoGather_T) { + test_IncreasePositionIdsLTXVideoGather(model, model_ref, manager, comparator, true); +} + +TEST_F(TransformationTestsF, IncreasePositionIdsLTXVideoGather_F) { + test_IncreasePositionIdsLTXVideoGather(model, model_ref, manager, comparator, false); +} + TEST_F(TransformationTestsF, IncreasePositionIdsPrecisionForQwen25VL) { { auto position_ids = std::make_shared(ov::element::i64, ov::PartialShape{ 3, -1, -1 }); @@ -729,102 +780,99 @@ TEST_F(TransformationTestsF, IncreasePositionIdsPrecisionForQwen25VL) { comparator.enable(FunctionsComparator::CmpValues::ATTRIBUTES); } -TEST_F(TransformationTestsF, IncreasePositionIdsPrecisionForQwen3VL) { - // Qwen3-VL pattern: position_ids -> Convert(i64->i32) -> Reshape(unsqueeze) -> Convert(i32->f16) - // -> MatMul(Broadcast, Convert) -> Reshape(transpose) -> Gather -> Concat(self,self) - // -> Sin/Cos -> Reshape(unsqueeze) -> RoPE - { - auto position_ids = std::make_shared(ov::element::i64, ov::PartialShape{ 3, -1 }); - auto input_convert = std::make_shared(position_ids, ov::element::i32); - // Qwen3-VL uses Reshape instead of Unsqueeze - auto input_reshape = std::make_shared(input_convert, - std::make_shared(ov::element::i32, ov::Shape{4}, std::vector{3, -1, 1, 1}), true); - auto convert_2 = std::make_shared(input_reshape, ov::element::f16); - - auto shape_of = std::make_shared(input_convert, ov::element::i32); - auto gather_0 = std::make_shared(shape_of, +namespace { + +// Shared builder for Qwen3-family RoPE precision tests. The `insert_pre_convert` callback +// takes the i32 Convert output of `position_ids` and returns the node whose output feeds the +// f16/f32 Convert on the MatMul path, letting each test inject model-specific pre-Convert ops +// (e.g. Reshape/StridedSlice). When `is_ref` is true, the returned model carries the f16<->f32 +// Convert nodes that IncreasePositionIdsPrecision is expected to insert. +template +std::shared_ptr build_qwen3vl_rope_model(InsertPreConvert&& insert_pre_convert, bool is_ref) { + auto position_ids = std::make_shared(ov::element::i64, ov::PartialShape{3, -1}); + auto input_convert = std::make_shared(position_ids, ov::element::i32); + auto input_reshape = insert_pre_convert(input_convert); + auto convert_2 = std::make_shared(input_reshape, + is_ref ? ov::element::f32 : ov::element::f16); + + auto shape_of = std::make_shared(input_convert, ov::element::i32); + auto gather_0 = std::make_shared(shape_of, + std::make_shared(ov::element::i32, ov::Shape{1}, std::vector{1}), + std::make_shared(ov::element::i32, ov::Shape{}, std::vector{0})); + auto concat = std::make_shared(ov::OutputVector{ std::make_shared(ov::element::i32, ov::Shape{1}, std::vector{1}), - std::make_shared(ov::element::i32, ov::Shape{}, std::vector{0})); - auto concat = std::make_shared(ov::OutputVector{ - std::make_shared(ov::element::i32, ov::Shape{1}, std::vector{1}), - gather_0, - std::make_shared(ov::element::i32, ov::Shape{1}, std::vector{64}), - std::make_shared(ov::element::i32, ov::Shape{1}, std::vector{1})}, 0); - auto broadcast = std::make_shared( - std::make_shared(ov::element::f16, ov::Shape{1, 1, 64, 1}), concat); - auto matmul = std::make_shared(broadcast, convert_2); - - // Reshape(transpose) -> Gather(select channel 0) - auto reshape_transpose = std::make_shared(matmul, - std::make_shared(ov::element::i32, ov::Shape{4}, std::vector{3, -1, 1, 64}), true); - auto gather_ch0 = std::make_shared(reshape_transpose, - std::make_shared(ov::element::i32, ov::Shape{}, std::vector{0}), - std::make_shared(ov::element::i32, ov::Shape{}, std::vector{0})); - - // Concat(self, self) to produce [?, 1, 128] - auto concat_2 = std::make_shared(ov::OutputVector{gather_ch0, gather_ch0}, 2); + gather_0, + std::make_shared(ov::element::i32, ov::Shape{1}, std::vector{64}), + std::make_shared(ov::element::i32, ov::Shape{1}, std::vector{1})}, 0); + auto broadcast = std::make_shared( + std::make_shared(ov::element::f16, ov::Shape{1, 1, 64, 1}), concat); + + std::shared_ptr matmul_lhs = broadcast; + if (is_ref) { + matmul_lhs = std::make_shared(broadcast, ov::element::f32); + } + auto matmul = std::make_shared(matmul_lhs, convert_2); - auto cos = std::make_shared(concat_2); - auto sin = std::make_shared(concat_2); - auto cos_unsqueeze = std::make_shared(cos, - std::make_shared(ov::element::i32, ov::Shape{4}, std::vector{-1, 1, 1, 128}), true); - auto sin_unsqueeze = std::make_shared(sin, - std::make_shared(ov::element::i32, ov::Shape{4}, std::vector{-1, 1, 1, 128}), true); + auto reshape_transpose = std::make_shared(matmul, + std::make_shared(ov::element::i32, ov::Shape{4}, std::vector{3, -1, 1, 64}), true); + auto gather_ch0 = std::make_shared(reshape_transpose, + std::make_shared(ov::element::i32, ov::Shape{}, std::vector{0}), + std::make_shared(ov::element::i32, ov::Shape{}, std::vector{0})); - auto input_2 = std::make_shared(ov::element::f16, ov::PartialShape{ -1, 8, -1, 128}); - auto rope = std::make_shared(ov::OutputVector{input_2, cos_unsqueeze, sin_unsqueeze}, - ov::op::internal::RoPE::Config()); + auto concat_2 = std::make_shared(ov::OutputVector{gather_ch0, gather_ch0}, 2); - model = std::make_shared(ov::OutputVector{rope}, ov::ParameterVector{position_ids, input_2}); - manager.register_pass(); + std::shared_ptr cos = std::make_shared(concat_2); + std::shared_ptr sin = std::make_shared(concat_2); + if (is_ref) { + cos = std::make_shared(cos, ov::element::f16); + sin = std::make_shared(sin, ov::element::f16); } - { - auto position_ids = std::make_shared(ov::element::i64, ov::PartialShape{ 3, -1 }); - auto input_convert = std::make_shared(position_ids, ov::element::i32); - auto input_reshape = std::make_shared(input_convert, - std::make_shared(ov::element::i32, ov::Shape{4}, std::vector{3, -1, 1, 1}), true); - // Changed: Convert to f32 instead of f16 - auto convert_2 = std::make_shared(input_reshape, ov::element::f32); - - auto shape_of = std::make_shared(input_convert, ov::element::i32); - auto gather_0 = std::make_shared(shape_of, - std::make_shared(ov::element::i32, ov::Shape{1}, std::vector{1}), - std::make_shared(ov::element::i32, ov::Shape{}, std::vector{0})); - auto concat = std::make_shared(ov::OutputVector{ - std::make_shared(ov::element::i32, ov::Shape{1}, std::vector{1}), - gather_0, - std::make_shared(ov::element::i32, ov::Shape{1}, std::vector{64}), - std::make_shared(ov::element::i32, ov::Shape{1}, std::vector{1})}, 0); - auto broadcast = std::make_shared( - std::make_shared(ov::element::f16, ov::Shape{1, 1, 64, 1}), concat); - // Changed: Insert Convert(f16->f32) after Broadcast - auto broadcast_to_f32 = std::make_shared(broadcast, ov::element::f32); - auto matmul = std::make_shared(broadcast_to_f32, convert_2); + auto cos_unsqueeze = std::make_shared(cos, + std::make_shared(ov::element::i32, ov::Shape{4}, std::vector{-1, 1, 1, 128}), true); + auto sin_unsqueeze = std::make_shared(sin, + std::make_shared(ov::element::i32, ov::Shape{4}, std::vector{-1, 1, 1, 128}), true); - auto reshape_transpose = std::make_shared(matmul, - std::make_shared(ov::element::i32, ov::Shape{4}, std::vector{3, -1, 1, 64}), true); - auto gather_ch0 = std::make_shared(reshape_transpose, - std::make_shared(ov::element::i32, ov::Shape{}, std::vector{0}), - std::make_shared(ov::element::i32, ov::Shape{}, std::vector{0})); + auto input_2 = std::make_shared(ov::element::f16, ov::PartialShape{-1, 8, -1, 128}); + auto rope = std::make_shared( + ov::OutputVector{input_2, cos_unsqueeze, sin_unsqueeze}, ov::op::internal::RoPE::Config()); - auto concat_2 = std::make_shared(ov::OutputVector{gather_ch0, gather_ch0}, 2); + return std::make_shared(ov::OutputVector{rope}, ov::ParameterVector{position_ids, input_2}); +} - auto cos = std::make_shared(concat_2); - auto sin = std::make_shared(concat_2); - // Changed: Insert Convert(f32->f16) after Cos and Sin - auto cos_to_f16 = std::make_shared(cos, ov::element::f16); - auto sin_to_f16 = std::make_shared(sin, ov::element::f16); - auto cos_unsqueeze = std::make_shared(cos_to_f16, - std::make_shared(ov::element::i32, ov::Shape{4}, std::vector{-1, 1, 1, 128}), true); - auto sin_unsqueeze = std::make_shared(sin_to_f16, - std::make_shared(ov::element::i32, ov::Shape{4}, std::vector{-1, 1, 1, 128}), true); +} // namespace - auto input_2 = std::make_shared(ov::element::f16, ov::PartialShape{ -1, 8, -1, 128}); - auto rope = std::make_shared(ov::OutputVector{input_2, cos_unsqueeze, sin_unsqueeze}, - ov::op::internal::RoPE::Config()); +TEST_F(TransformationTestsF, IncreasePositionIdsPrecisionForQwen3VL) { + // Qwen3-VL pattern: position_ids -> Convert(i64->i32) -> Reshape(unsqueeze) -> Convert(i32->f16) + // -> MatMul(Broadcast, Convert) -> Reshape(transpose) -> Gather -> Concat(self,self) + // -> Sin/Cos -> Reshape(unsqueeze) -> RoPE + auto insert_pre_convert = [](const std::shared_ptr& input_convert) -> std::shared_ptr { + // Qwen3-VL uses Reshape instead of Unsqueeze + return std::make_shared(input_convert, + std::make_shared(ov::element::i32, ov::Shape{4}, std::vector{3, -1, 1, 1}), true); + }; + model = build_qwen3vl_rope_model(insert_pre_convert, /*is_ref=*/false); + manager.register_pass(); + model_ref = build_qwen3vl_rope_model(insert_pre_convert, /*is_ref=*/true); + comparator.enable(FunctionsComparator::CmpValues::ATTRIBUTES); +} - model_ref = std::make_shared(ov::OutputVector{rope}, ov::ParameterVector{position_ids, input_2}); - } +TEST_F(TransformationTestsF, IncreasePositionIdsPrecisionForQwen35) { + // Qwen3.5 pattern: identical to Qwen3-VL but with an extra Reshape + StridedSlice inserted + // between Convert(i64->i32) and the pre-Convert(f16) Reshape. + auto insert_pre_convert = [](const std::shared_ptr& input_convert) -> std::shared_ptr { + auto input_reshape_0 = std::make_shared(input_convert, + std::make_shared(ov::element::i32, ov::Shape{3}, std::vector{4, -1, 1}), true); + auto begin = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{1}, {1}); + auto end = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{1}, {0}); + auto stride = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{1}, {1}); + auto input_slice_0 = std::make_shared(input_reshape_0, begin, end, stride, + std::vector{}, std::vector{}); + return std::make_shared(input_slice_0, + std::make_shared(ov::element::i32, ov::Shape{4}, std::vector{3, -1, 1, 1}), true); + }; + model = build_qwen3vl_rope_model(insert_pre_convert, /*is_ref=*/false); + manager.register_pass(); + model_ref = build_qwen3vl_rope_model(insert_pre_convert, /*is_ref=*/true); comparator.enable(FunctionsComparator::CmpValues::ATTRIBUTES); } diff --git a/src/plugins/intel_gpu/tests/unit/transformations/keep_xattention_threshold_precision_test.cpp b/src/plugins/intel_gpu/tests/unit/transformations/keep_xattention_threshold_precision_test.cpp new file mode 100644 index 000000000000..c49d53c9aa13 --- /dev/null +++ b/src/plugins/intel_gpu/tests/unit/transformations/keep_xattention_threshold_precision_test.cpp @@ -0,0 +1,121 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "plugin/transformations/keep_xattention_threshold_precision.hpp" + +#include + +#include "common_test_utils/ov_test_utils.hpp" +#include "intel_gpu/primitives/paged_attention.hpp" +#include "openvino/core/model.hpp" +#include "openvino/op/paged_attention.hpp" +#include "openvino/op/parameter.hpp" +#include "openvino/op/result.hpp" +#include "openvino/op/util/precision_sensitive_attribute.hpp" + +using namespace testing; +using namespace ov::intel_gpu; + +namespace { + +using PAExt = ov::op::PagedAttentionExtension; + +std::shared_ptr make_pa_model_with_threshold_et(const ov::element::Type& thr_et) { + // Build a minimal model containing PagedAttentionExtension with valid 28 inputs. + // We keep shapes mostly dynamic but MUST satisfy rank/type checks in + // ov::op::PagedAttentionExtension::validate_and_infer_types(). + + const size_t thr_idx = cldnn::paged_attention::PagedAttentionInputIdx::XATTENTION_THRESHOLD; + + ov::ParameterVector params(28); + // 0..4: query/key/value/key_cache/value_cache + params[0] = std::make_shared(ov::element::f16, ov::PartialShape::dynamic(2)); + params[1] = std::make_shared(ov::element::f16, ov::PartialShape::dynamic(2)); + params[2] = std::make_shared(ov::element::f16, ov::PartialShape::dynamic(2)); + params[3] = std::make_shared(ov::element::f16, ov::PartialShape::dynamic()); + params[4] = std::make_shared(ov::element::f16, ov::PartialShape::dynamic()); + + // 5..8: i32 vectors + for (size_t i = 5; i <= 8; i++) + params[i] = std::make_shared(ov::element::i32, ov::PartialShape::dynamic(1)); + + // 9: scale scalar real + params[9] = std::make_shared(ov::element::f32, ov::PartialShape::dynamic(0)); + // 10: sliding_window scalar i32 + params[10] = std::make_shared(ov::element::i32, ov::PartialShape::dynamic(0)); + // 11: alibi_slopes rank1 real + params[11] = std::make_shared(ov::element::f32, ov::PartialShape::dynamic(1)); + // 12: max_context_len scalar i32 + params[12] = std::make_shared(ov::element::i32, ov::PartialShape::dynamic(0)); + // 13: score_aggregation_window rank0/1 i32 + params[13] = std::make_shared(ov::element::i32, ov::PartialShape::dynamic(0)); + + // 14: rotated_block_indices rank1 i32 + params[14] = std::make_shared(ov::element::i32, ov::PartialShape::dynamic(1)); + // 15: rotation_deltas rank1/2 i32 + params[15] = std::make_shared(ov::element::i32, ov::PartialShape::dynamic(1)); + // 16: rotation_trig_lut rank1/2 f16/f32 + params[16] = std::make_shared(ov::element::f16, ov::PartialShape::dynamic(1)); + + // 17: xattention_threshold rank1 f16/f32 (or other type in negative test) + params[thr_idx] = std::make_shared(thr_et, ov::PartialShape::dynamic(1)); + + // 18..19: scalars i32 + params[18] = std::make_shared(ov::element::i32, ov::PartialShape::dynamic(0)); + params[19] = std::make_shared(ov::element::i32, ov::PartialShape::dynamic(0)); + + // 20: sinks rank1/4 any type (use f16) + params[20] = std::make_shared(ov::element::f16, ov::PartialShape::dynamic(1)); + + // 21: scalar i32 + params[21] = std::make_shared(ov::element::i32, ov::PartialShape::dynamic(0)); + // 22..25: vectors/matrices i32 + params[22] = std::make_shared(ov::element::i32, ov::PartialShape::dynamic(1)); + params[23] = std::make_shared(ov::element::i32, ov::PartialShape::dynamic(1)); + params[24] = std::make_shared(ov::element::i32, ov::PartialShape::dynamic(1)); + params[25] = std::make_shared(ov::element::i32, ov::PartialShape::dynamic(1)); + // 26..27: qq_bias inputs + params[26] = std::make_shared(ov::element::u8, ov::PartialShape::dynamic(1)); + params[27] = std::make_shared(ov::element::i32, ov::PartialShape::dynamic(1)); + + ov::OutputVector inputs; + inputs.reserve(params.size()); + for (const auto& p : params) + inputs.emplace_back(p); + + auto pa = std::make_shared(inputs); + auto res = std::make_shared(pa); + return std::make_shared(ov::ResultVector{res}, params); +} + +std::shared_ptr get_pa_ext(const std::shared_ptr& model) { + return ov::as_type_ptr(model->get_results().at(0)->input_value(0).get_node_shared_ptr()); +} + +std::shared_ptr make_ref_pa_model_with_precision_sensitive_threshold(const ov::element::Type& thr_et) { + auto model = make_pa_model_with_threshold_et(thr_et); + const size_t thr_idx = cldnn::paged_attention::PagedAttentionInputIdx::XATTENTION_THRESHOLD; + auto pa = get_pa_ext(model); + OPENVINO_ASSERT(pa != nullptr); + ov::mark_as_precision_sensitive(pa->input(thr_idx)); + return model; +} + +} // namespace + +TEST_F(TransformationTestsF, KeepXAttentionThresholdPrecisionMarksF32Threshold) { + model = make_pa_model_with_threshold_et(ov::element::f32); + model_ref = make_ref_pa_model_with_precision_sensitive_threshold(ov::element::f32); + + manager.register_pass(); +} + +TEST_F(TransformationTestsF, KeepXAttentionThresholdPrecisionMarksF16Threshold) { + // PagedAttentionExtension validation allows only f16/f32 on xattention_threshold port, + // so only the valid real-type cases can be covered here. + model = make_pa_model_with_threshold_et(ov::element::f16); + model_ref = make_ref_pa_model_with_precision_sensitive_threshold(ov::element::f16); + + manager.register_pass(); +} diff --git a/src/plugins/intel_gpu/tests/unit/transformations/kv_cache_fusion_test.cpp b/src/plugins/intel_gpu/tests/unit/transformations/kv_cache_fusion_test.cpp index 00c3924e58e7..562e09b0890f 100644 --- a/src/plugins/intel_gpu/tests/unit/transformations/kv_cache_fusion_test.cpp +++ b/src/plugins/intel_gpu/tests/unit/transformations/kv_cache_fusion_test.cpp @@ -90,7 +90,6 @@ TEST_F(TransformationTestsF, KVCacheFusionTest3) { auto var_split = std::make_shared(parameter, ov::op::v0::Constant::create(ov::element::i32, ov::Shape{}, {1}), ov::op::v0::Constant::create(ov::element::i32, ov::Shape{1}, {32})); - std::cout << var_split->get_output_partial_shape(0) << std::endl; auto beam_idx = std::make_shared(ov::element::i32, ov::PartialShape{1}); auto axis = std::make_shared(ov::element::i64, ov::Shape{}, 0); auto gather_past = std::make_shared(past, beam_idx, axis); diff --git a/src/plugins/intel_gpu/tests/unit/transformations/pa_kv_reorder_fusion_test.cpp b/src/plugins/intel_gpu/tests/unit/transformations/pa_kv_reorder_fusion_test.cpp deleted file mode 100644 index d2990ec9fdbb..000000000000 --- a/src/plugins/intel_gpu/tests/unit/transformations/pa_kv_reorder_fusion_test.cpp +++ /dev/null @@ -1,162 +0,0 @@ -// Copyright (C) 2018-2026 Intel Corporation -// SPDX-License-Identifier: Apache-2.0 -// - -#include "common_test_utils/ov_test_utils.hpp" - -#include "openvino/core/model.hpp" -#include "openvino/op/concat.hpp" -#include "openvino/op/constant.hpp" -#include "openvino/op/gather.hpp" -#include "openvino/op/parameter.hpp" -#include "openvino/op/result.hpp" -#include "openvino/op/scatter_update.hpp" - -#include "intel_gpu/op/pa_kv_reorder.hpp" -#include "plugin/transformations/pa_kv_reorder_fusion.hpp" - -#include - -using namespace testing; -using namespace ov::intel_gpu; - -namespace ov { -namespace test { -namespace intel_gpu { - -TEST_F(TransformationTestsF, PaKVReorderFusion_basic) { - disable_result_friendly_names_check(); - { - auto key_cache = std::make_shared(ov::element::f16, ov::PartialShape{4, 2}); - key_cache->set_friendly_name("key_cache.0_clone_for_k_update"); - auto value_cache = std::make_shared(ov::element::f16, ov::PartialShape{4, 2}); - value_cache->set_friendly_name("value_cache.0_clone_for_v_update"); - - auto block_indices = std::make_shared(ov::element::i32, ov::PartialShape{2}); - block_indices->set_friendly_name("block_indices"); - auto block_indices_begins = std::make_shared(ov::element::i32, ov::PartialShape{2}); - block_indices_begins->set_friendly_name("block_indices_begins"); - - auto block_update_indices = std::make_shared(ov::element::i32, ov::PartialShape{2}); - block_update_indices->set_friendly_name("block_update_indices"); - auto block_update_indices_begins = std::make_shared(ov::element::i32, ov::PartialShape{2}); - block_update_indices_begins->set_friendly_name("block_update_indices_begins"); - - auto gather_axis = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{}, {0}); - auto scatter_axis = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{}, {0}); - - auto key_gather = std::make_shared(key_cache, block_update_indices, gather_axis); - auto value_gather = std::make_shared(value_cache, block_update_indices, gather_axis); - - auto key_scatter = std::make_shared(key_cache, block_indices, key_gather, scatter_axis); - auto value_scatter = std::make_shared(value_cache, block_indices, value_gather, scatter_axis); - - auto concat = std::make_shared(ov::OutputVector{key_scatter, value_scatter}, 0); - auto result = std::make_shared(concat); - - model = std::make_shared(ov::ResultVector{result}, - ov::ParameterVector{key_cache, - value_cache, - block_indices, - block_indices_begins, - block_update_indices, - block_update_indices_begins}); - - manager.register_pass(false, - std::vector{0, 1, 3, 2}, - std::vector{0, 1, 2, 3}, - ov::element::f16, - ov::element::f16, - ov::element::f16); - } - - { - auto key_cache = std::make_shared(ov::element::f16, ov::PartialShape{4, 2}); - key_cache->set_friendly_name("key_cache.0_clone_for_k_update"); - auto value_cache = std::make_shared(ov::element::f16, ov::PartialShape{4, 2}); - value_cache->set_friendly_name("value_cache.0_clone_for_v_update"); - - auto block_indices = std::make_shared(ov::element::i32, ov::PartialShape{2}); - block_indices->set_friendly_name("block_indices"); - auto block_indices_begins = std::make_shared(ov::element::i32, ov::PartialShape{2}); - block_indices_begins->set_friendly_name("block_indices_begins"); - - auto block_update_indices = std::make_shared(ov::element::i32, ov::PartialShape{2}); - block_update_indices->set_friendly_name("block_update_indices"); - auto block_update_indices_begins = std::make_shared(ov::element::i32, ov::PartialShape{2}); - block_update_indices_begins->set_friendly_name("block_update_indices_begins"); - - auto pa_kv_reorder = std::make_shared(key_cache, - value_cache, - block_indices, - block_indices_begins, - block_update_indices, - block_update_indices_begins); - pa_kv_reorder->set_friendly_name("pa_kv_reorder_0"); - auto result = std::make_shared(pa_kv_reorder); - - model_ref = std::make_shared(ov::ResultVector{result}, - ov::ParameterVector{key_cache, - value_cache, - block_indices, - block_indices_begins, - block_update_indices, - block_update_indices_begins}); - comparator.enable(FunctionsComparator::ATTRIBUTES); - } -} - -TEST_F(TransformationTestsF, PaKVReorderFusion_skip_on_mismatched_block_indices) { - disable_result_friendly_names_check(); - auto key_cache = std::make_shared(ov::element::f16, ov::PartialShape{4, 2}); - key_cache->set_friendly_name("key_cache.0_clone_for_k_update"); - auto value_cache = std::make_shared(ov::element::f16, ov::PartialShape{4, 2}); - value_cache->set_friendly_name("value_cache.0_clone_for_v_update"); - - auto block_indices_k = std::make_shared(ov::element::i32, ov::PartialShape{2}); - block_indices_k->set_friendly_name("block_indices_k"); - auto block_indices_v = std::make_shared(ov::element::i32, ov::PartialShape{2}); - block_indices_v->set_friendly_name("block_indices_v"); - - auto block_indices_begins = std::make_shared(ov::element::i32, ov::PartialShape{2}); - block_indices_begins->set_friendly_name("block_indices_begins"); - auto block_update_indices = std::make_shared(ov::element::i32, ov::PartialShape{2}); - block_update_indices->set_friendly_name("block_update_indices"); - auto block_update_indices_begins = std::make_shared(ov::element::i32, ov::PartialShape{2}); - block_update_indices_begins->set_friendly_name("block_update_indices_begins"); - - auto gather_axis = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{}, {0}); - auto scatter_axis = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{}, {0}); - - auto key_gather = std::make_shared(key_cache, block_update_indices, gather_axis); - auto value_gather = std::make_shared(value_cache, block_update_indices, gather_axis); - - auto key_scatter = std::make_shared(key_cache, block_indices_k, key_gather, scatter_axis); - auto value_scatter = std::make_shared(value_cache, block_indices_v, value_gather, scatter_axis); - - auto concat = std::make_shared(ov::OutputVector{key_scatter, value_scatter}, 0); - auto result = std::make_shared(concat); - - model = std::make_shared(ov::ResultVector{result}, - ov::ParameterVector{key_cache, - value_cache, - block_indices_k, - block_indices_v, - block_indices_begins, - block_update_indices, - block_update_indices_begins}); - - manager.register_pass(false, - std::vector{0, 1, 3, 2}, - std::vector{0, 1, 2, 3}, - ov::element::f16, - ov::element::f16, - ov::element::f16); - - model_ref = model->clone(); - comparator.enable(FunctionsComparator::ATTRIBUTES); -} - -} // namespace intel_gpu -} // namespace test -} // namespace ov diff --git a/src/plugins/intel_gpu/tests/unit/transformations/reduce_fc_dimensions_test.cpp b/src/plugins/intel_gpu/tests/unit/transformations/reduce_fc_dimensions_test.cpp new file mode 100644 index 000000000000..99031cf28638 --- /dev/null +++ b/src/plugins/intel_gpu/tests/unit/transformations/reduce_fc_dimensions_test.cpp @@ -0,0 +1,156 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include + +#include "common_test_utils/ov_test_utils.hpp" +#include "intel_gpu/op/fully_connected.hpp" +#include "intel_gpu/op/placeholder.hpp" +#include "openvino/core/model.hpp" +#include "openvino/op/add.hpp" +#include "openvino/op/constant.hpp" +#include "openvino/op/convert.hpp" +#include "openvino/op/multiply.hpp" +#include "openvino/op/parameter.hpp" +#include "openvino/op/reshape.hpp" +#include "openvino/op/result.hpp" +#include "openvino/pass/manager.hpp" +#include "plugin/transformations/reduce_fc_dimensions.hpp" + +using namespace testing; +using namespace ov::intel_gpu; + +namespace ov { +namespace test { +namespace intel_gpu { + +// Regular case, transformation should trigger +TEST_F(TransformationTestsF, ReduceFCDimensions1) { + { + auto input1 = std::make_shared(ov::element::f32, ov::PartialShape{1, 1, -1, 16}); + auto weights_const = ov::op::v0::Constant::create(ov::element::u8, ov::Shape{32, 16}, {1}); + auto convert = std::make_shared(weights_const, ov::element::f32); + auto scale_const = ov::op::v0::Constant::create(ov::element::f32, ov::Shape{32, 1}, {1}); + auto scale = std::make_shared(convert, scale_const); + auto no_bias = std::make_shared(); + auto fc = std::make_shared(input1, scale, no_bias); + + model = std::make_shared(ov::OutputVector{fc}, ov::ParameterVector{input1}); + manager.register_pass(); + } + { + auto input1 = std::make_shared(ov::element::f32, ov::PartialShape{1, 1, -1, 16}); + auto squeeze_const = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{3}, {1, -1, 16}); + auto squeeze = std::make_shared(input1, squeeze_const, false); + auto weights_const = ov::op::v0::Constant::create(ov::element::u8, ov::Shape{32, 16}, {1}); + auto convert = std::make_shared(weights_const, ov::element::f32); + auto scale_const = ov::op::v0::Constant::create(ov::element::f32, ov::Shape{32, 1}, {1}); + auto scale = std::make_shared(convert, scale_const); + auto no_bias = std::make_shared(); + auto fc = std::make_shared(squeeze, scale, no_bias); + auto unsqueeze_const = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{4}, {1, 1, -1, 32}); + auto unsqueeze = std::make_shared(fc, unsqueeze_const, false); + + model_ref = std::make_shared(ov::OutputVector{unsqueeze}, ov::ParameterVector{input1}); + } +} + +// Incorrect input size, transformation should not trigger +TEST_F(TransformationTestsF, ReduceFCDimensions2) { + { + auto input1 = std::make_shared(ov::element::f32, ov::PartialShape{1, 4, -1, 16}); + auto weights_const = ov::op::v0::Constant::create(ov::element::u8, ov::Shape{32, 16}, {1}); + auto convert = std::make_shared(weights_const, ov::element::f32); + auto scale_const = ov::op::v0::Constant::create(ov::element::f32, ov::Shape{32, 1}, {1}); + auto scale = std::make_shared(convert, scale_const); + auto no_bias = std::make_shared(); + auto fc = std::make_shared(input1, scale, no_bias); + + model = std::make_shared(ov::OutputVector{fc}, ov::ParameterVector{input1}); + manager.register_pass(); + } + { + model_ref = model->clone(); + } +} + +// Bias present, transformation should not trigger +TEST_F(TransformationTestsF, ReduceFCDimensions3) { + { + auto input1 = std::make_shared(ov::element::f32, ov::PartialShape{1, 1, -1, 16}); + auto weights_const = ov::op::v0::Constant::create(ov::element::u8, ov::Shape{32, 16}, {1}); + auto convert = std::make_shared(weights_const, ov::element::f32); + auto scale_const = ov::op::v0::Constant::create(ov::element::f32, ov::Shape{32, 1}, {1}); + auto scale = std::make_shared(convert, scale_const); + auto bias = ov::op::v0::Constant::create(ov::element::f32, ov::Shape{1, 1, 1, 32}, {1.0}); + auto fc = std::make_shared(input1, scale, bias); + + model = std::make_shared(ov::OutputVector{fc}, ov::ParameterVector{input1}); + manager.register_pass(); + } + { + model_ref = model->clone(); + } +} + +// 3D weight, transformation should not trigger +TEST_F(TransformationTestsF, ReduceFCDimensions4) { + { + auto input1 = std::make_shared(ov::element::f32, ov::PartialShape{1, 1, -1, 16}); + auto weights_const = ov::op::v0::Constant::create(ov::element::u8, ov::Shape{4, 32, 16}, {1}); + auto convert = std::make_shared(weights_const, ov::element::f32); + auto scale_const = ov::op::v0::Constant::create(ov::element::f32, ov::Shape{4, 32, 1}, {1}); + auto scale = std::make_shared(convert, scale_const); + auto no_bias = std::make_shared(); + auto fc = std::make_shared(input1, scale, no_bias); + + model = std::make_shared(ov::OutputVector{fc}, ov::ParameterVector{input1}); + manager.register_pass(); + } + { + model_ref = model->clone(); + } +} + +// Dynamic result dim, transformation should not trigger +TEST_F(TransformationTestsF, ReduceFCDimensions5) { + { + auto input1 = std::make_shared(ov::element::f32, ov::PartialShape{1, 1, -1, 16}); + auto weights_param = std::make_shared(ov::element::u8, ov::PartialShape{-1, 16}); + auto convert = std::make_shared(weights_param, ov::element::f32); + auto scale_param = std::make_shared(ov::element::f32, ov::PartialShape{-1, 1}); + auto scale = std::make_shared(convert, scale_param); + auto no_bias = std::make_shared(); + auto fc = std::make_shared(input1, scale, no_bias); + + model = std::make_shared(ov::OutputVector{fc}, ov::ParameterVector{input1, weights_param, scale_param}); + manager.register_pass(); + } + { + model_ref = model->clone(); + } +} + +// Dynamic inner dim, transformation should not trigger +TEST_F(TransformationTestsF, ReduceFCDimensions6) { + { + auto input1 = std::make_shared(ov::element::f32, ov::PartialShape{1, 1, 10, -1}); + auto weights_param = std::make_shared(ov::element::u8, ov::PartialShape{32, -1}); + auto convert = std::make_shared(weights_param, ov::element::f32); + auto scale_const = ov::op::v0::Constant::create(ov::element::f32, ov::Shape{32, 1}, {1}); + auto scale = std::make_shared(convert, scale_const); + auto no_bias = std::make_shared(); + auto fc = std::make_shared(input1, scale, no_bias); + + model = std::make_shared(ov::OutputVector{fc}, ov::ParameterVector{input1, weights_param}); + manager.register_pass(); + } + { + model_ref = model->clone(); + } +} + +} // namespace intel_gpu +} // namespace test +} // namespace ov diff --git a/src/plugins/intel_gpu/tests/unit/transformations/unsqueeze_broadcast_reshape_sdpa_fusion_test.cpp b/src/plugins/intel_gpu/tests/unit/transformations/unsqueeze_broadcast_reshape_sdpa_fusion_test.cpp index 8fba52a07706..d1c6043d6b87 100644 --- a/src/plugins/intel_gpu/tests/unit/transformations/unsqueeze_broadcast_reshape_sdpa_fusion_test.cpp +++ b/src/plugins/intel_gpu/tests/unit/transformations/unsqueeze_broadcast_reshape_sdpa_fusion_test.cpp @@ -15,6 +15,7 @@ #include "openvino/op/reshape.hpp" #include "openvino/op/transpose.hpp" #include "openvino/op/unsqueeze.hpp" +#include "openvino/op/concat.hpp" #include "intel_gpu/op/sdpa.hpp" #include "intel_gpu/op/read_value.hpp" @@ -321,6 +322,178 @@ TEST_F(TransformationTestsF, UnsqueezeBroadReshapeSDPAFusion6) { } } +TEST_F(TransformationTestsF, UnsqueezeBroadReshapeSDPAFusion7) { + std::vector in0_order = {0, 1, 2, 3}; + std::vector in1_order = {0, 1, 2, 3}; + std::vector in2_order = {0, 1, 2, 3}; + std::vector out_order = {0, 1, 2, 3}; + std::vector shape_k_val = {1, 1, 1, 842, 256}; + std::vector shape_v_val = {1, 1, 1, 842, 256}; + std::vector pattern_shape = {0, 8, -1, 256}; + std::vector target_shape_k = {1, 1, 8, 842, 256}; + std::vector target_shape_v = {1, 2, 8, 842, 256}; + const bool is_causal = true; + { + auto input_q = std::make_shared(ov::element::f16, ov::Shape{1, 8, 10, 256}); + auto rope_key_input_1 = std::make_shared(ov::element::f16, ov::Shape{1, 1, 10, 256}); + auto rope_key_input_2 = std::make_shared(ov::element::f16, ov::Shape{1, 1, 832, 256}); + auto cos = std::make_shared(ov::element::f16, ov::Shape{1, 1, 1, 256}); + auto sin = std::make_shared(ov::element::f16, ov::Shape{1, 1, 1, 256}); + auto key_rope_1 = std::make_shared(ov::OutputVector{rope_key_input_1, cos, sin}, ov::op::internal::RoPE::Config()); + auto key_rope_2 = std::make_shared(ov::OutputVector{rope_key_input_2, cos, sin}, ov::op::internal::RoPE::Config()); + auto key_concat = std::make_shared(ov::OutputVector{key_rope_1, key_rope_2}, 2); + auto pre_reshape_value_input_1 = std::make_shared(ov::element::f16, ov::Shape{1, 10, 256}); + auto pre_reshape_value_input_2 = std::make_shared(ov::element::f16, ov::Shape{1, 832, 256}); + auto value_pre_reshape_1 = std::make_shared(pre_reshape_value_input_1, ov::op::v0::Constant::create(ov::element::i32, ov::Shape{4}, {1, 1, 10, 256}), false); + auto value_pre_reshape_2 = std::make_shared(pre_reshape_value_input_2, ov::op::v0::Constant::create(ov::element::i32, ov::Shape{4}, {1, 1, 832, 256}), false); + auto value_concat = std::make_shared(ov::OutputVector{value_pre_reshape_1, value_pre_reshape_2}, 2); + + auto shape_v = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{5}, shape_v_val); + auto shape_k = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{5}, shape_k_val); + auto key_pre_reshape = std::make_shared(key_concat, shape_k, false); + auto broadcast_k_const = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{5}, target_shape_k); + auto key_broadcast = std::make_shared(key_pre_reshape, broadcast_k_const, ov::op::BroadcastType::BIDIRECTIONAL); + + auto value_pre_reshape = std::make_shared(value_concat, shape_v, false); + auto broadcast_v_const = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{5}, target_shape_v); + auto value_broadcast = std::make_shared(value_pre_reshape, broadcast_v_const, ov::op::BroadcastType::BIDIRECTIONAL); + auto pattern = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{4}, pattern_shape); + auto key_reshape = std::make_shared(key_broadcast, pattern, true); + auto value_reshape = std::make_shared(value_broadcast, pattern, true); + auto inputs = ov::OutputVector{input_q, key_reshape, value_reshape}; + auto sdpa = std::make_shared(inputs, is_causal, in0_order, in1_order, in2_order, out_order); + + model = std::make_shared(ov::OutputVector{sdpa}, ov::ParameterVector{input_q, rope_key_input_1, rope_key_input_2, cos, sin, pre_reshape_value_input_1, pre_reshape_value_input_2}); + manager.register_pass(); + } + { + model_ref = model->clone(); + comparator.enable(FunctionsComparator::ATTRIBUTES); + } +} + +TEST_F(TransformationTestsF, UnsqueezeBroadReshapeSDPAFusion8) { + std::vector in0_order = {0, 1, 2, 3}; + std::vector in1_order = {0, 1, 2, 3}; + std::vector in2_order = {0, 1, 2, 3}; + std::vector out_order = {0, 1, 2, 3}; + std::vector shape_5d_val = {1, 1, 1, 968, 256}; + std::vector target_shape_bc = {1, 1, 8, 968, 256}; + std::vector pattern_4d = {0, 8, -1, 256}; + const bool is_causal = true; + + { + auto input_q = std::make_shared(ov::element::f16, ov::Shape{1, 8, 968, 256}); + auto rope_input = std::make_shared(ov::element::f16, ov::Shape{1, 968, 256}); + auto cos = std::make_shared(ov::element::f16, ov::Shape{1, 968, 256}); + auto sin = std::make_shared(ov::element::f16, ov::Shape{1, 968, 256}); + auto input_k_3d = std::make_shared(ov::OutputVector{rope_input, cos, sin}, ov::op::internal::RoPE::Config()); + auto k_shape_5d = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{5}, shape_5d_val); + auto k_5d = std::make_shared(input_k_3d, k_shape_5d, false); + auto bc_const = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{5}, target_shape_bc); + auto k_bc = std::make_shared(k_5d, bc_const, ov::op::BroadcastType::BIDIRECTIONAL); + auto k_shape_4d = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{4}, pattern_4d); + auto k_4d = std::make_shared(k_bc, k_shape_4d, true); + auto v_input = std::make_shared(ov::element::f16, ov::Shape{1, 968, 256}); + auto v_trans_const = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{3}, {0, 1, 2}); + auto input_v_3d = std::make_shared(v_input, v_trans_const); + auto v_shape_5d = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{5}, shape_5d_val); + auto v_5d = std::make_shared(input_v_3d, v_shape_5d, false); + auto v_bc = std::make_shared(v_5d, bc_const, ov::op::BroadcastType::BIDIRECTIONAL); + auto v_shape_4d = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{4}, pattern_4d); + auto v_4d = std::make_shared(v_bc, v_shape_4d, true); + auto inputs = ov::OutputVector{input_q, k_4d, v_4d}; + auto sdpa = std::make_shared(inputs, is_causal, in0_order, in1_order, in2_order, out_order); + + model = std::make_shared(ov::OutputVector{sdpa}, ov::ParameterVector{input_q, rope_input, cos, sin, v_input}); + manager.register_pass(); + } + { + auto input_q = std::make_shared(ov::element::f16, ov::Shape{1, 8, 968, 256}); + auto rope_input = std::make_shared(ov::element::f16, ov::Shape{1, 968, 256}); + auto cos = std::make_shared(ov::element::f16, ov::Shape{1, 968, 256}); + auto sin = std::make_shared(ov::element::f16, ov::Shape{1, 968, 256}); + auto input_k_3d = std::make_shared(ov::OutputVector{rope_input, cos, sin}, ov::op::internal::RoPE::Config()); + auto v_input = std::make_shared(ov::element::f16, ov::Shape{1, 968, 256}); + auto v_trans_const = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{3}, {0, 1, 2}); + auto input_v_3d = std::make_shared(v_input, v_trans_const); + std::vector ref_4d_shape_val = {1, 1, 968, 256}; + auto k_ref_shape_4d = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{4}, ref_4d_shape_val); + auto v_ref_shape_4d = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{4}, ref_4d_shape_val); + auto k_ref_4d = std::make_shared(input_k_3d, k_ref_shape_4d, false); + auto v_ref_4d = std::make_shared(input_v_3d, v_ref_shape_4d, false); + + auto inputs = ov::OutputVector{input_q, k_ref_4d, v_ref_4d}; + auto sdpa = std::make_shared(inputs, is_causal, in0_order, in1_order, in2_order, out_order); + + model_ref = std::make_shared(ov::OutputVector{sdpa}, ov::ParameterVector{input_q, rope_input, cos, sin, v_input}); + comparator.enable(FunctionsComparator::ATTRIBUTES); + } +} + +TEST_F(TransformationTestsF, UnsqueezeBroadReshapeSDPAFusion9) { + std::vector in0_order = {0, 1, 2, 3}; + std::vector in1_order = {0, 1, 2, 3}; + std::vector in2_order = {0, 1, 2, 3}; + std::vector out_order = {0, 1, 2, 3}; + + // GQA 16x Broadcast Logic (32 Q heads, 2 KV heads) + std::vector shape_5d_val = {-1, 1, 2, 1, 128}; + std::vector target_shape_bc = {1, 1, 1, 16, 1}; // Broadcast by 16x + std::vector pattern_4d = {0, 0, 32, 128}; + const bool is_causal = true; + + { + auto input_q = std::make_shared(ov::element::f16, ov::PartialShape{-1, 1, 32, 128}); + auto rope_input = std::make_shared(ov::element::f16, ov::PartialShape{-1, 1, 256}); + auto cos = std::make_shared(ov::element::f16, ov::PartialShape{-1, 1, 256}); + auto sin = std::make_shared(ov::element::f16, ov::PartialShape{-1, 1, 256}); + auto input_k = std::make_shared(ov::OutputVector{rope_input, cos, sin}, ov::op::internal::RoPE::Config()); + auto k_shape_5d = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{5}, shape_5d_val); + auto k_5d = std::make_shared(input_k, k_shape_5d, true); + auto bc_const = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{5}, target_shape_bc); + auto k_bc = std::make_shared(k_5d, bc_const, ov::op::BroadcastType::BIDIRECTIONAL); + auto k_shape_4d = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{4}, pattern_4d); + auto k_4d = std::make_shared(k_bc, k_shape_4d, true); + + auto v_input = std::make_shared(ov::element::f16, ov::PartialShape{-1, 1, 256}); + auto v_trans_const = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{3}, {0, 1, 2}); + auto input_v = std::make_shared(v_input, v_trans_const); + auto v_shape_5d = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{5}, shape_5d_val); + auto v_5d = std::make_shared(input_v, v_shape_5d, true); + auto v_bc = std::make_shared(v_5d, bc_const, ov::op::BroadcastType::BIDIRECTIONAL); + auto v_shape_4d = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{4}, pattern_4d); + auto v_4d = std::make_shared(v_bc, v_shape_4d, true); + + auto sdpa = std::make_shared( + ov::OutputVector{input_q, k_4d, v_4d}, is_causal, in0_order, in1_order, in2_order, out_order); + + model = std::make_shared(ov::OutputVector{sdpa}, ov::ParameterVector{input_q, rope_input, cos, sin, v_input}); + manager.register_pass(); + } + { + auto input_q = std::make_shared(ov::element::f16, ov::PartialShape{-1, 1, 32, 128}); + auto rope_input = std::make_shared(ov::element::f16, ov::PartialShape{-1, 1, 256}); + auto cos = std::make_shared(ov::element::f16, ov::PartialShape{-1, 1, 256}); + auto sin = std::make_shared(ov::element::f16, ov::PartialShape{-1, 1, 256}); + auto input_k = std::make_shared(ov::OutputVector{rope_input, cos, sin}, ov::op::internal::RoPE::Config()); + auto v_input = std::make_shared(ov::element::f16, ov::PartialShape{-1, 1, 256}); + auto v_trans_const = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{3}, {0, 1, 2}); + auto input_v = std::make_shared(v_input, v_trans_const); + std::vector ref_4d_shape_val = {0, 1, 2, 128}; + auto k_ref_shape_4d = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{4}, ref_4d_shape_val); + auto v_ref_shape_4d = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{4}, ref_4d_shape_val); + auto k_ref_4d = std::make_shared(input_k, k_ref_shape_4d, true); + auto v_ref_4d = std::make_shared(input_v, v_ref_shape_4d, true); + + auto sdpa = std::make_shared( + ov::OutputVector{input_q, k_ref_4d, v_ref_4d}, is_causal, in0_order, in1_order, in2_order, out_order); + + model_ref = std::make_shared(ov::OutputVector{sdpa}, ov::ParameterVector{input_q, rope_input, cos, sin, v_input}); + comparator.enable(FunctionsComparator::ATTRIBUTES); + } +} + } // namespace intel_gpu } // namespace test } // namespace ov diff --git a/src/plugins/intel_gpu/thirdparty/CMakeLists.txt b/src/plugins/intel_gpu/thirdparty/CMakeLists.txt index f5c54d4feb65..a614a5d86399 100644 --- a/src/plugins/intel_gpu/thirdparty/CMakeLists.txt +++ b/src/plugins/intel_gpu/thirdparty/CMakeLists.txt @@ -36,6 +36,7 @@ if(ENABLE_ONEDNN_FOR_GPU) if(CMAKE_COMPILER_IS_GNUCXX OR OV_COMPILER_IS_CLANG OR (OV_COMPILER_IS_INTEL_LLVM AND UNIX)) ov_add_compiler_flags(-Wno-undef) ov_add_compiler_flags(-Wno-missing-declarations) + ov_add_compiler_flags(-Wno-ignored-attributes) if(CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL 11 AND CMAKE_COMPILER_IS_GNUCXX) ov_add_compiler_flags(-Wno-array-bounds) ov_add_compiler_flags(-Wno-stringop-overflow) @@ -57,6 +58,7 @@ if(ENABLE_ONEDNN_FOR_GPU) # pass common variables foreach(cmake_var IN ITEMS CMAKE_SYSTEM_NAME CMAKE_SYSTEM_VERSION CMAKE_SYSTEM_PROCESSOR CMAKE_TOOLCHAIN_FILE + CMAKE_SYSROOT CMAKE_VERBOSE_MAKEFILE CMAKE_GENERATOR CMAKE_CXX_COMPILER CMAKE_C_COMPILER CMAKE_CXX_COMPILER_LAUNCHER CMAKE_C_COMPILER_LAUNCHER) @@ -65,6 +67,14 @@ if(ENABLE_ONEDNN_FOR_GPU) endif() endforeach() + # CMAKE_FIND_ROOT_PATH is a CMake list (semicolon-separated); pass via CMAKE_CACHE_ARGS like other lists below. + set(onednn_gpu_cache_args + "-DDNNL_ENABLE_PRIMITIVE:STRING=${ONEDNN_ENABLED_PRIMITIVES}" + "-DDNNL_ENABLE_PRIMITIVE_GPU_ISA:STRING=${ONEDNN_ENABLED_ISA}") + if(CMAKE_FIND_ROOT_PATH) + list(APPEND onednn_gpu_cache_args "-DCMAKE_FIND_ROOT_PATH:STRING=${CMAKE_FIND_ROOT_PATH}") + endif() + # pass compilation flags foreach(lang IN ITEMS C CXX) foreach(build_type IN ITEMS "" "_DEBUG" "_MINSIZEREL" "_RELEASE" "_RELWITHDEBINFO") @@ -95,15 +105,29 @@ if(ENABLE_ONEDNN_FOR_GPU) endif() if(OpenCL_INCLUDE_DIR) list(APPEND cmake_extra_args "-DOpenCL_INCLUDE_DIR=${OpenCL_INCLUDE_DIR}") + elseif(TARGET OpenCL::OpenCL) + # If OpenCL target exists but OpenCL_INCLUDE_DIR is not set, extract from target + get_target_property(opencl_include_dirs OpenCL::OpenCL INTERFACE_INCLUDE_DIRECTORIES) + if(opencl_include_dirs) + list(APPEND cmake_extra_args "-DOpenCL_INCLUDE_DIRS=${opencl_include_dirs}") + endif() endif() set(onednn_gpu_lib "${CMAKE_STATIC_LIBRARY_PREFIX}${DNNL_GPU_LIBRARY_NAME}${CMAKE_STATIC_LIBRARY_SUFFIX}") set(ONEDNN_GPU_LIB_PATH ${ONEDNN_INSTALL_DIR}/lib/${onednn_gpu_lib} CACHE FILEPATH "Path to oneDNN GPU library") set(ONEDNN_GPU_DIR ${CMAKE_CURRENT_SOURCE_DIR}/onednn_gpu CACHE FILEPATH "Path to oneDNN GPU repository") - if(GPU_RUNTIME STREQUAL "L0") + if(GPU_RUNTIME STREQUAL "ZE") set(ONEDNN_GPU_RT "ZE") + # OneDNN is using different version of L0 headers + # Disable LTO to avoid build issues + # Enable once headers are unified MFDNN-14973 + set(ONEDNN_ENABLE_LTO OFF) elseif(GPU_RUNTIME STREQUAL "OCL") set(ONEDNN_GPU_RT "OCL") + set(ONEDNN_ENABLE_LTO ${ENABLE_LTO}) + elseif(GPU_RUNTIME STREQUAL "SYCL") + set(ONEDNN_GPU_RT "SYCL") + set(ONEDNN_ENABLE_LTO ${ENABLE_LTO}) endif() ExternalProject_Add(onednn_gpu_build @@ -115,7 +139,7 @@ if(ENABLE_ONEDNN_FOR_GPU) # Configure Step Options: CMAKE_ARGS ${cmake_extra_args} - "-DCMAKE_INTERPROCEDURAL_OPTIMIZATION_RELEASE=${ENABLE_LTO}" + "-DCMAKE_INTERPROCEDURAL_OPTIMIZATION_RELEASE=${ONEDNN_ENABLE_LTO}" "-DCMAKE_POLICY_DEFAULT_CMP0069=NEW" "-DDNNL_TARGET_ARCH=${ONEDNN_TARGET_ARCH}" "-DDNNL_CPU_RUNTIME=NONE" @@ -134,9 +158,6 @@ if(ENABLE_ONEDNN_FOR_GPU) "-DDNNL_EXPERIMENTAL_PROFILING=ON" "-DONEDNN_BUILD_GRAPH=OFF" "-DDNNL_EXPERIMENTAL_GROUPED_MEMORY=ON" - # specifically for Conan, because it overrides CMAKE_PREFIX_PATH and oneDNN's FindOpenCL.cmake is ignored - # Conan's FindOpenCL.cmake module does not set OpenCL_INCLUDE_DIRS, so we need to set it manually - "-DOpenCL_INCLUDE_DIRS=$" # Conan calls cmake with default value for CMP0091, so we have to bypass it to oneDNN build # because we bypass conan_toolchain.cmake via CMAKE_TOOLCHAIN_FILE "-DCMAKE_POLICY_DEFAULT_CMP0091=NEW" @@ -144,8 +165,7 @@ if(ENABLE_ONEDNN_FOR_GPU) # The arguments below requires list to be passed as argument # which doesn't work properly when passed to CMAKE_ARGS. # Thus we pass it via CMAKE_CACHE_ARGS - "-DDNNL_ENABLE_PRIMITIVE:STRING=${ONEDNN_ENABLED_PRIMITIVES}" - "-DDNNL_ENABLE_PRIMITIVE_GPU_ISA:STRING=${ONEDNN_ENABLED_ISA}" + ${onednn_gpu_cache_args} # Build Step Options: BUILD_BYPRODUCTS ${ONEDNN_GPU_LIB_PATH} # Target Options: diff --git a/src/plugins/intel_gpu/thirdparty/onednn_gpu b/src/plugins/intel_gpu/thirdparty/onednn_gpu index 1b20afd0bb84..6569fb284ea8 160000 --- a/src/plugins/intel_gpu/thirdparty/onednn_gpu +++ b/src/plugins/intel_gpu/thirdparty/onednn_gpu @@ -1 +1 @@ -Subproject commit 1b20afd0bb84e1161b901e4275fd2ab92e8caad5 +Subproject commit 6569fb284ea8e5ec628090a3d1d400485eed84b5 diff --git a/src/plugins/intel_npu/README.md b/src/plugins/intel_npu/README.md index 0fc9ecc90937..c910aad6a0b3 100644 --- a/src/plugins/intel_npu/README.md +++ b/src/plugins/intel_npu/README.md @@ -190,7 +190,7 @@ The following properties are supported (may differ based on current system confi | `ov::supported_properties`/
`SUPPORTED_METRICS`/
`SUPPORTED_CONFIG_KEYS` | RO | Returns a list of all supported properties.
Can be queried on runtime. | `N/A` | `N/A` | | `ov::caching_properties`/
`CACHING_PROPERTIES` | RW | Returns a list of all properties that are used by OpenVINO cache to build the hash key. | `N/A` | `N/A` | | `ov::compilation_num_threads`/
`COMPILATION_NUM_THREADS` | RW | Maximum number of threads that can be used for compilation tasks. | `N/A` | `N/A` | -| `ov::num_streams`/
`NUM_STREAMS` | RO | Not used by the NPU plugin.
Always set to 1. | `AUTO/`
`INT` | `1` | +| `ov::num_streams`/
`NUM_STREAMS` | RW | Sets the per-executor stream/thread limit. Executor layout depends on mode: `AUTO` uses only the task executor; `0` runs `start_async` on the caller thread, uses a single wait executor thread, and disables the callback executor; explicit positive values use separate executors for start, wait, and callback stages, each configured with `num_streams` threads. | `AUTO/`
`INT` | `AUTO` | | `ov::optimal_number_of_infer_requests`/
`OPTIMAL_NUMBER_OF_INFER_REQUESTS` | RO | Returns the optimal number of inference requests to be used by the application. Depends on the platform version and on ov::hint::performance_mode. Please see the table below. | `N/A` | `N/A` | | `ov::range_for_async_infer_requests`/
`RANGE_FOR_ASYNC_INFER_REQUESTS` | RO | Returns a tuple (bottom, top, step).
Not used by the NPU plugin. | `N/A` | `N/A` | | `ov::range_for_streams`/
`RANGE_FOR_STREAMS` | RO | Returns a tuple (bottom, top).
Not used by the NPU plugin. | `N/A`| `N/A` | @@ -199,16 +199,16 @@ The following properties are supported (may differ based on current system confi | `ov::hint::performance_mode`/
`PERFORMANCE_HINT` | RW | Sets the performance profile used to determine default values of Tiles/DMAs/NIREQs.
Default values for each profile are documented below. | `THROUGHPUT`/
`LATENCY`/
`UNDEFINED` | `UNDEFINED` | | `ov::hint::num_requests`/
`PERFORMANCE_HINT_NUM_REQUESTS` | RW | Sets the number of outstanding inference requests. | `[0-]` | `1` | | `ov::hint::model_priority`/
`MODEL_PRIORITY` | RW | Assigns a priority for the model execution. | `LOW`/
`MEDIUM`/
`HIGH` | `MEDIUM` | -| `ov::hint::enable_cpu_pinning`/
`ENABLE_CPU_PINNING` | RW | Allows CPU threads pinning during inference. | `YES`/ `NO` /
`NO` +| `ov::hint::enable_cpu_pinning`/
`ENABLE_CPU_PINNING` | RW | This property is deprecated and has no effect on the NPU Plugin. It will be removed in the OpenVINO 2027.0 release. | `YES`/ `NO` /
`NO` | `ov::log::level`/
`LOG_LEVEL` | RW | Sets the log level for NPU Plugin. An environment variable is also made available to expose logs from early initialization phase: OV_NPU_LOG_LEVEL. | `LOG_NONE`/
`LOG_ERROR`/
`LOG_WARNING`/
`LOG_INFO`/
`LOG_DEBUG`/
`LOG_TRACE` | `LOG_NONE` | | `ov::cache_dir`/
`CACHE_DIR` | RW | Folder path to be used by the OpenVINO cache. | Any string pointing towards a valid directory path | empty | +| `ov::cache_encryption_callbacks`/
`CACHE_ENCRYPTION_CALLBACKS` | WO | Encryption/Decryption functions called when exporting or reading the blob. | ov::EncryptionCallbacks structures populated with any function respecting signature `std::string(const std::string&)` for both encryption and decryption callbacks | ov::EncryptionCallbacks{nullptr, nullptr} | | `ov::cache_mode`/
`CACHE_MODE` | RW | If `CACHE_DIR` has been set, then this option indicates whether or not the size of the compiled model binary object will be reduced by decoupling a portion of the weights. | `OPTIMIZE_SIZE` /
`OPTIMIZE_SPEED` | `OPTIMIZE_SPEED` | | `ov::available_devices`/
`AVAILABLE_DEVICES` | RO | Returns the list of enumerated NPU devices.
NPU plugin does not currently support multiple devices. | `N/A`| `N/A` | | `ov::device::id`/
`DEVICE_ID` | RW | Device identifier. Empty means auto detection. | empty/
`3720`/
`4000` | empty | | `ov::device::uuid`/
| RO | Returns the Universal Unique ID of the NPU device. | `N/A`| `N/A` | | `ov::device::architecture`/
`DEVICE_ARCHITECTURE` | RO | Returns the platform information. | `N/A`| `N/A` | | `ov::device::full_name`/
`FULL_DEVICE_NAME` | RO | Returns the full name of the NPU device. | `N/A`| `N/A` | -| `ov::internal::exclusive_async_requests`/
`EXCLUSIVE_ASYNC_REQUESTS` | RW | Allows to use exclusive task executor for asynchronous infer requests. | `YES`/ `NO`| `NO` | | `ov::device::type`/
`DEVICE_TYPE` | RO | Returns the type of device, discrete or integrated. | `DISCRETE` /
`INTEGRATED` | `N/A` | | `ov::device::gops`/
`DEVICE_GOPS` | RO | Returns the Giga OPS per second count (GFLOPS or GIOPS) for a set of precisions supported by specified device. | `N/A`| `N/A` | | `ov::device::pci_info`/
`DEVICE_PCI_INFO` | RO | Returns the PCI bus information of device. See PCIInfo struct definition for details | `N/A`| `N/A` | diff --git a/src/plugins/intel_npu/cmake/download_compiler_libs.cmake b/src/plugins/intel_npu/cmake/download_compiler_libs.cmake index f07b9075fc89..b2d6ab694b24 100644 --- a/src/plugins/intel_npu/cmake/download_compiler_libs.cmake +++ b/src/plugins/intel_npu/cmake/download_compiler_libs.cmake @@ -4,14 +4,12 @@ # This script resolves the prebuilt NPU Plugin Compiler dependency by downloading and extracting the appropriate # archive based on the current platform. The expected location of the archive and naming convention is as follows: -# vcl version: 8.1.0 -# release: releases/unified/2026/20 # storage location: https://storage.openvinotoolkit.org/dependencies/thirdparty # WINDOWS: -# windows2022: npu_compiler_vcl_windows_2022-8_1_0-727e603.zip +# windows2022: npu_compiler_vcl_windows_2022--.zip # LINUX: -# ubuntu22.04: npu_compiler_vcl_ubuntu_22_04-8_1_0-727e603.tar.gz -# ubuntu24.04: npu_compiler_vcl_ubuntu_24_04-8_1_0-727e603.tar.gz +# ubuntu22.04: npu_compiler_vcl_ubuntu_22_04--.tar.gz +# ubuntu24.04: npu_compiler_vcl_ubuntu_24_04--.tar.gz # # This script replicates cmake/dependencies.cmake common OV dependency resolution logic including: # THIRDPARTY_SERVER_PATH environment variable or cmake options support that allows @@ -49,12 +47,12 @@ if(ENABLE_INTEL_NPU_COMPILER) message(STATUS "Resolving prebuilt NPU Plugin Compiler dependencies...") set(PLUGIN_COMPILER_VERSION_MAJOR 8) - set(PLUGIN_COMPILER_VERSION_MINOR 1) + set(PLUGIN_COMPILER_VERSION_MINOR 2) set(PLUGIN_COMPILER_VERSION_PATCH 0) - set(PLUGIN_COMPILER_COMMIT_SHA 727e603) - set(PLUGIN_COMPILER_WINDOWS_2022_CHECKSUM f7cb6501df6e38fc90e6591470069d3a705749113b39438e92391271bc6835c1) - set(PLUGIN_COMPILER_UBUNTU_22_04_CHECKSUM d50bf866a37a2d599709fb6a805de86fd9b052a67b588d2a83558a224aaf7e95) - set(PLUGIN_COMPILER_UBUNTU_24_04_CHECKSUM 3c9c5960c4a86577652cd9a96cb4630576ec398f126fd8b37979063b63929448) + set(PLUGIN_COMPILER_COMMIT_SHA 4fd12bb) + set(PLUGIN_COMPILER_WINDOWS_2022_CHECKSUM e43aa2fdd9b51d08901ca14f32048a9b3d6e26aeb6ddd68059966dda599196ee) + set(PLUGIN_COMPILER_UBUNTU_22_04_CHECKSUM ea8cbeb32d56962ccea808fa177f95fafb39ec83acbb6f19c545ed7f007b4bdb) + set(PLUGIN_COMPILER_UBUNTU_24_04_CHECKSUM ec943662b847ec7659fab52eb2c4e32c105a5fd59e1d4bccfe738aca57a92825) set(PLUGIN_COMPILER_VERSION_UNDERSCORE "${PLUGIN_COMPILER_VERSION_MAJOR}_${PLUGIN_COMPILER_VERSION_MINOR}_${PLUGIN_COMPILER_VERSION_PATCH}") message(STATUS "The prebuilt compiler version is ${PLUGIN_COMPILER_VERSION_MAJOR}.${PLUGIN_COMPILER_VERSION_MINOR}.${PLUGIN_COMPILER_VERSION_PATCH}.${PLUGIN_COMPILER_COMMIT_SHA}") @@ -68,8 +66,10 @@ if(ENABLE_INTEL_NPU_COMPILER) set(PLUGIN_COMPILER_PACKAGE_EXT "zip") set(PLUGIN_COMPILER_ARCHIVE_TYPE "ARCHIVE_WIN") set(PLUGIN_COMPILER_LIB_NAME "openvino_intel_npu_compiler.dll") + set(PLUGIN_COMPILER_PDB_NAME "openvino_intel_npu_compiler.pdb") set(PLUGIN_COMPILER_LOADER_LIB_NAME "openvino_intel_npu_compiler_loader.dll") - elseif(UNIX AND NOT APPLE) + set(PLUGIN_COMPILER_LOADER_PDB_NAME "openvino_intel_npu_compiler_loader.pdb") + elseif(UNIX AND NOT APPLE AND NOT ANDROID) # Get the OS name and OS version execute_process(COMMAND lsb_release -is OUTPUT_VARIABLE OS_NAME OUTPUT_STRIP_TRAILING_WHITESPACE) execute_process(COMMAND lsb_release -rs OUTPUT_VARIABLE OS_VERSION OUTPUT_STRIP_TRAILING_WHITESPACE) @@ -93,18 +93,15 @@ if(ENABLE_INTEL_NPU_COMPILER) return() endif() - set(PLUGIN_COMPILER_PACKAGE_SUBDIR "") set(PLUGIN_COMPILER_PACKAGE_NAME "${PLUGIN_COMPILER_PACKAGE_PREFIX}-${PLUGIN_COMPILER_VERSION_UNDERSCORE}-${PLUGIN_COMPILER_COMMIT_SHA}.${PLUGIN_COMPILER_PACKAGE_EXT}") if(DEFINED ENV{THIRDPARTY_SERVER_PATH}) set(IE_PATH_TO_DEPS "$ENV{THIRDPARTY_SERVER_PATH}") - set(PLUGIN_COMPILER_PACKAGE_SUBDIR "npu_compiler/") elseif(DEFINED THIRDPARTY_SERVER_PATH) set(IE_PATH_TO_DEPS "${THIRDPARTY_SERVER_PATH}") - set(PLUGIN_COMPILER_PACKAGE_SUBDIR "npu_compiler/") endif() RESOLVE_DEPENDENCY(NPU_PLUGIN_COMPILER - ${PLUGIN_COMPILER_ARCHIVE_TYPE} "${PLUGIN_COMPILER_PACKAGE_SUBDIR}${PLUGIN_COMPILER_PACKAGE_NAME}" + ${PLUGIN_COMPILER_ARCHIVE_TYPE} "npu_compiler/${PLUGIN_COMPILER_PACKAGE_NAME}" TARGET_PATH "${TEMP}/${PLATFORM_SUBDIR}/npu_compiler_${PLUGIN_COMPILER_VERSION_UNDERSCORE}_${PLUGIN_COMPILER_COMMIT_SHA}" ENVIRONMENT "NPU_PLUGIN_COMPILER_ROOT" FOLDER @@ -116,6 +113,7 @@ if(ENABLE_INTEL_NPU_COMPILER) print_build_manifest("${NPU_PLUGIN_COMPILER}/build_manifest.json") set(PLUGIN_COMPILER_LIB_PATH "${NPU_PLUGIN_COMPILER}/lib") + set(PLUGIN_COMPILER_PDB_PATH "${NPU_PLUGIN_COMPILER}/pdb") if(USE_BUILD_TYPE_SUBFOLDER) set(PLUGIN_COMPILER_LIB_DESTINATION ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}) @@ -132,6 +130,17 @@ if(ENABLE_INTEL_NPU_COMPILER) install(FILES ${PLUGIN_COMPILER_LIB} DESTINATION ${OV_CPACK_PLUGINSDIR} COMPONENT ${NPU_PLUGIN_COMPONENT}) install(FILES ${PLUGIN_COMPILER_LOADER_LIB} DESTINATION ${OV_CPACK_PLUGINSDIR} COMPONENT ${NPU_PLUGIN_COMPONENT}) + + if(WIN32) + set(PLUGIN_COMPILER_PDB "${PLUGIN_COMPILER_PDB_PATH}/${PLUGIN_COMPILER_PDB_NAME}") + set(PLUGIN_COMPILER_LOADER_PDB "${PLUGIN_COMPILER_PDB_PATH}/${PLUGIN_COMPILER_LOADER_PDB_NAME}") + file(COPY "${PLUGIN_COMPILER_PDB}" DESTINATION "${PLUGIN_COMPILER_LIB_DESTINATION}") + file(COPY "${PLUGIN_COMPILER_LOADER_PDB}" DESTINATION "${PLUGIN_COMPILER_LIB_DESTINATION}") + message(STATUS "Copying prebuilt Plugin compiler PDB files from ${PLUGIN_COMPILER_PDB_PATH} to ${PLUGIN_COMPILER_LIB_DESTINATION}") + + install(FILES ${PLUGIN_COMPILER_PDB} DESTINATION ${OV_CPACK_PLUGINSDIR} COMPONENT pdb EXCLUDE_FROM_ALL) + install(FILES ${PLUGIN_COMPILER_LOADER_PDB} DESTINATION ${OV_CPACK_PLUGINSDIR} COMPONENT pdb EXCLUDE_FROM_ALL) + endif() else() message(FATAL_ERROR "Failed to download prebuilt NPU Plugin Compiler libraries. Can not use plugin compiler libraries!") endif() diff --git a/src/plugins/intel_npu/src/al/include/intel_npu/compat_string_parser.hpp b/src/plugins/intel_npu/src/al/include/intel_npu/compat_string_parser.hpp new file mode 100644 index 000000000000..46d54790c074 --- /dev/null +++ b/src/plugins/intel_npu/src/al/include/intel_npu/compat_string_parser.hpp @@ -0,0 +1,371 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +/* + This file implements a parser for compatibility strings used in the NPU + software stack. The compatibility string encodes requirements of a compiled + blob and metadata such as a target platform, minimum runtime and compiler + version. The string consists of a human-readable list of name=value pairs + called attributes. Basic encapsulation across SW layers is supported via + nested lists. Optional attributes are supported with brace-enclosed + attribute list. + + The grammar for the compatibility string is defined as follows: + + string ::= expr (';' expr)* + expr ::= attr | '{' string '}' + attr ::= name '=' value | name '=' list + list ::= '[' string ('|' string)* ']' + name ::= [a-z][_a-z0-9]* + value ::= [A-Z0-9][_A-Z0-9\.]* + + No whitespaces are allowed. + Attribute names are lower case. Attribute values are upper case. + + + Example usage: + + std::string_view compatibilityString = "name=A;nested=[key=X;ver=1.2|key=Y;ver=1.3];{optional=V1.4}"; + std::array legalAttributes = {"name", "nested"}; + Parser parser(compatibilityString, legalAttributes); + parser.getAttribute("name"); // returns "A" + auto nested = parser.getAttribute("nested"); // returns "[key=X;ver=1.2|key=Y;ver=1.3]" + Parser::splitList(nested); // returns ["key=X;ver=1.2", "key=Y;ver=1.3"] + // to be parsed separately + parser.getOptionalAttributes(); // returns { "optional": "V1.4"} as map + + + Compatibility string guidelines: + + The compatibility string represents the blob's runtime requirements in a + concise form: the target hardware and the minimum version of required + SW/FW runtime components. The compatibility string MUST be sufficient to + determine whether the blob will run correctly in the current environment without + accessing the blob content. + The string is human-readable but is not intended to be interpreted by the + end user nor edited. The string consists of attributes which primarily + represent HW target requirements, runtime component versions and may include + feature flags. + + The string should include the version of the components contributing to the + blob's generation (such as compiler). While not strictly required to + evaluate the runtime requirements a component version may be used to reject + known bad blobs in future releases or for debugging purposes. + Each component SHOULD serialize its own version as an attribute in the + compatibility string. The component encapsulating a blob in a binary + container MUST serialize the inner blob compatibility string in one of its + attributes as a list (e.g. `inner=[string]`). The component MUST NOT repeat + the information contained in the inner blob attributes. + + Attribute names should be short and may use abbreviations. For clarity + attribute names SHOULD be unique across components, e.g. `version` is not a + good attribute name. As attributes normally represent versions, the `_ver` + suffix is not recommended either. For consistency, versions SHOULD be + encoded as dot-separated integers, e.g. `compiler=8.1`. + In case of boolean flags the attribute should be set to `1` for true. False + values SHOULD NOT be serialized. The absence of a flag MUST be treated as + feature not being required. + + A component MUST reject the string if an attribute name is not recognized, + so a new requirement is automatically rejected by prior SW releases that did + not support given feature. + An unknown optional attribute (enclosed in braces) MUST be ignored. This allows + for augmenting metadata without breaking compatibility with earlier SW + versions. + + Version history: + 2026-05-06 Initial version. + +*/ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace intel_npu::compat { + +class Parser { +public: + using attr_map_type = std::map>; + + Parser() = default; + + template + Parser(std::string_view input, const Iterable& legalAttributeNames); + + template + void parse(std::string_view input, const Iterable& legalAttributeNames); + + const std::string& getAttribute(const std::string& name) const; + const attr_map_type& getAttributes() const { + return _attributes; + } + const attr_map_type& getOptionalAttributes() const { + return _optional_attributes; + } + + template + void validateAttributes(const Iterable& legalAttributeNames) const; + + // Breaks down a list of nested compatibility strings + static std::vector splitList(std::string_view list); + +private: + enum class CaptureMode { ATTR, STRING }; + + char peek() const; + char nextc(); + void expect(char c); + void reset(std::string_view input, CaptureMode mode = CaptureMode::ATTR); + void startCapture(CaptureMode mode); + std::string_view stopCapture(CaptureMode mode); + std::string_view parseString(); + void parseExpr(); + void parseAttr(); + std::vector parseList(); + template + std::string_view readChars(F condition); + std::string_view parseName(); + std::string_view parseValue(); + std::runtime_error errorAt(std::string_view message) const; + void setAttribute(std::string_view name, std::string_view value); + + // Safe ctype helpers to avoid undefined behavior on signed char + // clang-format off + static bool isLower(char c) { return std::islower(static_cast(c)); } + static bool isUpper(char c) { return std::isupper(static_cast(c)); } + static bool isDigit(char c) { return std::isdigit(static_cast(c)); } + static bool isGraph(char c) { return std::isgraph(static_cast(c)); } + // clang-format on + + std::string_view::iterator _current; + std::string_view::iterator _end; + + CaptureMode _captureMode = CaptureMode::ATTR; + std::string_view::iterator _captureStart; + std::string_view::iterator _captureEnd; + int _nesting = 0; // Nesting level for current capture mode + int _optional = 0; // Tracks top-level optional attributes + + attr_map_type _attributes; + attr_map_type _optional_attributes; +}; + +template +Parser::Parser(std::string_view input, const Iterable& legalAttributeNames) { + parse(input, legalAttributeNames); +} + +template +void Parser::parse(std::string_view input, const Iterable& legalAttributeNames) { + reset(input); + _attributes.clear(); + _optional_attributes.clear(); + parseString(); + if (peek() != 0) { + throw errorAt("at the end of the string"); + } + validateAttributes(legalAttributeNames); +} + +inline const std::string& Parser::getAttribute(const std::string& name) const { + auto it = _attributes.find(name); + if (it != _attributes.end()) { + return it->second; + } + throw std::runtime_error("Attribute not found: " + name); +} + +template +void Parser::validateAttributes(const Iterable& legalAttributeNames) const { + if (legalAttributeNames.size() == 0) { + return; // No validation if no legal attribute names provided + } + for (const auto& entry : _attributes) { + auto& name = entry.first; + if (std::find(legalAttributeNames.begin(), legalAttributeNames.end(), name) == legalAttributeNames.end()) { + throw std::runtime_error("Illegal attribute: " + name); + } + } +} + +inline std::vector Parser::splitList(std::string_view list) { + Parser parser; + parser.reset(list, CaptureMode::STRING); + auto items = parser.parseList(); + if (parser.peek() != 0) { + throw parser.errorAt("at the end of the list"); + } + return std::vector(items.begin(), items.end()); +} + +inline char Parser::peek() const { + return _current != _end ? *_current : 0; +} + +inline char Parser::nextc() { + assert(_current != _end); + return *_current++; +} + +inline void Parser::expect(char c) { + if (peek() != c) { + throw errorAt(std::string("expected '") + c + "'"); + } + nextc(); +} + +inline void Parser::reset(std::string_view input, CaptureMode mode) { + _current = input.begin(); + _end = input.end(); + _optional = 0; + _nesting = 0; + _captureMode = mode; + _captureStart = _end; + _captureEnd = _end; +} + +inline void Parser::startCapture(CaptureMode mode) { + if (_captureMode == mode) { + if (_nesting++ == 0) { + _captureStart = _current; + } + } +} + +inline std::string_view Parser::stopCapture(CaptureMode mode) { + if (_captureMode == mode) { + if (--_nesting == 0) { + _captureEnd = _current; + assert(_captureEnd != _captureStart); + // return std::string_view(_captureStart, _captureEnd); // C++20 + return std::string_view(&*_captureStart, _captureEnd - _captureStart); + } + } + return {}; +} + +// string ::= expr (';' expr)* +inline std::string_view Parser::parseString() { + startCapture(CaptureMode::STRING); + parseExpr(); + while (peek() == ';') { + nextc(); // ';' + parseExpr(); + } + return stopCapture(CaptureMode::STRING); +} + +// expr ::= attr | '{' string '}' +inline void Parser::parseExpr() { + if (peek() == '{') { + ++_optional; + nextc(); // '{' + parseString(); + expect('}'); + --_optional; + } else { + parseAttr(); + } +} + +// attr ::= name '=' value | name '=' list +inline void Parser::parseAttr() { + auto name = parseName(); + expect('='); + if (peek() == '[') { + startCapture(CaptureMode::ATTR); + parseList(); + auto list = stopCapture(CaptureMode::ATTR); + if (!list.empty()) { + setAttribute(name, list); + } + return; + } + auto value = parseValue(); + if (_captureMode == CaptureMode::ATTR && _nesting == 0) { + setAttribute(name, value); + } +} + +// list ::= '[' string ('|' string)* ']' +inline std::vector Parser::parseList() { + std::vector items; + expect('['); + items.push_back(parseString()); + while (peek() == '|') { + nextc(); // '|' + items.push_back(parseString()); + } + expect(']'); + return items; +} + +template +std::string_view Parser::readChars(F condition) { + auto start = _current; + while (peek() != 0 && condition(peek())) { + nextc(); + } + // return std::string_view(start, _current); // C++20 + return std::string_view(&*start, _current - start); +} + +// name ::= [a-z][_a-z0-9]* +inline std::string_view Parser::parseName() { + if (!isLower(peek())) { + throw errorAt("expected attribute name"); + } + return readChars([](char c) { + return isLower(c) || isDigit(c) || c == '_'; + }); +} + +// value ::= [A-Z0-9][_A-Z0-9\.]* +inline std::string_view Parser::parseValue() { + if (!isUpper(peek()) && !isDigit(peek())) { + throw errorAt("expected attribute value"); + } + return readChars([](char c) { + return isUpper(c) || isDigit(c) || c == '.' || c == '_'; + }); +} + +inline std::runtime_error Parser::errorAt(std::string_view message) const { + if (peek() == 0) { + return std::runtime_error(std::string(message) + " at the end of the string"); + } else { + std::string msg = "Unexpected character "; + char c = peek(); + if (isGraph(c)) { // Printable non-space character + msg += "'"; + msg += c; + msg += "': "; + } + msg += message; + return std::runtime_error(std::move(msg)); + } +} + +inline void Parser::setAttribute(std::string_view name, std::string_view value) { + if (_attributes.count(name) != 0 || _optional_attributes.count(name) != 0) { + throw std::runtime_error("Duplicate attribute: " + std::string(name)); + } + if (_optional > 0) { + _optional_attributes.emplace(name, value); + } else { + _attributes.emplace(name, value); + } +} + +} // namespace intel_npu::compat diff --git a/src/plugins/intel_npu/src/al/include/intel_npu/config/config.hpp b/src/plugins/intel_npu/src/al/include/intel_npu/config/config.hpp index 8204217ed27e..a85005c909da 100644 --- a/src/plugins/intel_npu/src/al/include/intel_npu/config/config.hpp +++ b/src/plugins/intel_npu/src/al/include/intel_npu/config/config.hpp @@ -289,7 +289,7 @@ struct OptionBase { return OptionMode::Both; } - // Overload this for private options. + // Overload this for public options. static bool isPublic() { return false; } @@ -381,7 +381,8 @@ struct OptionConcept final { bool (*isValueSupportedImpl)(std::string_view val) = nullptr; // better make this private, but won't be able to use aggregate initialization anymore in // "makeOptionModel" - std::shared_ptr (*validateAndParse)(std::string_view val) = nullptr; + std::shared_ptr (*validateAndParseFromString)(std::string_view val) = nullptr; + std::shared_ptr (*validateAndParseFromAny)(const ov::Any& val) = nullptr; std::optional> customValueCheckerOpt = std::nullopt; bool isValueSupported(std::string_view val) { if (customValueCheckerOpt.has_value()) { @@ -392,7 +393,7 @@ struct OptionConcept final { }; template -std::shared_ptr validateAndParse(std::string_view val) { +std::shared_ptr validateAndParseFromString(std::string_view val) { using ValueType = typename Opt::ValueType; try { @@ -404,6 +405,19 @@ std::shared_ptr validateAndParse(std::string_view val) { } } +template +std::shared_ptr validateAndParseFromAny(const ov::Any& val) { + using ValueType = typename Opt::ValueType; + + try { + auto parsedVal = val.as(); + Opt::validateValue(parsedVal); + return std::make_shared>(std::move(parsedVal), &Opt::toString); + } catch (const std::exception& e) { + OPENVINO_THROW("Failed to parse '", Opt::key().data(), "' option : ", e.what()); + } +} + template OptionConcept makeOptionModel( std::optional> customValueCheckerOpt = std::nullopt) { @@ -414,7 +428,8 @@ OptionConcept makeOptionModel( &Opt::mutability, &Opt::compilerSupportVersion, &Opt::isValueSupported, - &validateAndParse, + &validateAndParseFromString, + &validateAndParseFromAny, std::move(customValueCheckerOpt)}; } @@ -473,6 +488,8 @@ class Config { virtual void update(const ConfigMap& options); + virtual void updateAny(const ov::AnyMap& options); + void parseEnvVars(); template @@ -528,7 +545,7 @@ typename Opt::ValueType Config::get() const { OPENVINO_ASSERT(it->second != nullptr, "Got NULL OptionValue for :", Opt::key().data()); const auto optVal = std::dynamic_pointer_cast>(it->second); -#if defined(__CHROMIUMOS__) +#if defined(__CHROMIUMOS__) || defined(__ANDROID__) if (optVal == nullptr) { if (Opt::getTypeName() == it->second->getTypeName()) { const auto val = std::static_pointer_cast>(it->second); diff --git a/src/plugins/intel_npu/src/al/include/intel_npu/config/npuw.hpp b/src/plugins/intel_npu/src/al/include/intel_npu/config/npuw.hpp index 52a409686b10..c74626f3f506 100644 --- a/src/plugins/intel_npu/src/al/include/intel_npu/config/npuw.hpp +++ b/src/plugins/intel_npu/src/al/include/intel_npu/config/npuw.hpp @@ -194,6 +194,10 @@ struct NPUWStringEnumOptionTraits<::intel_npu::npuw::llm::GenerateHint> { } }; +struct PrefillHintOptionTraits : NPUWStringEnumOptionTraits<::intel_npu::npuw::llm::PrefillHint> {}; + +struct GenerateHintOptionTraits : NPUWStringEnumOptionTraits<::intel_npu::npuw::llm::GenerateHint> {}; + template <> struct NPUWStringEnumOptionTraits<::intel_npu::npuw::llm::AttentionHint> { using ValueType = ::intel_npu::npuw::llm::AttentionHint; @@ -235,6 +239,18 @@ struct NPUWStringEnumOptionTraits<::intel_npu::npuw::llm::AttentionHint> { } }; +struct PrefillAttentionHintOptionTraits : NPUWStringEnumOptionTraits<::intel_npu::npuw::llm::AttentionHint> { + static ValueType defaultValue() { + return ValueType::PYRAMID; + } +}; + +struct GenerateAttentionHintOptionTraits : NPUWStringEnumOptionTraits<::intel_npu::npuw::llm::AttentionHint> { + static ValueType defaultValue() { + return ValueType::STATIC; + } +}; + template <> struct NPUWStringEnumOptionTraits<::intel_npu::npuw::llm::MoEHint> { using ValueType = ::intel_npu::npuw::llm::MoEHint; @@ -272,6 +288,8 @@ struct NPUWStringEnumOptionTraits<::intel_npu::npuw::llm::MoEHint> { } }; +struct MoEHintOptionTraits : NPUWStringEnumOptionTraits<::intel_npu::npuw::llm::MoEHint> {}; + template struct NPUWStringEnumOptionBase : OptionBase { using ValueType = typename Traits::ValueType; @@ -311,7 +329,7 @@ struct NPUWStringEnumOptionBase : OptionBase, KEY) + DEFINE_NPUW_STRING_ENUM_OPT(OPT, TRAITS, KEY) #define INTEL_NPU_NPUW_ANYMAP_OPT(OPT, NS, VARNAME, KEY, GROUP, SURFACE, CACHING, BUILD) \ DEFINE_NPUW_ANYMAP_OPT(OPT, KEY) #include "intel_npu/config/npuw_option_defs.inc" diff --git a/src/plugins/intel_npu/src/al/include/intel_npu/config/npuw_option_defs.inc b/src/plugins/intel_npu/src/al/include/intel_npu/config/npuw_option_defs.inc index da573e30e991..c45743dcb6c6 100644 --- a/src/plugins/intel_npu/src/al/include/intel_npu/config/npuw_option_defs.inc +++ b/src/plugins/intel_npu/src/al/include/intel_npu/config/npuw_option_defs.inc @@ -11,6 +11,8 @@ INTEL_NPU_NPUW_SIMPLE_OPT(NPUW_ONLINE_KEEP_BLOCK_SIZE, std::size_t, 10, ov::inte INTEL_NPU_NPUW_SIMPLE_OPT(NPUW_ONLINE_DUMP_PLAN, std::string, "", ov::intel_npu::npuw::partitioning::online, dump_plan, "NPUW_ONLINE_DUMP_PLAN", ROOT, HIDDEN, UNCACHED, ALL) INTEL_NPU_NPUW_SIMPLE_OPT(NPUW_PLAN, std::string, "", ov::intel_npu::npuw::partitioning, plan, "NPUW_PLAN", ROOT, HIDDEN, UNCACHED, ALL) INTEL_NPU_NPUW_SIMPLE_OPT(NPUW_FOLD, bool, false, ov::intel_npu::npuw::partitioning, fold, "NPUW_FOLD", ROOT, EXPOSED, CACHED, ALL) +INTEL_NPU_NPUW_SIMPLE_OPT(NPUW_FOLD_ONLY, std::string, "", ov::intel_npu::npuw::partitioning, fold_only, "NPUW_FOLD_ONLY", ROOT, EXPOSED, CACHED, ALL) +INTEL_NPU_NPUW_SIMPLE_OPT(NPUW_FUSE_UNFOLDED, bool, false, ov::intel_npu::npuw::partitioning, fuse_unfolded, "NPUW_FUSE_UNFOLDED", ROOT, EXPOSED, CACHED, ALL) INTEL_NPU_NPUW_SIMPLE_OPT(NPUW_CWAI, bool, false, ov::intel_npu::npuw::partitioning, cwai, "NPUW_CWAI", ROOT, EXPOSED, CACHED, ALL) INTEL_NPU_NPUW_SIMPLE_OPT(NPUW_DQ, bool, false, ov::intel_npu::npuw::partitioning, dyn_quant, "NPUW_DQ", ROOT, EXPOSED, CACHED, ALL) INTEL_NPU_NPUW_SIMPLE_OPT(NPUW_DQ_FULL, bool, true, ov::intel_npu::npuw::partitioning, dyn_quant_full, "NPUW_DQ_FULL", ROOT, EXPOSED, CACHED, ALL) @@ -32,6 +34,7 @@ INTEL_NPU_NPUW_SIMPLE_OPT(NPUW_DCOFF_TYPE, std::string, "", ov::intel_npu::npuw: INTEL_NPU_NPUW_SIMPLE_OPT(NPUW_DCOFF_SCALE, bool, false, ov::intel_npu::npuw::partitioning, dcoff_with_scale, "NPUW_DCOFF_SCALE", ROOT, EXPOSED, CACHED, ALL) INTEL_NPU_NPUW_SIMPLE_OPT(NPUW_FUNCALL_FOR_ALL, bool, false, ov::intel_npu::npuw::partitioning, funcall_for_all, "NPUW_FUNCALL_FOR_ALL", ROOT, EXPOSED, CACHED, ALL) INTEL_NPU_NPUW_SIMPLE_OPT(NPUW_PARALLEL_COMPILE, bool, false, ov::intel_npu::npuw, parallel_compilation, "NPUW_PARALLEL_COMPILE", ROOT, HIDDEN, UNCACHED, ALL) +INTEL_NPU_NPUW_SIMPLE_OPT(NPUW_ENSURE_COMPATIBILITY, bool, false, ov::intel_npu::npuw, ensure_compatibility, "NPUW_ENSURE_COMPATIBILITY", ROOT, EXPOSED, CACHED, ALL) INTEL_NPU_NPUW_SIMPLE_OPT(NPUW_WEIGHTS_BANK, std::string, "", ov::intel_npu::npuw, weights_bank, "NPUW_WEIGHTS_BANK", ROOT, EXPOSED, CACHED, ALL) INTEL_NPU_NPUW_SIMPLE_OPT(NPUW_WEIGHTS_BANK_ALLOC, std::string, "", ov::intel_npu::npuw, weights_bank_alloc, "NPUW_WEIGHTS_BANK_ALLOC", ROOT, EXPOSED, CACHED, ALL) INTEL_NPU_NPUW_SIMPLE_OPT(NPUW_CACHE_DIR, std::string, "", ov::intel_npu::npuw, cache_dir, "NPUW_CACHE_DIR", ROOT, HIDDEN, UNCACHED, ALL) @@ -67,8 +70,8 @@ INTEL_NPU_NPUW_STRING_ENUM_OPT(NPUW_LLM_PREFILL_MOE_HINT, ::intel_npu::npuw::llm INTEL_NPU_NPUW_STRING_ENUM_OPT(NPUW_LLM_GENERATE_MOE_HINT, ::intel_npu::npuw::llm::MoEHint, MoEHintOptionTraits, ov::intel_npu::npuw::llm, generate_moe_hint, "NPUW_LLM_GENERATE_MOE_HINT", LLM, EXPOSED, CACHED, ALL) INTEL_NPU_NPUW_STRING_ENUM_OPT(NPUW_LLM_PREFILL_HINT, ::intel_npu::npuw::llm::PrefillHint, PrefillHintOptionTraits, ov::intel_npu::npuw::llm, prefill_hint, "NPUW_LLM_PREFILL_HINT", LLM, EXPOSED, CACHED, ALL) INTEL_NPU_NPUW_STRING_ENUM_OPT(NPUW_LLM_GENERATE_HINT, ::intel_npu::npuw::llm::GenerateHint, GenerateHintOptionTraits, ov::intel_npu::npuw::llm, generate_hint, "NPUW_LLM_GENERATE_HINT", LLM, EXPOSED, CACHED, ALL) -INTEL_NPU_NPUW_STRING_ENUM_OPT(NPUW_LLM_PREFILL_ATTENTION_HINT, ::intel_npu::npuw::llm::AttentionHint, AttentionHintOptionTraits, ov::intel_npu::npuw::llm, prefill_attn_hint, "NPUW_LLM_PREFILL_ATTENTION_HINT", LLM, EXPOSED, CACHED, ALL) -INTEL_NPU_NPUW_STRING_ENUM_OPT(NPUW_LLM_GENERATE_ATTENTION_HINT, ::intel_npu::npuw::llm::AttentionHint, AttentionHintOptionTraits, ov::intel_npu::npuw::llm, generate_attn_hint, "NPUW_LLM_GENERATE_ATTENTION_HINT", LLM, EXPOSED, CACHED, ALL) +INTEL_NPU_NPUW_STRING_ENUM_OPT(NPUW_LLM_PREFILL_ATTENTION_HINT, ::intel_npu::npuw::llm::AttentionHint, PrefillAttentionHintOptionTraits, ov::intel_npu::npuw::llm, prefill_attn_hint, "NPUW_LLM_PREFILL_ATTENTION_HINT", LLM, EXPOSED, CACHED, ALL) +INTEL_NPU_NPUW_STRING_ENUM_OPT(NPUW_LLM_GENERATE_ATTENTION_HINT, ::intel_npu::npuw::llm::AttentionHint, GenerateAttentionHintOptionTraits, ov::intel_npu::npuw::llm, generate_attn_hint, "NPUW_LLM_GENERATE_ATTENTION_HINT", LLM, EXPOSED, CACHED, ALL) INTEL_NPU_NPUW_ANYMAP_OPT(NPUW_LLM_PREFILL_CONFIG, ov::intel_npu::npuw::llm, prefill_config, "NPUW_LLM_PREFILL_CONFIG", LLM, EXPOSED, CACHED, ALL) INTEL_NPU_NPUW_ANYMAP_OPT(NPUW_LLM_ADDITIONAL_PREFILL_CONFIG, ov::intel_npu::npuw::llm, additional_prefill_config, "++NPUW_LLM_PREFILL_CONFIG", LLM, EXPOSED, CACHED, ALL) INTEL_NPU_NPUW_ANYMAP_OPT(NPUW_LLM_GENERATE_CONFIG, ov::intel_npu::npuw::llm, generate_config, "NPUW_LLM_GENERATE_CONFIG", LLM, EXPOSED, CACHED, ALL) @@ -77,6 +80,7 @@ INTEL_NPU_NPUW_ANYMAP_OPT(NPUW_LLM_SHARED_LM_HEAD_CONFIG, ov::intel_npu::npuw::l INTEL_NPU_NPUW_ANYMAP_OPT(NPUW_LLM_ADDITIONAL_SHARED_LM_HEAD_CONFIG, ov::intel_npu::npuw::llm, additional_shared_lm_head_config, "++NPUW_LLM_SHARED_HEAD_CONFIG", LLM, EXPOSED, CACHED, ALL) INTEL_NPU_NPUW_SIMPLE_OPT(NPUW_WHISPER, bool, false, ov::intel_npu::npuw::whisper, enabled, "NPUW_WHISPER", LLM, EXPOSED, CACHED, ALL) INTEL_NPU_NPUW_SIMPLE_OPT(NPUW_WHISPER_EOS_TOKEN, uint64_t, 50257, ov::intel_npu::npuw::whisper, whisper_eos_token, "NPUW_WHISPER_EOS_TOKEN", LLM, EXPOSED, UNCACHED, ALL) +INTEL_NPU_NPUW_SIMPLE_OPT(NPUW_WHISPER_DECOMPOSE_SDPA, bool, false, ov::intel_npu::npuw::whisper, whisper_decompose_sdpa, "NPUW_WHISPER_DECOMPOSE_SDPA", LLM, EXPOSED, CACHED, ALL) INTEL_NPU_NPUW_SIMPLE_OPT(NPUW_EAGLE, bool, false, ov::intel_npu::npuw::eagle, enabled, "NPUW_EAGLE", LLM, EXPOSED, CACHED, ALL) INTEL_NPU_NPUW_SIMPLE_OPT(NPUW_TEXT_EMBED, bool, false, ov::intel_npu::npuw::text_embed, enabled, "NPUW_TEXT_EMBED", LLM, EXPOSED, CACHED, ALL) INTEL_NPU_NPUW_SIMPLE_OPT(NPUW_KOKORO, bool, false, ov::intel_npu::npuw::kokoro, enabled, "NPUW_KOKORO", KOKORO, EXPOSED, UNCACHED, ALL) diff --git a/src/plugins/intel_npu/src/al/include/intel_npu/config/options.hpp b/src/plugins/intel_npu/src/al/include/intel_npu/config/options.hpp index 7dc2aaf72ba4..191ec179fe2c 100644 --- a/src/plugins/intel_npu/src/al/include/intel_npu/config/options.hpp +++ b/src/plugins/intel_npu/src/al/include/intel_npu/config/options.hpp @@ -467,32 +467,6 @@ struct BATCH_MODE final : OptionBase { } }; -struct EXCLUSIVE_ASYNC_REQUESTS final : OptionBase { - static std::string_view key() { - return ov::internal::exclusive_async_requests.name(); - } - - static bool defaultValue() { - return false; - } - - static bool isPublic() { - return false; - } - - static ov::PropertyMutability mutability() { - return ov::PropertyMutability::RW; - } - - static constexpr std::string_view getTypeName() { - return "bool"; - } - - static OptionMode mode() { - return OptionMode::RunTime; - } -}; - struct PROFILING_TYPE final : OptionBase { static std::string_view key() { return ov::intel_npu::profiling_type.name(); @@ -671,32 +645,30 @@ struct NUM_STREAMS final : OptionBase { return "ov::streams::Num"; } - // The only supported number for currently supported platforms. - // FIXME: update in the future static ov::streams::Num defaultValue() { - return ov::streams::Num(1); + return ov::streams::AUTO; } static ov::streams::Num parse(std::string_view val) { std::istringstream stringStream = std::istringstream(std::string(val)); ov::streams::Num numberOfStreams; - stringStream >> numberOfStreams; return numberOfStreams; } - static std::string itoString(const ov::streams::Num& val) { + static std::string toString(const ov::streams::Num& val) { std::ostringstream stringStream; - stringStream << val; return stringStream.str(); } static void validateValue(const ov::streams::Num& num) { - if (defaultValue() != num && ov::streams::AUTO != num) { - OPENVINO_THROW("NUM_STREAMS can not be set"); + if (num != ov::streams::AUTO && num < 0) { + OPENVINO_THROW("NUM_STREAMS cannot be set to this value: ", + num, + ". Supported values are non-negative integers (including 0) or ov::streams::AUTO"); } } @@ -709,11 +681,14 @@ struct NUM_STREAMS final : OptionBase { } static ov::PropertyMutability mutability() { - return ov::PropertyMutability::RO; + return ov::PropertyMutability::RW; } }; -struct ENABLE_CPU_PINNING final : OptionBase { +OPENVINO_SUPPRESS_DEPRECATED_START +struct OPENVINO_DEPRECATED("This property is deprecated and has no effect on the NPU Plugin. It will be removed in " + "the OpenVINO 2027.0 release.") ENABLE_CPU_PINNING final + : OptionBase { static std::string_view key() { return ov::hint::enable_cpu_pinning.name(); } @@ -722,8 +697,9 @@ struct ENABLE_CPU_PINNING final : OptionBase { return false; } - static bool isPublic() { - return true; + static constexpr const char* deprecationMessage() { + return "The \"ENABLE_CPU_PINNING\" property is deprecated and has no effect on the NPU Plugin. It will " + "be removed in the OpenVINO 2027.0 release."; } static ov::PropertyMutability mutability() { @@ -734,6 +710,7 @@ struct ENABLE_CPU_PINNING final : OptionBase { return OptionMode::RunTime; } }; +OPENVINO_SUPPRESS_DEPRECATED_END struct WORKLOAD_TYPE final : OptionBase { static std::string_view key() { @@ -863,6 +840,28 @@ struct COMPILER_TYPE final : OptionBase { + static std::string_view key() { + return ov::intel_npu::compiler_version.name(); + } + + static uint32_t defaultValue() { + return 0; + } + + static OptionMode mode() { + return OptionMode::RunTime; + } + + static bool isPublic() { + return true; + } + + static ov::PropertyMutability mutability() { + return ov::PropertyMutability::RO; + } +}; + struct COMPILATION_MODE final : OptionBase { static std::string_view key() { return ov::intel_npu::compilation_mode.name(); @@ -1371,20 +1370,6 @@ struct ENABLE_WEIGHTLESS final : OptionBase { } }; -struct WEIGHTLESS_BLOB final : OptionBase { - static std::string_view key() { - return ov::intel_npu::weightless_blob.name(); - } - - static bool defaultValue() { - return false; - } - - static OptionMode mode() { - return OptionMode::CompileTime; - } -}; - struct SEPARATE_WEIGHTS_VERSION final : OptionBase { static std::string_view key() { return ov::intel_npu::separate_weights_version.name(); @@ -1523,12 +1508,105 @@ struct SHARED_COMMON_QUEUE final : OptionBase { } static bool defaultValue() { + return false; + } + + static OptionMode mode() { + return OptionMode::RunTime; + } +}; + +struct CACHE_ENCRYPTION_CALLBACKS final : OptionBase { + static std::string_view key() { + return ov::cache_encryption_callbacks.name(); + } + + static constexpr std::string_view getTypeName() { + return "ov::EncryptionCallbacks"; + } + + static OptionMode mode() { + return OptionMode::RunTime; + } + + static bool isPublic() { return true; } + static std::string toString(const ov::EncryptionCallbacks&) { + OPENVINO_THROW("Option ", ov::cache_encryption_callbacks.name(), " cannot be converted to string"); + } + + static ov::EncryptionCallbacks parse(std::string_view) { + OPENVINO_THROW("Option ", ov::cache_encryption_callbacks.name(), " cannot be parsed from string"); + } + + static ov::PropertyMutability mutability() { + return ov::PropertyMutability::WO; + } +}; + +struct RUNTIME_REQUIREMENTS final : OptionBase { + static std::string_view key() { + return ov::runtime_requirements.name(); + } + + static std::string defaultValue() { + return {}; + } + static OptionMode mode() { return OptionMode::RunTime; } + + static bool isPublic() { + return true; + } + + static ov::PropertyMutability mutability() { + return ov::PropertyMutability::RO; + } +}; + +struct COMPATIBILITY_CHECK final : OptionBase { + static std::string_view key() { + return ov::compatibility_check.name(); + } + + static constexpr std::string_view getTypeName() { + return "ov::CompatibilityCheck"; + } + + static ov::CompatibilityCheck defaultValue() { + return ov::CompatibilityCheck::NOT_APPLICABLE; + } + + static OptionMode mode() { + return OptionMode::RunTime; + } + + static ov::CompatibilityCheck parse(std::string_view val) { + std::istringstream stringStream = std::istringstream(std::string(val)); + ov::CompatibilityCheck check_result; + stringStream >> check_result; + + return check_result; + } + + static std::string toString(const ov::CompatibilityCheck& val) { + std::ostringstream stringStream; + stringStream << val; + + return stringStream.str(); + } + + static bool isPublic() { + return true; + } + + static ov::PropertyMutability mutability() { + return ov::PropertyMutability::RO; + } }; } // namespace intel_npu diff --git a/src/plugins/intel_npu/src/al/include/intel_npu/npu_private_properties.hpp b/src/plugins/intel_npu/src/al/include/intel_npu/npu_private_properties.hpp index 9d5de026c072..6bbdaaf2a9ab 100644 --- a/src/plugins/intel_npu/src/al/include/intel_npu/npu_private_properties.hpp +++ b/src/plugins/intel_npu/src/al/include/intel_npu/npu_private_properties.hpp @@ -338,16 +338,6 @@ static constexpr ov::Property batch_mode{"NPU_BATCH_MODE"}; */ static constexpr ov::Property separate_weights_version{"NPU_SEPARATE_WEIGHTS_VERSION"}; -/** - * @brief [Only for NPU Plugin] - * Type: bool. Default is "false". - * - * This option enables/disables the "weights separation" feature. If enabled, the result of compilation will be a binary - * object stripped of a significant amount of weights. Before running the model, these weights need to be provided by - * external means. - */ -static constexpr ov::Property weightless_blob{"NPU_WEIGHTLESS_BLOB"}; - /** * @brief [Only for NPU Plugin] * Type: enum. Default is "AUTO". @@ -452,7 +442,7 @@ static constexpr ov::Property export_raw_blob{"NPU_EXPORT_RAW_BLOB"}; /** * @brief [Only for NPU Plugin] - * Type: boolean, default is true. + * Type: boolean, default is false. * This option allows to enable/disable the usage of a shared common queue for all compiled models. If set to false, * each compiled model will have its own common queue. This option is added for enabling the isolation of compiled * models from each other, which can be required for some use cases. diff --git a/src/plugins/intel_npu/src/al/src/config/config.cpp b/src/plugins/intel_npu/src/al/src/config/config.cpp index a505f3a602f5..5ebf77e4bdf8 100644 --- a/src/plugins/intel_npu/src/al/src/config/config.cpp +++ b/src/plugins/intel_npu/src/al/src/config/config.cpp @@ -245,7 +245,16 @@ void Config::parseEnvVars() { envVar, opt.envVar().data()); - _impl[opt.key().data()] = opt.validateAndParse(envVar); + try { + _impl[opt.key().data()] = opt.validateAndParseFromString(envVar); + } catch (const std::exception& e) { + _log.warning( + "Environment variable '%s' with value '%s' was ignored for option '%s' due to error:\n%s", + opt.envVar().data(), + envVar, + opt.key().data(), + e.what()); + } } } }); @@ -260,7 +269,16 @@ void Config::update(const ConfigMap& options) { _log.trace("Update option '%s' to value '%s'", p.first.c_str(), p.second.c_str()); const auto opt = _desc->get(p.first); - _impl[opt.key().data()] = opt.validateAndParse(p.second); + _impl[opt.key().data()] = opt.validateAndParseFromString(p.second); + } +} + +void Config::updateAny(const ov::AnyMap& options) { + for (const auto& p : options) { + _log.trace("Update option '%s' to given 'ov::Any' value", p.first.c_str()); + + const auto opt = _desc->get(p.first); + _impl[opt.key().data()] = opt.validateAndParseFromAny(p.second); } } diff --git a/src/plugins/intel_npu/src/backend/include/zero_dynamic_pipeline.hpp b/src/plugins/intel_npu/src/backend/include/zero_dynamic_pipeline.hpp index 8f712fcef58c..1890e43ba93f 100644 --- a/src/plugins/intel_npu/src/backend/include/zero_dynamic_pipeline.hpp +++ b/src/plugins/intel_npu/src/backend/include/zero_dynamic_pipeline.hpp @@ -52,39 +52,17 @@ class DynamicPipeline final : public IPipeline { const void* arg_value, const ov::Strides& strides, const ov::Shape& shapes) { + // The strides are already divided by element size if (arg_index < _binding._inputs.size()) { _binding._inputs[arg_index].setArg(arg_value); - // Only store the valid shape dimensions - for (int64_t i = 0; i < _binding._inputs[arg_index]._dimsCount; i++) { - _binding._inputs[arg_index]._sizes[i] = shapes[i]; - } - - if (!strides.empty()) { - for (int64_t i = 0; i < _binding._inputs[arg_index]._dimsCount; i++) { - _binding._inputs[arg_index]._strides[i] = strides[i]; - } - } else { - // Need stride based on element but not byte, calc from shape - _binding._inputs[arg_index].updateStride(); - } + _binding._inputs[arg_index].setSize(shapes); + _binding._inputs[arg_index].setStrides(strides); } else { size_t output_index = static_cast(arg_index) - _binding._inputs.size(); if (output_index < _binding._outputs.size()) { _binding._outputs[output_index].setArg(arg_value); - - // Only store the valid shape dimensions - for (int64_t i = 0; i < _binding._outputs[output_index]._dimsCount; i++) { - _binding._outputs[output_index]._sizes[i] = shapes[i]; - } - - if (!strides.empty()) { - for (int64_t i = 0; i < _binding._outputs[output_index]._dimsCount; i++) { - _binding._outputs[output_index]._strides[i] = strides[i]; - } - } else { - // Need stride based on element but not byte, calc from shape - _binding._outputs[output_index].updateStride(); - } + _binding._outputs[output_index].setSize(shapes); + _binding._outputs[output_index].setStrides(strides); } } } diff --git a/src/plugins/intel_npu/src/backend/src/zero_dynamic_infer_request.cpp b/src/plugins/intel_npu/src/backend/src/zero_dynamic_infer_request.cpp index 1c809482e6f6..1ef94a1432b2 100644 --- a/src/plugins/intel_npu/src/backend/src/zero_dynamic_infer_request.cpp +++ b/src/plugins/intel_npu/src/backend/src/zero_dynamic_infer_request.cpp @@ -43,6 +43,8 @@ void ZeroDynamicInferRequest::sync_zero_tensor_with_graph(const ZeroInferRequest auto& levelZeroTensor = foundPort.is_input() ? get_level_zero_input(foundPort.idx) : _levelZeroOutputTensors.at(foundPort.idx); + auto originallevelZeroTensor = levelZeroTensor; + try { _logger.debug("sync_zero_tensor_with_graph - create zero tensor"); OV_ITT_TASK_NEXT(ZERO_SET_TENSOR, "create zero tensor"); @@ -70,12 +72,14 @@ void ZeroDynamicInferRequest::sync_zero_tensor_with_graph(const ZeroInferRequest } if (_pipelineIsCreated && !_dynamicBatchValueChanged) { - _logger.debug("sync_zero_tensor_with_graph - update command list"); + _logger.debug("sync_zero_tensor_with_graph - update graph arguments"); OPENVINO_ASSERT(levelZeroTensor->data(), "Empty buffer"); OV_ITT_TASK_NEXT(ZERO_SET_TENSOR, "update_graph_arguments"); - if (levelZeroTensor->get_byte_size() != tensor->get_byte_size()) { + if (originallevelZeroTensor != nullptr && originallevelZeroTensor->get_shape() != tensor->get_shape()) { + _logger.debug("sync_zero_tensor_with_graph - update graph arguments with user tensor pointer since " + "shape is changed"); _pipeline->update_graph_arguments(foundPort.is_input() ? _metadata.inputs.at(foundPort.idx).indexUsedByDriver : _metadata.outputs.at(foundPort.idx).indexUsedByDriver, @@ -84,6 +88,8 @@ void ZeroDynamicInferRequest::sync_zero_tensor_with_graph(const ZeroInferRequest _isTensorChanged = true; } else { // This L0 tensor shall have same info with user tensor + _logger.debug("sync_zero_tensor_with_graph - update graph arguments without user tensor pointer since " + "shape is not changed"); _pipeline->update_graph_arguments(foundPort.is_input() ? _metadata.inputs.at(foundPort.idx).indexUsedByDriver : _metadata.outputs.at(foundPort.idx).indexUsedByDriver, @@ -109,6 +115,7 @@ void ZeroDynamicInferRequest::sync_zero_tensors_with_graph(const ZeroInferReques get_level_zero_inputs(foundPort.idx).resize(tensors.size()); for (size_t i = 0; i < tensors.size(); i++) { + auto originalLevelZeroTensor = get_level_zero_input(foundPort.idx, i); try { _logger.debug("sync_zero_tensors_with_graph - create zero tensor"); OV_ITT_TASK_NEXT(ZERO_SET_TENSORS, "create_zero_tensor"); @@ -126,11 +133,22 @@ void ZeroDynamicInferRequest::sync_zero_tensors_with_graph(const ZeroInferReques if (_pipelineIsCreated && !_dynamicBatchValueChanged) { OPENVINO_ASSERT(get_level_zero_input(foundPort.idx, i)->data(), "Empty buffer"); OV_ITT_TASK_NEXT(ZERO_SET_TENSORS, "update_graph_arguments"); - _pipeline->update_graph_arguments(_metadata.inputs.at(foundPort.idx).indexUsedByDriver, - get_level_zero_input(foundPort.idx, i), - i, - tensors.at(i)._ptr); - _isTensorChanged = true; + if (originalLevelZeroTensor != nullptr && + originalLevelZeroTensor->get_shape() != tensors.at(i)->get_shape()) { + _logger.debug( + "set_tensors - update graph arguments with user tensor pointer since shape is changed"); + _pipeline->update_graph_arguments(_metadata.inputs.at(foundPort.idx).indexUsedByDriver, + get_level_zero_input(foundPort.idx, i), + i, + tensors.at(i)._ptr); + _isTensorChanged = true; + } else { + _logger.debug( + "set_tensors - update graph arguments without user tensor pointer since shape is not changed"); + _pipeline->update_graph_arguments(_metadata.inputs.at(foundPort.idx).indexUsedByDriver, + get_level_zero_input(foundPort.idx, i), + i); + } } } } @@ -269,6 +287,10 @@ void ZeroDynamicInferRequest::predict_shapes(std::vector& outputProps) { + if (outputProps.empty()) { + _logger.debug("check_tensor_and_predicted_shapes - no output props to check, skip check"); + return; + } // check_tensor in set_tensor already checked the input tensor and output tensor with metadata // Check again here to see if the shape is right compared with predicted shape // If user set output tensor, need check if the tensor is large enough diff --git a/src/plugins/intel_npu/src/backend/src/zero_dynamic_pipeline.cpp b/src/plugins/intel_npu/src/backend/src/zero_dynamic_pipeline.cpp index ff41fa8cf6c6..45ec77c2e9ee 100644 --- a/src/plugins/intel_npu/src/backend/src/zero_dynamic_pipeline.cpp +++ b/src/plugins/intel_npu/src/backend/src/zero_dynamic_pipeline.cpp @@ -58,9 +58,7 @@ DynamicPipeline::DynamicPipeline(const std::shared_ptr& i OPENVINO_ASSERT(dynamicGraph != nullptr, "Failed to cast graph to IDynamicGraph"); if (!_sync_output_with_fences) { - _event_pool = std::make_shared(_init_structs->getDevice(), - _init_structs->getContext(), - _batch_size ? static_cast(_batch_size) : 1); + _event_pool = std::make_shared(_init_structs, _batch_size ? static_cast(_batch_size) : 1); _events.reserve(_batch_size); for (size_t i = 0; i < _batch_size; i++) { @@ -216,11 +214,8 @@ void DynamicPipeline::push() { } } - // L0 wrapper handle closed command list - command_lists->resetCommandList(); - dynamicGraph->execute(_init_structs, - command_lists->getBinding(), + graphArguments, command_lists->getHandles(), commandQueueHandle, fence, @@ -259,7 +254,6 @@ void DynamicPipeline::reset() const { _events.at(i)->reset(); } } - _logger.debug("reset - completed"); } @@ -267,7 +261,7 @@ void DynamicPipeline::update_graph_arguments(uint32_t index, const std::shared_ptr& zeroTensor, const std::shared_ptr& userTensor) { OV_ITT_TASK_CHAIN(ZERO_EXECUTOR_IP_UMCL, itt::domains::LevelZeroBackend, "DynamicPipeline", "updateCommandList"); - _logger.debug("update_graph_arguments - update command list"); + _logger.debug("update_graph_arguments - started"); // This is the tensor with right shape and strides // The required check is alredy done in inferRequest const std::shared_ptr& tensor = userTensor ? userTensor : zeroTensor; @@ -289,6 +283,7 @@ void DynamicPipeline::update_graph_arguments(uint32_t index, tensor->get_shape()); } } + _logger.debug("update_graph_arguments - completed"); } void DynamicPipeline::update_graph_arguments(uint32_t index, diff --git a/src/plugins/intel_npu/src/backend/src/zero_infer_request.cpp b/src/plugins/intel_npu/src/backend/src/zero_infer_request.cpp index 8987914d4e9d..f1490d060bf9 100644 --- a/src/plugins/intel_npu/src/backend/src/zero_infer_request.cpp +++ b/src/plugins/intel_npu/src/backend/src/zero_infer_request.cpp @@ -19,8 +19,6 @@ #include "transformations/utils/utils.hpp" #include "zero_variable_state.hpp" -namespace intel_npu { - namespace { std::shared_ptr validate_compiled_model( @@ -29,8 +27,47 @@ std::shared_ptr validate_compiled_model( return compiledModel; } +bool has_related_shape_tensor(const std::vector& metadata, size_t descriptorIdx) { + const auto& relatedDescriptor = metadata.at(descriptorIdx).relatedDescriptorIndex; + return relatedDescriptor.has_value() && metadata.at(relatedDescriptor.value()).isShapeTensor; +} + +void copy_tensor_with_optional_shape_view(const std::shared_ptr& srcTensor, + const std::shared_ptr& dstTensor, + bool srcTensorIsUserTensor, + bool hasRelatedShapeTensor) { + if (hasRelatedShapeTensor && srcTensor->get_shape() != dstTensor->get_shape()) { + // With dynamic bounds (shape tensor present), Level Zero tensor is allocated for max shape. + // User tensor can be smaller for the current inference, so we create a smaller view over + // the same Level Zero buffer and copy using the user-visible shape. + if (srcTensorIsUserTensor) { + OPENVINO_ASSERT(srcTensor->get_byte_size() <= dstTensor->get_byte_size(), + "Source byte size exceeds destination tensor allocation for dynamic-shape view copy"); + + auto viewTensor = ov::make_tensor(dstTensor->get_element_type(), + srcTensor->get_shape(), + static_cast(dstTensor->data())); + srcTensor->copy_to(viewTensor); + return; + } + + OPENVINO_ASSERT(dstTensor->get_byte_size() <= srcTensor->get_byte_size(), + "Source byte size exceeds destination tensor allocation for dynamic-shape view copy"); + + auto viewTensor = ov::make_tensor(srcTensor->get_element_type(), + dstTensor->get_shape(), + static_cast(srcTensor->data())); + viewTensor->copy_to(dstTensor); + return; + } + + srcTensor->copy_to(dstTensor); +} + } // namespace +namespace intel_npu { + std::optional determine_dynamic_batch_size(const IODescriptor& desc, const ov::PartialShape& ioShape, const std::shared_ptr& tensor, @@ -467,10 +504,14 @@ void ZeroInferRequest::sync_zero_tensor_with_graph(const ZeroInferRequest::Found itt::domains::LevelZeroBackend, "ZeroInferRequest", "sync_zero_tensor_with_graph"); + const auto& metadata = foundPort.is_input() ? _metadata.inputs : _metadata.outputs; auto& levelZeroTensor = foundPort.is_input() ? get_level_zero_input(foundPort.idx) : _levelZeroOutputTensors.at(foundPort.idx); - if (_initStructs->getMutableCommandListExtVersion() >= ZE_MAKE_VERSION(1, 0)) { + // For dynamic bounds (related shape tensor present), we must keep Level Zero allocation at max shape. + // Therefore user-memory import is allowed only when there is no related shape tensor. + if (_initStructs->getMutableCommandListExtVersion() >= ZE_MAKE_VERSION(1, 0) && + !has_related_shape_tensor(metadata, foundPort.idx)) { bool updateCommandListArg = false; try { _logger.debug("sync_zero_tensor_with_graph - create zero tensor"); @@ -586,7 +627,10 @@ void ZeroInferRequest::sync_zero_tensors_with_graph(const ZeroInferRequest::Foun "ZeroInferRequest", "sync_zero_tensors_with_graph"); - if (_initStructs->getMutableCommandListExtVersion() >= ZE_MAKE_VERSION(1, 0)) { + // For dynamic bounds (related shape tensor present), we must keep Level Zero allocation at max shape. + // Therefore user-memory import is allowed only when there is no related shape tensor. + if (_initStructs->getMutableCommandListExtVersion() >= ZE_MAKE_VERSION(1, 0) && + !has_related_shape_tensor(_metadata.inputs, foundPort.idx)) { if (batchSize.has_value()) { get_level_zero_inputs(foundPort.idx).resize(tensors.size()); for (size_t i = 0; i < tensors.size(); i++) { @@ -903,6 +947,11 @@ void ZeroInferRequest::prepare_inputs() { OPENVINO_ASSERT(!inputDescriptor.isInitInputWeights, "This path should not be used for running inferences for the \"init\" model"); + if (inputDescriptor.isMainInputWeights) { + // These values were set while running the "WeightlessGraph::init" method + continue; + } + if (inputDescriptor.isShapeTensor) { OPENVINO_ASSERT(inputDescriptor.relatedDescriptorIndex.has_value(), "The link between the dynamic tensor and its shape tensor is missing, entry name: ", @@ -978,11 +1027,6 @@ void ZeroInferRequest::prepare_inputs() { continue; } - if (inputDescriptor.isMainInputWeights) { - // These values were set while running the "WeightlessGraph::init" method - continue; - } - const auto& levelZeroTensor = get_level_zero_input(inputIndex); OPENVINO_ASSERT(levelZeroTensor, "Input zero tensor is not allocated."); @@ -1000,7 +1044,10 @@ void ZeroInferRequest::prepare_inputs() { _logger.info("prepare_inputs - tensor is not allocated in the current Level Zero context"); OV_ITT_TASK_NEXT(ZERO_INFER, "memcpy"); - userTensor.at(SINGLE_TENSOR)->copy_to(levelZeroTensor); + copy_tensor_with_optional_shape_view(userTensor.at(SINGLE_TENSOR)._ptr, + levelZeroTensor, + true, + has_related_shape_tensor(_metadata.inputs, inputIndex)); } ++inputIndex; @@ -1057,7 +1104,10 @@ void ZeroInferRequest::get_result() { _logger.info("get_result - output tensor by index: %zu is not allocated in the current Level Zero context", outputIndex); OV_ITT_TASK_NEXT(ZERO_RESULT, "memcpy"); - levelZeroTensor->copy_to(userTensor._ptr); + copy_tensor_with_optional_shape_view(levelZeroTensor, + userTensor._ptr, + false, + has_related_shape_tensor(_metadata.outputs, outputIndex)); } levelZeroTensor->detach_imported_allocation_for_custom_tensor(); @@ -1130,13 +1180,21 @@ void ZeroInferRequest::check_tensor(const ov::Output& port, if (port_length > 0) { const auto& port_max_shape = port_partial_shape.get_max_shape(); + const auto& port_min_shape = port_partial_shape.get_min_shape(); for (auto i = 0; i < port_length; ++i) { - if (tensor_shape[i] > port_max_shape[i]) { + if (port_min_shape[i] != port_max_shape[i] && tensor_shape[i] > port_max_shape[i]) { OPENVINO_THROW("The tensor shape is not compatible with the model input/output max shape: got ", tensor_shape, " expecting max shape ", port_max_shape); } + + if (port_min_shape[i] == port_max_shape[i] && tensor_shape[i] != port_min_shape[i]) { + OPENVINO_THROW("The tensor shape is not compatible with the model input/output shape: got ", + tensor_shape, + " expecting shape ", + port_min_shape); + } } } } diff --git a/src/plugins/intel_npu/src/backend/src/zero_pipeline.cpp b/src/plugins/intel_npu/src/backend/src/zero_pipeline.cpp index 311e2fea1936..710bfde2b0f2 100644 --- a/src/plugins/intel_npu/src/backend/src/zero_pipeline.cpp +++ b/src/plugins/intel_npu/src/backend/src/zero_pipeline.cpp @@ -152,9 +152,7 @@ Pipeline::Pipeline(const std::shared_ptr& init_structs, "In-order execution doesn't work in case synchronization of the inferences is done using events"); if (!_sync_output_with_fences || _run_inferences_sequentially) { - _event_pool = std::make_shared(_init_structs->getDevice(), - _init_structs->getContext(), - _batch_size ? static_cast(_batch_size) : 1); + _event_pool = std::make_shared(_init_structs, _batch_size ? static_cast(_batch_size) : 1); _events.reserve(_batch_size); for (size_t i = 0; i < _batch_size; i++) { diff --git a/src/plugins/intel_npu/src/backend/src/zero_profiling.cpp b/src/plugins/intel_npu/src/backend/src/zero_profiling.cpp index fbd32fe6ce17..c168ad3c272f 100644 --- a/src/plugins/intel_npu/src/backend/src/zero_profiling.cpp +++ b/src/plugins/intel_npu/src/backend/src/zero_profiling.cpp @@ -41,7 +41,7 @@ bool ProfilingPool::create() { } ProfilingPool::~ProfilingPool() { - if (_handle) { + if (_handle && _init_structs->getContext()) { _init_structs->getProfilingDdiTable().pfnProfilingPoolDestroy(_handle); } } @@ -61,7 +61,7 @@ LayerStatistics ProfilingQuery::getLayerStatistics() const { } ProfilingQuery::~ProfilingQuery() { - if (_handle) { + if (_handle && _init_structs->getContext()) { _init_structs->getProfilingDdiTable().pfnProfilingQueryDestroy(_handle); } } @@ -238,14 +238,18 @@ int64_t NpuInferProfiling::convertCCtoUS(int64_t val_cc) const { NpuInferProfiling::~NpuInferProfiling() { /// deallocate npu_ts_infer_start and npu_ts_infer_end, allocated externally by ze driver + auto context = _init_structs->getContext(); + if (context == nullptr) { + return; + } if (npu_ts_infer_start != nullptr) { - auto ze_ret = zeMemFree(_init_structs->getContext(), npu_ts_infer_start); + auto ze_ret = zeMemFree(context, npu_ts_infer_start); if (ZE_RESULT_SUCCESS != ze_ret) { _logger.error("zeMemFree on npu_ts_infer_start failed %#X", uint64_t(ze_ret)); } } if (npu_ts_infer_end != nullptr) { - auto ze_ret = zeMemFree(_init_structs->getContext(), npu_ts_infer_end); + auto ze_ret = zeMemFree(context, npu_ts_infer_end); if (ZE_RESULT_SUCCESS != ze_ret) { _logger.error("zeMemFree on npu_ts_infer_end failed %#X", uint64_t(ze_ret)); } diff --git a/src/plugins/intel_npu/src/common/include/intel_npu/common/device_helpers.hpp b/src/plugins/intel_npu/src/common/include/intel_npu/common/device_helpers.hpp index 9f6fcfcddf2f..a2cbd8feedc0 100644 --- a/src/plugins/intel_npu/src/common/include/intel_npu/common/device_helpers.hpp +++ b/src/plugins/intel_npu/src/common/include/intel_npu/common/device_helpers.hpp @@ -24,6 +24,18 @@ std::string getCompilationPlatform(const std::string_view platform, * @brief Gets the device by its ID. */ std::shared_ptr getDeviceById(const ov::SoPtr& engineBackend, const std::string& deviceId); + +/** + * @brief Gets the optimal number of infer requests in parallel for the given platform and performance mode. + * @param platform The platform for which to get the optimal number of infer requests. + * @param performanceMode The performance mode for which to get the optimal number of infer requests. + * @return The optimal number of infer requests in parallel. + * @note This is the value provided by the plugin, application should query and consider it, but may supply its own + * preference for number of parallel requests via dedicated configuration + */ +uint32_t getOptimalNumberOfInferRequestsInParallel(std::string_view platform, + const ov::hint::PerformanceMode performanceMode); + } // namespace utils } // namespace intel_npu diff --git a/src/plugins/intel_npu/src/common/include/intel_npu/common/filtered_config.hpp b/src/plugins/intel_npu/src/common/include/intel_npu/common/filtered_config.hpp index ee5cda6ea5de..da70d6c1dbda 100644 --- a/src/plugins/intel_npu/src/common/include/intel_npu/common/filtered_config.hpp +++ b/src/plugins/intel_npu/src/common/include/intel_npu/common/filtered_config.hpp @@ -35,6 +35,14 @@ class FilteredConfig final : public Config { */ void update(const ConfigMap& options) override; + /** + * @brief Additional update method for properties that don't have their values convertible to string objects. + * @note In the future, code will be refactored to use only this update method to optimize redundant flow + * ov::Any-->std::string-->Opt::ValueType + * @param options A map of key-value pairs representing the new configuration options. + */ + void updateAny(const ov::AnyMap& options) override; + /** * @brief Checks if a specific option exists in the configuration's descriptorDesc. * @param key The key of the option to check. diff --git a/src/plugins/intel_npu/src/common/include/intel_npu/common/icompiler_adapter.hpp b/src/plugins/intel_npu/src/common/include/intel_npu/common/icompiler_adapter.hpp index 8830ebf61747..588681b2d9c7 100644 --- a/src/plugins/intel_npu/src/common/include/intel_npu/common/icompiler_adapter.hpp +++ b/src/plugins/intel_npu/src/common/include/intel_npu/common/icompiler_adapter.hpp @@ -37,6 +37,8 @@ class ICompilerAdapter { virtual std::optional> get_supported_options() const = 0; virtual bool is_option_supported(std::string optName, std::optional optValue = std::nullopt) const = 0; + virtual bool validate_compatibility_descriptor(const std::string& compatibilityDescriptor) const = 0; + virtual ~ICompilerAdapter() = default; }; diff --git a/src/plugins/intel_npu/src/common/include/intel_npu/common/idynamic_graph.hpp b/src/plugins/intel_npu/src/common/include/intel_npu/common/idynamic_graph.hpp index cd9bf9e1e9b8..89eb0b4a6bcc 100644 --- a/src/plugins/intel_npu/src/common/include/intel_npu/common/idynamic_graph.hpp +++ b/src/plugins/intel_npu/src/common/include/intel_npu/common/idynamic_graph.hpp @@ -37,6 +37,7 @@ class IDynamicGraph : public IGraph { void setArg(const void* arg); void setSize(const ov::Shape& shape); + void setStrides(const ov::Strides& strides, int32_t elementSize = 1); void set(const void* basePtr, int64_t offset, std::shared_ptr tensor); void updateStride(); bool compare(const MemRefType& memref); diff --git a/src/plugins/intel_npu/src/common/include/intel_npu/common/igraph.hpp b/src/plugins/intel_npu/src/common/include/intel_npu/common/igraph.hpp index f323dec4bcc5..0714ca5bce1d 100644 --- a/src/plugins/intel_npu/src/common/include/intel_npu/common/igraph.hpp +++ b/src/plugins/intel_npu/src/common/include/intel_npu/common/igraph.hpp @@ -75,6 +75,8 @@ class IGraph : public std::enable_shared_from_this { virtual std::optional is_profiling_blob() const = 0; + virtual std::optional get_compatibility_descriptor() const; + protected: virtual void initialize_impl(const FilteredConfig& config); diff --git a/src/plugins/intel_npu/src/common/include/intel_npu/common/iparser.hpp b/src/plugins/intel_npu/src/common/include/intel_npu/common/iparser.hpp index 62bb850d220f..da98dc0c2901 100644 --- a/src/plugins/intel_npu/src/common/include/intel_npu/common/iparser.hpp +++ b/src/plugins/intel_npu/src/common/include/intel_npu/common/iparser.hpp @@ -28,7 +28,8 @@ class IParser { const ov::Tensor& mainBlob, const FilteredConfig& config, const std::optional>& initBlobs = std::nullopt, - std::optional>&& model = std::nullopt) const = 0; + std::optional>&& model = std::nullopt, + const std::optional& compatibilityDescriptor = std::nullopt) const = 0; virtual ~IParser() = default; }; diff --git a/src/plugins/intel_npu/src/common/src/device_helpers.cpp b/src/plugins/intel_npu/src/common/src/device_helpers.cpp index fbd0375d5def..d98905ed5257 100644 --- a/src/plugins/intel_npu/src/common/src/device_helpers.cpp +++ b/src/plugins/intel_npu/src/common/src/device_helpers.cpp @@ -71,4 +71,13 @@ std::shared_ptr utils::getDeviceById(const ov::SoPtr& e return nullptr; } +uint32_t utils::getOptimalNumberOfInferRequestsInParallel(std::string_view platform, + const ov::hint::PerformanceMode performanceMode) { + if (performanceMode != ov::hint::PerformanceMode::THROUGHPUT) { + return 1; + } + + return (platform == ov::intel_npu::Platform::NPU3720) ? 4 : 8; +} + } // namespace intel_npu diff --git a/src/plugins/intel_npu/src/common/src/filtered_config.cpp b/src/plugins/intel_npu/src/common/src/filtered_config.cpp index 0c3f92995d7e..eafe2b4ecf77 100644 --- a/src/plugins/intel_npu/src/common/src/filtered_config.cpp +++ b/src/plugins/intel_npu/src/common/src/filtered_config.cpp @@ -32,7 +32,22 @@ void FilteredConfig::update(const ConfigMap& options) { if (isAvailable(p.first)) { const auto opt = _desc->get(p.first); - _impl[opt.key().data()] = opt.validateAndParse(p.second); + _impl[opt.key().data()] = opt.validateAndParseFromString(p.second); + } else { + OPENVINO_THROW("[ NOT_FOUND ] Option '" + p.first + "' is not supported for current configuration"); + } + } +} + +void FilteredConfig::updateAny(const ov::AnyMap& options) { + auto log = Logger::global().clone("Config"); + + for (const auto& p : options) { + log.trace("Update option '%s' to given 'ov::Any' value", p.first.c_str()); + + if (isAvailable(p.first)) { + const auto opt = _desc->get(p.first); + _impl[opt.key().data()] = opt.validateAndParseFromAny(p.second); } else { OPENVINO_THROW("[ NOT_FOUND ] Option '" + p.first + "' is not supported for current configuration"); } diff --git a/src/plugins/intel_npu/src/common/src/idynamic_graph.cpp b/src/plugins/intel_npu/src/common/src/idynamic_graph.cpp index fcc82b804695..0777edd04fb7 100644 --- a/src/plugins/intel_npu/src/common/src/idynamic_graph.cpp +++ b/src/plugins/intel_npu/src/common/src/idynamic_graph.cpp @@ -15,28 +15,60 @@ void IDynamicGraph::MemRefType::setArg(const void* arg) { void IDynamicGraph::MemRefType::setSize(const ov::Shape& shape) { // Note: check difference between shape from compiler and shape from IR. - _sizes.resize(shape.size()); - _strides.resize(shape.size()); - _dimsCount = static_cast(shape.size()); - for (size_t i = 0; i < shape.size(); ++i) { - _sizes[i] = shape[i]; - _strides[i] = 0; // Strides are not set yet, will be updated + if (_dimsCount == 0) { + _dimsCount = static_cast(shape.size()); + _sizes.resize(shape.size()); + _strides.resize(shape.size()); + } else if (_dimsCount != static_cast(shape.size())) { + OPENVINO_THROW("Dimension count mismatch. Current dimension count: ", + _dimsCount, + ", new dimension count: ", + shape.size()); + } + + for (int64_t i = 0; i < _dimsCount; ++i) { + _sizes[i] = static_cast(shape[i]); + } +} + +void IDynamicGraph::MemRefType::setStrides(const ov::Strides& strides, int32_t elementSize) { + if (_dimsCount == 0) { + OPENVINO_THROW("Dimension count is zero, shall call setSize before setStrides"); + } else if (_dimsCount != static_cast(strides.size())) { + OPENVINO_THROW("Dimension count mismatch. Current dimension count: ", + _dimsCount, + ", new dimension count: ", + strides.size()); + } + + for (int64_t i = 0; i < _dimsCount; ++i) { + _strides[i] = static_cast(strides[i] / elementSize); } } void IDynamicGraph::MemRefType::set(const void* arg, int64_t offset, std::shared_ptr tensor) { _basePtr = _data = arg; _offset = offset; + if (_dimsCount == 0) { + _dimsCount = static_cast(tensor->get_shape().size()); + _sizes.resize(_dimsCount); + _strides.resize(_dimsCount); + } else if (_dimsCount != static_cast(tensor->get_shape().size())) { + OPENVINO_THROW("Dimension count mismatch. Current dimension count: ", + _dimsCount, + ", new dimension count: ", + tensor->get_shape().size()); + } + auto& shape = tensor->get_shape(); - for (size_t j = 0; j < shape.size(); j++) { - _sizes[j] = shape[j]; + for (int64_t j = 0; j < _dimsCount; j++) { + _sizes[j] = static_cast(shape[j]); } auto& strides = tensor->get_strides(); size_t elementSize = tensor->get_element_type().bitwidth() < 8 ? 1 : tensor->get_element_type().size(); - for (size_t j = 0; j < strides.size(); j++) { - _strides[j] = strides[j] / elementSize; + for (int64_t j = 0; j < _dimsCount; j++) { + _strides[j] = static_cast(strides[j] / elementSize); } - _dimsCount = shape.size(); } void IDynamicGraph::MemRefType::updateStride() { @@ -75,6 +107,7 @@ std::ostream& operator<<(std::ostream& os, const IDynamicGraph::MemRefType& memR os << stride << " "; } os << "]"; + return os; } diff --git a/src/plugins/intel_npu/src/common/src/igraph.cpp b/src/plugins/intel_npu/src/common/src/igraph.cpp index 7009720d4f04..513890005dfa 100644 --- a/src/plugins/intel_npu/src/common/src/igraph.cpp +++ b/src/plugins/intel_npu/src/common/src/igraph.cpp @@ -24,6 +24,10 @@ void IGraph::set_argument_value_with_strides(uint32_t, const void*, const std::v OPENVINO_THROW("set_argument_value_with_strides not implemented"); } +std::optional IGraph::get_compatibility_descriptor() const { + OPENVINO_THROW("get_compatibility_descriptor not implemented"); +} + void IGraph::initialize(const FilteredConfig& config) { std::lock_guard lock(_initialize_mutex); diff --git a/src/plugins/intel_npu/src/compiler_adapter/CMakeLists.txt b/src/plugins/intel_npu/src/compiler_adapter/CMakeLists.txt index 3aa5b01104f5..08161856f575 100644 --- a/src/plugins/intel_npu/src/compiler_adapter/CMakeLists.txt +++ b/src/plugins/intel_npu/src/compiler_adapter/CMakeLists.txt @@ -8,7 +8,7 @@ file(GLOB_RECURSE SOURCES *.cpp *.hpp *.h) source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${SOURCES}) -add_library(${TARGET_NAME} STATIC ${SOURCES} $) +add_library(${TARGET_NAME} STATIC ${SOURCES} $ $) ov_build_target_faster(${TARGET_NAME} PCH_HEADER "src/precomp.hpp" diff --git a/src/plugins/intel_npu/src/compiler_adapter/include/compiler_impl.hpp b/src/plugins/intel_npu/src/compiler_adapter/include/compiler_impl.hpp index bf377bc382cb..9f1552ea2b96 100644 --- a/src/plugins/intel_npu/src/compiler_adapter/include/compiler_impl.hpp +++ b/src/plugins/intel_npu/src/compiler_adapter/include/compiler_impl.hpp @@ -19,9 +19,8 @@ namespace intel_npu { class VCLCompilerImpl final : public std::enable_shared_from_this { public: - VCLCompilerImpl(); + VCLCompilerImpl(const std::string& library_dir); ~VCLCompilerImpl(); - static const std::shared_ptr getInstance(); /** * @brief Transforms a network from the OpenVINO model representation to a format executable @@ -29,9 +28,11 @@ class VCLCompilerImpl final : public std::enable_shared_from_this& model, const FilteredConfig& config) const; + std::pair> compile(const std::shared_ptr& model, + const FilteredConfig& config) const; /** * @brief Compiles the model, weights separation enabled. All init schedules along with the main one are compiled in @@ -88,15 +89,27 @@ class VCLCompilerImpl final : public std::enable_shared_from_this getLinkedLibrary() const; + /** + * @brief Validates the compatibility descriptor against the current device information. + * This function is used as a fallback check when the driver on the system does not support the required API + * @param compatibilityDescriptor The compatibility descriptor (string) to be validated + * @param in_device_desc Pointer to a device descriptor containing the device ID, number of + * tiles and stepping information + * @return false if the platform does not meet the requirements specified in the compatibility descriptor, + * true if the platform is compatible + */ + bool validate_compatibility_descriptor(const std::string& compatibilityDescriptor, + vcl_device_desc_t* in_device_desc) const; + private: /** - * @brief Compiles the given model according to the given configuration. During the model serialization step, the - * "WeightlessCacheAttribute" may be stored within the serialized model if requested. + * @brief Compiles the given model according to the given configuration. During the model serialization step, + * the "WeightlessCacheAttribute" may be stored within the serialized model if requested. * @note Storing the "WeightlessCacheAttribute" is necessary if the "weights separation" flow is being used. */ - ov::Tensor compile(const std::shared_ptr& model, - const FilteredConfig& config, - const bool storeWeightlessCacheAttributeFlag) const; + std::pair> compile(const std::shared_ptr& model, + const FilteredConfig& config, + const bool storeWeightlessCacheAttributeFlag) const; vcl_log_handle_t _logHandle = nullptr; vcl_compiler_handle_t _compilerHandle = nullptr; diff --git a/src/plugins/intel_npu/src/compiler_adapter/include/driver_compiler_adapter.hpp b/src/plugins/intel_npu/src/compiler_adapter/include/driver_compiler_adapter.hpp index 8603cd0b9130..432d85a13034 100644 --- a/src/plugins/intel_npu/src/compiler_adapter/include/driver_compiler_adapter.hpp +++ b/src/plugins/intel_npu/src/compiler_adapter/include/driver_compiler_adapter.hpp @@ -32,6 +32,8 @@ class DriverCompilerAdapter final : public ICompilerAdapter { uint32_t get_version() const override; + bool validate_compatibility_descriptor(const std::string& compatibilityDescriptor) const override; + private: bool isCompilerOptionSupported(const FilteredConfig& config, const ze_graph_compiler_version_info_t& compilerVersion, diff --git a/src/plugins/intel_npu/src/compiler_adapter/include/dynamic_graph.hpp b/src/plugins/intel_npu/src/compiler_adapter/include/dynamic_graph.hpp index 954f6a5f5889..8a6b34392576 100644 --- a/src/plugins/intel_npu/src/compiler_adapter/include/dynamic_graph.hpp +++ b/src/plugins/intel_npu/src/compiler_adapter/include/dynamic_graph.hpp @@ -10,8 +10,9 @@ #include "intel_npu/common/idynamic_graph.hpp" #include "intel_npu/common/network_metadata.hpp" +#include "intel_npu/utils/vm/npu_vm_runtime_api.hpp" #include "intel_npu/utils/zero/zero_init.hpp" -#include "npu_vm_runtime_api.hpp" +#include "intel_npu/utils/zero/zero_wrappers.hpp" #include "openvino/runtime/so_ptr.hpp" namespace intel_npu { @@ -19,6 +20,9 @@ class DynamicGraph final : public IDynamicGraph { public: struct MemRefTypeImpl { npu_vm_runtime_mem_ref_handle_t _memRef; + bool _ptrUpdated = false; + bool _shapeUpdated = false; + bool _strideUpdated = false; MemRefTypeImpl() : _memRef(nullptr) {} @@ -30,6 +34,36 @@ class DynamicGraph final : public IDynamicGraph { // Update current MemRef handle to use latest metadata if (_memRef == nullptr) { createMemRef(memref._dimsCount); + } else { + // Create a temporary MemRefType based on current handle and compare, use arg to create right size + MemRefType tempMemRef(memref._basePtr, + memref._data, + memref._offset, + memref._sizes, + memref._strides, + memref._dimsCount); + alignWithHandle(tempMemRef); + // Check ptr + if (memref._basePtr != tempMemRef._basePtr || memref._data != tempMemRef._data || + memref._offset != tempMemRef._offset) { + _ptrUpdated = true; + } else { + _ptrUpdated = false; + } + + // Check shape + if (memref._sizes != tempMemRef._sizes) { + _shapeUpdated = true; + } else { + _shapeUpdated = false; + } + + // Check strides + if (memref._strides != tempMemRef._strides) { + _strideUpdated = true; + } else { + _strideUpdated = false; + } } auto result = npuVMRuntimeSetMemRef(_memRef, memref._basePtr, @@ -77,10 +111,17 @@ class DynamicGraph final : public IDynamicGraph { } }; - struct GraphArgumentsImpl : public GraphArguments { + struct GraphArgumentsImpl { std::vector _inputMemRefs; std::vector _outputMemRefs; npu_vm_runtime_execute_params_t _executeParams = {}; + + ~GraphArgumentsImpl() { + if (_executeParams.executionContext != nullptr) { + npuVMRuntimeDestroyExecutionContext(_executeParams.executionContext); + _executeParams.executionContext = nullptr; + } + } }; class Impl { @@ -156,6 +197,8 @@ class DynamicGraph final : public IDynamicGraph { std::optional is_profiling_blob() const override; + std::optional get_compatibility_descriptor() const override; + private: void initialize_impl(const FilteredConfig& config) override; @@ -166,6 +209,11 @@ class DynamicGraph final : public IDynamicGraph { NetworkMetadata _metadata; + // Preserve previous behavior: when shared common queue is disabled and a new queue is created due to a priority + // change, keep the same workload type to avoid creating a queue with an unexpected workload. + std::optional _workloadType = std::nullopt; + std::shared_ptr _commandQueue = nullptr; + /** * @brief Stores the number of subgraphs for dynamic models * @note the number of subgraphs will be one for static models diff --git a/src/plugins/intel_npu/src/compiler_adapter/include/graph.hpp b/src/plugins/intel_npu/src/compiler_adapter/include/graph.hpp index c74177a1ee5c..18ac0ccbae4e 100644 --- a/src/plugins/intel_npu/src/compiler_adapter/include/graph.hpp +++ b/src/plugins/intel_npu/src/compiler_adapter/include/graph.hpp @@ -12,6 +12,7 @@ #include "intel_npu/common/igraph.hpp" #include "intel_npu/utils/zero/zero_init.hpp" +#include "intel_npu/utils/zero/zero_wrappers.hpp" #include "openvino/runtime/so_ptr.hpp" #include "ze_graph_ext_wrappers.hpp" @@ -25,6 +26,7 @@ class Graph : public IGraph { NetworkMetadata metadata, std::optional blob, const FilteredConfig& config, + const std::optional& compatibilityDescriptor = std::nullopt, const bool blobIsPersistent = false, const bool calledFromWeightlessGraph = false); @@ -61,6 +63,8 @@ class Graph : public IGraph { void evict_memory() override; + std::optional get_compatibility_descriptor() const override; + ~Graph() override; protected: @@ -76,11 +80,17 @@ class Graph : public IGraph { GraphDescriptor _graphDesc; NetworkMetadata _metadata; + // Preserve previous behavior: when shared common queue is disabled and a new queue is created due to a priority + // change, keep the same workload type to avoid creating a queue with an unexpected workload. + std::optional _workloadType = std::nullopt; + std::shared_ptr _commandQueue = nullptr; + mutable std::mutex _commandQueueDescMutex; CommandQueueDesc _commandQueueDesc; std::vector> _lastSubmittedEvent; std::optional _blob; + std::optional _compatibilityDescriptor; // In the case of the import path, the blob is released after graph initialization so it can not be any longer // exported diff --git a/src/plugins/intel_npu/src/compiler_adapter/include/parser.hpp b/src/plugins/intel_npu/src/compiler_adapter/include/parser.hpp index 1097d29ced59..4d96c46dfca7 100644 --- a/src/plugins/intel_npu/src/compiler_adapter/include/parser.hpp +++ b/src/plugins/intel_npu/src/compiler_adapter/include/parser.hpp @@ -22,7 +22,8 @@ class Parser final : public IParser { const ov::Tensor& mainBlob, const FilteredConfig& config, const std::optional>& initBlobs = std::nullopt, - std::optional>&& model = std::nullopt) const override; + std::optional>&& model = std::nullopt, + const std::optional& compatibilityDescriptor = std::nullopt) const override; private: std::shared_ptr _zeroInitStruct; diff --git a/src/plugins/intel_npu/src/compiler_adapter/include/plugin_compiler_adapter.hpp b/src/plugins/intel_npu/src/compiler_adapter/include/plugin_compiler_adapter.hpp index 77208296b314..2e402cee30dd 100644 --- a/src/plugins/intel_npu/src/compiler_adapter/include/plugin_compiler_adapter.hpp +++ b/src/plugins/intel_npu/src/compiler_adapter/include/plugin_compiler_adapter.hpp @@ -33,6 +33,8 @@ class PluginCompilerAdapter final : public ICompilerAdapter { uint32_t get_version() const override; + bool validate_compatibility_descriptor(const std::string& compatibilityDescriptor) const override; + private: std::shared_ptr _zeroInitStruct; diff --git a/src/plugins/intel_npu/src/compiler_adapter/include/ze_graph_ext_wrappers.hpp b/src/plugins/intel_npu/src/compiler_adapter/include/ze_graph_ext_wrappers.hpp index 735e74e05da6..0dd6d4df3089 100644 --- a/src/plugins/intel_npu/src/compiler_adapter/include/ze_graph_ext_wrappers.hpp +++ b/src/plugins/intel_npu/src/compiler_adapter/include/ze_graph_ext_wrappers.hpp @@ -40,7 +40,8 @@ class ZeGraphExtWrappers { GraphDescriptor getGraphDescriptor(SerializedIR serializedIR, const std::string& buildFlags, - const bool bypassUmdCache = false) const; + const bool bypassUmdCache = false, + const bool secureCompile = false) const; GraphDescriptor getGraphDescriptor(const void* data, size_t size) const; diff --git a/src/plugins/intel_npu/src/compiler_adapter/src/compiler_adapter_factory.cpp b/src/plugins/intel_npu/src/compiler_adapter/src/compiler_adapter_factory.cpp index d25699973e5c..539ea6ac99e9 100644 --- a/src/plugins/intel_npu/src/compiler_adapter/src/compiler_adapter_factory.cpp +++ b/src/plugins/intel_npu/src/compiler_adapter/src/compiler_adapter_factory.cpp @@ -60,7 +60,7 @@ std::unique_ptr CompilerAdapterFactory::getCompiler(const ov:: // It is required to check if the device is compatible with the provided platform, as the driver compiler // will be used. auto deviceName = engineBackend->getDevice()->getName(); - if (deviceName != platform && deviceName != "AUTO_DETECT") { + if (!platform.empty() && deviceName != platform && deviceName != "AUTO_DETECT") { OPENVINO_THROW("Could not find a valid NPU device for the provided configuration."); } diff --git a/src/plugins/intel_npu/src/compiler_adapter/src/compiler_impl.cpp b/src/plugins/intel_npu/src/compiler_adapter/src/compiler_impl.cpp index 76dd94241f63..cca2c372fb84 100644 --- a/src/plugins/intel_npu/src/compiler_adapter/src/compiler_impl.cpp +++ b/src/plugins/intel_npu/src/compiler_adapter/src/compiler_impl.cpp @@ -4,6 +4,7 @@ #include "compiler_impl.hpp" +#include #include #include @@ -11,6 +12,7 @@ #include "intel_npu/npu_private_properties.hpp" #include "intel_npu/profiling.hpp" #include "intel_npu/utils/utils.hpp" +#include "intel_npu/utils/vcl/vcl_allocator.hpp" #include "intel_npu/utils/vcl/vcl_api.hpp" #include "model_serializer.hpp" #include "openvino/runtime/make_tensor.hpp" @@ -38,76 +40,6 @@ UsedVersion getUsedVclVersion(uint16_t pluginMajor, uint16_t pluginMinor, const return {usedMajor, usedMinor}; } -struct vcl_allocator : vcl_allocator2_t { - vcl_allocator() : vcl_allocator2_t{allocate, deallocate} {} - - static uint8_t* allocate(vcl_allocator2_t* allocator, size_t size) { - vcl_allocator* vclAllocator = static_cast(allocator); - vclAllocator->m_size = intel_npu::utils::align_size_to_standard_page_size(size); - auto allocatedPtr = reinterpret_cast( - vclAllocator->m_allocator.allocate(vclAllocator->m_size, intel_npu::utils::STANDARD_PAGE_SIZE)); - if (allocatedPtr == nullptr) { - OPENVINO_THROW("Failed to allocate aligned memory for allocator"); - } - memset(allocatedPtr + size, 0, vclAllocator->m_size - size); - vclAllocator->m_allocated = allocatedPtr; - return allocatedPtr; - } - - static void deallocate(vcl_allocator2_t* allocator, uint8_t* ptr) { - if (ptr == nullptr) { - OPENVINO_THROW("Pointer is nullptr in deallocate!"); - } - vcl_allocator* vclAllocator = static_cast(allocator); - vclAllocator->m_allocator.deallocate(ptr, vclAllocator->m_size, intel_npu::utils::STANDARD_PAGE_SIZE); - } - ov::Allocator m_allocator; - uint8_t* m_allocated = nullptr; - size_t m_size = 0; -}; - -struct vcl_allocator_2 : vcl_allocator2_t { - vcl_allocator_2() : vcl_allocator2_t{allocate, deallocate} {} - - static uint8_t* allocate(vcl_allocator2_t* allocator, size_t size) { - vcl_allocator_2* vclAllocator = static_cast(allocator); - size_t alignedSize = intel_npu::utils::align_size_to_standard_page_size(size); - auto allocatedPtr = reinterpret_cast( - vclAllocator->m_allocator.allocate(alignedSize, intel_npu::utils::STANDARD_PAGE_SIZE)); - if (allocatedPtr == nullptr) { - OPENVINO_THROW("Failed to allocate aligned memory for allocator"); - } - memset(allocatedPtr + size, 0, alignedSize - size); - vclAllocator->m_info.emplace_back(std::make_pair(allocatedPtr, alignedSize)); - return allocatedPtr; - } - - static void deallocate(vcl_allocator2_t* allocator, uint8_t* ptr) { - if (ptr == nullptr) { - OPENVINO_THROW("Pointer is nullptr in deallocate!"); - } - vcl_allocator_2* vclAllocator = static_cast(allocator); - // 1 is the placeholder value, as size is not needed in deallocate - vclAllocator->m_allocator.deallocate(ptr, 1, intel_npu::utils::STANDARD_PAGE_SIZE); - } - ov::Allocator m_allocator; - std::vector> m_info; -}; - -ov::Tensor make_tensor_from_aligned_addr(uint8_t* allocated, size_t size) { - ov::Allocator allocator; - auto tensor = ov::Tensor(ov::element::u8, ov::Shape{size}, allocated); - auto impl = ov::get_tensor_impl(std::move(tensor)); - std::shared_ptr ptr(allocated, [allocator = std::move(allocator), size](uint8_t* p) mutable { - if (p == nullptr) { - OPENVINO_THROW("Pointer is nullptr in memory deallocation of make_tensor_from_aligned_addr!"); - } - allocator.deallocate(p, size, intel_npu::utils::STANDARD_PAGE_SIZE); - }); - impl._so = std::move(ptr); - return ov::make_tensor(impl); -} - } // namespace namespace intel_npu { @@ -162,24 +94,13 @@ static inline std::string getLatestVCLLog(vcl_log_handle_t logHandle) { } \ } -const std::shared_ptr VCLCompilerImpl::getInstance() { - static std::mutex mutex; - static std::weak_ptr weak_compiler; - - std::lock_guard lock(mutex); - auto compiler = weak_compiler.lock(); - if (!compiler) { - compiler = std::make_shared(); - weak_compiler = compiler; - } - return compiler; -} - -VCLCompilerImpl::VCLCompilerImpl() : _logHandle(nullptr), _logger("VCLCompilerImpl", Logger::global().level()) { +VCLCompilerImpl::VCLCompilerImpl(const std::string& library_dir) + : _logHandle(nullptr), + _logger("VCLCompilerImpl", Logger::global().level()) { _logger.debug("VCLCompilerImpl constructor start"); // Load VCL library - (void)VCLApi::getInstance(); + (void)VCLApi::getInstance(library_dir); // Initialize the VCL API THROW_ON_FAIL_FOR_VCL("vclGetVersion", vclGetVersion(&_vclVersion, &_vclProfilingVersion), nullptr); @@ -208,10 +129,6 @@ VCLCompilerImpl::VCLCompilerImpl() : _logHandle(nullptr), _logger("VCLCompilerIm // info will be processed in compile phase if passed by user. _logger.info("Device description is not provided, using default values"); uint32_t defaultTileCount = std::numeric_limits::max(); - if (_vclVersion.major == 7 && _vclVersion.minor < 6) { - // For vcl <= 7.5, need to use smaller value to pass check - defaultTileCount = std::numeric_limits::max(); - } vcl_device_desc_t device_desc = {sizeof(vcl_device_desc_t), 0x00, std::numeric_limits::max(), @@ -251,13 +168,16 @@ std::shared_ptr VCLCompilerImpl::getLinkedLibrary() const { return VCLApi::getInstance()->getLibrary(); } -ov::Tensor VCLCompilerImpl::compile(const std::shared_ptr& model, const FilteredConfig& config) const { +std::pair> VCLCompilerImpl::compile( + const std::shared_ptr& model, + const FilteredConfig& config) const { return compile(model, config, false); } -ov::Tensor VCLCompilerImpl::compile(const std::shared_ptr& model, - const FilteredConfig& config, - const bool storeWeightlessCacheAttributeFlag) const { +std::pair> VCLCompilerImpl::compile( + const std::shared_ptr& model, + const FilteredConfig& config, + const bool storeWeightlessCacheAttributeFlag) const { _logger.debug("compile start"); /// Check the linked vcl version whether supported in plugin @@ -306,38 +226,75 @@ ov::Tensor VCLCompilerImpl::compile(const std::shared_ptr& mode buildFlags.c_str(), buildFlags.size()}; - if (usedVersion.Major >= 7 && usedVersion.Minor >= 4) { - // support the lastest vcl api - // For VCL 7.4 and later, we can use vclAllocatedExecutableCreate2 - _logger.debug("Using vclAllocatedExecutableCreate2 for 7.4 <= VCL"); - vcl_allocator allocator; + if (usedVersion.Major > 7 || (usedVersion.Major == 7 && usedVersion.Minor >= 7)) { + // Support only the lastest VCL api + auto allocator = std::make_shared(); uint8_t* blob = nullptr; - size_t size = 0; - - auto result = vclAllocatedExecutableCreate2(_compilerHandle, exeDesc, &allocator, &blob, &size); + size_t blobSize = 0; + uint8_t* compatibilityStringBuffer = nullptr; + size_t compatibilityStringSize = 0; + + auto result = vclAllocatedExecutableCreate3(_compilerHandle, + exeDesc, + allocator.get(), + &blob, + &blobSize, + &compatibilityStringBuffer, + &compatibilityStringSize); if (result != VCL_RESULT_SUCCESS) { - OPENVINO_THROW("Compilation failed. vclAllocatedExecutableCreate2 result: 0x", + // Check if allocations were performed before throwing exception + auto tracked_allocations = allocator->m_info; + for (const auto& [buffer, size] : tracked_allocations) { + allocator->deallocate(allocator.get(), buffer); + } + OPENVINO_THROW("Compilation failed. vclAllocatedExecutableCreate3 result: 0x", std::hex, uint64_t(result), " - ", getLatestVCLLog(_logHandle)); } - if (size == 0 || blob == nullptr) { - OPENVINO_THROW("Failed to create VCL executable, size is zero or blob is null"); - } + OPENVINO_ASSERT(blobSize != 0 && blob != nullptr, + "Failed to create VCL executable, the blob size is zero or the blob is null"); + + // Retrieve the real allocated size for the blob from the allocator + auto it = std::find_if(allocator->m_info.begin(), + allocator->m_info.end(), + [blob](const std::pair& item) { + return item.first == blob; + }); + + OPENVINO_ASSERT(it != allocator->m_info.end(), "Failed to find the allocated blob in the allocator records"); + size_t alignedBlobSize = it->second; + // The allocated size from VCL will be equal or smaller than the allocated size in allocator - _logger.debug("Blob size from VCL: %zu ptr %p", size, static_cast(blob)); - _logger.debug("Allocated vector size: %zu ptr: %p", - allocator.m_size, - static_cast(allocator.m_allocated)); + _logger.debug("Blob size from VCL: %zu ptr %p", blobSize, static_cast(blob)); + _logger.debug("Allocated vector size: %zu ptr: %p", alignedBlobSize, static_cast(blob)); + + ov::Tensor alignedBlob = make_tensor_from_aligned_addr(blob, alignedBlobSize, allocator); + allocator->m_info.erase(it); - _logger.debug("compile end, blob size:%d", allocator.m_size); - return make_tensor_from_aligned_addr(allocator.m_allocated, allocator.m_size); + std::optional compatibilityString; + + // Populate compatibility string only when VCL provides a buffer. + if (compatibilityStringBuffer != nullptr) { + OPENVINO_ASSERT(compatibilityStringSize != 0, + "Failed to create VCL executable, the compatibility descriptor size is zero"); + compatibilityString = + std::string(reinterpret_cast(compatibilityStringBuffer), compatibilityStringSize); + _logger.debug("Compatibility string from VCL: %s", compatibilityString->c_str()); + + allocator->deallocate(allocator.get(), compatibilityStringBuffer); + } + + return std::make_pair>(std::move(alignedBlob), + std::move(compatibilityString)); } else { - OPENVINO_THROW("Not supported VCL version: %d.%d, please use VCL 6.1 or later", + OPENVINO_THROW("Unsupported VCL version: ", _vclVersion.major, - _vclVersion.minor); + ".", + _vclVersion.minor, + ", please use VCL 7.7 or later"); } } @@ -392,20 +349,23 @@ std::vector VCLCompilerImpl::compileWsOneShot(const std::shared_ptr< _logger.debug("compiler vcl version: %d.%d", _vclVersion.major, _vclVersion.minor); _logger.debug("Using vclAllocatedExecutableCreateWSOneShot"); - vcl_allocator_2 allocator; + auto allocator = std::make_shared(); THROW_ON_FAIL_FOR_VCL("vclAllocatedExecutableCreateWSOneShot", - vclAllocatedExecutableCreateWSOneShot(_compilerHandle, exeDesc, &allocator), + vclAllocatedExecutableCreateWSOneShot(_compilerHandle, exeDesc, allocator.get()), _logHandle); - if (allocator.m_info.size() == 0) { + if (allocator->m_info.size() == 0) { OPENVINO_THROW("Failed to create VCL executable, blobCount is zero"); } std::vector initMainTensors; - for (auto& blob : allocator.m_info) { - initMainTensors.emplace_back(make_tensor_from_aligned_addr(blob.first, blob.second)); + for (const auto& blob : allocator->m_info) { + initMainTensors.emplace_back(make_tensor_from_aligned_addr(blob.first, blob.second, allocator)); } + // Clean up m_info, delegating actual physical frees strictly to the Tensor/Deleter from now on. + allocator->m_info.clear(); + return initMainTensors; } @@ -415,7 +375,8 @@ ov::Tensor VCLCompilerImpl::compileWsIterative(const std::shared_ptr& _logger.debug("compileWsIterative start"); FilteredConfig updatedConfig = config; updatedConfig.update({{ov::intel_npu::ws_compile_call_number.name(), std::to_string(callNumber)}}); - return compile(model, updatedConfig, true); + // The compatibility descriptor is not supported in this case + return compile(model, updatedConfig, true).first; } std::vector VCLCompilerImpl::process_profiling_output(const std::vector& profData, @@ -579,4 +540,39 @@ bool VCLCompilerImpl::is_option_supported(std::string option, std::optional(static_cast(Logger::global().level()) + 1); + + // Create a temporary compiler instance for the query to pass the correct device description. + // The private compiler instance used by this class was created with default device description. + vcl_log_handle_t logHandle = nullptr; + vcl_compiler_handle_t compilerHandle = nullptr; + try { + THROW_ON_FAIL_FOR_VCL("vclCompilerCreate", + vclCompilerCreate(&compilerDesc, in_device_desc, &compilerHandle, &logHandle), + nullptr); + + const char* optname_ch = ov::compatibility_check.name(); + const char* optvalue_ch = compatibilityDescriptor.c_str(); + _logger.debug("is_option_supported start for option: %s, value: %s", optname_ch, optvalue_ch); + THROW_ON_FAIL_FOR_VCL("vclGetCompilerIsOptionSupported", + vclGetCompilerIsOptionSupported(compilerHandle, optname_ch, optvalue_ch), + logHandle); + if (compilerHandle) { + vclCompilerDestroy(compilerHandle); + } + return true; + } catch (const std::exception& e) { + _logger.debug("Exception in is_option_supported: %s", e.what()); + if (compilerHandle) { + vclCompilerDestroy(compilerHandle); + } + } + + return false; +} + } // namespace intel_npu diff --git a/src/plugins/intel_npu/src/compiler_adapter/src/driver_compiler_adapter.cpp b/src/plugins/intel_npu/src/compiler_adapter/src/driver_compiler_adapter.cpp index a606bdb6bf92..9b9c36da2b07 100644 --- a/src/plugins/intel_npu/src/compiler_adapter/src/driver_compiler_adapter.cpp +++ b/src/plugins/intel_npu/src/compiler_adapter/src/driver_compiler_adapter.cpp @@ -82,7 +82,10 @@ std::shared_ptr DriverCompilerAdapter::compile(const std::shared_ptr().empty() || updatedConfig.get(); - auto graphDesc = _zeGraphExt->getGraphDescriptor(std::move(serializedIR), buildFlags, bypassCache); + // If blob encryption is requested, enable secure compilation in the driver + const bool secureCompile = updatedConfig.has(CACHE_ENCRYPTION_CALLBACKS::key().data()) && + updatedConfig.get().encrypt != nullptr; + auto graphDesc = _zeGraphExt->getGraphDescriptor(std::move(serializedIR), buildFlags, bypassCache, secureCompile); _logger.debug("compile end"); OV_ITT_TASK_NEXT(COMPILE_BLOB, "getNetworkMeta"); @@ -94,7 +97,8 @@ std::shared_ptr DriverCompilerAdapter::compile(const std::shared_ptr DriverCompilerAdapter::compileWS(std::shared_ptr&& model, @@ -287,6 +291,29 @@ std::optional> DriverCompilerAdapter::get_supported_opt } bool DriverCompilerAdapter::is_option_supported(std::string optName, std::optional optValue) const { + // This is a special case, as RUNTIME_REQUIREMENTS is a read-only runtime property + // used to signal that compiler can provide a compatibility string through a dedicated + // VCL compiler method. It is not a regular settable option. + // Therefore, we cannot rely on the compiler's usual option support checking method alone. + if (optName == RUNTIME_REQUIREMENTS::key()) { + if (optValue.has_value()) + OPENVINO_THROW("The option '", + RUNTIME_REQUIREMENTS::key(), + "' is a read-only property and does not accept any value."); + + // Compatibility string generation is not yet supported through the L0 API, even if compiler supports it + return false; + } + // The COMPATIBILITY_CHECK option is used to signal if compiler adapter supports + // the validateCompatibilityDescriptor method + if (optName == COMPATIBILITY_CHECK::key()) { + if (optValue.has_value()) + OPENVINO_THROW("Compatibility string should be verified with validate_compatibility_descriptor()"); + + // Compatibility string validation is not yet supported through the L0 API + return false; + } + auto isOptionSupported = _zeGraphExt->isOptionSupported(std::move(optName), std::move(optValue)); return isOptionSupported.value_or(false); } @@ -311,4 +338,8 @@ bool DriverCompilerAdapter::isCompilerOptionSupported(const FilteredConfig& conf (compilerVersion.minor >= minorCompilerOptSupportValue)); } +bool DriverCompilerAdapter::validate_compatibility_descriptor(const std::string& compatibilityDescriptor) const { + OPENVINO_THROW_NOT_IMPLEMENTED("Compatibility descriptor validation is not yet supported through the L0 API"); +} + } // namespace intel_npu diff --git a/src/plugins/intel_npu/src/compiler_adapter/src/dynamic_graph.cpp b/src/plugins/intel_npu/src/compiler_adapter/src/dynamic_graph.cpp index 817666f24d64..65ddf32522a3 100644 --- a/src/plugins/intel_npu/src/compiler_adapter/src/dynamic_graph.cpp +++ b/src/plugins/intel_npu/src/compiler_adapter/src/dynamic_graph.cpp @@ -73,23 +73,25 @@ void DynamicGraphImpl::initialize(std::optional& blob, NetworkMetada _binding._inputs.resize(metadata.inputs.size()); - // dump output of _metadata - _logger.debug("Dump metadata info from blob"); - _logger.debug("Metadata inputs: %d", metadata.inputs.size()); - for (const auto& input : metadata.inputs) { - _logger.debug("Input compiler name: %s input node name: %s shapeFromCompiler: %s shapeFromIRModel: %s", - input.nameFromCompiler.c_str(), - input.nodeFriendlyName.c_str(), - input.shapeFromCompiler.to_string().c_str(), - input.shapeFromIRModel.has_value() ? input.shapeFromIRModel->to_string().c_str() : "N/A"); - } - _logger.debug("Metadata outputs: %d", metadata.outputs.size()); - for (const auto& output : metadata.outputs) { - _logger.debug("Output compiler name: %s output node name: %s shapeFromCompiler: %s shapeFromIRModel: %s", - output.nameFromCompiler.c_str(), - output.nodeFriendlyName.c_str(), - output.shapeFromCompiler.to_string().c_str(), - output.shapeFromIRModel.has_value() ? output.shapeFromIRModel->to_string().c_str() : "N/A"); + if (_logger.level() >= ov::log::Level::DEBUG) { + // dump output of _metadata + _logger.debug("Dump metadata info from blob"); + _logger.debug("Metadata inputs: %d", metadata.inputs.size()); + for (const auto& input : metadata.inputs) { + _logger.debug("Input compiler name: %s input node name: %s shapeFromCompiler: %s shapeFromIRModel: %s", + input.nameFromCompiler.c_str(), + input.nodeFriendlyName.c_str(), + input.shapeFromCompiler.to_string().c_str(), + input.shapeFromIRModel.has_value() ? input.shapeFromIRModel->to_string().c_str() : "N/A"); + } + _logger.debug("Metadata outputs: %d", metadata.outputs.size()); + for (const auto& output : metadata.outputs) { + _logger.debug("Output compiler name: %s output node name: %s shapeFromCompiler: %s shapeFromIRModel: %s", + output.nameFromCompiler.c_str(), + output.nodeFriendlyName.c_str(), + output.shapeFromCompiler.to_string().c_str(), + output.shapeFromIRModel.has_value() ? output.shapeFromIRModel->to_string().c_str() : "N/A"); + } } auto& inputs = _binding._inputs; @@ -281,20 +283,16 @@ void DynamicGraphImpl::setArgumentValueWithStrides(uint32_t argi, if (argi < inputs.size()) { _logger.debug("setArgumentValueWithStrides for index %d (input %d)", argi, argi); inputs[argi].setArg(argv); - - for (int64_t i = 0; i < inputs[argi]._dimsCount; i++) { - inputs[argi]._strides[i] = strides[i]; - } + // The passed strides are based on element + inputs[argi].setStrides(strides); } else { auto& outputs = _binding._outputs; auto idx = argi - inputs.size(); _logger.debug("setArgumentValueWithStrides for index %d (output %d)", argi, idx); if (idx < outputs.size()) { outputs[idx].setArg(argv); - - for (int64_t i = 0; i < outputs[idx]._dimsCount; i++) { - outputs[idx]._strides[i] = strides[i]; - } + // The passed strides are based on elemnt + outputs[idx].setStrides(strides); } } } @@ -306,12 +304,13 @@ void DynamicGraphImpl::executeGraph(const std::shared_ptr ze_fence_handle_t fence, ze_event_handle_t event, ze_graph_profiling_pool_handle_t profiling) { + _logger.debug("Start to execute graph with runtime engine"); std::shared_ptr argsImpl = args._impl ? std::static_pointer_cast(args._impl) : std::make_shared(); + bool noTensorChange = true; npu_vm_runtime_execute_params_t* params = &argsImpl->_executeParams; - for (auto& in : args._inputs) { std::shared_ptr inImpl = std::static_pointer_cast(in._impl); @@ -322,6 +321,8 @@ void DynamicGraphImpl::executeGraph(const std::shared_ptr inImpl->UpdateMemRefHandleStatus(in); if (args._impl == nullptr) { argsImpl->_inputMemRefs.push_back(inImpl->_memRef); + } else if (inImpl->_ptrUpdated || inImpl->_shapeUpdated || inImpl->_strideUpdated) { + noTensorChange = false; } } for (auto& out : args._outputs) { @@ -334,6 +335,37 @@ void DynamicGraphImpl::executeGraph(const std::shared_ptr outImpl->UpdateMemRefHandleStatus(out); if (args._impl == nullptr) { argsImpl->_outputMemRefs.push_back(outImpl->_memRef); + } else if (outImpl->_ptrUpdated || outImpl->_shapeUpdated || outImpl->_strideUpdated) { + noTensorChange = false; + } + } + + if (args._impl == nullptr || !noTensorChange) { + _logger.debug("Reset command list to run with runtime"); + // Reset commandLists since there are tensor with new shapes or it is the first execution, can not reuse command + // list with update + for (auto& cmdList : commandLists) { + zeCommandListReset(cmdList); + } + } else { + _logger.debug("Reuse command list without update since no tensor change detected"); + + auto result = zeCommandQueueExecuteCommandLists(commandQueue, + static_cast(commandLists.size()), + commandLists.data(), + fence); + if (result != ZE_RESULT_SUCCESS) { + OPENVINO_THROW("Failed to submit command lists"); + } + return; + } + + // Prepare execution context for each graph arguments + if (params->executionContext == nullptr) { + if (npuVMRuntimeCreateExecutionContext(_engine, ¶ms->executionContext) != NPU_VM_RUNTIME_RESULT_SUCCESS) { + OPENVINO_THROW("Failed to create a VM execution context"); + } else { + _logger.debug("Execution context is created successfully."); } } @@ -350,6 +382,7 @@ void DynamicGraphImpl::executeGraph(const std::shared_ptr params->inferenceFence = fence; params->event = event; + _logger.debug("Execute graph with runtime engine"); if (npuVMRuntimeExecute(_engine, params) != NPU_VM_RUNTIME_RESULT_SUCCESS) { OPENVINO_THROW("Failed to execute VM runtime engine"); } @@ -419,6 +452,7 @@ DynamicGraph::DynamicGraph(const std::shared_ptr& zeroIni } _impl = std::make_unique(); + // TODO: metadata needs to be parsed even when CREATE_EXECUTOR is 0 or DEFER_WEIGHTS_LOAD is YES, keep here to // support pure compilation without vm runtime initialize VM execution engine, metadata, input&output // descriptors @@ -464,9 +498,7 @@ std::pair>> DynamicGraph::export_b result = ((result << 7) + result) + static_cast(*it); } - std::stringstream str; - str << "Blob size: " << blobSize << ", hash: " << std::hex << result; - _logger.info(str.str().c_str()); + _logger.info("Blob size: %zu, hash: %x", blobSize, result); } size_t size = utils::align_size_to_standard_page_size(blobSize); @@ -477,7 +509,7 @@ std::pair>> DynamicGraph::export_b _logger.error("Write padding to stream failed. Blob is broken!"); return std::make_pair(0, std::nullopt); } - _logger.info("Blob size with padding: %ld", size); + _logger.info("Blob size with padding: %zu", size); } _logger.info("Write blob to stream successfully."); return std::make_pair(size, std::nullopt); @@ -503,6 +535,16 @@ void DynamicGraph::set_workload_type(const ov::WorkloadType workloadType) { std::lock_guard lock(_commandQueueDescMutex); auto zeWorkloadType = zeroUtils::toZeQueueWorkloadType(workloadType); + + if (_commandQueue && zeWorkloadType.has_value()) { + // When shared common queue is disabled, workload type is set per command queue. + // Update the existing queue if it has already been created. + _commandQueue->setWorkloadType(zeWorkloadType.value()); + _workloadType = workloadType; + + return; + } + if (_commandQueueDesc.workload() == zeWorkloadType) { return; } @@ -520,6 +562,18 @@ void DynamicGraph::set_model_priority(const ov::hint::Priority modelPriority) { return; } _commandQueueDesc.set_priority(zeModelPriority); + + if (_commandQueue) { + // When shared common queue is disabled, workload type is set per command queue. + // Recreate the queue with the new priority while preserving the current workload type. + if (_workloadType.has_value()) { + auto zeWorkloadType = zeroUtils::toZeQueueWorkloadType(_workloadType.value()); + _commandQueueDesc.set_workload(zeWorkloadType); + _workloadType = std::nullopt; // Clear the cached workload type after applying it to the new queue + } + + _commandQueue = ZeroCmdQueuePool::getInstance().getCommandQueue(_zeroInitStruct, _commandQueueDesc); + } } void DynamicGraph::set_argument_value(uint32_t argi, const void* argv) const { @@ -586,6 +640,11 @@ void DynamicGraph::initialize_impl(const FilteredConfig& config) { commandQueueOptions, this, config.get()}; + + if (config.get() == false) { + // Keep it alive per compiled model when the shared common queue feature is disabled. + _commandQueue = ZeroCmdQueuePool::getInstance().getCommandQueue(_zeroInitStruct, _commandQueueDesc); + } } _logger.debug("Graph initialize finish"); @@ -722,4 +781,9 @@ std::optional DynamicGraph::is_profiling_blob() const { return std::nullopt; } +std::optional DynamicGraph::get_compatibility_descriptor() const { + _logger.warning("Compatibility descriptor is not supported for DynamicGraph"); + return std::nullopt; +} + } // namespace intel_npu diff --git a/src/plugins/intel_npu/src/compiler_adapter/src/graph.cpp b/src/plugins/intel_npu/src/compiler_adapter/src/graph.cpp index a03d8421bc85..1fe9c0d31f9e 100644 --- a/src/plugins/intel_npu/src/compiler_adapter/src/graph.cpp +++ b/src/plugins/intel_npu/src/compiler_adapter/src/graph.cpp @@ -13,6 +13,7 @@ #include "intel_npu/utils/zero/zero_cmd_queue_pool.hpp" #include "intel_npu/utils/zero/zero_utils.hpp" #include "openvino/runtime/make_tensor.hpp" +#include "openvino/util/file_util.hpp" namespace intel_npu { @@ -22,6 +23,7 @@ Graph::Graph(const std::shared_ptr& zeGraphExt, NetworkMetadata metadata, std::optional blob, const FilteredConfig& config, + const std::optional& compatibilityDescriptor, const bool blobIsPersistent, const bool calledFromWeightlessGraph) : IGraph(), @@ -30,6 +32,7 @@ Graph::Graph(const std::shared_ptr& zeGraphExt, _graphDesc(graphDesc), _metadata(std::move(metadata)), _blob(std::move(blob)), + _compatibilityDescriptor(compatibilityDescriptor), _blobIsPersistent(blobIsPersistent), _logger("Graph", config.get()) { if (!config.get() || config.get()) { @@ -63,6 +66,16 @@ void Graph::set_workload_type(const ov::WorkloadType workloadType) { std::lock_guard lock(_commandQueueDescMutex); auto zeWorkloadType = zeroUtils::toZeQueueWorkloadType(workloadType); + + if (_commandQueue && zeWorkloadType.has_value()) { + // When shared common queue is disabled, workload type is set per command queue. + // Update the existing queue if it has already been created. + _commandQueue->setWorkloadType(zeWorkloadType.value()); + _workloadType = workloadType; + + return; + } + if (_commandQueueDesc.workload() == zeWorkloadType) { return; } @@ -80,6 +93,18 @@ void Graph::set_model_priority(const ov::hint::Priority modelPriority) { return; } _commandQueueDesc.set_priority(zeModelPriority); + + if (_commandQueue) { + // When shared common queue is disabled, workload type is set per command queue. + // Recreate the queue with the new priority while preserving the current workload type. + if (_workloadType.has_value()) { + auto zeWorkloadType = zeroUtils::toZeQueueWorkloadType(_workloadType.value()); + _commandQueueDesc.set_workload(zeWorkloadType); + _workloadType = std::nullopt; // Clear the cached workload type after applying it to the new queue + } + + _commandQueue = ZeroCmdQueuePool::getInstance().getCommandQueue(_zeroInitStruct, _commandQueueDesc); + } } ze_graph_handle_t Graph::get_handle() const { @@ -119,10 +144,7 @@ std::pair>> Graph::export_blob(std for (const uint8_t* it = blobPtr; it != blobPtr + blobSize; ++it) { result = ((result << 7) + result) + static_cast(*it); } - - std::stringstream str; - str << "Blob size: " << blobSize << ", hash: " << std::hex << result; - _logger.info(str.str().c_str()); + _logger.info("Blob size: %zu, hash: %x", blobSize, result); } size_t size = utils::align_size_to_standard_page_size(blobSize); @@ -135,7 +157,7 @@ std::pair>> Graph::export_blob(std return std::make_pair(0, std::nullopt); } - _logger.info("Blob size with padding: %ld", size); + _logger.info("Blob size with padding: %zu", size); } _logger.info("Write blob to stream successfully."); @@ -143,7 +165,8 @@ std::pair>> Graph::export_blob(std } std::vector Graph::process_profiling_output(const std::vector& profData) const { - auto compiler = VCLCompilerImpl::getInstance(); + auto ov_lib_path = ov::util::path_to_string(ov::util::get_ov_lib_path()); + auto compiler = std::make_shared(ov_lib_path); OPENVINO_ASSERT(compiler != nullptr, "Profiling post-processing requires the NPU plugin compiler library"); std::vector blob(_blob->get_byte_size()); @@ -196,6 +219,11 @@ void Graph::initialize_impl(const FilteredConfig& config) { this, config.get(), }; + + if (config.get() == false) { + // Keep it alive per compiled model when the shared common queue feature is disabled. + _commandQueue = ZeroCmdQueuePool::getInstance().getCommandQueue(_zeroInitStruct, _commandQueueDesc); + } } _zeGraphExt->initializeGraph(_graphDesc); @@ -269,6 +297,10 @@ uint32_t Graph::get_last_submitted_id() const { return _lastSubmittedId; } +std::optional Graph::get_compatibility_descriptor() const { + return _compatibilityDescriptor; +} + std::optional Graph::is_profiling_blob() const { if (_zeroInitStruct->getGraphDdiTable().version() < ZE_MAKE_VERSION(1, 16)) { _logger.debug("Cannot determine if the blob was compiled for profiling"); diff --git a/src/plugins/intel_npu/src/compiler_adapter/src/parser.cpp b/src/plugins/intel_npu/src/compiler_adapter/src/parser.cpp index c1f221367d7c..702592747ac5 100644 --- a/src/plugins/intel_npu/src/compiler_adapter/src/parser.cpp +++ b/src/plugins/intel_npu/src/compiler_adapter/src/parser.cpp @@ -8,6 +8,7 @@ #include "graph.hpp" #include "intel_npu/common/itt.hpp" #include "intel_npu/config/options.hpp" +#include "intel_npu/utils/vm/npu_vm_runtime_api.hpp" #include "weightless_graph.hpp" namespace intel_npu { @@ -26,7 +27,8 @@ Parser::Parser(const std::shared_ptr& zeroInitStruct) std::shared_ptr Parser::parse(const ov::Tensor& mainBlob, const FilteredConfig& config, const std::optional>& initBlobs, - std::optional>&& model) const { + std::optional>&& model, + const std::optional& compatibilityDescriptor) const { OV_ITT_TASK_CHAIN(PARSE_BLOB, itt::domains::NPUPlugin, "Parser", "parse"); // Detect blob format @@ -38,8 +40,9 @@ std::shared_ptr Parser::parse(const ov::Tensor& mainBlob, } else { header.assign(static_cast(data), size); } - if (header.find("llvm") != std::string::npos) { - _logger.debug("Create graph for LLVM IR, use internal function to get metadata!"); + if (header.find("llvm") != std::string::npos || header.find("NPUByte\x00") != std::string::npos) { + _logger.debug("Create graph for dynamic blob, use internal function to get metadata!"); + NPUVMRuntimeApi::initializeFromBlob(data, size); return std::make_shared(_zeroInitStruct, mainBlob, true, config); } @@ -72,6 +75,7 @@ std::shared_ptr Parser::parse(const ov::Tensor& mainBlob, std::move(mainNetworkMetadata), mainBlob, config, + compatibilityDescriptor, blobIsPersistent); } diff --git a/src/plugins/intel_npu/src/compiler_adapter/src/plugin_compiler_adapter.cpp b/src/plugins/intel_npu/src/compiler_adapter/src/plugin_compiler_adapter.cpp index 1578ee10e1b9..7a25b614f656 100644 --- a/src/plugins/intel_npu/src/compiler_adapter/src/plugin_compiler_adapter.cpp +++ b/src/plugins/intel_npu/src/compiler_adapter/src/plugin_compiler_adapter.cpp @@ -15,6 +15,7 @@ #include "intel_npu/npu_private_properties.hpp" #include "intel_npu/utils/logger/logger.hpp" #include "intel_npu/utils/utils.hpp" +#include "intel_npu/utils/vm/npu_vm_runtime_api.hpp" #include "intel_npu/utils/zero/zero_api.hpp" #include "intel_npu/utils/zero/zero_result.hpp" #include "mem_usage.hpp" @@ -34,7 +35,8 @@ PluginCompilerAdapter::PluginCompilerAdapter(const std::shared_ptr(ov_lib_path); OPENVINO_ASSERT(vclCompilerPtr != nullptr, "VCL compiler is nullptr"); auto vclLib = vclCompilerPtr->getLinkedLibrary(); _logger.info("PLUGIN VCL compiler is loading"); @@ -64,10 +66,12 @@ std::shared_ptr PluginCompilerAdapter::compile(const std::shared_ptrcompile(model, config); + auto [tensor, compatibilityDescriptor] = _compiler->compile(model, config); _logger.debug("compile end"); - if (config.get() == "HostCompile") { + if (config.get().find("HostCompile") == 0) { + NPUVMRuntimeApi::initializeFromBlob(tensor.data(), tensor.get_byte_size()); + // metadata will be obtained in initialze() of DynamicGraph _logger.debug("Use dynamicGraph to hold blob for HostCompile mode!"); return std::make_shared(_zeroInitStruct, std::move(tensor), true, config); @@ -99,6 +103,7 @@ std::shared_ptr PluginCompilerAdapter::compile(const std::shared_ptrgetDevice()) { + ze_device_properties_t device_properties = {}; + device_properties.stype = ZE_STRUCTURE_TYPE_DEVICE_PROPERTIES; + auto result = zeDeviceGetProperties(_zeroInitStruct->getDevice(), &device_properties); + + if (result == ZE_RESULT_SUCCESS) { + vcl_device_desc_t vcl_desc = {sizeof(vcl_device_desc_t), + device_properties.deviceId, + static_cast(device_properties.subdeviceId), + device_properties.numSlices}; + + _logger.info("Validating compatibility logic using deviceID: 0x%X, maxTiles: %u", + vcl_desc.deviceID, + vcl_desc.tileCount); + + return _compiler->validate_compatibility_descriptor(compatibilityDescriptor, &vcl_desc); + } + } + + return false; +} + } // namespace intel_npu diff --git a/src/plugins/intel_npu/src/compiler_adapter/src/weightless_graph.cpp b/src/plugins/intel_npu/src/compiler_adapter/src/weightless_graph.cpp index ca6a73f7d4dc..1e46fc409b2d 100644 --- a/src/plugins/intel_npu/src/compiler_adapter/src/weightless_graph.cpp +++ b/src/plugins/intel_npu/src/compiler_adapter/src/weightless_graph.cpp @@ -4,6 +4,7 @@ #include "weightless_graph.hpp" +#include #include #include #include @@ -176,8 +177,9 @@ WeightlessGraph::WeightlessGraph(const std::shared_ptr& zeGr std::move(mainMetadata), std::move(mainBlob), config, + /* compatibilityDescriptor = */ std::nullopt, blobIsPersistent, - true), + /* calledFromWeightlessGraph = */ true), _initsGraphDesc(initGraphDesc), _initBlobs(std::move(initBlobs)), _initsMetadata(std::move(initMetadata)), @@ -235,13 +237,11 @@ std::pair>> WeightlessGraph::expor totalResult += result; - std::stringstream str; if (blobIndex == MAIN_SCHEDULE_INDEX) { - str << "Main blob size " << blobSize << ", hash " << std::hex << result; + _wgLogger.info("Main blob size: %" PRIu64 ", hash: %x", blobSize, result); } else { - str << "Init part " << blobIndex << " blob size " << blobSize << ", hash " << std::hex << result; + _wgLogger.info("Init part %zu blob size %" PRIu64 ", hash: %x", blobIndex, blobSize, result); } - _wgLogger.info(str.str().c_str()); } size_t size = utils::align_size_to_standard_page_size(blobSize); @@ -254,7 +254,7 @@ std::pair>> WeightlessGraph::expor return 0; } - _wgLogger.info("Blob size with padding: %ld", size); + _wgLogger.info("Blob size with padding: %zu", size); } return size; @@ -277,9 +277,7 @@ std::pair>> WeightlessGraph::expor ++blobIndex; } - std::stringstream str; - str << "Blob size: " << totalBlobSize << ", hash: " << std::hex << totalResult; - _wgLogger.info(str.str().c_str()); + _wgLogger.info("Blob size: %" PRIu64 ", hash: %x", totalBlobSize, totalResult); _wgLogger.info("Write blob to stream successfully."); return std::make_pair(totalBlobSize, initSizes); @@ -595,19 +593,25 @@ void WeightlessGraph::release_init_blob(const size_t initIndex) { } void WeightlessGraph::release_graphs() { - size_t initIndex = 0; if (_zeGraphExt != nullptr) { for (auto& initGraphDesc : _initsGraphDesc) { - _zeGraphExt->destroyGraph(initGraphDesc); + if (initGraphDesc._handle) { + _zeGraphExt->destroyGraph(initGraphDesc); + } + } + + _wgLogger.debug("Init graphs are destroyed"); + } - if (!_blobIsPersistent && _initBlobs != std::nullopt && _initBlobs->at(initIndex)) { + if (!_blobIsPersistent && _initBlobs != std::nullopt) { + for (size_t initIndex = 0; initIndex < _initBlobs->size(); ++initIndex) { + if (_initBlobs->at(initIndex)) { _initBlobs->at(initIndex) = ov::Tensor(); } - - initIndex++; } + + _wgLogger.debug("Init blobs are released"); } - _wgLogger.debug("Init graphs are destroyed"); } WeightlessGraph::~WeightlessGraph() { diff --git a/src/plugins/intel_npu/src/compiler_adapter/src/ze_graph_ext_wrappers.cpp b/src/plugins/intel_npu/src/compiler_adapter/src/ze_graph_ext_wrappers.cpp index 90b57d95b225..a408bbd07498 100644 --- a/src/plugins/intel_npu/src/compiler_adapter/src/ze_graph_ext_wrappers.cpp +++ b/src/plugins/intel_npu/src/compiler_adapter/src/ze_graph_ext_wrappers.cpp @@ -153,17 +153,20 @@ ZeGraphExtWrappers::~ZeGraphExtWrappers() { } void ZeGraphExtWrappers::destroyGraph(GraphDescriptor& graphDescriptor) { - if (graphDescriptor._handle) { - _logger.debug("destroyGraph - perform pfnDestroy"); - - auto result = _zeroInitStruct->getGraphDdiTable().pfnDestroy(graphDescriptor._handle); - if (ZE_RESULT_SUCCESS != result) { - _logger.error("failed to destroy graph handle. L0 pfnDestroy result: %s, code %#X", - ze_result_to_string(result).c_str(), - uint64_t(result)); - } else { - graphDescriptor._handle = nullptr; - } + if (_zeroInitStruct == nullptr || _zeroInitStruct->getContext() == nullptr || graphDescriptor._handle == nullptr) { + _logger.warning("Context or graph is null while trying to destroy graph. Graph might be already destroyed."); + graphDescriptor._handle = nullptr; + return; + } + + _logger.debug("destroyGraph - perform pfnDestroy"); + auto result = _zeroInitStruct->getGraphDdiTable().pfnDestroy(graphDescriptor._handle); + if (ZE_RESULT_SUCCESS == result) { + graphDescriptor._handle = nullptr; + } else { + _logger.error("failed to destroy graph handle. L0 pfnDestroy result: %s, code %#X", + ze_result_to_string(result).c_str(), + uint64_t(result)); } } @@ -381,7 +384,8 @@ bool ZeGraphExtWrappers::canCpuVaBeImported(const void* data, size_t size) const GraphDescriptor ZeGraphExtWrappers::getGraphDescriptor(SerializedIR serializedIR, const std::string& buildFlags, - const bool bypassUmdCache) const { + const bool bypassUmdCache, + const bool secureCompile) const { ze_graph_handle_t graphHandle = nullptr; void* pNext = nullptr; ze_graph_input_hash_t modelHash; @@ -395,6 +399,14 @@ GraphDescriptor ZeGraphExtWrappers::getGraphDescriptor(SerializedIR serializedIR _logger.debug("getGraphDescriptor - set ZE_GRAPH_FLAG_DISABLE_CACHING"); flags |= ZE_GRAPH_FLAG_DISABLE_CACHING; } + if (secureCompile) { + if (_graphExtVersion < ZE_MAKE_VERSION(1, 17)) { + OPENVINO_THROW("Secure compilation was requested, but the current driver version does not support it."); + } else { + _logger.debug("getGraphDescriptor - set ZE_GRAPH_FLAG_SECURE_COMPILE"); + flags |= ZE_GRAPH_FLAG_SECURE_COMPILE; + } + } ze_graph_desc_2_t desc = {ZE_STRUCTURE_TYPE_GRAPH_DESC_2, pNext, diff --git a/src/plugins/intel_npu/src/plugin/include/async_infer_request.hpp b/src/plugins/intel_npu/src/plugin/include/async_infer_request.hpp index c999c336e17f..f3aa57990d60 100644 --- a/src/plugins/intel_npu/src/plugin/include/async_infer_request.hpp +++ b/src/plugins/intel_npu/src/plugin/include/async_infer_request.hpp @@ -16,22 +16,20 @@ class AsyncInferRequest final : public ov::IAsyncInferRequest { public: explicit AsyncInferRequest(const std::shared_ptr& inferRequest, const std::shared_ptr& requestExecutor, - const std::shared_ptr& getResultExecutor, + const std::shared_ptr& resultExecutor, const std::shared_ptr& callbackExecutor); AsyncInferRequest(const AsyncInferRequest&) = delete; AsyncInferRequest& operator=(const AsyncInferRequest&) = delete; - ~AsyncInferRequest(); + void cancel() override; - std::shared_ptr get_sync_infer_request() { - return _syncInferRequest; - } + ~AsyncInferRequest(); private: - std::shared_ptr _syncInferRequest; - std::shared_ptr _getResultExecutor; + std::shared_ptr _inferRequest; + std::shared_ptr _resultExecutor; }; } // namespace intel_npu diff --git a/src/plugins/intel_npu/src/plugin/include/compiled_model.hpp b/src/plugins/intel_npu/src/plugin/include/compiled_model.hpp index cb3f3173c191..62e5c06d5afc 100644 --- a/src/plugins/intel_npu/src/plugin/include/compiled_model.hpp +++ b/src/plugins/intel_npu/src/plugin/include/compiled_model.hpp @@ -4,6 +4,7 @@ #pragma once +#include #include #include "intel_npu/common/icompiled_model.hpp" @@ -25,9 +26,8 @@ class CompiledModel final : public ICompiledModel { * @param plugin Pointer towards the NPU plugin instance * @param device Backend specific object through which inference requests can be created * @param graph Object holding the graph handle along with distinct fields for metadata - * @param profiling Flag indicating if profiling was requested. Setting this to "true" will lead to storing the - * "compiler" parameter inside the newly created "CompiledModel". * @param config Custom configuration object + * @param batchSize Optional batch size value. */ CompiledModel(const std::shared_ptr& model, const std::shared_ptr& plugin, @@ -40,7 +40,7 @@ class CompiledModel final : public ICompiledModel { CompiledModel& operator=(const CompiledModel&) = delete; - ~CompiledModel() override; + ~CompiledModel() override = default; std::shared_ptr create_infer_request() const override; @@ -61,16 +61,18 @@ class CompiledModel final : public ICompiledModel { void release_memory() override; private: + // For special config, stream executors must be set accordingly to ensure correct behavior. void configure_stream_executors(); Logger _logger; - const std::shared_ptr _device; - std::shared_ptr _resultExecutor; + const std::shared_ptr _device; std::unique_ptr _propertiesManager; - std::shared_ptr _graph; + std::shared_ptr _resultExecutor = nullptr; + mutable std::once_flag _streamExecutorsInitFlag; + std::optional _batchSize; }; diff --git a/src/plugins/intel_npu/src/plugin/include/executor.hpp b/src/plugins/intel_npu/src/plugin/include/executor.hpp new file mode 100644 index 000000000000..3124afa75b12 --- /dev/null +++ b/src/plugins/intel_npu/src/plugin/include/executor.hpp @@ -0,0 +1,30 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#pragma once + +#include +#include +#include + +#include "openvino/runtime/threading/itask_executor.hpp" + +namespace intel_npu { + +/** + * @brief Creates a task executor with fixed-size or adaptive worker behavior. + * + * @param name Base thread name prefix used for worker threads. + * @param workers Baseline number of workers to keep active. + * @param allowWorkerGrowth If true, allows creating additional workers on demand. + * @param idleTimeout Idle timeout after which extra adaptive workers can stop. + * @return Shared pointer to the created task executor. + */ +std::shared_ptr make_executor( + std::string_view name, + size_t workers, + bool allowWorkerGrowth = false, + std::chrono::milliseconds idleTimeout = std::chrono::milliseconds{30'000}); + +} // namespace intel_npu diff --git a/src/plugins/intel_npu/src/plugin/include/metadata.hpp b/src/plugins/intel_npu/src/plugin/include/metadata.hpp index cd125566ee3a..6b21959ae862 100644 --- a/src/plugins/intel_npu/src/plugin/include/metadata.hpp +++ b/src/plugins/intel_npu/src/plugin/include/metadata.hpp @@ -4,10 +4,13 @@ #pragma once +#include #include +#include #include #include #include +#include #include #include @@ -17,7 +20,6 @@ #include "openvino/runtime/tensor.hpp" namespace intel_npu { - class MetadataBase { public: MetadataBase(uint32_t version, uint64_t blobDataSize); @@ -36,13 +38,29 @@ class MetadataBase { */ void read(const ov::Tensor& tensor); + /** + * @brief Populates this object from a pre-parsed human-readable metadata attribute map. + */ + void read_as_text(std::map> attrs); + virtual void read() = 0; + /** + * @note Layouts and encryption are intentionally omitted from the human-readable compatibility + * string. They are internal implementation details that don't affect cross-version compatibility and would only add + * noise for consumers. + * Compiler version is already contained within the compiler requirements field. + * + */ + virtual void read_as_text() = 0; + /** * @brief Writes metadata to a stream. */ virtual void write(std::ostream& stream) = 0; + virtual void write_as_text(std::ostream& stream) = 0; + virtual uint64_t get_blob_size() const; /** @@ -50,21 +68,25 @@ class MetadataBase { */ virtual std::optional> get_init_sizes() const; - virtual std::optional> get_input_layouts() const; - - virtual std::optional> get_output_layouts() const; - /** * @returns Batch size. Populated in case of plugin batching. */ virtual std::optional get_batch_size() const; + virtual std::optional> get_input_layouts() const; + + virtual std::optional> get_output_layouts() const; + + virtual std::optional get_compiler_version() const; + + virtual std::optional is_encrypted_blob() const; + + virtual std::optional get_compatibility_descriptor() const; + virtual ~MetadataBase() = default; static std::streampos getFileSize(std::istream& stream); - virtual size_t get_metadata_size() const = 0; - /** * @brief Returns a uint32_t value which represents two uint16_t values concatenated. * @details Convention for bumping the metadata version: @@ -95,7 +117,8 @@ class MetadataBase { protected: /** - * @brief Reads data from the source containing the metadata. The implementation depends on the type of source. + * @brief Reads data from the source containing the binary metadata. The implementation depends on the type of + * source. */ void read_data_from_source(char* destination, const size_t size); @@ -106,12 +129,17 @@ class MetadataBase { * @note This operation was detached from "write" since "write" writes at the beginning of the stream, while this * method writes at the end. This change allows better extension of class hierarchy. */ - void append_padding_blob_size_and_magic(std::ostream& stream); + void append_blob_size_and_magic(std::ostream& stream); uint32_t _version; uint64_t _blobDataSize; Logger _logger; + /** + * @brief Parsed key-value attributes from the human-readable metadata string. + */ + std::map> _textAttrs; + /** * @brief Where the metadata is read from. The type can be a stream, an OpenVINO tensor or "uninitialized_source". * @details Stored as attribute in order to avoid repeatedly passing the same arguments to some methods. @@ -130,6 +158,26 @@ class MetadataBase { */ constexpr std::string_view MAGIC_BYTES = "OVNPU"; +/** + * @brief Keys used in the metadata text format. + */ +namespace MetadataTextKeys { +constexpr std::string_view META = "meta"; +constexpr std::string_view OV = "ov"; +constexpr std::string_view WS_INITS = "ws_inits"; +constexpr std::string_view BATCH = "batch"; +constexpr std::string_view COMPAT_DESC = "desc"; +} // namespace MetadataTextKeys + +/** + * @brief List of known attributes in the human-readable metadata format. + */ +inline constexpr std::array metadataTextAttributes = {MetadataTextKeys::META, + MetadataTextKeys::OV, + MetadataTextKeys::WS_INITS, + MetadataTextKeys::BATCH, + MetadataTextKeys::COMPAT_DESC}; + /** * @brief List of supported version formats. */ @@ -137,11 +185,14 @@ constexpr uint32_t METADATA_VERSION_2_0{MetadataBase::make_version(2, 0)}; constexpr uint32_t METADATA_VERSION_2_1{MetadataBase::make_version(2, 1)}; constexpr uint32_t METADATA_VERSION_2_2{MetadataBase::make_version(2, 2)}; constexpr uint32_t METADATA_VERSION_2_3{MetadataBase::make_version(2, 3)}; +constexpr uint32_t METADATA_VERSION_2_4{MetadataBase::make_version(2, 4)}; +constexpr uint32_t METADATA_VERSION_2_5{MetadataBase::make_version(2, 5)}; +constexpr uint32_t METADATA_VERSION_2_6{MetadataBase::make_version(2, 6)}; /** * @brief Current metadata version. */ -constexpr uint32_t CURRENT_METADATA_VERSION{METADATA_VERSION_2_3}; +constexpr uint32_t CURRENT_METADATA_VERSION{METADATA_VERSION_2_6}; constexpr uint16_t CURRENT_METADATA_MAJOR_VERSION{MetadataBase::get_major(CURRENT_METADATA_VERSION)}; constexpr uint16_t CURRENT_METADATA_MINOR_VERSION{MetadataBase::get_minor(CURRENT_METADATA_VERSION)}; @@ -218,6 +269,8 @@ class Metadata : public MetadataBase { void read() override; + void read_as_text() override; + /** * @attention It's a must to first write metadata version in any metadata specialization. * @@ -227,7 +280,7 @@ class Metadata : public MetadataBase { */ void write(std::ostream& stream) override; - size_t get_metadata_size() const override; + void write_as_text(std::ostream& stream) override; protected: OpenvinoVersion _ovVersion; @@ -235,6 +288,8 @@ class Metadata : public MetadataBase { /** * @brief The version that adds support for init schedules (weights separation). + * + * @note The text format defines WS enablement as a boolean flag; actual sizes are not preserved */ template <> class Metadata : public Metadata { @@ -249,15 +304,17 @@ class Metadata : public Metadata { */ void read() override; + void read_as_text() override; + /** * @details The number of init schedules, along with the size of each init binary object are written in addition to * the information registered by the previous metadata versions. */ void write(std::ostream& stream) override; - std::optional> get_init_sizes() const override; + void write_as_text(std::ostream& stream) override; - size_t get_metadata_size() const override; + std::optional> get_init_sizes() const override; private: std::optional> _initSizes; @@ -272,16 +329,18 @@ class Metadata : public Metadata { public: Metadata(uint64_t blobSize, std::optional ovVersion = std::nullopt, - const std::optional> initSizes = std::nullopt, - const std::optional batchSize = std::nullopt); + const std::optional>& initSizes = std::nullopt, + const std::optional& batchSize = std::nullopt); void read() override; + void read_as_text() override; + void write(std::ostream& stream) override; - std::optional get_batch_size() const override; + void write_as_text(std::ostream& stream) override; - size_t get_metadata_size() const override; + std::optional get_batch_size() const override; private: std::optional _batchSize; @@ -297,7 +356,7 @@ class Metadata : public Metadata { Metadata(uint64_t blobSize, const std::optional& ovVersion = std::nullopt, const std::optional>& initSizes = std::nullopt, - const std::optional batchSize = std::nullopt, + const std::optional& batchSize = std::nullopt, const std::optional>& inputLayouts = std::nullopt, const std::optional>& outputLayouts = std::nullopt); @@ -305,8 +364,6 @@ class Metadata : public Metadata { void write(std::ostream& stream) override; - size_t get_metadata_size() const override; - std::optional> get_input_layouts() const override; std::optional> get_output_layouts() const override; @@ -316,6 +373,86 @@ class Metadata : public Metadata { std::optional> _outputLayouts; }; +/** + * @brief Stores the compiler version. + */ +template <> +class Metadata : public Metadata { +public: + Metadata(uint64_t blobSize, + const std::optional& ovVersion = std::nullopt, + const std::optional>& initSizes = std::nullopt, + const std::optional& batchSize = std::nullopt, + const std::optional>& inputLayouts = std::nullopt, + const std::optional>& outputLayouts = std::nullopt, + const std::optional& compilerVersion = std::nullopt); + + void read() override; + + void write(std::ostream& stream) override; + + std::optional get_compiler_version() const override; + +private: + std::optional _compilerVersion; +}; + +/** + * @brief Checks whether raw blob is encrypted or not. + * @details Ignores unencrypted main blob size and stores the size after encryption instead if it is not nullopt. + */ +template <> +class Metadata : public Metadata { +public: + Metadata(uint64_t blobSize, + const std::optional& ovVersion = std::nullopt, + const std::optional>& initSizes = std::nullopt, + const std::optional& batchSize = std::nullopt, + const std::optional>& inputLayouts = std::nullopt, + const std::optional>& outputLayouts = std::nullopt, + const std::optional& compilerVersion = std::nullopt, + const std::optional& blobSizeAfterEncryption = std::nullopt); + + void read() override; + + void write(std::ostream& stream) override; + + std::optional is_encrypted_blob() const override; + +private: + std::optional _isEncryptedBlob; +}; + +/** + * @brief Stores the compiler requirements string. + */ +template <> +class Metadata : public Metadata { +public: + Metadata(uint64_t blobSize, + const std::optional& ovVersion = std::nullopt, + const std::optional>& initSizes = std::nullopt, + const std::optional batchSize = std::nullopt, + const std::optional>& inputLayouts = std::nullopt, + const std::optional>& outputLayouts = std::nullopt, + const std::optional compilerVersion = std::nullopt, + const std::optional& blobSizeAfterEncryption = std::nullopt, + const std::optional compatibilityDescriptor = std::nullopt); + + void read() override; + + void read_as_text() override; + + void write(std::ostream& stream) override; + + void write_as_text(std::ostream& stream) override; + + std::optional get_compatibility_descriptor() const override; + +private: + std::optional _compatibilityDescriptor; +}; + /** * @brief Creates a Metadata object. * @@ -343,4 +480,6 @@ std::unique_ptr read_metadata_from(std::istream& stream); */ std::unique_ptr read_metadata_from(const ov::Tensor& tensor); +std::unique_ptr read_as_text(std::string_view input); + } // namespace intel_npu diff --git a/src/plugins/intel_npu/src/plugin/include/metrics.hpp b/src/plugins/intel_npu/src/plugin/include/metrics.hpp index dae6f6c85db7..61a784deca8b 100644 --- a/src/plugins/intel_npu/src/plugin/include/metrics.hpp +++ b/src/plugins/intel_npu/src/plugin/include/metrics.hpp @@ -48,6 +48,7 @@ class Metrics final { const ov::SoPtr _backend; std::vector _supportedMetrics; std::vector _supportedConfigKeys; + static constexpr uint32_t _maxNumOfOptimalInferRequests = 8u; const std::vector _optimizationCapabilities = { ov::device::capability::FP16, ov::device::capability::INT8, @@ -55,10 +56,10 @@ class Metrics final { }; // Metric to provide a hint for a range for number of async infer requests. (bottom bound, upper bound, step) - const std::tuple _rangeForAsyncInferRequests{1u, 10u, 1u}; + const std::tuple _rangeForAsyncInferRequests{1u, _maxNumOfOptimalInferRequests, 1u}; // Metric to provide information about a range for streams.(bottom bound, upper bound) - const std::tuple _rangeForStreams{1u, 4u}; + const std::tuple _rangeForStreams{0u, _maxNumOfOptimalInferRequests}; std::string getDeviceName(const std::string& specifiedDeviceName) const; std::shared_ptr getDevice(const std::string& specifiedDeviceName) const; diff --git a/src/plugins/intel_npu/src/plugin/include/plugin.hpp b/src/plugins/intel_npu/src/plugin/include/plugin.hpp index a960232186fe..07eb6ba26a29 100644 --- a/src/plugins/intel_npu/src/plugin/include/plugin.hpp +++ b/src/plugins/intel_npu/src/plugin/include/plugin.hpp @@ -80,6 +80,9 @@ class Plugin : public ov::IPlugin { std::unique_ptr metadata, const ov::AnyMap& properties) const; + ov::CompatibilityCheck validate_compatibility_descriptor(ov::intel_npu::CompilerType compilerType, + const ov::AnyMap& arguments) const; + std::unique_ptr _backendsRegistry; // _backend might not be set by the plugin; certain actions, such as offline compilation, might be supported. diff --git a/src/plugins/intel_npu/src/plugin/include/properties.hpp b/src/plugins/intel_npu/src/plugin/include/properties.hpp index 90f015f5f5a9..500e90a10a97 100644 --- a/src/plugins/intel_npu/src/plugin/include/properties.hpp +++ b/src/plugins/intel_npu/src/plugin/include/properties.hpp @@ -73,6 +73,7 @@ class Properties final { std::string determinePlatform(const ov::AnyMap& properties) const; std::string determineDeviceId(const ov::AnyMap& properties) const; ov::intel_npu::CompilerType determineCompilerType(const ov::AnyMap& properties) const; + ov::intel_npu::CompilerType determineCompilerTypeForCompatibilityCheck() const; private: struct CopyState { @@ -84,6 +85,7 @@ class Properties final { ov::intel_npu::CompilerType currentlyUsedCompiler; std::string currentlyUsedPlatform; bool compilerConfigsFilteredByCompiler; + bool compatibilityCheckFiltered; std::map>> properties; std::vector supportedProperties; @@ -98,15 +100,23 @@ class Properties final { Logger _logger; ov::intel_npu::CompilerType _currentlyUsedCompiler = ov::intel_npu::CompilerType::PREFER_PLUGIN; + ov::intel_npu::CompilerType _compilerForCompatibilityCheck = ov::intel_npu::CompilerType::DRIVER; std::string _currentlyUsedPlatform; - bool _compilerConfigsFilteredByCompiler = - false; ///< Boolean to check whether properties was filtered with compiler supported properties + // Boolean to check whether properties were filtered with compiler supported properties + bool _compilerConfigsFilteredByCompiler = false; + // Boolean to signal that compatibility check was already filtered by compiler support + bool _compatibilityCheckFiltered = false; // properties map: {name -> [supported, mutable, eval function]} std::map>> _properties; std::vector _supportedProperties; + // The compatibility_check property is supported only in case at least one of the compilers (CID or CIP) supports it + // To avoid loading the compiler library and check the support when the property is registered, the check can + // be performed at a later stage, when the property is actually queried. + bool disable_compatibility_check_if_needed(); + /** * @brief Checks whether a property was registered by its name */ diff --git a/src/plugins/intel_npu/src/plugin/npuw/attn/attn_subgraph.cpp b/src/plugins/intel_npu/src/plugin/npuw/attn/attn_subgraph.cpp new file mode 100644 index 000000000000..a533a111c5c3 --- /dev/null +++ b/src/plugins/intel_npu/src/plugin/npuw/attn/attn_subgraph.cpp @@ -0,0 +1,1187 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "attn_subgraph.hpp" + +#include +#include +#include + +#include "../attention.hpp" +#include "../compiled_model.hpp" +#include "../host_flash_attention.hpp" +#include "../infer_request_utils.hpp" +#include "../just_sync_infer_request.hpp" +#include "../logging.hpp" +#include "../partitioning/partitioning.hpp" +#include "../partitioning/patterns/sdpa.hpp" +#include "../pyramid_attention.hpp" +#include "../serialization.hpp" + +namespace ov { +namespace npuw { +namespace attn { +namespace { + +constexpr uint32_t ATTN_KV_DIM = 3; + +struct BehaviorIO { + std::vector> inputs; + std::vector> outputs; +}; + +struct PyramidRequestSet { + std::vector> infer_requests; + std::vector> pipeline_requests; + std::vector> anchors; +}; + +struct HFARequestSet { + enum TileIdx : std::size_t { + REGULAR_TILE = 0, + FINAL_TILE = 1, + COUNT = 2, + }; + + std::array, COUNT> infer_requests{}; + std::array, COUNT> pipeline_requests{}; +}; + +struct RuntimeState { + std::unordered_map call_io; + runtime::attention::Selector::Ptr attention_selector; + runtime::pyramid_attention::Selector::Ptr pyramid_selector; + runtime::host_flash_attention::Selector::Ptr hfa_selector; + ov::SoPtr cached_attention_mask; + std::optional hfa_runtime_ctx; + PyramidRequestSet pyramid_requests; + HFARequestSet hfa_requests; + ov::SoPtr base_request; + ov::SoPtr base_pipeline_request; +}; + +ov::npuw::JustInferRequest& get_request(ov::npuw::v1::subgraphs::InferContext& ctx) { + auto* request = dynamic_cast(&ctx.infer_request); + OPENVINO_ASSERT(request != nullptr, "Expected JustInferRequest for attention runtime behavior"); + return *request; +} + +BehaviorKind get_behavior_kind(const ov::npuw::v1::subgraphs::Context& ctx) { + if (const auto* kind = ctx.get_if()) { + return *kind; + } + OPENVINO_THROW("Attention behavior context is missing BehaviorKind"); +} + +template +T* get_compiled_state(ov::npuw::v1::subgraphs::Context& context) { + auto* state = context.get_if>(); + return state == nullptr ? nullptr : state->get(); +} + +template +const T* get_compiled_state(const ov::npuw::v1::subgraphs::Context& context) { + auto* state = context.get_if>(); + return state == nullptr ? nullptr : state->get(); +} + +const ov::npuw::v1::subgraphs::CompiledPipeline& get_subgraph_pipeline(ov::npuw::v1::subgraphs::InferContext& ctx, + std::size_t real_idx) { + return get_request(ctx).subgraph_pipeline(real_idx); +} + +const ov::SoPtr& get_compiled_submodel(ov::npuw::v1::subgraphs::InferContext& ctx, + std::size_t real_idx) { + return get_request(ctx).compiled_submodel(real_idx); +} + +std::size_t get_param_base(ov::npuw::v1::subgraphs::InferContext& ctx, std::size_t real_idx) { + return get_request(ctx).subgraph_param_base(real_idx); +} + +RuntimeState& get_runtime_state(ov::npuw::v1::subgraphs::InferContext& ctx) { + OPENVINO_ASSERT(ctx.runtime_state != nullptr, "Expected runtime state storage for attention behavior"); + auto* state = ctx.runtime_state->get_if(); + if (state == nullptr) { + state = &ctx.runtime_state->emplace(); + } + return *state; +} + +BehaviorIO& get_behavior_io(RuntimeState& state, + std::size_t subgraph_idx, + std::size_t num_inputs, + std::size_t num_outputs) { + auto& io = state.call_io[subgraph_idx]; + if (io.inputs.size() != num_inputs) { + io.inputs.resize(num_inputs); + } + if (io.outputs.size() != num_outputs) { + io.outputs.resize(num_outputs); + } + return io; +} + +void ensure_base_requests(ov::npuw::v1::subgraphs::InferContext& ctx, RuntimeState& state) { + auto& request = get_request(ctx); + if (!state.base_request) { + state.base_request = request.get_subrequest(ctx.real_subgraph_idx); + } + if (request.is_subrequest_pipelined(ctx.real_subgraph_idx) && !state.base_pipeline_request) { + state.base_pipeline_request = request.get_pipeline_subrequest(ctx.real_subgraph_idx); + } +} + +void ensure_dynamic_selector(ov::npuw::v1::subgraphs::InferContext& ctx, RuntimeState& state) { + if (state.attention_selector) { + return; + } + const auto& pipeline = get_subgraph_pipeline(ctx, ctx.real_subgraph_idx); + const auto* dynamic = ov::npuw::attn::get_compiled_dynamic(pipeline.context); + OPENVINO_ASSERT(dynamic != nullptr, "Missing compiled dynamic attention state"); + + auto& request = get_request(ctx); + if (!ctx.compiled_model.attention_dynamic_enabled()) { + state.attention_selector = std::make_shared(); + return; + } + + state.attention_selector = runtime::attention::PositionIDs::find(*dynamic, request); + if (!state.attention_selector) { + LOG_WARN("Dynamic capability is enabled, but no run-time features were found."); + state.attention_selector = std::make_shared(); + } +} + +void ensure_pyramid_selector(ov::npuw::v1::subgraphs::InferContext& ctx, RuntimeState& state) { + if (state.pyramid_selector) { + return; + } + const auto& pipeline = get_subgraph_pipeline(ctx, ctx.real_subgraph_idx); + const auto* pyramid = ov::npuw::attn::get_compiled_pyramid(pipeline.context); + OPENVINO_ASSERT(pyramid != nullptr, "Missing compiled pyramid attention state"); + + auto& request = get_request(ctx); + const auto pyramid_count = pyramid->_compiled_models.size(); + if (!ctx.compiled_model.attention_dynamic_enabled()) { + state.pyramid_selector.reset(new runtime::pyramid_attention::All(pyramid_count)); + return; + } + + state.pyramid_selector = runtime::pyramid_attention::PositionIDs::find(*pyramid, request); + if (!state.pyramid_selector) { + LOG_WARN("Pyramid dynamic capability is enabled, but no run-time features were found."); + state.pyramid_selector.reset(new runtime::pyramid_attention::All(pyramid_count)); + } +} + +void ensure_hfa_selector(ov::npuw::v1::subgraphs::InferContext& ctx, RuntimeState& state) { + if (state.hfa_selector) { + return; + } + const auto& pipeline = get_subgraph_pipeline(ctx, ctx.real_subgraph_idx); + const auto* hfa = ov::npuw::attn::get_compiled_hfa(pipeline.context); + OPENVINO_ASSERT(hfa != nullptr, "Missing compiled HFA state"); + + auto& request = get_request(ctx); + const size_t query_size = hfa->_sdpa_attention_info._query_size; + state.hfa_selector = runtime::host_flash_attention::PositionIDs::find(query_size, request); + if (!state.hfa_selector) { + OPENVINO_THROW("HFA dynamic capability is enabled, but no run-time features were found."); + } +} + +void ensure_pyramid_requests(ov::npuw::v1::subgraphs::InferContext& ctx, RuntimeState& state) { + if (!state.pyramid_requests.infer_requests.empty()) { + return; + } + + ensure_base_requests(ctx, state); + + const auto& compiled_model = get_compiled_submodel(ctx, ctx.real_subgraph_idx); + const auto& pipeline = get_subgraph_pipeline(ctx, ctx.real_subgraph_idx); + const auto* pyramid = ov::npuw::attn::get_compiled_pyramid(pipeline.context); + OPENVINO_ASSERT(pyramid != nullptr, "Missing compiled pyramid attention state"); + + auto& request = get_request(ctx); + const auto& pyramid_models = pyramid->_compiled_models; + const size_t num_pyramid_models = pyramid_models.size(); + const bool is_piped = request.is_subrequest_pipelined(ctx.real_subgraph_idx); + + state.pyramid_requests.infer_requests.resize(num_pyramid_models); + if (is_piped) { + state.pyramid_requests.pipeline_requests.resize(num_pyramid_models); + } + + for (size_t model_idx = 0; model_idx + 1 < num_pyramid_models; ++model_idx) { + state.pyramid_requests.infer_requests[model_idx] = pyramid_models[model_idx]->create_infer_request(); + if (is_piped) { + state.pyramid_requests.pipeline_requests[model_idx] = pyramid_models[model_idx]->create_infer_request(); + } + + const size_t num_inputs = pyramid_models[model_idx]->inputs().size(); + OPENVINO_ASSERT(num_inputs == compiled_model->inputs().size(), "Unexpected pyramid input count mismatch"); + + for (size_t input_idx = 0; input_idx < num_inputs; ++input_idx) { + const auto pyramid_input = pyramid_models[model_idx]->inputs()[input_idx]; + const auto main_input = compiled_model->inputs()[input_idx]; + + auto main_tensor_ptr = state.base_request->get_tensor(main_input)->data(); + auto pyramid_tensor = state.pyramid_requests.infer_requests[model_idx]->get_tensor(pyramid_input); + auto shared_tensor = ov::get_tensor_impl( + ov::Tensor(pyramid_tensor->get_element_type(), pyramid_tensor->get_shape(), main_tensor_ptr)); + state.pyramid_requests.infer_requests[model_idx]->set_tensor(pyramid_input, shared_tensor); + + if (is_piped) { + auto pipeline_tensor = state.pyramid_requests.pipeline_requests[model_idx]->get_tensor(pyramid_input); + auto pipeline_tensor_ptr = state.base_pipeline_request->get_tensor(main_input)->data(); + auto shared_pipeline_tensor = ov::get_tensor_impl( + ov::Tensor(pipeline_tensor->get_element_type(), pipeline_tensor->get_shape(), pipeline_tensor_ptr)); + state.pyramid_requests.pipeline_requests[model_idx]->set_tensor(pyramid_input, shared_pipeline_tensor); + } + } + } + + if (num_pyramid_models > 0) { + const size_t last_model_idx = num_pyramid_models - 1; + state.pyramid_requests.infer_requests[last_model_idx] = state.base_request; + if (is_piped) { + state.pyramid_requests.pipeline_requests[last_model_idx] = state.base_pipeline_request; + } + + const size_t num_inputs = compiled_model->inputs().size(); + for (size_t input_idx = 0; input_idx < num_inputs; ++input_idx) { + const auto main_input = compiled_model->inputs()[input_idx]; + state.pyramid_requests.anchors.push_back(state.base_request->get_tensor(main_input)); + if (is_piped) { + state.pyramid_requests.anchors.push_back(state.base_pipeline_request->get_tensor(main_input)); + } + } + } +} + +void ensure_hfa_requests(ov::npuw::v1::subgraphs::InferContext& ctx, RuntimeState& state) { + if (state.hfa_requests.infer_requests[HFARequestSet::REGULAR_TILE]) { + return; + } + + ensure_base_requests(ctx, state); + + const auto& pipeline = get_subgraph_pipeline(ctx, ctx.real_subgraph_idx); + const auto* hfa = ov::npuw::attn::get_compiled_hfa(pipeline.context); + OPENVINO_ASSERT(hfa != nullptr, "Missing compiled HFA state"); + + auto& request = get_request(ctx); + const bool is_piped = request.is_subrequest_pipelined(ctx.real_subgraph_idx); + + state.hfa_requests.infer_requests[HFARequestSet::REGULAR_TILE] = hfa->_compiled_tile_model->create_infer_request(); + state.hfa_requests.infer_requests[HFARequestSet::FINAL_TILE] = state.base_request; + if (is_piped) { + state.hfa_requests.pipeline_requests[HFARequestSet::REGULAR_TILE] = + hfa->_compiled_tile_model->create_infer_request(); + state.hfa_requests.pipeline_requests[HFARequestSet::FINAL_TILE] = state.base_pipeline_request; + } + + const size_t num_inputs = hfa->_compiled_tile_model->inputs().size(); + for (size_t input_idx = 0; input_idx < num_inputs; ++input_idx) { + const auto tile_input = hfa->_compiled_tile_model->inputs()[input_idx]; + const auto final_tile_input = hfa->_compiled_final_tile_model->inputs()[input_idx]; + + auto main_tensor = state.base_request->get_tensor(final_tile_input); + state.hfa_requests.infer_requests[HFARequestSet::REGULAR_TILE]->set_tensor(tile_input, main_tensor); + + if (is_piped) { + auto pipeline_tensor = state.base_pipeline_request->get_tensor(final_tile_input); + state.hfa_requests.pipeline_requests[HFARequestSet::REGULAR_TILE]->set_tensor(tile_input, pipeline_tensor); + } + } + + if (!state.hfa_runtime_ctx.has_value()) { + state.hfa_runtime_ctx.emplace(); + } + + state.hfa_runtime_ctx->initialize_mask_cache( + *hfa, + request.subgraph_device(ctx.real_subgraph_idx), + [&request](const ov::element::Type& dtype, const ov::Shape& shape, const std::string& device) { + return request.allocate_mem(dtype, shape, device); + }); + + const auto& tile_in = hfa->_sdpa_attention_info._tile_input_indices; + auto state_acc = state.hfa_requests.infer_requests[HFARequestSet::REGULAR_TILE]->get_tensor( + hfa->_compiled_tile_model->inputs()[tile_in.acc]); + auto state_max = state.hfa_requests.infer_requests[HFARequestSet::REGULAR_TILE]->get_tensor( + hfa->_compiled_tile_model->inputs()[tile_in.max]); + auto state_sum = state.hfa_requests.infer_requests[HFARequestSet::REGULAR_TILE]->get_tensor( + hfa->_compiled_tile_model->inputs()[tile_in.d]); + + runtime::host_flash_attention::HFARuntimeContext::initialize_state_tensors(state_acc, state_max, state_sum); + runtime::host_flash_attention::HFARuntimeContext::StateBuffers initial_buffers{state_acc, state_max, state_sum}; + state.hfa_runtime_ctx->initialize_state_buffers( + initial_buffers, + *hfa, + request.subgraph_device(ctx.real_subgraph_idx), + [&request](const ov::element::Type& dtype, const ov::Shape& shape, const std::string& device) { + return request.allocate_mem(dtype, shape, device); + }); +} + +void prepare_dynamic(ov::npuw::v1::subgraphs::InferContext& ctx) { + auto& state = get_runtime_state(ctx); + ensure_dynamic_selector(ctx, state); + state.attention_selector->prepare(get_request(ctx).history_size()); + state.cached_attention_mask = {}; +} + +void prepare_pyramid(ov::npuw::v1::subgraphs::InferContext& ctx) { + auto& request = get_request(ctx); + auto& state = get_runtime_state(ctx); + ensure_pyramid_selector(ctx, state); + ensure_pyramid_requests(ctx, state); + state.pyramid_selector->prepare(request.history_size()); + state.cached_attention_mask = {}; + + const auto pyramid_id = state.pyramid_selector->pyramid_id(); + request.set_active_subrequest(ctx.real_subgraph_idx, state.pyramid_requests.infer_requests.at(pyramid_id)); + if (request.is_subrequest_pipelined(ctx.real_subgraph_idx)) { + request.set_pipeline_subrequest(ctx.real_subgraph_idx, state.pyramid_requests.pipeline_requests.at(pyramid_id)); + } +} + +void prepare_hfa(ov::npuw::v1::subgraphs::InferContext& ctx) { + auto& request = get_request(ctx); + auto& state = get_runtime_state(ctx); + ensure_hfa_selector(ctx, state); + ensure_hfa_requests(ctx, state); + state.hfa_selector->prepare(request.history_size()); + state.cached_attention_mask = {}; + if (state.hfa_runtime_ctx) { + state.hfa_runtime_ctx->clear_mask_cache(); + } + request.set_active_subrequest(ctx.real_subgraph_idx, state.base_request); + if (request.is_subrequest_pipelined(ctx.real_subgraph_idx)) { + request.set_pipeline_subrequest(ctx.real_subgraph_idx, state.base_pipeline_request); + } +} + +void extract_and_copy_tile(const ov::SoPtr& source_tensor, + const ov::SoPtr& dest_tensor, + uint32_t sequence_dim, + int64_t sequence_offset, + int64_t sequence_length, + const std::string& tensor_name) { + if (!dest_tensor->is_continuous()) { + OPENVINO_THROW("HFA tile extraction error: destination tensor for '", + tensor_name, + "' is not continuous - cannot perform direct copy"); + } + + auto source_view = ov::npuw::util::view(source_tensor, sequence_dim, sequence_offset, sequence_length); + const auto dest_type = dest_tensor->get_element_type(); + const auto source_type = source_tensor->get_element_type(); + + if (dest_type == source_type) { + ov::npuw::util::copy_tensor_by_dim(source_view, dest_tensor, sequence_dim, sequence_dim); + return; + } + + LOG_WARN("Performing type conversion for " << tensor_name << " tile: " << source_type << " -> " << dest_type); + auto intermediate_tensor = ov::Tensor(source_type, source_view->get_shape()); + ov::npuw::util::copy_tensor_by_dim(source_view, + ov::get_tensor_impl(intermediate_tensor), + sequence_dim, + sequence_dim); + + const size_t total_elements = intermediate_tensor.get_size(); + if (dest_type == ov::element::f32 && source_type == ov::element::f16) { + auto src_data = intermediate_tensor.data(); + auto dst_data = dest_tensor->data(); + for (size_t i = 0; i < total_elements; ++i) { + dst_data[i] = static_cast(src_data[i]); + } + return; + } + if (dest_type == ov::element::f16 && source_type == ov::element::f32) { + auto src_data = intermediate_tensor.data(); + auto dst_data = dest_tensor->data(); + for (size_t i = 0; i < total_elements; ++i) { + dst_data[i] = static_cast(src_data[i]); + } + return; + } + OPENVINO_THROW("Unsupported type conversion for ", tensor_name, " tile: ", source_type, " -> ", dest_type); +} + +bool can_reuse_tensor_zero_copy(const ov::SoPtr& source_tensor, + const ov::SoPtr& dest_tensor, + uint32_t sequence_dim, + int64_t sequence_offset, + int64_t tile_length) { + const auto source_shape = source_tensor->get_shape(); + const int64_t source_full_length = static_cast(source_shape[sequence_dim]); + return (sequence_offset == 0 && tile_length == source_full_length && + dest_tensor->get_element_type() == source_tensor->get_element_type()); +} + +ov::npuw::v1::subgraphs::RuntimeBehaviorFactory make_runtime_factory() { + return [](const ov::npuw::v1::subgraphs::Context& ctx) -> ov::npuw::v1::subgraphs::ISubgraphBehavior::Ptr { + class AttnBehavior final : public ov::npuw::v1::subgraphs::ISubgraphBehavior { + public: + explicit AttnBehavior(BehaviorKind kind) : m_kind(kind) {} + + void prepare(ov::npuw::v1::subgraphs::InferContext& ctx) override { + switch (m_kind) { + case BehaviorKind::Dynamic: + prepare_dynamic(ctx); + return; + case BehaviorKind::Pyramid: + prepare_pyramid(ctx); + return; + case BehaviorKind::HFA: + prepare_hfa(ctx); + return; + } + OPENVINO_THROW("Unsupported attention behavior kind"); + } + + bool bind_function_input(ov::npuw::v1::subgraphs::InferContext& ctx, + std::size_t input_idx, + const ov::SoPtr& tensor) override { + auto& state = get_runtime_state(ctx); + const auto& compiled_model = get_compiled_submodel(ctx, ctx.real_subgraph_idx); + const auto& pipeline = get_subgraph_pipeline(ctx, ctx.real_subgraph_idx); + switch (m_kind) { + case BehaviorKind::Dynamic: + if (const auto* dynamic = ov::npuw::attn::get_compiled_dynamic(pipeline.context)) { + auto& io = + get_behavior_io(state, ctx.subgraph_idx, get_param_base(ctx, ctx.real_subgraph_idx), 0u); + ensure_dynamic_selector(ctx, state); + auto kv_param_it = + std::find_if(dynamic->params.begin(), dynamic->params.end(), [&](const auto& p) { + return p.idx == input_idx; + }); + const bool is_kv_param = (kv_param_it != dynamic->params.end()); + const bool is_mask = (input_idx == dynamic->mask_idx); + const auto& iport = compiled_model->inputs()[input_idx]; + if (is_mask) { + // Mask requires context-dependent construction — defer to prologue() + io.inputs.at(input_idx) = tensor; + } else if (is_kv_param) { + const auto& param = *kv_param_it; + const auto pos_id = state.attention_selector->length(); + if (pos_id == -1) { + // Fallback: dynamic range not identified — bind full tensor + ctx.target_request->set_tensor(iport, tensor); + } else { + const auto past_len = state.attention_selector->past_length(); + const auto& view = ov::npuw::util::view(tensor, param.dim, 0, past_len); + const auto& shape = view->get_shape(); + const bool do_copy = get_request(ctx).subgraph_needs_copy(ctx.subgraph_idx) && + !get_request(ctx).attention_no_copy(); + if (do_copy && ov::shape_size(shape) > 0) { + const auto& dst = ctx.target_request->get_tensor(iport); + dst->set_shape(shape); + view->copy_to(dst._ptr); + } else if (do_copy && ov::shape_size(shape) == 0) { + ctx.target_request->get_tensor(iport)->set_shape(shape); + } else { + ctx.target_request->set_tensor(iport, view); + } + } + } else { + // Non-KV, non-mask: bind directly + ctx.target_request->set_tensor(iport, tensor); + } + return true; + } + return false; + case BehaviorKind::Pyramid: + if (const auto* pyramid = ov::npuw::attn::get_compiled_pyramid(pipeline.context)) { + auto& io = + get_behavior_io(state, ctx.subgraph_idx, get_param_base(ctx, ctx.real_subgraph_idx), 0u); + ensure_pyramid_selector(ctx, state); + const auto pyramid_id = state.pyramid_selector->pyramid_id(); + const auto& info = pyramid->_attention_infos[pyramid_id]; + const bool is_mask = (input_idx == info.mask_idx); + auto param_it = std::find_if(info.params.begin(), info.params.end(), [&](const auto& p) { + return p.idx == input_idx; + }); + const bool is_kv_param = (param_it != info.params.end()); + const auto& iport = compiled_model->inputs()[input_idx]; + if (is_mask) { + // Mask requires context-dependent construction — defer to prologue() + io.inputs.at(input_idx) = tensor; + } else if (is_kv_param) { + const auto& param = *param_it; + using namespace ov::npuw::runtime; + // iport comes from the main compiled model (full KV shape). + // For set_tensor we must use the port from the pyramid model's compiled + // model so the zero backend shape check passes. For the last pyramid model + // _compiled_models[pyramid_id] == the main compiled model, so pyramid_iport + // == iport and there is no behavioural difference. + // get_tensor calls below intentionally keep iport: they look up by index / + // name without strict shape validation. + const auto& pyramid_iport = pyramid->_compiled_models[pyramid_id]->inputs()[input_idx]; + if (state.pyramid_selector->length() == -1) { + // Fallback: dynamic range not identified — bind directly + ctx.target_request->set_tensor(pyramid_iport, tensor); + } else { + // Pyramid dynamic range identified + const auto past_len = state.pyramid_selector->past_length(); + const auto this_case = state.pyramid_selector->this_case(); + + // Strided I/O is available for non-last pyramid models compiled with + // enable_strides_for. The last model reuses the main compiled subgraph (compiled + // without enable_strides_for), so it always falls back to the copy path. + const bool use_tensor_view = + pyramid->_can_use_tensor_view && (pyramid_id < pyramid->num_models() - 1); + + const auto& input_shape = tensor->get_shape(); + if (this_case == pyramid_attention::Selector::Case::PREFILL) { + if (static_cast(input_shape[param.dim]) == past_len) { + ctx.target_request->set_tensor(pyramid_iport, tensor); + } else { + const auto& view = ov::npuw::util::view(tensor, param.dim, 0, past_len); + const auto& shape = view->get_shape(); + if (ov::shape_size(shape) == 0) { + ctx.target_request->get_tensor(iport)->set_shape(shape); + } else if (use_tensor_view) { + const auto model_past_len = static_cast(info.context_length) - + static_cast(info.query_size); + LOG_DEBUG("Use tensor view: past_len=" << past_len << " model_past_len=" + << model_past_len); + ctx.target_request->set_tensor( + pyramid_iport, + ov::npuw::util::view(tensor, param.dim, 0, model_past_len)); + } else { + const auto& dst = ctx.target_request->get_tensor(iport); + ov::npuw::util::copy_tensor_by_dim(view, + dst, + static_cast(param.dim), + static_cast(param.dim)); + } + } + } else { + NPUW_ASSERT(this_case == pyramid_attention::Selector::Case::GENERATE); + NPUW_ASSERT(static_cast(input_shape[param.dim]) != past_len); + const auto& dst = ctx.target_request->get_tensor(iport); + if (dst->get_shape() == input_shape) { + ctx.target_request->set_tensor(pyramid_iport, tensor); + } else if (use_tensor_view) { + const auto model_past_len = static_cast(info.context_length) - + static_cast(info.query_size); + LOG_DEBUG("Use tensor view: past_len=" << past_len + << " model_past_len=" << model_past_len); + ctx.target_request->set_tensor( + pyramid_iport, + ov::npuw::util::view(tensor, param.dim, 0, model_past_len)); + } else { + const auto& view = ov::npuw::util::view(tensor, param.dim, 0, past_len); + const auto& dst_slice = ov::npuw::util::view(dst, param.dim, 0, past_len); + ov::npuw::util::copy_tensor_by_dim(view, + dst_slice, + static_cast(param.dim), + static_cast(param.dim)); + } + } + } + } else { + // Non-KV, non-mask: bind directly + ctx.target_request->set_tensor(iport, tensor); + } + return true; + } + return false; + case BehaviorKind::HFA: + if (const auto* hfa = ov::npuw::attn::get_compiled_hfa(pipeline.context)) { + auto& io = get_behavior_io(state, + ctx.subgraph_idx, + get_param_base(ctx, ctx.real_subgraph_idx), + hfa->_compiled_final_tile_model->outputs().size()); + io.inputs.at(input_idx) = tensor; + return true; + } + return false; + } + OPENVINO_THROW("Unsupported attention behavior kind"); + } + + bool bind_function_output(ov::npuw::v1::subgraphs::InferContext& ctx, + std::size_t output_idx, + const ov::SoPtr& tensor) override { + (void)ctx; + (void)output_idx; + (void)tensor; + return false; + } + + void prologue(ov::npuw::v1::subgraphs::InferContext& ctx) override { + if (ctx.opaque_prologue) { + ctx.opaque_prologue(); + } + auto& state = get_runtime_state(ctx); + const auto& compiled_model = get_compiled_submodel(ctx, ctx.real_subgraph_idx); + const auto& pipeline = get_subgraph_pipeline(ctx, ctx.real_subgraph_idx); + auto& io = get_behavior_io(state, ctx.subgraph_idx, get_param_base(ctx, ctx.real_subgraph_idx), 0u); + switch (m_kind) { + case BehaviorKind::Dynamic: + if (const auto* dynamic = ov::npuw::attn::get_compiled_dynamic(pipeline.context)) { + auto mask_iport = compiled_model->inputs()[dynamic->mask_idx]; + const auto& graph_mask = io.inputs.at(dynamic->mask_idx); + const auto this_case = state.attention_selector->this_case(); + auto pos_id = state.attention_selector->length(); + + if (pos_id == -1) { + ctx.target_request->set_tensor(mask_iport, graph_mask); + return; + } + + const auto past_len = state.attention_selector->past_length(); + const auto present_len = dynamic->query_size; + auto set_or_copy = [&](const ov::SoPtr& view) { + if (!get_request(ctx).subgraph_needs_copy(ctx.subgraph_idx)) { + ctx.target_request->set_tensor(mask_iport, view); + } else { + const auto& dst = ctx.target_request->get_tensor(mask_iport); + dst->set_shape(view->get_shape()); + view->copy_to(dst._ptr); + } + }; + + using namespace ov::npuw::runtime; + if (this_case == attention::Selector::Case::GENERATE) { + set_or_copy(ov::npuw::util::view(ov::get_tensor_impl(dynamic->attend_all), + ATTN_KV_DIM, + 0, + past_len + 1)); + return; + } + if (this_case == attention::Selector::Case::PREFILL) { + if (state.cached_attention_mask) { + ctx.target_request->set_tensor(mask_iport, state.cached_attention_mask); + return; + } + + auto full_mask_shape = graph_mask->get_shape(); + auto actual_mask_shape = full_mask_shape; + actual_mask_shape[ATTN_KV_DIM] = present_len + past_len; + + const auto& dst = ctx.target_request->get_tensor(mask_iport); + dst->set_shape(actual_mask_shape); + + const auto& present_dst_view = + ov::npuw::util::view(dst, ATTN_KV_DIM, past_len, present_len); + const auto& present_src_view = + ov::npuw::util::view(graph_mask, + ATTN_KV_DIM, + full_mask_shape[ATTN_KV_DIM] - present_len, + present_len); + present_src_view->copy_to(present_dst_view._ptr); + + if (past_len > 0) { + const auto& past_dst_view = ov::npuw::util::view(dst, ATTN_KV_DIM, 0, past_len); + const auto& past_src_view = ov::npuw::util::view(graph_mask, ATTN_KV_DIM, 0, past_len); + past_src_view->copy_to(past_dst_view._ptr); + } + state.cached_attention_mask = dst; + return; + } + } + return; + case BehaviorKind::Pyramid: + if (const auto* pyramid = ov::npuw::attn::get_compiled_pyramid(pipeline.context)) { + const auto pyramid_id = state.pyramid_selector->pyramid_id(); + const auto& dynamic = pyramid->_attention_infos[pyramid_id]; + auto mask_iport = pyramid->_compiled_models[pyramid_id]->inputs()[dynamic.mask_idx]; + const auto& graph_mask = io.inputs.at(dynamic.mask_idx); + const auto this_case = state.pyramid_selector->this_case(); + const auto present_len = dynamic.query_size; + const auto& dst = ctx.target_request->get_tensor(mask_iport); + + auto copy_mask_segment = [&](std::size_t dst_offset, + std::size_t src_offset, + std::size_t length) { + if (length == 0) { + return; + } + const auto& dst_view = ov::npuw::util::view(dst, ATTN_KV_DIM, dst_offset, length); + const auto& src_view = ov::npuw::util::view(graph_mask, ATTN_KV_DIM, src_offset, length); + ov::npuw::util::copy_tensor_by_dim(src_view, dst_view, ATTN_KV_DIM, ATTN_KV_DIM); + }; + + if (state.pyramid_selector->length() == -1) { + ctx.target_request->set_tensor(mask_iport, graph_mask); + return; + } + + const auto past_len = state.pyramid_selector->past_length(); + if (state.cached_attention_mask) { + ctx.target_request->set_tensor(mask_iport, state.cached_attention_mask); + return; + } + + const auto full_mask_shape = graph_mask->get_shape(); + using namespace ov::npuw::runtime; + if (this_case == pyramid_attention::Selector::Case::GENERATE) { + const auto dst_shape = dst->get_shape(); + if (dst_shape == full_mask_shape) { + ctx.target_request->set_tensor(mask_iport, graph_mask); + state.cached_attention_mask = graph_mask; + return; + } + + const std::size_t dst_present_offset = dst_shape[ATTN_KV_DIM] - present_len; + copy_mask_segment(dst_present_offset, + full_mask_shape[ATTN_KV_DIM] - present_len, + present_len); + copy_mask_segment(0, 0, dst_present_offset); + state.cached_attention_mask = dst; + return; + } + if (this_case == pyramid_attention::Selector::Case::PREFILL) { + copy_mask_segment(past_len, full_mask_shape[ATTN_KV_DIM] - present_len, present_len); + copy_mask_segment(0, 0, past_len); + state.cached_attention_mask = dst; + return; + } + OPENVINO_ASSERT(false, "Unsupported pyramid attention case"); + } + return; + case BehaviorKind::HFA: + return; + } + OPENVINO_THROW("Unsupported attention behavior kind"); + } + + void run(ov::npuw::v1::subgraphs::InferContext& ctx) override { + switch (m_kind) { + case BehaviorKind::Dynamic: + case BehaviorKind::Pyramid: + ctx.legacy_infer(); + return; + case BehaviorKind::HFA: + if (const auto* hfa_desc = ov::npuw::attn::get_compiled_hfa( + get_subgraph_pipeline(ctx, ctx.real_subgraph_idx).context)) { + auto& state = get_runtime_state(ctx); + auto& io = get_behavior_io(state, + ctx.subgraph_idx, + get_param_base(ctx, ctx.real_subgraph_idx), + hfa_desc->_compiled_final_tile_model->outputs().size()); + + OPENVINO_ASSERT(hfa_desc->is_valid(), "HFA configuration must be valid"); + const int64_t tile_size = hfa_desc->_tile_size; + const int64_t total_kv_length = state.hfa_selector->context_length(); + const int64_t num_tiles = total_kv_length / tile_size; + OPENVINO_ASSERT(total_kv_length % tile_size == 0, + "HFA total KV length must be multiple of tile size for now"); + + const auto& hfa_inputs = io.inputs; + const auto& sdpa_info = hfa_desc->_sdpa_attention_info; + const auto& sdpa_in = sdpa_info._sdpa_indices; + + auto past_key_tensor = hfa_inputs.at(sdpa_in.past_key); + auto past_value_tensor = hfa_inputs.at(sdpa_in.past_value); + auto query_tensor = hfa_inputs.at(sdpa_in.query); + auto present_key_tensor = hfa_inputs.at(sdpa_in.present_key); + auto attention_mask_tensor = hfa_inputs.at(sdpa_in.attention_mask); + auto present_value_tensor = hfa_inputs.at(sdpa_in.present_value); + auto& regular_tile_request = state.hfa_requests.infer_requests[HFARequestSet::REGULAR_TILE]; + auto& final_tile_request = state.hfa_requests.infer_requests[HFARequestSet::FINAL_TILE]; + auto attention_output_tensor = + final_tile_request->get_tensor(hfa_desc->_compiled_final_tile_model->outputs()[0]); + const auto& tile_in = sdpa_info._tile_input_indices; + const auto& tile_out = sdpa_info._tile_output_indices; + + ov::SoPtr state_acc, state_max, state_sum; + if (state.hfa_runtime_ctx && state.hfa_runtime_ctx->has_state_buffers()) { + const auto& current_buffer = state.hfa_runtime_ctx->get_current_state_buffers(); + state_acc = current_buffer.acc; + state_max = current_buffer.max; + state_sum = current_buffer.sum; + regular_tile_request->set_tensor(hfa_desc->_compiled_tile_model->inputs()[tile_in.acc], + state_acc); + regular_tile_request->set_tensor(hfa_desc->_compiled_tile_model->inputs()[tile_in.max], + state_max); + regular_tile_request->set_tensor(hfa_desc->_compiled_tile_model->inputs()[tile_in.d], + state_sum); + } else { + state_acc = + regular_tile_request->get_tensor(hfa_desc->_compiled_tile_model->inputs()[tile_in.acc]); + state_max = + regular_tile_request->get_tensor(hfa_desc->_compiled_tile_model->inputs()[tile_in.max]); + state_sum = + regular_tile_request->get_tensor(hfa_desc->_compiled_tile_model->inputs()[tile_in.d]); + runtime::host_flash_attention::HFARuntimeContext::initialize_state_tensors(state_acc, + state_max, + state_sum); + } + + regular_tile_request->set_tensor(hfa_desc->_compiled_tile_model->inputs()[tile_in.q], + query_tensor); + final_tile_request->set_tensor(hfa_desc->_compiled_final_tile_model->inputs()[tile_in.q], + query_tensor); + regular_tile_request->set_tensor(hfa_desc->_compiled_tile_model->outputs()[tile_out.acc], + state_acc); + regular_tile_request->set_tensor(hfa_desc->_compiled_tile_model->outputs()[tile_out.max], + state_max); + regular_tile_request->set_tensor(hfa_desc->_compiled_tile_model->outputs()[tile_out.d], + state_sum); + final_tile_request->set_tensor(hfa_desc->_compiled_final_tile_model->inputs()[tile_in.acc], + state_acc); + final_tile_request->set_tensor(hfa_desc->_compiled_final_tile_model->inputs()[tile_in.max], + state_max); + final_tile_request->set_tensor(hfa_desc->_compiled_final_tile_model->inputs()[tile_in.d], + state_sum); + final_tile_request->set_tensor(hfa_desc->_compiled_final_tile_model->outputs()[0], + attention_output_tensor); + + const uint32_t K_SEQ_DIM = static_cast(sdpa_info._k_seq_dim); + const uint32_t V_SEQ_DIM = static_cast(sdpa_info._v_seq_dim); + constexpr uint32_t MASK_KV_SEQ_DIM = 3; + size_t next_available_mask_buffer_idx = 0; + + auto process_tile = [&](auto& request, + auto& model, + const ov::SoPtr& k_source, + const ov::SoPtr& v_source, + int64_t kv_offset, + int64_t mask_offset, + int64_t tile_length, + bool async = false) { + auto k_tile_buffer = request->get_tensor(model->inputs()[tile_in.k]); + auto v_tile_buffer = request->get_tensor(model->inputs()[tile_in.v]); + auto mask_tile_buffer = request->get_tensor(model->inputs()[tile_in.mask]); + + if (can_reuse_tensor_zero_copy(k_source, + k_tile_buffer, + K_SEQ_DIM, + kv_offset, + tile_length)) { + request->set_tensor(model->inputs()[tile_in.k], k_source); + } else if (hfa_desc->_can_use_tensor_view) { + request->set_tensor(model->inputs()[tile_in.k], + ov::npuw::util::view(k_source, K_SEQ_DIM, kv_offset, tile_length)); + } else { + extract_and_copy_tile(k_source, k_tile_buffer, K_SEQ_DIM, kv_offset, tile_length, "K"); + } + + if (can_reuse_tensor_zero_copy(v_source, + v_tile_buffer, + V_SEQ_DIM, + kv_offset, + tile_length)) { + request->set_tensor(model->inputs()[tile_in.v], v_source); + } else if (hfa_desc->_can_use_tensor_view) { + request->set_tensor(model->inputs()[tile_in.v], + ov::npuw::util::view(v_source, V_SEQ_DIM, kv_offset, tile_length)); + } else { + extract_and_copy_tile(v_source, v_tile_buffer, V_SEQ_DIM, kv_offset, tile_length, "V"); + } + + if (attention_mask_tensor) { + if (can_reuse_tensor_zero_copy(attention_mask_tensor, + mask_tile_buffer, + MASK_KV_SEQ_DIM, + mask_offset, + tile_length)) { + request->set_tensor(model->inputs()[tile_in.mask], attention_mask_tensor); + } else if (state.hfa_runtime_ctx.has_value()) { + auto cached_tile = + state.hfa_runtime_ctx->find_cached_mask_tile(attention_mask_tensor, + mask_offset, + tile_length); + if (cached_tile) { + request->set_tensor(model->inputs()[tile_in.mask], cached_tile); + } else { + ov::SoPtr cached_mask_tile = + state.hfa_runtime_ctx->get_mask_tile_buffer(next_available_mask_buffer_idx); + extract_and_copy_tile(attention_mask_tensor, + cached_mask_tile, + MASK_KV_SEQ_DIM, + mask_offset, + tile_length, + "Mask"); + state.hfa_runtime_ctx->cache_mask_tile(attention_mask_tensor, + mask_offset, + tile_length, + cached_mask_tile); + request->set_tensor(model->inputs()[tile_in.mask], cached_mask_tile); + next_available_mask_buffer_idx++; + } + } else { + extract_and_copy_tile(attention_mask_tensor, + mask_tile_buffer, + MASK_KV_SEQ_DIM, + mask_offset, + tile_length, + "Mask"); + } + } + + if (async) { + request->start_async(); + if (state.hfa_runtime_ctx && state.hfa_runtime_ctx->has_state_buffers()) { + state.hfa_runtime_ctx->prepare_next_state_buffers(); + } + request->wait(); + } else { + request->infer(); + } + }; + + int64_t mask_tile_offset = 0; + int64_t kv_tile_offset = 0; + for (int64_t tile_idx = 0; tile_idx < num_tiles - 1; ++tile_idx) { + process_tile(regular_tile_request, + hfa_desc->_compiled_tile_model, + past_key_tensor, + past_value_tensor, + kv_tile_offset, + mask_tile_offset, + tile_size); + kv_tile_offset += tile_size; + mask_tile_offset += tile_size; + } + + if (num_tiles > 0) { + const size_t present_seq_length = present_key_tensor->get_shape()[K_SEQ_DIM]; + const int64_t final_tile_length = static_cast(present_seq_length); + OPENVINO_ASSERT( + final_tile_length == tile_size, + "Final tile must process entire present KV sequence in a single inference. " + "This is guaranteed during compilation (tile_size = query_size = present_seq_length)."); + const int64_t mask_total_length = attention_mask_tensor->get_shape()[MASK_KV_SEQ_DIM]; + const int64_t final_mask_offset = mask_total_length - final_tile_length; + process_tile(final_tile_request, + hfa_desc->_compiled_final_tile_model, + present_key_tensor, + present_value_tensor, + 0, + final_mask_offset, + final_tile_length, + true); + } + + if (state.hfa_runtime_ctx && state.hfa_runtime_ctx->has_state_buffers()) { + state.hfa_runtime_ctx->switch_buffers(); + } + return; + } + return; + } + OPENVINO_THROW("Unsupported attention behavior kind"); + } + + private: + BehaviorKind m_kind; + }; + + return std::make_unique(get_behavior_kind(ctx)); + }; +} + +} // namespace + +void put_compiled_dynamic(v1::subgraphs::Context& context, CompiledDynamicState state) { + context.put(std::move(state)); +} + +void put_compiled_pyramid(v1::subgraphs::Context& context, CompiledPyramidState state) { + context.put(std::move(state)); +} + +void put_compiled_hfa(v1::subgraphs::Context& context, CompiledHFAState state) { + context.put(std::move(state)); +} + +ov::npuw::compiled::Attention* get_compiled_dynamic(v1::subgraphs::Context& context) { + return get_compiled_state(context); +} + +const ov::npuw::compiled::Attention* get_compiled_dynamic(const v1::subgraphs::Context& context) { + return get_compiled_state(context); +} + +ov::npuw::compiled::PyramidAttention* get_compiled_pyramid(v1::subgraphs::Context& context) { + return get_compiled_state(context); +} + +const ov::npuw::compiled::PyramidAttention* get_compiled_pyramid(const v1::subgraphs::Context& context) { + return get_compiled_state(context); +} + +ov::npuw::compiled::HostFlashAttention* get_compiled_hfa(v1::subgraphs::Context& context) { + return get_compiled_state(context); +} + +const ov::npuw::compiled::HostFlashAttention* get_compiled_hfa(const v1::subgraphs::Context& context) { + return get_compiled_state(context); +} + +bool has_compiled_state(const v1::subgraphs::CompiledPipeline& pipeline) { + return get_compiled_dynamic(pipeline.context) != nullptr || get_compiled_pyramid(pipeline.context) != nullptr || + get_compiled_hfa(pipeline.context) != nullptr; +} + +void serialize_compiled_state(v1::subgraphs::Context& context, + ov::npuw::s11n::Stream& stream, + const ov::npuw::s11n::SubmodelDeserializeCtx* submodel_ctx) { + std::optional dynamic; + if (const auto* state = get_compiled_dynamic(context)) { + dynamic = *state; + } + stream & dynamic; + if (stream.input() && dynamic.has_value()) { + put_compiled_dynamic(context, std::make_shared(dynamic.value())); + } + + std::optional pyramid; + if (const auto* state = get_compiled_pyramid(context)) { + pyramid = *state; + } + stream & pyramid; + if (stream.input() && pyramid.has_value()) { + put_compiled_pyramid(context, std::make_shared(pyramid.value())); + } + + auto* mutable_pyramid = get_compiled_pyramid(context); + if (mutable_pyramid != nullptr) { + size_t num_models = 0; + if (stream.output()) { + num_models = mutable_pyramid->_compiled_models.size(); + } + stream & num_models; + + if (stream.output()) { + for (size_t i = 0; i < num_models - 1; ++i) { + std::stringstream ss; + mutable_pyramid->_compiled_models[i]->export_model(ss); + std::string model_str = ss.str(); + stream & model_str; + } + } else if (num_models > 0) { + mutable_pyramid->_compiled_models.resize(num_models); + NPUW_ASSERT(submodel_ctx != nullptr); + for (size_t i = 0; i < num_models - 1; ++i) { + std::string model_str; + stream & model_str; + std::stringstream ss(model_str); + mutable_pyramid->_compiled_models[i] = + submodel_ctx->plugin->get_core()->import_model(ss, + submodel_ctx->device, + submodel_ctx->import_config); + } + if (submodel_ctx->compiled_model) { + mutable_pyramid->_compiled_models[num_models - 1] = submodel_ctx->compiled_model; + LOG_DEBUG("Reused compiled_model for the last pyramid attention model"); + } + } + } + + std::optional hfa; + if (const auto* state = get_compiled_hfa(context)) { + hfa = *state; + } + stream & hfa; + if (stream.input() && hfa.has_value()) { + put_compiled_hfa(context, std::make_shared(hfa.value())); + } + + auto* mutable_hfa = get_compiled_hfa(context); + if (mutable_hfa != nullptr) { + bool has_compiled_model = false; + if (stream.output()) { + has_compiled_model = mutable_hfa->_compiled_tile_model != nullptr; + } + stream & has_compiled_model; + if (has_compiled_model) { + if (stream.output()) { + std::stringstream ss; + mutable_hfa->_compiled_tile_model->export_model(ss); + std::string model_str = ss.str(); + stream & model_str; + } else { + NPUW_ASSERT(submodel_ctx != nullptr); + std::string model_str; + stream & model_str; + std::stringstream ss(model_str); + mutable_hfa->_compiled_tile_model = + submodel_ctx->plugin->get_core()->import_model(ss, + submodel_ctx->device, + submodel_ctx->import_config); + LOG_DEBUG("Imported compiled tile model for host flash attention"); + } + } + if (stream.input()) { + NPUW_ASSERT(submodel_ctx != nullptr); + mutable_hfa->_compiled_final_tile_model = submodel_ctx->compiled_model; + LOG_DEBUG("Set compiled final tile model reference for host flash attention"); + } + } +} + +void attach_runtime_behavior(ov::npuw::v1::subgraphs::CompiledPipeline& compiled_pipeline, + ov::npuw::v1::subgraphs::Context& compiled_context, + BehaviorKind kind) { + compiled_pipeline.registration.group = ov::npuw::patterns::attn::SDPA::group_name(); + compiled_pipeline.registration.name = ov::npuw::patterns::attn::SDPA::pattern_name(); + compiled_context.put(kind); + ov::npuw::v1::subgraphs::RuntimeBehaviorSpec spec; + spec.registration = compiled_pipeline.registration; + spec.context = compiled_context; + spec.factory = make_runtime_factory(); + spec.handles_function_prologue = true; + compiled_pipeline.runtime_behavior = std::move(spec); +} + +std::vector register_patterns( + ov::npuw::v1::subgraphs::PatternRegistry& registry) { + std::vector registrations; + registrations.reserve(3); + + // Matcher-only registrations so the registry handles SDPA/SDPADecomposed isolation + // (replaces the legacy HNDL_ATTN fallback in snapshot.cpp) + registrations.emplace_back(registry.on().scoped()); + registrations.emplace_back(registry.on().scoped()); + + // Behavior registration: fires for any function tagged "attn" (i.e. from either SDPA + // or SDPADecomposed pattern). The at_partition callback checks whether dynamic + // attention was detected (f._attention set by Partitioner::attention()) and, if so, + // stashes the compile-time descriptor in the pipeline context for later stages. + ov::npuw::v1::subgraphs::PatternRegistration attn_behavior; + attn_behavior.tag = ov::npuw::patterns::attn::SDPA::isolation_tag(); + attn_behavior.partition_stage = [](ov::npuw::Function& f, ov::npuw::v1::subgraphs::Context& ctx) { + if (f._attention.has_value()) { + put_compiled_dynamic(ctx, std::make_shared(f._attention.value(), f._model)); + ctx.put(BehaviorKind::Dynamic); + return; + } + if (f._pyramid_attention.has_value()) { + put_compiled_pyramid(ctx, + std::make_shared(f._pyramid_attention.value())); + ctx.put(BehaviorKind::Pyramid); + return; + } + if (f._host_flash_attention.has_value()) { + put_compiled_hfa(ctx, + std::make_shared(f._host_flash_attention.value())); + ctx.put(BehaviorKind::HFA); + } + }; + attn_behavior.compile_stage = [](ov::npuw::v1::subgraphs::CompiledPipeline& compiled_pipeline, + ov::npuw::v1::subgraphs::Context& compiled_context) { + const auto* kind = compiled_context.get_if(); + if (kind == nullptr) { + return; + } + attach_runtime_behavior(compiled_pipeline, compiled_context, *kind); + }; + registrations.emplace_back(registry.add(std::move(attn_behavior))); + + return registrations; +} + +} // namespace attn +} // namespace npuw +} // namespace ov diff --git a/src/plugins/intel_npu/src/plugin/npuw/attn/attn_subgraph.hpp b/src/plugins/intel_npu/src/plugin/npuw/attn/attn_subgraph.hpp new file mode 100644 index 000000000000..111f2f0080aa --- /dev/null +++ b/src/plugins/intel_npu/src/plugin/npuw/attn/attn_subgraph.hpp @@ -0,0 +1,62 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#pragma once + +#include +#include + +#include "../v1/subgraph_pipeline.hpp" + +namespace ov { +namespace npuw { +namespace compiled { +struct Attention; +struct PyramidAttention; +struct HostFlashAttention; +} // namespace compiled +namespace orc { +class Stream; +} +namespace s11n { +using Stream = ::ov::npuw::orc::Stream; +struct SubmodelDeserializeCtx; +} // namespace s11n +namespace attn { + +enum class BehaviorKind { Dynamic, Pyramid, HFA }; + +using CompiledDynamicState = std::shared_ptr; +using CompiledPyramidState = std::shared_ptr; +using CompiledHFAState = std::shared_ptr; + +void put_compiled_dynamic(v1::subgraphs::Context& context, CompiledDynamicState state); +void put_compiled_pyramid(v1::subgraphs::Context& context, CompiledPyramidState state); +void put_compiled_hfa(v1::subgraphs::Context& context, CompiledHFAState state); + +ov::npuw::compiled::Attention* get_compiled_dynamic(v1::subgraphs::Context& context); +const ov::npuw::compiled::Attention* get_compiled_dynamic(const v1::subgraphs::Context& context); + +ov::npuw::compiled::PyramidAttention* get_compiled_pyramid(v1::subgraphs::Context& context); +const ov::npuw::compiled::PyramidAttention* get_compiled_pyramid(const v1::subgraphs::Context& context); + +ov::npuw::compiled::HostFlashAttention* get_compiled_hfa(v1::subgraphs::Context& context); +const ov::npuw::compiled::HostFlashAttention* get_compiled_hfa(const v1::subgraphs::Context& context); + +bool has_compiled_state(const v1::subgraphs::CompiledPipeline& pipeline); + +void serialize_compiled_state(v1::subgraphs::Context& context, + ov::npuw::s11n::Stream& stream, + const ov::npuw::s11n::SubmodelDeserializeCtx* submodel_ctx); + +std::vector register_patterns( + ov::npuw::v1::subgraphs::PatternRegistry& registry); + +void attach_runtime_behavior(ov::npuw::v1::subgraphs::CompiledPipeline& compiled_pipeline, + ov::npuw::v1::subgraphs::Context& compiled_context, + BehaviorKind kind); + +} // namespace attn +} // namespace npuw +} // namespace ov diff --git a/src/plugins/intel_npu/src/plugin/npuw/base_sync_infer_request.cpp b/src/plugins/intel_npu/src/plugin/npuw/base_sync_infer_request.cpp index b9780d31acc0..951ea2e5baa5 100644 --- a/src/plugins/intel_npu/src/plugin/npuw/base_sync_infer_request.cpp +++ b/src/plugins/intel_npu/src/plugin/npuw/base_sync_infer_request.cpp @@ -7,6 +7,7 @@ #include +#include "attn/attn_subgraph.hpp" #include "compiled_model.hpp" #include "infer_request_utils.hpp" // to utilize copy_tensor_by_dim #include "intel_npu/config/npuw.hpp" @@ -14,6 +15,7 @@ #include "intel_npu/utils/zero/zero_remote_tensor.hpp" #include "intel_npu/utils/zero/zero_utils.hpp" #include "logging.hpp" +#include "moe/moe_subgraph.hpp" #include "openvino/core/parallel.hpp" #include "util.hpp" @@ -23,9 +25,6 @@ ov::npuw::IBaseInferRequest::IBaseInferRequest(const std::shared_ptrm_compiled_submodels.size()) { m_subrequests.resize(m_num_submodels, {}); m_completion_cbs.resize(m_num_submodels, {}); - if (m_npuw_model->m_acc_check) { - m_ref_subrequests.resize(m_num_submodels); - } // Initialize profiling m_profile.report_on_die = ov::npuw::profiling_enabled(); @@ -65,81 +64,9 @@ ov::npuw::IBaseInferRequest::RqPtrs ov::npuw::IBaseInferRequest::create_infer_re } NPUW_ASSERT(rqs.size() == nireq); - // TODO: Support creation and return of multiple infer requests - if (m_npuw_model->m_acc_check && m_ref_subrequests.at(id) == nullptr) { - if (nireq > 1) { - OPENVINO_THROW("NPUW: TEMPORARY LIMITATION: Couldn't create reference infer " - "requests if 'nireq' is set to > 1!"); - } - LOG_INFO("Create reference subrequest for submodel [" << id << "] on " << m_npuw_model->m_ref_device << "..."); - LOG_BLOCK(); - if (m_npuw_model->submodel_device(id) != m_npuw_model->m_ref_device) { - auto& ref_submodel = m_npuw_model->m_compiled_submodels.at(id).ref_compiled_model; - ov::SoPtr ref_infer_request = {ref_submodel->create_infer_request(), - ref_submodel._so}; - NPUW_ASSERT(ref_infer_request); - m_ref_subrequests.at(id) = std::move(ref_infer_request); - LOG_INFO("Done"); - } else { - LOG_INFO("Skip creation of reference subrequest for submodule[" - << id << "] on reference device: " << m_npuw_model->m_ref_device << ", as actual subrequest [" - << id << "] has been already created on " << "it ."); - } - } - return rqs; } -void ov::npuw::IBaseInferRequest::ensure_subrequest_is_accurate(std::size_t idx) { - LOG_INFO("Check if subrequest[" << idx << "] is accurate..."); - LOG_BLOCK(); - if (m_ref_subrequests.at(idx) != nullptr && m_subrequests.at(idx)._ptr != m_ref_subrequests.at(idx)._ptr) { - NPUW_ASSERT(m_npuw_model->m_compiled_submodels.at(idx).switched_to_ref == false); - NPUW_ASSERT(m_npuw_model->m_compiled_submodels.at(idx).replaced_by.value_or(idx) == idx); - - const auto& ref_comp_model = m_ref_subrequests.at(idx)->get_compiled_model(); - const auto& actual_comp_model = m_subrequests.at(idx)->get_compiled_model(); - NPUW_ASSERT(actual_comp_model->inputs().size() == ref_comp_model->inputs().size()); - // Setting inputs: - for (size_t i = 0; i < actual_comp_model->inputs().size(); i++) { - const auto& itensor = m_subrequests.at(idx)->get_tensor(actual_comp_model->inputs()[i]); - m_ref_subrequests.at(idx)->set_tensor(ref_comp_model->inputs()[i], itensor); - } - m_ref_subrequests.at(idx)->infer(); - - LOG_INFO("Compare actual outputs against references:"); - bool tensors_converge = true; - for (size_t i = 0; i < actual_comp_model->outputs().size(); i++) { - LOG_INFO(" - " << actual_comp_model->outputs()[i]); - const auto& actual_tensor = m_subrequests.at(idx)->get_tensor(actual_comp_model->outputs()[i]); - const auto& ref_tensor = m_ref_subrequests.at(idx)->get_tensor(ref_comp_model->outputs()[i]); - LOG_BLOCK(); - tensors_converge &= m_npuw_model->m_acc_check(actual_tensor, ref_tensor); - } - LOG_INFO((tensors_converge ? "PASS" : "FAIL")); - - if (!tensors_converge) { - LOG_INFO("Subrequest is inaccurate, failover to reference."); - // FIXME: We need to copy reference tensors to actual only in single-model-inference mode - // or if our subgraph is last in the chain. - for (size_t i = 0; i < actual_comp_model->outputs().size(); i++) { - const auto& actual_tensor = m_subrequests.at(idx)->get_tensor(actual_comp_model->outputs()[i]); - const auto& ref_tensor = m_ref_subrequests.at(idx)->get_tensor(ref_comp_model->outputs()[i]); - ref_tensor->copy_to(actual_tensor._ptr); - } - m_npuw_model->m_compiled_submodels.at(idx).compiled_model = - m_npuw_model->m_compiled_submodels.at(idx).ref_compiled_model; - m_npuw_model->m_compiled_submodels.at(idx).switched_to_ref = true; - m_subrequests.at(idx) = m_ref_subrequests.at(idx); - update_subrequest_links(idx); - } - - LOG_INFO("Done"); - } else { - LOG_INFO("Skipped, subrequest is launched on reference device."); - } -} - ov::SoPtr ov::npuw::IBaseInferRequest::get_tensor(const ov::Output& port) const { std::unique_lock lock(m_io_storages_mutex); @@ -297,9 +224,6 @@ void ov::npuw::IBaseInferRequest::infer() { run_subrequest_for_success(idx); }); complete_subrequest(idx); - if (m_npuw_model->m_acc_check) { - ensure_subrequest_is_accurate(idx); - } } // Increment counter regardless if dumps etc are enabled or not. @@ -404,7 +328,7 @@ void ov::npuw::IBaseInferRequest::unpack_closure(std::size_t idx, RqPtr request) // Skip MoE expert submodels - MoE experts require special unpacking logic according to the // expert selection, which is handled later in the inference flow. - if (func_desc.moe_experts.has_value()) { + if (ov::npuw::moe::has_compiled_experts(func_desc.pipeline)) { return; } @@ -495,15 +419,12 @@ void ov::npuw::IBaseInferRequest::bind_global_params(std::size_t idx, RqPtr requ const auto& proto_comp_model_desc = m_npuw_model->m_compiled_submodels[real_idx]; - if (proto_comp_model_desc.moe_experts.has_value()) { + if (ov::npuw::moe::has_compiled_experts(proto_comp_model_desc.pipeline)) { // Expert submodel does not have global parameters to bind return; } const bool is_spatial = proto_comp_model_desc.spatial.has_value(); - const bool is_attention = proto_comp_model_desc.attention.has_value(); - const bool is_pyramid_attention = proto_comp_model_desc.pyramid_attention.has_value(); - const bool is_hfa_attention = proto_comp_model_desc.host_flash_attention.has_value(); // a list of ports to copy tensors, if needed: FROM -> TO std::vector, ov::Output>> copy_list; @@ -519,41 +440,6 @@ void ov::npuw::IBaseInferRequest::bind_global_params(std::size_t idx, RqPtr requ }); }; - // Check if the given subgraph's input is dynamic - auto is_attn_param = [&](std::size_t sub_in_idx) -> bool { - if (!is_attention) { - return false; // Early return - } - auto& attn = proto_comp_model_desc.attention.value(); - return std::any_of(attn.params.begin(), attn.params.end(), [&](const auto& p) -> bool { - return p.idx == sub_in_idx; - }); - }; - - auto is_pyramid_attn_param = [&](std::size_t sub_in_idx) -> bool { - if (!is_pyramid_attention) { - return false; // Early return - } - - auto pyramid_id = m_pyramid_selector->pyramid_id(); - auto& pyramid_attn = proto_comp_model_desc.pyramid_attention.value()._attention_infos[pyramid_id]; - return std::any_of(pyramid_attn.params.begin(), pyramid_attn.params.end(), [&](const auto& p) -> bool { - return p.idx == sub_in_idx; - }); - }; - - auto is_hfa_attn_param = [&](std::size_t sub_in_idx) -> bool { - if (!is_hfa_attention) { - return false; // Early return - } - // Check if sub_in_idx matches any SDPA parameter index - auto& hfa_attn = proto_comp_model_desc.host_flash_attention.value()._sdpa_attention_info; - const auto& sdpa_in = hfa_attn._sdpa_indices; - return sub_in_idx == sdpa_in.query || sub_in_idx == sdpa_in.past_key || sub_in_idx == sdpa_in.past_value || - sub_in_idx == sdpa_in.present_key || sub_in_idx == sdpa_in.present_value || - sub_in_idx == sdpa_in.attention_mask; - }; - for (auto&& it : iodesc.global_params) { std::size_t param_idx{}, sub_in_idx{}; std::tie(param_idx, sub_in_idx) = it; @@ -575,12 +461,8 @@ void ov::npuw::IBaseInferRequest::bind_global_params(std::size_t idx, RqPtr requ // function pipelining NPUW_ASSERT(false && "Global parameter can't be spatial"); m_spatial_io[real_idx].inputs.at(sub_in_idx) = g_tnsr; - } else if (is_attn_param(sub_in_idx) || is_pyramid_attn_param(sub_in_idx)) { - // Register for future use - m_attention_io[idx].inputs.at(sub_in_idx) = g_tnsr; - } else if (is_hfa_attn_param(sub_in_idx)) { - // Register for future use - m_hfa_io[idx].inputs.at(sub_in_idx) = g_tnsr; + } else if (bind_behavior_input(idx, real_idx, sub_in_idx, g_tnsr, request)) { + continue; } else { // Lock mutex just in case. m_input_allocated might be altered in parallel in get_tensor() std::unique_lock lock(m_io_storages_mutex); @@ -618,16 +500,6 @@ void ov::npuw::IBaseInferRequest::bind_global_params(std::size_t idx, RqPtr requ // Run host-side quantized gather, if required handle_quant_host_gather(idx, request); - // Handle attention inputs, if required - m_profile["attn(io)"].record([&]() { - bind_attention_inputs(idx, request); - }); - - // Handle pyramid attention inputs, if required - m_profile["attn(io)"].record([&]() { - bind_pyramid_attention_inputs(idx, request); - }); - LOG_DEBUG("Done"); } @@ -722,175 +594,12 @@ void ov::npuw::IBaseInferRequest::handle_quant_host_gather(std::size_t idx, RqPt } } -void ov::npuw::IBaseInferRequest::bind_attention_inputs(std::size_t idx, RqPtr request) { - auto& comp_model_desc = m_npuw_model->m_compiled_submodels[real(idx)]; - if (!comp_model_desc.attention) { - return; - } - - LOG_DEBUG("Binding Attention inputs..."); - LOG_BLOCK(); - - const auto& dynamic = comp_model_desc.attention.value(); - auto& r = request; - - const auto pos_id = m_attention_selector->length(); - if (pos_id == -1) { - // Dynamic range couldn't be identified - fallback to the default - // (worst case) behavior - for (auto&& param : dynamic.params) { - const auto& iport = comp_model_desc.compiled_model->inputs()[param.idx]; - const auto& input = m_attention_io[idx].inputs.at(param.idx); - r->set_tensor(iport, input); - } - } else { - const auto past_len = m_attention_selector->past_length(); - const auto do_copy = needs_copy(idx) && !m_npuw_model->m_cfg.get<::intel_npu::NPUW_ATTN_NO_COPY>(); - - // Set the past k/v values first - for (auto&& param : dynamic.params) { - const auto& iport = comp_model_desc.compiled_model->inputs()[param.idx]; - const auto& input = m_attention_io[idx].inputs.at(param.idx); - const auto& view = ov::npuw::util::view(input, param.dim, 0, past_len); - const auto shape = view->get_shape(); - - LOG_DEBUG(iport); - LOG_BLOCK(); - if (do_copy && ov::shape_size(shape) > 0) { - // FIXME: Same devices that don't tolerate set_, also don't tolerate strided inputs - const auto& dst = r->get_tensor(iport); - const auto old_ptr = dst->data(); - dst->set_shape(shape); - const auto new_ptr = dst->data(); - if (old_ptr != new_ptr) { - m_footprint[m_npuw_model->submodel_device(real(idx))] += dst->get_byte_size(); - } - LOG_DEBUG("Do copy: " << shape << "..."); - view->copy_to(dst._ptr); - } else if (do_copy && ov::shape_size(shape) == 0) { - // Special case for 0ths chunk. - // Zero the tensor shape but not set to view - // (a view tensor can't be extended) - r->get_tensor(iport)->set_shape(shape); - } else { - r->set_tensor(iport, view); - } - } // for(params) - } - - LOG_DEBUG("Done"); -} - -void ov::npuw::IBaseInferRequest::bind_pyramid_attention_inputs(std::size_t idx, RqPtr request) { - auto& comp_model_desc = m_npuw_model->m_compiled_submodels[real(idx)]; - if (!comp_model_desc.pyramid_attention) { - return; - } - - LOG_DEBUG("Binding Pyramid Attention inputs..."); - LOG_BLOCK(); - - const auto pyramid_id = m_pyramid_selector->pyramid_id(); - const auto& pyramid_attention = comp_model_desc.pyramid_attention.value(); - const auto& attention_info = pyramid_attention._attention_infos[pyramid_id]; - const auto& pyramid_model = pyramid_attention._compiled_models[pyramid_id]; - - const auto pos_id = m_pyramid_selector->length(); - if (pos_id == -1) { - // Pyramid dynamic range couldn't be identified - fallback to the default - // (worst case) behavior - for (auto&& param : attention_info.params) { - const auto& iport = pyramid_model->inputs()[param.idx]; - const auto& input = m_attention_io[idx].inputs.at(param.idx); - request->set_tensor(iport, input); - } - - return; - } - - // Pyramid dynamic range identified - const auto past_len = m_pyramid_selector->past_length(); - const auto infer_case = m_pyramid_selector->this_case(); - - using namespace ov::npuw::runtime; - - // Process each KV parameter based on inference case - if (infer_case == pyramid_attention::Selector::Case::PREFILL) { - // PREFILL: Set or copy past KV to destination tensors - for (auto&& param : attention_info.params) { - const auto& iport = pyramid_model->inputs()[param.idx]; - const auto& input = m_attention_io[idx].inputs.at(param.idx); - const auto& input_shape = input->get_shape(); - - LOG_DEBUG(iport); - LOG_BLOCK(); - - // Optimization for the last chunk: Direct tensor reuse when shapes match - if (static_cast(input_shape[param.dim]) == past_len) { - request->set_tensor(iport, input); - continue; - } - - // Create view of past KV data - const auto& view = ov::npuw::util::view(input, param.dim, 0, past_len); - const auto& shape = view->get_shape(); - - // Handle empty shape case (first chunk) - if (ov::shape_size(shape) == 0) { - request->get_tensor(iport)->set_shape(shape); - continue; - } - - // Copy past KV to full destination tensor - LOG_DEBUG("Do copy: " << shape << "..."); - const auto& dst = request->get_tensor(iport); - ov::npuw::util::copy_tensor_by_dim(view, - dst, - static_cast(param.dim), - static_cast(param.dim)); - } - } else if (infer_case == pyramid_attention::Selector::Case::GENERATE) { - // GENERATE: Set or copy past KV, preserving existing data - for (auto&& param : attention_info.params) { - const auto& iport = pyramid_model->inputs()[param.idx]; - const auto& input = m_attention_io[idx].inputs.at(param.idx); - const auto& input_shape = input->get_shape(); - - LOG_DEBUG(iport); - LOG_BLOCK(); - - // Validation: ensure space for new tokens - if (static_cast(input_shape[param.dim]) == past_len) { - NPUW_ASSERT(false && "Past KV is full, no space for generation"); - } - - const auto& dst = request->get_tensor(iport); - const auto& dst_shape = dst->get_shape(); - - // Optimization: Direct tensor reuse when destination matches input - if (dst_shape == input_shape) { - request->set_tensor(iport, input); - continue; - } - - // FIXME: No need to copy whole past KV, just the new part - - // Create view of past KV data - const auto& view = ov::npuw::util::view(input, param.dim, 0, past_len); - - // Copy past KV to sliced destination (preserve space for new tokens) - LOG_DEBUG("Do copy: " << view->get_shape() << "..."); - const auto& dst_slice = ov::npuw::util::view(dst, param.dim, 0, past_len); - ov::npuw::util::copy_tensor_by_dim(view, - dst_slice, - static_cast(param.dim), - static_cast(param.dim)); - } - } else { - NPUW_ASSERT(false && "Unsupported pyramid attention case"); - } - - LOG_DEBUG("Done"); +bool ov::npuw::IBaseInferRequest::bind_behavior_input(std::size_t, + std::size_t, + std::size_t, + const ov::SoPtr&, + RqPtr) { + return false; } void ov::npuw::IBaseInferRequest::bind_global_results(std::size_t idx, RqPtr request) { diff --git a/src/plugins/intel_npu/src/plugin/npuw/base_sync_infer_request.hpp b/src/plugins/intel_npu/src/plugin/npuw/base_sync_infer_request.hpp index 9e7111cbe506..e116f4b5890d 100644 --- a/src/plugins/intel_npu/src/plugin/npuw/base_sync_infer_request.hpp +++ b/src/plugins/intel_npu/src/plugin/npuw/base_sync_infer_request.hpp @@ -13,8 +13,6 @@ #include #include -#include "attention.hpp" -#include "host_flash_attention.hpp" #include "openvino/runtime/iasync_infer_request.hpp" #include "openvino/runtime/isync_infer_request.hpp" #include "openvino/runtime/so_ptr.hpp" @@ -81,6 +79,14 @@ class IBaseInferRequest : public ov::ISyncInferRequest { return m_history_size; } + std::size_t get_run_iteration() const { + return m_run_iter; + } + + bool should_copy_subgraph_input(std::size_t idx) const { + return needs_copy(idx); + } + protected: int64_t m_history_size = 0; @@ -92,7 +98,6 @@ class IBaseInferRequest : public ov::ISyncInferRequest { // their inference requests anymore - they must be stored // only once in the subrequests list RqPtrs create_infer_requests(std::size_t id, size_t nireq = 1); - void ensure_subrequest_is_accurate(std::size_t idx); virtual void update_subrequest_links(std::size_t idx) = 0; std::shared_ptr m_npuw_model; @@ -137,34 +142,11 @@ class IBaseInferRequest : public ov::ISyncInferRequest { }; std::vector m_spatial_io; - // FIXME: All comments for SpatialIO above apply here as well. - struct AttentionIO { - std::vector> inputs; // # of elements - # of graph-side inputs - }; - std::vector m_attention_io; - - // Host Flash Attention I/O structure - // Stores input and output tensors for HFA tiled inference - struct HostFlashAttentionIO { - std::vector> inputs; // # of elements - # of original SDPA model inputs - std::vector> outputs; // # of elements - # of original SDPA model outputs - }; - std::vector m_hfa_io; - // FIXME: Currently is initialized/managed by subclass as well. // Moved here dumping purposes only // Represents spatial run-time info runtime::spatial::Selector::Ptr m_spatial_selector; - // Same thing about this one - runtime::attention::Selector::Ptr m_attention_selector; - - // Separate selector for pyramid attention - runtime::pyramid_attention::Selector::Ptr m_pyramid_selector; - - // Host flash attention selector for dynamic execution - runtime::host_flash_attention::Selector::Ptr m_hfa_selector; - // This structure tracks how every individual subrequest // access the model's top-level (global, public, etc) parameters // and results. Again, is managed by subclasses @@ -197,12 +179,14 @@ class IBaseInferRequest : public ov::ISyncInferRequest { void unpack_closure(std::size_t idx, RqPtr request); virtual void bind_global_params(std::size_t idx, RqPtr request); virtual void bind_global_results(std::size_t idx, RqPtr request); + virtual bool bind_behavior_input(std::size_t idx, + std::size_t real_idx, + std::size_t input_idx, + const ov::SoPtr& tensor, + RqPtr request); void alloc_quant_gather_tensors(std::size_t idx, RqPtr request); void handle_quant_host_gather(std::size_t idx, RqPtr request); - void bind_attention_inputs(std::size_t idx, RqPtr request); - void bind_pyramid_attention_inputs(std::size_t idx, RqPtr request); - void dump_input_tensors(std::size_t idx); void dump_output_tensors(std::size_t idx); @@ -232,8 +216,6 @@ class IBaseInferRequest : public ov::ISyncInferRequest { std::size_t next(std::size_t idx_base) const; std::size_t real(std::size_t idx) const; - RqPtrs m_ref_subrequests; - using now_t = std::optional; now_t now_idx() const; diff --git a/src/plugins/intel_npu/src/plugin/npuw/compiled_model.cpp b/src/plugins/intel_npu/src/plugin/npuw/compiled_model.cpp index d8fc99affb52..99ebd4abbb00 100644 --- a/src/plugins/intel_npu/src/plugin/npuw/compiled_model.cpp +++ b/src/plugins/intel_npu/src/plugin/npuw/compiled_model.cpp @@ -3,16 +3,18 @@ // #include "compiled_model.hpp" -#include -#include +#include +#include #include #include #include #include "accuracy/comparator.hpp" +#include "attn/attn_subgraph.hpp" #include "intel_npu/npu_private_properties.hpp" #include "just_sync_infer_request.hpp" #include "logging.hpp" +#include "moe/moe_subgraph.hpp" #include "openvino/core/parallel.hpp" #include "openvino/op/util/op_types.hpp" #include "openvino/pass/constant_folding.hpp" @@ -26,6 +28,7 @@ #include "plugin.hpp" #include "unfold_sync_infer_request.hpp" #include "util.hpp" +#include "v1/elements/accuracy_checked.hpp" #include "v1/elements/failsafe.hpp" // required for get_properties_per_device() @@ -33,7 +36,9 @@ #include "intel_npu/config/npuw.hpp" #include "intel_npu/npuw_private_properties.hpp" #include "llm_compiled_model.hpp" +#include "openvino/core/descriptor/tensor.hpp" #include "openvino/core/rt_info/weightless_caching_attributes.hpp" +#include "openvino/op/result.hpp" #include "openvino/runtime/device_id_parser.hpp" #include "openvino/runtime/internal_properties.hpp" #include "openvino/runtime/properties.hpp" @@ -41,6 +46,22 @@ #include "transformations/convert_precision.hpp" namespace { +std::string canonical_device_name(const std::string& device_name) { + const auto dot_pos = device_name.find('.'); + return dot_pos == std::string::npos ? device_name : device_name.substr(0, dot_pos); +} + +void pre_load_transform(const std::shared_ptr& model, const ov::AnyMap& props); + +std::size_t find_device_index(const std::vector& devices, const std::string& device_name) { + const auto canonical_name = canonical_device_name(device_name); + const auto it = std::find_if(devices.begin(), devices.end(), [&](const std::string& candidate) { + return canonical_device_name(candidate) == canonical_name; + }); + NPUW_ASSERT(it != devices.end()); + return static_cast(it - devices.begin()); +} + void split_properties(const ov::AnyMap& properties, ov::AnyMap& npu_plugin_properties, ov::AnyMap& npuw_path_properties) { @@ -62,7 +83,8 @@ std::map any_copy(const ov::AnyMap& params) { } bool can_use_weightless_flow(const ::intel_npu::Config& config) { - return config.get<::intel_npu::NPUW_FOLD>() || config.get<::intel_npu::NPUW_CWAI>(); + return config.get<::intel_npu::NPUW_FOLD>() || !config.get<::intel_npu::NPUW_FOLD_ONLY>().empty() || + config.get<::intel_npu::NPUW_CWAI>(); } bool should_use_weightless_flow(const ov::AnyMap& non_npuw_props, @@ -88,6 +110,88 @@ bool should_use_weightless_flow(const ov::AnyMap& non_npuw_props, return is_weightless; } +ov::npuw::s11n::WeightsContext make_import_weights_ctx(const ov::AnyMap& properties, + bool is_weightless, + const ov::npuw::s11n::BF16Cache& bf16_consts) { + using namespace ov::npuw::s11n; + + std::string weights_path; + WeightsContext::ConstsCache consts_cache; + ov::FileHandleProvider handle_provider = nullptr; + if (is_weightless) { + if (const auto handle_it = properties.find(ov::intel_npu::npuw::weights_handle_provider.name()); + handle_it != properties.end()) { + if (handle_it->second.is()) { + handle_provider = handle_it->second.as(); + } else { + LOG_WARN("WEIGHTS_HANDLE_PROVIDER property is present but is not a FileHandleProvider; falling back to " + "other weightless import sources"); + } + } + if (!handle_provider && properties.find(ov::weights_path.name()) != properties.end()) { + weights_path = properties.at(ov::weights_path.name()).as(); + NPUW_ASSERT(!weights_path.empty() && + "Empty weights_path. Please provide WEIGHTS_PATH or MODEL_PTR in the configuration."); + } else if (!handle_provider && properties.find(ov::hint::model.name()) != properties.end()) { + auto model_ptr = std::const_pointer_cast( + properties.at(ov::hint::model.name()).as>()) + ->clone(); + NPUW_ASSERT( + model_ptr && + "Empty model passed in MODEL_PTR. Please provide WEIGHTS_PATH or MODEL_PTR in the configuration."); + pre_load_transform(model_ptr, {}); + for (const auto& node : model_ptr->get_ordered_ops()) { + if (!ov::op::util::is_constant(node)) { + continue; + } + const auto& c = std::static_pointer_cast(node); + auto rt_info = c->get_rt_info(); + auto weightless_cache_attr = rt_info.find(ov::WeightlessCacheAttribute::get_type_info_static()); + if (weightless_cache_attr == rt_info.end()) { + continue; + } + std::size_t offset = weightless_cache_attr->second.as().bin_offset; + std::size_t size = c->get_byte_size(); + consts_cache[{offset, size}] = node; + } + } else if (!handle_provider) { + NPUW_ASSERT(false && "Blob is weightless but no WEIGHTS_PATH nor MODEL_PTR property is provided!"); + } + } + + WeightsPtr weights = nullptr; + if (is_weightless) { + std::shared_ptr mapped_memory; + if (handle_provider) { + ov::FileHandle handle = handle_provider(); + mapped_memory = ov::load_mmap_object(handle); + } else if (!weights_path.empty()) { + mapped_memory = ov::load_mmap_object(ov::util::make_path(weights_path)); + } + if (mapped_memory) { + weights = std::make_shared(mapped_memory->data(), mapped_memory->size(), mapped_memory); + } + } + + return WeightsContext(weights, weights_path, consts_cache, bf16_consts, handle_provider); +} + +std::function get_encrypt_callback(const ov::AnyMap& properties) { + if (auto it = properties.find(ov::cache_encryption_callbacks.name()); it != properties.end()) { + return it->second.as().encrypt; + } + return {}; +} + +std::function get_decrypt_callback_or_throw(const ov::AnyMap& properties) { + if (auto it = properties.find(ov::cache_encryption_callbacks.name()); it != properties.end()) { + if (const auto decrypt = it->second.as().decrypt) { + return decrypt; + } + } + OPENVINO_THROW("Blob is encrypted, but no decryption callback was provided"); +} + std::set device_list_to_set(const std::string& device_list) { std::set result; if (!device_list.empty()) { @@ -102,7 +206,16 @@ std::set device_list_to_set(const std::string& device_list) { namespace ov { namespace npuw { + namespace { +ov::AnyMap make_submodel_import_config(const std::string& device, const ::intel_npu::Config& cfg) { + ov::AnyMap import_config; + if (ov::npuw::util::starts_with(device, "NPU") && cfg.get<::intel_npu::NPUW_UNFOLD_IREQS>()) { + import_config["NPU_RUN_INFERENCES_SEQUENTIALLY"] = "YES"; + } + return import_config; +} + ov::npuw::DeviceProperties get_properties_per_device(const std::shared_ptr& plugin, const std::string& device_priorities, const ov::AnyMap& properties) { @@ -211,6 +324,12 @@ ov::npuw::ICompiledModel::ICompiledModel(const std::shared_ptr& model ov::npuw::CompiledModel::CompiledModel(const std::shared_ptr& model, const std::shared_ptr& plugin, const ov::AnyMap& properties) + : CompiledModel(model, plugin, properties, nullptr) {} + +ov::npuw::CompiledModel::CompiledModel(const std::shared_ptr& model, + const std::shared_ptr& plugin, + const ov::AnyMap& properties, + const ov::npuw::v1::subgraphs::PatternRegistry* subgraph_patterns) : ov::npuw::ICompiledModel_v0(model, plugin), m_options_desc(std::make_shared<::intel_npu::OptionsDesc>()), m_cfg(m_options_desc), @@ -268,9 +387,27 @@ ov::npuw::CompiledModel::CompiledModel(const std::shared_ptr& model, // Store original constants' offset for serialization purposes store_const_offsets(model); + std::optional combined_subgraph_patterns; + std::vector builtin_pattern_registrations; + combined_subgraph_patterns.emplace(); + if (subgraph_patterns != nullptr) { + combined_subgraph_patterns->append_from(*subgraph_patterns); + } + builtin_pattern_registrations = + ov::npuw::moe::register_patterns(*combined_subgraph_patterns, + m_cfg.get<::intel_npu::NPUW_MOE_TOKEN_CHUNK_SIZE>()); + + { + auto attn_registrations = ov::npuw::attn::register_patterns(*combined_subgraph_patterns); + for (auto& r : attn_registrations) { + builtin_pattern_registrations.push_back(std::move(r)); + } + } + ov::npuw::PartitioningContext ctx; // Identify based on compiler version, user config and pattern ctx.use_host_gather_quant = should_use_quantized_host_gather(model, npuw_props); + ctx.subgraph_patterns = &combined_subgraph_patterns.value(); ov::npuw::Partitioning partitioning; m_profile["partitioning"].record([&]() { @@ -400,6 +537,12 @@ ov::npuw::CompiledModel::CompiledModel(const std::shared_ptr& model, subgraph._sinks, subgraph._parameters, m_name + '_' + std::to_string(id)); + m_compiled_submodels[id].pipeline.registration = subgraph._pipeline.registration; + m_compiled_submodels[id].pipeline.context = subgraph._pipeline.context; + if (subgraph._pipeline.compile_stage) { + subgraph._pipeline.compile_stage(m_compiled_submodels[id].pipeline, + m_compiled_submodels[id].pipeline.context); + } } else { LOG_BLOCK(); auto& fcn_template = partitioning.functions.at(subgraph._funcall); @@ -423,45 +566,6 @@ ov::npuw::CompiledModel::CompiledModel(const std::shared_ptr& model, m_compiled_submodels[id].spatial = compiled::Spatial(fcn_template._spatial.value(), fcn_template._model); } - // Does the same for dynamic. FIXME: This selection should be hidden here - // in the object semantics - if (fcn_template._attention) { - m_compiled_submodels[id].attention = - compiled::Attention(fcn_template._attention.value(), fcn_template._model); - } - - if (fcn_template._pyramid_attention) { - LOG_INFO("Creating compiled::PyramidAttention for Subgraph[" << id << "] (function " - << subgraph._funcall << ")"); - m_compiled_submodels[id].pyramid_attention = - compiled::PyramidAttention(fcn_template._pyramid_attention.value()); - } - - if (fcn_template._host_flash_attention) { - LOG_INFO("Creating compiled::HostFlashAttention for Subgraph[" << id << "] (function " - << subgraph._funcall << ")"); - m_compiled_submodels[id].host_flash_attention = - compiled::HostFlashAttention(fcn_template._host_flash_attention.value()); - } - - if (fcn_template._moe_experts) { - m_compiled_submodels[id].moe_experts = compiled::MoEExperts(fcn_template._moe_experts.value()); - - // Point model to first chunk size model (actual compilation handled by compile_for_success) - const auto& models_to_compile = m_compiled_submodels[id].moe_experts.value()._models_to_compile; - NPUW_ASSERT(!models_to_compile.empty() && "Fatal: MoEExperts has no models to compile!"); - m_compiled_submodels[id].model = models_to_compile.begin()->second; - } - - if (fcn_template._moe_experts_downstream) { - m_compiled_submodels[id].moe_experts_downstream = - compiled::MoEDownstream(fcn_template._moe_experts_downstream.value()); - - // Set the model to compile from MoEDownstream - m_compiled_submodels[id].model = - m_compiled_submodels[id].moe_experts_downstream.value()._model_to_compile; - } - LOG_INFO("Subgraph[" << id << "] is a function body for " << subgraph._funcall); } else { // ...and refer to it in other calls @@ -470,6 +574,19 @@ ov::npuw::CompiledModel::CompiledModel(const std::shared_ptr& model, } auto& closure_desc = m_compiled_submodels[id].closure.get(); + m_compiled_submodels[id].pipeline.registration = fcn_template._pipeline.registration; + m_compiled_submodels[id].pipeline.context = fcn_template._pipeline.context; + if (compiled_fcn_iter == compiledFunctions.end()) { + if (fcn_template._pipeline.compile_stage) { + fcn_template._pipeline.compile_stage(m_compiled_submodels[id].pipeline, + m_compiled_submodels[id].pipeline.context); + ov::npuw::moe::clear_partition_state(fcn_template._pipeline.context); + } + } else { + const auto real_id = m_compiled_submodels[id].replaced_by.value(); + m_compiled_submodels[id].pipeline.runtime_behavior = + m_compiled_submodels[real_id].pipeline.runtime_behavior; + } m_compiled_submodels[id].host_gather = subgraph._host_gather; m_compiled_submodels[id].quant_unpack_gather = subgraph._quant_unpack_gather; m_compiled_submodels[id].param_base = fcn_template._param_offset; @@ -486,6 +603,10 @@ ov::npuw::CompiledModel::CompiledModel(const std::shared_ptr& model, OPENVINO_THROW("Fatal: submodel ", id, " is neither a model nor a function call!"); } const std::size_t real_id = m_compiled_submodels[id].replaced_by.value_or(id); + auto& pipeline = m_compiled_submodels[id].pipeline; + pipeline.is_function_call = + m_compiled_submodels[id].replaced_by.has_value() && m_compiled_submodels[id].replaced_by.value() != id; + pipeline.function_body_subgraph_idx = m_compiled_submodels[id].replaced_by; // FIXME: a hotfix for a crash where we have too many names in submodel's output // Do it just once if that's a function @@ -498,15 +619,17 @@ ov::npuw::CompiledModel::CompiledModel(const std::shared_ptr& model, }; // Fix tensor names for MoE expert models - if (const auto& moe_experts = m_compiled_submodels[real_id].moe_experts) { - for (const auto& [chunk_size, model] : moe_experts.value()._models_to_compile) { + if (const auto* moe_experts = + ov::npuw::moe::get_compiled_experts(m_compiled_submodels[real_id].pipeline.context)) { + for (const auto& [chunk_size, model] : moe_experts->_models_to_compile) { fix_tensor_names(model); } } // Fix tensor names for pyramid attention models - if (const auto& pyramid_attn = m_compiled_submodels[real_id].pyramid_attention) { - for (const auto& model : pyramid_attn.value()._models_to_compile) { + if (const auto* pyramid_attn = + ov::npuw::attn::get_compiled_pyramid(m_compiled_submodels[real_id].pipeline.context)) { + for (const auto& model : pyramid_attn->_models_to_compile) { fix_tensor_names(model); } } @@ -565,8 +688,8 @@ ov::npuw::CompiledModel::CompiledModel(const std::shared_ptr& model, auto forced_dev_it = std::find(m_dev_list.begin(), m_dev_list.end(), forced_device); if (forced_dev_it == m_dev_list.end()) { LOG_WARN("Target device for Subgraph[" << id << "] was set to " << forced_device - << ", but was not found in the device list: " << "[" - << dev_list_str << "] -- ignoring"); + << ", but was not found in the device list: " + << "[" << dev_list_str << "] -- ignoring"); } else { LOG_INFO("Force Subgraph[" << id << "] target device to " << *forced_dev_it); devices.push_back(*forced_dev_it); @@ -594,19 +717,7 @@ ov::npuw::CompiledModel::CompiledModel(const std::shared_ptr& model, "]"); } - if (m_acc_check) { - if (submodel_device(real_id) != m_ref_device) { - LOG_INFO("Compile Subgraph[" << real_id << "] for reference device: " << m_ref_device << "."); - LOG_BLOCK(); - m_compiled_submodels.at(real_id).ref_compiled_model = - compile_submodel(m_compiled_submodels.at(real_id).model, m_ref_device); - LOG_INFO("Done (reference)"); - } else { - LOG_INFO("Skip compilation of submodel[" << real_id << "] for reference device: " << m_ref_device - << ", as original submodel[" << real_id - << "] has been already compiled for it."); - } - } + LOG_INFO("Done (Subgraph[" << id << "])."); }; // compile // Parallel compilation is unstable so is disabled by default. @@ -729,588 +840,504 @@ bool ov::npuw::CompiledModel::should_use_quantized_host_gather(const std::shared return false; } -void ov::npuw::CompiledModel::CompiledModelDesc::serialize(std::ostream& stream, - const ov::npuw::s11n::WeightsContext& ctx) const { +void ov::npuw::CompiledModel::CompiledModelDesc::serialize(ov::npuw::s11n::Stream& stream, + const ov::npuw::s11n::WeightsContext& ctx, + std::optional orc_device_index, + const ov::npuw::s11n::SubmodelDeserializeCtx* submodel_ctx) { using namespace ov::npuw::s11n; - LOG_DEBUG("Serializing CompiledModelDesc..."); + if (stream.output()) { + LOG_DEBUG("Serializing CompiledModelDesc..."); + } else { + LOG_DEBUG("Deserializing CompiledModelDesc..."); + } LOG_BLOCK(); - write(stream, replaced_by); - - write(stream, param_base); - write(stream, forced_to_fcall); - - write(stream, host_gather.dst_idx); - write(stream, host_gather.src_idx); - write(stream, host_gather.idx_idx); - - write(stream, quant_unpack_gather.dst_idx); - write(stream, quant_unpack_gather.src_w_idx); - write(stream, quant_unpack_gather.src_z_idx); - write(stream, quant_unpack_gather.src_s_idx); - write(stream, quant_unpack_gather.idx_idx); - - write(stream, spatial); - write(stream, attention); + ov::SoPtr imported_compiled_model; + std::optional resolved_submodel_ctx; + if (orc_device_index.has_value() || (stream.input() && submodel_ctx != nullptr && submodel_ctx->device_by_index)) { + std::size_t device_index = orc_device_index.value_or(0u); + stream & device_index; - // Serialize compiled submodels for pyramid attention, except the last one - write(stream, pyramid_attention); - if (pyramid_attention.has_value()) { - size_t num_models = pyramid_attention.value()._compiled_models.size(); - write(stream, num_models); + bool has_compiled_model = static_cast(compiled_model); + stream & has_compiled_model; - for (size_t i = 0; i < num_models - 1; ++i) { - std::stringstream ss; - auto compiled_model = pyramid_attention.value()._compiled_models[i]; - compiled_model->export_model(ss); - write(stream, ss.str()); - } - } - - // Serialize MoE experts - write(stream, moe_experts); - if (moe_experts.has_value()) { - const auto& moe = moe_experts.value(); - size_t num_compiled_models = moe._compiled_models.size(); - write(stream, num_compiled_models); - - for (const auto& [chunk_size, compiled_model] : moe._compiled_models) { - write(stream, chunk_size); - if (compiled_model) { - write(stream, true); - std::stringstream ss; - compiled_model->export_model(ss); - write(stream, ss.str()); - } else { - write(stream, false); + if (stream.output()) { + if (has_compiled_model) { + std::stringstream buffer(std::ios::in | std::ios::out | std::ios::binary); + compiled_model->export_model(buffer); + auto model_blob = buffer.str(); + stream & model_blob; } - } - } - - // Serialize MoE experts downstream - write(stream, moe_experts_downstream); - if (moe_experts_downstream.has_value()) { - const auto& moe_downstream = moe_experts_downstream.value(); - if (moe_downstream._compiled_model) { - write(stream, true); - std::stringstream ss; - moe_downstream._compiled_model->export_model(ss); - write(stream, ss.str()); - } else { - write(stream, false); - } - } - - // Serialize host flash attention - write(stream, host_flash_attention); - if (host_flash_attention.has_value()) { - // Serialize compiled tile model - if (host_flash_attention.value()._compiled_tile_model) { - write(stream, true); - std::stringstream ss; - host_flash_attention.value()._compiled_tile_model->export_model(ss); - write(stream, ss.str()); } else { - write(stream, false); + NPUW_ASSERT(submodel_ctx != nullptr && "Submodel deserialization context must be provided for ORC import"); + NPUW_ASSERT(submodel_ctx->device_by_index && + "Submodel deserialization context must provide device_by_index for ORC import"); + const auto device = submodel_ctx->device_by_index(device_index); + const auto import_config = submodel_ctx->import_config_for_device(device); + if (has_compiled_model) { + std::string model_blob; + stream & model_blob; + std::stringstream buffer(model_blob, std::ios::in | std::ios::out | std::ios::binary); + imported_compiled_model = submodel_ctx->plugin->get_core()->import_model(buffer, device, import_config); + } + compiled_model = imported_compiled_model; + resolved_submodel_ctx.emplace(submodel_ctx->plugin, device, compiled_model, import_config); + submodel_ctx = &(*resolved_submodel_ctx); + } + } + + stream & replaced_by & param_base & forced_to_fcall & host_gather.dst_idx & host_gather.src_idx & + host_gather.idx_idx & quant_unpack_gather.dst_idx & quant_unpack_gather.src_w_idx & + quant_unpack_gather.src_z_idx & quant_unpack_gather.src_s_idx & quant_unpack_gather.idx_idx & spatial; + + // Function calls share pipeline.context with their function body at runtime. + // There is no need to serialize the compiled moe/attn state for each call – + // doing so would re-import NPU blobs for every repeated layer (one per call), + // causing O(N_layers) memory growth on import. Only the function body + // (compiled_model is set, or replaced_by is absent) writes/reads state. + const bool is_fcall = replaced_by.has_value() && !static_cast(compiled_model); + if (!is_fcall) { + ov::npuw::moe::serialize_compiled_state(pipeline.context, stream, submodel_ctx); + ov::npuw::attn::serialize_compiled_state(pipeline.context, stream, submodel_ctx); + + if (stream.input()) { + if (ov::npuw::attn::get_compiled_dynamic(pipeline.context) != nullptr) { + ov::npuw::attn::attach_runtime_behavior(pipeline, + pipeline.context, + ov::npuw::attn::BehaviorKind::Dynamic); + } else if (ov::npuw::attn::get_compiled_pyramid(pipeline.context) != nullptr) { + ov::npuw::attn::attach_runtime_behavior(pipeline, + pipeline.context, + ov::npuw::attn::BehaviorKind::Pyramid); + } else if (ov::npuw::attn::get_compiled_hfa(pipeline.context) != nullptr) { + ov::npuw::attn::attach_runtime_behavior(pipeline, pipeline.context, ov::npuw::attn::BehaviorKind::HFA); + } else if (ov::npuw::moe::get_compiled_experts(pipeline.context) != nullptr) { + ov::npuw::moe::attach_runtime_behavior(pipeline, + pipeline.context, + ov::npuw::moe::BehaviorRole::EXPERTS, + true); + } else if (ov::npuw::moe::get_compiled_downstream(pipeline.context) != nullptr) { + ov::npuw::moe::attach_runtime_behavior(pipeline, + pipeline.context, + ov::npuw::moe::BehaviorRole::DOWNSTREAM, + true); + } } } auto& closure_desc = closure.get(); - write(stream, closure_desc.is_remote); - write(stream, closure_desc.closure_uid); + stream & closure_desc.is_remote & closure_desc.closure_uid; if (ctx.is_weightless) { - write_weightless(stream, scales, ctx); - write_weightless(stream, zerops, ctx); + serialize_weightless(stream, scales, ctx); + serialize_weightless(stream, zerops, ctx); - write(stream, closure_desc.closure.size()); + std::size_t closure_size = closure_desc.closure.size(); + stream & closure_size; std::vector cpu_closures; std::vector cpu_closure_ids; std::vector non_cpu_tensors; std::vector non_cpu_tensors_ids; - for (std::size_t cidx = 0; cidx < closure_desc.closure.size(); ++cidx) { - if (closure_desc.closure_uid[cidx] == -1) { // CPU closure - cpu_closure_ids.push_back(cidx); - cpu_closures.push_back(closure_desc.closure[cidx]); - } else { - non_cpu_tensors_ids.push_back(cidx); - non_cpu_tensors.push_back(lazy_closure[cidx]); // must be there + if (stream.output()) { + for (std::size_t cidx = 0; cidx < closure_desc.closure.size(); ++cidx) { + if (closure_desc.closure_uid[cidx] == -1) { + cpu_closure_ids.push_back(cidx); + cpu_closures.push_back(closure_desc.closure[cidx]); + } else { + non_cpu_tensors_ids.push_back(cidx); + non_cpu_tensors.push_back(lazy_closure[cidx]); + } + } + stream & cpu_closure_ids; + serialize_weightless(stream, cpu_closures, ctx); + stream & non_cpu_tensors_ids & non_cpu_tensors; + } else { + closure_desc.closure.resize(closure_size); + lazy_closure.resize(closure_size); + stream & cpu_closure_ids; + serialize_weightless(stream, cpu_closures, ctx); + std::size_t tidx = 0; + for (const auto& idx : cpu_closure_ids) { + closure_desc.closure[idx] = std::move(cpu_closures[tidx++]); + } + stream & non_cpu_tensors_ids & non_cpu_tensors; + std::size_t ltidx = 0; + for (const auto& idx : non_cpu_tensors_ids) { + lazy_closure[idx] = std::move(non_cpu_tensors[ltidx++]); + } + for (std::size_t cidx = 0; cidx < closure_desc.closure.size(); ++cidx) { + if (closure_desc.closure_uid[cidx] != -1 && lazy_closure[cidx]) { + lazy_closure[cidx].read_weight(ctx); + } } } - - write(stream, cpu_closure_ids); - write_weightless(stream, cpu_closures, ctx); - write(stream, non_cpu_tensors_ids); - write(stream, non_cpu_tensors); } else { - write(stream, scales); - write(stream, zerops); + stream & scales & zerops; - write(stream, closure_desc.closure.size()); - std::vector cpu_closures; + std::size_t closure_size = closure_desc.closure.size(); + stream & closure_size; std::vector cpu_closure_ids; - for (std::size_t cidx = 0; cidx < closure_desc.closure.size(); ++cidx) { - if (closure_desc.closure_uid[cidx] == -1) { // CPU closure, not in the bank - cpu_closure_ids.push_back(cidx); - cpu_closures.push_back(closure_desc.closure[cidx]); + if (stream.output()) { + std::vector cpu_closures; + for (std::size_t cidx = 0; cidx < closure_desc.closure.size(); ++cidx) { + if (closure_desc.closure_uid[cidx] == -1) { + cpu_closure_ids.push_back(cidx); + cpu_closures.push_back(closure_desc.closure[cidx]); + } + } + stream & cpu_closure_ids; + for (auto& tensor : cpu_closures) { + stream & tensor; + } + } else { + stream & cpu_closure_ids; + closure_desc.closure.resize(closure_size); + for (const auto& cidx : cpu_closure_ids) { + stream & closure_desc.closure[cidx]; } - } - - write(stream, cpu_closure_ids); - - for (const auto& tensor : cpu_closures) { - write(stream, tensor); } } LOG_DEBUG("DONE."); } -void ov::npuw::CompiledModel::CompiledModelDesc::deserialize( - std::istream& stream, - const ov::npuw::s11n::WeightsContext& ctx, - const ov::npuw::s11n::SubmodelDeserializeCtx& submodel_ctx) { - using namespace ov::npuw::s11n; - - LOG_DEBUG("Deserializing CompiledModelDesc..."); - LOG_BLOCK(); - - read(stream, replaced_by); - - read(stream, param_base); - read(stream, forced_to_fcall); - - read(stream, host_gather.dst_idx); - read(stream, host_gather.src_idx); - read(stream, host_gather.idx_idx); - - read(stream, quant_unpack_gather.dst_idx); - read(stream, quant_unpack_gather.src_w_idx); - read(stream, quant_unpack_gather.src_z_idx); - read(stream, quant_unpack_gather.src_s_idx); - read(stream, quant_unpack_gather.idx_idx); - - read(stream, spatial); - read(stream, attention); - - read(stream, pyramid_attention); - if (pyramid_attention.has_value()) { - size_t num_models = 0; - read(stream, num_models); - pyramid_attention.value()._compiled_models.resize(num_models); - - if (num_models > 0) { - // Import all pyramid models except the last one - for (size_t i = 0; i < num_models - 1; ++i) { - std::string model_str; - read(stream, model_str); - std::stringstream ss(model_str); - pyramid_attention->_compiled_models[i] = - submodel_ctx.plugin->get_core()->import_model(ss, submodel_ctx.device, submodel_ctx.import_config); - } - - // Reuse the already compiled model for the last pyramid attention model - if (submodel_ctx.compiled_model) { - pyramid_attention->_compiled_models[num_models - 1] = submodel_ctx.compiled_model; - LOG_DEBUG("Reused compiled_model for the last pyramid attention model"); - } - } +ov::npuw::CompiledModel::~CompiledModel() { + if (m_eval_future.valid()) { + m_eval_future.wait(); } +} - // Deserialize MoE experts - read(stream, moe_experts); - if (moe_experts.has_value()) { - size_t num_compiled_models = 0; - read(stream, num_compiled_models); - - for (size_t i = 0; i < num_compiled_models; ++i) { - size_t chunk_size = 0; - read(stream, chunk_size); - - bool has_model = false; - read(stream, has_model); - - if (has_model) { - std::string model_str; - read(stream, model_str); - std::stringstream ss(model_str); - auto compiled_model = - submodel_ctx.plugin->get_core()->import_model(ss, submodel_ctx.device, submodel_ctx.import_config); - moe_experts->_compiled_models[chunk_size] = compiled_model; - LOG_DEBUG("Imported MoE compiled model for chunk_size=" << chunk_size); - } - } +void ov::npuw::CompiledModel::export_model(std::ostream& raw_stream) const { + serialize_orc(raw_stream); +} - LOG_DEBUG("Deserialized " << moe_experts->_compiled_models.size() << " MoE expert models"); +std::shared_ptr ov::npuw::CompiledModel::import_model( + std::istream& stream, + const std::shared_ptr& plugin, + const ov::AnyMap& properties) { + if (!ov::npuw::orc::is_orc(stream).has_value()) { + OPENVINO_THROW("Legacy flat NPUW CompiledModel blobs are no longer supported. Re-export the model with the " + "current OpenVINO package."); } + return deserialize_orc(stream, plugin, properties); +} - // Deserialize MoE experts downstream - read(stream, moe_experts_downstream); - if (moe_experts_downstream.has_value()) { - bool has_model = false; - read(stream, has_model); +void ov::npuw::CompiledModel::ensure_phase0_compatibility() const { + for (std::size_t idx = 0; idx < m_compiled_submodels.size(); ++idx) { + const auto& subm = m_compiled_submodels[idx]; + const auto name = format_subgraph_name(idx, ""); + const auto real_idx = subm.replaced_by.value_or(idx); + const auto device = submodel_device(real_idx); + auto fail = [&](const auto& feature) { + OPENVINO_THROW("Cannot produce ORC-compatible blob: subgraph ", + idx, + " (\"", + name, + "\") has ", + feature, + " - not yet versioned for NPUW phase 0. Recompile without NPUW_ENSURE_COMPATIBILITY or wait " + "for a later phase."); + }; - if (has_model) { - std::string model_str; - read(stream, model_str); - std::stringstream ss(model_str); - auto compiled_model = - submodel_ctx.plugin->get_core()->import_model(ss, submodel_ctx.device, submodel_ctx.import_config); - moe_experts_downstream->_compiled_model = compiled_model; - LOG_DEBUG("Imported MoE downstream compiled model"); + if (!ov::npuw::util::starts_with(device, "NPU")) { + fail(std::string("device \"") + device + "\""); } - } - - // Deserialize host flash attention - read(stream, host_flash_attention); - if (host_flash_attention.has_value()) { - bool has_compiled_model = false; - read(stream, has_compiled_model); - if (has_compiled_model) { - std::string model_str; - read(stream, model_str); - std::stringstream ss(model_str); - host_flash_attention->_compiled_tile_model = - submodel_ctx.plugin->get_core()->import_model(ss, submodel_ctx.device, submodel_ctx.import_config); - LOG_DEBUG("Imported compiled tile model for host flash attention"); + if (subm.spatial.has_value()) { + fail("Spatial"); } - - // Set reference to the final tile model (which is the main compiled_model for HFA) - host_flash_attention->_compiled_final_tile_model = submodel_ctx.compiled_model; - LOG_DEBUG("Set compiled final tile model reference for host flash attention"); - } - - auto& closure_desc = closure.get(); - - read(stream, closure_desc.is_remote); - read(stream, closure_desc.closure_uid); - - if (ctx.weights || !ctx.consts_cache.empty()) { - read_weightless(stream, scales, ctx); - read_weightless(stream, zerops, ctx); - - std::size_t closure_size = 0; - read(stream, closure_size); - closure_desc.closure.resize(closure_size); - lazy_closure.resize(closure_size); - - std::vector cpu_closure_ids; - read(stream, cpu_closure_ids); - - std::vector cpu_closures; - read_weightless(stream, cpu_closures, ctx); - std::size_t tidx = 0; - for (const auto& idx : cpu_closure_ids) { - closure_desc.closure[idx] = std::move(cpu_closures[tidx++]); + if (ov::npuw::attn::get_compiled_dynamic(subm.pipeline.context) != nullptr) { + fail("Attention"); } - - std::vector non_cpu_tensors_ids; - read(stream, non_cpu_tensors_ids); - - std::vector non_cpu_tensors; - read(stream, non_cpu_tensors); - std::size_t ltidx = 0; - for (const auto& idx : non_cpu_tensors_ids) { - lazy_closure[idx] = std::move(non_cpu_tensors[ltidx++]); + if (ov::npuw::attn::get_compiled_pyramid(subm.pipeline.context) != nullptr) { + fail("PyramidAttention"); } - - // Also read weights into LazyTensors - for (std::size_t cidx = 0; cidx < closure_desc.closure.size(); ++cidx) { - if (closure_desc.closure_uid[cidx] != -1 && - lazy_closure[cidx]) { // previously registered before serialization - lazy_closure[cidx].read_weight(ctx); - } + if (ov::npuw::attn::get_compiled_hfa(subm.pipeline.context) != nullptr) { + fail("HostFlashAttention"); } - } else { - read(stream, scales); - read(stream, zerops); - - std::size_t closure_size = 0; - read(stream, closure_size); - std::vector cpu_closure_ids; - read(stream, cpu_closure_ids); - closure_desc.closure.resize(closure_size); - for (const auto& cidx : cpu_closure_ids) { - read(stream, closure_desc.closure[cidx]); + if (ov::npuw::moe::get_compiled_experts(subm.pipeline.context) != nullptr) { + fail("MoEExperts"); + } + if (ov::npuw::moe::get_compiled_downstream(subm.pipeline.context) != nullptr) { + fail("MoEDownstream"); + } + if (subm.pipeline.runtime_behavior.has_value()) { + fail("runtime behavior"); } } - - LOG_DEBUG("DONE."); } -ov::npuw::CompiledModel::~CompiledModel() { - if (m_eval_future.valid()) { - m_eval_future.wait(); - } +void ov::npuw::CompiledModel::serialize_orc(std::ostream& stream) const { + ov::npuw::orc::write_file_header(stream, ov::npuw::orc::schema_npuw::NPUW_ORC_PARTITIONED_SCHEMA); + serialize_orc_container(stream, true, get_encrypt_callback(m_non_npuw_props)); } -void ov::npuw::CompiledModel::export_model(std::ostream& stream) const { - using namespace ov::npuw::s11n; +void ov::npuw::CompiledModel::serialize_orc_container(std::ostream& stream, + bool include_weights_bank, + const std::function& encrypt, + const ov::npuw::s11n::BF16Cache* bf16_consts) const { + using namespace ov::npuw; - // Identify encryption flow - bool encryption_required = false; - EncryptionCallbacks enc_callbacks; - if (auto it = m_non_npuw_props.find(ov::cache_encryption_callbacks.name()); - it != m_non_npuw_props.end() && it->second.as().encrypt) { - LOG_INFO("Encryption will be done via the function provided."); - encryption_required = true; - enc_callbacks.encrypt = it->second.as().encrypt; + if (m_cfg.get<::intel_npu::NPUW_ENSURE_COMPATIBILITY>()) { + if (encrypt) { + OPENVINO_THROW("Cannot produce ORC-compatible blob: encrypted export is not yet supported in " + "NPUW_ENSURE_COMPATIBILITY mode"); + } + ensure_phase0_compatibility(); } - // Identify either full flow or weightless bool is_weightless = should_use_weightless_flow(m_non_npuw_props, m_cfg, m_const_to_offset); - if (!is_weightless) { - LOG_INFO("Serialization will be done via flow with weights."); - } - - // Write header regardless of encryption requirement - to identify NPUW serializated blobs - // Serialize magic number first - write(stream, NPUW_SERIALIZATION_INDICATOR); - // Serilize CompiledModel identifier - write(stream, NPUW_COMPILED_MODEL_INDICATOR); - // Serialize general meta info - write(stream, OPENVINO_VERSION_MAJOR); - write(stream, OPENVINO_VERSION_MINOR); - write(stream, OPENVINO_VERSION_PATCH); - write(stream, std::string(NPUW_SERIALIZATION_VERSION)); - // Serialize encrypted flag - write(stream, encryption_required); - // Write flow identifier - write(stream, is_weightless); - - if (!encryption_required) { - CompiledContext ctx(false, nullptr, nullptr, m_bf16_consts); - serialize(stream, ctx); - - write(stream, m_weights_bank->get_name()); - if (!is_weightless) { - // Serialize weights bank - // Note: no need to encrypt weights in full flow - m_weights_bank->serialize(stream); + LOG_INFO("Serialization will be done via " << (is_weightless ? "weightless" : "flow with weights") << "."); + // For top-level CompiledModel export we serialize this model's own BF16 cache. + // For nested export (e.g. inside LLMCompiledModel) the parent may provide a + // cache collected before graph splitting / BF16->FP16 conversion, and that + // propagated view must win so weightless import can reconstruct tensors correctly. + const auto& bf16_cache = bf16_consts != nullptr ? *bf16_consts : m_bf16_consts; + + ov::AnyMap serializable_props = m_non_npuw_props; + serializable_props.erase(ov::cache_encryption_callbacks.name()); + s11n::WeightsContext weights_ctx(is_weightless, m_const_to_offset); + + auto write_children = [&](std::ostream& body_stream) { + for (std::size_t idx = 0; idx < m_compiled_submodels.size(); ++idx) { + auto& subm = const_cast(m_compiled_submodels[idx]); + const auto real_idx = subm.replaced_by.value_or(idx); + const auto device_index = real_idx == idx ? find_device_index(m_dev_list, submodel_device(real_idx)) : 0u; + ov::npuw::orc::with_leaf_section(body_stream, + CompiledModelDesc::kOrcType, + CompiledModelDesc::kOrcVersion, + [&] { + auto desc_stream = ov::npuw::s11n::Stream::writer(body_stream); + subm.serialize(desc_stream, weights_ctx, device_index); + }); + } + + if (include_weights_bank) { + ov::npuw::orc::with_leaf_section(body_stream, weights::Bank::kOrcType, weights::Bank::kOrcVersion, [&] { + auto weights_stream = ov::npuw::s11n::Stream::writer(body_stream); + auto bank_name = m_weights_bank->get_name(); + weights_stream & bank_name; + if (!is_weightless) { + weights_stream&* m_weights_bank; + } + }); } - return; - } + }; - // In case of weightless flow the whole blob will be encrypted on NPUW side. - std::stringstream non_encrypted_stream; - if (is_weightless) { - non_encrypted_stream.copyfmt(stream); - CompiledContext ctx(false, nullptr, nullptr, m_bf16_consts); - serialize(non_encrypted_stream, ctx); - std::string encrypted = enc_callbacks.encrypt(non_encrypted_stream.str()); - write(stream, encrypted); - } else { - // In case of blob with weights only encrypt XML part of the model - CompiledContext ctx(true, enc_callbacks.encrypt, nullptr, m_bf16_consts); - serialize(stream, ctx); - } + const auto root_flags = + encrypt ? static_cast(ov::npuw::orc::SectionFlag::ENCRYPTED) : 0u; + ov::npuw::orc::with_section(stream, kOrcType, kOrcVersion, root_flags, [&] { + ov::npuw::orc::with_leaf_section(stream, ov::npuw::orc::META_SECTION_TYPE, 0u, [&] { + auto meta_stream = ov::npuw::s11n::Stream::writer(stream); + meta_stream & m_name; + meta_stream& inputs() & outputs(); + meta_stream & m_inputs_to_submodels_inputs & m_outputs_to_submodels_outputs & m_param_subscribers & + m_submodels_input_to_prev_output; + meta_stream & m_dev_list; + meta_stream& const_cast<::intel_npu::Config&>(m_cfg); + meta_stream & serializable_props; + meta_stream & is_weightless; + // Persist the BF16 interpretation map that weightless import later feeds + // into LazyTensor::read_weight() when it decides whether raw bytes should + // be read as FP16 directly or converted from BF16 source storage. + meta_stream& const_cast(bf16_cache); + if (encrypt) { + std::stringstream payload_stream(std::ios::in | std::ios::out | std::ios::binary); + write_children(payload_stream); + auto encrypted_payload = encrypt(payload_stream.str()); + meta_stream & encrypted_payload; + } + }); + if (!encrypt) { + write_children(stream); + } + }); +} - write(stream, m_weights_bank->get_name()); - if (!is_weightless) { - // Serialize weights bank - // Note: no need to encrypt weights in full flow - m_weights_bank->serialize(stream); +std::shared_ptr ov::npuw::CompiledModel::deserialize_orc( + std::istream& stream, + const std::shared_ptr& plugin, + const ov::AnyMap& properties) { + const auto header = ov::npuw::orc::read_file_header(stream); + if (header.schema_uuid != ov::npuw::orc::schema_npuw::NPUW_ORC_PARTITIONED_SCHEMA) { + OPENVINO_THROW("Unsupported ORC schema for NPUW CompiledModel"); } + return deserialize_orc_container(stream, plugin, properties, true, {}); } -std::shared_ptr ov::npuw::CompiledModel::import_model( +std::shared_ptr ov::npuw::CompiledModel::deserialize_orc_container( std::istream& stream, const std::shared_ptr& plugin, - const ov::AnyMap& properties) { - LOG_INFO("Deserializing CompiledModel..."); - LOG_BLOCK(); + const ov::AnyMap& properties, + bool require_weights_bank, + const std::function& decrypt) { + ov::npuw::orc::ScopedReadSection root(stream); + if (root.header().type != kOrcType || root.header().version > kOrcVersion || + ov::npuw::orc::has_flag(root.header().flags, ov::npuw::orc::SectionFlag::LEAF)) { + OPENVINO_THROW("Unsupported ORC NPUW root section"); + } - using namespace ov::npuw::s11n; + // Variables populated during metadata read but used in the child-reading + // phase below. Extracted before the version branch so both paths share + // the same child-consuming lambdas. + std::shared_ptr compiled; + bool is_weightless = false; + std::string encrypted_payload; - // Sanity check magic number - ov::npuw::s11n::IndicatorType serialization_indicator; - read(stream, serialization_indicator); - NPUW_ASSERT(serialization_indicator == NPUW_SERIALIZATION_INDICATOR && "This blob wasn't serialized via NPUW!"); - - ov::npuw::s11n::IndicatorType compiled_indicator; - read(stream, compiled_indicator); - NPUW_ASSERT(compiled_indicator == NPUW_COMPILED_MODEL_INDICATOR && - "This blob wasn't serialized via CompiledModel!"); - - // Deserialize general meta info - int vmajor, vminor, vpatch; - std::string s11n_version; - read(stream, vmajor); - read(stream, vminor); - read(stream, vpatch); - read(stream, s11n_version); - - if (vmajor != OPENVINO_VERSION_MAJOR || vminor != OPENVINO_VERSION_MINOR || vpatch != OPENVINO_VERSION_PATCH || - s11n_version != std::string(NPUW_SERIALIZATION_VERSION)) { - OPENVINO_THROW("This blobs was serialized with different OV version!", - "\nSerialized by OV ", - vmajor, - '.', - vminor, - '.', - vpatch, - "\nCurrent OV version ", - OPENVINO_VERSION_MAJOR, - '.', - OPENVINO_VERSION_MINOR, - '.', - OPENVINO_VERSION_PATCH, - "\nNPUW serialized by version ", - s11n_version, - "\nNPUW current serialization version ", - NPUW_SERIALIZATION_VERSION); - } - - bool encrypted = false; - read(stream, encrypted); - bool is_weightless = true; - read(stream, is_weightless); + const bool encrypted = ov::npuw::orc::has_flag(root.header().flags, ov::npuw::orc::SectionFlag::ENCRYPTED); - auto read_and_finalize_bank = [&](std::istream& model_stream, - const std::shared_ptr& compiled) { - // Deserialize weights bank name - std::string bank_name; - read(model_stream, bank_name); + // Read the model-level metadata fields. In v0 these were written as raw + // s11n bytes directly into the container body; v1 wraps them in an + // explicit META leaf child so the section tree is fully self-describing. + auto read_meta_fields = [&]() { + auto meta_stream = ov::npuw::s11n::Stream::reader(stream); + std::string model_name; + ov::ParameterVector parameters; + ov::NodeVector results; + meta_stream & model_name & parameters & results; + + auto ov_model = std::make_shared(ov::as_output_vector(results), parameters, model_name); + compiled = std::make_shared(ov_model, plugin, true); + compiled->m_name = std::move(model_name); + meta_stream & compiled->m_inputs_to_submodels_inputs & compiled->m_outputs_to_submodels_outputs & + compiled->m_param_subscribers & compiled->m_submodels_input_to_prev_output; + meta_stream & compiled->m_dev_list; + meta_stream & compiled->m_cfg; + compiled->m_cfg.parseEnvVars(); + meta_stream & compiled->m_non_npuw_props; + meta_stream & is_weightless; + meta_stream & compiled->m_bf16_consts; + if (encrypted) { + meta_stream & encrypted_payload; + } + }; + + if (root.header().version == 0) { + // v0: metadata written as raw s11n bytes at the start of the container body. + read_meta_fields(); + } else if (root.header().version == 1) { + // v1: metadata wrapped in a META leaf child section. + ov::npuw::orc::ScopedReadSection meta(stream); + if (meta.header().type != ov::npuw::orc::META_SECTION_TYPE || + !ov::npuw::orc::has_flag(meta.header().flags, ov::npuw::orc::SectionFlag::LEAF)) { + OPENVINO_THROW("Expected ORC NPUW metadata section, got type ", meta.header().type); + } + read_meta_fields(); + meta.expect_end(); + } else { + OPENVINO_THROW("Unsupported ORC NPUW PartitionedModel version ", root.header().version); + } + + compiled->m_import_weights_ctx = make_import_weights_ctx(properties, is_weightless, compiled->m_bf16_consts); + bool have_weights = false; + auto peek_child_header = [](std::istream& child_source) { + const auto saved = child_source.tellg(); + auto peek_stream = ov::npuw::s11n::Stream::reader(child_source); + ov::npuw::orc::SectionHeader header; + peek_stream & header; + child_source.seekg(saved); + return header; + }; + auto consume_submodel = [&](std::istream& child_source) { + ov::npuw::orc::ScopedReadSection child(child_source); + if (child.header().type != CompiledModelDesc::kOrcType) { + OPENVINO_THROW("Unexpected ORC child type ID ", child.header().type, " in NPUW CompiledModel container"); + } + if (child.header().version != CompiledModelDesc::kOrcVersion) { + OPENVINO_THROW("Unsupported ORC NPUW subgraph version ", child.header().version); + } + + compiled->m_compiled_submodels.emplace_back(); + auto& submodel = compiled->m_compiled_submodels.back(); + auto child_stream = ov::npuw::s11n::Stream::reader(child_source); + ov::npuw::s11n::SubmodelDeserializeCtx submodel_ctx( + plugin, + submodel.compiled_model, + [&](std::size_t device_index) { + return compiled->m_dev_list.at(device_index); + }, + [&](const std::string& device) { + return make_submodel_import_config(device, compiled->m_cfg); + }); + submodel.serialize(child_stream, compiled->m_import_weights_ctx, std::nullopt, &submodel_ctx); + child.expect_end(); + }; + + auto consume_weights_bank = [&](std::istream& child_source) { + ov::npuw::orc::ScopedReadSection child(child_source); + if (child.header().type != weights::Bank::kOrcType) { + OPENVINO_THROW("Unexpected ORC child type ID ", child.header().type, " in NPUW CompiledModel container"); + } + if (child.header().version != weights::Bank::kOrcVersion) { + OPENVINO_THROW("Unsupported ORC NPUW weights version ", child.header().version); + } + + auto child_stream = ov::npuw::s11n::Stream::reader(child_source); + std::string bank_name; + child_stream & bank_name; + compiled->m_weights_bank = ov::npuw::weights::bank(bank_name, compiled->get_plugin()->get_core(), ""); if (is_weightless) { - compiled->m_weights_bank = ov::npuw::weights::bank(bank_name, compiled->get_plugin()->get_core(), ""); + child.expect_end(); compiled->finalize_weights_bank(); } else { - compiled->m_weights_bank = - ov::npuw::weights::Bank::deserialize(model_stream, compiled->get_plugin()->get_core(), bank_name); + child_stream& * compiled->m_weights_bank; + child.expect_end(); compiled->reconstruct_closure(); } + have_weights = true; }; - if (!encrypted) { - CompiledContext ctx(false, nullptr, nullptr); - auto compiled_model = ov::npuw::CompiledModel::deserialize(stream, plugin, properties, ctx); - NPUW_ASSERT(compiled_model && "Couldn't import NPUW compiled model!"); - read_and_finalize_bank(stream, compiled_model); - LOG_INFO("Done."); - return compiled_model; - } - - EncryptionCallbacks enc_callbacks; - NPUW_ASSERT(properties.count(ov::cache_encryption_callbacks.name()) && - properties.at(ov::cache_encryption_callbacks.name()).as().decrypt && - "Model is encrypted but no decrypt function was provided!"); - enc_callbacks.decrypt = properties.at(ov::cache_encryption_callbacks.name()).as().decrypt; - - LOG_INFO("Decryption will be done via the function provided."); + if (encrypted) { + root.expect_end(); - std::shared_ptr compiled_model = nullptr; - - // Model is encrypted - if (is_weightless) { - std::string encrypted_str; - read(stream, encrypted_str); - std::istringstream decrypted_stream(std::move(enc_callbacks.decrypt(encrypted_str))); - CompiledContext ctx(false, nullptr, nullptr); - compiled_model = ov::npuw::CompiledModel::deserialize(decrypted_stream, plugin, properties, ctx); + const auto decrypt_fn = decrypt ? decrypt : get_decrypt_callback_or_throw(properties); + std::istringstream decrypted_stream(std::move(decrypt_fn(encrypted_payload))); + while (decrypted_stream.peek() != std::char_traits::eof() && + peek_child_header(decrypted_stream).type == CompiledModelDesc::kOrcType) { + consume_submodel(decrypted_stream); + } + if (require_weights_bank) { + if (decrypted_stream.peek() == std::char_traits::eof()) { + OPENVINO_THROW("Missing ORC weights bank container"); + } + consume_weights_bank(decrypted_stream); + } } else { - CompiledContext ctx(true, nullptr, enc_callbacks.decrypt); - compiled_model = ov::npuw::CompiledModel::deserialize(stream, plugin, properties, ctx); + while (!root.done() && peek_child_header(stream).type == CompiledModelDesc::kOrcType) { + consume_submodel(stream); + } + if (require_weights_bank) { + if (root.done()) { + OPENVINO_THROW("Missing ORC weights bank container"); + } + consume_weights_bank(stream); + } else if (!root.done()) { + OPENVINO_THROW("Unexpected ORC child after CompiledModelDesc containers"); + } + root.expect_end(); } - NPUW_ASSERT(compiled_model && "Couldn't import NPUW compiled model!"); - read_and_finalize_bank(stream, compiled_model); + if (require_weights_bank && !have_weights) { + OPENVINO_THROW("Missing ORC weights bank container"); + } - LOG_INFO("Done."); - return compiled_model; + compiled->implement_properties(); + return compiled; } void ov::npuw::CompiledModel::serialize(std::ostream& stream, const ov::npuw::s11n::CompiledContext& enc_ctx) const { - LOG_INFO("Serializing CompiledModel..."); - LOG_BLOCK(); - - using namespace ov::npuw::s11n; - - auto write_model = [&](std::ostream& model_stream) { - // Serialize name - write(model_stream, m_name); - - // Serialize inputs and outputs - write(model_stream, inputs()); - write(model_stream, outputs()); - - // Serialize meta - write(model_stream, m_inputs_to_submodels_inputs); - write(model_stream, m_outputs_to_submodels_outputs); - write(model_stream, m_param_subscribers); - write(model_stream, m_submodels_input_to_prev_output); - - // Write device list - write(model_stream, m_dev_list); - - // Write config - write(model_stream, m_cfg); - // FIXME: utilize overload instead - write(model_stream, m_non_npuw_props.size()); - for (const auto& p : m_non_npuw_props) { - // Skip properties which don't need to/can't be serialized - // FIXME: extend the logic - if (p.first == ov::cache_encryption_callbacks.name()) { - write(model_stream, false); - continue; - } - write(model_stream, true); - write(model_stream, p.first); - write_any(model_stream, p.second); - } - - // Write flow identifier - bool is_weightless = should_use_weightless_flow(m_non_npuw_props, m_cfg, m_const_to_offset); - write(model_stream, is_weightless); - - // Write bf16 consts cache - write(model_stream, enc_ctx.bf16_consts); - - // Create weightless context - WeightsContext ctx(is_weightless, m_const_to_offset); - - // Serialize compiled submodels - write(model_stream, m_compiled_submodels.size()); - for (std::size_t i = 0; i < m_compiled_submodels.size(); ++i) { - auto& subm = m_compiled_submodels[i]; - auto real_idx = subm.replaced_by.value_or(i); - // Write device idx - // FIXME: if there is no compiled submodel, device_it is not set. - auto dev_idx = [&]() { - auto it = std::find(m_dev_list.begin(), m_dev_list.end(), submodel_device(real_idx)); - NPUW_ASSERT(it != m_dev_list.end()); - return it - m_dev_list.begin(); - }; - write(model_stream, real_idx == i ? dev_idx() : 0); - // Write ICompiledModel if it's there - if (subm.compiled_model) { - write(model_stream, true); - // FIXME: workaround for import/export model since import model seem to reset the file pointer - std::stringstream ss; - subm.compiled_model->export_model(ss); - write(model_stream, ss.str()); - } else { - write(model_stream, false); - } - // Write the rest of the submodel desc - subm.serialize(model_stream, ctx); - } - }; - - std::stringstream non_encrypted_stream; if (enc_ctx.encrypted) { NPUW_ASSERT(enc_ctx.encrypt && "Encryption function isn't provided!"); - non_encrypted_stream.copyfmt(stream); - write_model(non_encrypted_stream); - std::string encrypted_str = enc_ctx.encrypt(non_encrypted_stream.str()); - write(stream, encrypted_str); - } else { - write_model(stream); } - - LOG_INFO("Done."); + // Preserve the caller-provided BF16 cache for nested serialization. + // LLMCompiledModel relies on this to pass original-model BF16 metadata down + // into child CompiledModel blobs. + serialize_orc_container(stream, + false, + enc_ctx.encrypted ? enc_ctx.encrypt : std::function{}, + &enc_ctx.bf16_consts); } std::shared_ptr ov::npuw::CompiledModel::deserialize( @@ -1318,198 +1345,15 @@ std::shared_ptr ov::npuw::CompiledModel::deserialize( const std::shared_ptr& plugin, const ov::AnyMap& properties, const ov::npuw::s11n::CompiledContext& enc_ctx) { - LOG_INFO("Deserializing CompiledModel..."); - LOG_BLOCK(); - - using namespace ov::npuw::s11n; - - auto read_model = [&](std::istream& model_stream) { - // Deserialize model name first - std::string model_name; - read(stream, model_name); - - // Create a dummy CompiledModel with an empty ov::Model - this will skip the constructor flow - // to continue deserialization - ov::ParameterVector parameters; - ov::NodeVector results; - - read(stream, parameters); - read(stream, results); - - auto ov_model = std::make_shared(ov::as_output_vector(results), parameters, model_name); - - auto compiled = std::make_shared(ov_model, plugin, true); - - // Deserialize meta - compiled->m_name = model_name; - read(stream, compiled->m_inputs_to_submodels_inputs); - read(stream, compiled->m_outputs_to_submodels_outputs); - read(stream, compiled->m_param_subscribers); - read(stream, compiled->m_submodels_input_to_prev_output); - - // Deserialize device list - read(stream, compiled->m_dev_list); - - // Deserialize config - read(stream, compiled->m_cfg); - compiled->m_cfg.parseEnvVars(); - // FIXME: utilize overload instead - std::size_t props_size; - read(stream, props_size); - for (std::size_t i = 0; i < props_size; ++i) { - bool should_read = true; - read(stream, should_read); - // Skip properties which don't need to/can't be deserialized - // FIXME: extend the logic - if (!should_read) { - continue; - } - std::string key; - read(stream, key); - ov::Any val; - read_any(stream, val); - compiled->m_non_npuw_props[key] = std::move(val); - } - compiled->implement_properties(); - - // Read flow identifier - bool is_weightless = false; - read(stream, is_weightless); - - // Read bf16 consts cache - read(stream, compiled->m_bf16_consts); - - // Initialize weights stream if weightless flow - std::string weights_path; - std::shared_ptr model_ptr; - // Cache model's constants - WeightsContext::ConstsCache consts_cache; - ov::FileHandleProvider handle_provider = nullptr; - if (is_weightless) { - // Check if weights_handle_provider function is provided - if (const auto handle_it = properties.find(ov::intel_npu::npuw::weights_handle_provider.name()); - handle_it != properties.end()) { - if (handle_it->second.is()) { - handle_provider = handle_it->second.as(); - } - } else if (properties.find(ov::weights_path.name()) != properties.end()) { - weights_path = properties.at(ov::weights_path.name()).as(); - NPUW_ASSERT(!weights_path.empty() && - "Empty weights_path. Please provide WEIGHTS_PATH or MODEL_PTR in the configuration."); - } else if (properties.find(ov::hint::model.name()) != properties.end()) { - model_ptr = std::const_pointer_cast( - properties.at(ov::hint::model.name()).as>()) - ->clone(); - NPUW_ASSERT( - model_ptr && - "Empty model passed in MODEL_PTR. Please provide WEIGHTS_PATH or MODEL_PTR in the configuration."); - // NOTE: very important to do preprocessing here since MODEL_PTR contains the original model - // without any plugin preprocessing. Due to some passes (e.g. bf16_to_f16) we might get - // modified weights and thus mismatch during weights read. - // FIXME: consider adding a config it will influence weight modification in the future - pre_load_transform(model_ptr, {}); - // Fill the cache - for (const auto& node : model_ptr->get_ordered_ops()) { - if (ov::op::util::is_constant(node)) { - const auto& c = std::static_pointer_cast(node); - auto rt_info = c->get_rt_info(); - auto weightless_cache_attr = rt_info.find(ov::WeightlessCacheAttribute::get_type_info_static()); - if (weightless_cache_attr == rt_info.end()) { - continue; - } - std::size_t offset = - weightless_cache_attr->second.as().bin_offset; - std::size_t size = c->get_byte_size(); - consts_cache[{offset, size}] = node; - } - } - } else { - NPUW_ASSERT(false && "Blob is weightless but no WEIGHTS_PATH nor MODEL_PTR property is provided!"); - } - } - - ov::npuw::s11n::WeightsPtr weights = nullptr; - if (is_weightless) { - std::shared_ptr mapped_memory; - // Use handle_provider if available, otherwise use default mmap with weights_path - if (handle_provider) { - ov::FileHandle handle = handle_provider(); - mapped_memory = ov::load_mmap_object(handle); - } else if (!weights_path.empty()) { - mapped_memory = ov::load_mmap_object(ov::util::make_path(weights_path)); - } - if (mapped_memory) { - weights = std::make_shared(mapped_memory->data(), - mapped_memory->size(), - mapped_memory); - } - } - - // FIXME: prolong lifetime of ov::Model for import with MODEL_PTR. - // Unclear why it's needed, but without saving consts_cache until bank evaluation, - // the memory is freed somewhere. - compiled->m_import_weights_ctx = - WeightsContext(weights, weights_path, consts_cache, compiled->m_bf16_consts, handle_provider); - - // Deserialize compiled submodels - std::size_t subm_size = 0; - read(stream, subm_size); - compiled->m_compiled_submodels.resize(subm_size); - for (std::size_t i = 0; i < subm_size; ++i) { - std::size_t device_idx = 0; - read(stream, device_idx); - - bool has_compiled_model = false; - read(stream, has_compiled_model); - - // Build import config for NPU device - ov::AnyMap import_config; - const auto& device = compiled->m_dev_list[device_idx]; - if (ov::npuw::util::starts_with(device, "NPU")) { - // Pass NPU_RUN_INFERENCES_SEQUENTIALLY if NPUW_UNFOLD_IREQS is enabled - if (compiled->m_cfg.get<::intel_npu::NPUW_UNFOLD_IREQS>()) { - import_config["NPU_RUN_INFERENCES_SEQUENTIALLY"] = "YES"; - } - } - - if (has_compiled_model) { - // Import model from the plugin - // FIXME: workaround for import/export model since import model seems to reset the file pointer - std::string buf; - read(stream, buf); - std::stringstream buffer(buf); - compiled->m_compiled_submodels[i].compiled_model = - plugin->get_core()->import_model(buffer, device, import_config); - } - - // Create unified deserialization context for submodels with dynamic mechanisms - // (Pyramid Attention, Host Flash Attention, etc.) - ov::npuw::s11n::SubmodelDeserializeCtx submodel_ctx(plugin, - device, - compiled->m_compiled_submodels[i].compiled_model, - import_config); - compiled->m_compiled_submodels[i].deserialize(stream, compiled->m_import_weights_ctx, submodel_ctx); - } - - compiled->implement_properties(); - compiled->report_io(); - LOG_INFO("Done."); - return compiled; - }; - - std::shared_ptr compiled = nullptr; if (enc_ctx.encrypted) { - std::string encrypted_string; - read(stream, encrypted_string); - std::istringstream decrypted_stream(std::move(enc_ctx.decrypt(encrypted_string))); - compiled = read_model(decrypted_stream); - } else { - compiled = read_model(stream); - } - - NPUW_ASSERT(compiled && "Couldn't create NPUW compiled model!"); - - return compiled; + NPUW_ASSERT(enc_ctx.decrypt && "Decryption function isn't provided!"); + } + return deserialize_orc_container( + stream, + plugin, + properties, + false, + enc_ctx.encrypted ? enc_ctx.decrypt : std::function{}); } void ov::npuw::CompiledModel::reconstruct_closure() { @@ -1540,6 +1384,14 @@ std::size_t ov::npuw::CompiledModel::num_submodels() const { return m_compiled_submodels.size(); } +bool ov::npuw::CompiledModel::attention_dynamic_enabled() const { + return m_cfg.get<::intel_npu::NPUW_ATTN_DYN>(); +} + +bool ov::npuw::CompiledModel::attention_no_copy() const { + return m_cfg.get<::intel_npu::NPUW_ATTN_NO_COPY>(); +} + std::shared_ptr ov::npuw::CompiledModel::get_weights_bank() const { return m_weights_bank; } @@ -1796,7 +1648,7 @@ bool ov::npuw::CompiledModel::compile_for_success(std::size_t id, const std::vec dump_on_fail(id, device, "Avoided due to workaround"); continue; } - if (desc.attention.has_value()) { + if (ov::npuw::attn::get_compiled_dynamic(desc.pipeline.context) != nullptr) { bool has_dynamic = false; for (const auto& input : desc.model->inputs()) { if (input.get_partial_shape().is_dynamic()) { @@ -1836,61 +1688,113 @@ bool ov::npuw::CompiledModel::compile_for_success(std::size_t id, const std::vec }; }; - auto make_wrapped = [&](const std::shared_ptr& model, - const std::string& profile_suffix, - const std::vector& devs) -> ov::SoPtr { + auto make_failsafe = [&](const std::shared_ptr& model, + const std::string& profile_suffix, + const std::vector& devs) -> ov::SoPtr { + if (!m_cfg.get<::intel_npu::NPUW_FALLBACK_EXEC>()) { + std::exception_ptr last_failure; + auto factory = make_factory(model, profile_suffix); + for (const auto& device : devs) { + try { + auto compiled = factory(device); + OPENVINO_ASSERT(compiled._ptr != nullptr, + "Failsafe factory returned null compiled model for device ", + device); + return compiled; + } catch (...) { + last_failure = std::current_exception(); + } + } + if (last_failure) { + std::rethrow_exception(last_failure); + } + OPENVINO_THROW("No candidate devices available for compilation"); + } + return ov::npuw::failsafe::CompiledModel::create(model, get_plugin(), devs, make_factory(model, profile_suffix)); }; - if (auto& moe_experts_opt = desc.moe_experts; moe_experts_opt.has_value()) { - LOG_INFO("Compiling MoE expert models for Subgraph[" << id << "]..."); - LOG_BLOCK(); - - auto& moe_experts = moe_experts_opt.value(); - LOG_INFO("Total MoE models to compile: " << moe_experts._models_to_compile.size()); - - for (const auto& entry : moe_experts._models_to_compile) { - LOG_INFO("Compiling MoE expert model for chunk_size=" << entry.first); - moe_experts.set_compiled_model( - entry.first, - make_wrapped(entry.second, "/moe_chunk_" + std::to_string(entry.first), devices)); - LOG_INFO("Successfully compiled MoE expert model for chunk_size=" << entry.first); + auto make_wrapped = [&](const std::shared_ptr& model, + const std::string& profile_suffix, + const std::vector& devs) -> ov::SoPtr { + auto main_cm = make_failsafe(model, profile_suffix, devs); + if (m_acc_check) { + const auto exec_devs = main_cm->get_property(ov::execution_devices.name()).as>(); + const std::string actual_device = exec_devs.empty() ? "" : exec_devs.front(); + if (actual_device != m_ref_device) { + LOG_INFO("Wrapping with AccuracyChecked (main: " << actual_device << ", ref: " << m_ref_device << ")."); + auto ref_cm = compile_submodel(model, m_ref_device); + return ov::npuw::accuracy_checked::CompiledModel::create(model, + get_plugin(), + std::move(main_cm), + std::move(ref_cm), + m_acc_check); + } } + return main_cm; + }; - const auto& compiled_models = moe_experts._compiled_models; - OPENVINO_ASSERT(!compiled_models.empty(), "Expected at least one compiled MoE expert model"); - desc.compiled_model = compiled_models.begin()->second; - LOG_INFO("MoE expert compilation complete for Subgraph[" << id << "]"); + if (desc.pipeline.compile_executor) { + ov::npuw::v1::subgraphs::CompileContext compile_context{desc.model, desc.compiled_model, devices, make_wrapped}; + desc.pipeline.compile_executor(compile_context); } else { desc.compiled_model = make_wrapped(desc.model, "", main_candidates); } - if (auto& moe_downstream_opt = desc.moe_experts_downstream; moe_downstream_opt.has_value()) { - LOG_INFO("Wrapping compiled model into MoE downstream for Subgraph[" << id << "]..."); - OPENVINO_ASSERT(desc.compiled_model, "Expected compiled model before wrapping MoE downstream"); - moe_downstream_opt->set_compiled_model(std::move(desc.compiled_model)); - desc.compiled_model = moe_downstream_opt->_compiled_model; - } - - if (desc.pyramid_attention.has_value()) { + if (auto* pyramid_attn = ov::npuw::attn::get_compiled_pyramid(desc.pipeline.context)) { LOG_INFO("Compiling pyramid attention submodels for Subgraph[" << id << "]..."); LOG_BLOCK(); - auto& pyramid_attn = desc.pyramid_attention.value(); - const auto& pyramid_attn_models = pyramid_attn._models_to_compile; + const auto& pyramid_attn_models = pyramid_attn->_models_to_compile; const size_t total_models = pyramid_attn_models.size(); const size_t models_to_compile = total_models > 0 ? total_models - 1 : 0; std::vector> compiled_models(total_models); + // Check if device supports strided I/O for non-last pyramid models. + // The last model reuses the already-compiled main subgraph model (compiled without + // enable_strides_for), so strided I/O only applies to the explicitly compiled models. + bool support_strides_for = false; + std::string npu_device_str; + std::string saved_strides; + for (const auto& device : devices) { + if (ov::npuw::util::starts_with(device, "NPU") && models_to_compile > 0 && + !pyramid_attn->_attention_infos.empty()) { + const auto supported_properties = + get_npuw_plugin()->get_core()->get_property(device, ov::supported_properties); + support_strides_for = std::find(supported_properties.begin(), + supported_properties.end(), + ov::intel_npu::enable_strides_for.name()) != supported_properties.end(); + if (support_strides_for) { + pyramid_attn->_can_use_tensor_view = true; + const auto& first_model = pyramid_attn_models[0]; + const auto& first_info = pyramid_attn->_attention_infos[0]; + npu_device_str = device; + const auto& strides_key = ov::intel_npu::enable_strides_for.name(); + const ov::Any existing_any = + ov::npuw::util::at::_(m_meta_devices[npu_device_str]).at_or(strides_key, std::string{}); + saved_strides = existing_any.as(); + std::string strided_inputs = saved_strides; + for (const auto& param : first_info.params) { + if (!strided_inputs.empty()) { + strided_inputs += ","; + } + strided_inputs += first_model->inputs()[param.idx].get_any_name(); + } + m_meta_devices[npu_device_str][strides_key] = strided_inputs; + LOG_INFO("Enabled using tensor view for device: " << device + << " for pyramid inputs: " << strided_inputs); + } // if(support_strides_for) + } // if(npu) + } // for(devices) + auto compile_one = [&](size_t model_id) { compiled_models[model_id] = make_wrapped(pyramid_attn_models[model_id], "/pyramid_" + std::to_string(model_id), devices); }; - const bool par_opt = m_cfg.get<::intel_npu::NPUW_PARALLEL_COMPILE>(); if (par_opt && models_to_compile > 0) { ov::parallel_for(models_to_compile, compile_one); @@ -1905,18 +1809,29 @@ bool ov::npuw::CompiledModel::compile_for_success(std::size_t id, const std::vec compiled_models[total_models - 1] = desc.compiled_model; } - pyramid_attn.set_compiled_models(std::move(compiled_models)); + pyramid_attn->set_compiled_models(std::move(compiled_models)); LOG_INFO("Pyramid attention compilation complete for Subgraph[" << id << "]"); - } - if (desc.host_flash_attention.has_value()) { + if (support_strides_for && !npu_device_str.empty()) { + const auto& strides_key = ov::intel_npu::enable_strides_for.name(); + if (saved_strides.empty()) { + m_meta_devices[npu_device_str].erase(strides_key); + } else { + m_meta_devices[npu_device_str][strides_key] = saved_strides; + } + } + } // if (pyramid_attn) + + if (auto* hfa = ov::npuw::attn::get_compiled_hfa(desc.pipeline.context)) { LOG_INFO("Compiling host flash attention tile models for Subgraph[" << id << "]..."); LOG_BLOCK(); - auto& hfa = desc.host_flash_attention.value(); - if (!hfa._tile_model_to_compile) { + if (!hfa->_tile_model_to_compile) { LOG_WARN("Host flash attention tile model is null, skipping compilation"); } else { + bool supports_strides_for = false; + std::string npu_device_str; + std::string saved_strides; for (const auto& device : devices) { if (!ov::npuw::util::starts_with(device, "NPU")) { continue; @@ -1924,26 +1839,45 @@ bool ov::npuw::CompiledModel::compile_for_success(std::size_t id, const std::vec const auto supported_properties = get_npuw_plugin()->get_core()->get_property(device, ov::supported_properties); - const auto support_strides_for = + const bool support_strides_for = std::find(supported_properties.begin(), supported_properties.end(), ov::intel_npu::enable_strides_for.name()) != supported_properties.end(); if (!support_strides_for) { - continue; + break; } - hfa._can_use_tensor_view = true; - auto strided_inputs_name = std::string(hfa_tile_input_id_to_string(HFATileInputId::K_TILE)) + "," + - std::string(hfa_tile_input_id_to_string(HFATileInputId::V_TILE)); - m_meta_devices[device][ov::intel_npu::enable_strides_for.name()] = strided_inputs_name; - LOG_INFO("Enabled using tensor view for device: " << device << " for inputs: " << strided_inputs_name); + hfa->_can_use_tensor_view = true; + npu_device_str = device; + const auto& strides_key = ov::intel_npu::enable_strides_for.name(); + const ov::Any existing_any = + ov::npuw::util::at::_(m_meta_devices[device]).at_or(strides_key, std::string{}); + saved_strides = existing_any.as(); + std::string strided_inputs = saved_strides; + if (!strided_inputs.empty()) { + strided_inputs += ","; + } + strided_inputs += std::string(hfa_tile_input_id_to_string(HFATileInputId::K_TILE)) + "," + + std::string(hfa_tile_input_id_to_string(HFATileInputId::V_TILE)); + m_meta_devices[device][strides_key] = strided_inputs; + supports_strides_for = true; + LOG_INFO("Enabled using tensor view for device: " << device << " for inputs: " << strided_inputs); } - hfa.set_compiled_tile_model(make_wrapped(hfa._tile_model_to_compile, "/hfa_tile", devices)); - hfa.set_compiled_final_tile_model(desc.compiled_model); + hfa->set_compiled_tile_model(make_wrapped(hfa->_tile_model_to_compile, "/hfa_tile", devices)); + hfa->set_compiled_final_tile_model(desc.compiled_model); LOG_INFO("Host flash attention compilation complete for Subgraph[" << id << "]"); + + if (supports_strides_for && !npu_device_str.empty()) { + const auto& strides_key = ov::intel_npu::enable_strides_for.name(); + if (saved_strides.empty()) { + m_meta_devices[npu_device_str].erase(strides_key); + } else { + m_meta_devices[npu_device_str][strides_key] = saved_strides; + } + } } - } + } // if (hfa) return true; } @@ -2011,15 +1945,16 @@ void ov::npuw::CompiledModel::dump_subgraph_model(std::size_t id, LOG_INFO("Dumping Subgraph[" << id << "]"); LOG_BLOCK(); if (real_id != id) { - LOG_INFO("NOTE: Dumping Subgraph[" << real_id << "]" << " as it is a function body for Subgraph[" << id << "]"); + LOG_INFO("NOTE: Dumping Subgraph[" << real_id << "]" + << " as it is a function body for Subgraph[" << id << "]"); } const std::string dump_dir = m_cfg.get<::intel_npu::NPUW_DUMP_SUBS_DIR>(); // Dump MoE expert models if present - if (m_compiled_submodels[id].moe_experts) { + if (const auto* moe_experts = ov::npuw::moe::get_compiled_experts(m_compiled_submodels[id].pipeline.context)) { LOG_INFO("NOTE: Subgraph[" << id << "] has MoE experts mechanism."); - const auto& moe_models = m_compiled_submodels[id].moe_experts.value()._models_to_compile; + const auto& moe_models = moe_experts->_models_to_compile; if (moe_models.empty()) { LOG_WARN("MoE experts models are empty (already compiled and cleared)"); @@ -2038,6 +1973,21 @@ void ov::npuw::CompiledModel::dump_subgraph_model(std::size_t id, return; // MoE experts don't have a single model to dump } + // Dump MoE downstream model if present (the shape-reduced model passed to compile) + if (const auto* moe_downstream = + ov::npuw::moe::get_compiled_downstream(m_compiled_submodels[id].pipeline.context)) { + LOG_INFO("NOTE: Subgraph[" << id << "] has MoE downstream mechanism."); + if (moe_downstream->_model_to_compile) { + std::string downstream_model_name = format_subgraph_name(id, funcall) + "_moe_downstream.xml"; + std::string downstream_model_dump_path = ov::util::path_join({dump_dir, downstream_model_name}).string(); + ov::save_model(moe_downstream->_model_to_compile, downstream_model_dump_path); + LOG_INFO("Wrote " << downstream_model_dump_path); + } else { + LOG_WARN("MoE downstream model already compiled and cleared, cannot dump"); + } + return; + } + const auto model_to_dump = m_compiled_submodels[real_id].model; if (!model_to_dump) { LOG_WARN("Model is null, cannot dump Subgraph[" << id << "]"); @@ -2050,9 +2000,9 @@ void ov::npuw::CompiledModel::dump_subgraph_model(std::size_t id, LOG_INFO("Wrote " << model_dump_path); // Dump pyramid attention models if present - if (m_compiled_submodels[id].pyramid_attention) { + if (const auto* pyramid_attn = ov::npuw::attn::get_compiled_pyramid(m_compiled_submodels[id].pipeline.context)) { LOG_INFO("NOTE: Subgraph[" << id << "] has a pyramid attention mechanism."); - const auto& pyramid_attention_models = m_compiled_submodels[id].pyramid_attention.value()._models_to_compile; + const auto& pyramid_attention_models = pyramid_attn->_models_to_compile; for (std::size_t idx = 0; idx < pyramid_attention_models.size(); ++idx) { std::string pyramid_attention_model_name = format_subgraph_name(id, funcall) + "_pyramid_" + ov::npuw::util::fmt(idx, pyramid_attention_models.size()) + @@ -2065,16 +2015,15 @@ void ov::npuw::CompiledModel::dump_subgraph_model(std::size_t id, } // Dump host flash attention models if present - if (m_compiled_submodels[id].host_flash_attention) { + if (const auto* hfa = ov::npuw::attn::get_compiled_hfa(m_compiled_submodels[id].pipeline.context)) { LOG_INFO("NOTE: Subgraph[" << id << "] has a host flash attention mechanism."); - const auto& hfa_tile_model = m_compiled_submodels[id].host_flash_attention.value()._tile_model_to_compile; + const auto& hfa_tile_model = hfa->_tile_model_to_compile; std::string hfa_tile_model_name = format_subgraph_name(id, funcall) + "_hfa_tile.xml"; std::string hfa_tile_model_dump_path = ov::util::path_join({dump_dir, hfa_tile_model_name}).string(); ov::save_model(hfa_tile_model, hfa_tile_model_dump_path); LOG_INFO("Wrote " << hfa_tile_model_dump_path); - const auto& hfa_final_tile_model = - m_compiled_submodels[id].host_flash_attention.value()._final_tile_model_to_compile; + const auto& hfa_final_tile_model = hfa->_final_tile_model_to_compile; std::string hfa_final_tile_model_name = format_subgraph_name(id, funcall) + "_hfa_final_tile.xml"; std::string hfa_final_tile_model_dump_path = ov::util::path_join({dump_dir, hfa_final_tile_model_name}).string(); @@ -2110,8 +2059,9 @@ void ov::npuw::CompiledModel::dump_subgraph_composition(const std::vector_models_to_compile; if (!moe_models.empty()) { for (const auto& [chunk_size, model] : moe_models) { moe_subgraphs.push_back(base_name + "_moe_chunk_" + std::to_string(chunk_size) + ".xml"); @@ -2119,17 +2069,21 @@ void ov::npuw::CompiledModel::dump_subgraph_composition(const std::vector_models_to_compile; for (std::size_t idx = 0; idx < pyramid_models.size(); ++idx) { attn_subgraphs.push_back(base_name + "_pyramid_" + ov::npuw::util::fmt(idx, pyramid_models.size()) + ".xml"); } } - if (m_compiled_submodels[real_id].host_flash_attention) { + if (ov::npuw::attn::get_compiled_hfa(m_compiled_submodels[real_id].pipeline.context) != nullptr) { attn_subgraphs.push_back(base_name + "_hfa_tile.xml"); attn_subgraphs.push_back(base_name + "_hfa_final_tile.xml"); } @@ -2233,8 +2187,19 @@ std::shared_ptr ov::npuw::CompiledModel::create_bas return m_dev_list.size() == 1 || !m_cfg.get<::intel_npu::NPUW_FALLBACK_EXEC>(); }; + auto no_subgraph_behavior_concern = [&]() { + for (std::size_t idx = 0u; idx < m_compiled_submodels.size(); idx++) { + if (m_compiled_submodels[idx].pipeline.runtime_behavior.has_value()) { + LOG_WARN("Subgraph[" << idx << "] has a runtime behavior, unfold can't be done"); + return false; + } + } + return true; + }; + std::shared_ptr result; - if (m_cfg.get<::intel_npu::NPUW_UNFOLD_IREQS>() && no_spatial_unpack() && no_failsafe_concern()) { + if (m_cfg.get<::intel_npu::NPUW_UNFOLD_IREQS>() && no_spatial_unpack() && no_failsafe_concern() && + no_subgraph_behavior_concern()) { result = std::make_shared(non_const_this_sptr); } else { result = std::make_shared(non_const_this_sptr); @@ -2266,12 +2231,10 @@ std::shared_ptr ov::npuw::CompiledModel::get_runtime_model() co OPENVINO_NOT_IMPLEMENTED; } -std::shared_ptr ov::npuw::CompiledModel::get_npuw_plugin() const { +std::shared_ptr ov::npuw::CompiledModel::get_npuw_plugin() const { auto plugin = get_plugin(); OPENVINO_ASSERT(plugin); - auto npuw_plugin = std::dynamic_pointer_cast(plugin); - OPENVINO_ASSERT(npuw_plugin); - return npuw_plugin; + return plugin; } ov::Any ov::npuw::CompiledModel::get_property(const std::string& name) const { @@ -2295,10 +2258,6 @@ std::string ov::npuw::CompiledModel::submodel_device(const std::size_t idx) cons return ""; } - if (comp_subm_desc.switched_to_ref) { - return m_ref_device; - } - const auto exec_devs = comp_subm_desc.compiled_model->get_property(ov::execution_devices.name()).as>(); return exec_devs.empty() ? "" : exec_devs.front(); @@ -2489,6 +2448,7 @@ void ov::npuw::CompiledModel::implement_properties() { BIND(npuw::partitioning::online::dump_plan, NPUW_ONLINE_DUMP_PLAN), BIND(npuw::partitioning::plan, NPUW_PLAN), BIND(npuw::partitioning::fold, NPUW_FOLD), + BIND(npuw::partitioning::fold_only, NPUW_FOLD_ONLY), BIND(npuw::partitioning::cwai, NPUW_CWAI), BIND(npuw::partitioning::dyn_quant, NPUW_DQ), BIND(npuw::partitioning::dyn_quant_full, NPUW_DQ_FULL), @@ -2505,6 +2465,7 @@ void ov::npuw::CompiledModel::implement_properties() { BIND(npuw::partitioning::dcoff_with_scale, NPUW_DCOFF_SCALE), BIND(npuw::partitioning::attn_hfa_fused, NPUW_ATTN_HFA_FUSED), BIND(npuw::parallel_compilation, NPUW_PARALLEL_COMPILE), + BIND(npuw::ensure_compatibility, NPUW_ENSURE_COMPATIBILITY), BIND(npuw::funcall_async, NPUW_FUNCALL_ASYNC), BIND(npuw::unfold_ireqs, NPUW_UNFOLD_IREQS), BIND(npuw::weights_bank, NPUW_WEIGHTS_BANK), diff --git a/src/plugins/intel_npu/src/plugin/npuw/compiled_model.hpp b/src/plugins/intel_npu/src/plugin/npuw/compiled_model.hpp index 3ec230f52079..29022332790c 100644 --- a/src/plugins/intel_npu/src/plugin/npuw/compiled_model.hpp +++ b/src/plugins/intel_npu/src/plugin/npuw/compiled_model.hpp @@ -18,11 +18,13 @@ #include "openvino/runtime/shared_buffer.hpp" #include "openvino/runtime/so_ptr.hpp" #include "openvino/util/mmap_object.hpp" +#include "orc/schema_npuw.hpp" #include "partitioning/partitioning.hpp" #include "perf.hpp" #include "pyramid_attention.hpp" #include "serialization.hpp" #include "spatial.hpp" +#include "v1/subgraph_pipeline.hpp" #include "weights_bank.hpp" namespace intel_npu { @@ -70,6 +72,10 @@ class ICompiledModel_v0 : public ov::npuw::ICompiledModel { // Forward declarations class InferRequest; +namespace v1::subgraphs { +class Context; +} + namespace moe { class MoEExecutor; } @@ -80,9 +86,21 @@ class CompiledModel : public ov::npuw::ICompiledModel_v0 { std::map>>; public: + static constexpr ov::npuw::orc::TypeId kOrcType = + static_cast(ov::npuw::orc::schema_npuw::PartitionedModel::ID); + // Version 1 introduced an explicit META leaf child section for the model's + // own serialized fields, making the PartitionedModel container fully + // navigable without schema knowledge. Version 0 (pre-release only) wrote + // those fields as raw s11n bytes at the start of the container body. + static constexpr ov::npuw::orc::Version kOrcVersion = 1u; + CompiledModel(const std::shared_ptr& model, const std::shared_ptr& plugin, const ov::AnyMap& properties); + CompiledModel(const std::shared_ptr& model, + const std::shared_ptr& plugin, + const ov::AnyMap& properties, + const ov::npuw::v1::subgraphs::PatternRegistry* subgraph_patterns); CompiledModel(const std::shared_ptr& model, const std::shared_ptr& plugin, const bool serialized); @@ -107,6 +125,8 @@ class CompiledModel : public ov::npuw::ICompiledModel_v0 { std::shared_ptr internal_request) const override; std::string submodel_device(std::size_t idx) const override; std::size_t num_submodels() const override; + bool attention_dynamic_enabled() const; + bool attention_no_copy() const; std::shared_ptr get_weights_bank() const override; void set_weights_bank(std::shared_ptr bank) override; void finalize_weights_bank() override; @@ -139,6 +159,24 @@ class CompiledModel : public ov::npuw::ICompiledModel_v0 { const std::shared_ptr& plugin, const ov::AnyMap& properties, const ov::npuw::s11n::CompiledContext& ctx); + static std::shared_ptr deserialize_orc(std::istream& stream, + const std::shared_ptr& plugin, + const ov::AnyMap& properties); + void serialize_orc(std::ostream& stream) const; + static std::shared_ptr deserialize_orc_container( + std::istream& stream, + const std::shared_ptr& plugin, + const ov::AnyMap& properties, + bool require_weights_bank, + const std::function& decrypt); + void serialize_orc_container(std::ostream& stream, + bool include_weights_bank, + const std::function& encrypt, + // Nested serializers may need to preserve BF16 metadata + // collected by a parent model (e.g. LLMCompiledModel) + // rather than this object's local snapshot. + const ov::npuw::s11n::BF16Cache* bf16_consts = nullptr) const; + void ensure_phase0_compatibility() const; // This is used for removing too long output tensor names to fix some compilation issues // NB: These two methods has nothing to do with this particular class and should be @@ -146,7 +184,7 @@ class CompiledModel : public ov::npuw::ICompiledModel_v0 { void remove_long_output_names(const std::shared_ptr& model); void fill_empty_tensor_names(const std::shared_ptr& model); - std::shared_ptr get_npuw_plugin() const; + std::shared_ptr get_npuw_plugin() const; std::shared_ptr create_sync_infer_request() const override; // API for easily create and manage NPUW infer-requests (promoted to ICompiledModel_v0) @@ -212,6 +250,12 @@ class CompiledModel : public ov::npuw::ICompiledModel_v0 { void init_profiling(); struct CompiledModelDesc { + static constexpr ov::npuw::orc::TypeId kOrcType = + static_cast(ov::npuw::orc::schema_npuw::Subgraph::ID); + // Version 0 is the frozen baseline on the wire. Any further layout + // changes must be introduced through a new versioned payload. + static constexpr ov::npuw::orc::Version kOrcVersion = 0u; + std::set devices_to_avoid; std::shared_ptr model; ov::SoPtr compiled_model; @@ -221,35 +265,9 @@ class CompiledModel : public ov::npuw::ICompiledModel_v0 { Subgraph::Gather host_gather; Subgraph::QuantUnpackGather quant_unpack_gather; std::optional spatial; - std::optional attention; - std::optional pyramid_attention; - std::optional host_flash_attention; - std::optional moe_experts; - std::optional moe_experts_downstream; - - // Infer requests for pyramid attention models (if pyramid_attention is present) - std::vector> pyramid_infer_requests; - - // Pipeline infer requests for pyramid attention models (if pyramid_attention is present and pipelining is - // enabled) - std::vector> pyramid_pipeline_requests; - - // HFA tile model indices for infer request vectors - enum HFATileIdx : size_t { - REGULAR_TILE = 0, // Regular tile model (intermediate tiles) - FINAL_TILE = 1, // Final tile model (last tile with division and transpose) - COUNT = 2 // Total number of HFA tile models - }; - - // Infer requests for host flash attention tile models (if host_flash_attention is present) - // [REGULAR_TILE]: regular tile model, [FINAL_TILE]: final tile model - std::vector> hfa_infer_requests; + ov::npuw::v1::subgraphs::CompiledPipeline pipeline; - // Pipeline infer requests for host flash attention tile models (if host_flash_attention is present and - // pipelining is enabled) - std::vector> hfa_pipeline_requests; - - // Infer requests for MoE expert models with different chunk sizes (if moe_experts is present) + // Infer requests for MoE expert models with different chunk sizes (if MoE expert state is present) // Map: chunk_size -> infer_request std::map> moe_infer_requests; @@ -279,17 +297,13 @@ class CompiledModel : public ov::npuw::ICompiledModel_v0 { bool forced_to_fcall = false; - // FIXME: Take it out of structure - ov::SoPtr ref_compiled_model; - bool switched_to_ref = false; - // Metrics execution_stats stat; - void serialize(std::ostream& stream, const ov::npuw::s11n::WeightsContext& ctx) const; - void deserialize(std::istream& stream, - const ov::npuw::s11n::WeightsContext& ctx, - const ov::npuw::s11n::SubmodelDeserializeCtx& submodel_ctx); + void serialize(ov::npuw::s11n::Stream& stream, + const ov::npuw::s11n::WeightsContext& ctx, + std::optional orc_device_index = std::nullopt, + const ov::npuw::s11n::SubmodelDeserializeCtx* submodel_ctx = nullptr); }; std::vector m_compiled_submodels; diff --git a/src/plugins/intel_npu/src/plugin/npuw/embedding/embedding_infer_request.cpp b/src/plugins/intel_npu/src/plugin/npuw/embedding/embedding_infer_request.cpp index 0282e5c0c2b1..7a801545b95c 100644 --- a/src/plugins/intel_npu/src/plugin/npuw/embedding/embedding_infer_request.cpp +++ b/src/plugins/intel_npu/src/plugin/npuw/embedding/embedding_infer_request.cpp @@ -21,7 +21,8 @@ ov::npuw::EmbeddingInferRequest::EmbeddingInferRequest(const std::shared_ptrm_prefill_compiled->create_infer_request(); + m_prefill_base_request = m_npuw_llm_compiled_model->m_prefill_compiled->create_base_infer_request(); + m_prefill_request = m_npuw_llm_compiled_model->m_prefill_compiled->wrap_async_infer_request(m_prefill_base_request); for (const auto& input_port : m_prefill_request->get_compiled_model()->inputs()) { m_prefill_in_ports.emplace(input_port.get_any_name(), input_port); // Cache past_key_values ports for efficient clearing @@ -120,6 +121,8 @@ void ov::npuw::EmbeddingInferRequest::infer_chunked_prefill(ov::SoPtrcopy_to(pos_ids_slice._ptr); + m_prefill_base_request->update_history_size(kvcache_desc.num_stored_tokens); + m_prefill_request->infer(); auto src = ov::npuw::util::make_tensor_slice(output_tensor, diff --git a/src/plugins/intel_npu/src/plugin/npuw/embedding/embedding_infer_request.hpp b/src/plugins/intel_npu/src/plugin/npuw/embedding/embedding_infer_request.hpp index c3e45d597d84..9759d4417d7b 100644 --- a/src/plugins/intel_npu/src/plugin/npuw/embedding/embedding_infer_request.hpp +++ b/src/plugins/intel_npu/src/plugin/npuw/embedding/embedding_infer_request.hpp @@ -34,6 +34,7 @@ class EmbeddingInferRequest : public ov::npuw::LLMInferBaseRequest { std::unordered_map> m_prefill_out_ports; std::vector> m_prefill_past_kv_ports; + std::shared_ptr m_prefill_base_request; std::shared_ptr m_prefill_request; ov::SoPtr m_input_ids_in_tensor; diff --git a/src/plugins/intel_npu/src/plugin/npuw/embedding/remove_empty_kv_inputs.cpp b/src/plugins/intel_npu/src/plugin/npuw/embedding/remove_empty_kv_inputs.cpp index 93c2c53b3ddd..dc712df0fe75 100644 --- a/src/plugins/intel_npu/src/plugin/npuw/embedding/remove_empty_kv_inputs.cpp +++ b/src/plugins/intel_npu/src/plugin/npuw/embedding/remove_empty_kv_inputs.cpp @@ -4,6 +4,7 @@ #include "remove_empty_kv_inputs.hpp" +#include "../util.hpp" #include "openvino/core/graph_util.hpp" #include "openvino/op/ops.hpp" #include "openvino/op/util/node_util.hpp" @@ -60,6 +61,19 @@ class RemoveEmptyKVTensors : public ov::pass::MatcherPass { auto callback = [=](opp::Matcher& m) { auto& node_to_output = m.get_pattern_value_map(); auto matched_param = ov::as_type_ptr(node_to_output.at(param).get_node_shared_ptr()); + + // Note: Additional precaution for only KVCache parameters to match. + // As an example linear cache of GDN blocks can match pattern above too. + // Note 2: In Whisper model KVCache state names got badly handled by StatefulToStateless pass, + // so their created parameters names are not standard. As Whisper rename goes after this + // transformation, we need to ensure matching of these incorrect names as well + // to remove all empty KV inputs. + std::string param_name = matched_param->get_friendly_name(); + if (!ov::npuw::util::isPastKeyValuesKey(param_name) && !ov::npuw::util::isPastKeyValuesValue(param_name) && + !ov::npuw::util::isRestoredPastKeyValueParam(param_name)) { + return false; + } + auto matched_node_concat = node_to_output.at(concat).get_node_shared_ptr(); ctx.get().old_params.push_back(matched_param); diff --git a/src/plugins/intel_npu/src/plugin/npuw/host_flash_attention.cpp b/src/plugins/intel_npu/src/plugin/npuw/host_flash_attention.cpp index 5ed823a8319a..ec945bde2af0 100644 --- a/src/plugins/intel_npu/src/plugin/npuw/host_flash_attention.cpp +++ b/src/plugins/intel_npu/src/plugin/npuw/host_flash_attention.cpp @@ -618,18 +618,23 @@ static std::shared_ptr create_hfa_tile_model(const ov::Shape& q_shape // ======================================================================== LOG_DEBUG("Using traditional broadcast computation (DISABLED loop-based) - materializes K/V broadcast"); - // Broadcast K and V tiles from kv_num_heads to num_heads - auto [k_broadcast, v_broadcast] = broadcast_kv_tiles(f32_nodes.k_tile_f32, - f32_nodes.v_tile_f32, - batch, - num_heads, - kv_num_heads, - tile_size, - head_dim); if (fused_flash_attention) { - // Execute fused flash attention node - results = execute_fused_flash_attention(f32_nodes, f32_nodes.q_f32, k_broadcast, v_broadcast, is_final_tile); + // Execute fused flash attention node MHA, GQA + results = execute_fused_flash_attention(f32_nodes, + f32_nodes.q_f32, + f32_nodes.k_tile_f32, + f32_nodes.v_tile_f32, + is_final_tile); } else { + // Broadcast K and V tiles from kv_num_heads to num_heads + auto [k_broadcast, v_broadcast] = broadcast_kv_tiles(f32_nodes.k_tile_f32, + f32_nodes.v_tile_f32, + batch, + num_heads, + kv_num_heads, + tile_size, + head_dim); + // Execute flash attention algorithm with broadcasted K/V results = execute_host_flash_attention(f32_nodes, f32_nodes.q_f32, // Q: original 4D diff --git a/src/plugins/intel_npu/src/plugin/npuw/infer_request_utils.cpp b/src/plugins/intel_npu/src/plugin/npuw/infer_request_utils.cpp index 7a484df9acf0..d9460559803c 100644 --- a/src/plugins/intel_npu/src/plugin/npuw/infer_request_utils.cpp +++ b/src/plugins/intel_npu/src/plugin/npuw/infer_request_utils.cpp @@ -4,8 +4,11 @@ #include "infer_request_utils.hpp" +#include + #include "logging.hpp" #include "openvino/runtime/make_tensor.hpp" // get_tensor_impl +#include "util.hpp" #include "util_xarch.hpp" // FIXME: Use ov::npuw::util::view instead @@ -208,3 +211,69 @@ void ov::npuw::util::pad_position_ids(const ov::SoPtr& padded_posit padded_data + padded_offset + keep_elements); } } + +void ov::npuw::util::copy_per_layer_inputs_chunk_to_right(const ov::SoPtr& src, + const ov::SoPtr& dst, + uint32_t src_offset_tokens, + uint32_t chunk_tokens) { + const auto src_seq_len = src->get_shape().at(1); + const auto dst_seq_len = dst->get_shape().at(1); + OPENVINO_ASSERT(chunk_tokens > 0u, "chunk_tokens must be > 0"); + OPENVINO_ASSERT(src_offset_tokens <= src_seq_len, + "src_offset_tokens exceeds source seq_len. src_offset_tokens=", + src_offset_tokens, + ", src_seq_len=", + src_seq_len); + OPENVINO_ASSERT(chunk_tokens <= src_seq_len - src_offset_tokens, + "chunk range exceeds source seq_len by given offset. src_offset_tokens=", + src_offset_tokens, + ", chunk_tokens=", + chunk_tokens, + ", src_seq_len=", + src_seq_len); + OPENVINO_ASSERT(chunk_tokens <= dst_seq_len, + "chunk_tokens exceeds destination seq_len. chunk_tokens=", + chunk_tokens, + ", dst_seq_len=", + dst_seq_len); + + const auto src_seq_len_bytes = src->get_byte_size(); + const auto dst_seq_len_bytes = dst->get_byte_size(); + OPENVINO_ASSERT(src_seq_len > 0u, "per_layer_inputs src has zero seq_len"); + OPENVINO_ASSERT(dst_seq_len > 0u, "per_layer_inputs dst has zero seq_len"); + OPENVINO_ASSERT(src_seq_len_bytes % src_seq_len == 0u, + "per_layer_inputs src byte size is not divisible by seq_len. byte_size=", + src_seq_len_bytes, + ", seq_len=", + src_seq_len); + OPENVINO_ASSERT(dst_seq_len_bytes % dst_seq_len == 0u, + "per_layer_inputs dst byte size is not divisible by seq_len. byte_size=", + dst_seq_len_bytes, + ", seq_len=", + dst_seq_len); + const auto src_per_token_bytes = src_seq_len_bytes / src_seq_len; + const auto dst_per_token_bytes = dst_seq_len_bytes / dst_seq_len; + OPENVINO_ASSERT(src_per_token_bytes == dst_per_token_bytes, + "per-token byte size mismatch between src and dst. src=", + src_per_token_bytes, + ", dst=", + dst_per_token_bytes); + + OPENVINO_ASSERT(static_cast(chunk_tokens) <= std::numeric_limits::max() / src_per_token_bytes, + "chunk byte size overflow. chunk_tokens=", + chunk_tokens, + ", per_token_bytes=", + src_per_token_bytes); + OPENVINO_ASSERT(static_cast(src_offset_tokens) <= std::numeric_limits::max() / src_per_token_bytes, + "offset byte size overflow. src_offset_tokens=", + src_offset_tokens, + ", per_token_bytes=", + src_per_token_bytes); + + const size_t chunk_bytes = static_cast(chunk_tokens) * src_per_token_bytes; + const size_t offset_bytes = static_cast(src_offset_tokens) * src_per_token_bytes; + + std::copy_n(reinterpret_cast(src->data()) + offset_bytes, + chunk_bytes, + reinterpret_cast(dst->data()) + dst->get_byte_size() - chunk_bytes); +} diff --git a/src/plugins/intel_npu/src/plugin/npuw/infer_request_utils.hpp b/src/plugins/intel_npu/src/plugin/npuw/infer_request_utils.hpp index bd1d89ae8444..e79c34a352c8 100644 --- a/src/plugins/intel_npu/src/plugin/npuw/infer_request_utils.hpp +++ b/src/plugins/intel_npu/src/plugin/npuw/infer_request_utils.hpp @@ -43,6 +43,13 @@ std::optional> find_port_by_names(const std::vector& padded_position_ids, const ov::SoPtr& position_ids); +// Copy chunk_tokens from src starting at src_offset_tokens into dst, right-aligned on seq_len dim. +// Leading bytes in dst are left unchanged. +void copy_per_layer_inputs_chunk_to_right(const ov::SoPtr& src, + const ov::SoPtr& dst, + uint32_t src_offset_tokens, + uint32_t chunk_tokens); + } // namespace util } // namespace npuw } // namespace ov diff --git a/src/plugins/intel_npu/src/plugin/npuw/just_sync_infer_request.cpp b/src/plugins/intel_npu/src/plugin/npuw/just_sync_infer_request.cpp index fb298bb28e4d..769b4af352f7 100644 --- a/src/plugins/intel_npu/src/plugin/npuw/just_sync_infer_request.cpp +++ b/src/plugins/intel_npu/src/plugin/npuw/just_sync_infer_request.cpp @@ -5,13 +5,17 @@ #include "just_sync_infer_request.hpp" #include +#include +#include #include #include +#include "attn/attn_subgraph.hpp" #include "compiled_model.hpp" #include "host_flash_attention.hpp" #include "infer_request_utils.hpp" // to utilize copy_tensor_by_dim #include "logging.hpp" +#include "moe/moe_subgraph.hpp" #include "openvino/core/except.hpp" #include "openvino/core/parallel.hpp" #include "openvino/runtime/iasync_infer_request.hpp" @@ -54,6 +58,48 @@ std::string ov::npuw::JustInferRequest::subgraph_device(size_t idx) { return m_npuw_model->submodel_device(idx); } +void ov::npuw::JustInferRequest::set_active_subrequest(size_t idx, ov::SoPtr request) { + m_subrequests[idx] = std::move(request); +} + +ov::SoPtr ov::npuw::JustInferRequest::get_pipeline_subrequest(size_t idx) const { + return idx < m_funcall_pipeline.size() ? m_funcall_pipeline[idx].subrequest : ov::SoPtr{}; +} + +void ov::npuw::JustInferRequest::set_pipeline_subrequest(size_t idx, ov::SoPtr request) { + if (idx < m_funcall_pipeline.size()) { + m_funcall_pipeline[idx].subrequest = std::move(request); + } +} + +bool ov::npuw::JustInferRequest::is_subrequest_pipelined(size_t idx) const { + return is_pipelined(idx); +} + +std::size_t ov::npuw::JustInferRequest::history_size() const { + return get_history_size(); +} + +bool ov::npuw::JustInferRequest::subgraph_needs_copy(std::size_t idx) const { + return needs_copy(idx); +} + +bool ov::npuw::JustInferRequest::attention_no_copy() const { + return m_npuw_model->attention_no_copy(); +} + +const ov::SoPtr& ov::npuw::JustInferRequest::compiled_submodel(size_t idx) const { + return m_npuw_model->m_compiled_submodels.at(idx).compiled_model; +} + +const ov::npuw::v1::subgraphs::CompiledPipeline& ov::npuw::JustInferRequest::subgraph_pipeline(size_t idx) const { + return m_npuw_model->m_compiled_submodels.at(idx).pipeline; +} + +std::size_t ov::npuw::JustInferRequest::subgraph_param_base(size_t idx) const { + return m_npuw_model->m_compiled_submodels.at(idx).param_base; +} + // ==================================================================================================== // Memory Access Simulation & Function Memory Management // ==================================================================================================== @@ -245,19 +291,11 @@ ov::npuw::JustInferRequest::JustInferRequest(const std::shared_ptroutputs().size(); - m_hfa_io[i].outputs.resize(num_outputs); - } // if(hfa) - // Initialize the MoE IO placeholders, if required - if (proto_comp_model_desc.moe_experts) { + if (ov::npuw::moe::has_compiled_experts(proto_comp_model_desc.pipeline)) { // Sanity check: ensure only one MoE function type exists if (has_moe && moe_real_idx != real_idx) { OPENVINO_THROW("Only single MoE type is permitted for model"); @@ -369,23 +373,6 @@ ov::npuw::JustInferRequest::JustInferRequest(const std::shared_ptrm_compiled_submodels[real_idx]; - if (proto_comp_model_desc.pyramid_attention) { - setup_pyramid_infer_requests(real_idx, is_piped); - } - // Create HFA tile infer requests if this function has host flash attention - if (proto_comp_model_desc.host_flash_attention) { - setup_hfa_infer_requests(real_idx, - is_piped, - /* enable_hfa_optimizations */ true); - } - } - LOG_INFO("DONE"); } // for(submodels) @@ -422,6 +409,7 @@ ov::npuw::JustInferRequest::JustInferRequest(const std::shared_ptrm_cfg.get<::intel_npu::NPUW_ATTN_DYN>()) { - // Even if the attention is detected and ready to go dynamic, - // force it on the full range - LOG_WARN("Dynamic capability is enabled, but won't be used due to user preference"); - m_attention_selector.reset(new runtime::attention::All()); - } else { - const auto& dyn = m_npuw_model->m_compiled_submodels.at(dynamic_sub_idx).attention.value(); - m_attention_selector = runtime::attention::PositionIDs::find(dyn, *this); - if (!m_attention_selector) { - LOG_WARN("Dynamic capability is enabled, but no run-time features were found."); - m_attention_selector.reset(new runtime::attention::All()); - } - } - LOG_VERB("Done"); + // Initialize MoE executor if MoE was detected + if (has_moe) { + initialize_moe_executor(); } +} - // Handle pyramid attention - if (has_pyramid) { - const auto& pyramid_dyn = m_npuw_model->m_compiled_submodels.at(pyramid_sub_idx).pyramid_attention.value(); - const auto pyramid_count = pyramid_dyn._compiled_models.size(); - if (!m_npuw_model->m_cfg.get<::intel_npu::NPUW_ATTN_DYN>()) { - // Even if the attention is detected and ready to go pyramid, - // force it on the full range - m_pyramid_selector.reset(new runtime::pyramid_attention::All(pyramid_count)); - } else { - m_pyramid_selector = runtime::pyramid_attention::PositionIDs::find(pyramid_dyn, *this); - if (!m_pyramid_selector) { - LOG_WARN("Pyramid dynamic capability is enabled, but no run-time features were found."); - // Create All selector with the number of pyramid models - m_pyramid_selector.reset(new runtime::pyramid_attention::All(pyramid_count)); - } +void ov::npuw::JustInferRequest::initialize_subgraph_behaviors() { + m_subgraph_behaviors.resize(m_num_submodels); + m_subgraph_runtime_states.resize(m_num_submodels); + for (std::size_t idx = 0; idx < m_num_submodels; idx++) { + const auto& comp_model_desc = m_npuw_model->m_compiled_submodels[idx]; + if (!comp_model_desc.compiled_model && !comp_model_desc.replaced_by) { + continue; + } + const auto* spec = get_runtime_behavior_spec(idx); + if (spec == nullptr) { + continue; + } + if (spec->factory) { + m_subgraph_behaviors[idx] = spec->factory(spec->context); } + if (m_subgraph_behaviors[idx]) { + continue; + } + const auto* post_legacy_hook = spec->context.get_if(); + if (!post_legacy_hook) { + continue; + } + m_subgraph_behaviors[idx] = std::make_unique( + [post_legacy_hook = *post_legacy_hook](ov::npuw::v1::subgraphs::InferContext& ctx) { + ctx.legacy_infer(); + post_legacy_hook(ctx); + }); } +} - if (has_hfa) { - const auto& hfa_desc = m_npuw_model->m_compiled_submodels.at(hfa_sub_idx).host_flash_attention.value(); - const size_t query_size = hfa_desc._sdpa_attention_info._query_size; - m_hfa_selector = runtime::host_flash_attention::PositionIDs::find(query_size, *this); - if (!m_hfa_selector) { - // HFA requires PositionIDs selector - cannot fallback to 'All' like Dynamic/Pyramid - // because HFA uses tile-based execution without a full-range infer request - OPENVINO_THROW("HFA dynamic capability is enabled, but no run-time features were found."); - } - LOG_VERB("Done"); +ov::npuw::v1::subgraphs::InferContext ov::npuw::JustInferRequest::make_behavior_context(std::size_t real_idx, + std::size_t idx) { + const bool is_function_call = m_npuw_model->m_compiled_submodels[idx].replaced_by.has_value(); + return ov::npuw::v1::subgraphs::InferContext{ + *m_npuw_model, + *this, + idx, + real_idx, + m_subrequests[real_idx], + real_idx < m_subgraph_runtime_states.size() ? &m_subgraph_runtime_states[real_idx] : nullptr, + [this, real_idx, idx]() { + legacy_infer(real_idx, idx); + }, + is_function_call ? std::function{[this, idx]() { + function_prologue(idx); + }} + : std::function{}, + [this, real_idx, idx]() { + OPENVINO_ASSERT(m_moe_executor != nullptr, "Expected MoE executor for opaque MoE run"); + m_moe_executor->run(real_idx, idx); + }}; +} + +bool ov::npuw::JustInferRequest::bind_behavior_input(std::size_t idx, + std::size_t real_idx, + std::size_t input_idx, + const ov::SoPtr& tensor, + RqPtr request) { + auto* behavior = get_subgraph_behavior(idx); + if (behavior == nullptr) { + return false; } + auto ctx = ov::npuw::v1::subgraphs::InferContext{ + *m_npuw_model, + *this, + idx, + real_idx, + std::move(request), + real_idx < m_subgraph_runtime_states.size() ? &m_subgraph_runtime_states[real_idx] : nullptr, + {}, + {}, + {}}; + return behavior->bind_function_input(ctx, input_idx, tensor); +} - // Initialize MoE executor if MoE was detected - if (has_moe) { - initialize_moe_executor(); +const ov::npuw::v1::subgraphs::RuntimeBehaviorSpec* ov::npuw::JustInferRequest::get_runtime_behavior_spec( + std::size_t idx) const { + if (idx >= m_npuw_model->m_compiled_submodels.size()) { + return nullptr; } + const auto& desc = m_npuw_model->m_compiled_submodels[idx]; + const auto behavior_idx = desc.replaced_by.value_or(idx); + const auto& behavior_desc = m_npuw_model->m_compiled_submodels[behavior_idx]; + if (!behavior_desc.pipeline.runtime_behavior.has_value()) { + return nullptr; + } + return &behavior_desc.pipeline.runtime_behavior.value(); +} + +ov::npuw::v1::subgraphs::ISubgraphBehavior* ov::npuw::JustInferRequest::get_subgraph_behavior(std::size_t idx) const { + if (idx >= m_subgraph_behaviors.size()) { + return nullptr; + } + return m_subgraph_behaviors[idx].get(); +} + +bool ov::npuw::JustInferRequest::behavior_handles_function_prologue(std::size_t idx) const { + const auto* spec = get_runtime_behavior_spec(idx); + return spec != nullptr && spec->handles_function_prologue; } void ov::npuw::JustInferRequest::set_tensor(const ov::Output& port, @@ -620,21 +661,20 @@ void ov::npuw::JustInferRequest::prepare_for_infer() { LOG_DEBUG("Preparing to infer..."); LOG_BLOCK(); - if (m_pyramid_selector) { - m_pyramid_selector->prepare(get_history_size()); - - // Get the pyramid model ID based on current sequence length (updated in prepare()) - auto pyramid_id = m_pyramid_selector->pyramid_id(); + // Adjust spatial input range, if supported + if (m_spatial_selector) { + m_spatial_selector->prepare(); + } - for (auto&& id : m_funcall_heads) { - auto& comp_model_desc = m_npuw_model->m_compiled_submodels[id]; - if (comp_model_desc.pyramid_attention.has_value()) { - m_subrequests[id] = comp_model_desc.pyramid_infer_requests[pyramid_id]; - if (is_pipelined(id)) { - m_funcall_pipeline[id].subrequest = comp_model_desc.pyramid_pipeline_requests[pyramid_id]; - } - } + for (std::size_t idx = 0; idx < m_num_submodels; idx++) { + auto* behavior = get_subgraph_behavior(idx); + if (behavior == nullptr) { + continue; } + auto& comp_model_desc = m_npuw_model->m_compiled_submodels[idx]; + const auto real_idx = comp_model_desc.replaced_by.value_or(idx); + auto ctx = make_behavior_context(real_idx, idx); + behavior->prepare(ctx); } // Submit global parameters (if needed) for the first subgraph @@ -647,27 +687,6 @@ void ov::npuw::JustInferRequest::prepare_for_infer() { unpack_closure(id, m_subrequests[id]); } - // Adjust spatial input range, if supported - if (m_spatial_selector) { - m_spatial_selector->prepare(); - } - - // So do the dynamic range - if (m_attention_selector) { - m_attention_selector->prepare(get_history_size()); - } - - // HFA selector - if (m_hfa_selector) { - m_hfa_selector->prepare(get_history_size()); - if (m_hfa_runtime_ctx) { - m_hfa_runtime_ctx->clear_mask_cache(); - } - } - - // FIXME: attention-specific, needs to be moved out after refactoring - m_cached_attention_mask = {}; - LOG_DEBUG("Done"); } @@ -730,19 +749,12 @@ void ov::npuw::JustInferRequest::function_prologue(std::size_t idx) { auto& func_desc = m_npuw_model->m_compiled_submodels[real_idx]; const bool is_spatial = func_desc.spatial.has_value(); - const bool is_dynamic = func_desc.attention.has_value(); - const bool is_pyramid = func_desc.pyramid_attention.has_value(); - const bool is_hfa = func_desc.host_flash_attention.has_value(); - const bool is_moe = func_desc.moe_experts.has_value() || func_desc.moe_experts_downstream.has_value(); - - // Generalized: check if input is neither param nor mask - auto is_non_param_mask = [](const auto& info, std::size_t in_idx) { - const bool not_param = std::none_of(info.params.begin(), info.params.end(), [&](auto&& p) { - return p.idx == in_idx; - }); - const bool not_mask = in_idx != info.mask_idx; - return not_param && not_mask; - }; + const bool is_moe = ov::npuw::moe::has_compiled_state(func_desc.pipeline); + auto* behavior = get_subgraph_behavior(idx); + std::optional behavior_ctx; + if (behavior != nullptr) { + behavior_ctx.emplace(make_behavior_context(real_idx, idx)); + } // Function call prologue: // 1. Walk through function dependencies and set the respective tensors @@ -774,25 +786,9 @@ void ov::npuw::JustInferRequest::function_prologue(std::size_t idx) { if (is_spatial) { // Spatial case - defer m_spatial_io[real_idx].inputs.at(i) = i_tensor; - } else if (is_dynamic) { - // Set tensor only if it is non-dynamic (dynamic are managed by the infer_dynamic) - if (is_non_param_mask(*func_desc.attention, i)) { - m_subrequests[real_idx]->set_tensor(iport, i_tensor); - } else { - m_attention_io[idx].inputs.at(i) = i_tensor; - } - } else if (is_pyramid) { - // Pyramid attention - auto pyramid_id = m_pyramid_selector->pyramid_id(); - const auto& info = func_desc.pyramid_attention.value()._attention_infos[pyramid_id]; - if (is_non_param_mask(info, i)) { - m_subrequests[real_idx]->set_tensor(iport, i_tensor); - } else { - m_attention_io[idx].inputs.at(i) = i_tensor; - } - } else if (is_hfa) { - // Host Flash Attention case - defer, use dedicated HFA I/O structure - m_hfa_io[idx].inputs.at(i) = i_tensor; + } else if (behavior != nullptr && behavior_ctx.has_value() && + behavior->bind_function_input(*behavior_ctx, i, i_tensor)) { + continue; } else if (is_moe) { // MoE layer: delegate to executor for input binding if (m_moe_executor->function_prologue_moe_input(idx, real_idx, i, i_tensor)) { @@ -805,19 +801,6 @@ void ov::npuw::JustInferRequest::function_prologue(std::size_t idx) { } // if (link_iter) } // for(param_base) - // 1.5: Do attention prologue if needed - if (is_dynamic) { - m_profile["attn(act)"] += ov::npuw::perf::ms_to_run([&]() { - function_prologue_attn(real_idx, idx); - }); - } - - if (is_pyramid) { - m_profile["attn(act)"] += ov::npuw::perf::ms_to_run([&]() { - function_prologue_pyramid_attn(real_idx, idx); - }); - } - // 2. Unpack the function closure -- right here, if pipelining if not enabled. // If it is enabled, the flow is a little bit different - see run_subrequest_for_success() // for details. @@ -836,12 +819,12 @@ void ov::npuw::JustInferRequest::function_prologue(std::size_t idx) { LOG_DEBUG("Binding result[" << i << "]..."); auto& oport = func_desc.compiled_model->outputs()[i]; auto o_tensor = m_funcall_result.at({idx, i}); - if (func_desc.moe_experts) { + if (ov::npuw::moe::has_compiled_experts(func_desc.pipeline)) { // MoE case - delegate to executor for output binding m_moe_executor->function_prologue_moe_output(idx, i, o_tensor); - } else if (is_hfa) { - // HFA case - defer, store in dedicated HFA I/O structure - m_hfa_io[idx].outputs.at(i) = o_tensor; + } else if (behavior != nullptr && behavior_ctx.has_value() && + behavior->bind_function_output(*behavior_ctx, i, o_tensor)) { + continue; } else if (!is_spatial) { // Non-spatial case - set immediately m_subrequests[real_idx]->set_tensor(oport, o_tensor); @@ -853,172 +836,6 @@ void ov::npuw::JustInferRequest::function_prologue(std::size_t idx) { LOG_DEBUG("Done"); } -void ov::npuw::JustInferRequest::function_prologue_attn(std::size_t real_idx, std::size_t idx) { - auto& comp_model_desc = m_npuw_model->m_compiled_submodels[real_idx]; - NPUW_ASSERT(comp_model_desc.attention.has_value()); - - auto& r = m_subrequests[real_idx]; - - const auto& dynamic = comp_model_desc.attention.value(); - auto mask_iport = comp_model_desc.compiled_model->inputs()[dynamic.mask_idx]; - - const auto& graph_mask = m_attention_io[idx].inputs.at(dynamic.mask_idx); - const auto this_case = m_attention_selector->this_case(); - auto pos_id = m_attention_selector->length(); - - if (pos_id == -1) { - // Dynamic range couldn't be identified - fallback to the default - // (worst case) behavior - r->set_tensor(mask_iport, graph_mask); - } else { - const auto past_len = m_attention_selector->past_length(); - const auto present_len = dynamic.query_size; - // FIXME: get the right dim - const uint32_t kv_dim = 3; - - auto set_or_copy = [&](const auto& view) { - if (!needs_copy(idx)) { - r->set_tensor(mask_iport, view); - } else { - const auto& dst = r->get_tensor(mask_iport); - dst->set_shape(view->get_shape()); - view->copy_to(dst._ptr); - } - }; - - // Now set the mask. Here comes very strong chunking & SDPA knowledge again - using namespace ov::npuw::runtime; - if (this_case == attention::Selector::Case::GENERATE) { - // Take a view from our "attend_all" mask - const auto& view = ov::npuw::util::view(ov::get_tensor_impl(dynamic.attend_all), kv_dim, 0, past_len + 1); - set_or_copy(view); - } else if (this_case == attention::Selector::Case::PREFILL) { - // Use our in-graph synthesized mask - if (m_cached_attention_mask) { - // All sub models are sharing the same attention mask, we can use the cached attention - // mask directly to avoid redundant tensor copy - m_subrequests[real_idx]->set_tensor(mask_iport, m_cached_attention_mask); - return; - } - - // Handle attention mask concatenation for SDPA: - // The attention mask is composed with 2 parts: - // The 1st part is for the "present", which is at the tail: starting from past_len to context_len - // The 2nd part is for the "past", which is at the beginning: starting from 0 to past_len - auto full_mask_shape = graph_mask->get_shape(); - auto actual_mask_shape = full_mask_shape; - actual_mask_shape[kv_dim] = present_len + past_len; - - // Reshape the input to the proper shape - const auto& dst = r->get_tensor(mask_iport); - dst->set_shape(actual_mask_shape); - - // Copy "present" attention mask - const auto& present_dst_view = ov::npuw::util::view(dst, kv_dim, past_len, present_len); - const auto& present_src_view = - ov::npuw::util::view(graph_mask, kv_dim, full_mask_shape[kv_dim] - present_len, present_len); - present_src_view->copy_to(present_dst_view._ptr); - - // Copy "past" attention mask - if (past_len > 0) { - const auto& past_dst_view = ov::npuw::util::view(dst, kv_dim, 0, past_len); - const auto& past_src_view = ov::npuw::util::view(graph_mask, kv_dim, 0, past_len); - past_src_view->copy_to(past_dst_view._ptr); - } - m_cached_attention_mask = dst; - } else { - NPUW_ASSERT(false && "Reached the unreachable code"); - } - } -} - -void ov::npuw::JustInferRequest::function_prologue_pyramid_attn(std::size_t real_idx, std::size_t idx) { - auto& comp_model_desc = m_npuw_model->m_compiled_submodels[real_idx]; - NPUW_ASSERT(comp_model_desc.pyramid_attention.has_value()); - - auto& r = m_subrequests[real_idx]; - auto pyramid_id = m_pyramid_selector->pyramid_id(); - - const auto& dynamic = comp_model_desc.pyramid_attention.value()._attention_infos[pyramid_id]; - auto mask_iport = - comp_model_desc.pyramid_attention.value()._compiled_models[pyramid_id]->inputs()[dynamic.mask_idx]; - - const auto& graph_mask = m_attention_io[idx].inputs.at(dynamic.mask_idx); - const auto this_case = m_pyramid_selector->this_case(); - const auto present_len = dynamic.query_size; - - // FIXME: get the right dim - const uint32_t kv_dim = 3; - - // Get destination tensor once for all code paths - const auto& dst = r->get_tensor(mask_iport); - - // Lambda: copy attention mask segment from source to destination - auto copy_mask_segment = [&](std::size_t dst_offset, std::size_t src_offset, std::size_t length) { - if (length == 0) { - return; - } - const auto& dst_view = ov::npuw::util::view(dst, kv_dim, dst_offset, length); - const auto& src_view = ov::npuw::util::view(graph_mask, kv_dim, src_offset, length); - ov::npuw::util::copy_tensor_by_dim(src_view, dst_view, kv_dim, kv_dim); - }; - - auto pos_id = m_pyramid_selector->length(); - if (pos_id == -1) { - // Pyramid dynamic range couldn't be identified - fallback to default behavior - r->set_tensor(mask_iport, graph_mask); - return; - } - - // Pyramid dynamic range identified - const auto past_len = m_pyramid_selector->past_length(); - - // Early return: reuse cached attention mask if available - if (m_cached_attention_mask) { - // All sub models are sharing the same attention mask, we can use the cached attention - // mask directly to avoid redundant tensor copy - m_subrequests[real_idx]->set_tensor(mask_iport, m_cached_attention_mask); - return; - } - - // Now set the mask. Here comes very strong chunking & SDPA knowledge again - using namespace ov::npuw::runtime; - const auto full_mask_shape = graph_mask->get_shape(); - - if (this_case == pyramid_attention::Selector::Case::GENERATE) { - const auto dst_shape = dst->get_shape(); - - // Optimization: if shapes match, use the full mask directly - if (dst_shape == full_mask_shape) { - r->set_tensor(mask_iport, graph_mask); - m_cached_attention_mask = graph_mask; - return; - } - - // FIXME: No need to copy whole attention mask, just mark the new tokens to valid - - std::size_t dst_present_offset = dst_shape[kv_dim] - present_len; - - // Copy present mask: tail of source -> tail of destination - copy_mask_segment(dst_present_offset, full_mask_shape[kv_dim] - present_len, present_len); - - // Copy past mask: head of source [0, dst_present_offset) -> head of destination - copy_mask_segment(0, 0, dst_present_offset); - - m_cached_attention_mask = dst; - } else if (this_case == pyramid_attention::Selector::Case::PREFILL) { - // Copy present mask: tail of source -> [past_len, past_len + present_len) - copy_mask_segment(past_len, full_mask_shape[kv_dim] - present_len, present_len); - - // Copy past mask: head of source -> [0, past_len) - copy_mask_segment(0, 0, past_len); - - m_cached_attention_mask = dst; - } else { - NPUW_ASSERT(false && "Unsupported pyramid attention case"); - } -} - void ov::npuw::JustInferRequest::initialize_moe_executor() { LOG_INFO("Creating MoE executor..."); @@ -1041,7 +858,7 @@ void ov::npuw::JustInferRequest::initialize_moe_executor() { auto& real_desc = m_npuw_model->m_compiled_submodels[real_idx]; // Check if the real function body has MoE experts - if (real_desc.moe_experts.has_value()) { + if (ov::npuw::moe::has_compiled_experts(real_desc.pipeline)) { m_moe_executor->prepare(i, real_idx, m_num_submodels, pool_size); } } @@ -1049,211 +866,11 @@ void ov::npuw::JustInferRequest::initialize_moe_executor() { LOG_INFO("MoE executor initialized successfully"); } -void ov::npuw::JustInferRequest::setup_pyramid_infer_requests(std::size_t real_idx, bool is_piped) { - auto& submodel_desc = m_npuw_model->m_compiled_submodels[real_idx]; - if (!submodel_desc.pyramid_attention.has_value()) { - return; - } - - LOG_INFO("Creating pyramid infer requests..."); - LOG_BLOCK(); - - const auto& pyramid_models = submodel_desc.pyramid_attention.value()._compiled_models; - const size_t num_pyramid_models = pyramid_models.size(); - - // Allocate storage for infer requests - submodel_desc.pyramid_infer_requests.resize(num_pyramid_models); - if (is_piped) { - submodel_desc.pyramid_pipeline_requests.resize(num_pyramid_models); - } - - // Create infer requests for all but the last pyramid model - for (size_t model_idx = 0; model_idx + 1 < num_pyramid_models; ++model_idx) { - submodel_desc.pyramid_infer_requests[model_idx] = pyramid_models[model_idx]->create_infer_request(); - if (is_piped) { - submodel_desc.pyramid_pipeline_requests[model_idx] = pyramid_models[model_idx]->create_infer_request(); - } - - // Share input tensors between pyramid and main infer requests - const size_t num_inputs = pyramid_models[model_idx]->inputs().size(); - NPUW_ASSERT(num_inputs == submodel_desc.compiled_model->inputs().size()); - for (size_t input_idx = 0; input_idx < num_inputs; ++input_idx) { - auto pyramid_input = pyramid_models[model_idx]->inputs()[input_idx]; - auto main_input = submodel_desc.compiled_model->inputs()[input_idx]; - - // Get tensor from main infer request and share its memory with the pyramid infer request - auto main_tensor_ptr = m_subrequests[real_idx]->get_tensor(main_input)->data(); - auto pyramid_tensor = submodel_desc.pyramid_infer_requests[model_idx]->get_tensor(pyramid_input); - auto shared_tensor = ov::get_tensor_impl( - ov::Tensor(pyramid_tensor->get_element_type(), pyramid_tensor->get_shape(), main_tensor_ptr)); - submodel_desc.pyramid_infer_requests[model_idx]->set_tensor(pyramid_input, shared_tensor); - - // Repeat for pipeline infer request if pipelined - if (is_piped) { - auto pipeline_tensor = submodel_desc.pyramid_pipeline_requests[model_idx]->get_tensor(pyramid_input); - auto pipeline_tensor_ptr = m_funcall_pipeline[real_idx].subrequest->get_tensor(main_input)->data(); - auto shared_pipeline_tensor = ov::get_tensor_impl( - ov::Tensor(pipeline_tensor->get_element_type(), pipeline_tensor->get_shape(), pipeline_tensor_ptr)); - submodel_desc.pyramid_pipeline_requests[model_idx]->set_tensor(pyramid_input, shared_pipeline_tensor); - } - } - } - - // For the last pyramid model, reuse the original model's infer requests - if (num_pyramid_models > 0) { - const size_t last_model_idx = num_pyramid_models - 1; - LOG_INFO("Reusing original infer requests for last pyramid model[" << last_model_idx << "]"); - submodel_desc.pyramid_infer_requests[last_model_idx] = m_subrequests[real_idx]; - if (is_piped) { - submodel_desc.pyramid_pipeline_requests[last_model_idx] = m_funcall_pipeline[real_idx].subrequest; - } - - // Anchor input tensors here; see m_pyramid_anchor_tensors. - const size_t num_inputs = submodel_desc.compiled_model->inputs().size(); - for (size_t input_idx = 0; input_idx < num_inputs; ++input_idx) { - auto main_input = submodel_desc.compiled_model->inputs()[input_idx]; - m_pyramid_anchor_tensors.emplace_back(real_idx, m_subrequests[real_idx]->get_tensor(main_input)); - if (is_piped) { - m_pyramid_anchor_tensors.emplace_back(real_idx, - m_funcall_pipeline[real_idx].subrequest->get_tensor(main_input)); - } - } - } - - if (num_pyramid_models > 0) { - LOG_INFO("Successfully created " << (num_pyramid_models - 1) - << " new pyramid infer requests and reused 1 original request"); - } -} - -void ov::npuw::JustInferRequest::setup_hfa_infer_requests(std::size_t real_idx, - bool is_piped, - bool enable_hfa_optimizations) { - auto& submodel_desc = m_npuw_model->m_compiled_submodels[real_idx]; - if (!submodel_desc.host_flash_attention.has_value()) { - return; - } - - LOG_INFO("Creating HFA tile infer requests..."); - LOG_BLOCK(); - - const auto& hfa = submodel_desc.host_flash_attention.value(); - - // Allocate storage for infer requests: [REGULAR_TILE] and [FINAL_TILE] - submodel_desc.hfa_infer_requests.resize(CompiledModel::CompiledModelDesc::HFATileIdx::COUNT); - if (is_piped) { - submodel_desc.hfa_pipeline_requests.resize(CompiledModel::CompiledModelDesc::HFATileIdx::COUNT); - } - - LOG_INFO("Creating infer request for HFA regular tile model..."); - submodel_desc.hfa_infer_requests[CompiledModel::CompiledModelDesc::HFATileIdx::REGULAR_TILE] = - hfa._compiled_tile_model->create_infer_request(); - if (is_piped) { - submodel_desc.hfa_pipeline_requests[CompiledModel::CompiledModelDesc::HFATileIdx::REGULAR_TILE] = - hfa._compiled_tile_model->create_infer_request(); - } - - // For final tile model, reuse the main compiled_model's infer request - // because compiled_model points to _compiled_final_tile_model for HFA - LOG_INFO("Reusing main infer request for HFA final tile model"); - submodel_desc.hfa_infer_requests[CompiledModel::CompiledModelDesc::HFATileIdx::FINAL_TILE] = - m_subrequests[real_idx]; - if (is_piped) { - submodel_desc.hfa_pipeline_requests[CompiledModel::CompiledModelDesc::HFATileIdx::FINAL_TILE] = - m_funcall_pipeline[real_idx].subrequest; - } - - // Share input tensors between HFA tile models and main infer request - // Note: Both tile models have the same input structure - const size_t num_inputs = hfa._compiled_tile_model->inputs().size(); - for (size_t input_idx = 0; input_idx < num_inputs; ++input_idx) { - auto tile_input = hfa._compiled_tile_model->inputs()[input_idx]; - auto final_tile_input = hfa._compiled_final_tile_model->inputs()[input_idx]; - - // Directly share tensor from main infer request to regular tile request - auto main_tensor = m_subrequests[real_idx]->get_tensor(final_tile_input); - submodel_desc.hfa_infer_requests[CompiledModel::CompiledModelDesc::HFATileIdx::REGULAR_TILE]->set_tensor( - tile_input, - main_tensor); - - // Repeat for pipeline infer request if pipelined - if (is_piped) { - auto pipeline_tensor = m_funcall_pipeline[real_idx].subrequest->get_tensor(final_tile_input); - submodel_desc.hfa_pipeline_requests[CompiledModel::CompiledModelDesc::HFATileIdx::REGULAR_TILE]->set_tensor( - tile_input, - pipeline_tensor); - } - } - - LOG_INFO("Successfully created HFA tile infer requests with shared input tensors"); - - // Initialize HFA optimizations (mask cache + state double-buffering) if enabled - if (enable_hfa_optimizations) { - LOG_INFO("HFA optimizations are ENABLED (mask cache + state double-buffering)"); - - // Initialize runtime context if needed - if (!m_hfa_runtime_ctx) { - m_hfa_runtime_ctx.emplace(); - } - - LOG_INFO("Pre-allocating HFA mask tile buffers..."); - - // Initialize pre-allocated buffers - m_hfa_runtime_ctx->initialize_mask_cache( - hfa, - m_npuw_model->submodel_device(real_idx), - [this](const ov::element::Type& dtype, const ov::Shape& shape, const std::string& device) { - return allocMem(dtype, shape, device); - }); - - LOG_INFO("Pre-allocated " << m_hfa_runtime_ctx->num_mask_tile_buffers() << " mask tile buffer(s)"); - - // Initialize state buffers and double-buffering for first inference - LOG_INFO("Initializing HFA state tensors and double-buffering..."); - - // Get pre-cached indices - const auto& tile_in = hfa._sdpa_attention_info._tile_input_indices; - - // Get state tensors from regular tile request - auto state_acc = - submodel_desc.hfa_infer_requests[CompiledModel::CompiledModelDesc::HFATileIdx::REGULAR_TILE]->get_tensor( - hfa._compiled_tile_model->inputs()[tile_in.acc]); - auto state_max = - submodel_desc.hfa_infer_requests[CompiledModel::CompiledModelDesc::HFATileIdx::REGULAR_TILE]->get_tensor( - hfa._compiled_tile_model->inputs()[tile_in.max]); - auto state_sum = - submodel_desc.hfa_infer_requests[CompiledModel::CompiledModelDesc::HFATileIdx::REGULAR_TILE]->get_tensor( - hfa._compiled_tile_model->inputs()[tile_in.d]); - - // Initialize state tensors with zeros/minus infinity - runtime::host_flash_attention::HFARuntimeContext::initialize_state_tensors(state_acc, state_max, state_sum); - - // Setup double-buffering with initialized state - runtime::host_flash_attention::HFARuntimeContext::StateBuffers initial_buffers{state_acc, state_max, state_sum}; - - m_hfa_runtime_ctx->initialize_state_buffers( - initial_buffers, - hfa, - m_npuw_model->submodel_device(real_idx), - [this](const ov::element::Type& dtype, const ov::Shape& shape, const std::string& device) { - return allocMem(dtype, shape, device); - }); - - LOG_INFO("HFA state tensors and double-buffering initialized successfully"); - } else { - LOG_INFO("HFA optimizations are DISABLED - will extract mask tiles on-the-fly without state caching"); - - // Clear runtime context if it exists - if (m_hfa_runtime_ctx) { - m_hfa_runtime_ctx.reset(); - } - } -} - void ov::npuw::JustInferRequest::run_subrequest_for_success(std::size_t idx) { auto& comp_model_desc = m_npuw_model->m_compiled_submodels[idx]; const auto real_idx = comp_model_desc.replaced_by.value_or(idx); bool next_prepared = false; + auto* behavior = get_subgraph_behavior(idx); // Feeding the global Parameters is now part of the common // execution pipeline: See how it is done in @@ -1261,9 +878,13 @@ void ov::npuw::JustInferRequest::run_subrequest_for_success(std::size_t idx) { // the subrequest' outputs to global Results, if relevant. bind_global_results(idx); - if (comp_model_desc.replaced_by) { + if (comp_model_desc.replaced_by && !behavior_handles_function_prologue(idx)) { function_prologue(idx); } + if (behavior != nullptr) { + auto ctx = make_behavior_context(real_idx, idx); + behavior->prologue(ctx); + } dump_input_tensors(idx); LOG_DEBUG("Trying to run subrequest[" << idx << "]..."); @@ -1273,6 +894,10 @@ void ov::npuw::JustInferRequest::run_subrequest_for_success(std::size_t idx) { LOG_DEBUG("Done: " << idx << "(exec subrequest)"); dump_output_tensors(idx); // FIXME: Called here unconditionally, need to refactor + if (behavior != nullptr) { + auto ctx = make_behavior_context(real_idx, idx); + behavior->epilogue(ctx); + } if (is_pipelined(idx) && m_funcall_pipeline[idx].next) { // Swap the next (pipelined, semi-prepared) infer request in the chain // with the default (to be accessed next) one. @@ -1283,7 +908,8 @@ void ov::npuw::JustInferRequest::run_subrequest_for_success(std::size_t idx) { void ov::npuw::JustInferRequest::unsafe_during(std::size_t real_idx, std::size_t idx, const std::function& f) { auto& comp_model_desc = m_npuw_model->m_compiled_submodels[real_idx]; - if (!comp_model_desc.spatial && !comp_model_desc.host_flash_attention.has_value() && !comp_model_desc.moe_experts) { + if (!comp_model_desc.spatial && ov::npuw::attn::get_compiled_hfa(comp_model_desc.pipeline.context) == nullptr && + !ov::npuw::moe::has_compiled_experts(comp_model_desc.pipeline)) { // Normal: trigger request asynchronously, run `f` in this context // FIXME: dynamic could hit here too, but it has special logic // around execution which makes it harder to run than a plain start_async() @@ -1299,358 +925,6 @@ void ov::npuw::JustInferRequest::unsafe_during(std::size_t real_idx, std::size_t } } -// ==================================================================================================== -// Host Flash Attention (HFA) Helper Functions -// ==================================================================================================== - -// Extract tile slice with optional type conversion -void ov::npuw::JustInferRequest::hfa_extract_and_copy_tile(const ov::SoPtr& source_tensor, - const ov::SoPtr& dest_tensor, - uint32_t sequence_dim, - int64_t sequence_offset, - int64_t sequence_length, - const std::string& tensor_name) { - if (!dest_tensor->is_continuous()) { - OPENVINO_THROW("HFA tile extraction error: destination tensor for '", - tensor_name, - "' is not continuous - cannot perform direct copy"); - } - - auto source_view = ov::npuw::util::view(source_tensor, sequence_dim, sequence_offset, sequence_length); - const auto dest_type = dest_tensor->get_element_type(); - const auto source_type = source_tensor->get_element_type(); - - if (dest_type == source_type) { - ov::npuw::util::copy_tensor_by_dim(source_view, dest_tensor, sequence_dim, sequence_dim); - } else { - LOG_WARN("Performing type conversion for " << tensor_name << " tile: " << source_type << " -> " << dest_type); - - // Copy to intermediate buffer first - auto intermediate_tensor = ov::Tensor(source_type, source_view->get_shape()); - ov::npuw::util::copy_tensor_by_dim(source_view, - ov::get_tensor_impl(intermediate_tensor), - sequence_dim, - sequence_dim); - - // Convert element-by-element - const size_t total_elements = intermediate_tensor.get_size(); - if (dest_type == ov::element::f32 && source_type == ov::element::f16) { - // FP16 -> FP32 conversion - auto src_data = intermediate_tensor.data(); - auto dst_data = dest_tensor->data(); - for (size_t i = 0; i < total_elements; ++i) { - dst_data[i] = static_cast(src_data[i]); - } - } else if (dest_type == ov::element::f16 && source_type == ov::element::f32) { - // FP32 -> FP16 conversion - auto src_data = intermediate_tensor.data(); - auto dst_data = dest_tensor->data(); - for (size_t i = 0; i < total_elements; ++i) { - dst_data[i] = static_cast(src_data[i]); - } - } else { - OPENVINO_THROW("Unsupported type conversion for ", tensor_name, " tile: ", source_type, " -> ", dest_type); - } - } -} - -// Check if tensor can be reused directly (zero-copy optimization) -bool ov::npuw::JustInferRequest::hfa_can_reuse_tensor_zero_copy(const ov::SoPtr& source_tensor, - const ov::SoPtr& dest_tensor, - uint32_t sequence_dim, - int64_t sequence_offset, - int64_t tile_length) { - const auto source_shape = source_tensor->get_shape(); - const int64_t source_full_length = static_cast(source_shape[sequence_dim]); - - // Zero-copy conditions: - // 1. Offset must be 0 (no slicing from middle) - // 2. Tile length must match full source length (using entire tensor) - // 3. Element types must match (no conversion needed) - return (sequence_offset == 0 && tile_length == source_full_length && - dest_tensor->get_element_type() == source_tensor->get_element_type()); -} - -// ==================================================================================================== -// Host Flash Attention (HFA) Tiled Inference -// ==================================================================================================== -// -// Implements Flash Attention using tile-based processing to reduce memory footprint. -// -// Algorithm: -// - Split KV cache into tiles of size tile_size -// - For tiles [0..N-2]: Use regular_tile_model, output intermediate states (acc, max, d) -// - For tile [N-1]: Use final_tile_model, output final result with division + transpose -// - Maintain numerically stable online softmax across tiles -// -// Optimizations: -// - Zero-copy when tile size equals full tensor size -// - Pre-cached input/output indices to avoid repeated map lookups -// - Automatic FP16/FP32 conversion when needed -// -// ==================================================================================================== - -void ov::npuw::JustInferRequest::run_hfa_tiled_inference(std::size_t real_idx, std::size_t idx) { - // ================================================================================================ - // SECTION 1: Configuration and Validation - // ================================================================================================ - - auto& comp_model_desc = m_npuw_model->m_compiled_submodels[real_idx]; - auto& hfa_desc = comp_model_desc.host_flash_attention.value(); - - NPUW_ASSERT(hfa_desc.is_valid() && "HFA configuration must be valid"); - NPUW_ASSERT(comp_model_desc.hfa_infer_requests.size() == CompiledModel::CompiledModelDesc::HFATileIdx::COUNT && - "HFA infer requests must be created"); - - // Calculate tile configuration - const int64_t tile_size = hfa_desc._tile_size; - const int64_t total_kv_length = m_hfa_selector->context_length(); - const int64_t num_tiles = total_kv_length / tile_size; - - NPUW_ASSERT(total_kv_length % tile_size == 0 && "HFA total KV length must be multiple of tile size for now"); - - // ================================================================================================ - // SECTION 2: Input/Output Tensor Extraction - // ================================================================================================ - - const auto& hfa_inputs = m_hfa_io[idx].inputs; - const auto& hfa_outputs = m_hfa_io[idx].outputs; - const auto& sdpa_info = hfa_desc._sdpa_attention_info; - - // Use pre-cached SDPA indices - const auto& sdpa_in = sdpa_info._sdpa_indices; - - auto past_key_tensor = hfa_inputs.at(sdpa_in.past_key); - auto past_value_tensor = hfa_inputs.at(sdpa_in.past_value); - auto query_tensor = hfa_inputs.at(sdpa_in.query); - auto present_key_tensor = hfa_inputs.at(sdpa_in.present_key); - auto attention_mask_tensor = hfa_inputs.at(sdpa_in.attention_mask); - auto present_value_tensor = hfa_inputs.at(sdpa_in.present_value); - - auto attention_output_tensor = hfa_outputs.at(0); - - // ================================================================================================ - // SECTION 3: State Initialization - // ================================================================================================ - - // Get tile infer requests - auto& regular_tile_request = - comp_model_desc.hfa_infer_requests[CompiledModel::CompiledModelDesc::HFATileIdx::REGULAR_TILE]; - auto& final_tile_request = - comp_model_desc.hfa_infer_requests[CompiledModel::CompiledModelDesc::HFATileIdx::FINAL_TILE]; - - // Use pre-cached indices (populated during compilation) - const auto& tile_in = sdpa_info._tile_input_indices; - const auto& tile_out = sdpa_info._tile_output_indices; - - // Get pre-initialized state buffers from runtime context (initialized during setup phase) - // If optimizations are disabled, get tensors directly from request (no double-buffering) - ov::SoPtr state_acc, state_max, state_sum; - - if (m_hfa_runtime_ctx && m_hfa_runtime_ctx->has_state_buffers()) { - // Use pre-initialized state from current buffer (double-buffering enabled) - const auto& current_buffer = m_hfa_runtime_ctx->get_current_state_buffers(); - - state_acc = current_buffer.acc; - state_max = current_buffer.max; - state_sum = current_buffer.sum; - - regular_tile_request->set_tensor(hfa_desc._compiled_tile_model->inputs()[tile_in.acc], state_acc); - regular_tile_request->set_tensor(hfa_desc._compiled_tile_model->inputs()[tile_in.max], state_max); - regular_tile_request->set_tensor(hfa_desc._compiled_tile_model->inputs()[tile_in.d], state_sum); - } else { - // Optimizations disabled: use tensors directly without double-buffering - state_acc = regular_tile_request->get_tensor(hfa_desc._compiled_tile_model->inputs()[tile_in.acc]); - state_max = regular_tile_request->get_tensor(hfa_desc._compiled_tile_model->inputs()[tile_in.max]); - state_sum = regular_tile_request->get_tensor(hfa_desc._compiled_tile_model->inputs()[tile_in.d]); - - // Initialize state tensors for each inference run (no caching) - runtime::host_flash_attention::HFARuntimeContext::initialize_state_tensors(state_acc, state_max, state_sum); - } - - // Set query tensor once (constant across all tiles) - regular_tile_request->set_tensor(hfa_desc._compiled_tile_model->inputs()[tile_in.q], query_tensor); - final_tile_request->set_tensor(hfa_desc._compiled_final_tile_model->inputs()[tile_in.q], query_tensor); - - // Set output state tensors for regular tile model (will be updated in-place after each tile execution) - regular_tile_request->set_tensor(hfa_desc._compiled_tile_model->outputs()[tile_out.acc], state_acc); - regular_tile_request->set_tensor(hfa_desc._compiled_tile_model->outputs()[tile_out.max], state_max); - regular_tile_request->set_tensor(hfa_desc._compiled_tile_model->outputs()[tile_out.d], state_sum); - - // Set accumulated state tensors as inputs for final tile (will read from regular tiles' outputs) - final_tile_request->set_tensor(hfa_desc._compiled_final_tile_model->inputs()[tile_in.acc], state_acc); - final_tile_request->set_tensor(hfa_desc._compiled_final_tile_model->inputs()[tile_in.max], state_max); - final_tile_request->set_tensor(hfa_desc._compiled_final_tile_model->inputs()[tile_in.d], state_sum); - - // Final attention output - final_tile_request->set_tensor(hfa_desc._compiled_final_tile_model->outputs()[0], attention_output_tensor); - - // ================================================================================================ - // SECTION 4: Tile Processing Loop - // ================================================================================================ - - // Dimension configuration for tensor slicing - const uint32_t K_SEQ_DIM = static_cast(sdpa_info._k_seq_dim); - const uint32_t V_SEQ_DIM = static_cast(sdpa_info._v_seq_dim); - constexpr uint32_t MASK_KV_SEQ_DIM = 3; - - size_t next_available_mask_buffer_idx = 0; // Track next available pre-allocated mask buffer for cache misses - - // Helper lambda: Process a single tile - auto process_tile = [&](auto& request, - auto& model, - const ov::SoPtr& k_source, - const ov::SoPtr& v_source, - int64_t kv_offset, - int64_t mask_offset, - int64_t tile_length, - bool async = false) { - // Get tile input buffers - auto k_tile_buffer = request->get_tensor(model->inputs()[tile_in.k]); - auto v_tile_buffer = request->get_tensor(model->inputs()[tile_in.v]); - auto mask_tile_buffer = request->get_tensor(model->inputs()[tile_in.mask]); - - // Extract K tile - if (hfa_can_reuse_tensor_zero_copy(k_source, k_tile_buffer, K_SEQ_DIM, kv_offset, tile_length)) { - request->set_tensor(model->inputs()[tile_in.k], k_source); - } else if (hfa_desc._can_use_tensor_view) { - request->set_tensor(model->inputs()[tile_in.k], - ov::npuw::util::view(k_source, K_SEQ_DIM, kv_offset, tile_length)); - } else { - hfa_extract_and_copy_tile(k_source, k_tile_buffer, K_SEQ_DIM, kv_offset, tile_length, "K"); - } - - // Extract V tile - if (hfa_can_reuse_tensor_zero_copy(v_source, v_tile_buffer, V_SEQ_DIM, kv_offset, tile_length)) { - request->set_tensor(model->inputs()[tile_in.v], v_source); - } else if (hfa_desc._can_use_tensor_view) { - request->set_tensor(model->inputs()[tile_in.v], - ov::npuw::util::view(v_source, V_SEQ_DIM, kv_offset, tile_length)); - - } else { - hfa_extract_and_copy_tile(v_source, v_tile_buffer, V_SEQ_DIM, kv_offset, tile_length, "V"); - } - // Extract mask tile with caching (if enabled) to avoid redundant extraction - if (attention_mask_tensor) { - // Check if zero-copy is possible (rare case where full mask matches tile) - if (hfa_can_reuse_tensor_zero_copy(attention_mask_tensor, - mask_tile_buffer, - MASK_KV_SEQ_DIM, - mask_offset, - tile_length)) { - request->set_tensor(model->inputs()[tile_in.mask], attention_mask_tensor); - } else if (m_hfa_runtime_ctx.has_value()) { - // Cache is enabled - try to find cached tile - auto cached_tile = - m_hfa_runtime_ctx->find_cached_mask_tile(attention_mask_tensor, mask_offset, tile_length); - if (cached_tile) { - // Cache hit - reuse previously extracted tile - request->set_tensor(model->inputs()[tile_in.mask], cached_tile); - LOG_DEBUG("HFA: Cache hit for mask tile [offset=" << mask_offset << ", length=" << tile_length - << "]"); - } else { - // Cache miss - extract and cache this tile - LOG_DEBUG("HFA: Cache miss for mask tile [offset=" << mask_offset << ", length=" << tile_length - << "], extracting..."); - - // Use pre-allocated mask buffer for this tile - ov::SoPtr cached_mask_tile = - m_hfa_runtime_ctx->get_mask_tile_buffer(next_available_mask_buffer_idx); - - // Extract mask data into the pre-allocated buffer - hfa_extract_and_copy_tile(attention_mask_tensor, - cached_mask_tile, - MASK_KV_SEQ_DIM, - mask_offset, - tile_length, - "Mask"); - - // Cache the extracted tile for future reuse - m_hfa_runtime_ctx->cache_mask_tile(attention_mask_tensor, - mask_offset, - tile_length, - cached_mask_tile); - - // Use the cached tensor for this inference - request->set_tensor(model->inputs()[tile_in.mask], cached_mask_tile); - - // Move to next pre-allocated buffer for next cache miss - next_available_mask_buffer_idx++; - } - } else { - // Cache is disabled - extract mask tile on-the-fly directly into tile buffer - LOG_DEBUG("HFA: Extracting mask tile on-the-fly [offset=" << mask_offset << ", length=" << tile_length - << "] (cache disabled)"); - hfa_extract_and_copy_tile(attention_mask_tensor, - mask_tile_buffer, - MASK_KV_SEQ_DIM, - mask_offset, - tile_length, - "Mask"); - } - } - - // Execute tile (async mode pre-initializes next state buffer in parallel if optimizations enabled) - if (async) { - request->start_async(); - if (m_hfa_runtime_ctx && m_hfa_runtime_ctx->has_state_buffers()) { - m_hfa_runtime_ctx->prepare_next_state_buffers(); - } - request->wait(); - } else { - request->infer(); - } - }; - - int64_t mask_tile_offset = 0; - int64_t kv_tile_offset = 0; - - // Process regular tiles (all but the last one) - // Each regular tile processes past KV cache and outputs intermediate states (acc, max, d) - for (int64_t tile_idx = 0; tile_idx < num_tiles - 1; ++tile_idx) { - process_tile(regular_tile_request, - hfa_desc._compiled_tile_model, - past_key_tensor, - past_value_tensor, - kv_tile_offset, - mask_tile_offset, - tile_size); - - kv_tile_offset += tile_size; - mask_tile_offset += tile_size; - } - - // Process final tile separately - // Final tile processes present KV tokens and produces final attention output - if (num_tiles > 0) { - const size_t present_seq_length = present_key_tensor->get_shape()[K_SEQ_DIM]; - const int64_t final_tile_length = static_cast(present_seq_length); - - // Verify that final tile can process entire present KV in one inference - NPUW_ASSERT(final_tile_length == tile_size && - "Final tile must process entire present KV sequence in a single inference. " - "This is guaranteed during compilation (tile_size = query_size = present_seq_length)."); - - // Calculate mask offset for final tile (points to tail of mask corresponding to present tokens) - const int64_t mask_total_length = attention_mask_tensor->get_shape()[MASK_KV_SEQ_DIM]; - const int64_t final_mask_offset = mask_total_length - final_tile_length; - - process_tile(final_tile_request, - hfa_desc._compiled_final_tile_model, - present_key_tensor, - present_value_tensor, - 0, - final_mask_offset, - final_tile_length, - true); // async: pre-init next state buffer - } - - // Switch to other buffer for next inference (only if optimizations enabled) - if (m_hfa_runtime_ctx && m_hfa_runtime_ctx->has_state_buffers()) { - m_hfa_runtime_ctx->switch_buffers(); - } -} - void ov::npuw::JustInferRequest::unsafe_infer_spatial(std::size_t real_idx, std::size_t) { auto& comp_model_desc = m_npuw_model->m_compiled_submodels[real_idx]; NPUW_ASSERT(comp_model_desc.spatial.has_value()); @@ -1754,21 +1028,25 @@ void ov::npuw::JustInferRequest::unsafe_infer_spatial(std::size_t real_idx, std: } } -void ov::npuw::JustInferRequest::unsafe_infer(std::size_t real_idx, std::size_t idx) { +void ov::npuw::JustInferRequest::legacy_infer(std::size_t real_idx, std::size_t idx) { auto& comp_model_desc = m_npuw_model->m_compiled_submodels[real_idx]; auto& r = m_subrequests[real_idx]; if (comp_model_desc.spatial) { unsafe_infer_spatial(real_idx, idx); - } else if (comp_model_desc.host_flash_attention) { - run_hfa_tiled_inference(real_idx, idx); - } else if (comp_model_desc.moe_experts.has_value()) { - // Use MoEExecutor - m_moe_executor->run(real_idx, idx); } else { r->infer(); // Run normally } } +void ov::npuw::JustInferRequest::unsafe_infer(std::size_t real_idx, std::size_t idx) { + if (auto* behavior = get_subgraph_behavior(idx)) { + auto ctx = make_behavior_context(real_idx, idx); + behavior->run(ctx); + return; + } + legacy_infer(real_idx, idx); +} + void ov::npuw::JustInferRequest::unsafe_run_this_prep_next(std::size_t idx, bool& next_prepared) { auto& comp_model_desc = m_npuw_model->m_compiled_submodels[idx]; auto real_idx = comp_model_desc.replaced_by.value_or(idx); diff --git a/src/plugins/intel_npu/src/plugin/npuw/just_sync_infer_request.hpp b/src/plugins/intel_npu/src/plugin/npuw/just_sync_infer_request.hpp index c1916e0911e1..125ed36f9090 100644 --- a/src/plugins/intel_npu/src/plugin/npuw/just_sync_infer_request.hpp +++ b/src/plugins/intel_npu/src/plugin/npuw/just_sync_infer_request.hpp @@ -10,6 +10,7 @@ #include #include +#include "attn/attn_subgraph.hpp" #include "base_sync_infer_request.hpp" #include "host_flash_attention.hpp" #include "moe/moe_executor.hpp" @@ -19,6 +20,7 @@ #include "openvino/runtime/make_tensor.hpp" #include "openvino/runtime/tensor.hpp" #include "spatial.hpp" +#include "v1/subgraph_pipeline.hpp" namespace ov { namespace npuw { @@ -82,6 +84,16 @@ class JustInferRequest final : public IBaseInferRequest, public ISubrequestAcces bool unpack_required(size_t idx, size_t cidx) override; bool needs_copy_closure(size_t idx, size_t cidx) override; std::string subgraph_device(size_t idx) override; + void set_active_subrequest(size_t idx, ov::SoPtr request); + ov::SoPtr get_pipeline_subrequest(size_t idx) const; + void set_pipeline_subrequest(size_t idx, ov::SoPtr request); + bool is_subrequest_pipelined(size_t idx) const; + std::size_t history_size() const; + bool subgraph_needs_copy(std::size_t idx) const; + bool attention_no_copy() const; + const ov::SoPtr& compiled_submodel(size_t idx) const; + const ov::npuw::v1::subgraphs::CompiledPipeline& subgraph_pipeline(size_t idx) const; + std::size_t subgraph_param_base(size_t idx) const; protected: //////////////////////////////////// @@ -109,39 +121,28 @@ class JustInferRequest final : public IBaseInferRequest, public ISubrequestAcces void bind_global_parameters(std::size_t idx); void bind_global_results(std::size_t idx); using IBaseInferRequest::bind_global_results; + bool bind_behavior_input(std::size_t idx, + std::size_t real_idx, + std::size_t input_idx, + const ov::SoPtr& tensor, + RqPtr request) override; void function_prologue(std::size_t idx); - void function_prologue_attn(std::size_t real_idx, std::size_t idx); - void function_prologue_pyramid_attn(std::size_t real_idx, std::size_t idx); void unsafe_during(std::size_t real_idx, std::size_t idx, const std::function& f); void unsafe_infer(std::size_t real_idx, std::size_t idx); void unsafe_infer_spatial(std::size_t real_idx, std::size_t idx); void unsafe_run_this_prep_next(std::size_t idx, bool& next_prepared_p); - void run_hfa_tiled_inference(std::size_t real_idx, std::size_t idx); - - // HFA helper functions - static void hfa_extract_and_copy_tile(const ov::SoPtr& source_tensor, - const ov::SoPtr& dest_tensor, - uint32_t sequence_dim, - int64_t sequence_offset, - int64_t sequence_length, - const std::string& tensor_name); - - static bool hfa_can_reuse_tensor_zero_copy(const ov::SoPtr& source_tensor, - const ov::SoPtr& dest_tensor, - uint32_t sequence_dim, - int64_t sequence_offset, - int64_t tile_length); + void legacy_infer(std::size_t real_idx, std::size_t idx); + ov::npuw::v1::subgraphs::InferContext make_behavior_context(std::size_t real_idx, std::size_t idx); + const ov::npuw::v1::subgraphs::RuntimeBehaviorSpec* get_runtime_behavior_spec(std::size_t idx) const; + ov::npuw::v1::subgraphs::ISubgraphBehavior* get_subgraph_behavior(std::size_t idx) const; + bool behavior_handles_function_prologue(std::size_t idx) const; +protected: void connect_subrequests(); - - // Helper function to setup pyramid attention infer requests - void setup_pyramid_infer_requests(std::size_t real_idx, bool is_piped); - - // Helper function to setup host flash attention tile infer requests - void setup_hfa_infer_requests(std::size_t real_idx, bool is_piped, bool enable_hfa_optimizations = true); + void initialize_subgraph_behaviors(); // Helper function to initialize/reinitialize MoE executor void initialize_moe_executor(); @@ -170,22 +171,11 @@ class JustInferRequest final : public IBaseInferRequest, public ISubrequestAcces // Cached check if we do FOLDing and need to update closures in the repeating blocks bool m_closure_update_required = false; - // Cached attention mask for SDPA operations to avoid recomputing - ov::SoPtr m_cached_attention_mask; - - // HFA runtime context (holds cached masks, pre-allocated buffers, and state buffers) - std::optional m_hfa_runtime_ctx; - // MoE executor (encapsulates MoE inference logic and profiling) std::unique_ptr m_moe_executor; - // Anchor tensors that keep pyramid shared buffers alive. - // The pyramid infer requests share memory via raw pointers wrapped in ov::Tensor. - // Without these anchors the original tensor SoPtr can drop to zero when - // bind_pyramid_attention_inputs calls set_tensor on m_subrequests for the last chunk, - // freeing the buffer while pyramid requests still hold raw pointers into it. - // Each entry is {real_idx, tensor} so recreate can selectively remove by submodel. - std::vector>> m_pyramid_anchor_tensors; + std::vector m_subgraph_behaviors; + std::vector m_subgraph_runtime_states; }; } // namespace npuw diff --git a/src/plugins/intel_npu/src/plugin/npuw/kv_cache_block_manager.cpp b/src/plugins/intel_npu/src/plugin/npuw/kv_cache_block_manager.cpp new file mode 100644 index 000000000000..b55a3a55c819 --- /dev/null +++ b/src/plugins/intel_npu/src/plugin/npuw/kv_cache_block_manager.cpp @@ -0,0 +1,168 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "kv_cache_block_manager.hpp" + +#include "logging.hpp" +#include "util.hpp" + +namespace ov { +namespace npuw { + +KVCacheBlockManager::KVCacheBlockManager(uint32_t block_size, + uint32_t max_blocks, + const ov::Shape& base_shape, + ov::element::Type elem_type, + const std::string& device, + const std::shared_ptr& plugin) + : block_size_(block_size), + max_blocks_(max_blocks), + element_type_(elem_type), + block_shape_(base_shape), + device_(device), + plugin_(plugin) { + OPENVINO_ASSERT(block_size_ > 0 && max_blocks_ > 0, + "KVCacheBlockManager: block_size and max_blocks must be > 0, got block_size=", + block_size_, + " max_blocks=", + max_blocks_); + OPENVINO_ASSERT(device_ == "CPU" || plugin_ != nullptr, + "KVCacheBlockManager: plugin must be non-null for non-CPU device '", + device_, + "'"); + // Check that the sequence dimension (dim 2 for K/non-transposed-V, dim 3 for transposed V) + // equals block_size. + OPENVINO_ASSERT(base_shape.size() == 4 && (base_shape[2] == block_size || base_shape[3] == block_size), + "KVCacheBlockManager: base_shape ", + base_shape, + " does not have block_size=", + block_size, + " in sequence dimension (expected at dim 2 or 3)"); + + // Initialize block pool (tensors allocated on-demand, not here) + blocks_.reserve(max_blocks); + for (uint32_t i = 0; i < max_blocks; ++i) { + blocks_.push_back({}); + } + + // Push block IDs to stack in reverse order (so block 0 is on top). + for (uint32_t i = max_blocks; i > 0; --i) { + free_block_ids_.push(i - 1); + } + + LOG_INFO("KVCacheBlockManager initialized: " << "block_size=" << block_size << ", max_blocks=" << max_blocks + << ", total_capacity=" << (block_size * max_blocks) << " tokens" + << ", device=" << device); +} + +std::optional KVCacheBlockManager::allocate_block() { + if (free_block_ids_.empty()) { + LOG_WARN("KVCacheBlockManager: No free blocks available! " << "All " << max_blocks_ << " blocks are in use."); + return std::nullopt; + } + + uint32_t block_id = free_block_ids_.top(); + free_block_ids_.pop(); + + auto& block = blocks_[block_id]; + + // Allocate actual memory on-demand + if (!block.tensor) { + block.tensor = ov::npuw::util::allocMem(element_type_, block_shape_, device_, plugin_); + LOG_DEBUG("KVCacheBlockManager: Allocated memory for block " << block_id << " (shape=" << block_shape_ + << ", device=" << device_ << ")"); + } + + // Reset block state + block.num_tokens = 0; + block.is_allocated = true; + + LOG_VERB("KVCacheBlockManager: Allocated block " << block_id + << " (free blocks remaining: " << free_block_ids_.size() << ")"); + + return block_id; +} + +ov::SoPtr KVCacheBlockManager::get_block_tensor(uint32_t block_id) const { + validate_block_id(block_id); + + auto& block = blocks_[block_id]; + + if (!block.tensor) { + OPENVINO_THROW("KVCacheBlockManager: Block ", + block_id, + " has no allocated tensor. " + "Call allocate_block() first."); + } + + return block.tensor; +} + +void KVCacheBlockManager::update_block_tokens(uint32_t block_id, uint32_t num_tokens) { + validate_block_id(block_id); + + if (!blocks_[block_id].is_allocated) { + OPENVINO_THROW("KVCacheBlockManager: Cannot update tokens on free block ", + block_id, + ". Call allocate_block() first."); + } + + if (num_tokens > block_size_) { + OPENVINO_THROW("KVCacheBlockManager: Cannot set ", + num_tokens, + " tokens in block ", + block_id, + " (capacity: ", + block_size_, + ")"); + } + + auto& block = blocks_[block_id]; + block.num_tokens = num_tokens; + + LOG_VERB("KVCacheBlockManager: Updated block " << block_id << " tokens: " << num_tokens << "/" << block_size_); +} + +uint32_t KVCacheBlockManager::get_block_tokens(uint32_t block_id) const { + validate_block_id(block_id); + return blocks_[block_id].num_tokens; +} + +std::vector KVCacheBlockManager::get_allocated_blocks() const { + std::vector allocated; + allocated.reserve(max_blocks_ - free_block_ids_.size()); + + for (uint32_t i = 0; i < blocks_.size(); ++i) { + if (blocks_[i].is_allocated) { + allocated.push_back(i); + } + } + + return allocated; +} + +void KVCacheBlockManager::clear_all() { + LOG_DEBUG("KVCacheBlockManager: Clearing all blocks"); + + for (auto& block : blocks_) { + block.num_tokens = 0; + block.is_allocated = false; + } + + // Rebuild free stack (push in reverse order so block 0 is on top) + std::stack empty; + free_block_ids_.swap(empty); + for (uint32_t i = max_blocks_; i > 0; --i) { + free_block_ids_.push(i - 1); + } +} + +void KVCacheBlockManager::validate_block_id(uint32_t block_id) const { + if (block_id >= max_blocks_) { + OPENVINO_THROW("KVCacheBlockManager: Invalid block ID ", block_id, " (valid range: 0-", max_blocks_ - 1, ")"); + } +} + +} // namespace npuw +} // namespace ov diff --git a/src/plugins/intel_npu/src/plugin/npuw/kv_cache_block_manager.hpp b/src/plugins/intel_npu/src/plugin/npuw/kv_cache_block_manager.hpp new file mode 100644 index 000000000000..65b82d9f8203 --- /dev/null +++ b/src/plugins/intel_npu/src/plugin/npuw/kv_cache_block_manager.hpp @@ -0,0 +1,163 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "openvino/runtime/iplugin.hpp" +#include "openvino/runtime/itensor.hpp" +#include "openvino/runtime/so_ptr.hpp" + +namespace ov { +namespace npuw { + +/** + * @brief Block-based KV Cache Manager + * + * Manages KV cache memory using fixed-size blocks instead of a single continuous buffer. + * This approach significantly reduces memory waste when actual prompt length is much smaller + * than the configured max_prompt_size. + * + * Key benefits: + * - Memory efficiency: Only allocate blocks as needed + * - Flexible allocation: Adapt to variable-length prompts without pre-allocating max buffer + * + * Example usage: + * KVCacheBlockManager manager(512, 16, shape, type, "NPU", plugin); + * auto block_id = manager.allocate_block(); + * auto tensor = manager.get_block_tensor(block_id.value()); + * manager.update_block_tokens(block_id.value(), 256); + * manager.clear_all(); + */ +class KVCacheBlockManager { +public: + /** + * @brief Construct a new KV Cache Block Manager + * + * @param block_size Number of tokens per block + * @param max_blocks Maximum number of blocks in the pool + * @param base_shape Base shape for block tensors [batch, num_heads, seq_len, head_dim] + * @param elem_type Element type (e.g., fp16, fp32) + * @param device Target device for memory allocation ("NPU", "CPU") + * @param plugin Plugin instance for memory allocation + */ + KVCacheBlockManager(uint32_t block_size, + uint32_t max_blocks, + const ov::Shape& base_shape, + ov::element::Type elem_type, + const std::string& device, + const std::shared_ptr& plugin); + + ~KVCacheBlockManager() = default; + + // Disable copy + KVCacheBlockManager(const KVCacheBlockManager&) = delete; + KVCacheBlockManager& operator=(const KVCacheBlockManager&) = delete; + + // Allow move + KVCacheBlockManager(KVCacheBlockManager&&) = default; + KVCacheBlockManager& operator=(KVCacheBlockManager&&) = default; + + /** + * @brief Allocate a new block from the free pool + * + * @return Block ID if successful, std::nullopt if no free blocks available + */ + std::optional allocate_block(); + + /** + * @brief Get the tensor associated with a block + * + * @param block_id Block ID + * @return Tensor for the block + */ + ov::SoPtr get_block_tensor(uint32_t block_id) const; + + /** + * @brief Update the number of tokens stored in a block + * + * @param block_id Block ID + * @param num_tokens New token count (must be <= block_size) + */ + void update_block_tokens(uint32_t block_id, uint32_t num_tokens); + + /** + * @brief Get the number of tokens in a block + * + * @param block_id Block ID + * @return Number of tokens + */ + uint32_t get_block_tokens(uint32_t block_id) const; + + /** + * @brief Get list of all currently allocated block IDs + * + * @return Vector of block IDs + */ + std::vector get_allocated_blocks() const; + + /** + * @brief Reset all blocks to FREE state and clear token counts. + * + * Note: tensor memory is retained in the pool for reuse; no device + * deallocation occurs. Individual block release is not supported — + * use this method to reset the entire pool between requests. + */ + void clear_all(); + + /** + * @brief Get block size (tokens per block) + */ + uint32_t get_block_size() const { + return block_size_; + } + + /** + * @brief Get maximum number of blocks + */ + uint32_t get_max_blocks() const { + return max_blocks_; + } + + /** + * @brief Get number of currently free (unallocated) blocks + */ + uint32_t num_free_blocks() const { + return static_cast(free_block_ids_.size()); + } + +private: + /** + * @brief Represents a single block of KV cache memory + */ + struct Block { + ov::SoPtr tensor; ///< Block memory tensor (allocated on-demand) + uint32_t num_tokens = 0; ///< Number of tokens stored in this block + bool is_allocated = false; ///< True when block is in use; false = free + }; + + uint32_t block_size_; ///< Number of tokens per block + uint32_t max_blocks_; ///< Maximum blocks in pool + std::vector blocks_; ///< All blocks (free + allocated) + std::stack free_block_ids_; ///< Stack of free block IDs (LIFO for better reuse) + + ov::element::Type element_type_; ///< Element type for tensors + ov::Shape block_shape_; ///< Shape for block tensors + std::string device_; ///< Target device + std::shared_ptr plugin_; ///< Plugin for memory allocation + + /** + * @brief Validate block ID + */ + void validate_block_id(uint32_t block_id) const; +}; + +} // namespace npuw +} // namespace ov diff --git a/src/plugins/intel_npu/src/plugin/npuw/lazy_tensor.cpp b/src/plugins/intel_npu/src/plugin/npuw/lazy_tensor.cpp index cd59bfe04b72..3d3c137e327f 100644 --- a/src/plugins/intel_npu/src/plugin/npuw/lazy_tensor.cpp +++ b/src/plugins/intel_npu/src/plugin/npuw/lazy_tensor.cpp @@ -16,6 +16,7 @@ #include "openvino/runtime/shared_buffer.hpp" #include "openvino/util/file_util.hpp" #include "openvino/util/mmap_object.hpp" +#include "orc.hpp" #include "util.hpp" using ov::npuw::weights::LazyTensor; @@ -144,51 +145,6 @@ void Const::detach() { m_mmaped_weights.reset(); } -void Const::serialize(std::ostream& stream) const { - using namespace ov::npuw::s11n; - write(stream, m_cached_type.to_string()); - write(stream, m_cached_shape); - write(stream, m_offset); - write(stream, m_byte_size); - - // FIXME: handle a special case: - // 1) We added a Constant to the model before compilation (e.g. int RoPE patterns) - // 2) This Constant became a parameter during folding - // 3) Thus it became a LazyTensor, but there is no original data in weights file, - // so it will fail in read_weight() during weightless deserialization. - // In this case we need to include Constant's data into the blob. - if (m_copied_if_not_in_model) { - LOG_WARN("Some pattern introduced a new Constant node not present in the original weights file. This will " - "increase the blob size by " - << m_byte_size << " bytes."); - write(stream, true); - write(stream, m_copied_if_not_in_model); - // detach the tensor - m_copied_if_not_in_model = ov::Tensor(); - } else { - write(stream, false); - } -} - -Const Const::deserialize(std::istream& stream) { - using namespace ov::npuw::s11n; - Const c; - std::string type_str; - read(stream, type_str); - c.m_cached_type = ov::element::Type(type_str); - read(stream, c.m_cached_shape); - read(stream, c.m_offset); - read(stream, c.m_byte_size); - - bool contains_weight = false; - read(stream, contains_weight); - if (contains_weight) { - read(stream, c.m_read_from_bin); - } - - return c; -} - std::size_t Concat::hash() const { std::size_t seed = std::hash()(axis) + 0x9e3779b9; for (auto& lt : tensors) { @@ -230,20 +186,6 @@ void Concat::detach() { } } -void Concat::serialize(std::ostream& stream) const { - using namespace ov::npuw::s11n; - write(stream, axis); - write(stream, tensors); -} - -Concat Concat::deserialize(std::istream& stream) { - using namespace ov::npuw::s11n; - Concat c; - read(stream, c.axis); - read(stream, c.tensors); - return c; -} - std::size_t Unpack::hash() const { std::size_t seed = w.get_hash() + 0x9e3779b9; seed ^= z.get_hash() + 0x9e3779b9; @@ -294,28 +236,6 @@ void Unpack::detach() { s.detach(); } -void Unpack::serialize(std::ostream& stream) const { - using namespace ov::npuw::s11n; - write(stream, type.to_string()); - write(stream, shape); - write(stream, w); - write(stream, z); - write(stream, s); -} - -Unpack Unpack::deserialize(std::istream& stream) { - using namespace ov::npuw::s11n; - Unpack u; - std::string type_str; - read(stream, type_str); - u.type = ov::element::Type(type_str); - read(stream, u.shape); - read(stream, u.w); - read(stream, u.z); - read(stream, u.s); - return u; -} - std::size_t Permute::hash() const { std::size_t seed = tensor.get_hash() + 0x9e3779b9; for (const auto& axis : axes) { @@ -350,20 +270,6 @@ void Permute::detach() { tensor.detach(); } -void Permute::serialize(std::ostream& stream) const { - using namespace ov::npuw::s11n; - write(stream, axes); - write(stream, tensor); -} - -Permute Permute::deserialize(std::istream& stream) { - using namespace ov::npuw::s11n; - Permute p; - read(stream, p.axes); - read(stream, p.tensor); - return p; -} - std::size_t Convert::hash() const { std::size_t seed = type.hash() + 0x9e3779b9; seed ^= tensor.get_hash() + 0x9e3779b9; @@ -391,22 +297,6 @@ void Convert::detach() { tensor.detach(); } -void Convert::serialize(std::ostream& stream) const { - using namespace ov::npuw::s11n; - write(stream, type.to_string()); - write(stream, tensor); -} - -Convert Convert::deserialize(std::istream& stream) { - using namespace ov::npuw::s11n; - Convert c; - std::string type_str; - read(stream, type_str); - c.type = ov::element::Type(type_str); - read(stream, c.tensor); - return c; -} - std::size_t Gather::hash() const { std::size_t seed = w.get_hash() + 0x9e3779b9; seed ^= t.get_element_type().hash() + 0x9e3779b9; @@ -466,28 +356,19 @@ void Gather::detach() { w.detach(); } -void Gather::serialize(std::ostream& stream) const { - using namespace ov::npuw::s11n; - write(stream, dst_type.to_string()); - write(stream, dst_shape); - write(stream, w); - write(stream, t); -} - -Gather Gather::deserialize(std::istream& stream) { - using namespace ov::npuw::s11n; - Gather g; - std::string type_str; - read(stream, type_str); - g.dst_type = ov::element::Type(type_str); - read(stream, g.dst_shape); - read(stream, g.w); - read(stream, g.t); - return g; -} } // namespace op -enum class TransformType : int { CONST = 0, CONCAT, UNPACK, PERMUTE, CONVERT, GATHER }; +// Stable, permanently assigned op-type IDs. +// Once assigned, IDs are NEVER changed and NEVER reused, even if an op is retired. +// Retired IDs must be kept as comments to prevent accidental recycling. +enum class TransformType : std::uint16_t { + CONST = 1, + CONCAT = 2, + UNPACK = 3, + PERMUTE = 4, + CONVERT = 5, + GATHER = 6, +}; struct LazyTensorImpl { LazyTensorImpl() = default; @@ -501,9 +382,8 @@ struct LazyTensorImpl { void detach(); - void serialize(std::ostream& stream) const; - static std::shared_ptr deserialize(std::istream& stream); void read_weight(const ov::npuw::s11n::WeightsContext& ctx); + void serialize(ov::npuw::orc::Stream& stream); LazyTensor::Transform m_transform; std::size_t m_hash = 0; @@ -524,6 +404,174 @@ struct overloaded : Ts... { template overloaded(Ts...) -> overloaded; +namespace { + +ov::npuw::weights::TransformType get_transform_type(const ov::npuw::weights::op::Const&) { + return ov::npuw::weights::TransformType::CONST; +} + +ov::npuw::weights::TransformType get_transform_type(const ov::npuw::weights::op::Concat&) { + return ov::npuw::weights::TransformType::CONCAT; +} + +ov::npuw::weights::TransformType get_transform_type(const ov::npuw::weights::op::Unpack&) { + return ov::npuw::weights::TransformType::UNPACK; +} + +ov::npuw::weights::TransformType get_transform_type(const ov::npuw::weights::op::Permute&) { + return ov::npuw::weights::TransformType::PERMUTE; +} + +ov::npuw::weights::TransformType get_transform_type(const ov::npuw::weights::op::Convert&) { + return ov::npuw::weights::TransformType::CONVERT; +} + +ov::npuw::weights::TransformType get_transform_type(const ov::npuw::weights::op::Gather&) { + return ov::npuw::weights::TransformType::GATHER; +} + +} // namespace + +namespace ov { +namespace npuw { +namespace weights { +namespace op { + +void Const::serialize(ov::npuw::orc::Stream& stream) { + std::string type_str; + if (stream.output()) { + type_str = m_cached_type.to_string(); + } + stream & type_str & m_cached_shape & m_offset & m_byte_size; + if (stream.input()) { + m_cached_type = ov::element::Type(type_str); + } + + bool contains_weight = static_cast(m_copied_if_not_in_model); + stream & contains_weight; + if (contains_weight) { + if (stream.output()) { + stream & m_copied_if_not_in_model; + m_copied_if_not_in_model = ov::Tensor(); + } else { + stream & m_read_from_bin; + } + } +} + +void Concat::serialize(ov::npuw::orc::Stream& stream) { + stream & axis & tensors; +} + +void Unpack::serialize(ov::npuw::orc::Stream& stream) { + std::string type_str; + if (stream.output()) { + type_str = type.to_string(); + } + stream & type_str & shape & w & z & s; + if (stream.input()) { + type = ov::element::Type(type_str); + } +} + +void Permute::serialize(ov::npuw::orc::Stream& stream) { + stream & axes & tensor; +} + +void Convert::serialize(ov::npuw::orc::Stream& stream) { + std::string type_str; + if (stream.output()) { + type_str = type.to_string(); + } + stream & type_str & tensor; + if (stream.input()) { + type = ov::element::Type(type_str); + } +} + +void Gather::serialize(ov::npuw::orc::Stream& stream) { + std::string type_str; + if (stream.output()) { + type_str = dst_type.to_string(); + } + stream & type_str & dst_shape & w & t; + if (stream.input()) { + dst_type = ov::element::Type(type_str); + } +} + +} // namespace op + +void LazyTensorImpl::serialize(ov::npuw::orc::Stream& stream) { + stream & m_hash; + + if (stream.output()) { + std::visit( + [&](auto& op) { + const auto type_id = static_cast(get_transform_type(op)); + auto section = ov::npuw::orc::make_payload_section(type_id, op.kVersion, op); + ov::npuw::orc::serialize(stream, section); + }, + m_transform); + return; + } + + ov::npuw::orc::Section section; + ov::npuw::orc::serialize(stream, section); + switch (static_cast(section.type)) { + case TransformType::CONCAT: + m_transform.emplace(ov::npuw::orc::load_versioned_payload(section)); + break; + case TransformType::CONST: + m_transform.emplace(ov::npuw::orc::load_versioned_payload(section)); + break; + case TransformType::CONVERT: + m_transform.emplace(ov::npuw::orc::load_versioned_payload(section)); + break; + case TransformType::PERMUTE: + m_transform.emplace(ov::npuw::orc::load_versioned_payload(section)); + break; + case TransformType::UNPACK: + m_transform.emplace(ov::npuw::orc::load_versioned_payload(section)); + break; + case TransformType::GATHER: + m_transform.emplace(ov::npuw::orc::load_versioned_payload(section)); + break; + default: + OPENVINO_THROW("ORC LazyTensor: unknown op_type ", section.type, " — please upgrade NPUW"); + break; + } +} + +void LazyTensor::serialize(ov::npuw::orc::Stream& stream) { + bool is_initialized = static_cast(m_impl); + stream & is_initialized; + if (!is_initialized) { + if (stream.input()) { + m_impl.reset(); + } + return; + } + + if (stream.output()) { + m_impl->serialize(stream); + } else { + m_impl = std::make_shared(); + m_impl->serialize(stream); + } +} + +} // namespace weights + +namespace orc { +void serialize(ov::npuw::orc::Stream& stream, ov::npuw::weights::LazyTensor& var) { + var.serialize(stream); +} +} // namespace orc + +} // namespace npuw +} // namespace ov + LazyTensorImpl::LazyTensorImpl(LazyTensor::Transform&& t) : m_transform(std::move(t)), m_hash(std::visit(overloaded{[](const auto& op) { @@ -612,71 +660,6 @@ void LazyTensorImpl::detach() { m_transform); } -void LazyTensorImpl::serialize(std::ostream& stream) const { - using namespace ov::npuw::s11n; - write(stream, m_hash); - // FIXME: create proper op identificators instead of int - std::visit(overloaded{ - [&stream](const op::Concat& op) { - write(stream, static_cast(TransformType::CONCAT)); - op.serialize(stream); - }, - [&stream](const op::Const& op) { - write(stream, static_cast(TransformType::CONST)); - op.serialize(stream); - }, - [&stream](const op::Convert& op) { - write(stream, static_cast(TransformType::CONVERT)); - op.serialize(stream); - }, - [&stream](const op::Permute& op) { - write(stream, static_cast(TransformType::PERMUTE)); - op.serialize(stream); - }, - [&stream](const op::Unpack& op) { - write(stream, static_cast(TransformType::UNPACK)); - op.serialize(stream); - }, - [&stream](const op::Gather& op) { - write(stream, static_cast(TransformType::GATHER)); - op.serialize(stream); - }, - }, - m_transform); -} - -std::shared_ptr LazyTensorImpl::deserialize(std::istream& stream) { - using namespace ov::npuw::s11n; - auto lt_impl = std::make_shared(); - read(stream, lt_impl->m_hash); - int op_type; - read(stream, op_type); - switch (TransformType(op_type)) { - case TransformType::CONCAT: - lt_impl->m_transform = op::Concat::deserialize(stream); - break; - case TransformType::CONST: - lt_impl->m_transform = op::Const::deserialize(stream); - break; - case TransformType::CONVERT: - lt_impl->m_transform = op::Convert::deserialize(stream); - break; - case TransformType::PERMUTE: - lt_impl->m_transform = op::Permute::deserialize(stream); - break; - case TransformType::UNPACK: - lt_impl->m_transform = op::Unpack::deserialize(stream); - break; - case TransformType::GATHER: - lt_impl->m_transform = op::Gather::deserialize(stream); - break; - default: - NPUW_ASSERT(false && "Unsupported type"); - break; - } - return lt_impl; -} - LazyTensor::LazyTensor(const std::shared_ptr& const_ptr) : m_impl(std::make_shared(op::Const(const_ptr))) {} LazyTensor::LazyTensor(const std::vector& to_concat, const std::size_t axis) @@ -765,28 +748,6 @@ void LazyTensor::detach() { } } -void LazyTensor::serialize(std::ostream& stream) const { - using namespace ov::npuw::s11n; - if (!m_impl) { - write(stream, false); - return; - } - write(stream, true); - m_impl->serialize(stream); -} - -LazyTensor LazyTensor::deserialize(std::istream& stream) { - using namespace ov::npuw::s11n; - bool is_initialized; - read(stream, is_initialized); - LazyTensor lt; - if (!is_initialized) { - return lt; - } - lt.m_impl = LazyTensorImpl::deserialize(stream); - return lt; -} - std::size_t LazyTensor::Hash::operator()(const LazyTensor& lt) const { return lt.get_hash(); } diff --git a/src/plugins/intel_npu/src/plugin/npuw/lazy_tensor.hpp b/src/plugins/intel_npu/src/plugin/npuw/lazy_tensor.hpp index 66ab1bef45bf..42c5aec72518 100644 --- a/src/plugins/intel_npu/src/plugin/npuw/lazy_tensor.hpp +++ b/src/plugins/intel_npu/src/plugin/npuw/lazy_tensor.hpp @@ -73,22 +73,26 @@ class LazyTensor { }; Meta eval_meta() const; - void serialize(std::ostream& stream) const; - static LazyTensor deserialize(std::istream& stream); void read_weight(const ov::npuw::s11n::WeightsContext& ctx); operator bool() const; private: + friend void ov::npuw::orc::serialize(ov::npuw::orc::Stream& stream, LazyTensor& var); + void serialize(ov::npuw::orc::Stream& stream); std::shared_ptr m_impl = nullptr; }; namespace op { +// These v0 payloads are now part of the frozen on-wire baseline. Any new field +// or semantic change must be introduced through a versioned successor payload +// instead of changing the existing v0 layout. class Const { friend struct ov::npuw::weights::LazyTensorImpl; public: - Const() = default; + static constexpr std::uint16_t kVersion = 0u; + Const() = default; explicit Const(const std::shared_ptr& n); std::size_t hash() const; bool operator==(const Const& other) const; @@ -96,8 +100,7 @@ class Const { LazyTensor::Meta eval_meta() const; void read_weight(const ov::npuw::s11n::WeightsContext& ctx); void detach(); - void serialize(std::ostream& stream) const; - static Const deserialize(std::istream& stream); + void serialize(ov::npuw::orc::Stream& stream); private: std::shared_ptr m_node = nullptr; @@ -120,6 +123,8 @@ class Concat { friend struct ov::npuw::weights::LazyTensorImpl; public: + static constexpr std::uint16_t kVersion = 0u; + Concat() = default; Concat(const std::vector& _tensors, std::size_t _axis) : tensors(_tensors), axis(_axis) {} @@ -129,8 +134,7 @@ class Concat { LazyTensor::Meta eval_meta() const; void read_weight(const ov::npuw::s11n::WeightsContext& ctx); void detach(); - void serialize(std::ostream& stream) const; - static Concat deserialize(std::istream& stream); + void serialize(ov::npuw::orc::Stream& stream); private: std::vector tensors; @@ -141,6 +145,8 @@ class Unpack { friend struct ov::npuw::weights::LazyTensorImpl; public: + static constexpr std::uint16_t kVersion = 0u; + Unpack() = default; Unpack(const LazyTensor& _w, const LazyTensor& _z, const LazyTensor& _s, ov::element::Type _type, ov::Shape _shape) : w(_w), @@ -155,8 +161,7 @@ class Unpack { LazyTensor::Meta eval_meta() const; void read_weight(const ov::npuw::s11n::WeightsContext& ctx); void detach(); - void serialize(std::ostream& stream) const; - static Unpack deserialize(std::istream& stream); + void serialize(ov::npuw::orc::Stream& stream); private: LazyTensor w, z, s; @@ -168,6 +173,8 @@ class Permute { friend struct ov::npuw::weights::LazyTensorImpl; public: + static constexpr std::uint16_t kVersion = 0u; + Permute() = default; Permute(const LazyTensor& _tensor, const std::vector& _axes) : tensor(_tensor), axes(_axes) {} @@ -177,8 +184,7 @@ class Permute { LazyTensor::Meta eval_meta() const; void read_weight(const ov::npuw::s11n::WeightsContext& ctx); void detach(); - void serialize(std::ostream& stream) const; - static Permute deserialize(std::istream& stream); + void serialize(ov::npuw::orc::Stream& stream); private: LazyTensor tensor; @@ -189,6 +195,8 @@ class Convert { friend struct ov::npuw::weights::LazyTensorImpl; public: + static constexpr std::uint16_t kVersion = 0u; + Convert() = default; Convert(const LazyTensor& _tensor, ov::element::Type _type) : tensor(_tensor), type(_type) {} @@ -198,8 +206,7 @@ class Convert { LazyTensor::Meta eval_meta() const; void read_weight(const ov::npuw::s11n::WeightsContext& ctx); void detach(); - void serialize(std::ostream& stream) const; - static Convert deserialize(std::istream& stream); + void serialize(ov::npuw::orc::Stream& stream); private: LazyTensor tensor; @@ -210,6 +217,8 @@ class Gather { friend struct ov::npuw::weights::LazyTensorImpl; public: + static constexpr std::uint16_t kVersion = 0u; + Gather() = default; Gather(const LazyTensor& _w, const ov::Tensor& _t, const ov::element::Type& _dst_type, const ov::Shape& _dst_shape) : w(_w), @@ -223,8 +232,7 @@ class Gather { LazyTensor::Meta eval_meta() const; void read_weight(const ov::npuw::s11n::WeightsContext& ctx); void detach(); - void serialize(std::ostream& stream) const; - static Gather deserialize(std::istream& stream); + void serialize(ov::npuw::orc::Stream& stream); private: LazyTensor w; diff --git a/src/plugins/intel_npu/src/plugin/npuw/llm_compiled_model.cpp b/src/plugins/intel_npu/src/plugin/npuw/llm_compiled_model.cpp index 92ef105aea2f..0a5d08013336 100644 --- a/src/plugins/intel_npu/src/plugin/npuw/llm_compiled_model.cpp +++ b/src/plugins/intel_npu/src/plugin/npuw/llm_compiled_model.cpp @@ -11,14 +11,16 @@ #include "llm_infer_request.hpp" #include "logging.hpp" #include "moe_transformations/apply_moe_device_routed_transforms.hpp" +#include "npuw_transformations/add_position_ids_param.hpp" #include "npuw_transformations/convert_kvcache_to_precision.hpp" #include "npuw_transformations/decompose_gqa.hpp" #include "npuw_transformations/lora_stateful_to_stateless.hpp" #include "npuw_transformations/optimize_value_tensors.hpp" -#include "npuw_transformations/patch_phi3_sliding_mask.hpp" +#include "npuw_transformations/patch_sliding_window_mask.hpp" #include "npuw_transformations/reshape_sliced_head_to_static.hpp" #include "npuw_transformations/reshape_to_static.hpp" #include "npuw_transformations/slice_out_embeds.hpp" +#include "npuw_transformations/split_kvcache_into_blocks.hpp" #include "openvino/op/convert.hpp" #include "openvino/op/greater.hpp" #include "openvino/op/ops.hpp" @@ -35,6 +37,7 @@ #include "openvino/pass/validate.hpp" #include "openvino/runtime/iasync_infer_request.hpp" #include "openvino/runtime/properties.hpp" +#include "partitioning/patterns/fold_const.hpp" #include "partitioning/patterns/moe.hpp" #include "partitioning/patterns/pre_compute.hpp" #include "partitioning/patterns/sdpa.hpp" @@ -207,7 +210,6 @@ std::optional extract_npu_descriptor(const std::shared_ptrget_property(ov::device::architecture.name(), ov::AnyMap{}).as(); desc.max_tiles = plugin->get_property(ov::intel_npu::max_tiles.name(), ov::AnyMap{}).as(); @@ -248,8 +250,8 @@ std::optional extract_npu_descriptor(const std::shared_ptr= ONEAPI_MAKE_VERSION(7, 29)) { - // Flash attention tile is supported starting from compiler version 7.29 on NPU5010 + if (desc.arch == "5010" && desc.compiler_ver >= ONEAPI_MAKE_VERSION(8, 1)) { + // Flash attention tile with GQA is supported starting from compiler version 8.1 on NPU5010 desc.support_flash_attention_tile = true; } @@ -314,14 +316,52 @@ ov::AnyMap get_default_common_config(const std::optional& npudesc) { } else { config.emplace("NPUW_FUNCALL_FOR_ALL", "YES"); } + auto set_max_tiles_based_on_arch = [&config, &npudesc]() { + if (!npudesc.has_value()) { + return; + } + LOG_DEBUG("NPU architecture detected: " << npudesc->arch << ", max tiles: " << npudesc->max_tiles + << ", compiler DQ support: " << (npudesc->compiler_dq ? "YES" : "NO")); + + // Platform parameter has a higher priority than deviceID + std::string npu_platform; + if (npudesc->arch != ov::intel_npu::Platform::AUTO_DETECT) { + npu_platform = ov::intel_npu::Platform::standardize(npudesc->arch); + } else { + npu_platform = npudesc->arch; + } + + bool set_npu_tiles = false; + std::string arch_added_compilation_param; + if (npu_platform == ov::intel_npu::Platform::NPU3720) { + // Keep baseline settings. + } else if (npu_platform == ov::intel_npu::Platform::NPU4000) { + set_npu_tiles = true; + arch_added_compilation_param = "optimization-level=3"; + } else if (npu_platform == ov::intel_npu::Platform::NPU5010 || + npu_platform == ov::intel_npu::Platform::NPU5020) { + set_npu_tiles = true; + } else if (npu_platform == ov::intel_npu::Platform::AUTO_DETECT) { + arch_added_compilation_param = "performance-hint-override=latency"; + } else { + LOG_WARN("Unknown NPU platform: " << npu_platform << ". Default config will be used."); + } + + if (set_npu_tiles) { + config["NPU_TILES"] = npudesc->max_tiles; + } + + if (!arch_added_compilation_param.empty()) { + config["NPU_COMPILATION_MODE_PARAMS"] = + config["NPU_COMPILATION_MODE_PARAMS"].as() + " " + arch_added_compilation_param; + } + }; + set_max_tiles_based_on_arch(); return config; } ov::AnyMap get_default_prefill_config(const std::shared_ptr& model, const std::optional& npudesc) { auto config = get_default_common_config(npudesc); - if (npudesc.has_value() && npudesc->arch == "4000" && npudesc->max_tiles != -1) { - config.emplace("NPU_TILES", npudesc->max_tiles); - } // Specify NPUW DQ if Compiler DQ is not enabled if (!npudesc.has_value() || !npudesc->compiler_dq) { if (is_cw_compressed(model)) { @@ -373,7 +413,7 @@ void merge_config_with(ov::AnyMap& lhs, const ov::AnyMap& rhs) { void split_llm_properties(const ov::AnyMap& properties, ov::AnyMap& llm_properties, ov::AnyMap& other_properties) { for (auto it = properties.begin(); it != properties.end(); ++it) { - if (it->first.find("NPUW_LLM") != it->first.npos) { + if (it->first.find("NPUW_LLM") != it->first.npos || it->first.find("NPUW_WHISPER") != it->first.npos) { llm_properties.insert(*it); } else { other_properties.insert(*it); @@ -406,6 +446,7 @@ void update_config_for_whisper(ov::AnyMap& config) { void disable_ws_for_whisper(ov::AnyMap& config) { config.erase("NPUW_FUNCALL_FOR_ALL"); config.erase("NPUW_FOLD"); + config.erase("NPUW_FOLD_ONLY"); config.erase("NPUW_CWAI"); } @@ -517,6 +558,16 @@ std::shared_ptr check_and_cut_lm_head(const std::shared_ptr& model) { + auto long_rope = std::make_shared(); + bool matched = false; + long_rope->transform_cb = [&]() { + matched = true; + }; + long_rope->run_on_model(model); + return matched; +} + } // namespace // Apply DEVICE_ROUTED MoE transformations to models @@ -637,8 +688,6 @@ ov::npuw::LLMCompiledModel::LLMCompiledModel(const std::shared_ptr& m ov::AnyMap other_props; split_llm_properties(properties, npuw_llm_props, other_props); const auto npudesc = extract_npu_descriptor(plugin, other_props); - auto use_whisper_key = pop_option(other_props, std::string("NPUW_WHISPER")); - auto whisper_eos_token = pop_option(other_props, std::string("NPUW_WHISPER_EOS_TOKEN")); auto use_eagle_key = pop_option(other_props, std::string("NPUW_EAGLE")); // Remove map-valued section configs before m_cfg.update(any_copy(...)), since Config expects string options. @@ -672,14 +721,14 @@ ov::npuw::LLMCompiledModel::LLMCompiledModel(const std::shared_ptr& m LOG_INFO("Set NPUW_ATTN_HFA_FUSED to YES"); } - m_is_whisper = use_whisper_key.value_or(false).as() == true; + m_is_whisper = m_cfg.get<::intel_npu::NPUW_WHISPER>(); if (m_is_whisper) { m_cfg.update({{"NPUW_LLM_SHARED_HEAD", "NO"}}); m_cfg.update({{"NPUW_LLM_PREFILL_CHUNK_SIZE", "0"}}); m_cfg.update({{"NPUW_LLM_CACHE_ROPE", "NO"}}); m_cfg.update({{"NPUW_LLM_OPTIMIZE_V_TENSORS", "NO"}}); - m_eos_token_id = whisper_eos_token.value_or(50257).as(); + m_eos_token_id = m_cfg.get<::intel_npu::NPUW_WHISPER_EOS_TOKEN>(); } m_is_eagle = use_eagle_key.value_or(false).as() == true; @@ -773,6 +822,8 @@ ov::npuw::LLMCompiledModel::LLMCompiledModel(const std::shared_ptr& m LOG_DEBUG("Text-embedding model rebuild"); ov::npuw::util::PrepareTextEmbeddingModel(seq_len_dim).run_on_model(kvcache_model); } else { + LOG_DEBUG("Adding position_ids input in case it doesn't exist in model: LFM-2 case."); + ov::npuw::AddPositionIdsParam().run_on_model(kvcache_model); LOG_DEBUG("Transform kvcache model from stateful to stateless."); ov::pass::StatefulToStateless().run_on_model(kvcache_model); } @@ -788,8 +839,8 @@ ov::npuw::LLMCompiledModel::LLMCompiledModel(const std::shared_ptr& m auto lm_head_model = check_and_cut_lm_head(kvcache_model, m_cfg); if (!m_is_whisper) { - LOG_DEBUG("Try patch Phi-3 sliding window mask, if it exists."); - ov::npuw::PatchPhi3SlidingMask().run_on_model(kvcache_model); + LOG_DEBUG("Try patch sliding window attention mask (Phi-3, Gemma-2, Gemma-3, Gemma-4), if it exists."); + ov::npuw::PatchSlidingWindowMask().run_on_model(kvcache_model); } LOG_DEBUG("Creating prefill model as clone of transformed kvcache one."); @@ -805,11 +856,32 @@ ov::npuw::LLMCompiledModel::LLMCompiledModel(const std::shared_ptr& m m_kvcache_desc = KVCacheDesc{whisper_max_prompt_size, whisper_kvcache_size, 0u, whisper_seq_len_dim, 1u}; whisper_lhs_seq_size = static_cast(prefill_model->input("encoder_hidden_states").get_partial_shape()[1].get_length()); + auto whisper_decompose_sdpa = m_cfg.get<::intel_npu::NPUW_WHISPER_DECOMPOSE_SDPA>(); + if (whisper_decompose_sdpa) { + m_kvcache_desc.max_prompt_size = whisper_kvcache_size - 1; + } - ov::npuw::util::PrepareWhisperPrefillModel(m_kvcache_desc.max_prompt_size, - whisper_lhs_seq_size) - .run_on_model(prefill_model); // Whisper decoder model + auto prepare_prefill_model = ov::npuw::util::PrepareWhisperPrefillModel(m_kvcache_desc.max_prompt_size, + whisper_lhs_seq_size, + whisper_decompose_sdpa); + prepare_prefill_model.run_on_model(prefill_model); // Whisper decoder model ov::npuw::util::PrepareWhisperKVCacheModel().run_on_model(kvcache_model); // Whisper decoder_with_past model + + // FIXME: Whisper Decompose SDPA + // WA: to mock new "cross_attention_qk_scaled_scores" outputs in original model + if (whisper_decompose_sdpa) { + m_decomposed_sdpa_size = prepare_prefill_model.get_decomposed_sdpa_size(); + auto& mutable_outputs = const_cast>&>(this->outputs()); + for (size_t idx = 0; idx < m_decomposed_sdpa_size; idx++) { + auto fake_param = std::make_shared(ov::element::f32, ov::PartialShape{}); + auto fake_result = std::make_shared(fake_param); + fake_result->output(0).get_tensor().add_names( + {WhisperInferRequest::whisper_layer_names::qk_scores, + WhisperInferRequest::whisper_layer_names::qk_scores_ + std::to_string(idx)}); + + mutable_outputs.emplace_back(fake_result->output(0)); + } + } } LOG_DEBUG("Make prefill model with static shapes"); @@ -923,6 +995,11 @@ ov::npuw::LLMCompiledModel::LLMCompiledModel(const std::shared_ptr& m LOG_DEBUG("Converting KV-cache in prefill model to" << kv_kache_storage_type); ov::npuw::ConvertKVCacheToPrecision(kv_kache_storage_type).run_on_model(prefill_model); + std::optional user_compilation_mode_params = std::nullopt; + if (const auto it = other_props.find("NPU_COMPILATION_MODE_PARAMS"); it != other_props.end()) { + user_compilation_mode_params = it->second.as(); + } + auto prefill_config = prefill_config_opt.value_or(get_default_prefill_config(prefill_model, npudesc)).as(); @@ -934,6 +1011,14 @@ ov::npuw::LLMCompiledModel::LLMCompiledModel(const std::shared_ptr& m auto generate_config = generate_config_opt.value_or(get_default_generate_config(npudesc, generate_hint)).as(); + const std::optional default_compilation_mode_params = + [&prefill_config]() -> std::optional { + if (const auto it = prefill_config.find("NPU_COMPILATION_MODE_PARAMS"); it != prefill_config.end()) { + return it->second.as(); + } + return std::nullopt; + }(); + auto prefill_config_addition_value = prefill_config_addition.has_value() ? prefill_config_addition.value().as() : ov::AnyMap{}; auto generate_config_addition_value = @@ -944,12 +1029,11 @@ ov::npuw::LLMCompiledModel::LLMCompiledModel(const std::shared_ptr& m merge_config_with(prefill_config, prefill_config_addition_value); merge_config_with(generate_config, generate_config_addition_value); - // Convert LLM-specific attention hints to NPUW_ATTN - if (npuw_llm_props.count("NPUW_LLM_PREFILL_ATTENTION_HINT")) { - prefill_config["NPUW_ATTN"] = npuw_llm_props["NPUW_LLM_PREFILL_ATTENTION_HINT"]; - } - if (npuw_llm_props.count("NPUW_LLM_GENERATE_ATTENTION_HINT")) { - generate_config["NPUW_ATTN"] = npuw_llm_props["NPUW_LLM_GENERATE_ATTENTION_HINT"]; + if (user_compilation_mode_params.has_value() && default_compilation_mode_params.has_value() && + user_compilation_mode_params.value() != default_compilation_mode_params.value()) { + LOG_WARN("User-provided NPU_COMPILATION_MODE_PARAMS overrides arch-aware setting \"" + << default_compilation_mode_params.value() << "\". User value: \"" + << user_compilation_mode_params.value() << "\"."); } // Generate a random weights bank name unique to this LLMCompiledModel object @@ -966,12 +1050,28 @@ ov::npuw::LLMCompiledModel::LLMCompiledModel(const std::shared_ptr& m {"NPUW_ONLINE_KEEP_BLOCK_SIZE", "4"}, {"NPUW_UNFOLD_IREQS", "NO"}, }; - if (prefill_attn_dyn || prefill_attn_pyramid || prefill_attn_hfa) { + + if (m_use_chunk_prefill && (prefill_attn_pyramid || prefill_attn_hfa || prefill_attn_dyn)) { + prefill_config["NPUW_ATTN"] = ::intel_npu::NPUW_LLM_PREFILL_ATTENTION_HINT::toString(prefill_attn_hint); merge_config_with(prefill_config, dyn_attn_opts); } - if (generate_attn_dyn || generate_attn_pyramid || generate_attn_hfa) { + + if (generate_attn_pyramid || generate_attn_hfa || generate_attn_dyn) { + generate_config["NPUW_ATTN"] = ::intel_npu::NPUW_LLM_GENERATE_ATTENTION_HINT::toString(generate_attn_hint); merge_config_with(generate_config, dyn_attn_opts); } + + // Note: with dynamic attention in EITHER STAGE, we have to + // explicitly disable the run-time fallback to so extra ov::Model + // references won't be held by the npuw::CompiledModel, resulting + // in a higher memory consumption. This behavior should be reworked! + // The reason here is that NPUW_DEVICES may come as a global setting, + // impacting all the stages. + if (prefill_attn_dyn || generate_attn_dyn) { + prefill_config["NPUW_FALLBACK_EXEC"] = "NO"; + generate_config["NPUW_FALLBACK_EXEC"] = "NO"; + } + if (is_moe) { // Apply MoE configuration for prefill stage const auto prefill_moe_hint = m_cfg.get<::intel_npu::NPUW_LLM_PREFILL_MOE_HINT>(); @@ -981,26 +1081,22 @@ ov::npuw::LLMCompiledModel::LLMCompiledModel(const std::shared_ptr& m const auto generate_moe_hint = m_cfg.get<::intel_npu::NPUW_LLM_GENERATE_MOE_HINT>(); apply_moe_config(generate_config, generate_moe_hint, "GENERATE"); - // Apply model transformations only to GENERATE stage (PREFILL doesn't support DEVICE_ROUTED transformations) + // Fold shape-compute chains (ShapeOf→Gather→Concat etc.) in the prefill model before + // online partitioning runs pattern matching (e.g. GPTOSSRouter). Must run after + // ReshapeToStatic has made all shapes static so that ShapeOf bounds are resolvable. + ov::npuw::patterns::util::FoldShapeComputeChain().run_on_model(prefill_model); + for (auto&& model_variant : generate_model_variants) { + ov::npuw::patterns::util::FoldShapeComputeChain().run_on_model(model_variant); + } + if (generate_moe_hint == ::intel_npu::npuw::llm::MoEHint::DEVICE_ROUTED) { - LOG_INFO("Applying DEVICE_ROUTED MoE transformations to " << generate_model_variants.size() << " variants"); + // Apply model transformations only to GENERATE stage (PREFILL doesn't support DEVICE_ROUTED + // transformations) for (auto&& model_variant : generate_model_variants) { ov::npuw::ApplyMoEDeviceRoutedTransforms().run_on_model(model_variant); } - LOG_INFO("DEVICE_ROUTED MoE transformations completed"); } } - // Note: with dynamic attention in EITHER STAGE, we have to - // explicitly disable the run-time fallback to so extra ov::Model - // references won't be held by the npuw::CompiledModel, resulting - // in a higher memory consumption. This behavior should be reworked! - // The reason here is that NPUW_DEVICES may come as a global setting, - // impacting all the stages. - if (prefill_attn_dyn || generate_attn_dyn) { - const ov::AnyMap no_runtime_fallback = {{"NPUW_FALLBACK_EXEC", "NO"}}; - merge_config_with(prefill_config, no_runtime_fallback); - merge_config_with(generate_config, no_runtime_fallback); - } if (m_is_whisper) { update_config_for_whisper(prefill_config); @@ -1019,8 +1115,9 @@ ov::npuw::LLMCompiledModel::LLMCompiledModel(const std::shared_ptr& m LOG_DEBUG("Caching preROPE "); const uint32_t CACHE_ROPE_START = 2048; const bool is_best = (generate_hint == ::intel_npu::npuw::llm::GenerateHint::BEST_PERF); + const bool force_rope_cache = has_phi_v5_longrope_pattern(prefill_model); - if (!is_best || (max_prompt_len >= CACHE_ROPE_START)) { + if (!is_best || (max_prompt_len >= CACHE_ROPE_START || force_rope_cache)) { LOG_DEBUG("Enable RoPE Cache for prefill"); ov::npuw::patterns::pre_compute::RopeCache rope_prefill_cacher( max_prompt_len, @@ -1031,7 +1128,7 @@ ov::npuw::LLMCompiledModel::LLMCompiledModel(const std::shared_ptr& m // Apply RoPE Cache to all generate variant models for (size_t i = 0; i < generate_model_variants.size(); ++i) { const uint32_t kv_size = m_kvcache_sizes[i]; - if (!is_best || (kv_size >= CACHE_ROPE_START)) { + if (!is_best || (kv_size >= CACHE_ROPE_START || force_rope_cache)) { LOG_DEBUG("Enable RoPE Cache for generate variant with size: " << kv_size); ov::npuw::patterns::pre_compute::RopeCache rope_cacher( kv_size, @@ -1143,7 +1240,7 @@ void ov::npuw::LLMCompiledModel::export_model(std::ostream& stream) const { } } -void ov::npuw::LLMCompiledModel::serialize(std::ostream& stream, const ov::npuw::s11n::CompiledContext& ctx) const { +void ov::npuw::LLMCompiledModel::serialize(std::ostream& raw_stream, const ov::npuw::s11n::CompiledContext& ctx) const { LOG_INFO("Serializing LLMCompiledModel..."); LOG_BLOCK(); @@ -1158,41 +1255,32 @@ void ov::npuw::LLMCompiledModel::serialize(std::ostream& stream, const ov::npuw: } auto write_model_meta = [&](std::ostream& model_stream) { + auto stream = Stream::writer(model_stream); // Serialize name - write(model_stream, m_name); + stream & m_name; // Serialize inputs and outputs - write(model_stream, inputs()); - write(model_stream, outputs()); + stream& inputs() & outputs(); // Serialize LLMCompiledModel-specific data - write(model_stream, m_kvcache_desc.max_prompt_size); - write(model_stream, m_kvcache_desc.total_size); - write(model_stream, m_kvcache_desc.num_stored_tokens); - write(model_stream, m_kvcache_desc.dim); - write(model_stream, m_kvcache_desc.max_generation_token_len); - write(model_stream, m_kvcache_desc.v_tensors_transposed_pre); - write(model_stream, m_kvcache_desc.v_tensors_transposed_gen); - write(model_stream, m_prefill_chunk_size); - write(model_stream, m_use_chunk_prefill); - write(model_stream, m_max_lora_rank); - write(model_stream, m_enable_prefix_caching); - write(model_stream, m_prefix_caching_block_size); - write(model_stream, m_prefix_caching_max_num_blocks); - write(model_stream, m_is_whisper); - write(model_stream, m_eos_token_id); - write(model_stream, m_is_eagle); - write(model_stream, m_is_embedding); + stream & m_kvcache_desc.max_prompt_size & m_kvcache_desc.total_size & m_kvcache_desc.num_stored_tokens & + m_kvcache_desc.dim & m_kvcache_desc.max_generation_token_len & m_kvcache_desc.v_tensors_transposed_pre & + m_kvcache_desc.v_tensors_transposed_gen & m_prefill_chunk_size & m_use_chunk_prefill & m_max_lora_rank & + m_enable_prefix_caching & m_prefix_caching_block_size & m_prefix_caching_max_num_blocks & m_is_whisper & + m_eos_token_id & m_decomposed_sdpa_size & m_is_eagle & m_is_embedding; // Write config - write(model_stream, m_cfg); + stream & m_cfg; // Serialize KV cache model variants - write(model_stream, m_kvcache_sizes); - write(model_stream, static_cast(m_generate_compiled_variants.size())); + auto variant_count = static_cast(m_generate_compiled_variants.size()); + stream & m_kvcache_sizes & variant_count; // Serialize CompiledModels // Note: no need to pass any encryption here as it's done in export_model() + // This cache is collected on the original LLM graph before BF16->FP16 + // conversion and submodel splitting. Child CompiledModels must serialize + // this propagated view, not just their local post-transform snapshot. CompiledContext enc_ctx(false, nullptr, nullptr, m_bf16_consts); // Serialize all generate variants @@ -1202,7 +1290,7 @@ void ov::npuw::LLMCompiledModel::serialize(std::ostream& stream, const ov::npuw: m_prefill_compiled->serialize(model_stream, enc_ctx); const bool is_shared_lm_head = m_lm_head_compiled != nullptr; - write(model_stream, is_shared_lm_head); + stream & is_shared_lm_head; if (is_shared_lm_head) { m_lm_head_compiled->serialize(model_stream, enc_ctx); } @@ -1211,24 +1299,24 @@ void ov::npuw::LLMCompiledModel::serialize(std::ostream& stream, const ov::npuw: std::stringstream non_encrypted_stream; if (ctx.encrypted) { NPUW_ASSERT(ctx.encrypt && "Encryption function isn't provided!"); - non_encrypted_stream.copyfmt(stream); + non_encrypted_stream.copyfmt(raw_stream); write_model_meta(non_encrypted_stream); std::string encrypted_str = ctx.encrypt(non_encrypted_stream.str()); - write(stream, encrypted_str); + write(raw_stream, encrypted_str); } else { - write_model_meta(stream); + write_model_meta(raw_stream); } // Serialize bank name const auto& kv_bank = m_kvcache_compiled->get_weights_bank(); const auto& p_bank = m_prefill_compiled->get_weights_bank(); NPUW_ASSERT(kv_bank && p_bank && kv_bank == p_bank && "Prefill and KVCache models' weight bank should be shared!"); - write(stream, kv_bank->get_name()); + auto stream = Stream::writer(raw_stream); + auto bank_name = kv_bank->get_name(); + stream & bank_name; if (!is_weightless) { - // Serialize weights bank - // Note: no need to encrypt weights in full flow - kv_bank->serialize(stream); + stream&* kv_bank; } LOG_INFO("Done."); @@ -1289,9 +1377,9 @@ std::shared_ptr ov::npuw::LLMCompiledModel::import_m auto read_and_finalize_banks = [&](std::istream& model_stream, const std::shared_ptr& compiled) { - // Deserialize weights bank name + auto stream = Stream::reader(model_stream); std::string bank_name; - read(model_stream, bank_name); + stream & bank_name; if (is_weightless) { auto bank = ov::npuw::weights::bank(bank_name, compiled->get_plugin()->get_core(), ""); @@ -1309,8 +1397,8 @@ std::shared_ptr ov::npuw::LLMCompiledModel::import_m compiled->m_lm_head_compiled->finalize_weights_bank(); } } else { - auto bank = - ov::npuw::weights::Bank::deserialize(model_stream, compiled->get_plugin()->get_core(), bank_name); + auto bank = ov::npuw::weights::bank(bank_name, compiled->get_plugin()->get_core(), ""); + stream&* bank; compiled->m_kvcache_compiled->set_weights_bank(bank); for (const auto& compiled_variant : compiled->m_generate_compiled_variants) { @@ -1375,49 +1463,40 @@ std::shared_ptr ov::npuw::LLMCompiledModel::deserial using namespace ov::npuw::s11n; auto read_model_meta = [&](std::istream& model_stream) { + auto stream = Stream::reader(model_stream); // Deserialize model name first std::string model_name; - read(model_stream, model_name); + stream & model_name; // Create a dummy CompiledModel with an empty ov::Model - this will skip the constructor flow // to continue deserialization ov::ParameterVector parameters; ov::NodeVector results; - read(model_stream, parameters); - read(model_stream, results); + stream & parameters & results; auto ov_model = std::make_shared(ov::as_output_vector(results), parameters, model_name); auto compiled = std::make_shared(ov_model, plugin, true); // Deserialize LLMCompiledModel-specific data - read(model_stream, compiled->m_kvcache_desc.max_prompt_size); - read(model_stream, compiled->m_kvcache_desc.total_size); - read(model_stream, compiled->m_kvcache_desc.num_stored_tokens); - read(model_stream, compiled->m_kvcache_desc.dim); - read(model_stream, compiled->m_kvcache_desc.max_generation_token_len); - read(model_stream, compiled->m_kvcache_desc.v_tensors_transposed_pre); - read(model_stream, compiled->m_kvcache_desc.v_tensors_transposed_gen); - read(model_stream, compiled->m_prefill_chunk_size); - read(model_stream, compiled->m_use_chunk_prefill); - read(model_stream, compiled->m_max_lora_rank); - read(model_stream, compiled->m_enable_prefix_caching); - read(model_stream, compiled->m_prefix_caching_block_size); - read(model_stream, compiled->m_prefix_caching_max_num_blocks); - read(model_stream, compiled->m_is_whisper); - read(model_stream, compiled->m_eos_token_id); - read(model_stream, compiled->m_is_eagle); - read(model_stream, compiled->m_is_embedding); + stream & compiled->m_kvcache_desc.max_prompt_size & compiled->m_kvcache_desc.total_size & + compiled->m_kvcache_desc.num_stored_tokens & compiled->m_kvcache_desc.dim & + compiled->m_kvcache_desc.max_generation_token_len & compiled->m_kvcache_desc.v_tensors_transposed_pre & + compiled->m_kvcache_desc.v_tensors_transposed_gen & compiled->m_prefill_chunk_size & + compiled->m_use_chunk_prefill & compiled->m_max_lora_rank & compiled->m_enable_prefix_caching & + compiled->m_prefix_caching_block_size & compiled->m_prefix_caching_max_num_blocks & compiled->m_is_whisper & + compiled->m_eos_token_id & compiled->m_decomposed_sdpa_size & compiled->m_is_eagle & + compiled->m_is_embedding; // Deserialize config - read(model_stream, compiled->m_cfg); + stream & compiled->m_cfg; compiled->implement_properties(); // Deserialize KV cache model variants - read(model_stream, compiled->m_kvcache_sizes); + stream & compiled->m_kvcache_sizes; uint32_t num_variants = 0; - read(model_stream, num_variants); + stream & num_variants; compiled->m_generate_compiled_variants.reserve(num_variants); @@ -1438,7 +1517,7 @@ std::shared_ptr ov::npuw::LLMCompiledModel::deserial compiled->m_prefill_compiled = ov::npuw::CompiledModel::deserialize(model_stream, plugin, properties, enc_ctx); bool is_shared_lm_head = false; - read(model_stream, is_shared_lm_head); + stream & is_shared_lm_head; if (is_shared_lm_head) { compiled->m_lm_head_compiled = ov::npuw::CompiledModel::deserialize(model_stream, plugin, properties, enc_ctx); @@ -1541,6 +1620,7 @@ void ov::npuw::LLMCompiledModel::implement_properties() { BIND(npuw::llm::shared_lm_head, NPUW_LLM_SHARED_HEAD, get), BIND(npuw::whisper::enabled, NPUW_WHISPER, get), BIND(npuw::whisper::whisper_eos_token, NPUW_WHISPER_EOS_TOKEN, get), + BIND(npuw::whisper::whisper_decompose_sdpa, NPUW_WHISPER_DECOMPOSE_SDPA, get), BIND(npuw::eagle::enabled, NPUW_EAGLE, get), BIND(npuw::text_embed::enabled, NPUW_TEXT_EMBED, get)}); #undef BIND diff --git a/src/plugins/intel_npu/src/plugin/npuw/llm_compiled_model.hpp b/src/plugins/intel_npu/src/plugin/npuw/llm_compiled_model.hpp index b1c13601e3b2..2a91ee3cfc47 100644 --- a/src/plugins/intel_npu/src/plugin/npuw/llm_compiled_model.hpp +++ b/src/plugins/intel_npu/src/plugin/npuw/llm_compiled_model.hpp @@ -124,6 +124,7 @@ class LLMCompiledModel : public ov::npuw::ICompiledModel { bool m_is_whisper = false; uint64_t m_eos_token_id = 0; + size_t m_decomposed_sdpa_size = 0; bool m_is_embedding = false; diff --git a/src/plugins/intel_npu/src/plugin/npuw/llm_eagle3_extension.cpp b/src/plugins/intel_npu/src/plugin/npuw/llm_eagle3_extension.cpp index 9e431b599ce0..d3b2a50eb366 100644 --- a/src/plugins/intel_npu/src/plugin/npuw/llm_eagle3_extension.cpp +++ b/src/plugins/intel_npu/src/plugin/npuw/llm_eagle3_extension.cpp @@ -191,17 +191,17 @@ void Eagle3Extension::initialize(bool is_eagle_model, bool has_eagle_tree_mask_input = in_ports.find(Eagle3LayerNames::eagle_tree_mask) != in_ports.end(); bool has_last_hidden_state_output = out_ports.find(Eagle3LayerNames::last_hidden_state) != out_ports.end(); - if (has_hidden_states_input && has_eagle_tree_mask_input && has_last_hidden_state_output) { + if (has_hidden_states_input && has_last_hidden_state_output) { m_role = Eagle3ModelRole::Draft; - LOG_INFO("Eagle3 Draft Model detected"); - } else if (!has_hidden_states_input && has_eagle_tree_mask_input && has_last_hidden_state_output) { + LOG_INFO("Eagle3 Draft Model detected" << (has_eagle_tree_mask_input ? " (topk)" : " (top1)")); + } else if (!has_hidden_states_input && has_last_hidden_state_output) { m_role = Eagle3ModelRole::Target; - LOG_INFO("Eagle3 Target Model detected"); + LOG_INFO("Eagle3 Target Model detected" << (has_eagle_tree_mask_input ? " (topk)" : " (top1)")); } else { OPENVINO_THROW( "Eagle3 mode enabled via NPUW_EAGLE property, but model structure doesn't match Draft or Target pattern. " - "Draft requires: hidden_states input + eagle_tree_mask input + last_hidden_state output. " - "Target requires: eagle_tree_mask input + last_hidden_state output."); + "Draft requires: hidden_states input + last_hidden_state output. " + "Target requires: last_hidden_state output (no hidden_states input)."); } // Create sampling state for external pipeline communication @@ -210,14 +210,14 @@ void Eagle3Extension::initialize(bool is_eagle_model, void Eagle3Extension::prepare_inputs(const std::shared_ptr& request, const std::unordered_map>& in_ports) { - // Eagle3 models (both Draft and Target) MUST have eagle_tree_mask + // eagle_tree_mask is optional: present in topk pipeline, absent in top1 pipeline auto tree_mask_it = in_ports.find(Eagle3LayerNames::eagle_tree_mask); - OPENVINO_ASSERT(tree_mask_it != in_ports.end(), "Eagle3 model must have eagle_tree_mask input port"); - OPENVINO_ASSERT(m_eagle_tree_mask, "Eagle3 model requires eagle_tree_mask tensor to be provided by user"); - - auto padded_tree_mask = request->get_tensor(tree_mask_it->second); - pad_tree_mask_input(m_eagle_tree_mask, padded_tree_mask); - LOG_VERB("Eagle3: Set eagle_tree_mask input tensor"); + if (tree_mask_it != in_ports.end()) { + OPENVINO_ASSERT(m_eagle_tree_mask, "Eagle3 model requires eagle_tree_mask tensor to be provided by user"); + auto padded_tree_mask = request->get_tensor(tree_mask_it->second); + pad_tree_mask_input(m_eagle_tree_mask, padded_tree_mask); + LOG_VERB("Eagle3: Set eagle_tree_mask input tensor"); + } // Draft models MUST have hidden_states if (m_role == Eagle3ModelRole::Draft) { @@ -313,14 +313,14 @@ void Eagle3Extension::prepare_inputs_for_chunk( const std::unordered_map>& in_ports, uint32_t chunk_start_token, uint32_t chunk_token_count) { - // Eagle3 models (both Draft and Target) MUST have eagle_tree_mask + // eagle_tree_mask is optional: present in topk pipeline, absent in top1 pipeline auto tree_mask_it = in_ports.find(Eagle3LayerNames::eagle_tree_mask); - OPENVINO_ASSERT(tree_mask_it != in_ports.end(), "Eagle3 model must have eagle_tree_mask input port"); - OPENVINO_ASSERT(m_eagle_tree_mask, "Eagle3 model requires eagle_tree_mask tensor to be provided by user"); - - auto padded_tree_mask = request->get_tensor(tree_mask_it->second); - pad_tree_mask_input(m_eagle_tree_mask, padded_tree_mask); - LOG_VERB("Eagle3 Chunk: Set eagle_tree_mask input tensor (full mask, not chunked)"); + if (tree_mask_it != in_ports.end()) { + OPENVINO_ASSERT(m_eagle_tree_mask, "Eagle3 model requires eagle_tree_mask tensor to be provided by user"); + auto padded_tree_mask = request->get_tensor(tree_mask_it->second); + pad_tree_mask_input(m_eagle_tree_mask, padded_tree_mask); + LOG_VERB("Eagle3 Chunk: Set eagle_tree_mask input tensor (full mask, not chunked)"); + } // Draft models MUST have hidden_states for chunked prefill if (m_role != Eagle3ModelRole::Draft) { diff --git a/src/plugins/intel_npu/src/plugin/npuw/llm_infer_base_request.cpp b/src/plugins/intel_npu/src/plugin/npuw/llm_infer_base_request.cpp index dc28a17e4d4e..e74ceb9236da 100644 --- a/src/plugins/intel_npu/src/plugin/npuw/llm_infer_base_request.cpp +++ b/src/plugins/intel_npu/src/plugin/npuw/llm_infer_base_request.cpp @@ -1,4 +1,4 @@ -// Copyright (C) 2025 Intel Corporation +// Copyright (C) 2018-2026 Intel Corporation // SPDX-License-Identifier: Apache-2.0 // @@ -8,6 +8,8 @@ #include "infer_request_utils.hpp" +// NOTE: This is a basic method for updating KVCache after handling its part. +// It is not intended to work with model supporting Linear Cache. void ov::npuw::LLMInferBaseRequest::update_kvcache_for( std::shared_ptr request, const std::unordered_map>& in_ports, diff --git a/src/plugins/intel_npu/src/plugin/npuw/llm_infer_base_request.hpp b/src/plugins/intel_npu/src/plugin/npuw/llm_infer_base_request.hpp index 7f28b45cc771..7537cffb65a5 100644 --- a/src/plugins/intel_npu/src/plugin/npuw/llm_infer_base_request.hpp +++ b/src/plugins/intel_npu/src/plugin/npuw/llm_infer_base_request.hpp @@ -23,6 +23,7 @@ class LLMInferBaseRequest : public ov::ISyncInferRequest { static constexpr const char* logits = "logits"; static constexpr const char* token_type_ids = "token_type_ids"; static constexpr const char* longrope_input = "npuw_longrope_input"; + static constexpr const char* per_layer_inputs = "per_layer_inputs"; }; struct layer_ids { @@ -45,11 +46,11 @@ class LLMInferBaseRequest : public ov::ISyncInferRequest { } protected: - void update_kvcache_for(std::shared_ptr request, - const PortsMap& in_ports, - const PortsMap& out_ports, - uint32_t num_tokens, - bool v_transposed); + virtual void update_kvcache_for(std::shared_ptr request, + const PortsMap& in_ports, + const PortsMap& out_ports, + uint32_t num_tokens, + bool v_transposed); void init_tensor(const ov::Output& port); void init_ports(); diff --git a/src/plugins/intel_npu/src/plugin/npuw/llm_infer_request.cpp b/src/plugins/intel_npu/src/plugin/npuw/llm_infer_request.cpp index 40928285ee90..0e88c2d1f19a 100644 --- a/src/plugins/intel_npu/src/plugin/npuw/llm_infer_request.cpp +++ b/src/plugins/intel_npu/src/plugin/npuw/llm_infer_request.cpp @@ -103,7 +103,6 @@ void process_longrope(const std::shared_ptr& infer_req, longrope_input->data()[0] = max_pos_id; } } - } // anonymous namespace void ov::npuw::LLMInferRequest::init_lora_states() { @@ -140,16 +139,27 @@ ov::npuw::LLMInferRequest::LLMInferRequest(const std::shared_ptrget_compiled_model()->inputs()) { m_prefill_in_ports.emplace(input_port.get_any_name(), input_port); - // Cache past_key_values ports for efficient clearing - if (input_port.get_any_name().find(layer_names::past_key_values) != std::string::npos) { - m_prefill_past_kv_ports.push_back(input_port); - } } for (const auto& output_port : m_prefill_request->get_compiled_model()->outputs()) { m_prefill_out_ports.emplace(output_port.get_any_name(), output_port); } + for (const auto& input_port : m_kvcache_request->get_compiled_model()->inputs()) { + const auto& all_names = input_port.get_names(); + for (const auto& name : all_names) { + if (ov::npuw::util::starts_with(name, layer_names::past_key_values)) { + m_kvcache_past_names.push_back(name); + break; + } + if (ov::npuw::util::starts_with_past_lincache(name)) { + m_lincache_past_names.push_back(name); + break; + } + } + } + init_pre_alloc_device(); + m_stored_tokens_state = std::make_shared(); init_lora_states(); m_eagle3_ext.initialize(m_npuw_llm_compiled_model->m_is_eagle, m_prefill_in_ports, m_prefill_out_ports); @@ -158,6 +168,8 @@ ov::npuw::LLMInferRequest::LLMInferRequest(const std::shared_ptrget_compiled_model(); const auto& variant_in_ports = m_generate_variant_in_ports.at(largest_kvcache_req); - // FIXME: Find only matching by names outputs and copy them, having previously checked that such inputs - // exist - for (std::size_t i = layer_ids::kStartOutputKVCacheLayers; i < kvcache_compiled->outputs().size(); ++i) { - const auto& output_name = kvcache_compiled->outputs()[i].get_any_name(); - const auto& input_name = - std::regex_replace(output_name, std::regex("present"), layer_names::past_key_values); - if (variant_in_ports.find(input_name) == variant_in_ports.end()) { - continue; - } - auto kvcache_in_tensor = largest_kvcache_req->get_tensor(variant_in_ports.at(input_name)); + for (const auto& kv_past_name : m_kvcache_past_names) { + auto kvcache_in_tensor = largest_kvcache_req->get_tensor(variant_in_ports.at(kv_past_name)); // NB: Use fill_tensor_bytes to zero-fill regardless of element type. // fill_tensor would fail with a type mismatch error for i8 kv-cache ov::npuw::util::fill_tensor_bytes(kvcache_in_tensor, 0u); } + for (const auto& lincache_past_name : m_lincache_past_names) { + auto lincache_in_tensor = largest_kvcache_req->get_tensor(variant_in_ports.at(lincache_past_name)); + ov::npuw::util::fill_tensor_bytes(lincache_in_tensor, 0u); + } } m_generate_initialized = false; @@ -286,11 +293,19 @@ void ov::npuw::LLMInferRequest::create_generate_request_variants( std::unordered_map> largest_past_kv_tensors; for (const auto& input_port : largest_generate_request->get_compiled_model()->inputs()) { const auto& input_name = input_port.get_any_name(); - if (input_name.find(layer_names::past_key_values) != std::string::npos) { + if (ov::npuw::util::starts_with(input_name, layer_names::past_key_values)) { largest_past_kv_tensors[input_name] = largest_generate_request->get_tensor(input_port); } } + std::unordered_map> past_lin_tensors; + for (const auto& input_port : largest_generate_request->get_compiled_model()->inputs()) { + const auto& input_name = input_port.get_any_name(); + if (ov::npuw::util::starts_with_past_lincache(input_name)) { + past_lin_tensors[input_name] = largest_generate_request->get_tensor(input_port); + } + } + // Create all variant requests and share past KV tensors for (size_t i = 0; i < compiled_model->m_generate_compiled_variants.size(); ++i) { std::shared_ptr generate_request; @@ -305,20 +320,25 @@ void ov::npuw::LLMInferRequest::create_generate_request_variants( // Share past KV tensors from the largest variant for (const auto& input_port : generate_request->get_compiled_model()->inputs()) { const auto& input_name = input_port.get_any_name(); - if (input_name.find(layer_names::past_key_values) != std::string::npos) { - if (largest_past_kv_tensors.find(input_name) != largest_past_kv_tensors.end()) { - auto largest_tensor = largest_past_kv_tensors[input_name]; - auto small_shape = input_port.get_shape(); - - // Wrap the largest tensor's data pointer with smaller shape - auto shared_tensor = ov::SoPtr( - ov::make_tensor(input_port.get_element_type(), small_shape, largest_tensor->data()), - nullptr); - - generate_request->set_tensor(input_port, shared_tensor); - } else { - OPENVINO_ASSERT(false, "Unexpected input name: ", input_name); - } + if (ov::npuw::util::starts_with(input_name, layer_names::past_key_values)) { + OPENVINO_ASSERT(largest_past_kv_tensors.find(input_name) != largest_past_kv_tensors.end(), + "Unexpected input name: ", + input_name); + auto largest_tensor = largest_past_kv_tensors[input_name]; + auto small_shape = input_port.get_shape(); + + // Wrap the largest tensor's data pointer with smaller shape + auto shared_tensor = ov::SoPtr( + ov::make_tensor(input_port.get_element_type(), small_shape, largest_tensor->data()), + nullptr); + + generate_request->set_tensor(input_port, shared_tensor); + } else if (ov::npuw::util::starts_with_past_lincache(input_name)) { + OPENVINO_ASSERT(past_lin_tensors.find(input_name) != past_lin_tensors.end(), + "Unexpected input name: ", + input_name); + auto lin_tensor = past_lin_tensors[input_name]; + generate_request->set_tensor(input_port, lin_tensor); } } } @@ -477,9 +497,23 @@ void ov::npuw::LLMInferRequest::prepare_for_new_conversation(int64_t prompt_leng uu::fill_tensor(m_prefill_request->get_tensor(m_prefill_in_ports.at(layer_names::attention_mask)), 0); uu::fill_tensor(m_prefill_request->get_tensor(m_prefill_in_ports.at(layer_names::position_ids)), 0); - // Clear all past_key_values tensors - use cached ports for efficiency - for (const auto& port : m_prefill_past_kv_ports) { - uu::fill_tensor_bytes(m_prefill_request->get_tensor(port), 0u); + // Gemma4: Clear per_layer_inputs if present + if (auto per_layer_port = m_prefill_in_ports.find(layer_names::per_layer_inputs); + per_layer_port != m_prefill_in_ports.end()) { + uu::fill_tensor_bytes(m_prefill_request->get_tensor(per_layer_port->second), 0u); + } + + for (const auto& input_name : m_kvcache_past_names) { + // NOTE: Non-chunked prefill model doesn't contain input KVCache. + if (m_prefill_in_ports.find(input_name) != m_prefill_in_ports.end()) { + uu::fill_tensor_bytes(m_prefill_request->get_tensor(m_prefill_in_ports.at(input_name)), 0u); + } + } + + for (const auto& input_name : m_lincache_past_names) { + if (m_prefill_in_ports.find(input_name) != m_prefill_in_ports.end()) { + uu::fill_tensor_bytes(m_prefill_request->get_tensor(m_prefill_in_ports.at(input_name)), 0u); + } } m_npuw_llm_compiled_model->m_kvcache_desc.num_stored_tokens = 0u; @@ -496,19 +530,18 @@ void ov::npuw::LLMInferRequest::copy_kvcache() { LOG_DEBUG("Copying kv-cache from prefill to generate model."); LOG_BLOCK(); auto& kvcache_desc = m_npuw_llm_compiled_model->m_kvcache_desc; - const auto& kvcache_compiled = m_kvcache_request->get_compiled_model(); // FIXME: Find only matching by names outputs and copy them, having previously checked that such inputs exist - ov::parallel_for(kvcache_compiled->outputs().size() - layer_ids::kStartOutputKVCacheLayers, [&](size_t out_idx) { - const std::size_t i = layer_ids::kStartOutputKVCacheLayers + out_idx; - const auto& output_name = kvcache_compiled->outputs()[i].get_any_name(); + ov::parallel_for(m_kvcache_past_names.size(), [&](size_t out_idx) { + const auto& input_name = m_kvcache_past_names[out_idx]; + auto kvcache_in_tensor = m_kvcache_request->get_tensor(m_kvcache_in_ports.at(input_name)); + + const auto& output_name = std::regex_replace(input_name, std::regex(layer_names::past_key_values), "present"); + OPENVINO_ASSERT(m_prefill_out_ports.find(output_name) != m_prefill_out_ports.end(), + "Incosistent input/output naming for KV cache: ", + output_name, + " not found in prefill model outputs."); auto prefill_out_tensor = m_prefill_request->get_tensor(m_prefill_out_ports.at(output_name)); - const auto& input_name = std::regex_replace(output_name, std::regex("present"), layer_names::past_key_values); - if (m_kvcache_in_ports.find(input_name) == m_kvcache_in_ports.end()) { - // FIXME: Totally wrong debug message. input_name is an invalid name of input layer. - LOG_DEBUG("Input name " << input_name << " doesn't contain kv cache. Skipping."); - return; - } const auto is_value_tensor = output_name.find("value") != std::string::npos; const auto kv_dim = [&](bool v_trans) -> uint32_t { return (is_value_tensor && v_trans) ? 3u : kvcache_desc.dim; @@ -516,7 +549,6 @@ void ov::npuw::LLMInferRequest::copy_kvcache() { const auto& pre_kv_dim = kv_dim(kvcache_desc.v_tensors_transposed_pre); const auto& gen_kv_dim = kv_dim(kvcache_desc.v_tensors_transposed_gen); - auto kvcache_in_tensor = m_kvcache_request->get_tensor(m_kvcache_in_ports.at(input_name)); const auto prefill_chunk_size = m_npuw_llm_compiled_model->m_prefill_chunk_size; const bool use_chunk_prefill = m_npuw_llm_compiled_model->m_use_chunk_prefill; @@ -589,6 +621,76 @@ void ov::npuw::LLMInferRequest::copy_kvcache() { LOG_DEBUG("Done."); } +void ov::npuw::LLMInferRequest::update_kvcache_for( + std::shared_ptr request, + const std::unordered_map>& in_ports, + const std::unordered_map>& out_ports, + uint32_t num_tokens, + bool v_transposed) { + namespace uu = ov::npuw::util; + auto& kvcache_desc = m_npuw_llm_compiled_model->m_kvcache_desc; + + for (std::size_t i = 0; i < m_kvcache_past_names.size(); ++i) { + const auto& input_name = m_kvcache_past_names[i]; + OPENVINO_ASSERT(in_ports.find(input_name) != in_ports.end(), + "There is no ", + input_name, + " in input ports map, while it is expected!"); + const auto& output_name = std::regex_replace(input_name, std::regex(layer_names::past_key_values), "present"); + OPENVINO_ASSERT(out_ports.find(output_name) != out_ports.end(), + "There is no ", + output_name, + " in output ports map, while it is expected!"); + + auto dst_tensor = request->get_tensor(in_ports.at(input_name)); + const auto& kv_dim = (output_name.find("value") != std::string::npos && v_transposed) ? 3u : kvcache_desc.dim; + auto dst_slice = uu::make_tensor_slice(dst_tensor, + kv_dim, + kvcache_desc.num_stored_tokens - num_tokens, + kvcache_desc.num_stored_tokens); + auto src_tensor = request->get_tensor(out_ports.at(output_name)); + + // NOTE: Sometimes present kv layer can contain greater seq_len + // than was sent to be processed + uint32_t src_seq_len = static_cast(src_tensor->get_shape()[kv_dim]); + OPENVINO_ASSERT(num_tokens <= src_seq_len); + if (src_seq_len > num_tokens) { + auto src_slice = uu::make_tensor_slice(src_tensor, kv_dim, src_seq_len - num_tokens, src_seq_len); + uu::copy_tensor_by_dim(src_slice, dst_slice, kv_dim, kv_dim); + } else { + uu::copy_tensor_by_dim(src_tensor, dst_slice, kv_dim, kv_dim); + } + } +} + +void ov::npuw::LLMInferRequest::copy_lincache( + std::shared_ptr from_request, + std::shared_ptr to_request, + const std::unordered_map>& from_ports, + const std::unordered_map>& to_ports) { + namespace uu = ov::npuw::util; + LOG_DEBUG("Copying linear cache."); + LOG_BLOCK(); + ov::parallel_for(m_lincache_past_names.size(), [&](size_t out_idx) { + const auto& input_name = m_lincache_past_names[out_idx]; + OPENVINO_ASSERT(to_ports.find(input_name) != to_ports.end(), + "Incosistent input/output naming for linear cache: ", + input_name, + " not found in model inputs."); + auto to_tensor = to_request->get_tensor(to_ports.at(input_name)); + + const auto& output_name = std::regex_replace(input_name, std::regex("past"), "present"); + OPENVINO_ASSERT(from_ports.find(output_name) != from_ports.end(), + "Incosistent input/output naming for linear cache: ", + output_name, + " not found in model outputs."); + auto from_tensor = from_request->get_tensor(from_ports.at(output_name)); + + from_tensor->copy_to(to_tensor._ptr); + }); + LOG_DEBUG("Done."); +} + void ov::npuw::LLMInferRequest::trim_kvcache_for_speculative_decoding(ov::SoPtr position_ids) { auto& kvcache_desc = m_npuw_llm_compiled_model->m_kvcache_desc; // FIXME: It won't work with Qwen2.5-VL/Omni for now. @@ -603,16 +705,11 @@ void ov::npuw::LLMInferRequest::trim_kvcache_for_speculative_decoding(ov::SoPtr< } void ov::npuw::LLMInferRequest::clear_chunk_prefill_kv_cache() { - const auto& prefill_compiled = m_prefill_request->get_compiled_model(); - - for (std::size_t i = layer_ids::kStartOutputKVCacheLayers; i < prefill_compiled->outputs().size(); ++i) { - const auto& output_name = prefill_compiled->outputs()[i].get_any_name(); - const auto& input_name = std::regex_replace(output_name, std::regex("present"), "past_key_values"); - if (m_prefill_in_ports.find(input_name) == m_prefill_in_ports.end()) { - // FIXME: Totally wrong debug message. input_name is an invalid name of input layer. - LOG_DEBUG("Input name " << input_name << " doesn't contain kv cache. Skipping."); - continue; - } + for (const auto& input_name : m_kvcache_past_names) { + OPENVINO_ASSERT(m_prefill_in_ports.find(input_name) != m_prefill_in_ports.end(), + "Incosistent input/output naming for KV cache: ", + input_name, + " not found in prefill model inputs."); auto chunk_prefill_kvcache_in_tensor = m_prefill_request->get_tensor(m_prefill_in_ports.at(input_name)); @@ -625,14 +722,15 @@ void ov::npuw::LLMInferRequest::clear_chunk_prefill_kv_cache() { void ov::npuw::LLMInferRequest::infer_chunked_prefill(ov::SoPtr input_ids, ov::SoPtr attention_mask, - ov::SoPtr position_ids) { + ov::SoPtr position_ids, + ov::SoPtr per_layer_inputs) { LOG_DEBUG("Calling chunked inference for prefill model."); LOG_BLOCK(); const auto input_prompt_len = input_ids->get_shape()[layer_ids::INPUT_IDS_SEQ_LEN_DIM]; - // For LLM, model accepts 2d inputs_embeds[BATCH, SEQ_LEN] - // For VLM, model accepts 3d inputs_ids[BATCH, SEQ_LEN, EMB_SIZE] + // For LLM, model accepts 2d inputs_ids[BATCH, SEQ_LEN] + // For VLM, model accepts 3d inputs_embeds[BATCH, SEQ_LEN, EMB_SIZE] bool is_input_embeds = input_ids->get_shape().size() == 2 ? false : true; const auto input_ids_elem_size = input_ids->get_element_type().size(); @@ -700,7 +798,7 @@ void ov::npuw::LLMInferRequest::infer_chunked_prefill(ov::SoPtr inp reinterpret_cast(input_ids_in_tensor->data()) + input_ids_in_tensor->get_byte_size() - current_prefill_bytes); - // NB: Regular LLM uses 2D position_ids [BATCH, SEQ_LEN], Qwen2.5 VL/Omni uses 3D position_ids + // NB: Regular LLM uses 2D position_ids [BATCH, SEQ_LEN], Qwen2.5 VL/Omni, Qwen3.5 VL use 3D position_ids // [3, BATCH, SEQ_LEN] // Copy postion ids with considering the 3D position_ids auto last_dim = position_ids->get_shape().size() - 1; @@ -729,6 +827,17 @@ void ov::npuw::LLMInferRequest::infer_chunked_prefill(ov::SoPtr inp // Update history size for dynamic context: // dynamic attention selector needs history size to determin the past KV shape and attention mask shape m_prefill_base_request->update_history_size(kvcache_desc.num_stored_tokens); + + // Gemma4: copy the current chunk of per_layer_inputs right-aligned on seq_len dim. + // Source shape: [1, input_prompt_len, num_layers, proj_dim] + // Dest shape: [1, chunk_prompt_len, num_layers, proj_dim] (static) + if (per_layer_inputs) { + auto dst = m_prefill_request->get_tensor(m_prefill_in_ports.at(layer_names::per_layer_inputs)); + ov::npuw::util::copy_per_layer_inputs_chunk_to_right(per_layer_inputs, + dst, + kvcache_desc.num_stored_tokens, + static_cast(current_prompts_len)); + } }); m_llm_profile["1/prefill:3b.infer"].record([&]() { @@ -769,6 +878,8 @@ void ov::npuw::LLMInferRequest::infer_chunked_prefill(ov::SoPtr inp static_cast(current_prompts_len), kvcache_desc.v_tensors_transposed_pre); + copy_lincache(m_prefill_request, m_prefill_request, m_prefill_out_ports, m_prefill_in_ports); + // Update attention mask for the next iteration std::copy_n(attn_mask_in_tensor->data() + attn_mask_in_tensor->get_size() - current_prompts_len, current_prompts_len, @@ -786,7 +897,8 @@ void ov::npuw::LLMInferRequest::infer_chunked_prefill(ov::SoPtr inp void ov::npuw::LLMInferRequest::infer_whole_prefill(ov::SoPtr input_ids, ov::SoPtr attention_mask, ov::SoPtr position_ids, - ov::SoPtr token_type_ids) { + ov::SoPtr token_type_ids, + ov::SoPtr per_layer_inputs) { LOG_DEBUG("Calling inference for prefill model in a single launch."); LOG_BLOCK(); @@ -818,6 +930,13 @@ void ov::npuw::LLMInferRequest::infer_whole_prefill(ov::SoPtr input if (m_eagle3_ext.is_eagle3_model()) { m_eagle3_ext.prepare_inputs(m_prefill_request, m_prefill_in_ports); } + + // Gemma4: pass per_layer_inputs from external inputs into the prefill sub-request. + // Shape is [1, seq_len, num_layers, proj_dim]; copy right-aligned on the seq_len dim. + if (per_layer_inputs) { + auto dst = m_prefill_request->get_tensor(m_prefill_in_ports.at(layer_names::per_layer_inputs)); + ov::npuw::util::copy_to_right(per_layer_inputs, dst); + } }); m_llm_profile["1/prefill:3b.infer"].record([&]() { @@ -833,7 +952,8 @@ void ov::npuw::LLMInferRequest::infer_whole_prefill(ov::SoPtr input void ov::npuw::LLMInferRequest::infer_prefill(ov::SoPtr input_ids, ov::SoPtr attention_mask, ov::SoPtr position_ids, - ov::SoPtr token_type_ids) { + ov::SoPtr token_type_ids, + ov::SoPtr per_layer_inputs) { LOG_DEBUG("Calling inference for prefill model..."); LOG_BLOCK(); @@ -862,9 +982,9 @@ void ov::npuw::LLMInferRequest::infer_prefill(ov::SoPtr input_ids, OPENVINO_ASSERT(!token_type_ids, "Chunking is not implemented for Gemma model family yet. " "Please set NPUW_LLM_PREFILL_HINT to 'STATIC'"); - infer_chunked_prefill(input_ids, attention_mask, position_ids); + infer_chunked_prefill(input_ids, attention_mask, position_ids, per_layer_inputs); } else { - infer_whole_prefill(input_ids, attention_mask, position_ids, token_type_ids); + infer_whole_prefill(input_ids, attention_mask, position_ids, token_type_ids, per_layer_inputs); } }); @@ -892,7 +1012,8 @@ void ov::npuw::LLMInferRequest::infer_prefill(ov::SoPtr input_ids, void ov::npuw::LLMInferRequest::infer_generate(ov::SoPtr input_ids, ov::SoPtr attention_mask, ov::SoPtr position_ids, - ov::SoPtr token_type_ids) { + ov::SoPtr token_type_ids, + ov::SoPtr per_layer_inputs) { LOG_DEBUG("Calling inference for generate model..."); LOG_BLOCK(); auto& kvcache_desc = m_npuw_llm_compiled_model->m_kvcache_desc; @@ -920,6 +1041,7 @@ void ov::npuw::LLMInferRequest::infer_generate(ov::SoPtr input_ids, LOG_DEBUG("Copy kv-cache from prefill to generate model."); if (kvcache_desc.num_stored_tokens > 0) { copy_kvcache(); + copy_lincache(m_prefill_request, m_kvcache_request, m_prefill_out_ports, m_kvcache_in_ports); } LOG_DEBUG("Prepare inputs."); @@ -983,6 +1105,13 @@ void ov::npuw::LLMInferRequest::infer_generate(ov::SoPtr input_ids, if (m_eagle3_ext.is_eagle3_model()) { m_eagle3_ext.prepare_inputs(m_kvcache_request, m_kvcache_in_ports); } + + // Gemma4: pass per_layer_inputs from external inputs into the generate sub-request. + // Shape is [1, 1, num_layers, proj_dim] during generate; copy right-aligned on seq_len dim. + if (per_layer_inputs) { + auto dst = m_kvcache_request->get_tensor(m_kvcache_in_ports.at(layer_names::per_layer_inputs)); + ov::npuw::util::copy_to_right(per_layer_inputs, dst); + } }); m_llm_profile["N/generate:2.infer"].record([&]() { @@ -1002,7 +1131,10 @@ void ov::npuw::LLMInferRequest::infer_generate(ov::SoPtr input_ids, kvcache_desc.v_tensors_transposed_gen); } }); - m_llm_profile["N/generate:4.lm_head"].record([&]() { + m_llm_profile["N/generate:4.copy_lincache"].record([&]() { + copy_lincache(m_kvcache_request, m_kvcache_request, m_kvcache_out_ports, m_kvcache_in_ports); + }); + m_llm_profile["N/generate:5.lm_head"].record([&]() { m_lm_head_request->wait(); LOG_DEBUG("Calling inference for LM head model -- done."); @@ -1018,6 +1150,9 @@ void ov::npuw::LLMInferRequest::infer_generate(ov::SoPtr input_ids, kvcache_desc.v_tensors_transposed_gen); } }); + m_llm_profile["N/generate:4.copy_lincache"].record([&]() { + copy_lincache(m_kvcache_request, m_kvcache_request, m_kvcache_out_ports, m_kvcache_in_ports); + }); m_logits = m_kvcache_request->get_tensor(m_kvcache_out_ports.at(layer_names::logits)); } @@ -1034,8 +1169,33 @@ void ov::npuw::LLMInferRequest::infer() { auto input_ids = get_tensor(ov::npuw::util::find_port_by_name(inputs, m_input_ids_name).value()); auto attention_mask = get_tensor(ov::npuw::util::find_port_by_name(inputs, layer_names::attention_mask).value()); - // FIXME: position_ids might be optional for some models! - auto position_ids = get_tensor(ov::npuw::util::find_port_by_name(inputs, layer_names::position_ids).value()); + + auto position_ids = ov::npuw::util::TensorPtr(); + auto position_ids_opt = ov::npuw::util::find_port_by_name(inputs, layer_names::position_ids); + if (position_ids_opt.has_value()) { + position_ids = get_tensor(position_ids_opt.value()); + } else { + // Sync num_stored_tokens before infer with external updates + auto& kvcache_desc = m_npuw_llm_compiled_model->m_kvcache_desc; + auto externally_set_num_stored_tokens = m_stored_tokens_state->get_num_stored_tokens(); + if (externally_set_num_stored_tokens > kvcache_desc.total_size) { + OPENVINO_THROW("Number of updated stored tokens in KV-Cache is greater than total KV-Cache size."); + } + if (externally_set_num_stored_tokens < 0) { + OPENVINO_THROW("Number of updated stored tokens is negative!"); + } + if (externally_set_num_stored_tokens == 0) { + m_first_run = true; + } + kvcache_desc.num_stored_tokens = static_cast(externally_set_num_stored_tokens); + + // FIXME: ov::SoPtr? + position_ids = + ov::make_tensor(ov::element::i64, ov::Shape{input_ids->get_shape()[0], input_ids->get_shape()[1]}); + std::iota(position_ids->data(), + position_ids->data() + position_ids->get_size(), + kvcache_desc.num_stored_tokens); + } auto token_type_ids = ov::npuw::util::TensorPtr(); @@ -1044,6 +1204,13 @@ void ov::npuw::LLMInferRequest::infer() { token_type_ids = get_tensor(type_ids_port.value()); } + // Gemma4: Extract per_layer_inputs if present + auto per_layer_inputs = ov::npuw::util::TensorPtr(); + if (auto per_layer_port = ov::npuw::util::find_port_by_name(inputs, layer_names::per_layer_inputs); + per_layer_port.has_value()) { + per_layer_inputs = get_tensor(per_layer_port.value()); + } + // NB: For VLM, the "inputs_embeds" contains float values (embeddings) OPENVINO_ASSERT(ov::element::f32 == input_ids->get_element_type() || ov::element::i64 == input_ids->get_element_type()); @@ -1089,7 +1256,7 @@ void ov::npuw::LLMInferRequest::infer() { // both main and draft models for most of LLMs. if (input_ids->get_shape()[layer_ids::INPUT_IDS_SEQ_LEN_DIM] > 1 && position_ids->data()[0] == m_first_position_id) { - infer_prefill(input_ids, attention_mask, position_ids, token_type_ids); + infer_prefill(input_ids, attention_mask, position_ids, token_type_ids, per_layer_inputs); } else { // FIXME: Need to make the solution smarter. // Qwen2.5VL uses 3D position_ids but current `trim_kvcache_for_speculative_decoding` @@ -1098,7 +1265,12 @@ void ov::npuw::LLMInferRequest::infer() { if (position_ids->get_shape().size() < 3) { trim_kvcache_for_speculative_decoding(position_ids); } - infer_generate(input_ids, attention_mask, position_ids, token_type_ids); + infer_generate(input_ids, attention_mask, position_ids, token_type_ids, per_layer_inputs); + } + + if (!position_ids_opt.has_value()) { + // Sync num_stored_tokens after infer with internal updates + m_stored_tokens_state->set_num_stored_tokens(m_npuw_llm_compiled_model->m_kvcache_desc.num_stored_tokens); } } @@ -1128,6 +1300,10 @@ ov::SoPtr ov::npuw::LLMInferRequest::get_tensor(const ov::Output> ov::npuw::LLMInferRequest::query_state() const { auto states = m_variableStates; + if (m_stored_tokens_state) { + states.push_back(m_stored_tokens_state); + } + // Add Eagle3 sampling state if available auto eagle3_state = m_eagle3_ext.get_sampling_state(); if (eagle3_state) { diff --git a/src/plugins/intel_npu/src/plugin/npuw/llm_infer_request.hpp b/src/plugins/intel_npu/src/plugin/npuw/llm_infer_request.hpp index 82ffb341b576..8f8576396e77 100644 --- a/src/plugins/intel_npu/src/plugin/npuw/llm_infer_request.hpp +++ b/src/plugins/intel_npu/src/plugin/npuw/llm_infer_request.hpp @@ -12,6 +12,7 @@ #include "llm_infer_base_request.hpp" #include "llm_lora_states.hpp" #include "llm_prefix_caching.hpp" +#include "llm_stored_tokens_state.hpp" #include "openvino/core/descriptor/output.hpp" #include "perf.hpp" @@ -33,7 +34,15 @@ class LLMInferRequest : public ov::npuw::LLMInferBaseRequest { void apply_lora(); void clear_chunk_prefill_kv_cache(); void copy_kvcache(); - + void update_kvcache_for(std::shared_ptr request, + const PortsMap& in_ports, + const PortsMap& out_ports, + uint32_t num_tokens, + bool v_transposed) override; + void copy_lincache(std::shared_ptr from_request, + std::shared_ptr to_request, + const std::unordered_map>& from_ports, + const std::unordered_map>& to_ports); // Create and initialize generate variant requests with memory sharing void create_generate_request_variants(const std::shared_ptr& compiled_model); @@ -46,22 +55,26 @@ class LLMInferRequest : public ov::npuw::LLMInferBaseRequest { void infer_chunked_prefill(ov::SoPtr input_ids, ov::SoPtr attention_mask, - ov::SoPtr position_ids); + ov::SoPtr position_ids, + ov::SoPtr per_layer_inputs); void infer_whole_prefill(ov::SoPtr input_ids, ov::SoPtr attention_mask, ov::SoPtr position_ids, - ov::SoPtr input_token_ids); + ov::SoPtr token_type_ids, + ov::SoPtr per_layer_inputs); void infer_prefill(ov::SoPtr input_ids, ov::SoPtr attention_mask, ov::SoPtr position_ids, - ov::SoPtr input_token_ids); + ov::SoPtr token_type_ids, + ov::SoPtr per_layer_inputs); void infer_generate(ov::SoPtr input_ids, ov::SoPtr attention_mask, ov::SoPtr position_ids, - ov::SoPtr input_token_ids); + ov::SoPtr token_type_ids, + ov::SoPtr per_layer_inputs); // Multiple generate inference request variants, each with a different KV cache size std::vector> m_generate_requests; @@ -93,8 +106,8 @@ class LLMInferRequest : public ov::npuw::LLMInferBaseRequest { ov::Output m_lm_head_logits_port; - // Cache past_key_values ports for efficient clearing in prepare_for_new_conversation - std::vector> m_prefill_past_kv_ports; + std::vector m_kvcache_past_names; + std::vector m_lincache_past_names; // NB: It can be either input_ids(LLM) or inputs_embeds(VLM) std::string m_input_ids_name; @@ -110,6 +123,9 @@ class LLMInferRequest : public ov::npuw::LLMInferBaseRequest { // Support Eagle3 speculative decoding Eagle3Extension m_eagle3_ext; + // Support reset of stored tokens to 0 from external pipeline + ov::SoPtr m_stored_tokens_state; + // Support LoRA std::vector> m_variableStates; void init_lora_states(); diff --git a/src/plugins/intel_npu/src/plugin/npuw/llm_stored_tokens_state.hpp b/src/plugins/intel_npu/src/plugin/npuw/llm_stored_tokens_state.hpp new file mode 100644 index 000000000000..ffa45c1f18e0 --- /dev/null +++ b/src/plugins/intel_npu/src/plugin/npuw/llm_stored_tokens_state.hpp @@ -0,0 +1,52 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#pragma once + +#include "openvino/runtime/itensor.hpp" +#include "openvino/runtime/ivariable_state.hpp" +#include "openvino/runtime/make_tensor.hpp" + +namespace ov { +namespace npuw { + +// Special VariableState for already processed/stored tokens in LLM. +// Allows external pipelines to call reset on that state where needed. +class StoredTokensState : public ov::IVariableState { +public: + friend class ov::npuw::LLMInferRequest; + StoredTokensState() : ov::IVariableState("npuw_stored_tokens_state") { + auto tensor = ov::Tensor(ov::element::i64, ov::Shape{1}); + m_state = ov::get_tensor_impl(tensor); + reset(); + } + + void reset() override { + m_state->data()[0] = 0; + } + + void set_state(const ov::SoPtr&) override { + OPENVINO_THROW("StoredTokensState::set_state() should not be called!"); + } + + // Returns a copy of state to prevent external modfication. + ov::SoPtr get_state() const override { + const auto* state_data = m_state->data(); + auto result = ov::Tensor(ov::element::i64, ov::Shape{1}); + result.data()[0] = state_data[0]; + return ov::get_tensor_impl(result); + } + +private: + int64_t get_num_stored_tokens() const { + return m_state->data()[0]; + } + + void set_num_stored_tokens(int64_t num_tokens) { + m_state->data()[0] = num_tokens; + } +}; + +} // namespace npuw +} // namespace ov diff --git a/src/plugins/intel_npu/src/plugin/npuw/moe/moe_config.hpp b/src/plugins/intel_npu/src/plugin/npuw/moe/moe_config.hpp index e40f30d2a6d9..4173bbddc196 100644 --- a/src/plugins/intel_npu/src/plugin/npuw/moe/moe_config.hpp +++ b/src/plugins/intel_npu/src/plugin/npuw/moe/moe_config.hpp @@ -36,9 +36,14 @@ struct MoEConfig { // Example: param[5] -> [param[10], param[11], param[12], param[13]] for K=4 experts std::map> param_mapping; - // Input parameter indices - std::optional router_scores_idx; // Index of router scores input - std::optional expert_input_param_idx; // Index of expert input (token embeddings) + // Holds original-model index (for prologue matching / param_mapping lookup) + // and compiled-model index (for direct port binding after transformation). + struct ParamIndex { + std::optional original; // index in original model + std::optional compiled; // index in compiled/transformed model + }; + ParamIndex router_scores; // router scores input parameter + ParamIndex expert_input; // expert activation input parameter // Compiled models for different chunk sizes (for EXPERT_ITERATIVE mode) // Key: chunk_size (e.g., 256, 128, 64, 32, 16) diff --git a/src/plugins/intel_npu/src/plugin/npuw/moe/moe_executor.cpp b/src/plugins/intel_npu/src/plugin/npuw/moe/moe_executor.cpp index cfc46862cbc3..c307f6bda647 100644 --- a/src/plugins/intel_npu/src/plugin/npuw/moe/moe_executor.cpp +++ b/src/plugins/intel_npu/src/plugin/npuw/moe/moe_executor.cpp @@ -4,6 +4,8 @@ #include "moe_executor.hpp" +#include + #include "../compiled_model.hpp" // For CompiledModel::CompiledModelDesc #include "../logging.hpp" #include "moe_infer_utils.hpp" @@ -31,24 +33,25 @@ void MoEExecutor::prepare(size_t idx, size_t real_idx, size_t num_sublayers, siz const auto& desc = *static_cast(m_accessor.get_submodel_desc(real_idx)); - if (!desc.moe_experts.has_value()) { + const auto* moe_experts = get_compiled_experts(desc.pipeline.context); + if (moe_experts == nullptr) { OPENVINO_THROW("MoEExecutor::prepare called on non-MoE submodel[real_idx=", real_idx, "]"); } - const auto& moe_experts = desc.moe_experts.value(); - // Step 1: Initialize MoE config (only once, on first prepare call) if (!m_config.is_valid()) { LOG_DEBUG("Creating shared MoE config"); - m_config.num_experts = moe_experts.num_experts; - m_config.num_active_experts = moe_experts.num_active_experts; - m_config.input_token_count = moe_experts.input_token_count; - m_config.expert_hidden_dim = moe_experts.expert_hidden_dim; - m_config.param_mapping = moe_experts._param_mapping; - m_config.router_scores_idx = moe_experts._router_scores_idx; - m_config.expert_input_param_idx = moe_experts._expert_input_param_idx; - m_config.compiled_models = moe_experts._compiled_models; + m_config.num_experts = moe_experts->num_experts; + m_config.num_active_experts = moe_experts->num_active_experts; + m_config.input_token_count = moe_experts->input_token_count; + m_config.expert_hidden_dim = moe_experts->expert_hidden_dim; + m_config.param_mapping = moe_experts->_param_mapping; + m_config.router_scores.original = moe_experts->_router_scores.original; + m_config.router_scores.compiled = moe_experts->_router_scores.compiled; + m_config.expert_input.original = moe_experts->_expert_input.original; + m_config.expert_input.compiled = moe_experts->_expert_input.compiled; + m_config.compiled_models = moe_experts->_compiled_models; // Validate configuration if (!m_config.is_valid()) { @@ -136,18 +139,16 @@ bool MoEExecutor::function_prologue_moe_input(size_t idx, *static_cast(m_accessor.get_submodel_desc(real_idx)); // MoE expert layer: handle router scores and expert inputs - if (desc.moe_experts) { - const auto& moe = desc.moe_experts.value(); - + if (const auto* moe = get_compiled_experts(desc.pipeline.context)) { // Router scores: store for later use in MoE inference - if (moe._router_scores_idx.has_value() && param_idx == moe._router_scores_idx.value()) { + if (moe->_router_scores.original.has_value() && param_idx == moe->_router_scores.original.value()) { NPUW_ASSERT(idx < m_moe_io.size() && "MoEExecutor::prepare() must be called first"); m_moe_io[idx].router_scores = i_tensor; return true; // Handled by MoE } // Expert inputs: store for later use (batch mode: cache binding, iterative mode: chunking) - if (moe._expert_input_param_idx.has_value() && param_idx == moe._expert_input_param_idx.value()) { + if (moe->_expert_input.original.has_value() && param_idx == moe->_expert_input.original.value()) { NPUW_ASSERT(idx < m_moe_io.size() && "MoEExecutor::prepare() must be called first"); m_moe_io[idx].expert_input = i_tensor; return true; // Handled by MoE @@ -159,12 +160,11 @@ bool MoEExecutor::function_prologue_moe_input(size_t idx, } // MoE downstream layer: bind expert output accumulator or other parameters - if (desc.moe_experts_downstream) { - const auto& moe_downstream = desc.moe_experts_downstream.value(); + if (const auto* moe_downstream = get_compiled_downstream(desc.pipeline.context)) { const auto& iport = desc.compiled_model->inputs()[param_idx]; auto real_request = m_accessor.get_subrequest(real_idx); - if (param_idx == moe_downstream.expert_output_param_idx) { + if (param_idx == moe_downstream->expert_output_param_idx) { // Bind expert output accumulator auto output_buffer = m_resources.expert_output_accumulator; NPUW_ASSERT(output_buffer && "MoE output buffer not available"); @@ -177,7 +177,7 @@ bool MoEExecutor::function_prologue_moe_input(size_t idx, } // Should not reach here if is_moe flag is set correctly - NPUW_ASSERT(false && "MoE flag set but no moe_experts/moe_experts_downstream found"); + NPUW_ASSERT(false && "MoE flag set but no compiled MoE state found"); return false; } @@ -215,37 +215,27 @@ void MoEExecutor::run(size_t real_idx, size_t idx) { OPENVINO_THROW("MoE: Router scores are required but not available"); } - // Parse router scores and populate routing maps - // Note: parse_selected_experts_from_router() clears the maps internally before populating - std::vector selected_experts; + // Dispatch to appropriate inference function. + // EXPERT_BATCH: parse upfront (single token, routing maps needed for cache lookup). + // EXPERT_ITERATIVE: parse is fused inside run_expert_iterative, expert-row by row, + // overlapping with NPU execution to hide the per-layer parse overhead. if (processing_mode == MoEProcessingMode::EXPERT_BATCH) { + std::vector selected_experts; m_profile->batch["Parse Router Output"].record([&]() { selected_experts = ov::npuw::moe::parse_selected_experts_from_router(io.router_scores, num_experts, m_token_to_experts, m_expert_to_tokens); }); - } else { - m_profile->iterative["Parse Router Output"].record([&]() { - selected_experts = ov::npuw::moe::parse_selected_experts_from_router(io.router_scores, - num_experts, - m_token_to_experts, - m_expert_to_tokens); - }); - } - - if (selected_experts.empty()) { - OPENVINO_THROW("MoE: No experts selected by router"); - } - - // Dispatch to appropriate inference function - if (processing_mode == MoEProcessingMode::EXPERT_BATCH) { + if (selected_experts.empty()) { + OPENVINO_THROW("MoE: No experts selected by router"); + } m_profile->batch["Total Expert Batch"].record([&]() { run_expert_batch(idx, real_idx, selected_experts); }); } else { m_profile->iterative["Total Expert Iterative"].record([&]() { - run_expert_iterative(idx, real_idx, selected_experts); + run_expert_iterative(idx); }); } @@ -302,8 +292,10 @@ void MoEExecutor::run_expert_batch(size_t idx, size_t real_idx, const std::vecto // Step 4: Bind input/output tensors to cached request (must bind every time as these tensors change) // 4.1: Bind expert input (token embeddings) - if (m_config.expert_input_param_idx.has_value()) { - const auto expert_input_idx = m_config.expert_input_param_idx.value(); + // Use the transformed parameter index to access the compiled model's port correctly. + // (unroll_expert_dimension may shift the expert_input parameter index relative to the original model) + if (m_config.expert_input.compiled.has_value()) { + const auto expert_input_idx = m_config.expert_input.compiled.value(); const auto& expert_input_port = desc.compiled_model->inputs()[expert_input_idx]; auto expert_input_tensor = io.expert_input; if (!expert_input_tensor) { @@ -331,176 +323,255 @@ void MoEExecutor::run_expert_batch(size_t idx, size_t real_idx, const std::vecto }); } -void MoEExecutor::run_expert_iterative(size_t idx, size_t real_idx, const std::vector& selected_experts) { - LOG_DEBUG("\n[EXPERT_ITERATIVE] Processing multiple tokens by iterating through experts"); +void MoEExecutor::run_expert_iterative(size_t idx) { + LOG_DEBUG("\n[EXPERT_ITERATIVE] Processing multiple tokens with async inference and overlapping NPU execution"); - const auto input_token_count = m_config.input_token_count; const auto& io = m_moe_io[idx]; - // Clear output buffer before accumulating expert outputs - if (!m_resources.expert_output_accumulator) { + // Precondition checks + if (!m_resources.expert_output_accumulator) OPENVINO_THROW("MoE: Expert output accumulator is null"); - } + if (!io.expert_input) + OPENVINO_THROW("MoE: Expert input tensor is not set — prologue must bind it before run()"); + if (!m_config.router_scores.compiled.has_value()) + OPENVINO_THROW("MoE: Router scores index not available"); + if (!m_config.expert_input.compiled.has_value()) + OPENVINO_THROW("MoE: Expert input parameter index not available"); + if (m_resources.sorted_chunk_sizes.empty()) + OPENVINO_THROW("MoE: Sorted chunk sizes cannot be empty"); + + // Clear output accumulator before accumulating expert outputs std::memset(m_resources.expert_output_accumulator->data(), 0, m_resources.expert_output_accumulator->get_byte_size()); - // Get tensor references (constant across all experts) - if (!m_config.router_scores_idx.has_value()) { - OPENVINO_THROW("MoE: Router scores index not available"); - } - if (!m_config.expert_input_param_idx.has_value()) { - OPENVINO_THROW("MoE: Expert input parameter index not available"); - } - - auto router_source = io.router_scores; auto expert_input_source = io.expert_input; - // Calculate output embedding dimension from any compiled model (all have same output shape) - auto any_compiled_model = m_config.compiled_models.begin()->second; - auto output_shape = any_compiled_model->outputs()[0].get_shape(); - size_t embed_dim = (output_shape.size() == 4) ? output_shape[3] : output_shape[1]; - - // Process each expert sequentially - for (size_t expert_id : selected_experts) { - LOG_DEBUG("\n Processing Expert[" << expert_id << "]..."); - - // Get tokens assigned to this expert - const auto& tokens_for_expert = m_expert_to_tokens.at(expert_id); - const size_t total_tokens = tokens_for_expert.size(); - - LOG_DEBUG(" Expert[" << expert_id << "] processing " << total_tokens << " tokens"); - - // Precompute expert slot for each token - // This eliminates map lookup + linear search in scatter_expert_outputs hot loop - std::vector expert_slots_for_tokens(total_tokens); - for (size_t i = 0; i < total_tokens; ++i) { - size_t token_id = tokens_for_expert[i]; - const auto& expert_ids = m_token_to_experts.at(token_id); - auto it = std::find(expert_ids.begin(), expert_ids.end(), expert_id); - if (it == expert_ids.end()) { - OPENVINO_THROW("MoE: Token should have this expert"); - } - expert_slots_for_tokens[i] = std::distance(expert_ids.begin(), it); + // Output embedding dimension + const auto output_shape = m_config.compiled_models.begin()->second->outputs()[0].get_shape(); + const size_t embed_dim = (output_shape.size() == 4) ? output_shape[3] : output_shape[1]; + + // num_tokens and num_experts come from config, validated once during prepare(). + // The router tensor's token dimension is guaranteed to match input_token_count because + // both are derived from the same compiled model structure at prepare() time. + const size_t num_tokens = m_config.input_token_count; + const size_t num_experts = m_config.num_experts; + + // Chunk-size selector + auto select_chunk = [&](size_t remaining) -> size_t { + const size_t smallest = m_resources.sorted_chunk_sizes.back(); + const size_t largest = m_resources.sorted_chunk_sizes.front(); + if (remaining <= smallest) + return smallest; + if (remaining >= largest) + return largest; + for (auto it = m_resources.sorted_chunk_sizes.rbegin(); it != m_resources.sorted_chunk_sizes.rend(); ++it) { + if (*it >= remaining) + return *it; } - - // Process tokens in dynamic chunks based on available compiled models - // Priority: use largest possible chunk_size (256 > 128 > 64 > 32) - size_t processed_tokens = 0; - size_t last_chunk_size = 0; // Track last used chunk_size to detect changes - // Note: When switching to a new expert, last_chunk_size resets to 0, - // ensuring weights are unpacked for the first chunk of each expert - while (processed_tokens < total_tokens) { - size_t remaining_tokens = total_tokens - processed_tokens; - - // Chunk selection strategy: - // - remaining_tokens <= smallest chunk → use smallest chunk - // - remaining_tokens >= largest chunk → use largest chunk - // - otherwise → use smallest chunk that is >= remaining_tokens - // Pre-sorted in descending order: [256, 128, 64, 32, 16] - if (m_resources.sorted_chunk_sizes.empty()) { - OPENVINO_THROW("MoE: Sorted chunk sizes cannot be empty"); - } - - size_t selected_chunk_size; - size_t smallest_chunk = m_resources.sorted_chunk_sizes.back(); - size_t largest_chunk = m_resources.sorted_chunk_sizes.front(); - - if (remaining_tokens <= smallest_chunk) { - // Use smallest chunk - selected_chunk_size = smallest_chunk; - } else if (remaining_tokens >= largest_chunk) { - // Use largest chunk - selected_chunk_size = largest_chunk; - } else { - // Find smallest chunk >= remaining_tokens - // Since sorted descending, iterate from end to find first >= remaining_tokens - selected_chunk_size = smallest_chunk; // Default to smallest - for (auto it = m_resources.sorted_chunk_sizes.rbegin(); it != m_resources.sorted_chunk_sizes.rend(); - ++it) { - if (*it >= remaining_tokens) { - selected_chunk_size = *it; - break; + return smallest; + }; + + // Double-buffer request helpers + size_t global_slot = 0; + auto get_req = [&](size_t cs, size_t slot) -> RqPtr& { + return m_resources.chunk_infer_requests.at(cs)[slot & 1]; + }; + // Tracks which expert_id is currently loaded into each (chunk_size, slot) request. + // Key = cs * 2 + (slot & 1): unique because all chunk_sizes are distinct integers, + // so cs1 * 2 + b1 == cs2 * 2 + b2 only when cs1 == cs2 and b1 == b2. + std::map req_expert_state; + auto do_unpack = [&](size_t cs, size_t slot, RqPtr& req, size_t expert_id) { + const size_t key = cs * 2 + (slot & 1); + auto it = req_expert_state.find(key); + if (it == req_expert_state.end() || it->second != expert_id) { + m_profile->iterative["Unpack Closure"].record([&]() { + unpack_single_expert_closure(idx, req, expert_id); + }); + req_expert_state[key] = expert_id; + } + }; + + // Expert data ring buffer: + // 2 slots alternate per non-empty expert. At most one slot is "in-flight" for + // scatter at any time; the other slot is being filled for the next expert. + // Safety: expert[k+2] reuses the slot that expert[k] used, and by then expert[k]'s + // last work item has already been drained (pipeline depth = 1). + struct ExpertData { + std::vector tokens; + std::vector slots; + }; + std::array expert_ring; + size_t expert_ring_idx = 0; // incremented per non-empty expert + + // In-flight item tracking + struct InflightItem { + size_t cs; + size_t req_slot; + size_t chunk_start; + size_t chunk_tokens; + size_t ring_idx; // expert_ring slot index at launch time + }; + std::optional inflight; + + // Drain the in-flight item referenced by `inflight` (wait + scatter). + // Called both inside the pipeline loop (to drain the previous item while NPU + // runs the current one) and once after the loop to drain the final item. + auto do_drain = [&]() { + NPUW_ASSERT(inflight.has_value() && "do_drain called with no in-flight item"); + auto& req = get_req(inflight->cs, inflight->req_slot); + m_profile->iterative["NPU Wait"].record([&]() { + req->wait(); + }); + const auto& data = expert_ring[inflight->ring_idx & 1]; + const auto cm = m_config.compiled_models.at(inflight->cs); + auto output = req->get_tensor(cm->outputs()[0]); + m_profile->iterative["Scatter Output"].record([&]() { + ov::npuw::moe::scatter_expert_outputs(output, + m_resources.expert_output_accumulator, + data.tokens, + inflight->chunk_start, + inflight->chunk_tokens, + embed_dim, + num_tokens, + data.slots); + }); + }; + + // Per-token counter: how many earlier experts have already selected this token. + // Gives each token's expert-slot index in O(1) without a full two-pass scan. + std::vector token_slot_count(num_tokens, 0); + + // Parse-ahead buffer: pre-scan the NEXT expert's router row after drain(k-1) but + // while NPU k is still executing. This hides the O(num_tokens) threshold scan + // inside the NPU overlap window instead of paying for it on the critical path + // before item k+1's dispatch. + // + // The buffer stores only token IDs that passed the threshold (v > 1e-6). + // Slot assignment (O(selected) not O(num_tokens)) is still done at the + // top of the next expert iteration. + // + // Timeline with parse-ahead: + // CPU: [Unpack(k)+Gather(k)] → start(k) → drain(k-1) → [Parse(k+1)] → [Unpack(k+1)+Gather(k+1)] → start(k+1) + // NPU: [=================NPU(k)==================] [==NPU(k+1)==] + // + struct ParseAheadBuffer { + std::vector tokens; // pre-filtered token IDs for next expert + size_t expert_id = SIZE_MAX; // which expert was pre-scanned (sentinel = none) + }; + ParseAheadBuffer parse_ahead; + + // Per-expert loop: three phases — Parse, Dispatch, Prefetch. + auto stream_and_run = [&](auto* data) { + for (size_t expert_id = 0; expert_id < num_experts; ++expert_id) { + // -- Parse: determine which tokens this expert processes -- + const size_t ring_slot = expert_ring_idx & 1; + auto& cur = expert_ring[ring_slot]; + cur.tokens.clear(); + cur.slots.clear(); + + const auto* row = data + expert_id * num_tokens; + // Shared by both the parse-ahead fast path and the full threshold scan below. + auto fill_token = [&](size_t token_id) { + cur.tokens.push_back(token_id); + cur.slots.push_back(token_slot_count[token_id]++); + }; + m_profile->iterative["Parse Router Row"].record([&]() { + if (parse_ahead.expert_id == expert_id) { + // Fast path: token IDs already filtered; only slot updates remain. + for (size_t token_id : parse_ahead.tokens) { + fill_token(token_id); + } + parse_ahead.expert_id = SIZE_MAX; // consumed + } else { + // Full O(num_tokens) threshold scan (first expert, or parse-ahead missed). + for (size_t token_id = 0; token_id < num_tokens; ++token_id) { + if (is_nonzero(row[token_id])) { + fill_token(token_id); + } } } - } + }); - // Actual tokens to process in this iteration - size_t current_chunk_size = std::min(selected_chunk_size, remaining_tokens); + if (cur.tokens.empty()) { + continue; // expert not selected — do not advance ring_idx + } - LOG_DEBUG(" Processing tokens [" << processed_tokens << ", " << (processed_tokens + current_chunk_size) - << ") with chunk_size=" << selected_chunk_size); + // -- Dispatch: slice tokens into chunks and pipeline NPU execution -- + size_t processed = 0; + while (processed < cur.tokens.size()) { + const size_t cs = select_chunk(cur.tokens.size() - processed); + const size_t actual = std::min(cs, cur.tokens.size() - processed); + const size_t s = global_slot & 1; + auto& req = get_req(cs, s); + + do_unpack(cs, s, req, expert_id); + { + const auto cm = m_config.compiled_models.at(cs); + auto router_dest = req->get_tensor(cm->inputs()[m_config.router_scores.compiled.value()]); + auto input_dest = req->get_tensor(cm->inputs()[m_config.expert_input.compiled.value()]); + m_profile->iterative["Gather Router Scores"].record([&]() { + ov::npuw::moe::gather_router_scores(io.router_scores, + router_dest, + expert_id, + cur.tokens, + processed, + actual); + }); + m_profile->iterative["Gather Expert Input"].record([&]() { + ov::npuw::moe::gather_expert_inputs(expert_input_source, + input_dest, + cur.tokens, + processed, + actual); + }); + } - // Get selected infer request from MoEResources - auto infer_request_it = m_resources.chunk_infer_requests.find(selected_chunk_size); - if (infer_request_it == m_resources.chunk_infer_requests.end()) { - OPENVINO_THROW("MoE: Infer request for chunk_size=", selected_chunk_size, " not found"); - } - auto selected_infer_request = infer_request_it->second; - - // Get input/output ports for selected infer request - auto selected_compiled_model = m_config.compiled_models.at(selected_chunk_size); - const auto& selected_router_iport = selected_compiled_model->inputs()[m_config.router_scores_idx.value()]; - const auto& selected_expert_input_iport = - selected_compiled_model->inputs()[m_config.expert_input_param_idx.value()]; - const auto& selected_oport = selected_compiled_model->outputs()[0]; - - auto selected_router_dest = selected_infer_request->get_tensor(selected_router_iport); - auto selected_expert_input_dest = selected_infer_request->get_tensor(selected_expert_input_iport); - auto selected_expert_output = selected_infer_request->get_tensor(selected_oport); - - // Step 1: Unpack expert weights when chunk_size changes (different infer request) - // This ensures each infer request has correct weights loaded - // Important: last_chunk_size is reset to 0 at the start of each expert loop, - // so the first chunk of a new expert will always trigger unpacking (0 != selected_chunk_size) - if (selected_chunk_size != last_chunk_size) { - m_profile->iterative["Unpack Closure"].record([&]() { - unpack_single_expert_closure(idx, selected_infer_request, expert_id); + // Start NPU first, then drain previous — this creates the CPU/NPU overlap. + m_profile->iterative["NPU Start"].record([&]() { + req->start_async(); }); - last_chunk_size = selected_chunk_size; - } + if (inflight) { + do_drain(); + } - // Step 2: Gather router scores for this chunk - m_profile->iterative["Gather Router Scores"].record([&]() { - ov::npuw::moe::gather_router_scores(router_source, - selected_router_dest, - expert_id, - tokens_for_expert, - processed_tokens, - current_chunk_size); - }); + inflight = InflightItem{cs, s, processed, actual, ring_slot}; + ++global_slot; + processed += actual; + } - // Step 3: Gather expert inputs for this chunk - m_profile->iterative["Gather Expert Input"].record([&]() { - ov::npuw::moe::gather_expert_inputs(expert_input_source, - selected_expert_input_dest, - tokens_for_expert, - processed_tokens, - current_chunk_size); - }); + // -- Prefetch: scan next expert's row while the last NPU chunk runs -- + if ((expert_id + 1) < num_experts) { + const size_t next_id = expert_id + 1; + const auto* next_row = data + next_id * num_tokens; + parse_ahead.tokens.clear(); + parse_ahead.expert_id = next_id; + for (size_t token_id = 0; token_id < num_tokens; ++token_id) { + if (is_nonzero(next_row[token_id])) { + parse_ahead.tokens.push_back(token_id); + } + } + } - // Step 4: Execute expert inference - m_profile->iterative["Expert Inference"].record([&]() { - selected_infer_request->infer(); - }); + ++expert_ring_idx; + } + }; - // Step 5: Scatter expert outputs back to global buffer - m_profile->iterative["Scatter Output"].record([&]() { - ov::npuw::moe::scatter_expert_outputs(selected_expert_output, - m_resources.expert_output_accumulator, - tokens_for_expert, - processed_tokens, - current_chunk_size, - embed_dim, - input_token_count, - expert_slots_for_tokens); - }); + const auto elem_type = io.router_scores->get_element_type(); + if (elem_type == ov::element::f32) { + stream_and_run(io.router_scores->data()); + } else if (elem_type == ov::element::f16) { + stream_and_run(io.router_scores->data()); + } else { + OPENVINO_THROW("MoE: Unsupported router element type for iterative inference"); + } - // Move to next chunk - processed_tokens += current_chunk_size; - } - LOG_DEBUG(" Expert[" << expert_id << "] completed"); + if (!inflight) { + OPENVINO_THROW("MoE: No experts selected by router"); } + + // Drain the last in-flight item + do_drain(); } void MoEExecutor::set_router_scores(size_t idx, @@ -511,11 +582,11 @@ void MoEExecutor::set_router_scores(size_t idx, const auto& io = m_moe_io[idx]; // Validate router scores index is provided - if (!m_config.router_scores_idx.has_value()) { + if (!m_config.router_scores.original.has_value()) { OPENVINO_THROW("MoE: Router input parameter index not specified for expert model"); } - const auto original_router_idx = m_config.router_scores_idx.value(); + const auto original_router_idx = m_config.router_scores.original.value(); const auto& param_mapping = m_config.param_mapping; // Find unrolled router score parameters @@ -580,8 +651,9 @@ void MoEExecutor::unpack_single_expert_closure(std::size_t idx, RqPtr request, s const auto& func_desc = *static_cast(m_accessor.get_submodel_desc(real_idx)); - NPUW_ASSERT(func_desc.moe_experts.has_value()); - const auto num_experts = func_desc.moe_experts->num_experts; + const auto* moe_experts = get_compiled_experts(func_desc.pipeline.context); + NPUW_ASSERT(moe_experts != nullptr); + const auto num_experts = moe_experts->num_experts; auto& desc_closure = comp_model_desc.closure.get().closure; @@ -675,11 +747,11 @@ void MoEExecutor::unpack_multiple_experts_closure(std::size_t idx, const auto& func_desc = *static_cast(m_accessor.get_submodel_desc(real_idx)); - NPUW_ASSERT(func_desc.moe_experts.has_value()); - const auto& moe_experts = func_desc.moe_experts.value(); - const auto num_experts = moe_experts.num_experts; // Total experts in model (e.g., 32) - const size_t K = expert_ids.size(); // Active experts (e.g., 4) - const auto& param_mapping = moe_experts._param_mapping; // original_idx -> [unrolled_indices] + const auto* moe_experts = get_compiled_experts(func_desc.pipeline.context); + NPUW_ASSERT(moe_experts != nullptr); + const auto num_experts = moe_experts->num_experts; // Total experts in model (e.g., 32) + const size_t K = expert_ids.size(); // Active experts (e.g., 4) + const auto& param_mapping = moe_experts->_param_mapping; // original_idx -> [unrolled_indices] auto& desc_closure = comp_model_desc.closure.get().closure; const auto& compiled_inputs = func_desc.compiled_model->inputs(); diff --git a/src/plugins/intel_npu/src/plugin/npuw/moe/moe_executor.hpp b/src/plugins/intel_npu/src/plugin/npuw/moe/moe_executor.hpp index 4f690e29fa49..2d50759591b2 100644 --- a/src/plugins/intel_npu/src/plugin/npuw/moe/moe_executor.hpp +++ b/src/plugins/intel_npu/src/plugin/npuw/moe/moe_executor.hpp @@ -14,6 +14,7 @@ #include "../util.hpp" // For TensorPtr #include "moe_config.hpp" #include "moe_resources.hpp" +#include "moe_subgraph.hpp" #include "moe_types.hpp" // For MoEIO #include "openvino/runtime/so_ptr.hpp" @@ -27,12 +28,9 @@ class CompiledModel; namespace ov { namespace npuw { - -// Forward declare CompiledModelDesc to avoid circular dependency -// Will be included in implementation file namespace moe { class MoEExecutor; -} +} // namespace moe // Import TensorPtr from util namespace for use in this header using ov::npuw::util::TensorPtr; @@ -257,16 +255,17 @@ class MoEExecutor { void run_expert_batch(size_t idx, size_t real_idx, const std::vector& selected_experts); /** - * @brief Execute expert iterative mode inference + * @brief Execute expert iterative mode inference. * - * Processing mode: Iterate through experts, each processes multiple tokens - * Uses dynamic chunk sizing and accumulates outputs. + * Processing mode: Iterate through experts, each processes multiple tokens. + * CPU gather and NPU inference are overlapped via double-buffering: + * while NPU executes chunk k, CPU prepares (unpack + gather) chunk k+1. + * Router row parsing for expert k+1 is also interleaved inside the NPU + * execution window of expert k. * * @param idx Function call index - * @param real_idx Submodel index - * @param selected_experts List of selected expert IDs */ - void run_expert_iterative(size_t idx, size_t real_idx, const std::vector& selected_experts); + void run_expert_iterative(size_t idx); /** * @brief Execute batch experts inference (deprecated) @@ -277,15 +276,6 @@ class MoEExecutor { run_expert_batch(idx, real_idx, selected_experts); } - /** - * @brief Execute iterative experts inference (deprecated) - * @deprecated Use run_expert_iterative() instead - */ - [[deprecated("Use run_expert_iterative() instead")]] - void run_iterative_experts(size_t idx, size_t real_idx, const std::vector& selected_experts) { - run_expert_iterative(idx, real_idx, selected_experts); - } - // === Helper functions === /** diff --git a/src/plugins/intel_npu/src/plugin/npuw/moe/moe_infer_utils.cpp b/src/plugins/intel_npu/src/plugin/npuw/moe/moe_infer_utils.cpp index 463bd377005b..789cdc253a67 100644 --- a/src/plugins/intel_npu/src/plugin/npuw/moe/moe_infer_utils.cpp +++ b/src/plugins/intel_npu/src/plugin/npuw/moe/moe_infer_utils.cpp @@ -65,6 +65,23 @@ ov::Tensor slice_expert_weight(const ov::Tensor& batched_weight, size_t expert_i return view_tensor; } +namespace { +// Returns the token-count dimension from a 4-D router output shape. +// Two layout variants exist depending on the opset version used during model export: +// Layout A: [num_experts, 1, token_num, 1] → returns shape[2] +// Layout B: [num_experts, token_num, 1, 1] → returns shape[1] +// ASSERTs if neither layout is recognised. +static size_t get_router_token_count(const ov::Shape& router_shape) { + NPUW_ASSERT(router_shape.size() == 4); + if (router_shape[1] == 1 && router_shape[3] == 1) + return router_shape[2]; // Layout A + if (router_shape[2] == 1 && router_shape[3] == 1) + return router_shape[1]; // Layout B + NPUW_ASSERT(false && "Unexpected router output shape - cannot determine token dimension!"); + return 0; // unreachable, suppress warning +} +} // namespace + std::vector parse_selected_experts_from_router(const ov::SoPtr& router_output, size_t num_experts, std::map>& token_to_experts, @@ -77,26 +94,20 @@ std::vector parse_selected_experts_from_router(const ov::SoPtrget_shape(); - if (shape.size() != 4 || shape[0] != num_experts || shape[1] != 1 || shape[3] != 1) { + if (shape.size() != 4 || shape[0] != num_experts) { NPUW_ASSERT(false && "Unexpected router output shape!"); } - size_t num_tokens = shape[2]; // token_num from shape + size_t num_tokens = get_router_token_count(shape); // Parse which expert each token selects based on non-zero weights auto parse_experts = [&](auto* data) { - // For each token, find which experts have non-zero weights - for (size_t token_id = 0; token_id < num_tokens; ++token_id) { - for (size_t expert_id = 0; expert_id < num_experts; ++expert_id) { - // Index calculation for shape [num_experts, 1, token_num, 1] - // data[expert_id, 0, token_id, 0] - size_t idx = expert_id * num_tokens + token_id; - - float value = std::abs(static_cast(data[idx])); - if (value > 1e-6f) { - // This token selected this expert + // Iterate expert-major: each expert's row is contiguous in memory. + for (size_t expert_id = 0; expert_id < num_experts; ++expert_id) { + const auto* row = data + expert_id * num_tokens; + for (size_t token_id = 0; token_id < num_tokens; ++token_id) { + if (is_nonzero(row[token_id])) { token_to_experts[token_id].push_back(expert_id); expert_to_tokens[expert_id].push_back(token_id); } @@ -152,31 +163,30 @@ void gather_router_scores(const ov::SoPtr& router_source, const std::vector& token_ids, size_t chunk_start, size_t chunk_size) { - auto router_source_shape = router_source->get_shape(); + const auto router_source_shape = router_source->get_shape(); - // Calculate expert offset in source tensor size_t expert_offset; if (router_source_shape.size() == 4) { - expert_offset = expert_id * router_source_shape[2]; // [num_experts, 1, token_num, 1] + expert_offset = expert_id * get_router_token_count(router_source_shape); } else if (router_source_shape.size() == 2) { expert_offset = expert_id * router_source_shape[1]; // [num_experts, token_num] } else { NPUW_ASSERT(false && "Unexpected router source shape"); + expert_offset = 0; // unreachable, suppress uninitialized warning } - // Gather router scores for chunk tokens - if (router_source->get_element_type() == ov::element::f16) { - const auto* src_base = router_source->data() + expert_offset; - auto* dst_base = router_dest->data(); + auto gather = [&](const auto* src_base, auto* dst_base) { + const size_t* ids = token_ids.data() + chunk_start; for (size_t i = 0; i < chunk_size; ++i) { - dst_base[i] = src_base[token_ids[chunk_start + i]]; - } - } else if (router_source->get_element_type() == ov::element::f32) { - const auto* src_base = router_source->data() + expert_offset; - auto* dst_base = router_dest->data(); - for (size_t i = 0; i < chunk_size; ++i) { - dst_base[i] = src_base[token_ids[chunk_start + i]]; + dst_base[i] = src_base[ids[i]]; } + }; + + const auto elem_type = router_source->get_element_type(); + if (elem_type == ov::element::f16) { + gather(router_source->data() + expert_offset, router_dest->data()); + } else if (elem_type == ov::element::f32) { + gather(router_source->data() + expert_offset, router_dest->data()); } else { NPUW_ASSERT(false && "Unsupported router element type for gathering"); } @@ -187,40 +197,24 @@ void gather_expert_inputs(const ov::SoPtr& input_source, const std::vector& token_ids, size_t chunk_start, size_t chunk_size) { - auto input_shape = input_source->get_shape(); - - // Determine dimensions - size_t hidden_dim; - size_t token_stride; - if (input_shape.size() == 2) { - hidden_dim = input_shape[1]; - token_stride = hidden_dim; - } else if (input_shape.size() == 4) { - hidden_dim = input_shape[3]; - token_stride = hidden_dim; - } else { - NPUW_ASSERT(false && "Unexpected expert input tensor shape"); - } - - // Gather input embeddings for chunk tokens - if (input_source->get_element_type() == ov::element::f16) { - const auto* src_base = input_source->data(); - auto* dst_base = input_dest->data(); - for (size_t i = 0; i < chunk_size; ++i) { - size_t token_id = token_ids[chunk_start + i]; - const auto* src_token = src_base + token_id * token_stride; - auto* dst_token = dst_base + i * hidden_dim; - std::memcpy(dst_token, src_token, hidden_dim * sizeof(ov::float16)); - } - } else if (input_source->get_element_type() == ov::element::f32) { - const auto* src_base = input_source->data(); - auto* dst_base = input_dest->data(); - for (size_t i = 0; i < chunk_size; ++i) { - size_t token_id = token_ids[chunk_start + i]; - const auto* src_token = src_base + token_id * token_stride; - auto* dst_token = dst_base + i * hidden_dim; - std::memcpy(dst_token, src_token, hidden_dim * sizeof(float)); + const auto input_shape = input_source->get_shape(); + NPUW_ASSERT((input_shape.size() == 2 || input_shape.size() == 4) && "Unexpected expert input tensor shape"); + // hidden_dim is always the last dimension for both supported layouts. + const size_t hidden_dim = input_shape.back(); + + auto gather = [&](const auto* src_base, auto* dst_base) { + const size_t elem_bytes = hidden_dim * sizeof(*src_base); + const size_t* ids = token_ids.data() + chunk_start; + for (size_t i = 0; i < chunk_size; ++i, dst_base += hidden_dim) { + std::memcpy(dst_base, src_base + ids[i] * hidden_dim, elem_bytes); } + }; + + const auto elem_type = input_source->get_element_type(); + if (elem_type == ov::element::f16) { + gather(input_source->data(), input_dest->data()); + } else if (elem_type == ov::element::f32) { + gather(input_source->data(), input_dest->data()); } else { NPUW_ASSERT(false && "Unsupported expert input element type for gathering"); } @@ -234,28 +228,26 @@ void scatter_expert_outputs(const ov::SoPtr& expert_output, size_t embed_dim, size_t input_token_count, const std::vector& expert_slots_for_tokens) { - auto elem_type = global_output_buffer->get_element_type(); - - for (size_t i = 0; i < chunk_size; ++i) { - size_t original_token_id = token_ids[chunk_start + i]; - size_t expert_slot = expert_slots_for_tokens[chunk_start + i]; - - // Calculate offsets - size_t src_offset = i * embed_dim; - size_t dst_offset = expert_slot * input_token_count * embed_dim + original_token_id * embed_dim; - - // Scatter output to global buffer - if (elem_type == ov::element::f32) { - const float* src = expert_output->data() + src_offset; - float* dst = global_output_buffer->data() + dst_offset; - std::memcpy(dst, src, embed_dim * sizeof(float)); - } else if (elem_type == ov::element::f16) { - const ov::float16* src = expert_output->data() + src_offset; - ov::float16* dst = global_output_buffer->data() + dst_offset; - std::memcpy(dst, src, embed_dim * sizeof(ov::float16)); - } else { - OPENVINO_THROW("MoE: Unsupported element type for chunk output relayout: ", elem_type); + // Hoist type dispatch and pointer acquisition outside the loop. + // slot_stride = number of elements between consecutive expert slots in the output buffer. + const size_t slot_stride = input_token_count * embed_dim; + + auto scatter = [&](const auto* src_base, auto* dst_base) { + const size_t elem_bytes = embed_dim * sizeof(*src_base); + const size_t* ids = token_ids.data() + chunk_start; + const size_t* slots = expert_slots_for_tokens.data() + chunk_start; + for (size_t i = 0; i < chunk_size; ++i) { + std::memcpy(dst_base + slots[i] * slot_stride + ids[i] * embed_dim, src_base + i * embed_dim, elem_bytes); } + }; + + const auto elem_type = global_output_buffer->get_element_type(); + if (elem_type == ov::element::f32) { + scatter(expert_output->data(), global_output_buffer->data()); + } else if (elem_type == ov::element::f16) { + scatter(expert_output->data(), global_output_buffer->data()); + } else { + OPENVINO_THROW("MoE: Unsupported element type for chunk output relayout: ", elem_type); } } diff --git a/src/plugins/intel_npu/src/plugin/npuw/moe/moe_infer_utils.hpp b/src/plugins/intel_npu/src/plugin/npuw/moe/moe_infer_utils.hpp index f78c0cab8a77..01c4bfb7d1bd 100644 --- a/src/plugins/intel_npu/src/plugin/npuw/moe/moe_infer_utils.hpp +++ b/src/plugins/intel_npu/src/plugin/npuw/moe/moe_infer_utils.hpp @@ -4,6 +4,7 @@ #pragma once +#include #include #include @@ -29,6 +30,11 @@ struct MoEProfile { MoEProfile(); }; +template +inline bool is_nonzero(T v) { + return std::abs(static_cast(v)) > 1e-6f; +} + /** * @brief Slice a single expert's weight from batched weight tensor. * diff --git a/src/plugins/intel_npu/src/plugin/npuw/moe/moe_resources.cpp b/src/plugins/intel_npu/src/plugin/npuw/moe/moe_resources.cpp index 4b1f140efe28..0947691bfb92 100644 --- a/src/plugins/intel_npu/src/plugin/npuw/moe/moe_resources.cpp +++ b/src/plugins/intel_npu/src/plugin/npuw/moe/moe_resources.cpp @@ -42,10 +42,12 @@ void MoEResources::initialize_expert_iterative_mode( } try { - auto infer_request = compiled_model->create_infer_request(); - chunk_infer_requests[chunk_size] = std::move(infer_request); + // Create 2 requests per chunk_size for double-buffering: + // slot 0 and slot 1 alternate so Unpack+Gather can overlap with NPU inference. + chunk_infer_requests[chunk_size][0] = compiled_model->create_infer_request(); + chunk_infer_requests[chunk_size][1] = compiled_model->create_infer_request(); sorted_chunk_sizes.push_back(chunk_size); - LOG_DEBUG(" Created chunk infer request for chunk_size=" << chunk_size); + LOG_DEBUG(" Created 2 chunk infer requests (double-buffer) for chunk_size=" << chunk_size); } catch (const std::exception& ex) { OPENVINO_THROW("MoE chunk infer request creation failed for chunk_size=", chunk_size, ": ", ex.what()); } @@ -60,10 +62,31 @@ void MoEResources::initialize_expert_iterative_mode( const size_t num_tokens = config.input_token_count; const size_t embed_dim = config.expert_hidden_dim; - ov::Shape buffer_shape = {active_experts, 1, num_tokens, embed_dim}; + // Derive accumulator shape from the expert model's output shape template. + // Two layout variants exist depending on the opset version used during model export: + // Layout A: [1, 1, chunk_size, embed_dim] → accumulator [active_experts, 1, num_tokens, embed_dim] + // Layout B: [1, chunk_size, 1, embed_dim] → accumulator [active_experts, num_tokens, 1, embed_dim] + // + // We use the first chunk size's compiled model output shape as the template: + // replace dim[0] with active_experts, replace any middle dim (1..n-2) equal to + // chunk_size with num_tokens, and always preserve the last dim (embed_dim) unchanged. + const size_t first_chunk_size = config.compiled_models.begin()->first; + auto first_model = config.compiled_models.begin()->second; + const auto output_shape = first_model->outputs()[0].get_shape(); + + ov::Shape buffer_shape(output_shape.size()); + buffer_shape[0] = active_experts; + for (size_t i = 1; i < output_shape.size() - 1; ++i) { + if (output_shape[i] == first_chunk_size) { + buffer_shape[i] = num_tokens; + } else { + buffer_shape[i] = output_shape[i]; // copy singleton (1) as-is + } + } + buffer_shape.back() = output_shape.back(); // always preserve embed_dim (last dim) + NPUW_ASSERT(buffer_shape.back() == embed_dim && "Accumulator last dim must equal expert_hidden_dim"); // Infer element type from first compiled model output - auto first_model = config.compiled_models.begin()->second; auto output_element_type = first_model->outputs()[0].get_element_type(); expert_output_accumulator = allocator(output_element_type, buffer_shape, device); diff --git a/src/plugins/intel_npu/src/plugin/npuw/moe/moe_resources.hpp b/src/plugins/intel_npu/src/plugin/npuw/moe/moe_resources.hpp index 47cd91e1348b..085e3091bb05 100644 --- a/src/plugins/intel_npu/src/plugin/npuw/moe/moe_resources.hpp +++ b/src/plugins/intel_npu/src/plugin/npuw/moe/moe_resources.hpp @@ -4,7 +4,9 @@ #pragma once +#include #include +#include #include #include @@ -47,10 +49,11 @@ struct MoEResources { // Used to select optimal chunk size for token batches std::vector sorted_chunk_sizes; - // Infer requests for different chunk sizes - // Map: chunk_size -> infer_request - // Reused across experts by unpacking different weights - std::map> chunk_infer_requests; + // Infer requests for different chunk sizes, double-buffered for pipeline overlap. + // Map: chunk_size -> [slot0, slot1] + // Pipeline alternates between slot 0 and slot 1 so that Unpack+Gather for item i+1 + // can run on the CPU while NPU is executing item i. + std::map, 2>> chunk_infer_requests; // Output accumulation buffer // Shape: [num_active_experts, 1, input_token_count, expert_hidden_dim] diff --git a/src/plugins/intel_npu/src/plugin/npuw/moe/moe_subgraph.cpp b/src/plugins/intel_npu/src/plugin/npuw/moe/moe_subgraph.cpp new file mode 100644 index 000000000000..4b80df840c7f --- /dev/null +++ b/src/plugins/intel_npu/src/plugin/npuw/moe/moe_subgraph.cpp @@ -0,0 +1,381 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "moe_subgraph.hpp" + +#include + +#include "../logging.hpp" +#include "../moe_transformations/moe_transformation.hpp" +#include "../partitioning/partitioning.hpp" +#include "../partitioning/patterns/moe.hpp" +#include "../serialization.hpp" +#include "openvino/core/except.hpp" + +namespace ov { +namespace npuw { +namespace moe { +namespace { + +const char* behavior_name(const BehaviorRole role) { + switch (role) { + case BehaviorRole::EXPERTS: + return ov::npuw::patterns::moe::GPTOSSExpert::pattern_name(); + case BehaviorRole::DOWNSTREAM: + return "MoEDownstream"; + } + OPENVINO_THROW("Unsupported MoE behavior role"); +} + +ov::npuw::v1::subgraphs::RuntimeBehaviorFactory make_runtime_factory() { + return [](const ov::npuw::v1::subgraphs::Context& ctx) -> ov::npuw::v1::subgraphs::ISubgraphBehavior::Ptr { + class MoEBehavior final : public ov::npuw::v1::subgraphs::ISubgraphBehavior { + public: + explicit MoEBehavior(const BehaviorRole role) : m_role(role) {} + + void prologue(ov::npuw::v1::subgraphs::InferContext& ctx) override { + if (ctx.opaque_prologue) { + ctx.opaque_prologue(); + } + } + + void run(ov::npuw::v1::subgraphs::InferContext& ctx) override { + if (m_role != BehaviorRole::EXPERTS) { + ctx.legacy_infer(); + return; + } + + OPENVINO_ASSERT(static_cast(ctx.opaque_run), + "Expected opaque run callback for MoE subgraph behavior"); + ctx.opaque_run(); + } + + private: + BehaviorRole m_role = BehaviorRole::EXPERTS; + }; + + return std::make_unique(ctx.get()); + }; +} + +} // namespace + +void attach_runtime_behavior(v1::subgraphs::CompiledPipeline& compiled_pipeline, + v1::subgraphs::Context& compiled_context, + const BehaviorRole role, + const bool handles_function_prologue) { + compiled_context.put(role); + compiled_pipeline.registration.group = ov::npuw::patterns::moe::GPTOSSExpert::group_name(); + compiled_pipeline.registration.name = behavior_name(role); + v1::subgraphs::RuntimeBehaviorSpec spec; + spec.registration = compiled_pipeline.registration; + spec.context = compiled_context; + spec.factory = make_runtime_factory(); + spec.handles_function_prologue = handles_function_prologue; + compiled_pipeline.runtime_behavior = std::move(spec); +} + +namespace { + +template +T* get_compiled_state(ov::npuw::v1::subgraphs::Context& context) { + auto* state = context.get_if>(); + return state == nullptr ? nullptr : state->get(); +} + +template +const T* get_compiled_state(const ov::npuw::v1::subgraphs::Context& context) { + auto* state = context.get_if>(); + return state == nullptr ? nullptr : state->get(); +} + +std::shared_ptr find_router_model(ov::npuw::v1::subgraphs::Context& ctx) { + auto* callbacks = ctx.get_if(); + if (callbacks == nullptr || !callbacks->find_tagged_model) { + return nullptr; + } + return callbacks->find_tagged_model(ov::npuw::patterns::moe::ROUTER_TAG); +} + +void transform_experts(ov::npuw::Function& function, + ov::npuw::v1::subgraphs::Context& ctx, + const std::size_t moe_chunk_size) { + auto router_model = find_router_model(ctx); + if (!router_model) { + return; + } + auto experts = ov::npuw::function::MoEExperts::from(function._model, router_model, moe_chunk_size); + if (experts.has_value()) { + ctx.put(std::move(experts.value())); + } +} + +void compile_transformed_expert_models(const CompiledExpertsState& runtime_experts, + ov::npuw::v1::subgraphs::CompileContext& compile_ctx) { + OPENVINO_ASSERT(runtime_experts != nullptr, "Expected compiled MoE experts state"); + LOG_INFO("Compiling MoE expert models..."); + LOG_BLOCK(); + // Compile from a snapshot so set_compiled_model() can erase the temporary + // source ov::Model safely. Erasing from the original map while iterating it + // is undefined behavior and may leave transformed expert models retained. + const auto models_to_compile = runtime_experts->_models_to_compile; + for (const auto& entry : models_to_compile) { + runtime_experts->set_compiled_model( + entry.first, + compile_ctx.compile_model(entry.second, "/moe_chunk_" + std::to_string(entry.first), compile_ctx.devices)); + } + const auto& compiled_models = runtime_experts->_compiled_models; + OPENVINO_ASSERT(!compiled_models.empty(), "Expected at least one compiled MoE expert model"); + compile_ctx.compiled_model = compiled_models.begin()->second; +} + +// Configure the generic compile stage to compile all expert variants produced by transform_experts(). +void configure_expert_compile(ov::npuw::v1::subgraphs::CompiledPipeline& compiled_pipeline, + ov::npuw::v1::subgraphs::Context& compiled_context) { + auto* experts = compiled_context.get_if(); + if (experts == nullptr) { + return; + } + auto compiled_experts = std::make_shared(*experts); + const auto& models_to_compile = compiled_experts->_models_to_compile; + OPENVINO_ASSERT(!models_to_compile.empty(), "Fatal: MoEExperts has no models to compile!"); + put_compiled_experts(compiled_context, compiled_experts); + // The compiled state owns the temporary models needed for compilation. Drop the + // partition-time transform state so those ov::Model instances do not stay pinned + // in the persistent subgraph context after compilation finishes. + compiled_context.erase(); + + attach_runtime_behavior(compiled_pipeline, compiled_context, BehaviorRole::EXPERTS, true); + compiled_pipeline.compile_executor = [compiled_experts](ov::npuw::v1::subgraphs::CompileContext& compile_ctx) { + compile_transformed_expert_models(compiled_experts, compile_ctx); + }; +} + +void transform_downstream(ov::npuw::Function& function, ov::npuw::v1::subgraphs::Context& ctx) { + auto router_model = find_router_model(ctx); + if (!router_model) { + return; + } + auto downstream = ov::npuw::function::create_moe_downstream(function._model, router_model); + if (downstream.has_value()) { + ctx.put(std::move(downstream.value())); + } +} + +void compile_transformed_downstream_model(const ov::npuw::v1::subgraphs::CompileExecutor& previous_compile_executor, + const CompiledDownstreamState& runtime_downstream, + ov::npuw::v1::subgraphs::CompileContext& compile_ctx) { + OPENVINO_ASSERT(runtime_downstream != nullptr, "Expected compiled MoE downstream state"); + compile_ctx.model = runtime_downstream->_model_to_compile; + if (previous_compile_executor) { + previous_compile_executor(compile_ctx); + } else { + compile_ctx.compiled_model = compile_ctx.compile_model(compile_ctx.model, "", compile_ctx.devices); + } + OPENVINO_ASSERT(compile_ctx.compiled_model, "Expected compiled model before wrapping MoE downstream"); + runtime_downstream->set_compiled_model(std::move(compile_ctx.compiled_model)); + compile_ctx.compiled_model = runtime_downstream->_compiled_model; +} + +// Configure the generic compile stage to wrap the default compilation result into the transformed downstream model. +void configure_downstream_compile(ov::npuw::v1::subgraphs::CompiledPipeline& compiled_pipeline, + ov::npuw::v1::subgraphs::Context& compiled_context) { + auto* downstream = compiled_context.get_if(); + if (downstream == nullptr) { + return; + } + auto compiled_downstream = std::make_shared(*downstream); + put_compiled_downstream(compiled_context, compiled_downstream); + // Keep only the compiled/runtime representation in the persistent context. + // The transform-time holder still owns the modified ov::Model and would keep it + // alive for the whole CompiledModel lifetime otherwise. + compiled_context.erase(); + + attach_runtime_behavior(compiled_pipeline, compiled_context, BehaviorRole::DOWNSTREAM, true); + const auto previous_compile_executor = compiled_pipeline.compile_executor; + compiled_pipeline.compile_executor = [previous_compile_executor, + compiled_downstream](ov::npuw::v1::subgraphs::CompileContext& compile_ctx) { + compile_transformed_downstream_model(previous_compile_executor, compiled_downstream, compile_ctx); + }; +} + +} // namespace + +void put_compiled_experts(v1::subgraphs::Context& context, CompiledExpertsState state) { + context.put(std::move(state)); +} + +void put_compiled_downstream(v1::subgraphs::Context& context, CompiledDownstreamState state) { + context.put(std::move(state)); +} + +ov::npuw::compiled::MoEExperts* get_compiled_experts(v1::subgraphs::Context& context) { + return get_compiled_state(context); +} + +const ov::npuw::compiled::MoEExperts* get_compiled_experts(const v1::subgraphs::Context& context) { + return get_compiled_state(context); +} + +ov::npuw::compiled::MoEDownstream* get_compiled_downstream(v1::subgraphs::Context& context) { + return get_compiled_state(context); +} + +const ov::npuw::compiled::MoEDownstream* get_compiled_downstream(const v1::subgraphs::Context& context) { + return get_compiled_state(context); +} + +bool has_compiled_experts(const v1::subgraphs::CompiledPipeline& pipeline) { + return get_compiled_experts(pipeline.context) != nullptr; +} + +bool has_compiled_downstream(const v1::subgraphs::CompiledPipeline& pipeline) { + return get_compiled_downstream(pipeline.context) != nullptr; +} + +bool has_compiled_state(const v1::subgraphs::CompiledPipeline& pipeline) { + return has_compiled_experts(pipeline) || has_compiled_downstream(pipeline); +} + +void clear_partition_state(v1::subgraphs::Context& context) { + context.erase(); + context.erase(); +} + +void serialize_compiled_state(v1::subgraphs::Context& context, + ov::npuw::s11n::Stream& stream, + const ov::npuw::s11n::SubmodelDeserializeCtx* submodel_ctx) { + std::optional moe_experts; + if (const auto* experts = get_compiled_experts(context)) { + moe_experts = *experts; + } + stream & moe_experts; + if (!stream.output() && moe_experts.has_value()) { + put_compiled_experts(context, std::make_shared(moe_experts.value())); + } + auto* mutable_moe_experts = get_compiled_experts(context); + if (mutable_moe_experts != nullptr) { + size_t num_compiled_models = 0; + if (stream.output()) { + num_compiled_models = mutable_moe_experts->_compiled_models.size(); + } + stream & num_compiled_models; + + if (stream.output()) { + for (const auto& [chunk_size, compiled_model] : mutable_moe_experts->_compiled_models) { + stream & chunk_size; + bool has_model = compiled_model != nullptr; + stream & has_model; + if (has_model) { + std::stringstream ss; + compiled_model->export_model(ss); + std::string model_str = ss.str(); + stream & model_str; + } + } + } else { + NPUW_ASSERT(submodel_ctx != nullptr); + for (size_t i = 0; i < num_compiled_models; ++i) { + size_t chunk_size = 0; + stream & chunk_size; + bool has_model = false; + stream & has_model; + if (has_model) { + std::string model_str; + stream & model_str; + std::stringstream ss(model_str); + mutable_moe_experts->_compiled_models[chunk_size] = + submodel_ctx->plugin->get_core()->import_model(ss, + submodel_ctx->device, + submodel_ctx->import_config); + LOG_DEBUG("Imported MoE compiled model for chunk_size=" << chunk_size); + } + } + LOG_DEBUG("Deserialized " << mutable_moe_experts->_compiled_models.size() << " MoE expert models"); + } + } + + std::optional moe_downstream; + if (const auto* downstream = get_compiled_downstream(context)) { + moe_downstream = *downstream; + } + stream & moe_downstream; + if (!stream.output() && moe_downstream.has_value()) { + put_compiled_downstream(context, std::make_shared(moe_downstream.value())); + } + auto* mutable_moe_downstream = get_compiled_downstream(context); + if (mutable_moe_downstream != nullptr) { + bool has_model = false; + if (stream.output()) { + has_model = mutable_moe_downstream->_compiled_model != nullptr; + } + stream & has_model; + if (has_model) { + if (stream.output()) { + std::stringstream ss; + mutable_moe_downstream->_compiled_model->export_model(ss); + std::string model_str = ss.str(); + stream & model_str; + } else { + NPUW_ASSERT(submodel_ctx != nullptr); + std::string model_str; + stream & model_str; + std::stringstream ss(model_str); + mutable_moe_downstream->_compiled_model = + submodel_ctx->plugin->get_core()->import_model(ss, + submodel_ctx->device, + submodel_ctx->import_config); + LOG_DEBUG("Imported MoE downstream compiled model"); + } + } + } +} + +std::vector register_patterns( + ov::npuw::v1::subgraphs::PatternRegistry& registry, + const std::size_t moe_chunk_size) { + std::vector registrations; + registrations.reserve(5); + + registrations.emplace_back(registry.on().scoped()); + registrations.emplace_back(registry.on().scoped()); + + registrations.emplace_back( + registry.on() + .at_partition([moe_chunk_size](ov::npuw::Function& function, ov::npuw::v1::subgraphs::Context& ctx) { + transform_experts(function, ctx, moe_chunk_size); + }) + .at_compile([](ov::npuw::v1::subgraphs::CompiledPipeline& compiled_pipeline, + ov::npuw::v1::subgraphs::Context& compiled_context) { + configure_expert_compile(compiled_pipeline, compiled_context); + }) + .scoped()); + + registrations.emplace_back( + registry.on() + .at_partition([moe_chunk_size](ov::npuw::Function& function, ov::npuw::v1::subgraphs::Context& ctx) { + transform_experts(function, ctx, moe_chunk_size); + }) + .at_compile([](ov::npuw::v1::subgraphs::CompiledPipeline& compiled_pipeline, + ov::npuw::v1::subgraphs::Context& compiled_context) { + configure_expert_compile(compiled_pipeline, compiled_context); + }) + .scoped()); + + ov::npuw::v1::subgraphs::PatternRegistration downstream_registration; + downstream_registration.partition_stage = [](ov::npuw::Function& function, ov::npuw::v1::subgraphs::Context& ctx) { + transform_downstream(function, ctx); + }; + downstream_registration.compile_stage = [](ov::npuw::v1::subgraphs::CompiledPipeline& compiled_pipeline, + ov::npuw::v1::subgraphs::Context& compiled_context) { + configure_downstream_compile(compiled_pipeline, compiled_context); + }; + registrations.emplace_back(registry.add(std::move(downstream_registration))); + + return registrations; +} + +} // namespace moe +} // namespace npuw +} // namespace ov diff --git a/src/plugins/intel_npu/src/plugin/npuw/moe/moe_subgraph.hpp b/src/plugins/intel_npu/src/plugin/npuw/moe/moe_subgraph.hpp new file mode 100644 index 000000000000..c044c876b24e --- /dev/null +++ b/src/plugins/intel_npu/src/plugin/npuw/moe/moe_subgraph.hpp @@ -0,0 +1,67 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#pragma once + +#include +#include +#include +#include +#include + +#include "../serialization.hpp" +#include "../v1/subgraph_pipeline.hpp" + +namespace ov { +class Model; +namespace npuw { +namespace function { +struct MoEExperts; +struct MoEDownstream; +} // namespace function +namespace compiled { +struct MoEExperts; +struct MoEDownstream; +} // namespace compiled +namespace moe { + +enum class BehaviorRole { + EXPERTS, + DOWNSTREAM, +}; + +using CompiledExpertsState = std::shared_ptr; +using CompiledDownstreamState = std::shared_ptr; + +void put_compiled_experts(v1::subgraphs::Context& context, CompiledExpertsState state); +void put_compiled_downstream(v1::subgraphs::Context& context, CompiledDownstreamState state); + +ov::npuw::compiled::MoEExperts* get_compiled_experts(v1::subgraphs::Context& context); +const ov::npuw::compiled::MoEExperts* get_compiled_experts(const v1::subgraphs::Context& context); + +ov::npuw::compiled::MoEDownstream* get_compiled_downstream(v1::subgraphs::Context& context); +const ov::npuw::compiled::MoEDownstream* get_compiled_downstream(const v1::subgraphs::Context& context); + +bool has_compiled_experts(const v1::subgraphs::CompiledPipeline& pipeline); +bool has_compiled_downstream(const v1::subgraphs::CompiledPipeline& pipeline); +bool has_compiled_state(const v1::subgraphs::CompiledPipeline& pipeline); + +void clear_partition_state(v1::subgraphs::Context& context); + +void serialize_compiled_state(v1::subgraphs::Context& context, + ov::npuw::s11n::Stream& stream, + const ov::npuw::s11n::SubmodelDeserializeCtx* submodel_ctx); + +void attach_runtime_behavior(v1::subgraphs::CompiledPipeline& pipeline, + v1::subgraphs::Context& context, + BehaviorRole role, + bool handles_function_prologue); + +std::vector register_patterns( + ov::npuw::v1::subgraphs::PatternRegistry& registry, + std::size_t moe_chunk_size); + +} // namespace moe +} // namespace npuw +} // namespace ov diff --git a/src/plugins/intel_npu/src/plugin/npuw/moe_transformations/device_routed_moe_transform.cpp b/src/plugins/intel_npu/src/plugin/npuw/moe_transformations/device_routed_moe_transform.cpp index 3f467f914d7d..1f91cc23cf72 100644 --- a/src/plugins/intel_npu/src/plugin/npuw/moe_transformations/device_routed_moe_transform.cpp +++ b/src/plugins/intel_npu/src/plugin/npuw/moe_transformations/device_routed_moe_transform.cpp @@ -4,8 +4,12 @@ #include "device_routed_moe_transform.hpp" +#include +#include +#include + #include "../logging.hpp" -#include "../partitioning/patterns/moe.hpp" +#include "moe_transformation_utils.hpp" #include "openvino/core/rt_info.hpp" #include "openvino/op/ops.hpp" @@ -13,8 +17,6 @@ namespace ov { namespace npuw { namespace pass { -namespace opp = ov::pass::pattern; - namespace { // ============================================================================ @@ -39,9 +41,10 @@ struct LayerNodes { struct RouterInfo { std::shared_ptr topk_node; ov::Output topk_indices_raw; - ov::Output topk_softmax_scores; + ov::Output router_scores; // scores from Scatter.input(2), replaces Scatter->Transpose int64_t k_value; - std::string layer_id; + std::shared_ptr scatter_transpose; // the Scatter -> Transpose node + std::shared_ptr output_multiply; // expert x scores Multiply }; // ============================================================================ @@ -57,28 +60,10 @@ inline ov::Output get_weight_source(const ov::Output& input) return input; } -// Extract layer ID from node name (e.g., "layers.0." from full name) -std::string extract_layer_id(const std::string& topk_name) { - size_t layers_pos = topk_name.find("layers."); - if (layers_pos == std::string::npos) { - return ""; - } - - size_t start = layers_pos; - size_t end = topk_name.find(".", start + 7); - if (end == std::string::npos) { - end = topk_name.find("/", start); - } - - return (end != std::string::npos) ? topk_name.substr(start, end - start + 1) : ""; -} - -// Check if node name belongs to the specified layer -inline bool belongs_to_layer(const std::string& node_name, const std::string& layer_id) { - return node_name.find(layer_id) != std::string::npos; -} - -// Check if a reshape operation is unsqueeze-like (only inserts dimensions with size 1) +// Check if a reshape operation is unsqueeze-like (only inserts dimensions with size 1). +// Used in collect_from_expert_output to detect activation-path Reshapes whose shape +// is not a Constant node (e.g. driven by ShapeOf chains) but are safe to replace with +// an Unsqueeze of the data input. bool is_unsqueeze_like_reshape(const std::shared_ptr& reshape) { auto input_shape = reshape->input_value(0).get_partial_shape(); auto output_shape = reshape->get_output_partial_shape(0); @@ -109,216 +94,296 @@ bool is_unsqueeze_like_reshape(const std::shared_ptr& resha } // ============================================================================ -// Router processing +// Router detection by topology // ============================================================================ -std::optional process_router_topk(const std::shared_ptr& topk_node) { - if (!topk_node || topk_node->get_mode() != ov::op::v11::TopK::Mode::MAX) { - return std::nullopt; +// Detects a MoE router pattern starting from a ScatterElementsUpdate node. +// Expected downstream chain: Scatter -> Transpose -> [Reshape/Unsqueeze]* -> Multiply -> ReduceSum +// The Scatter.input(1) must trace back to a TopK (router top-k selection). +// This approach is name-independent and works for both GPT-OSS and Qwen3. +std::optional detect_router_by_topology(const std::shared_ptr& scatter) { + // 1. Find Transpose as a direct consumer of Scatter.output(0) + std::shared_ptr transpose; + for (const auto& ti : scatter->output(0).get_target_inputs()) { + if (auto t = std::dynamic_pointer_cast(ti.get_node()->shared_from_this())) { + transpose = t; + break; + } } - - std::string topk_name = topk_node->get_friendly_name(); - if (topk_name.find(ov::npuw::patterns::moe::MLP_ROUTER_NAME) == std::string::npos) { + if (!transpose) return std::nullopt; - } - // Validate TopK indices shape (batch dimension should be 1, indicates it is model for decoding) - auto topk_indices_raw = topk_node->output(1); - auto indices_shape = topk_indices_raw.get_partial_shape(); - if (indices_shape.rank().is_static() && indices_shape.rank().get_length() == 2) { - if (indices_shape[0].is_static() && indices_shape[0].get_length() != 1) { - LOG_WARN(" TopK indices batch dimension is not 1, skipping"); + // 2. Walk downstream: Transpose -> [Reshape/Unsqueeze]* -> Multiply + // Follow single-consumer chain to find the output_multiply (expert x scores) + auto follow_single = [](const std::shared_ptr& n) -> std::shared_ptr { + const auto targets = n->output(0).get_target_inputs(); + if (targets.size() != 1) + return nullptr; + return targets.begin()->get_node()->shared_from_this(); + }; + + std::shared_ptr output_multiply; + auto cur = follow_single(transpose); + for (int hops = 0; hops < 4 && cur; ++hops) { + if (auto mul = std::dynamic_pointer_cast(cur)) { + output_multiply = mul; + break; + } + if (!std::dynamic_pointer_cast(cur) && + !std::dynamic_pointer_cast(cur)) { return std::nullopt; } + cur = follow_single(cur); } + if (!output_multiply) + return std::nullopt; - // Extract K value - auto k_input = topk_node->input_value(1); - auto k_const = std::dynamic_pointer_cast(k_input.get_node_shared_ptr()); - if (!k_const) { - LOG_WARN(" TopK K value is not a constant, skipping"); + // 3. Verify ReduceSum as the single consumer of output_multiply + if (!std::dynamic_pointer_cast(follow_single(output_multiply))) return std::nullopt; - } - int64_t k_value = k_const->cast_vector()[0]; - // Extract layer ID - std::string layer_id = extract_layer_id(topk_name); - if (layer_id.empty()) { - LOG_WARN(" Cannot extract layer ID from: " << topk_name); + // 4. Trace Scatter.input(1) back to TopK (indices, possibly through Convert) + auto indices_node = scatter->input_value(1).get_node_shared_ptr(); + if (auto conv = std::dynamic_pointer_cast(indices_node)) { + indices_node = conv->input_value(0).get_node_shared_ptr(); + } + auto topk_node = std::dynamic_pointer_cast(indices_node); + if (!topk_node || topk_node->get_mode() != ov::op::v11::TopK::Mode::MAX) { return std::nullopt; } - // Find Softmax for router scores - auto topk_values = topk_node->output(0); - std::shared_ptr topk_softmax = nullptr; - for (const auto& target : topk_values.get_target_inputs()) { - auto consumer = target.get_node()->shared_from_this(); - if (auto softmax = std::dynamic_pointer_cast(consumer)) { - topk_softmax = softmax; - break; + // 5. Validate TopK indices shape: batch dimension must be 1 (decoding stage only) + auto topk_indices_raw = topk_node->output(1); + auto indices_shape = topk_indices_raw.get_partial_shape(); + if (indices_shape.rank().is_static() && indices_shape.rank().get_length() >= 1) { + if (indices_shape[0].is_static() && indices_shape[0].get_length() != 1) { + return std::nullopt; } } - if (!topk_softmax) { - LOG_WARN(" No Softmax found for TopK values"); + // 6. Extract K value from TopK's constant K input + auto k_const = std::dynamic_pointer_cast(topk_node->input_value(1).get_node_shared_ptr()); + if (!k_const) return std::nullopt; - } + int64_t k_value = k_const->cast_vector()[0]; + + // 7. Router scores = Scatter.input(2): the k scores that were scattered into position + // GPT-OSS: Slice(Softmax(TopK.out(0))) Qwen3: Divide(TopK.out(0), ReduceSum(...)) + auto router_scores = scatter->input_value(2); - LOG_INFO("DeviceRoutedMoE: Processing router TopK: " << topk_name << " (K=" << k_value << ")"); + LOG_INFO("DeviceRoutedMoE: Detected MoE router by topology, K=" << k_value); - return RouterInfo{topk_node, topk_indices_raw, topk_softmax->output(0), k_value, layer_id}; + return RouterInfo{topk_node, topk_indices_raw, router_scores, k_value, transpose, output_multiply}; } // ============================================================================ -// Node collection per layer +// Expert node collection by backward BFS from expert output // ============================================================================ -LayerNodes collect_layer_nodes(const std::shared_ptr& model, const RouterInfo& router) { +// Collects expert subgraph nodes by tracing backward from the expert computation output. +// output_multiply has two inputs: expert computation output (BFS entry) and router scores (skip). +// This approach is name-independent and works for both GPT-OSS and Qwen3. +LayerNodes collect_from_expert_output(const RouterInfo& router) { LayerNodes nodes; - const std::string& layer_id = router.layer_id; - int64_t k_value = router.k_value; + nodes.transpose = router.scatter_transpose; + + // Identify which input of output_multiply is the expert computation output. + // The other input comes from the Scatter->Transpose chain (router broadcast path). + auto is_from_scatter_transpose = [&](const std::shared_ptr& n) -> bool { + std::shared_ptr cur = n; + for (int hops = 0; hops < 5 && cur; ++hops) { + if (cur == router.scatter_transpose) + return true; + // Follow input(0) through shape ops (Reshape has data at input 0, Unsqueeze too) + if (std::dynamic_pointer_cast(cur) || + std::dynamic_pointer_cast(cur)) { + cur = cur->input_value(0).get_node_shared_ptr(); + } else { + break; + } + } + return false; + }; + + ov::Output expert_output; + bool found = false; + for (size_t i = 0; i < 2 && !found; ++i) { + auto inp = router.output_multiply->input_value(i); + if (!is_from_scatter_transpose(inp.get_node_shared_ptr())) { + expert_output = inp; + found = true; + } + } + if (!found) { + LOG_WARN("collect_from_expert_output: cannot identify expert output in output_multiply"); + return nodes; + } - // Single pass through all nodes to collect relevant operations - for (const auto& n : model->get_ordered_ops()) { - std::string node_name = n->get_friendly_name(); + // Phase 1: BFS backward from expert_output, collecting all activation-path nodes. + // Constant-derived inputs (weights, scales, biases) are skipped in BFS but collected in phase 3. + std::queue> queue; + std::unordered_set> visited; - // Skip nodes not belonging to this layer or not MoE expert nodes - if (node_name.find(ov::npuw::patterns::moe::MLP_EXPERT_NAME) == std::string::npos || - !belongs_to_layer(node_name, layer_id)) { - continue; - } + auto start = expert_output.get_node_shared_ptr(); + queue.push(start); + visited.insert(start); + + std::vector> all_tiles; + std::vector> all_const_reshapes; + std::vector> all_dyn_reshapes; + std::vector> all_matmuls; + std::vector> all_adds; + std::vector> all_multiplies; + + while (!queue.empty()) { + auto n = queue.front(); + queue.pop(); - // Collect Tile nodes if (auto tile = std::dynamic_pointer_cast(n)) { - auto repeats_const = - std::dynamic_pointer_cast(tile->input_value(1).get_node_shared_ptr()); - if (repeats_const) { - auto repeats_data = repeats_const->cast_vector(); - if (!repeats_data.empty() && repeats_data[0] > k_value) { - if (nodes.num_experts == 0) { - nodes.num_experts = static_cast(repeats_data[0]); - } - nodes.tiles.push_back(tile); - } - } - continue; + all_tiles.push_back(tile); + continue; // Tile is the boundary; don't BFS further into shared hidden state } - - // Collect Reshape nodes if (auto reshape = std::dynamic_pointer_cast(n)) { - auto shape_const = - std::dynamic_pointer_cast(reshape->input_value(1).get_node_shared_ptr()); - - if (!shape_const) { - // Dynamic reshape - check if it's unsqueeze-like - if (is_unsqueeze_like_reshape(reshape)) { - nodes.dynamic_reshapes.push_back(reshape); - } - } else { - // Constant reshape - check if dim 0 is expert dimension - auto shape_data = shape_const->cast_vector(); - if (nodes.num_experts > 0 && !shape_data.empty() && - shape_data[0] == static_cast(nodes.num_experts)) { - nodes.constant_reshapes.push_back(reshape); - } + auto shape_src = reshape->input_value(1).get_node_shared_ptr(); + if (std::dynamic_pointer_cast(shape_src)) { + all_const_reshapes.push_back(reshape); + } else if (is_unsqueeze_like_reshape(reshape)) { + all_dyn_reshapes.push_back(reshape); } - continue; + } else if (auto matmul = std::dynamic_pointer_cast(n)) { + all_matmuls.push_back(matmul); + } else if (auto add = std::dynamic_pointer_cast(n)) { + all_adds.push_back(add); + } else if (auto mul = std::dynamic_pointer_cast(n)) { + all_multiplies.push_back(mul); } - // Collect MatMul nodes - if (auto matmul = std::dynamic_pointer_cast(n)) { - auto weight_source = get_weight_source(matmul->input_value(1)); - auto weight_node = weight_source.get_node_shared_ptr(); - - // Check if quantized weight comes from Multiply with expert-dimension constant - if (auto multiply = std::dynamic_pointer_cast(weight_node)) { - for (size_t i = 0; i < 2; ++i) { - auto mul_input = multiply->get_input_node_shared_ptr(i); - if (auto const_node = std::dynamic_pointer_cast(mul_input)) { - auto shape = const_node->get_shape(); - if (nodes.num_experts > 0 && shape.size() >= 2 && shape[0] == nodes.num_experts) { - nodes.matmuls.push_back(matmul); - break; - } - } - } - } + // Enqueue non-constant-derived inputs (activation paths only) + for (size_t i = 0; i < n->get_input_size(); ++i) { + auto inp_node = n->input_value(i).get_node_shared_ptr(); + if (visited.count(inp_node)) + continue; + if (moe_utils::is_constant_derived(inp_node)) + continue; + if (std::dynamic_pointer_cast(inp_node)) + continue; + visited.insert(inp_node); + queue.push(inp_node); + } + } + + // Phase 2: Determine num_experts from Tile repeats[0] > k_value + for (auto& tile : all_tiles) { + auto repeats_const = + std::dynamic_pointer_cast(tile->input_value(1).get_node_shared_ptr()); + if (!repeats_const) continue; + auto rdata = repeats_const->cast_vector(); + if (!rdata.empty() && rdata[0] > router.k_value) { + if (nodes.num_experts == 0) { + nodes.num_experts = static_cast(rdata[0]); + } + nodes.tiles.push_back(tile); } + } - // Collect Add nodes - if (auto add = std::dynamic_pointer_cast(n)) { - // Check both inputs for expert-dimension bias constant - for (size_t input_idx = 0; input_idx < 2; ++input_idx) { - auto bias_source = get_weight_source(add->input_value(input_idx)); - auto const_node = std::dynamic_pointer_cast(bias_source.get_node_shared_ptr()); + if (nodes.num_experts == 0) { + LOG_WARN("collect_from_expert_output: no Tile with repeats[0] > k_value found"); + return nodes; + } - if (const_node) { - auto shape = const_node->get_shape(); - if (nodes.num_experts > 0 && shape.size() >= 1 && shape[0] == nodes.num_experts) { - nodes.adds.push_back(add); - break; - } - } - } - continue; + // Phase 3: Filter collected nodes — keep only those whose weight/shape uses the expert dimension + for (auto& reshape : all_const_reshapes) { + auto shape_const = + std::dynamic_pointer_cast(reshape->input_value(1).get_node_shared_ptr()); + auto shape_data = shape_const->cast_vector(); + if (!shape_data.empty() && shape_data[0] == static_cast(nodes.num_experts)) { + nodes.constant_reshapes.push_back(reshape); } + } - // Collect Multiply nodes, e.g. AWQ multiply (one input from constant, other input is not constant/convert, and - // user is not MatMul) - if (auto multiply = std::dynamic_pointer_cast(n)) { - // Skip if this Multiply is used by MatMul (it's MatMul weights multiply, which has been processed by - // transform_matmuls) - bool used_by_matmul = false; - for (const auto& output : multiply->outputs()) { - for (const auto& target : output.get_target_inputs()) { - auto user = target.get_node()->shared_from_this(); - if (std::dynamic_pointer_cast(user)) { - used_by_matmul = true; - break; - } - } - if (used_by_matmul) - break; - } - if (used_by_matmul) { - continue; - } + nodes.dynamic_reshapes = std::move(all_dyn_reshapes); - // Check both inputs: one should be constant with expert dimension, other should not be constant/convert - for (size_t const_idx = 0; const_idx < 2; ++const_idx) { - size_t other_idx = 1 - const_idx; + // Returns true if a constant somewhere in the weight chain has shape[0] == expert_dim. + // Recursively peels Convert and both inputs of Multiply (the two recognized chain patterns). + // Returns false for any unrecognized node type, signalling an unknown weight chain. + std::function&, size_t, bool&)> weight_has_expert_dim; + weight_has_expert_dim = [&](const ov::Output& input, size_t expert_dim, bool& chain_recognized) -> bool { + auto node = input.get_node_shared_ptr(); + if (auto c = std::dynamic_pointer_cast(node)) { + return !c->get_shape().empty() && c->get_shape()[0] == expert_dim; + } + if (auto conv = std::dynamic_pointer_cast(node)) { + return weight_has_expert_dim(conv->input_value(0), expert_dim, chain_recognized); + } + if (auto mul = std::dynamic_pointer_cast(node)) { + return weight_has_expert_dim(mul->input_value(0), expert_dim, chain_recognized) || + weight_has_expert_dim(mul->input_value(1), expert_dim, chain_recognized); + } + // Unrecognized node in weight chain (e.g. Reshape, Gather, custom op). + chain_recognized = false; + return false; + }; - auto const_source = get_weight_source(multiply->input_value(const_idx)); - auto const_node = std::dynamic_pointer_cast(const_source.get_node_shared_ptr()); + for (auto& matmul : all_matmuls) { + bool chain_recognized = true; + bool has_expert_dim = weight_has_expert_dim(matmul->input_value(1), nodes.num_experts, chain_recognized); + if (!has_expert_dim) { + continue; // weight doesn't use expert dimension — not an expert MatMul + } + if (!chain_recognized) { + // Weight has the expert dimension but through an unrecognized chain: we cannot safely + // insert a Gather into it. Transforming other nodes (Tile, Reshape) without transforming + // this MatMul's weight would produce a shape mismatch at inference time. + LOG_WARN("collect_from_expert_output: MatMul '" << matmul->get_friendly_name() + << "' has expert-dim weight through an unrecognized chain; " + "skipping transformation for this layer"); + nodes.matmuls.clear(); + return nodes; + } + nodes.matmuls.push_back(matmul); + } - if (const_node) { - auto shape = const_node->get_shape(); - if (nodes.num_experts > 0 && shape.size() >= 1 && shape[0] == nodes.num_experts) { - // Check if other input is not constant/convert - auto other_input = multiply->input_value(other_idx); - auto other_source = get_weight_source(other_input); - auto other_node = other_source.get_node_shared_ptr(); - - if (!std::dynamic_pointer_cast(other_node)) { - nodes.multiplies.push_back(multiply); - break; - } - } + for (auto& add : all_adds) { + for (size_t i = 0; i < 2; ++i) { + auto bias_source = get_weight_source(add->input_value(i)); + if (auto c = std::dynamic_pointer_cast(bias_source.get_node_shared_ptr())) { + if (!c->get_shape().empty() && c->get_shape()[0] == nodes.num_experts) { + nodes.adds.push_back(add); + break; } } - continue; } + } - // Collect Transpose node - if (auto transpose = std::dynamic_pointer_cast(n)) { - auto input_node = transpose->input_value(0).get_node_shared_ptr(); - if (std::dynamic_pointer_cast(input_node) || - std::dynamic_pointer_cast(input_node)) { - nodes.transpose = transpose; - // Don't break - continue collecting other nodes + for (auto& mul : all_multiplies) { + // Skip if used as MatMul weight input (quantized weight chain, handled by transform_matmuls) + bool used_by_matmul = false; + for (const auto& out : mul->outputs()) { + for (const auto& ti : out.get_target_inputs()) { + if (std::dynamic_pointer_cast(ti.get_node()->shared_from_this())) { + used_by_matmul = true; + break; + } } + if (used_by_matmul) + break; + } + if (used_by_matmul) continue; + + // Keep only activation-scale multiplies (one constant input with expert dim, one activation input) + for (size_t i = 0; i < 2; ++i) { + auto const_src = get_weight_source(mul->input_value(i)); + if (auto c = std::dynamic_pointer_cast(const_src.get_node_shared_ptr())) { + if (!c->get_shape().empty() && c->get_shape()[0] == nodes.num_experts) { + auto other_src = get_weight_source(mul->input_value(1 - i)); + if (!std::dynamic_pointer_cast(other_src.get_node_shared_ptr())) { + nodes.multiplies.push_back(mul); + break; + } + } + } } } @@ -482,13 +547,46 @@ void transform_multiplies(LayerNodes& nodes, const std::shared_ptr& topk_softmax_scores) { +void transform_transpose(LayerNodes& nodes, const ov::Output& router_scores) { if (nodes.transpose) { auto transpose_input = nodes.transpose->input_value(0); auto input_node = transpose_input.get_node_shared_ptr(); - nodes.transpose->input(0).replace_source_output(topk_softmax_scores); - ov::copy_runtime_info(input_node, topk_softmax_scores.get_node_shared_ptr()); + nodes.transpose->input(0).replace_source_output(router_scores); + ov::copy_runtime_info(input_node, router_scores.get_node_shared_ptr()); + } +} + +// After transform_transpose, the router broadcast chain (Transpose → Reshape → Unsqueeze → Multiply) +// still has Reshape shape constants with dim-0 = num_experts. Update them to k_value. +void transform_router_broadcast_chain(const RouterInfo& router, size_t num_experts) { + if (!router.scatter_transpose) + return; + auto cur = router.scatter_transpose->shared_from_this(); + // Walk forward until we reach output_multiply (at most a few hops) + for (int hops = 0; hops < 6 && cur && cur != router.output_multiply; ++hops) { + if (auto reshape = std::dynamic_pointer_cast(cur)) { + auto shape_const = + std::dynamic_pointer_cast(reshape->input_value(1).get_node_shared_ptr()); + if (shape_const) { + auto shape_data = shape_const->cast_vector(); + if (!shape_data.empty() && shape_data[0] == static_cast(num_experts)) { + shape_data[0] = router.k_value; + auto new_shape = ov::op::v0::Constant::create(shape_const->get_element_type(), + shape_const->get_shape(), + shape_data); + reshape->input(1).replace_source_output(new_shape); + ov::copy_runtime_info(shape_const, new_shape); + LOG_DEBUG(" Updated router broadcast Reshape shape[0] from " << num_experts << " to " + << router.k_value); + } + } + } + // Advance to the single consumer + const auto targets = cur->output(0).get_target_inputs(); + if (targets.size() != 1) + break; + cur = targets.begin()->get_node()->shared_from_this(); } } @@ -496,9 +594,9 @@ bool apply_layer_transformation(const RouterInfo& router, LayerNodes& nodes) { // Validate we have required nodes if (!nodes.has_required_nodes()) { if (nodes.constant_reshapes.empty() && nodes.dynamic_reshapes.empty()) { - LOG_WARN(" Skipping layer " << router.layer_id << ": No Reshape nodes found"); + LOG_WARN(" Skipping layer: No Reshape nodes found"); } else { - LOG_WARN(" Skipping layer " << router.layer_id << ": No MatMul/Add nodes found"); + LOG_WARN(" Skipping layer: No MatMul/Add nodes found"); } return false; } @@ -515,9 +613,10 @@ bool apply_layer_transformation(const RouterInfo& router, LayerNodes& nodes) { transform_matmuls(nodes, topk_indices); transform_adds(nodes, topk_indices); transform_multiplies(nodes, topk_indices); - transform_transpose(nodes, router.topk_softmax_scores); + transform_transpose(nodes, router.router_scores); + transform_router_broadcast_chain(router, nodes.num_experts); - LOG_INFO("DeviceRoutedMoE transformation successful for " << router.layer_id); + LOG_INFO("DeviceRoutedMoE transformation successful"); LOG_INFO(" Tiles: " << nodes.tiles.size() << ", ConstReshapes: " << nodes.constant_reshapes.size() << ", DynReshapes: " << nodes.dynamic_reshapes.size() << ", MatMuls: " << nodes.matmuls.size() << ", Adds: " << nodes.adds.size() << ", Multiplies: " << nodes.multiplies.size() @@ -537,18 +636,20 @@ bool DeviceRoutedMoETransform::run_on_model(const std::shared_ptr& mo bool model_changed = false; - // Process each Router TopK node (one per MoE layer) + // Process each ScatterElementsUpdate node; validate each as a MoE router by topology for (const auto& node : model->get_ordered_ops()) { - auto topk_node = std::dynamic_pointer_cast(node); + const bool is_scatter = std::dynamic_pointer_cast(node) != nullptr || + std::dynamic_pointer_cast(node) != nullptr; + if (!is_scatter) + continue; - // Step 1: Process and validate router - auto router = process_router_topk(topk_node); - if (!router.has_value()) { + // Step 1: Detect router by topology (name-independent, works for GPT-OSS and Qwen3) + auto router = detect_router_by_topology(node); + if (!router.has_value()) continue; - } - // Step 2: Collect all nodes for this layer - auto layer_nodes = collect_layer_nodes(model, router.value()); + // Step 2: Collect expert nodes via backward BFS from output_multiply + auto layer_nodes = collect_from_expert_output(router.value()); // Step 3: Transform collected nodes (all-or-nothing) if (apply_layer_transformation(router.value(), layer_nodes)) { diff --git a/src/plugins/intel_npu/src/plugin/npuw/moe_transformations/moe_transformation.cpp b/src/plugins/intel_npu/src/plugin/npuw/moe_transformations/moe_transformation.cpp index c494c7583c41..7d3adee35754 100644 --- a/src/plugins/intel_npu/src/plugin/npuw/moe_transformations/moe_transformation.cpp +++ b/src/plugins/intel_npu/src/plugin/npuw/moe_transformations/moe_transformation.cpp @@ -52,6 +52,24 @@ static std::optional extract_k_from_router(const std::shared_ptr resolve_compiled_idx(const std::shared_ptr& transformed_model, + const char* tag, + std::optional original_idx) { + const auto& params = transformed_model->get_parameters(); + for (size_t i = 0; i < params.size(); ++i) { + if (params[i]->get_rt_info().count(tag) > 0) { + LOG_INFO("Post-transform [" << tag << "] idx: " << i << " (original: " << original_idx.value_or(SIZE_MAX) + << ")"); + return i; + } + } + LOG_WARN("[" << tag << "] not found in transformed model; falling back to original index"); + return original_idx; +} + // Helper function to build parameter mapping from RTInfo metadata static std::map> build_parameter_mapping_from_rtinfo( const std::shared_ptr& original_model, @@ -311,15 +329,19 @@ MoETransformConfig determine_transformation_params(const MoEStructureInfo& struc return config; } -// Helper: Update Reshape node's constant input at a specific dimension -// MoE Reshape patterns: 3D [num_experts, token_num, hidden_dim] or 4D [num_experts, 1, token_num, hidden_dim] -// - dimension_index can be: -// * 0: for num_experts (first dimension) -// * -2: for token_num (second-to-last dimension, works for both 3D and 4D) -static bool update_reshape_constant_dimension(const std::shared_ptr& reshape_node, - int dimension_index, - size_t old_value, - size_t new_value) { +// Helper: Update Reshape output-shape constant: replace old_value with new_value in +// dims [first_dim, last_dim_exclusive). Pass SIZE_MAX for last_dim_exclusive to stop +// before the last dimension (i.e. exclude hidden_dim at dim n-1). +// +// Typical MoE reshape layouts (3-D or 4-D): +// dim 0 : num_experts → call with first_dim=0, last_dim_exclusive=1 +// dims 1 .. n-2 : seq_len / size-1 expansion → call with first_dim=1, last_dim_exclusive=SIZE_MAX +// dim n-1 (last) : hidden_dim (never touched; excluded when last_dim_exclusive==SIZE_MAX) +static bool update_reshape_dimensions(const std::shared_ptr& reshape_node, + size_t old_value, + size_t new_value, + size_t first_dim = 0, + size_t last_dim_exclusive = SIZE_MAX) { auto shape_input = reshape_node->input_value(1); auto shape_const = std::dynamic_pointer_cast(shape_input.get_node_shared_ptr()); if (!shape_const) { @@ -328,34 +350,31 @@ static bool update_reshape_constant_dimension(const std::shared_ptrcast_vector(); - // MoE reshapes are either 3D or 4D + // Only handle 3-D and 4-D MoE reshape patterns if (shape_data.size() < 3 || shape_data.size() > 4) { return false; } - // Convert negative index to positive (e.g., -2 means second-to-last) - size_t actual_index; - if (dimension_index < 0) { - actual_index = shape_data.size() + dimension_index; - } else { - actual_index = dimension_index; + // Resolve SIZE_MAX sentinel: stop before the last dimension + size_t end = (last_dim_exclusive == SIZE_MAX) ? shape_data.size() - 1 : last_dim_exclusive; + end = std::min(end, shape_data.size()); + + bool updated = false; + for (size_t i = first_dim; i < end; ++i) { + if (shape_data[i] == static_cast(old_value)) { + LOG_DEBUG(" Updating Reshape '" << reshape_node->get_friendly_name() << "' shape[" << i << "] from " + << old_value << " to " << new_value); + shape_data[i] = static_cast(new_value); + updated = true; + } } - // Check if the dimension value matches and update it - if (actual_index < shape_data.size() && shape_data[actual_index] == static_cast(old_value)) { - LOG_DEBUG(" Updating Reshape '" << reshape_node->get_friendly_name() << "' shape[" << actual_index << "] from " - << old_value << " to " << new_value); - - shape_data[actual_index] = new_value; - + if (updated) { auto new_shape_const = std::make_shared(ov::element::i64, ov::Shape{shape_data.size()}, shape_data); reshape_node->input(1).replace_source_output(new_shape_const->output(0)); - - return true; } - - return false; + return updated; } // Helper: Check if parameter matches downstream pattern @@ -396,21 +415,31 @@ std::optional detect_and_transform_moe_downstream(const std::shar LOG_DEBUG("Detecting MoE downstream pattern..."); LOG_BLOCK(); - // Pattern to match: Parameter -> Convert -> ReduceSum - // Looking for a parameter with shape [N, 1, H, W] where N is total_experts_num + // Pattern to match: Parameter -> [Convert] -> ReduceSum + // Looking for a parameter with shape [N, ..., W] where: + // dim 0 = N (total experts, > 1) + // dim n-1 = W (hidden_dim) + // at least one of dims 1..n-2 equals 1 (singleton "batch" dimension) + // Both [N, 1, H, W] (GPT-OSS) and [N, H, 1, W] (Qwen) are accepted. const auto& params = model->get_parameters(); for (size_t param_idx = 0; param_idx < params.size(); ++param_idx) { const auto& param = params[param_idx]; - // Validate shape: must be [N, 1, H, W] where N > 1 + // Validate shape: must be 4-D with dim 0 > 1 auto param_shape = param->get_partial_shape(); if (!param_shape.rank().is_static() || param_shape.rank().get_length() != 4) { continue; } auto shape = param_shape.to_shape(); - if (shape[1] != 1 || shape[0] <= 1) { + if (shape[0] <= 1) { + continue; + } + + // At least one of dims 1..2 must be 1 (singleton expansion dim) + bool has_singleton_dim = (shape[1] == 1 || shape[2] == 1); + if (!has_singleton_dim) { continue; } @@ -681,10 +710,9 @@ void MoEModelTransformer::fix_parameters_with_num_experts(const std::shared_ptr< continue; } - // For MoE Reshape patterns (3D or 4D), num_experts is always at dimension 0 - // 3D: [num_experts, token_num, hidden_dim] - // 4D: [num_experts, 1, token_num, hidden_dim] - update_reshape_constant_dimension(reshape_node, 0, num_experts, num_target_experts); + // For MoE Reshape patterns (3D or 4D), num_experts is always at dimension 0. + // Scan only dim 0 (first_dim=0, last_dim_exclusive=1). + update_reshape_dimensions(reshape_node, num_experts, num_target_experts, 0, 1); } } @@ -764,19 +792,17 @@ void MoEModelTransformer::fix_token_count_for_expert_iterative( } } - // Also fix Reshape nodes that contain original_token_count in their shape constants + // Also fix Reshape nodes that contain original_token_count in their shape constants. + // Scan dims 1..n-2 (skip dim 0 = num_experts and last dim = hidden_dim) and replace any + // dimension whose value equals original_token_count with chunk_size. LOG_DEBUG("Fixing Reshape nodes with token_count in shape..."); for (const auto& node : model->get_ordered_ops()) { auto reshape_node = std::dynamic_pointer_cast(node); if (!reshape_node) { continue; } - - // For MoE Reshape patterns (3D or 4D), token_num is always at second-to-last dimension - // 3D: [num_experts, token_num, hidden_dim] - token_num at index 1 - // 4D: [num_experts, 1, token_num, hidden_dim] - token_num at index 2 - // Use -2 to refer to second-to-last dimension in both cases - update_reshape_constant_dimension(reshape_node, -2, original_token_count, chunk_size); + // Scan middle dims (1..n-2), skip dim 0 (num_experts) and last dim (hidden_dim). + update_reshape_dimensions(reshape_node, original_token_count, chunk_size, 1); } // Trigger shape inference to propagate changes through the model @@ -926,6 +952,19 @@ std::optional MoEExperts::from(const std::shared_ptr& mod } } + // Tag activation parameters before transformation so we can re-locate them afterwards + // (transformations may shift parameter indices). + constexpr const char* expert_input_tag = "npuw_moe_expert_input"; + constexpr const char* router_scores_tag = "npuw_moe_router_scores"; + auto tag_param = [&](std::optional idx, const char* tag) { + if (idx.has_value()) { + model->get_parameters()[idx.value()]->get_rt_info()[tag] = true; + LOG_DEBUG("Tagged [" << tag << "] at original index " << idx.value()); + } + }; + tag_param(structure_info->expert_input_param_idx, expert_input_tag); + tag_param(structure_info->router_scores_idx, router_scores_tag); + // Step 4: Transform models for each chunk size std::map> transformed_models; for (auto chunk_size : chunk_sizes) { @@ -949,6 +988,16 @@ std::optional MoEExperts::from(const std::shared_ptr& mod return std::nullopt; } + // Resolve compiled-model parameter indices via rt_info tags. + // All chunk-size variants share the same parameter ordering, so the first model suffices. + // router_scores may be absent in EXPERT_BATCH (unrolled into K closures); resolve_compiled_idx + // falls back to the original index, which is harmless since run_expert_batch uses param_mapping. + const auto& first_transformed_model = transformed_models.begin()->second; + const auto expert_input_compiled_idx = + resolve_compiled_idx(first_transformed_model, expert_input_tag, structure_info->expert_input_param_idx); + const auto router_scores_compiled_idx = + resolve_compiled_idx(first_transformed_model, router_scores_tag, structure_info->router_scores_idx); + // Step 5: Build parameter mapping from any transformed model (all have same mapping) // Note: For EXPERT_ITERATIVE mode (single expert), no unrolling happens, so mapping will be empty // For EXPERT_BATCH mode (K experts), unrolling creates the same mapping structure @@ -962,8 +1011,10 @@ std::optional MoEExperts::from(const std::shared_ptr& mod moe_experts._chunk_token_count = structure_info->is_expert_batch_mode() ? 0 : iterative_chunk_size; moe_experts._num_active_experts = k_value; // Store actual K from router moe_experts._transformed_models = std::move(transformed_models); - moe_experts._router_scores_idx = structure_info->router_scores_idx; - moe_experts._expert_input_param_idx = structure_info->expert_input_param_idx; + moe_experts._router_scores.original = structure_info->router_scores_idx; + moe_experts._router_scores.compiled = router_scores_compiled_idx; + moe_experts._expert_input.original = structure_info->expert_input_param_idx; + moe_experts._expert_input.compiled = expert_input_compiled_idx; moe_experts._param_mapping = std::move(param_mapping); if (!moe_experts.is_valid()) { @@ -998,8 +1049,10 @@ MoEExperts::MoEExperts(const function::MoEExperts& func_moe) { num_active_experts = func_moe._num_active_experts; input_token_count = func_moe._input_token_count; _models_to_compile = func_moe._transformed_models; - _router_scores_idx = func_moe._router_scores_idx; - _expert_input_param_idx = func_moe._expert_input_param_idx; + _router_scores.original = func_moe._router_scores.original; + _router_scores.compiled = func_moe._router_scores.compiled; + _expert_input.original = func_moe._expert_input.original; + _expert_input.compiled = func_moe._expert_input.compiled; _param_mapping = func_moe._param_mapping; LOG_DEBUG("Created compiled::MoEExperts:"); @@ -1011,11 +1064,11 @@ MoEExperts::MoEExperts(const function::MoEExperts& func_moe) { for (const auto& entry : _models_to_compile) { LOG_DEBUG(" Chunk size: " << entry.first); } - if (_router_scores_idx.has_value()) { - LOG_DEBUG(" Router scores parameter index: " << _router_scores_idx.value()); + if (_router_scores.original.has_value()) { + LOG_DEBUG(" Router scores parameter index: " << _router_scores.original.value()); } - if (_expert_input_param_idx.has_value()) { - LOG_DEBUG(" Expert input parameter index: " << _expert_input_param_idx.value()); + if (_expert_input.original.has_value()) { + LOG_DEBUG(" Expert input parameter index: " << _expert_input.original.value()); } } diff --git a/src/plugins/intel_npu/src/plugin/npuw/moe_transformations/moe_transformation.hpp b/src/plugins/intel_npu/src/plugin/npuw/moe_transformations/moe_transformation.hpp index ec754ce67fd5..7cb4808bca9b 100644 --- a/src/plugins/intel_npu/src/plugin/npuw/moe_transformations/moe_transformation.hpp +++ b/src/plugins/intel_npu/src/plugin/npuw/moe_transformations/moe_transformation.hpp @@ -1,5 +1,6 @@ // Copyright (C) 2018-2026 Intel Corporation // SPDX-License-Identifier: Apache-2.0 +// #pragma once @@ -25,6 +26,13 @@ namespace ov { namespace npuw { +// Holds original-model index (for prologue matching / param_mapping lookup) +// and compiled-model index (for direct port binding after transformation). +struct ParamIndex { + std::optional original; // index in original model + std::optional compiled; // index in compiled/transformed model +}; + namespace function { // Default chunk size variants for dynamic iterative mode chunking @@ -184,9 +192,9 @@ struct MoEExperts { // For EXPERT_BATCH: single model with chunk_size = 0 (no chunking, K active experts) std::map> _transformed_models; - // Parameter indices - std::optional _router_scores_idx; // Parameter index for router scores - std::optional _expert_input_param_idx; // Parameter index for expert's input (token embeddings) + // Parameter indices (original = in original model, compiled = in compiled/transformed model) + ParamIndex _router_scores; // router scores input parameter + ParamIndex _expert_input; // expert activation input parameter // Parameter mapping: original_param_idx -> [unrolled_param_indices] // For EXPERT_ITERATIVE mode: no unrolling, so this will be empty or identity mapping @@ -197,7 +205,7 @@ struct MoEExperts { // Validation helpers bool is_valid() const { return _num_experts > 0 && _expert_hidden_dim > 0 && !_transformed_models.empty() && - _router_scores_idx.has_value(); + _router_scores.original.has_value(); } size_t num_experts() const { @@ -235,14 +243,6 @@ struct MoEExperts { return nullptr; } - std::optional router_scores_idx() const { - return _router_scores_idx; - } - - std::optional expert_input_param_idx() const { - return _expert_input_param_idx; - } - const std::map>& param_mapping() const { return _param_mapping; } @@ -277,10 +277,10 @@ struct MoEExperts { // Store models temporarily for compilation (chunk_size -> model) std::map> _models_to_compile; - // Router scores parameter index (from Multiply in output path) - std::optional _router_scores_idx; - // Expert input parameter index (token embeddings) - std::optional _expert_input_param_idx; + // Router scores / expert input parameter indices + // (.original = in original model; .compiled = in compiled/transformed model) + ParamIndex _router_scores; + ParamIndex _expert_input; // Parameter mapping: original_param_idx -> [unrolled_param_indices] // Same for all chunk sizes (unrolling only in EXPERT_BATCH mode) diff --git a/src/plugins/intel_npu/src/plugin/npuw/moe_transformations/moe_transformation_utils.hpp b/src/plugins/intel_npu/src/plugin/npuw/moe_transformations/moe_transformation_utils.hpp new file mode 100644 index 000000000000..d00b51b98ebf --- /dev/null +++ b/src/plugins/intel_npu/src/plugin/npuw/moe_transformations/moe_transformation_utils.hpp @@ -0,0 +1,37 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#pragma once + +#include + +#include "openvino/op/constant.hpp" +#include "openvino/op/convert.hpp" +#include "openvino/op/multiply.hpp" + +namespace ov { +namespace npuw { +namespace moe_utils { + +// Returns true if the node's output is entirely determined by constants +// (no data-dependent values from Parameters flow through it). +// Recognizes: Constant, Convert(constant), Multiply(constant, constant). +inline bool is_constant_derived(const std::shared_ptr& n) { + if (!n) + return false; + if (std::dynamic_pointer_cast(n)) + return true; + if (auto conv = std::dynamic_pointer_cast(n)) { + return is_constant_derived(conv->input_value(0).get_node_shared_ptr()); + } + if (auto mul = std::dynamic_pointer_cast(n)) { + return is_constant_derived(mul->input_value(0).get_node_shared_ptr()) && + is_constant_derived(mul->input_value(1).get_node_shared_ptr()); + } + return false; +} + +} // namespace moe_utils +} // namespace npuw +} // namespace ov diff --git a/src/plugins/intel_npu/src/plugin/npuw/npuw_transformations/add_position_ids_param.cpp b/src/plugins/intel_npu/src/plugin/npuw/npuw_transformations/add_position_ids_param.cpp new file mode 100644 index 000000000000..79d06af75745 --- /dev/null +++ b/src/plugins/intel_npu/src/plugin/npuw/npuw_transformations/add_position_ids_param.cpp @@ -0,0 +1,121 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "add_position_ids_param.hpp" + +#include "openvino/op/ops.hpp" +#include "openvino/openvino.hpp" +#include "openvino/pass/manager.hpp" +#include "openvino/pass/matcher_pass.hpp" +#include "openvino/pass/pattern/multi_matcher.hpp" +#include "openvino/pass/pattern/op/optional.hpp" +#include "openvino/pass/pattern/op/or.hpp" +#include "openvino/pass/pattern/op/wrap_type.hpp" +#include "openvino/pass/validate.hpp" + +namespace opp = ov::pass::pattern; + +namespace { + +// diagnostics warnings on OPENVINO_MATCHER_PASS_RTTI() definition: visibility hidden +#ifdef __GNUC__ +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Wattributes" +#endif + +void set_node_name(std::shared_ptr node, const std::string& name) { + node->set_friendly_name(name); + node->get_output_tensor(0).set_names({name}); +} + +// TODO: Consolidate with similar pattern in prepare_embedding_model.cpp +class HardcodedPositionIdsMatcher : public ov::pass::MultiMatcher { +public: + OPENVINO_MATCHER_PASS_RTTI("ov::npuw::HardcodedPositionIdsMatcher"); + explicit HardcodedPositionIdsMatcher(ov::ParameterVector& new_params) { + auto range = opp::wrap_type(); + auto unsqueeze_axes = opp::wrap_type(); + auto unsqueeze = opp::wrap_type({range, unsqueeze_axes}); + + auto unsqueeze1_axes = opp::wrap_type(); + auto unsqueeze1 = opp::wrap_type({unsqueeze, unsqueeze1_axes}); + + // FIXME: Convert probably needs to be made optional in future as in similar transformation from + // prepare_embedding_model.cpp + auto convert = opp::wrap_type({unsqueeze1}); + auto matmul = opp::wrap_type({opp::any_input(), convert}); + auto transpose = opp::wrap_type({matmul, opp::any_input()}); + + auto concat = opp::wrap_type({transpose, transpose}); + auto cos = opp::wrap_type(concat); + auto sin = opp::wrap_type(concat); + + ov::pass::MultiMatcher::Callback callback = [=, &new_params](const auto& m) { + // NOTE: Range that mimics `position_ids` is consumed by RoPE operation as well as by Causal Mask creation + // (LessEqual operation) and Gated Short Convolution Block's ScattedNDUpdate operation. + // For static shapes case, it is not right to use actual `position_ids` for the second argument of + // LessEqual operation (=Q range), because causal triangular mask will only allow positions from + // the left till the real current positions in the sequence (inclusively), while our current items + // are lied at the right end of the static `input_ids` after a window of padding. + // Thus, the Range is preserved for Causal Mask creation, while added `position_ids` parameter is + // used only for RoPE and ScatterNDUpdate in Gated Short Convolution Block. + auto& pattern_to_output = m.at(cos).front(); + + auto range_node = pattern_to_output.at(range).get_node_shared_ptr(); + auto unsqueeze_node = pattern_to_output.at(unsqueeze).get_node_shared_ptr(); + // Point of branching: + auto unsqueeze1_node = pattern_to_output.at(unsqueeze1).get_node_shared_ptr(); + auto convert_node = pattern_to_output.at(convert).get_node_shared_ptr(); + + // Create `position_ids` parameter + auto position_ids = std::make_shared(ov::element::i64, ov::PartialShape{-1, -1}); + set_node_name(position_ids, "position_ids"); + + // Create new Unsqueeze node for point of branching + auto unsqueeze1_node_copy = + unsqueeze1_node->clone_with_new_inputs({position_ids, unsqueeze1_node->input_value(1)}); + convert_node->input(0).replace_source_output(unsqueeze1_node_copy->output(0)); + + // FIXME: For Gated Short Convolution Block, there is ScatterNDUpdate that also consumes generated + // positions. + // It seems to right to use the newly created `position_ids` for it as well, however, real tests show + // no difference against usage of hardcoded QRange: both are similarly accurate. + auto position_ids_squeezed = std::make_shared( + position_ids, + ov::op::v0::Constant::create(ov::element::i64, ov::Shape{1}, {0})); + OPENVINO_ASSERT(range_node->get_output_size() == 1, "Range node should have exactly one output"); + auto range_consumers = range_node->get_output_target_inputs(0); + for (auto&& consumer : range_consumers) { + if (consumer.get_node()->get_type_name() == std::string("Unsqueeze") && + consumer.get_node() == unsqueeze_node.get()) { + // Preserve original Range for Causal Mask creation + continue; + } + consumer.replace_source_output(position_ids_squeezed->output(0)); + } + + new_params.push_back(position_ids); + return true; + }; + + register_patterns({sin, cos}, std::move(callback)); + } +}; + +#ifdef __GNUC__ +# pragma GCC diagnostic pop +#endif +} // anonymous namespace + +bool ov::npuw::AddPositionIdsParam::run_on_model(const std::shared_ptr& model) { + ov::ParameterVector new_parameters; + ov::pass::Manager manager("add-position-ids-param"); + manager.set_per_pass_validation(false); + manager.register_pass(new_parameters); + manager.run_passes(model); + + model->add_parameters(new_parameters); + model->validate_nodes_and_infer_types(); + return true; +} diff --git a/src/plugins/intel_npu/src/plugin/npuw/npuw_transformations/patch_phi3_sliding_mask.hpp b/src/plugins/intel_npu/src/plugin/npuw/npuw_transformations/add_position_ids_param.hpp similarity index 53% rename from src/plugins/intel_npu/src/plugin/npuw/npuw_transformations/patch_phi3_sliding_mask.hpp rename to src/plugins/intel_npu/src/plugin/npuw/npuw_transformations/add_position_ids_param.hpp index ba884e334a26..b34001c089d7 100644 --- a/src/plugins/intel_npu/src/plugin/npuw/npuw_transformations/patch_phi3_sliding_mask.hpp +++ b/src/plugins/intel_npu/src/plugin/npuw/npuw_transformations/add_position_ids_param.hpp @@ -4,15 +4,13 @@ #pragma once -#include "openvino/pass/pass.hpp" +#include "openvino/openvino.hpp" namespace ov::npuw { -class PatchPhi3SlidingMask : public ov::pass::ModelPass { +class AddPositionIdsParam : public ov::pass::ModelPass { public: - OPENVINO_MODEL_PASS_RTTI("ov::npuw::PatchPhi3SlidingMask"); - explicit PatchPhi3SlidingMask() = default; + OPENVINO_MODEL_PASS_RTTI("ov::npuw::AddPositionIdsParam"); bool run_on_model(const std::shared_ptr& model) override; }; - } // namespace ov::npuw diff --git a/src/plugins/intel_npu/src/plugin/npuw/npuw_transformations/convert_kvcache_to_precision.cpp b/src/plugins/intel_npu/src/plugin/npuw/npuw_transformations/convert_kvcache_to_precision.cpp index a408c5c0a63d..720336a65a85 100644 --- a/src/plugins/intel_npu/src/plugin/npuw/npuw_transformations/convert_kvcache_to_precision.cpp +++ b/src/plugins/intel_npu/src/plugin/npuw/npuw_transformations/convert_kvcache_to_precision.cpp @@ -79,7 +79,6 @@ std::shared_ptr cvt_kvcache_to_low_precision(const std::shared_ptrinputs()) { @@ -112,7 +111,6 @@ std::shared_ptr cvt_kvcache_to_low_precision(const std::shared_ptrget_friendly_name() << "]"); ov::npuw::run_kv_cache_dynamic_quantization_passes(new_model, dq_params); } - return new_model; } diff --git a/src/plugins/intel_npu/src/plugin/npuw/npuw_transformations/kv_cache_compressed.cpp b/src/plugins/intel_npu/src/plugin/npuw/npuw_transformations/kv_cache_compressed.cpp index f841ec0ff84e..9bd57d0d04fc 100644 --- a/src/plugins/intel_npu/src/plugin/npuw/npuw_transformations/kv_cache_compressed.cpp +++ b/src/plugins/intel_npu/src/plugin/npuw/npuw_transformations/kv_cache_compressed.cpp @@ -100,7 +100,8 @@ ov::OutputVector dynamic_quantize_linear(std::shared_ptr input, size_t ov::OutputVector dynamic_quantize_linear_v3(const ov::Output& input, size_t reduction_axis, - const std::string& name_prefix) { + const std::string& name_prefix, + const ov::npuw::util::DynamicQuantStorageTypes& storage_types) { auto make_name = [&name_prefix](const std::string& suffix) { return name_prefix + "/" + suffix; }; @@ -131,13 +132,11 @@ ov::OutputVector dynamic_quantize_linear_v3(const ov::Output& input, auto multiplyInput = std::make_shared(input, cst_255); multiplyInput->set_friendly_name(make_name("Multiply_input_255")); - // ONNX spec: zp = qmin - (minClamped / scale) - auto cst_qmin = ov::op::v0::Constant::create(ov::element::f32, ov::Shape{1}, {-128.0f}); - auto minDivScale = std::make_shared(minClamped, multiplyScale); - minDivScale->set_friendly_name(make_name("Divide_min_by_scale")); + auto minNegated = std::make_shared(cst_zero, minClamped); + minNegated->set_friendly_name(make_name("Negate_min")); - auto zpFloat = std::make_shared(cst_qmin, minDivScale); - zpFloat->set_friendly_name(make_name("Subtract_zp")); + auto zpFloat = std::make_shared(minNegated, multiplyScale); + zpFloat->set_friendly_name(make_name("Divide_min_by_scale")); auto divideSpan = std::make_shared(multiplyInput, subtractSpan); divideSpan->set_friendly_name(make_name("Divide_span")); @@ -148,22 +147,29 @@ ov::OutputVector dynamic_quantize_linear_v3(const ov::Output& input, auto roundSpan = std::make_shared(divideSpan, ov::op::v5::Round::RoundMode::HALF_TO_EVEN); roundSpan->set_friendly_name(make_name("Round_span")); - auto clampZp = std::make_shared(roundZp, -128.0, 127.0); + auto clampZp = std::make_shared(roundZp, 0.0, 255.0); clampZp->set_friendly_name(make_name("Clamp_zp")); auto addQuant = std::make_shared(roundSpan, clampZp); addQuant->set_friendly_name(make_name("Add_quant")); - auto clampOutput = std::make_shared(addQuant, -128.0, 127.0); + auto clampOutput = std::make_shared(addQuant, 0.0, 255.0); clampOutput->set_friendly_name(make_name("Clamp_output")); - auto convertZp = std::make_shared(clampZp, ov::element::i8); + auto convertZp = std::make_shared(clampZp, storage_types.zero_point_type); convertZp->set_friendly_name(make_name("Convert_zp")); - auto convertOutput = std::make_shared(clampOutput, ov::element::i8); + auto convertOutput = std::make_shared(clampOutput, storage_types.quantized_data_type); convertOutput->set_friendly_name(make_name("Convert_output")); - return {convertOutput->output(0), multiplyScale->output(0), convertZp->output(0)}; + std::shared_ptr scaleOutput = multiplyScale; + if (storage_types.scale_type != ov::element::f32) { + auto convertScale = std::make_shared(multiplyScale, storage_types.scale_type); + convertScale->set_friendly_name(make_name("Convert_scale")); + scaleOutput = convertScale; + } + + return {convertOutput->output(0), scaleOutput->output(0), convertZp->output(0)}; } } // anonymous namespace @@ -218,6 +224,18 @@ ov::npuw::DecomposeDynamicQuantize3::DecomposeDynamicQuantize3() { auto dq_ptr = node_to_output.at(dynamic_quantize).get_node_shared_ptr(); auto dq_node = ov::as_type_ptr(dq_ptr); + const auto& attrs = dq_node->get_attrs(); + + if (attrs.quantization_type != ov::op::internal::DynamicQuantize::QuantizationType::Asymmetric || + attrs.quantization_dt != ov::element::i8) { + return false; + } + + const auto storage_types = ov::npuw::util::resolve_dynamic_quant_storage_types( + ov::npuw::util::DynamicQuantDecomposeMode::CompilerPatternI8, + false, + attrs.quantization_dt, + attrs.scale_dt); LOG_DEBUG("Found DynamicQuantize : " << dq_ptr->get_friendly_name() << " decomposing"); LOG_BLOCK(); @@ -230,7 +248,8 @@ ov::npuw::DecomposeDynamicQuantize3::DecomposeDynamicQuantize3() { auto dq_input = dq_ptr->input_value(0); auto has_zp = dq_ptr->outputs().size() == 3; - auto dq_results = dynamic_quantize_linear_v3(dq_input, reduction_axis, dq_ptr->get_friendly_name()); + auto dq_results = + dynamic_quantize_linear_v3(dq_input, reduction_axis, dq_ptr->get_friendly_name(), storage_types); dq_ptr->output(0).replace(dq_results[0]); dq_ptr->output(1).replace(dq_results[1]); @@ -437,8 +456,11 @@ void ov::npuw::run_kv_cache_dynamic_quantization_passes(const std::shared_ptr std::shared_ptr { const std::string kv_name = cacheName(is_key); - const bool is_asym = cfg.quantization_type == KVCacheCompressionConfig::QuantizationType::Asymmetric; + const auto storage_types = ov::npuw::util::resolve_dynamic_quant_storage_types( + ov::npuw::util::DynamicQuantDecomposeMode::HandcraftedSymmetricI8, + !is_asym, + cfg.quantization_dt); auto rank = dq_input.get_partial_shape().size(); std::vector shape_group_size(rank, 1); @@ -452,7 +474,7 @@ void ov::npuw::run_kv_cache_dynamic_quantization_passes(const std::shared_ptr(dq_input, dq_config); @@ -477,6 +499,11 @@ void ov::npuw::run_kv_cache_dynamic_quantization_passes(const std::shared_ptr fp_subtracted_zp = start_node; - if (cfg.quantization_type == KVCacheCompressionConfig::QuantizationType::Asymmetric) { + if (is_asym) { // Subtract zero-point - TODO: share this memory with DynamicQuantize/read/assign? - auto zp = create_parameter_with_name(cfg.quantization_dt, + auto zp = create_parameter_with_name(storage_types.zero_point_type, clear_embedding_index(start_node, isKey), make_dq_param_name("zp")); diff --git a/src/plugins/intel_npu/src/plugin/npuw/npuw_transformations/patch_phi3_sliding_mask.cpp b/src/plugins/intel_npu/src/plugin/npuw/npuw_transformations/patch_phi3_sliding_mask.cpp deleted file mode 100644 index 01d5d78e8e1c..000000000000 --- a/src/plugins/intel_npu/src/plugin/npuw/npuw_transformations/patch_phi3_sliding_mask.cpp +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (C) 2018-2026 Intel Corporation -// SPDX-License-Identifier: Apache-2.0 -// - -#include "patch_phi3_sliding_mask.hpp" - -#include "openvino/pass/manager.hpp" -#include "phi3_sliding_mask.hpp" - -namespace { - -bool patch_phi3_sliding_mask(const std::shared_ptr& model) { - ov::pass::Manager manager; - manager.register_pass(); - return manager.run_passes(model); -} - -} // namespace - -namespace ov::npuw { - -bool PatchPhi3SlidingMask::run_on_model(const std::shared_ptr& model) { - return patch_phi3_sliding_mask(model); -} - -} // namespace ov::npuw diff --git a/src/plugins/intel_npu/src/plugin/npuw/npuw_transformations/patch_sliding_window_mask.cpp b/src/plugins/intel_npu/src/plugin/npuw/npuw_transformations/patch_sliding_window_mask.cpp new file mode 100644 index 000000000000..7ef02f665c55 --- /dev/null +++ b/src/plugins/intel_npu/src/plugin/npuw/npuw_transformations/patch_sliding_window_mask.cpp @@ -0,0 +1,26 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "patch_sliding_window_mask.hpp" + +#include "openvino/pass/manager.hpp" +#include "sliding_window_mask.hpp" + +namespace { + +bool patch_sliding_window_mask(const std::shared_ptr& model) { + ov::pass::Manager manager; + manager.register_pass(); + return manager.run_passes(model); +} + +} // namespace + +namespace ov::npuw { + +bool PatchSlidingWindowMask::run_on_model(const std::shared_ptr& model) { + return patch_sliding_window_mask(model); +} + +} // namespace ov::npuw diff --git a/src/plugins/intel_npu/src/plugin/npuw/npuw_transformations/patch_sliding_window_mask.hpp b/src/plugins/intel_npu/src/plugin/npuw/npuw_transformations/patch_sliding_window_mask.hpp new file mode 100644 index 000000000000..a18d0fdef12a --- /dev/null +++ b/src/plugins/intel_npu/src/plugin/npuw/npuw_transformations/patch_sliding_window_mask.hpp @@ -0,0 +1,20 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#pragma once + +#include "openvino/pass/pass.hpp" + +namespace ov::npuw { + +// Public API for patching sliding window attention masks. +// Applies fixes for Phi-3, Gemma4, and other models with sliding window attention. +class PatchSlidingWindowMask : public ov::pass::ModelPass { +public: + OPENVINO_MODEL_PASS_RTTI("ov::npuw::PatchSlidingWindowMask"); + explicit PatchSlidingWindowMask() = default; + bool run_on_model(const std::shared_ptr& model) override; +}; + +} // namespace ov::npuw diff --git a/src/plugins/intel_npu/src/plugin/npuw/npuw_transformations/phi3_sliding_mask.hpp b/src/plugins/intel_npu/src/plugin/npuw/npuw_transformations/phi3_sliding_mask.hpp deleted file mode 100644 index 56fca1b8bd10..000000000000 --- a/src/plugins/intel_npu/src/plugin/npuw/npuw_transformations/phi3_sliding_mask.hpp +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (C) 2018-2026 Intel Corporation -// SPDX-License-Identifier: Apache-2.0 -// - -#pragma once - -#include "openvino/pass/pass.hpp" - -namespace ov::npuw { - -class Phi3SlidingMask : public ov::pass::ModelPass { -public: - OPENVINO_MODEL_PASS_RTTI("ov::npuw::Phi3SlidingMask"); - Phi3SlidingMask() = default; - bool run_on_model(const std::shared_ptr& model) override; -}; - -} // namespace ov::npuw \ No newline at end of file diff --git a/src/plugins/intel_npu/src/plugin/npuw/npuw_transformations/reshape_to_static.cpp b/src/plugins/intel_npu/src/plugin/npuw/npuw_transformations/reshape_to_static.cpp index 3491d8b2258f..17c55cdb4694 100644 --- a/src/plugins/intel_npu/src/plugin/npuw/npuw_transformations/reshape_to_static.cpp +++ b/src/plugins/intel_npu/src/plugin/npuw/npuw_transformations/reshape_to_static.cpp @@ -8,6 +8,7 @@ #include "../logging.hpp" #include "../util.hpp" #include "openvino/core/partial_shape.hpp" +#include "openvino/op/add.hpp" namespace { @@ -33,17 +34,19 @@ void reshape_to_static(std::shared_ptr model, new_shape = ov::PartialShape({1, input_size, input.get_partial_shape()[2]}); } else if (input_name.find("attention_mask") != std::string::npos) { new_shape = ov::PartialShape({1, kvcache_size}); - if (lhs_seq_size && kvcache_size > 4) + if (lhs_seq_size && !is_prefill) // NB: for whisper kvcache model attn mask should be size + 1 new_shape = ov::PartialShape({1, kvcache_size + 1}); } else if (input_name.find("position_ids") != std::string::npos) { const auto partial_shape_size = input.get_partial_shape().size(); - // NB: Regular LLM uses 2D shapes, Qwen2.5 VL/Omni uses 3D shapes + // NB: Regular LLM uses 2D shapes, Qwen2.5 VL/Omni, Qwen3.5 VL use 3D shapes // The first dimension (3) represents the three components of position encoding: time, height, and width // enabling alignment across multimodal inputs like text, audio, and video + // Update: Qwen3.5 has first dimension = 4. NPUW_ASSERT(partial_shape_size == 3u || partial_shape_size == 2u); - new_shape = - partial_shape_size == 3u ? ov::PartialShape({3, 1, input_size}) : ov::PartialShape({1, input_size}); + auto first_dim_value = input.get_partial_shape()[0]; + new_shape = partial_shape_size == 3u ? ov::PartialShape({first_dim_value, 1, input_size}) + : ov::PartialShape({1, input_size}); } else if (input_name.find("cache_position") != std::string::npos) { // NB: Whisper case new_shape = ov::PartialShape({1}); @@ -61,6 +64,55 @@ void reshape_to_static(std::shared_ptr model, new_shape = ov::PartialShape({input.get_partial_shape()[0], lora_rank}); } else if (ov::npuw::util::matchLoRAMatMulBString(input_name)) { new_shape = ov::PartialShape({input.get_partial_shape()[0], lora_rank}); + } else if (input_name.find("per_layer_inputs") != std::string::npos) { + // NB: Gemma4 per-layer embedding feature. + // Shape is [batch, seq_len, num_layers, projection_dim]. + // seq_len (dim 1) should match input_size (tokens being processed), + // while num_layers (dim 2) and projection_dim (dim 3) are model constants. + // These may be dynamic in the parameter's partial_shape itself, so fall back to + // reading them from the sibling input of the Add node that consumes this parameter + // (the other branch always carries a fully static shape, e.g. f32[1,S,42,256]). + const auto& partial_shape = input.get_partial_shape(); + NPUW_ASSERT(partial_shape.size() == 4u); + ov::Dimension num_layers = partial_shape[2]; + ov::Dimension proj_dim = partial_shape[3]; + if (!num_layers.is_static() || !proj_dim.is_static()) { + for (const auto& target : input.get_target_inputs()) { + const auto* node = target.get_node(); + if (!ov::is_type(node)) { + continue; + } + for (size_t i = 0; i < node->get_input_size(); ++i) { + if (i == target.get_index()) { + continue; + } + const auto sibling_shape = node->input(i).get_partial_shape(); + if (sibling_shape.size() != 4u) { + continue; + } + if (!num_layers.is_static() && sibling_shape[2].is_static()) { + num_layers = sibling_shape[2]; + } + if (!proj_dim.is_static() && sibling_shape[3].is_static()) { + proj_dim = sibling_shape[3]; + } + } + if (num_layers.is_static() && proj_dim.is_static()) { + break; + } + } + } + NPUW_ASSERT(num_layers.is_static()); // num_layers must be resolved + NPUW_ASSERT(proj_dim.is_static()); // projection_dim must be resolved + new_shape = ov::PartialShape({1, input_size, num_layers, proj_dim}); + } else if (ov::npuw::util::matchLinCacheString(input_name)) { + const auto& partial_shape = input.get_partial_shape(); + new_shape = partial_shape; + // NOTE: Batch axes of KVCache and Linear Cache have same positions, however + // need to track that this assumption holds in future versions. + const auto& shape_batch_dim = partial_shape[kv_axes_position.batch]; + NPUW_ASSERT(shape_batch_dim.is_dynamic() || shape_batch_dim.get_length() <= 1); + new_shape[kv_axes_position.batch] = 1; // batch_dim } else { const auto& partial_shape = input.get_partial_shape(); new_shape = partial_shape; diff --git a/src/plugins/intel_npu/src/plugin/npuw/npuw_transformations/phi3_sliding_mask.cpp b/src/plugins/intel_npu/src/plugin/npuw/npuw_transformations/sliding_window_mask.cpp similarity index 57% rename from src/plugins/intel_npu/src/plugin/npuw/npuw_transformations/phi3_sliding_mask.cpp rename to src/plugins/intel_npu/src/plugin/npuw/npuw_transformations/sliding_window_mask.cpp index 85d3d661c5ba..745c902123ff 100644 --- a/src/plugins/intel_npu/src/plugin/npuw/npuw_transformations/phi3_sliding_mask.cpp +++ b/src/plugins/intel_npu/src/plugin/npuw/npuw_transformations/sliding_window_mask.cpp @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 // -#include "phi3_sliding_mask.hpp" +#include "sliding_window_mask.hpp" #include "../logging.hpp" #include "openvino/op/ops.hpp" @@ -22,6 +22,119 @@ namespace { # pragma GCC diagnostic ignored "-Wattributes" #endif +// ============================================================================ +// Shared helper: Rebuild sliding window attention mask for static KV buffers +// with different paddings. +// ============================================================================ +// This function implements the common 4-step transformation used by +// Phi3, Gemma2, Gemma3 or Gemma4 sliding window attention patterns: +// +// 1. (K > pos_ids - window) & (K <= Q_range) +// -- Use temporal position_ids for the left window bound to handle right-padded +// past tokens, while preserving the causal mask for present tokens. +// 2. (K > Q_range - window) | (K < past_kv_len) +// -- Bound present tokens by window size while always allowing past tokens. +// 3. Result = 1 & 2 +// -- Together they form the correct sliding window mask for past and present +// tokens and the causal mask for present tokens. +// 4. Clean = 3 & attention_mask[past_kv_len:].T +// -- Remove padding artifacts. +void rebuild_sliding_window_mask(const std::shared_ptr& attention_mask_node_ptr, + const std::shared_ptr& position_ids_node_ptr, + const std::shared_ptr& matched_past_kv_len, + const std::shared_ptr& matched_full_ctx_len, + const std::shared_ptr& matched_key_range_row, + const std::shared_ptr& matched_neg_window_size, + const std::shared_ptr& matched_sliding_mask, + const std::shared_ptr& matched_sliding_and_causal_mask, + const char* log_prefix) { + auto neg_window_size_const = std::static_pointer_cast(matched_neg_window_size); + OPENVINO_ASSERT(neg_window_size_const->get_output_size() == 1, + "Sliding window size constant must be of size 1, but got " + + std::to_string(neg_window_size_const->get_output_size())); + + OPENVINO_ASSERT(attention_mask_node_ptr, "Passed attention_mask node is nullptr!"); + OPENVINO_ASSERT(position_ids_node_ptr, "Passed position_ids node is nullptr!"); + + // Create constants + auto const_zero = std::make_shared(ov::element::i64, ov::Shape{}, 0); + + // ======================================================================= + // STEP 1: (K > pos_ids - window) & (K <= Q_range) + // Use temporal position_ids for the left window bound to correctly handle + // right-padded past tokens, while preserving the causal mask for present tokens. + // ======================================================================= + std::shared_ptr query_range_as_pos_ids = position_ids_node_ptr; + if (neg_window_size_const->output(0).get_element_type() == ov::element::f32) { + query_range_as_pos_ids = std::make_shared(position_ids_node_ptr, ov::element::f32); + } + auto query_range_as_pos_ids_unsqueezed = + std::make_shared(query_range_as_pos_ids, const_zero); + auto const_three = std::make_shared(ov::element::i64, ov::Shape{}, 3); + auto query_range_as_pos_ids_col = + std::make_shared(query_range_as_pos_ids_unsqueezed, const_three); + auto query_range_as_pos_left_bound = + std::make_shared(query_range_as_pos_ids_col, matched_neg_window_size); + auto sliding_mask_for_right_padding = + std::make_shared(matched_key_range_row, query_range_as_pos_left_bound); + matched_sliding_and_causal_mask->input(0).replace_source_output(sliding_mask_for_right_padding); + + // ======================================================================= + // STEP 2: (K > Q_range - window) | (K < past_kv_len) + // Bound present tokens by window size while always allowing + // past prefill tokens. + // ======================================================================= + std::shared_ptr past_kv_len_argument = matched_past_kv_len; + if (neg_window_size_const->output(0).get_element_type() == ov::element::f32) { + past_kv_len_argument = std::make_shared(matched_past_kv_len, ov::element::f32); + } + auto only_past_tokens_mask = std::make_shared(matched_key_range_row, past_kv_len_argument); + auto sliding_mask_for_left_padding_or_only_past = + std::make_shared(matched_sliding_mask, only_past_tokens_mask); + + // ======================================================================= + // STEP 3: Result = 1 & 2 + // ======================================================================= + // NB: target_inputs must be captured BEFORE new_sliding_and_causal_mask is + // created, because creating it registers it as a consumer of + // matched_sliding_and_causal_mask. Capturing after would include the new + // node in target_inputs and cause a graph cycle when replacing outputs. + auto target_inputs = matched_sliding_and_causal_mask->output(0).get_target_inputs(); + auto new_sliding_and_causal_mask = + std::make_shared(matched_sliding_and_causal_mask, + sliding_mask_for_left_padding_or_only_past); + + // ======================================================================= + // STEP 4: Clean = 3 & attention_mask[past_kv_len:].T + // Remove padding artifacts. + // ======================================================================= + std::vector shape_rank_one{1}; + auto shape_rank_one_const = std::make_shared(ov::element::i64, ov::Shape{1}, shape_rank_one); + auto past_len_reshaped = std::make_shared(matched_past_kv_len, shape_rank_one_const, false); + auto full_ctx_len_reshaped = + std::make_shared(matched_full_ctx_len, shape_rank_one_const, false); + auto const_one_rank_one = std::make_shared(ov::element::i64, ov::Shape{1}, 1); + auto attention_mask_bool = std::make_shared(attention_mask_node_ptr, ov::element::boolean); + auto present_atten_mask_bool = std::make_shared(attention_mask_bool, + past_len_reshaped, + full_ctx_len_reshaped, + const_one_rank_one, + const_one_rank_one); + std::vector vector_shape{-1, 1}; + auto vector_shape_const = std::make_shared(ov::element::i64, ov::Shape{2}, vector_shape); + auto present_atten_mask_bool_col = + std::make_shared(present_atten_mask_bool, vector_shape_const, false); + auto clean_sliding_and_causal_mask = + std::make_shared(new_sliding_and_causal_mask, present_atten_mask_bool_col); + + // Replace all target inputs + for (auto&& input : target_inputs) { + input.replace_source_output(clean_sliding_and_causal_mask); + } + + LOG_INFO(std::string(log_prefix) + " sliding window attention mask pattern found and patched."); +} + class OldPhi3SlidingMaskMatcher : public ov::pass::MatcherPass { public: OPENVINO_MATCHER_PASS_RTTI("ov::npuw::patterns::OldPhi3SlidingMaskMatcher"); @@ -226,7 +339,6 @@ class Phi3SlidingMaskMatcher : public ov::pass::MatcherPass { // past tokens and left-padded present tokens. Logic to replace pattern is the same // as in Phi3SlidingMask rewriter, but adjusted to another set of operations for // creation of mask, obtained from transformers 4.53. - // Pattern is the same as in GemmaSlidingMask. // // Fix is a replace of following pattern: // 1. (K range > (Q range - sliding window).T) & (K range <= Q range.T) @@ -272,98 +384,130 @@ class Phi3SlidingMaskMatcher : public ov::pass::MatcherPass { auto sliding_and_causal_mask = opp::wrap_type({sliding_and_true, causal_mask}); auto callback = [=](opp::Matcher& m) { - LOG_INFO("Found (4.53) pattern for Phi-3 Sliding Window Attention, will be replaced with custom for static " - "shapes."); auto& node_to_output = m.get_pattern_value_map(); + + // Extract matched nodes from pattern auto optional_squeeze = node_to_output.find(past_kv_len_squeeze); auto matched_past_kv_len = optional_squeeze != node_to_output.end() ? optional_squeeze->second.get_node_shared_ptr() : node_to_output.at(past_kv_len).get_node_shared_ptr(); auto matched_full_ctx_len = node_to_output.at(full_ctx_len).get_node_shared_ptr(); - auto node_neg_window_size = node_to_output.at(neg_window_size).get_node_shared_ptr(); + auto matched_neg_window_size = node_to_output.at(neg_window_size).get_node_shared_ptr(); auto matched_sliding_mask = node_to_output.at(sliding_mask).get_node_shared_ptr(); auto matched_sliding_and_causal_mask = node_to_output.at(sliding_and_causal_mask).get_node_shared_ptr(); auto optional_convert = node_to_output.find(opt_key_range_row_f32); - std::shared_ptr matched_key_range_row = - optional_convert != node_to_output.end() ? optional_convert->second.get_node_shared_ptr() - : node_to_output.at(key_range_row).get_node_shared_ptr(); - auto matched_neg_window_size = std::static_pointer_cast(node_neg_window_size); - OPENVINO_ASSERT(matched_neg_window_size->get_output_size() == 1, - "Sliding window size constant must be of size 1, but got " + - std::to_string(matched_neg_window_size->get_output_size())); + auto matched_key_range_row = optional_convert != node_to_output.end() + ? optional_convert->second.get_node_shared_ptr() + : node_to_output.at(key_range_row).get_node_shared_ptr(); + + // Call shared transformation helper + rebuild_sliding_window_mask(attention_mask_node_ptr, + position_ids_node_ptr, + matched_past_kv_len, + matched_full_ctx_len, + matched_key_range_row, + matched_neg_window_size, + matched_sliding_mask, + matched_sliding_and_causal_mask, + "Phi-3, Gemma-2, Gemma-3"); + return true; + }; + register_matcher(std::make_shared(sliding_and_causal_mask, "Phi3SlidingMaskMatcher"), + std::move(callback)); + } +}; - std::shared_ptr passed_attention_mask = attention_mask_node_ptr; - std::shared_ptr passed_position_ids = position_ids_node_ptr; - OPENVINO_ASSERT(passed_attention_mask, "Passed attention_mask node is nullptr!"); - OPENVINO_ASSERT(passed_position_ids, "Passed position_ids node is nullptr!"); +class Gemma4SlidingMaskMatcher : public ov::pass::MatcherPass { +public: + OPENVINO_MATCHER_PASS_RTTI("ov::npuw::patterns::Gemma4SlidingMaskMatcher"); - auto const_zero = std::make_shared(ov::element::i64, ov::Shape{}, 0); - auto const_one = std::make_shared(ov::element::i64, ov::Shape{}, 1); - auto const_three = std::make_shared(ov::element::i64, ov::Shape{}, 3); + Gemma4SlidingMaskMatcher(const std::shared_ptr& attention_mask_node_ptr, + const std::shared_ptr& position_ids_node_ptr) { + // Fix Gemma-4 sliding window attention mask for static KV + // buffers with different paddings for past and present tokens. + // + // Gemma-4's generate model computes the Q range (cache_position) as: + // q_range = Range(0, seq_len, 1) -- seq_len is the statically-compiled Q + // -- sequence length (1 for standard generate) + // cache_pos = Add(q_range, past_kv_len) -- Q position in the full KV buffer + // q_idx = Unsqueeze x3(cache_pos) -- shape [1,1,seq_len,1] + // + // Unlike Phi-3's Range(past_kv_len, past_kv_len + seq_len), Gemma-4 uses an + // explicit Add, causing the pattern in Phi3SlidingMaskMatcher to miss it. + // + // With static KV buffers, the Q position >> actual temporal position_id, so + // SWA layers incorrectly exclude past prefill tokens (distance > sliding_window). + // + // Fix is identical to Phi3SlidingMaskMatcher: + // 1. (K > pos_ids - window) & (K <= Q_range) + // -- temporal position_ids for the left window bound; causal mask for present tokens + // 2. (K > Q_range - window) | (K < past_kv_len) + // -- bound present tokens by window size; always allow past tokens + // 3. Result = 1 & 2 + // -- correct sliding window + causal mask for past and present tokens + // 4. Clean = 3 & attention_mask[past_kv_len:].T + // -- remove padding artifacts + auto unsqueeze_sequence = [&](std::shared_ptr node) { + auto u1 = opp::wrap_type({node, opp::any_input()}); + auto u2 = opp::wrap_type({u1, opp::any_input()}); + auto u3 = opp::wrap_type({u2, opp::any_input()}); + return u3; + }; - // 1.(K range > (Q_pos range - sliding window).T) & (K range <= Q range.T) - std::shared_ptr query_range_as_pos_ids = passed_position_ids; - if (matched_neg_window_size->output(0).get_element_type() == ov::element::f32) { - query_range_as_pos_ids = std::make_shared(passed_position_ids, ov::element::f32); - } - auto query_range_as_pos_ids_unsqueezed = - std::make_shared(query_range_as_pos_ids, const_zero); - auto query_range_as_pos_ids_col = - std::make_shared(query_range_as_pos_ids_unsqueezed, const_three); - auto query_range_as_pos_left_bound = - std::make_shared(query_range_as_pos_ids_col, matched_neg_window_size); - auto sliding_mask_for_right_padding = - std::make_shared(matched_key_range_row, query_range_as_pos_left_bound); - matched_sliding_and_causal_mask->input(0).replace_source_output(sliding_mask_for_right_padding); - - // 2. (K range > (Q range - sliding window).T) | (K range < shape(past_key_values, 2)) - std::shared_ptr past_kv_len_argument = matched_past_kv_len; - if (matched_neg_window_size->output(0).get_element_type() == ov::element::f32) { - past_kv_len_argument = std::make_shared(matched_past_kv_len, ov::element::f32); - } - auto only_past_tokens_mask = - std::make_shared(matched_key_range_row, past_kv_len_argument); - auto sliding_mask_for_left_padding_or_only_past = - std::make_shared(matched_sliding_mask, only_past_tokens_mask); + // K (key) side: Range(0, past_kv_len + seq_len, 1) + auto past_kv_len = opp::wrap_type({opp::any_input(), opp::any_input(), opp::any_input()}); + auto past_kv_len_squeeze = opp::optional({past_kv_len}); + auto zero_const = opp::wrap_type(); + auto full_ctx_len = opp::wrap_type({opp::any_input(), past_kv_len_squeeze}); + auto key_range = opp::wrap_type({zero_const, full_ctx_len, opp::any_input()}); + auto key_range_row = unsqueeze_sequence(key_range); + auto opt_key_range_f32 = opp::optional({key_range_row->output(0)}); - // 3. Result = 1 & 2 - // Save target inputs first: - auto target_inputs = matched_sliding_and_causal_mask->output(0).get_target_inputs(); - auto new_sliding_and_causal_mask = - std::make_shared(matched_sliding_and_causal_mask, - sliding_mask_for_left_padding_or_only_past); + // Q (query) side: Gemma4-specific -- cache_pos = Range(0, seq_len) + past_kv_len + auto q_range = opp::wrap_type({opp::any_input(), opp::any_input(), opp::any_input()}); + auto cache_position = opp::wrap_type({q_range, past_kv_len_squeeze}); + auto query_range_column = unsqueeze_sequence(cache_position); - // 4. Removing extra padding via : 3 & attention_mask_input[past_kv_len:].T - std::vector shape_rank_one{1}; - auto shape_rank_one_const = - std::make_shared(ov::element::i64, ov::Shape{1}, shape_rank_one); - auto past_len_reshaped = - std::make_shared(matched_past_kv_len, shape_rank_one_const, false); - auto full_ctx_len_reshaped = - std::make_shared(matched_full_ctx_len, shape_rank_one_const, false); - auto const_one_rank_one = std::make_shared(ov::element::i64, ov::Shape{1}, 1); - auto attention_mask_bool = - std::make_shared(passed_attention_mask, ov::element::boolean); - auto present_atten_mask_bool = std::make_shared(attention_mask_bool, - past_len_reshaped, - full_ctx_len_reshaped, - const_one_rank_one, - const_one_rank_one); - std::vector vector_shape{-1, 1}; - auto vector_shape_const = - std::make_shared(ov::element::i64, ov::Shape{2}, vector_shape); - auto present_atten_mask_bool_col = - std::make_shared(present_atten_mask_bool, vector_shape_const, false); - auto clean_sliding_and_causal_mask = - std::make_shared(new_sliding_and_causal_mask, present_atten_mask_bool_col); - for (auto&& input : target_inputs) { - input.replace_source_output(clean_sliding_and_causal_mask); - } + // Sliding window & causal masks (positive form: 1 = attend) + auto neg_window_size = opp::wrap_type(); + auto query_left_bound = opp::wrap_type({query_range_column, neg_window_size}); + auto sliding_mask = opp::wrap_type({opt_key_range_f32, query_left_bound}); + auto sliding_and_true = opp::wrap_type({opp::any_input(), sliding_mask}); + auto causal_mask = opp::wrap_type({opt_key_range_f32, query_range_column}); + auto sliding_and_causal_mask = opp::wrap_type({sliding_and_true, causal_mask}); + + auto callback = [=](opp::Matcher& m) { + auto& node_to_output = m.get_pattern_value_map(); + // Extract matched nodes from pattern + auto optional_squeeze = node_to_output.find(past_kv_len_squeeze); + auto matched_past_kv_len = optional_squeeze != node_to_output.end() + ? optional_squeeze->second.get_node_shared_ptr() + : node_to_output.at(past_kv_len).get_node_shared_ptr(); + auto matched_full_ctx_len = node_to_output.at(full_ctx_len).get_node_shared_ptr(); + auto matched_neg_window_size = node_to_output.at(neg_window_size).get_node_shared_ptr(); + auto matched_sliding_mask = node_to_output.at(sliding_mask).get_node_shared_ptr(); + auto matched_sliding_and_causal_mask = node_to_output.at(sliding_and_causal_mask).get_node_shared_ptr(); + + auto optional_convert = node_to_output.find(opt_key_range_f32); + auto matched_key_range_row = optional_convert != node_to_output.end() + ? optional_convert->second.get_node_shared_ptr() + : node_to_output.at(key_range_row).get_node_shared_ptr(); + + // Call shared transformation helper + rebuild_sliding_window_mask(attention_mask_node_ptr, + position_ids_node_ptr, + matched_past_kv_len, + matched_full_ctx_len, + matched_key_range_row, + matched_neg_window_size, + matched_sliding_mask, + matched_sliding_and_causal_mask, + "Gemma4"); return true; }; - register_matcher(std::make_shared(sliding_and_causal_mask, "Phi3SlidingMaskMatcher"), + register_matcher(std::make_shared(sliding_and_causal_mask, "Gemma4SlidingMaskMatcher"), std::move(callback)); } }; @@ -376,7 +520,7 @@ class Phi3SlidingMaskMatcher : public ov::pass::MatcherPass { namespace ov::npuw { -bool Phi3SlidingMask::run_on_model(const std::shared_ptr& model) { +bool SlidingWindowMask::run_on_model(const std::shared_ptr& model) { std::shared_ptr attention_mask_node_ptr = nullptr; std::shared_ptr position_ids_node_ptr = nullptr; for (const auto& i : model->inputs()) { @@ -401,6 +545,7 @@ bool Phi3SlidingMask::run_on_model(const std::shared_ptr& model) { ov::pass::Manager manager; manager.set_per_pass_validation(true); const auto rewriter = manager.register_pass(); + rewriter->add_matcher(attention_mask_node_ptr, position_ids_node_ptr); rewriter->add_matcher(attention_mask_node_ptr, position_ids_node_ptr); rewriter->add_matcher(); return manager.run_passes(model); diff --git a/src/plugins/intel_npu/src/plugin/npuw/npuw_transformations/sliding_window_mask.hpp b/src/plugins/intel_npu/src/plugin/npuw/npuw_transformations/sliding_window_mask.hpp new file mode 100644 index 000000000000..e6660df3ffc9 --- /dev/null +++ b/src/plugins/intel_npu/src/plugin/npuw/npuw_transformations/sliding_window_mask.hpp @@ -0,0 +1,20 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#pragma once + +#include "openvino/pass/pass.hpp" + +namespace ov::npuw { + +// Fixes sliding window attention mask for static KV buffers with different paddings +// in LLM models. Supports Phi-3 (transformers 4.51 & 4.53), Gemma2, Gemma3, Gemma4 patterns. +class SlidingWindowMask : public ov::pass::ModelPass { +public: + OPENVINO_MODEL_PASS_RTTI("ov::npuw::SlidingWindowMask"); + SlidingWindowMask() = default; + bool run_on_model(const std::shared_ptr& model) override; +}; + +} // namespace ov::npuw diff --git a/src/plugins/intel_npu/src/plugin/npuw/npuw_transformations/split_kvcache_into_blocks.cpp b/src/plugins/intel_npu/src/plugin/npuw/npuw_transformations/split_kvcache_into_blocks.cpp new file mode 100644 index 000000000000..ba0b2228b278 --- /dev/null +++ b/src/plugins/intel_npu/src/plugin/npuw/npuw_transformations/split_kvcache_into_blocks.cpp @@ -0,0 +1,213 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "split_kvcache_into_blocks.hpp" + +#include + +#include "../logging.hpp" +#include "../util.hpp" +#include "openvino/core/graph_util.hpp" +#include "openvino/core/node.hpp" +#include "openvino/core/rt_info.hpp" +#include "openvino/op/concat.hpp" +#include "openvino/op/convert.hpp" +#include "openvino/op/parameter.hpp" + +namespace ov { +namespace npuw { +namespace pass { + +namespace { + +// Information collected in the scan phase; all fields are pre-computed so the +// transform phase never needs to re-query the parameter name. +struct KVCacheTransformInfo { + std::shared_ptr param; + std::shared_ptr concat; + ov::Output present_kv_input; + std::shared_ptr convert_node; // nullptr when Parameter→Concat directly + bool is_key; // true = K tensor + bool is_value; // true = V tensor + int64_t concat_axis; // sequence dimension index in the concat +}; + +// Walk a parameter's consumers to find the Concat it feeds (directly or via Convert). +// Returns {concat, convert_node}; concat is nullptr if the pattern is not found. +std::pair, std::shared_ptr> find_concat_for_param( + const std::shared_ptr& param) { + for (const auto& output : param->outputs()) { + for (const auto& target : output.get_target_inputs()) { + auto node = target.get_node()->shared_from_this(); + if (auto concat = ov::as_type_ptr(node)) { + return {concat, nullptr}; + } + if (ov::is_type(node)) { + for (const auto& cvt_out : node->outputs()) { + for (const auto& cvt_target : cvt_out.get_target_inputs()) { + auto next = cvt_target.get_node()->shared_from_this(); + if (auto concat = ov::as_type_ptr(next)) { + return {concat, node}; + } + } + } + } + } + } + return {nullptr, nullptr}; +} + +// Build a 4D block shape. seq_dim is the axis holding the sequence (2 or 3); +// all other dims are copied from orig_shape. +ov::Shape make_block_shape(const ov::PartialShape& orig_shape, int64_t seq_dim, size_t seq_size) { + ov::Shape s(4); + for (int i = 0; i < 4; ++i) { + s[i] = (i == seq_dim) ? seq_size : static_cast(orig_shape[i].get_length()); + } + return s; +} + +} // namespace + +SplitKVCacheIntoBlocks::SplitKVCacheIntoBlocks(uint32_t block_size, bool v_transposed) + : m_block_size(block_size), + m_v_transposed(v_transposed) {} + +bool SplitKVCacheIntoBlocks::run_on_model(const std::shared_ptr& model) { + bool model_changed = false; + + // --- Phase 1: Scan — collect parameters that need to be split ---------- + std::vector params_to_transform; + + for (const auto& param : model->get_parameters()) { + const std::string& name = param->get_friendly_name(); + + const bool is_key = ov::npuw::util::isPastKeyValuesKeyContiguous(name).has_value(); + const bool is_value = ov::npuw::util::isPastKeyValuesValueContiguous(name).has_value(); + if (!is_key && !is_value) { + continue; // not a contiguous KV cache parameter + } + + // Shape must be 4D and fully static before we can proceed. + const auto& orig_shape = param->get_partial_shape(); + if (orig_shape.rank().is_dynamic() || orig_shape.rank().get_length() != 4) { + LOG_WARN("SplitKVCacheIntoBlocks: Skipping " << name << " — expected 4D shape, got " << orig_shape); + continue; + } + if (!orig_shape[0].is_static() || !orig_shape[1].is_static() || !orig_shape[2].is_static() || + !orig_shape[3].is_static()) { + LOG_WARN("SplitKVCacheIntoBlocks: Skipping " << name << " — dynamic dimensions not supported"); + continue; + } + + // Locate the Concat this parameter feeds (directly or via Convert). + auto [concat, convert_node] = find_concat_for_param(param); + if (!concat) { + continue; + } + + // Locate the "present_kv" input — the non-param input of the Concat. + ov::Output present_kv_input; + bool found = false; + for (size_t i = 0; i < concat->get_input_size(); ++i) { + auto src = concat->input(i).get_source_output().get_node_shared_ptr(); + bool from_param = (src == param) || (ov::is_type(src) && + src->input(0).get_source_output().get_node_shared_ptr() == param); + if (!from_param) { + present_kv_input = concat->input(i).get_source_output(); + found = true; + break; + } + } + if (!found) { + continue; + } + + // Pre-compute the concat axis (sequence dimension) once. + const int64_t concat_axis = (is_key || (is_value && !m_v_transposed)) ? 2 : 3; + + params_to_transform.push_back({param, concat, present_kv_input, convert_node, is_key, is_value, concat_axis}); + } + + // --- Phase 2: Transform — replace each collected parameter with blocks -- + for (auto& info : params_to_transform) { + auto& param = info.param; + auto& concat = info.concat; + + const auto& orig_shape = param->get_partial_shape(); + const int64_t seq_len = orig_shape[info.concat_axis].get_length(); + const uint32_t num_full_blocks = static_cast(seq_len) / m_block_size; + const uint32_t tail_size = static_cast(seq_len) % m_block_size; + const uint32_t total_blocks = num_full_blocks + (tail_size > 0 ? 1 : 0); + + LOG_INFO("SplitKVCacheIntoBlocks: Transforming " << param->get_friendly_name() << " shape=" << orig_shape + << " → " << total_blocks << " blocks (tail=" << tail_size + << ")"); + + // Build block Parameter nodes. + ov::OutputVector block_outputs; + std::vector> new_params; + block_outputs.reserve(total_blocks); + new_params.reserve(total_blocks); + + auto make_block_param = [&](const std::string& suffix, size_t seq_size) { + auto block_shape = make_block_shape(orig_shape, info.concat_axis, seq_size); + auto p = std::make_shared(param->get_element_type(), block_shape); + const std::string block_name = param->get_friendly_name() + suffix; + p->set_friendly_name(block_name); + p->output(0).set_names({block_name}); + return p; + }; + + for (uint32_t i = 0; i < num_full_blocks; ++i) { + new_params.push_back(make_block_param("_block_" + std::to_string(i), m_block_size)); + block_outputs.push_back(new_params.back()); + } + if (tail_size > 0) { + new_params.push_back(make_block_param("_block_tail", tail_size)); + block_outputs.push_back(new_params.back()); + } + + model->add_parameters(new_params); + + // If the original path had a Convert, insert one after each block param. + ov::OutputVector inputs_for_concat; + inputs_for_concat.reserve(total_blocks + 1); + + if (info.convert_node) { + const auto target_type = info.convert_node->get_output_element_type(0); + for (const auto& blk : block_outputs) { + auto cvt = std::make_shared(blk, target_type); + cvt->set_friendly_name(blk.get_node()->get_friendly_name() + "_convert"); + inputs_for_concat.push_back(cvt); + } + } else { + inputs_for_concat = block_outputs; + } + inputs_for_concat.push_back(info.present_kv_input); + + // Replace the old Concat. + auto new_concat = std::make_shared(inputs_for_concat, info.concat_axis); + new_concat->set_friendly_name(concat->get_friendly_name()); + + ov::NodeVector new_nodes{new_concat}; + for (const auto& p : new_params) { + new_nodes.push_back(p); + } + copy_runtime_info({param, concat}, new_nodes); + + concat->output(0).replace(new_concat->output(0)); + model->remove_parameter(param); + + LOG_DEBUG("SplitKVCacheIntoBlocks: new concat shape " << new_concat->get_output_partial_shape(0)); + + model_changed = true; + } + + return model_changed; +} + +} // namespace pass +} // namespace npuw +} // namespace ov diff --git a/src/plugins/intel_npu/src/plugin/npuw/npuw_transformations/split_kvcache_into_blocks.hpp b/src/plugins/intel_npu/src/plugin/npuw/npuw_transformations/split_kvcache_into_blocks.hpp new file mode 100644 index 000000000000..9126614e4890 --- /dev/null +++ b/src/plugins/intel_npu/src/plugin/npuw/npuw_transformations/split_kvcache_into_blocks.hpp @@ -0,0 +1,73 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#pragma once + +#include "openvino/pass/pass.hpp" + +namespace ov { +namespace npuw { +namespace pass { + +/** + * @brief Transformation pass to split KV cache access into block-based pattern + * + * This pass identifies KV cache parameters in the model and transforms their access + * pattern from continuous memory (batch, num_heads, seq_len, head_dim) to block-based + * layout (num_blocks, batch, num_heads, block_size, head_dim). + * + * Transformation pattern: + * Before: + * Parameter(past_key) [1, 32, 2048, 128] \ + * Parameter(present_key) [1, 32, 1, 128] |-> Concat(axis=2) -> SDPA + * + * Parameter(past_value) [1, 32, 128, 2048] \ + * Parameter(present_value)[1, 32, 128, 1] |-> Concat(axis=3) -> SDPA (if v_transposed=true) + * + * After (block_size=1024, seq_len=2048): + * Parameter(past_key_block_0) [1, 32, 1024, 128] \ + * Parameter(past_key_block_1) [1, 32, 1024, 128] | + * Parameter(present_key) [1, 32, 1, 128] |-> Concat(axis=2) -> SDPA + * + * Parameter(past_value_block_0) [1, 32, 128, 1024] \ + * Parameter(past_value_block_1) [1, 32, 128, 1024] | + * Parameter(present_value) [1, 32, 128, 1] |-> Concat(axis=3) -> SDPA (if v_transposed=true) + * + * Note: Number of blocks is auto-calculated from original seq_len and block_size. + * If seq_len % block_size != 0, a tail block with remaining tokens is created. + * + * Benefits: + * - Reduces memory allocation (only allocate needed blocks) + * - Enables memory sharing across sequences + * - Improves memory locality for attention operations + * + * Note: This transformation is applied to both prefill and generate (decode) models + * where KV cache parameters are identified by naming convention + * (e.g., "past_key", "past_value", "past_key_values.X.key", "past_key_values.X.value"). + */ +class SplitKVCacheIntoBlocks : public ov::pass::ModelPass { +public: + OPENVINO_RTTI("ov::npuw::pass::SplitKVCacheIntoBlocks", "0"); + + /** + * @brief Construct transformation with block configuration + * + * @param block_size Number of tokens per block (default: 1024 for efficiency) + * @param v_transposed Whether V tensor is transposed (true: [B,H,D,S], false: [B,H,S,D]) + * + * The number of blocks is automatically calculated from the original past_key shape. + * If the sequence length is not evenly divisible by block_size, a tail block is created. + */ + explicit SplitKVCacheIntoBlocks(uint32_t block_size = 1024, bool v_transposed = true); + + bool run_on_model(const std::shared_ptr& model) override; + +private: + uint32_t m_block_size; + bool m_v_transposed; +}; + +} // namespace pass +} // namespace npuw +} // namespace ov diff --git a/src/plugins/intel_npu/src/plugin/npuw/orc.cpp b/src/plugins/intel_npu/src/plugin/npuw/orc.cpp new file mode 100644 index 000000000000..3b7aa71db1d7 --- /dev/null +++ b/src/plugins/intel_npu/src/plugin/npuw/orc.cpp @@ -0,0 +1,496 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "orc.hpp" + +#include +#include +#include +#include +#include + +namespace { + +constexpr std::array ORC_FILE_MAGIC = {'N', 'P', 'U', 'W', 'O', 'R', 'C', '\0'}; +constexpr std::uint16_t ORC_FILE_VERSION = 0u; + +std::streampos checked_tellp(std::ostream& stream, const char* context) { + const auto pos = stream.tellp(); + if (pos == std::streampos(-1)) { + OPENVINO_THROW("ORC export requires a seekable output stream for ", context); + } + return pos; +} + +std::streampos checked_tellg(std::istream& stream, const char* context) { + const auto pos = stream.tellg(); + if (pos == std::streampos(-1)) { + OPENVINO_THROW("ORC import requires a seekable input stream for ", context); + } + return pos; +} + +std::streamsize checked_stream_size(const std::size_t size) { + if (size > static_cast(std::numeric_limits::max())) { + OPENVINO_THROW("ORC size exceeds std::streamsize range"); + } + return static_cast(size); +} + +std::size_t checked_size(const std::uint64_t size) { + if (size > static_cast(std::numeric_limits::max())) { + OPENVINO_THROW("ORC section size exceeds std::size_t range"); + } + return static_cast(size); +} + +std::uint64_t checked_add(const std::uint64_t lhs, const std::uint64_t rhs) { + if (rhs > std::numeric_limits::max() - lhs) { + OPENVINO_THROW("ORC section size overflow"); + } + return lhs + rhs; +} + +constexpr std::uint64_t header_size() { + // Section header layout: type_id (u16) + version (u16) + flags (u32) + payload_size (u64) + return sizeof(ov::npuw::orc::TypeId) + sizeof(ov::npuw::orc::Version) + sizeof(ov::npuw::orc::SectionFlags) + + sizeof(std::uint64_t); +} + +void validate_section_shape(const ov::npuw::orc::Section& section) { + if (section.is_container()) { + if (!section.payload.empty()) { + OPENVINO_THROW("ORC container sections cannot store a raw payload"); + } + return; + } + + if (!section.children.empty()) { + OPENVINO_THROW("ORC raw sections cannot store nested child sections"); + } +} + +std::uint64_t encoded_size(const ov::npuw::orc::Section& section); + +std::uint64_t body_size(const ov::npuw::orc::Section& section) { + validate_section_shape(section); + if (!section.is_container()) { + return section.payload.size(); + } + + std::uint64_t size = 0u; + for (const auto& child : section.children) { + size = checked_add(size, encoded_size(child)); + } + return size; +} + +std::uint64_t encoded_size(const ov::npuw::orc::Section& section) { + return checked_add(header_size(), body_size(section)); +} + +void write_section(ov::npuw::orc::Stream& stream, const ov::npuw::orc::Section& section) { + validate_section_shape(section); + + ov::npuw::orc::SectionHeader header; + header.type = section.type; + header.version = section.version; + header.flags = section.flags; + header.size = body_size(section); + stream & header.type & header.version & header.flags & header.size; + + if (!section.is_container()) { + if (!section.payload.empty()) { + stream.bytes(const_cast(section.payload.data()), section.payload.size()); + } + return; + } + + for (const auto& child : section.children) { + write_section(stream, child); + } +} + +ov::npuw::orc::Section read_section(ov::npuw::orc::Stream& stream) { + ov::npuw::orc::SectionHeader header; + stream & header.type & header.version & header.flags & header.size; + + ov::npuw::orc::Section section; + section.type = header.type; + section.version = header.version; + section.flags = header.flags; + + const auto size = checked_size(header.size); + if (!section.is_container()) { + section.payload.resize(size); + if (size != 0u) { + stream.bytes(section.payload.data(), size); + } + return section; + } + + std::vector bytes(size); + if (size != 0u) { + stream.bytes(bytes.data(), size); + } + auto nested = ov::npuw::orc::Stream::memory_reader(bytes.data(), bytes.size()); + while (nested.remaining() != 0u) { + section.children.push_back(read_section(nested)); + } + return section; +} + +} // namespace + +ov::npuw::orc::Stream ov::npuw::orc::Stream::reader(std::istream& stream) { + Stream s; + s.m_input = &stream; + return s; +} + +void ov::npuw::orc::serialize(Stream& stream, Section& section) { + if (stream.output()) { + write_section(stream, section); + } else { + section = read_section(stream); + } +} + +void ov::npuw::orc::serialize(Stream& stream, SectionHeader& header) { + stream & header.type & header.version & header.flags & header.size; +} + +ov::npuw::orc::Stream ov::npuw::orc::Stream::memory_reader(const void* data, const std::size_t size) { + Stream s; + s.m_memory = reinterpret_cast(data); + s.m_memory_size = size; + return s; +} + +ov::npuw::orc::Stream ov::npuw::orc::Stream::writer(std::ostream& stream) { + Stream s; + s.m_output = &stream; + return s; +} + +bool ov::npuw::orc::try_read_bytes(std::istream& stream, void* data, const std::size_t size) { + const auto saved = stream.tellg(); + stream.read(reinterpret_cast(data), checked_stream_size(size)); + if (stream.good()) { + return true; + } + + stream.clear(); + stream.seekg(saved); + return false; +} + +bool ov::npuw::orc::Stream::input() const { + return m_input != nullptr || m_memory != nullptr; +} + +bool ov::npuw::orc::Stream::output() const { + return m_output != nullptr; +} + +bool ov::npuw::orc::Stream::memory() const { + return m_memory != nullptr; +} + +std::size_t ov::npuw::orc::Stream::remaining() const { + if (!memory()) { + OPENVINO_THROW("ORC remaining() is only available for memory-backed input streams"); + } + return m_memory_size - m_memory_offset; +} + +void ov::npuw::orc::Stream::bytes(void* data, const std::size_t size) { + if (output()) { + m_output->write(reinterpret_cast(data), checked_stream_size(size)); + if (!m_output->good()) { + OPENVINO_THROW("Failed to write ORC stream bytes"); + } + return; + } + + if (memory()) { + if (size > remaining()) { + OPENVINO_THROW("Unexpected end of ORC memory stream"); + } + if (size != 0u) { + std::memcpy(data, m_memory + m_memory_offset, size); + m_memory_offset += size; + } + return; + } + + m_input->read(reinterpret_cast(data), checked_stream_size(size)); + if (!m_input->good()) { + OPENVINO_THROW("Unexpected end of ORC input stream"); + } +} + +void ov::npuw::orc::serialize(Stream& stream, std::string& value) { + if (stream.output()) { + auto size = value.size(); + stream & size; + if (!value.empty()) { + stream.bytes(value.data(), value.size()); + } + return; + } + + std::size_t size = 0u; + stream & size; + value.resize(size); + if (size != 0u) { + stream.bytes(value.data(), value.size()); + } +} + +ov::npuw::orc::Section ov::npuw::orc::Section::raw(const TypeId type, + const Version version, + std::vector payload, + const SectionFlags flags) { + Section section; + section.type = type; + section.version = version; + section.flags = flags | SectionFlag::LEAF; + section.payload = std::move(payload); + return section; +} + +ov::npuw::orc::Section ov::npuw::orc::Section::container(const TypeId type, + const Version version, + std::vector
children, + const SectionFlags flags) { + Section section; + section.type = type; + section.version = version; + section.flags = flags; // LEAF not set; this is a structural container section + section.children = std::move(children); + return section; +} + +void ov::npuw::orc::write_file_header(std::ostream& stream, const SchemaUUID& uuid) { + auto writer = Stream::writer(stream); + + auto magic = ORC_FILE_MAGIC; + writer & magic; + auto version = ORC_FILE_VERSION; + writer & version; + auto uuid_copy = uuid; + writer & uuid_copy; +} + +ov::npuw::orc::OrcHeader ov::npuw::orc::read_file_header(std::istream& stream) { + const auto header = is_orc(stream); + if (!header) { + OPENVINO_THROW("Not an ORC file (invalid magic)"); + } + if (header->version != ORC_FILE_VERSION) { + OPENVINO_THROW("Unsupported ORC file version ", header->version); + } + + constexpr auto skip = + static_cast(ORC_FILE_MAGIC.size() + sizeof(ORC_FILE_VERSION) + sizeof(SchemaUUID)); + stream.seekg(skip, std::ios::cur); + if (!stream.good()) { + OPENVINO_THROW("Failed to seek past ORC file header"); + } + return *header; +} + +void ov::npuw::orc::write_file(std::ostream& stream, const Section& root, const SchemaUUID& uuid) { + write_file_header(stream, uuid); + auto writer = Stream::writer(stream); + write_section(writer, root); +} + +ov::npuw::orc::Section ov::npuw::orc::read_file(std::istream& stream) { + read_file_header(stream); + auto reader = Stream::reader(stream); + return read_section(reader); +} + +std::streamsize ov::npuw::orc::checked_stream_size(const std::size_t size) { + return ::checked_stream_size(size); +} + +std::size_t ov::npuw::orc::checked_size(const std::uint64_t size) { + return ::checked_size(size); +} + +std::streamoff ov::npuw::orc::checked_streamoff(const std::uint64_t size) { + if (size > static_cast(std::numeric_limits::max())) { + OPENVINO_THROW("ORC section size exceeds std::streamoff range"); + } + return static_cast(size); +} + +ov::npuw::orc::ScopedWriteSection::ScopedWriteSection(std::ostream& stream, + const TypeId type, + const Version version, + const SectionFlags flags) + : m_stream(stream) { + SectionHeader header; + header.type = type; + header.version = version; + header.flags = flags; + header.size = 0u; + + auto writer = Stream::writer(stream); + writer & header.type & header.version & header.flags; + m_size_pos = checked_tellp(stream, "section size patching"); + writer & header.size; + m_body_pos = checked_tellp(stream, "section body tracking"); +} + +void ov::npuw::orc::ScopedWriteSection::close() { + if (m_closed) { + return; + } + + const auto end_pos = checked_tellp(m_stream, "section size patching"); + const auto body_size = end_pos - m_body_pos; + if (body_size < 0) { + OPENVINO_THROW("ORC section body size underflow"); + } + + const auto restore_pos = end_pos; + m_stream.seekp(m_size_pos); + if (!m_stream.good()) { + OPENVINO_THROW("Failed to seek to ORC section size field"); + } + + auto writer = Stream::writer(m_stream); + auto size = static_cast(body_size); + writer & size; + m_stream.seekp(restore_pos); + if (!m_stream.good()) { + OPENVINO_THROW("Failed to restore ORC output position after patching section size"); + } + m_closed = true; +} + +ov::npuw::orc::ScopedReadSection::ScopedReadSection(std::istream& stream) : m_stream(stream) { + auto reader = Stream::reader(stream); + reader & m_header; + m_end = checked_tellg(stream, "section bounds") + checked_streamoff(m_header.size); +} + +const ov::npuw::orc::SectionHeader& ov::npuw::orc::ScopedReadSection::header() const { + return m_header; +} + +bool ov::npuw::orc::ScopedReadSection::done() const { + return checked_tellg(m_stream, "section iteration") >= m_end; +} + +std::size_t ov::npuw::orc::ScopedReadSection::remaining() const { + const auto pos = checked_tellg(m_stream, "section remaining"); + if (pos > m_end) { + OPENVINO_THROW("ORC stream advanced past the end of the current section"); + } + return checked_size(static_cast(m_end - pos)); +} + +void ov::npuw::orc::ScopedReadSection::expect_end() const { + if (!done() || checked_tellg(m_stream, "section completion") != m_end) { + OPENVINO_THROW("Malformed ORC section contents"); + } +} + +std::string ov::npuw::orc::ScopedReadSection::read_remaining_blob() { + std::string blob(remaining(), '\0'); + if (!blob.empty()) { + m_stream.read(blob.data(), checked_stream_size(blob.size())); + if (!m_stream.good()) { + OPENVINO_THROW("Unexpected end of ORC input stream"); + } + } + return blob; +} + +bool ov::npuw::orc::Schema::knows(const TypeId type) const { + return m_entries.count(type) != 0u; +} + +std::any ov::npuw::orc::Schema::load_any(const Section& section) const { + const auto it = m_entries.find(section.type); + if (it == m_entries.end()) { + OPENVINO_THROW("No ORC loader registered for section type ID ", section.type); + } + return it->second.loader(section, *this); +} + +std::vector ov::npuw::orc::Schema::load_children(const Section& container) const { + if (!container.is_container()) { + OPENVINO_THROW("ORC child loading requires a container section"); + } + + std::vector out; + out.reserve(container.children.size()); + for (const auto& child : container.children) { + if (!knows(child.type)) { + if (child.is_optional()) { + continue; + } + OPENVINO_THROW("Required ORC child section type ID ", child.type, " is not registered in the schema"); + } + out.push_back({child.type, load_any(child)}); + } + return out; +} + +const ov::npuw::orc::Section* ov::npuw::orc::Schema::find_child(const Section& container, const TypeId type) const { + if (!container.is_container()) { + OPENVINO_THROW("ORC child lookup requires a container section"); + } + + const auto it = std::find_if(container.children.begin(), container.children.end(), [type](const Section& child) { + return child.type == type; + }); + return it == container.children.end() ? nullptr : &(*it); +} + +const ov::npuw::orc::Section& ov::npuw::orc::Schema::require_child(const Section& container, const TypeId type) const { + const auto* child = find_child(container, type); + if (child == nullptr) { + OPENVINO_THROW("Required ORC child section type ID ", type, " is missing"); + } + return *child; +} + +std::optional ov::npuw::orc::is_orc(std::istream& stream) { + const auto saved = stream.tellg(); + const auto restore = [&] { + stream.clear(); + stream.seekg(saved); + }; + + std::array magic{}; + stream.read(reinterpret_cast(magic.data()), magic.size()); + if (!stream.good() || magic != ORC_FILE_MAGIC) { + restore(); + return std::nullopt; + } + + std::uint16_t version = 0u; + stream.read(reinterpret_cast(&version), sizeof(version)); + if (!stream.good()) { + restore(); + return std::nullopt; + } + + SchemaUUID uuid{}; + stream.read(reinterpret_cast(uuid.data()), uuid.size()); + if (!stream.good()) { + restore(); + return std::nullopt; + } + + restore(); + return OrcHeader{version, uuid}; +} diff --git a/src/plugins/intel_npu/src/plugin/npuw/orc.hpp b/src/plugins/intel_npu/src/plugin/npuw/orc.hpp new file mode 100644 index 000000000000..93aebbe4b2f5 --- /dev/null +++ b/src/plugins/intel_npu/src/plugin/npuw/orc.hpp @@ -0,0 +1,558 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "openvino/core/except.hpp" + +namespace ov { +namespace npuw { +namespace orc { + +using TypeId = std::uint16_t; +using Version = std::uint16_t; +using SectionFlags = std::uint64_t; +using SchemaUUID = std::array; + +// Reserved cross-schema type ID for the metadata leaf of any container section. +// Every container that carries its own serialized fields before child sections +// wraps those fields in a META leaf child (type = META_SECTION_TYPE, LEAF flag +// set). The container's own deserializer knows what fields to read; the type +// ID is schema-neutral so all containers share the same reserved slot. +// Type IDs 1–19 are reserved for future ORC infrastructure use. +constexpr TypeId META_SECTION_TYPE = 20u; + +enum class SectionFlag : SectionFlags { + OPTIONAL = 1ull << 0, + LEAF = 1ull << 1, // payload contains raw bytes only, no child sections + ENCRYPTED = 1ull << 2, +}; + +constexpr SectionFlags operator|(SectionFlag lhs, SectionFlag rhs) { + return static_cast(lhs) | static_cast(rhs); +} + +constexpr SectionFlags operator|(SectionFlags lhs, SectionFlag rhs) { + return lhs | static_cast(rhs); +} + +constexpr bool has_flag(SectionFlags flags, SectionFlag flag) { + return (flags & static_cast(flag)) != 0ull; +} + +class Stream; + +namespace detail { + +template +struct has_member_serialize : std::false_type {}; + +template +struct has_member_serialize().serialize(std::declval()))>> + : std::true_type {}; + +} // namespace detail + +class Stream { +public: + static Stream reader(std::istream& stream); + static Stream memory_reader(const void* data, std::size_t size); + static Stream writer(std::ostream& stream); + + bool input() const; + bool output() const; + bool memory() const; + std::size_t remaining() const; + + // Dispatches to value.serialize(*this) when the type provides a member serialize, + // otherwise falls back to the free-function serialize(stream, value) found via ADL. + template + Stream& operator&(T&& value) { + using plain_type = std::remove_const_t>; + auto& v = const_cast(value); + if constexpr (detail::has_member_serialize::value) { + v.serialize(*this); + } else { + serialize(*this, v); + } + return *this; + } + + void bytes(void* data, std::size_t size); + +private: + Stream() = default; + + std::istream* m_input = nullptr; + std::ostream* m_output = nullptr; + const std::byte* m_memory = nullptr; + std::size_t m_memory_size = 0u; + std::size_t m_memory_offset = 0u; +}; + +bool try_read_bytes(std::istream& stream, void* data, std::size_t size); + +template ::value || std::is_floating_point::value, bool> = true> +void serialize(Stream& stream, T& value) { + stream.bytes(&value, sizeof(value)); +} + +inline void serialize(Stream& stream, std::byte& value) { + stream.bytes(&value, sizeof(value)); +} + +void serialize(Stream& stream, std::string& value); + +template +void serialize(Stream& stream, std::pair& value) { + stream & value.first & value.second; +} + +template +void serialize(Stream& stream, std::vector& value) { + if (stream.output()) { + auto size = value.size(); + stream & size; + for (std::size_t idx = 0; idx < value.size(); ++idx) { + if constexpr (std::is_same_v) { + bool element = value[idx]; + stream & element; + } else { + stream& value[idx]; + } + } + return; + } + + value.clear(); + std::size_t size = 0u; + stream & size; + value.reserve(size); + for (std::size_t idx = 0; idx < size; ++idx) { + T element{}; + stream & element; + value.push_back(std::move(element)); + } +} + +template +void serialize(Stream& stream, std::array& value) { + for (auto& element : value) { + stream & element; + } +} + +template +void serialize(Stream& stream, std::optional& value) { + bool has_value = value.has_value(); + stream & has_value; + if (!has_value) { + value.reset(); + return; + } + + if (stream.output()) { + stream & value.value(); + return; + } + + T unpacked{}; + stream & unpacked; + value = std::move(unpacked); +} + +template +void serialize(Stream& stream, std::map& value) { + if (stream.output()) { + auto size = value.size(); + stream & size; + for (auto& el : value) { + auto pair = el; + stream & pair; + } + return; + } + + value.clear(); + std::size_t size = 0u; + stream & size; + for (std::size_t idx = 0u; idx < size; ++idx) { + std::pair elem{}; + stream & elem; + value[elem.first] = std::move(elem.second); + } +} + +template +void serialize(Stream& stream, std::unordered_set& value) { + if (stream.output()) { + auto size = value.size(); + stream & size; + for (auto el : value) { + stream & el; + } + return; + } + + value.clear(); + std::size_t size = 0u; + stream & size; + for (std::size_t idx = 0u; idx < size; ++idx) { + T elem{}; + stream & elem; + value.insert(std::move(elem)); + } +} + +template +void serialize(Stream& stream, std::unordered_set& value) { + if (stream.output()) { + auto size = value.size(); + stream & size; + for (auto el : value) { + stream & el; + } + return; + } + + value.clear(); + std::size_t size = 0u; + stream & size; + for (std::size_t idx = 0u; idx < size; ++idx) { + T elem{}; + stream & elem; + value.insert(std::move(elem)); + } +} + +template +std::vector encode(const T& value) { + std::stringstream buffer(std::ios::in | std::ios::out | std::ios::binary); + auto stream = Stream::writer(buffer); + auto& mutable_value = const_cast&>(value); + stream & mutable_value; + + const auto raw = buffer.str(); + std::vector out(raw.size()); + if (!raw.empty()) { + std::memcpy(out.data(), raw.data(), raw.size()); + } + return out; +} + +template +T decode(const std::vector& bytes) { + auto stream = Stream::memory_reader(bytes.data(), bytes.size()); + T out{}; + stream & out; + if (stream.remaining() != 0u) { + OPENVINO_THROW("ORC payload has trailing bytes"); + } + return out; +} + +struct SectionHeader { + TypeId type = 0u; + Version version = 0u; + SectionFlags flags = 0ull; + std::uint64_t size = 0u; +}; + +void serialize(Stream& stream, SectionHeader& header); + +struct Section { + TypeId type = 0u; + Version version = 0u; + SectionFlags flags = 0ull; + std::vector payload; + std::vector
children; + + bool is_optional() const { + return has_flag(flags, SectionFlag::OPTIONAL); + } + + bool is_leaf() const { + return has_flag(flags, SectionFlag::LEAF); + } + + bool is_container() const { + return !is_leaf(); + } + + static Section raw(TypeId type, Version version, std::vector payload, SectionFlags flags = 0ull); + static Section container(TypeId type, Version version, std::vector
children, SectionFlags flags = 0ull); +}; + +template +Section make_payload_section(TypeId type, Version version, const T& value, SectionFlags flags = 0ull) { + return Section::raw(type, version, encode(value), flags); +} + +// Serialize an ORC section (header + body) inline into a stream. +// On output: writes the section wire format. +// On input: reads and reconstructs a Section. +void serialize(Stream& stream, Section& section); + +// File-level metadata returned by is_orc(). +struct OrcHeader { + std::uint16_t version = 0u; + SchemaUUID schema_uuid{}; +}; + +void write_file_header(std::ostream& stream, const SchemaUUID& uuid); +OrcHeader read_file_header(std::istream& stream); +void write_file(std::ostream& stream, const Section& root, const SchemaUUID& uuid); +Section read_file(std::istream& stream); + +std::streamsize checked_stream_size(std::size_t size); +std::size_t checked_size(std::uint64_t size); +std::streamoff checked_streamoff(std::uint64_t size); + +class ScopedWriteSection { +public: + ScopedWriteSection(std::ostream& stream, TypeId type, Version version, SectionFlags flags = 0ull); + ScopedWriteSection(const ScopedWriteSection&) = delete; + ScopedWriteSection& operator=(const ScopedWriteSection&) = delete; + + void close(); + +private: + std::ostream& m_stream; + std::streampos m_size_pos{}; + std::streampos m_body_pos{}; + bool m_closed = false; +}; + +template +void with_section(std::ostream& stream, TypeId type, Version version, SectionFlags flags, Writer&& writer) { + ScopedWriteSection section(stream, type, version, flags); + writer(); + section.close(); +} + +template +void with_leaf_section(std::ostream& stream, TypeId type, Version version, Writer&& writer, SectionFlags flags = 0ull) { + ScopedWriteSection section(stream, type, version, flags | SectionFlag::LEAF); + writer(); + section.close(); +} + +class ScopedReadSection { +public: + explicit ScopedReadSection(std::istream& stream); + + const SectionHeader& header() const; + bool done() const; + std::size_t remaining() const; + void expect_end() const; + std::string read_remaining_blob(); + +private: + std::istream& m_stream; + SectionHeader m_header{}; + std::streampos m_end{}; +}; + +// Peeks at the stream to check whether it contains an ORC blob. +// Returns the file header (format_version + schema_uuid) on success, +// nullopt if the magic does not match. +// Does not consume any bytes — the stream position is restored on return. +std::optional is_orc(std::istream& stream); + +class Schema { +public: + using Loader = std::function; + + struct LoadedChild { + TypeId type = 0u; + std::any value; + }; + + template + void register_loader(TypeId type, LoaderT&& loader) { + if (m_entries.count(type) != 0u) { + OPENVINO_THROW("ORC schema already has a loader for type ID ", type); + } + + Entry entry; + entry.type = std::type_index(typeid(T)); + entry.loader = [fn = std::forward(loader)](const Section& section, const Schema& schema) -> std::any { + return std::any(fn(section, schema)); + }; + m_entries.emplace(type, std::move(entry)); + } + + bool knows(TypeId type) const; + std::any load_any(const Section& section) const; + + template + T load(const Section& section) const { + auto value = load_any(section); + auto* typed = std::any_cast(&value); + if (typed == nullptr) { + OPENVINO_THROW("ORC loader returned unexpected type for section type ID ", section.type); + } + return std::move(*typed); + } + + std::vector load_children(const Section& container) const; + const Section* find_child(const Section& container, TypeId type) const; + const Section& require_child(const Section& container, TypeId type) const; + +private: + struct Entry { + std::type_index type = std::type_index(typeid(void)); + Loader loader; + }; + + std::unordered_map m_entries; +}; + +template +struct has_prev : std::false_type {}; + +template +struct has_prev> : std::true_type {}; + +template +Target migrate_chain(Source source) { + if constexpr (std::is_same_v>, + std::remove_cv_t>>) { + return Target(std::move(source)); + } else { + static_assert(has_prev::value, "Target must define Prev to use migrate_chain"); + return Target(migrate_chain(std::move(source))); + } +} + +namespace detail { + +template +struct has_version : std::false_type {}; + +template +struct has_version> : std::true_type {}; + +template +Target load_versioned_payload_impl(const Section& section) { + static_assert(has_version::value, "Versioned ORC payload types must define kVersion"); + if (section.version == Current::kVersion) { + return migrate_chain(decode(section.payload)); + } + OPENVINO_THROW("Unsupported ORC section version ", section.version, " for type ID ", section.type); +} + +template +Target load_versioned_payload_impl(const Section& section) { + static_assert(has_version::value, "Versioned ORC payload types must define kVersion"); + if (section.version == Current::kVersion) { + return migrate_chain(decode(section.payload)); + } + return load_versioned_payload_impl(section); +} + +} // namespace detail + +// Multi-arg form: explicit version list — use as an escape hatch when the +// Prev chain is insufficient (e.g., legacy types without a frozen V0 struct, +// or migration logic too complex to express as typed constructors). +// Requires at least one version type (First) so this overload is unambiguous +// with the single-arg form when Target is the only template argument. +template +Target load_versioned_payload(const Section& section) { + if (section.is_container()) { + OPENVINO_THROW("Cannot decode a container section as a raw ORC payload"); + } + return detail::load_versioned_payload_impl(section); +} + +namespace detail { + +// Walk the Prev chain of Current (oldest → newest) to find a matching version, +// then migrate up to Target via migrate_chain. Requires Target to define Prev +// pointing at its immediately preceding frozen type. +template +Target load_versioned_payload_chain_impl(const Section& section) { + static_assert(has_version::value, "All versioned ORC types must define kVersion"); + if (section.version == Current::kVersion) { + return migrate_chain(decode(section.payload)); + } + if constexpr (has_prev::value) { + return load_versioned_payload_chain_impl(section); + } + OPENVINO_THROW("Unsupported ORC section version ", section.version, " for type ID ", section.type); +} + +} // namespace detail + +// Single-arg form: version list is derived automatically from the Target's Prev chain. +// +// For V1+ types, Target must define: +// using Prev = ; // updated when a new version ships +// static constexpr Version kVersion = ...; // via ORC_DECLARE_VERSION +// The chain is walked at compile time: Target::Prev → Prev::Prev → … → V0. +// Call site stays permanently neutral: load_versioned_payload(section). +// +// For V0 types (no Prev, kVersion=0): decodes directly — no chain needed. +template +Target load_versioned_payload(const Section& section) { + static_assert(detail::has_version::value, "Target must define kVersion"); + if (section.is_container()) { + OPENVINO_THROW("Cannot decode a container section as a raw ORC payload"); + } + if constexpr (has_prev::value) { + return detail::load_versioned_payload_chain_impl(section); + } else { + if (section.version != Target::kVersion) { + OPENVINO_THROW("Unsupported ORC section version ", section.version, " for type ID ", section.type); + } + return decode(section.payload); + } +} + +} // namespace orc +} // namespace npuw +} // namespace ov + +// Injects the standard versioning boilerplate into a frozen versioned type. +// PrevType must already define kVersion; ThisType must be the enclosing struct name. +// The base (V0) type defines kVersion = 0 manually — ORC_DECLARE_VERSION is only +// used for V1 and later. +// +// Example: +// struct V0 { +// static constexpr Version kVersion = 0; +// int x = 0; +// void serialize(Stream& stream) { stream & x; } +// }; +// struct V1 : V0 { +// ORC_DECLARE_VERSION(V1, V0) // kVersion = 1 +// int y = 0; +// void serialize(Stream& stream) { Prev::serialize(stream); stream & y; } +// }; +#define ORC_DECLARE_VERSION(ThisType, PrevType) \ + using Prev = PrevType; \ + static constexpr ::ov::npuw::orc::Version kVersion = 1u + Prev::kVersion; \ + ThisType() = default; \ + explicit ThisType(PrevType _prev_init) : PrevType(std::move(_prev_init)) {} diff --git a/src/plugins/intel_npu/src/plugin/npuw/orc/schema_npuw.hpp b/src/plugins/intel_npu/src/plugin/npuw/orc/schema_npuw.hpp new file mode 100644 index 000000000000..756b2785ed64 --- /dev/null +++ b/src/plugins/intel_npu/src/plugin/npuw/orc/schema_npuw.hpp @@ -0,0 +1,28 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#pragma once + +#include "../orc.hpp" + +namespace ov::npuw::orc::schema_npuw { + +inline constexpr SchemaUUID NPUW_ORC_PARTITIONED_SCHEMA = + {0x4E, 0x50, 0x55, 0x57, 0x43, 0x4D, 0x4F, 0x44, 0x50, 0x48, 0x41, 0x53, 0x45, 0x30, 0x30, 0x31}; + +// Keep the top-level on-wire type IDs centralized so future schemas can reuse +// the same registry without accidental collisions. +enum class PartitionedModel : TypeId { + ID = 100, +}; + +enum class Subgraph : TypeId { + ID = 200, +}; + +enum class WeightsBank : TypeId { + ID = 300, +}; + +} // namespace ov::npuw::orc::schema_npuw diff --git a/src/plugins/intel_npu/src/plugin/npuw/partitioning/online/compiler.cpp b/src/plugins/intel_npu/src/plugin/npuw/partitioning/online/compiler.cpp index 68d9f66ac578..3916d32aa17c 100644 --- a/src/plugins/intel_npu/src/plugin/npuw/partitioning/online/compiler.cpp +++ b/src/plugins/intel_npu/src/plugin/npuw/partitioning/online/compiler.cpp @@ -197,6 +197,7 @@ class Compiler { m_snapshot->earlyAvoids(); m_snapshot->earlyRegroup(); m_snapshot->repeatedBlocks(); + m_snapshot->fuseUnfolded(); m_snapshot->repeat([&] { m_snapshot->fuseRemnantsExtended(); }); @@ -215,6 +216,7 @@ class Compiler { // NB: the "fake" tag is stripped elsewhere, that's how it works m_snapshot->stripTag("compute"); }); + m_snapshot->fuseUnfolded(); m_snapshot->repeat([&] { m_snapshot->fuseRemnantsExtended(); }); @@ -223,11 +225,14 @@ class Compiler { } public: - Compiler(const std::shared_ptr& model, ::intel_npu::Config& cfg) + Compiler(const std::shared_ptr& model, + ::intel_npu::Config& cfg, + const ov::npuw::v1::subgraphs::PatternRegistry* subgraph_patterns) : m_model(model), m_snapshot(std::make_shared(model)), m_cfg(cfg) { PassContext ctx(m_cfg); + ctx.subgraph_patterns = subgraph_patterns; if (currentPipeline() == Pipeline::NONE && ctx.avoids.empty()) { none_fast(); @@ -351,10 +356,12 @@ class Compiler { } // namespace ov // TODO: decouple configuration for partitioning from the plugin's cfg. -ov::npuw::Ensemble ov::npuw::online::buildPartitioning(const std::shared_ptr& model, - ::intel_npu::Config& cfg) { +ov::npuw::Ensemble ov::npuw::online::buildPartitioning( + const std::shared_ptr& model, + ::intel_npu::Config& cfg, + const ov::npuw::v1::subgraphs::PatternRegistry* subgraph_patterns) { // Creates compiler and runs partitioning algorithm. - ov::npuw::online::Compiler partitioner(model, cfg); + ov::npuw::online::Compiler partitioner(model, cfg, subgraph_patterns); // Convert groups formed after passes into plugin-compatible data structure. return partitioner.getPartitioning(); diff --git a/src/plugins/intel_npu/src/plugin/npuw/partitioning/online/compiler.hpp b/src/plugins/intel_npu/src/plugin/npuw/partitioning/online/compiler.hpp index a07eec70d972..dd0ad57ce006 100644 --- a/src/plugins/intel_npu/src/plugin/npuw/partitioning/online/compiler.hpp +++ b/src/plugins/intel_npu/src/plugin/npuw/partitioning/online/compiler.hpp @@ -14,7 +14,9 @@ namespace ov { namespace npuw { namespace online { -ov::npuw::Ensemble buildPartitioning(const std::shared_ptr& model, ::intel_npu::Config& cfg); +ov::npuw::Ensemble buildPartitioning(const std::shared_ptr& model, + ::intel_npu::Config& cfg, + const ov::npuw::v1::subgraphs::PatternRegistry* subgraph_patterns = nullptr); } // namespace online } // namespace npuw diff --git a/src/plugins/intel_npu/src/plugin/npuw/partitioning/online/graph.cpp b/src/plugins/intel_npu/src/plugin/npuw/partitioning/online/graph.cpp index d9bf20259c4b..a71cecc14d12 100644 --- a/src/plugins/intel_npu/src/plugin/npuw/partitioning/online/graph.cpp +++ b/src/plugins/intel_npu/src/plugin/npuw/partitioning/online/graph.cpp @@ -1,4 +1,3 @@ -// // Copyright (C) 2018-2026 Intel Corporation // SPDX-License-Identifier: Apache-2.0 // @@ -93,8 +92,8 @@ own::ade::EdgeHandle own::ade::Graph::link(const own::ade::NodeHandle& src, cons auto edge = std::make_shared(src, dst); own::ade::EdgeHandle eh{edge}; m_edges.emplace(edge.get(), MetaPtr{edge, own::ade::Meta{}}); - src->m_dst_edges.insert(eh); - dst->m_src_edges.insert(eh); + src->m_dst_edges.insert(own::ade::EdgeHandle(eh)); + dst->m_src_edges.insert(own::ade::EdgeHandle(eh)); dst->src_nodes_cache_dirty = true; return eh; } @@ -147,7 +146,7 @@ std::vector own::ade::Graph::nodes() const { void own::ade::Graph::dfs(own::ade::NodeHandle& nh, std::unordered_set& visited, std::stack& stack) const { - visited.insert(nh); + visited.insert(own::ade::NodeHandle(nh)); auto dst_nodes = nh->dstNodes(); // FIXME: this was introduced to make the graph diff --git a/src/plugins/intel_npu/src/plugin/npuw/partitioning/online/graph.hpp b/src/plugins/intel_npu/src/plugin/npuw/partitioning/online/graph.hpp index 6f83a95c1c66..8a358aade2d4 100644 --- a/src/plugins/intel_npu/src/plugin/npuw/partitioning/online/graph.hpp +++ b/src/plugins/intel_npu/src/plugin/npuw/partitioning/online/graph.hpp @@ -1,4 +1,3 @@ -// // Copyright (C) 2018-2026 Intel Corporation // SPDX-License-Identifier: Apache-2.0 // diff --git a/src/plugins/intel_npu/src/plugin/npuw/partitioning/online/group.cpp b/src/plugins/intel_npu/src/plugin/npuw/partitioning/online/group.cpp index 76c46973e76d..b7c7b1fdbff2 100644 --- a/src/plugins/intel_npu/src/plugin/npuw/partitioning/online/group.cpp +++ b/src/plugins/intel_npu/src/plugin/npuw/partitioning/online/group.cpp @@ -32,9 +32,9 @@ Group::Group(const std::shared_ptr& node, m_id(gid), m_graph(g), m_snapshot(snapshot) { - m_input_layers.insert(node); - m_output_layers.insert(node); - m_content.insert(node); + m_input_layers.insert(std::shared_ptr(node)); + m_output_layers.insert(std::shared_ptr(node)); + m_content.insert(std::shared_ptr(node)); } Group::Group(size_t gid, @@ -57,14 +57,14 @@ void Group::includeExtraLayers(detail::OVNodeSet& input_layers, auto target_input = layer->get_input_source_output(i); auto layer_parent = target_input.get_node()->shared_from_this(); if (ov::op::util::is_parameter(layer_parent)) { - input_layers.insert(layer); + input_layers.insert(std::shared_ptr(layer)); } // Also include Converts if (!isOp(layer_parent)) { if (!ov::op::util::is_constant(layer_parent) && !ov::op::util::is_parameter(layer_parent) && !ov::op::util::is_output(layer_parent)) { NPUW_ASSERT(ov::is_type(layer_parent)); - extra_content.insert(layer_parent); + extra_content.insert(std::shared_ptr(layer_parent)); } } } @@ -73,14 +73,14 @@ void Group::includeExtraLayers(detail::OVNodeSet& input_layers, for (const auto& target_output : target_outputs) { auto layer_child = target_output.get_node()->shared_from_this(); if (ov::op::util::is_output(layer_child)) { - output_layers.insert(layer); + output_layers.insert(std::shared_ptr(layer)); } } } } for (const auto& layer : extra_content) { - content.insert(layer); + content.insert(std::shared_ptr(layer)); } } @@ -144,15 +144,17 @@ const std::unordered_set>& Group::getOutputs() const { } void Group::addInput(const std::shared_ptr& node) { - m_input_layers.insert(node); + m_input_layers.insert(std::shared_ptr(node)); + m_mic_io_valid = false; } void Group::addOutput(const std::shared_ptr& node) { - m_output_layers.insert(node); + m_output_layers.insert(std::shared_ptr(node)); + m_mic_io_valid = false; } void Group::addContent(const std::shared_ptr& node) { - m_content.insert(node); + m_content.insert(std::shared_ptr(node)); } size_t Group::getId() const { @@ -173,6 +175,7 @@ own::ade::NodeHandle Group::getHandle() const { // Not every input should be included - those layers which contained inside merging group are not inputs anymore void Group::updateInputLayers(const Group::GPtr& gptr_other) { + m_mic_io_valid = false; detail::OVNodeSet combined_input; combined_input.insert(m_input_layers.begin(), m_input_layers.end()); combined_input.insert(gptr_other->m_input_layers.begin(), gptr_other->m_input_layers.end()); @@ -185,12 +188,12 @@ void Group::updateInputLayers(const Group::GPtr& gptr_other) { auto node_prod = locked_snapshot->getNodeProducers(layer); for (const auto& prod : node_prod) { if (m_content.find(prod) == m_content.end()) { - selected.insert(prod); + selected.insert(std::shared_ptr(prod)); } } for (const auto& l : selected) { if (isOp(l)) { - m_input_layers.insert(layer); + m_input_layers.insert(std::shared_ptr(layer)); } } } @@ -199,6 +202,7 @@ void Group::updateInputLayers(const Group::GPtr& gptr_other) { // Not every output should be included - those layers which are consumed _ONLY_ by the merged group are not outputs // anymore void Group::updateOutputLayers(const Group::GPtr& gptr_other) { + m_mic_io_valid = false; detail::OVNodeSet combined_output; combined_output.insert(m_output_layers.begin(), m_output_layers.end()); combined_output.insert(gptr_other->m_output_layers.begin(), gptr_other->m_output_layers.end()); @@ -215,7 +219,7 @@ void Group::updateOutputLayers(const Group::GPtr& gptr_other) { } } if (!reject) { - m_output_layers.insert(layer); + m_output_layers.insert(std::shared_ptr(layer)); } } } @@ -276,7 +280,7 @@ void Group::fuseWith(const Group::GPtr& gptr_cons) { // Merge 2 contents together for (const auto& layer : gptr_cons->m_content) { - m_content.insert(layer); + m_content.insert(std::shared_ptr(layer)); } takeFlags(gptr_cons); @@ -297,7 +301,7 @@ void Group::fuseInputs(const std::pair& gptr_inputs) { // Update ov::node to own::ade::NodeHandle map and merge all contents together for (const auto& layer : absorbed_group->m_content) { node_to_gr->at(layer) = absorbing_group; - absorbing_group->m_content.insert(layer); + absorbing_group->m_content.insert(std::shared_ptr(layer)); } absorbing_group->takeFlags(absorbed_group); absorbing_group->updateInputLayers(absorbed_group); @@ -333,21 +337,26 @@ void Group::takeFlags(const Group::GPtr& gptr_other) { // Check if there is indirect path from this to gptr_cons bool Group::hasCycle(const Group::GPtr& gptr_cons) const { - std::unordered_set visited; + // Fast-path: if this group has at most 1 consumer, there is no alternative path + // to gptr_cons and therefore no cycle. + if (m_nh->dstNodes().size() <= 1) { + return false; + } + std::unordered_set visited; std::stack st; for (const auto& prod : gptr_cons->srcNodes()) { // skip self during this iter if (!(m_nh == prod)) { st.push(prod); + visited.insert(own::ade::NodeHandle(prod)); // mark at push time to avoid duplicate pushes } } while (!st.empty()) { auto nh = st.top(); st.pop(); - visited.insert(nh); if (nh == m_nh) { // Found another path from self to gptr_cons @@ -355,8 +364,9 @@ bool Group::hasCycle(const Group::GPtr& gptr_cons) const { } for (const auto& prod : nh->srcNodes()) { - if (visited.find(prod) == visited.end()) { + if (!visited.count(prod)) { st.push(prod); + visited.insert(own::ade::NodeHandle(prod)); // mark at push time } } } @@ -376,6 +386,10 @@ void Group::noFold() { m_nofold = true; } +void Group::unfreeze() { + m_frozen = false; +} + bool Group::isFrozen() const { return m_frozen; } @@ -415,33 +429,39 @@ void Group::setRepeated(const std::shared_ptr& rep) { PairMICSetIO Group::metaInterconnect(const Group::GPtr& gptr_prod) const { MICSet mics; + auto locked_snapshot = m_snapshot.lock(); auto ics = interconnect(gptr_prod); for (const auto& ic : ics) { - mics.insert({ov::npuw::online::util::getMetaDesc(ic.input_node), + mics.insert({locked_snapshot->getMetaDesc(ic.input_node), gptr_prod->m_reptrack.at(ic.input_node), ic.input_port, - ov::npuw::online::util::getMetaDesc(ic.output_node), + locked_snapshot->getMetaDesc(ic.output_node), m_reptrack.at(ic.output_node), ic.output_port}); } - MetaInterconnectIO mic_io; - auto locked_snapshot = m_snapshot.lock(); - for (const auto& oi : m_input_layers) { - mic_io.output_imeta.insert(ov::npuw::online::util::getMetaDesc(oi)); - } - for (const auto& oo : m_output_layers) { - mic_io.output_ometa.insert(ov::npuw::online::util::getMetaDesc(oo)); + // Cache the MetaInterconnectIO part — it only depends on m_input_layers / + // m_output_layers which are invalidated (m_mic_io_valid = false) whenever + // those sets change (addInput / addOutput / updateInputLayers / updateOutputLayers). + if (!m_mic_io_valid) { + m_cached_mic_io = MetaInterconnectIO{}; + for (const auto& oi : m_input_layers) { + m_cached_mic_io.output_imeta.insert(locked_snapshot->getMetaDesc(oi)); + } + for (const auto& oo : m_output_layers) { + m_cached_mic_io.output_ometa.insert(locked_snapshot->getMetaDesc(oo)); + } + m_mic_io_valid = true; } - return {mics, mic_io}; + return {mics, m_cached_mic_io}; } std::unordered_set Group::interconnect(const Group::GPtr& gptr_prod) const { std::unordered_set ics; auto locked_snapshot = m_snapshot.lock(); - auto ports_map = locked_snapshot->getPortsMap(); + const auto& ports_map = locked_snapshot->getPortsMap(); for (const auto& layer : m_content) { for (const auto& input_layer : locked_snapshot->getNodeProducers(layer)) { diff --git a/src/plugins/intel_npu/src/plugin/npuw/partitioning/online/group.hpp b/src/plugins/intel_npu/src/plugin/npuw/partitioning/online/group.hpp index 1fa793536455..aca77f1d9348 100644 --- a/src/plugins/intel_npu/src/plugin/npuw/partitioning/online/group.hpp +++ b/src/plugins/intel_npu/src/plugin/npuw/partitioning/online/group.hpp @@ -11,6 +11,7 @@ #include "graph.hpp" #include "openvino/openvino.hpp" +#include "repeated.hpp" #include "utils/utils.hpp" namespace ov { @@ -66,6 +67,7 @@ class Group : public std::enable_shared_from_this { size_t size() const; void freeze(); void noFold(); + void unfreeze(); bool isFrozen() const; bool isNoFold() const; const detail::OVNodeSet& getContent() const; @@ -119,6 +121,11 @@ class Group : public std::enable_shared_from_this { std::shared_ptr m_repeated = nullptr; // For each layer inside group, store it's history of repeated groups detail::ReptrackMap m_reptrack; + + // Cache for the MetaInterconnectIO part of metaInterconnect(). + // Invalidated whenever m_input_layers or m_output_layers change. + mutable bool m_mic_io_valid = false; + mutable MetaInterconnectIO m_cached_mic_io; }; } // namespace online diff --git a/src/plugins/intel_npu/src/plugin/npuw/partitioning/online/snapshot.cpp b/src/plugins/intel_npu/src/plugin/npuw/partitioning/online/snapshot.cpp index 0c6cc941758e..459050b89a3a 100644 --- a/src/plugins/intel_npu/src/plugin/npuw/partitioning/online/snapshot.cpp +++ b/src/plugins/intel_npu/src/plugin/npuw/partitioning/online/snapshot.cpp @@ -658,11 +658,10 @@ void Snapshot::earlyAvoids() { case PatternType::PATTERN: { // FIXME: refactor as more patterns are supported if (avoid.pattern != "RMSNorm" && avoid.pattern != "SinCos" && avoid.pattern != "GemmaRoPE" && - avoid.pattern != "DownsampleInterpolate" && avoid.pattern != "FloorModFP32" && - avoid.pattern != "CumSumSinGen" && avoid.pattern != "BoxMullerNoise" && - avoid.pattern != "AngleComplex") { + avoid.pattern != "FloorModFP32" && avoid.pattern != "CumSumSinGen" && + avoid.pattern != "BoxMullerNoise" && avoid.pattern != "AngleComplex") { LOG_WARN("OPENVINO_NPUW_AVOID only supports RMSNorm, SinCos, GemmaRoPE, " - "DownsampleInterpolate, FloorModFP32, CumSumSinGen, BoxMullerNoise " + "FloorModFP32, CumSumSinGen, BoxMullerNoise " "and AngleComplex as patterns " "(don't confuse with operations). " "Avoid pattern " @@ -676,8 +675,6 @@ void Snapshot::earlyAvoids() { rewr.add_matcher(shared_from_this(), avoid.device); } else if (avoid.pattern == "GemmaRoPE") { rewr.add_matcher(shared_from_this(), avoid.device); - } else if (avoid.pattern == "DownsampleInterpolate") { - rewr.add_matcher(shared_from_this(), avoid.device); } else if (avoid.pattern == "FloorModFP32") { rewr.add_matcher(shared_from_this(), avoid.device); } else if (avoid.pattern == "CumSumSinGen") { @@ -723,47 +720,60 @@ void Snapshot::earlyRegroup() { break; } case PatternType::PATTERN: { + bool pattern_handled = false; + if (m_ctx.subgraph_patterns != nullptr) { + pattern_handled = + m_ctx.subgraph_patterns->register_matcher(rewr, shared_from_this(), isolate.pattern, isolate.tag); + handle_patterns = pattern_handled || handle_patterns; + } + if (!pattern_handled) { + // Keep the legacy built-in matcher list as a fallback so existing ISOLATE pattern names + // continue to work unchanged when they are not handled by the injected pattern registry. #define HNDL(p) \ if (isolate.pattern == #p) { \ rewr.add_matcher(shared_from_this(), isolate.tag); \ - handle_patterns = true; \ + pattern_handled = true; \ } #define HNDL_FAKE(p) \ if (isolate.pattern == #p) { \ rewr_fake.add_matcher(shared_from_this(), isolate.tag); \ - handle_patterns = true; \ + pattern_handled = true; \ } #define HNDL_ATTN(p) \ if (isolate.pattern == #p) { \ rewr.add_matcher(shared_from_this(), isolate.tag); \ - handle_patterns = true; \ + pattern_handled = true; \ } #define HNDL_MOE(p) \ if (isolate.pattern == #p) { \ rewr.add_matcher(shared_from_this(), isolate.tag); \ - handle_patterns = true; \ - } - HNDL(RMSNorm); - HNDL(RMSNorm2); - HNDL(RMSNorm3); - HNDL(RMSNorm4); - HNDL(DQMatMulCWu4); - HNDL(DQMatMulGQu4); - HNDL(DQMatMulCWi4); - HNDL(DQMatMulGQi4); - HNDL(DQMatMulConv); - HNDL(VocabMatMul); - HNDL(VariadicSplit); - HNDL_MOE(GPTOSSExpert); - HNDL_MOE(GPTOSSRouter); - HNDL_FAKE(FakeConvert); - HNDL_FAKE(FakeQuantize); - HNDL_ATTN(SDPA); - HNDL_ATTN(SDPADecomposed); + pattern_handled = true; \ + } + HNDL(RMSNorm); + HNDL(RMSNorm2); + HNDL(RMSNorm3); + HNDL(RMSNorm4); + HNDL(DQMatMulCWu4); + HNDL(DQMatMulGQu4); + HNDL(DQMatMulCWi4); + HNDL(DQMatMulGQi4); + HNDL(DQMatMulConv); + HNDL(VocabMatMul); + HNDL(VariadicSplit); + HNDL_MOE(GPTOSSExpert); + HNDL_MOE(GPTOSSRouter); + HNDL_MOE(Qwen3Expert); + HNDL_MOE(Qwen3Router); + HNDL_FAKE(FakeConvert); + HNDL_FAKE(FakeQuantize); + HNDL_ATTN(SDPA); + HNDL_ATTN(SDPADecomposed); #undef HNDL_MOE #undef HNDL_ATTN #undef HNDL_FAKE #undef HNDL + handle_patterns = pattern_handled || handle_patterns; + } } } } @@ -822,7 +832,7 @@ void Snapshot::identifyUniques() { // This pass should only be called at the very beginning, // thus check and use only the single initial layer auto ov_node = group->getInitialNode(); - auto metadesc = ov::npuw::online::util::getMetaDesc(ov_node); + auto metadesc = getMetaDesc(ov_node); const auto& avoids = group->avoidedTargets(); const auto& special_tags = group->specialTags(); uniques[{metadesc, avoids, special_tags}].insert(group); @@ -1082,6 +1092,28 @@ void Snapshot::mergeUniques() { LOG_INFO("Online partitioning: executing mergeUniques pass..."); LOG_BLOCK(); + // Pre-build a rep-tag → GPtrSet index to replace the O(V) scan inside + // getRepGroups(). Without this, getRepGroups() is called once per distinct rep + // tag and each call scans all V graph nodes → O(V × nRepTags) = O(V²) total. + // With small chunk sizes (many KV blocks → large V), this dominates compilation. + // + // Correctness: each rep tag is visited at most once (merged_this_time guard). + // During the loop, tryMergeRepeating() may: + // - re-tag consumer groups (they get a new_rep) → stale entries in the index + // - remove producer groups from the graph + // Both cases are handled by the staleness filter below (rep-tag and graph checks). + std::unordered_map rep_index; + for (const auto& nh_i : m_graph->sorted()) { + if (!m_graph->contains(nh_i)) { + continue; + } + const Group::GPtr& g = m_graph->meta(nh_i).get(); + const auto& rep_i = g->repeated(); + if (rep_i && !g->isFrozen()) { + rep_index[rep_i.get()].insert(g); + } + } + std::unordered_set> merged_this_time; for (const auto& nh : m_graph->sorted()) { @@ -1094,7 +1126,17 @@ void Snapshot::mergeUniques() { GPtrSet repeating_groups; if (rep && rep->openForMerge() && merged_this_time.count(rep) == 0) { - repeating_groups = getRepGroups(group); + // Use the prebuilt index (O(|rep_set|)) instead of the O(V) graph scan. + // Apply a staleness filter: a group's rep tag may have changed if it was + // merged in an earlier iteration of this same mergeUniques() call. + auto it = rep_index.find(rep.get()); + if (it != rep_index.end()) { + for (const auto& g : it->second) { + if (m_graph->contains(g->getHandle()) && g->repeated().get() == rep.get() && !g->isFrozen()) { + repeating_groups.insert(g); + } + } + } } if (!repeating_groups.empty()) { @@ -1147,26 +1189,29 @@ std::shared_ptr Snapshot::tryGrowRepeatingGroups(const GPtrSet& repeat Group::GPtr prod_group = m_graph->meta(prod_nh).get(); LOG_DEBUG("prod_group:"); prod_group->dump(); - if (prod_group->repeated() && !prod_group->hasCycle(group) && prod_group->repeated() != this_rep_tag && - prod_group->avoidedTargets() == this_avoided && prod_group->specialTags() == this_special) { - auto meta_interconnect = group->metaInterconnect(prod_group); - - // FIXME: find a better way to reduce time complexity - // Need to align interconnects in the same format via sort, so they could be compared later - MICVec mic_sorted_key(meta_interconnect.first.begin(), meta_interconnect.first.end()); - std::sort(mic_sorted_key.begin(), mic_sorted_key.end()); - mics[{mic_sorted_key, meta_interconnect.second}].push_back({prod_group, group}); - LOG_DEBUG("Add the pair to the merge vector!"); - } else if (ov::npuw::debug_groups()) { - LOG_DEBUG("Couldn't add the pair to the merge vector due to failed checks:"); - LOG_BLOCK(); + if (prod_group->repeated()) { + bool cycle = prod_group->hasCycle(group); + if (!cycle && prod_group->repeated() != this_rep_tag && prod_group->avoidedTargets() == this_avoided && + prod_group->specialTags() == this_special) { + auto meta_interconnect = group->metaInterconnect(prod_group); + + // FIXME: find a better way to reduce time complexity + // Need to align interconnects in the same format via sort, so they could be compared later + MICVec mic_sorted_key(meta_interconnect.first.begin(), meta_interconnect.first.end()); + std::sort(mic_sorted_key.begin(), mic_sorted_key.end()); + mics[{mic_sorted_key, meta_interconnect.second}].push_back({prod_group, group}); + LOG_DEBUG("Add the pair to the merge vector!"); + } else if (ov::npuw::debug_groups()) { + LOG_DEBUG("Couldn't add the pair to the merge vector due to failed checks:"); + LOG_BLOCK(); #define INSPECT(x) LOG_DEBUG(#x " = " << (x)) - INSPECT(prod_group->specialTags()); - INSPECT(prod_group->repeated() != this_rep_tag); - INSPECT(prod_group->hasCycle(group)); - INSPECT(prod_group->avoidedTargets() == this_avoided); - INSPECT(prod_group->specialTags() == this_special); + INSPECT(prod_group->specialTags()); + INSPECT(prod_group->repeated() != this_rep_tag); + INSPECT(cycle); + INSPECT(prod_group->avoidedTargets() == this_avoided); + INSPECT(prod_group->specialTags() == this_special); #undef INSPECT + } } } } @@ -1380,7 +1425,7 @@ void Snapshot::completeRepeating(const std::shared_ptr& reptag, const for (const auto& gptr : gset) { for (const auto& layer : gptr->getContent()) { // FIXME: should it be a part of group's API instead? - const auto& metadesc = ov::npuw::online::util::getMetaDesc(layer); + const auto& metadesc = getMetaDesc(layer); const auto& archetype = gptr->getReptrack(layer); matches[{std::move(metadesc), std::move(archetype)}].insert(layer); } @@ -1465,6 +1510,17 @@ size_t Snapshot::graphSize() const { return m_graph->nodes().size(); } +std::string Snapshot::getMetaDesc(const std::shared_ptr& node) const { + auto id = node->get_instance_id(); + auto it = m_metadesc_cache.find(id); + if (it != m_metadesc_cache.end()) { + return it->second; + } + auto result = util::getMetaDesc(node); + m_metadesc_cache.emplace(id, result); + return result; +} + const OVPortsMap& Snapshot::getPortsMap() const { return m_ports_map; } @@ -1499,6 +1555,36 @@ void Snapshot::stripTag(const std::string& tag) { } } +void Snapshot::fuseUnfolded() { + if (!m_ctx.fuse_unfolded) { + return; + } + + LOG_INFO("Online partitioning: executing fuseUnfolded pass..."); + LOG_BLOCK(); + + const std::set fold_only_set(m_ctx.fold_only_tags.begin(), m_ctx.fold_only_tags.end()); + size_t stripped = 0; + + for (const auto& nh : m_graph->sorted()) { + Group::GPtr group = m_graph->meta(nh).get(); + if (!group->isFrozen() || !group->repeated()) { + continue; + } + const auto& tag = group->isolatedTag(); + if (fold_only_set.count(tag) == 0) { + // This group is repeated but not destined for folding: release it so + // fuseRemnants can absorb it into adjacent non-folded subgraphs. + group->unfreeze(); + group->setRepeated(nullptr); + ++stripped; + } + } + + LOG_INFO("Stripped reptag from " << stripped << " non-fold-only repeated groups."); + LOG_INFO("DONE"); +} + bool Snapshot::isRegularIOCase() const { LOG_INFO("Online partitioning: executing isRegularIOCase pass..."); LOG_BLOCK(); diff --git a/src/plugins/intel_npu/src/plugin/npuw/partitioning/online/snapshot.hpp b/src/plugins/intel_npu/src/plugin/npuw/partitioning/online/snapshot.hpp index ecb5e4ef68b6..eb3c40305189 100644 --- a/src/plugins/intel_npu/src/plugin/npuw/partitioning/online/snapshot.hpp +++ b/src/plugins/intel_npu/src/plugin/npuw/partitioning/online/snapshot.hpp @@ -55,6 +55,7 @@ class Snapshot : public std::enable_shared_from_this { void earlyRegroup(); void stripTag(const std::string& tag); + void fuseUnfolded(); // Passes to detect corner cases bool isRegularIOCase() const; @@ -67,6 +68,7 @@ class Snapshot : public std::enable_shared_from_this { void repeat(detail::Pass&& pass); void setCtx(const PassContext& ctx); size_t graphSize() const; + std::string getMetaDesc(const std::shared_ptr& node) const; private: detail::GPtrSet getRepGroups(const std::shared_ptr& group) const; @@ -100,6 +102,7 @@ class Snapshot : public std::enable_shared_from_this { detail::OVPortsMap m_ports_map; std::map>> m_layer_matches; + mutable std::unordered_map m_metadesc_cache; }; } // namespace online diff --git a/src/plugins/intel_npu/src/plugin/npuw/partitioning/online/utils/utils.cpp b/src/plugins/intel_npu/src/plugin/npuw/partitioning/online/utils/utils.cpp index 30fb9c1ef1f4..707699e8f1e3 100644 --- a/src/plugins/intel_npu/src/plugin/npuw/partitioning/online/utils/utils.cpp +++ b/src/plugins/intel_npu/src/plugin/npuw/partitioning/online/utils/utils.cpp @@ -90,6 +90,27 @@ std::optional ov::npuw::online::util::parseIsolate(co return std::optional{isolate}; } +std::vector ov::npuw::online::util::splitByComma(const std::string& s) { + std::vector tokens; + size_t pos = 0; + size_t start = 0; + + while ((pos = s.find(',', start)) != std::string::npos) { + auto token = s.substr(start, pos - start); + if (!token.empty()) { + tokens.push_back(std::move(token)); + } + start = pos + 1; + } + + auto tail = s.substr(start, s.size() - start); + if (!tail.empty()) { + tokens.push_back(std::move(tail)); + } + + return tokens; +} + size_t ov::npuw::online::util::getMinGraphSize(const ::intel_npu::Config& cfg) { std::size_t min_size = cfg.get<::intel_npu::NPUW_ONLINE_MIN_SIZE>(); @@ -124,27 +145,11 @@ std::vector ov::npuw::online::util::getAvoids(const ::i return {}; } - std::string s = std::move(avoids_opt); - - size_t pos = 0; - size_t start = 0; - std::string token; - - while ((pos = s.find(',', start)) != std::string::npos) { - token = s.substr(start, pos - start); + for (const auto& token : splitByComma(avoids_opt)) { auto avoid_opt = util::parseAvoid(token); - // Check that parsing was a success if (avoid_opt) { avoids.push_back(*avoid_opt); } - start = pos + 1; - } - - // Parse the tail - auto avoid_opt = util::parseAvoid(s.substr(start, s.size() - start)); - // Check that parsing was a success - if (avoid_opt) { - avoids.push_back(*avoid_opt); } if (!avoids.empty()) { @@ -170,21 +175,6 @@ std::vector ov::npuw::online::util::getIsolates(const std::vector isolates; - // Lambda to split comma-separated string into tokens - auto splitByComma = [](const std::string& s) { - std::vector tokens; - size_t pos = 0; - size_t start = 0; - - while ((pos = s.find(',', start)) != std::string::npos) { - tokens.push_back(s.substr(start, pos - start)); - start = pos + 1; - } - tokens.push_back(s.substr(start, s.size() - start)); // Handle tail - - return tokens; - }; - // Split input and expand presets std::vector expanded_tokens; for (const auto& token : splitByComma(isolates_unparsed)) { @@ -225,26 +215,7 @@ std::vector ov::npuw::online::util::getNoFolds(const std::string& n return {}; } - std::vector nofolds; - std::string s(nofolds_unparsed); - - size_t pos = 0; - size_t start = 0; - std::string token; - - while ((pos = s.find(',', start)) != std::string::npos) { - token = s.substr(start, pos - start); - if (!token.empty()) { - nofolds.push_back(token); - } - start = pos + 1; - } - - // Parse the tail - std::string tail = s.substr(start, s.size() - start); - if (!tail.empty()) { - nofolds.push_back(tail); - } + auto nofolds = splitByComma(nofolds_unparsed); if (!nofolds.empty()) { LOG_INFO("Online partitioning will mark specified tags as non-foldable."); @@ -264,4 +235,6 @@ ov::npuw::online::PassContext::PassContext(const ::intel_npu::Config& cfg) { avoids = ov::npuw::online::util::getAvoids(cfg); isolates = ov::npuw::online::util::getIsolates(cfg); nofolds = ov::npuw::online::util::getNoFolds(cfg); + fuse_unfolded = cfg.get<::intel_npu::NPUW_FUSE_UNFOLDED>(); + fold_only_tags = ov::npuw::online::util::splitByComma(cfg.get<::intel_npu::NPUW_FOLD_ONLY>()); } diff --git a/src/plugins/intel_npu/src/plugin/npuw/partitioning/online/utils/utils.hpp b/src/plugins/intel_npu/src/plugin/npuw/partitioning/online/utils/utils.hpp index abd5800ec1f2..9f27e94f2610 100644 --- a/src/plugins/intel_npu/src/plugin/npuw/partitioning/online/utils/utils.hpp +++ b/src/plugins/intel_npu/src/plugin/npuw/partitioning/online/utils/utils.hpp @@ -10,6 +10,7 @@ #include #include +#include "../../../v1/subgraph_pipeline.hpp" #include "attribute_visitor.hpp" #include "intel_npu/config/config.hpp" #include "openvino/openvino.hpp" @@ -42,6 +43,9 @@ struct PassContext { std::vector avoids; std::vector isolates; std::vector nofolds; + bool fuse_unfolded = false; + std::vector fold_only_tags; + const ov::npuw::v1::subgraphs::PatternRegistry* subgraph_patterns = nullptr; }; // Forward declaration @@ -77,6 +81,7 @@ std::string getMetaDesc(const std::shared_ptr& ov_node); std::optional parseAvoid(const std::string& s); std::optional parseIsolate(const std::string& s); std::tuple parse(const std::string& s); +std::vector splitByComma(const std::string& s); size_t getMinGraphSize(const ::intel_npu::Config& cfg); size_t getMinRepBlocks(const ::intel_npu::Config& cfg); @@ -97,7 +102,9 @@ static const std::map ISOL_PRESETS = {{"COMPUTE", "P:VariadicSplit/compute"}, {"FAKE", "P:FakeConvert/fake,P:FakeQuantize/fake"}, {"ATTN", "P:SDPA/attn,P:SDPADecomposed/attn"}, - {"MOE", "P:GPTOSSExpert/expert,P:GPTOSSRouter/router"}}; + {"MOE", + "P:GPTOSSExpert/expert,P:GPTOSSRouter/router," + "P:Qwen3Expert/expert,P:Qwen3Router/router"}}; } // namespace util } // namespace online diff --git a/src/plugins/intel_npu/src/plugin/npuw/partitioning/partitioning.cpp b/src/plugins/intel_npu/src/plugin/npuw/partitioning/partitioning.cpp index cf270dd58a32..a8664f977503 100644 --- a/src/plugins/intel_npu/src/plugin/npuw/partitioning/partitioning.cpp +++ b/src/plugins/intel_npu/src/plugin/npuw/partitioning/partitioning.cpp @@ -5,6 +5,7 @@ #include "partitioning.hpp" #include +#include #include "../logging.hpp" #include "../util.hpp" @@ -35,6 +36,119 @@ inline bool operator==(const std::reference_wrapper& lhs, const std::r } // namespace npuw } // namespace ov +void ov::npuw::v1::subgraphs::PatternRegistry::apply(ov::npuw::Function& function) const { + for (const auto& registration : m_registrations) { + if (!registration.tag.empty() && function.gettag() != registration.tag) { + continue; + } + + auto& func_pipeline = function._pipeline; + if (func_pipeline.registration.group.empty()) { + func_pipeline.registration.group = registration.group; + } + if (func_pipeline.registration.name.empty()) { + func_pipeline.registration.name = registration.name; + } + if (!registration.pattern.empty() && + std::find(func_pipeline.registration.patterns.begin(), + func_pipeline.registration.patterns.end(), + registration.pattern) == func_pipeline.registration.patterns.end()) { + func_pipeline.registration.patterns.push_back(registration.pattern); + } + + auto previous_partition_stage = func_pipeline.partition_stage; + func_pipeline.partition_stage = [previous_partition_stage, registration]( + ov::npuw::Function& staged_function, + ov::npuw::v1::subgraphs::Context& ctx) { + if (previous_partition_stage) { + previous_partition_stage(staged_function, ctx); + } + if (registration.partition_stage) { + registration.partition_stage(staged_function, ctx); + } + }; + + auto previous_compile_stage = func_pipeline.compile_stage; + func_pipeline.compile_stage = [previous_compile_stage, registration]( + ov::npuw::v1::subgraphs::CompiledPipeline& compiled_pipeline, + ov::npuw::v1::subgraphs::Context& compiled_context) { + if (previous_compile_stage) { + previous_compile_stage(compiled_pipeline, compiled_context); + } + if (registration.compile_stage) { + registration.compile_stage(compiled_pipeline, compiled_context); + } + if (registration.runtime_factory) { + ov::npuw::v1::subgraphs::RuntimeBehaviorSpec spec; + spec.registration = compiled_pipeline.registration; + spec.context = compiled_context; + spec.factory = registration.runtime_factory; + compiled_pipeline.runtime_behavior = std::move(spec); + } + }; + } +} + +void ov::npuw::v1::subgraphs::PatternRegistry::apply(ov::npuw::Subgraph& subgraph) const { + for (const auto& registration : m_registrations) { + if (!registration.tag.empty() && subgraph.gettag() != registration.tag) { + continue; + } + + auto& subgraph_pipeline = subgraph._pipeline; + if (subgraph_pipeline.registration.group.empty()) { + subgraph_pipeline.registration.group = registration.group; + } + if (subgraph_pipeline.registration.name.empty()) { + subgraph_pipeline.registration.name = registration.name; + } + if (!registration.pattern.empty() && + std::find(subgraph_pipeline.registration.patterns.begin(), + subgraph_pipeline.registration.patterns.end(), + registration.pattern) == subgraph_pipeline.registration.patterns.end()) { + subgraph_pipeline.registration.patterns.push_back(registration.pattern); + } + + auto previous_partition_stage = subgraph_pipeline.partition_stage; + subgraph_pipeline.partition_stage = [previous_partition_stage, registration]( + ov::npuw::Function& staged_function, + ov::npuw::v1::subgraphs::Context& ctx) { + if (previous_partition_stage) { + previous_partition_stage(staged_function, ctx); + } + if (registration.partition_stage) { + registration.partition_stage(staged_function, ctx); + } + }; + + auto previous_compile_stage = subgraph_pipeline.compile_stage; + subgraph_pipeline.compile_stage = [previous_compile_stage, registration]( + ov::npuw::v1::subgraphs::CompiledPipeline& compiled_pipeline, + ov::npuw::v1::subgraphs::Context& compiled_context) { + if (previous_compile_stage) { + previous_compile_stage(compiled_pipeline, compiled_context); + } + if (registration.compile_stage) { + registration.compile_stage(compiled_pipeline, compiled_context); + } + if (registration.runtime_factory) { + ov::npuw::v1::subgraphs::RuntimeBehaviorSpec spec; + spec.registration = compiled_pipeline.registration; + spec.context = compiled_context; + spec.factory = registration.runtime_factory; + compiled_pipeline.runtime_behavior = std::move(spec); + } + }; + } +} + +void ov::npuw::v1::subgraphs::PatternRegistry::append_from(const PatternRegistry& other) { + for (auto registration : other.m_registrations) { + registration.id = ++m_next_id; + m_registrations.push_back(std::move(registration)); + } +} + template struct std::hash> { std::size_t operator()(const std::pair& p) const noexcept { @@ -239,11 +353,6 @@ class Partitioner { using Match = std::function& node)>; void propagate(const std::string& func_name, const Match& test, ov::npuw::RepeatedBlock::MatchedBank& bank); - // Helper method to find and cache router model for MoE transformations - // Returns cached router model if available, otherwise searches P.functions - // for a function tagged as "router" and caches it for future use - std::shared_ptr getRouterModel(); - void createFunction(FunctionPipeline& func_ggg); // NB(dm): This method should get a better place, it is here only because @@ -342,12 +451,6 @@ class Partitioner { void spatial(const std::string& func_name); void attention(const std::string& func_name); - // MoE-specific transformations (require router model availability) - // transformMoeExperts: Transform expert functions (tag="expert") to optimized MoE expert models - // transformMoeDownstream: Transform downstream processing (pattern-based detection) - void transformMoeExperts(const std::string& func_name); - void transformMoeDownstream(const std::string& func_name); - void optimize(const std::string& func_name); void decompressionCutOff(const std::string& func_name); @@ -437,6 +540,9 @@ void Partitioner::identifySubgraphs() { group.sg._avoid_list = group.avoid_list; group.sg.settag(group.gettag()); + if (part_ctx.subgraph_patterns != nullptr) { + part_ctx.subgraph_patterns->apply(group.sg); + } // Note inputs and outputs are included in the above set, so if // we are here, those nodes should be present in the model. @@ -805,10 +911,24 @@ void Partitioner::identifySubgraphs() { std::vector Partitioner::initFunctionPipeline(FunctionPipelineType utype) { func_pipeline_type = utype; + std::set selected_repeated_ids; + if (func_pipeline_type == FunctionPipelineType::FOLD && !cfg.get<::intel_npu::NPUW_FOLD>()) { + const auto fold_only_tags_vec = ov::npuw::online::util::splitByComma(cfg.get<::intel_npu::NPUW_FOLD_ONLY>()); + const std::set fold_only_tags(fold_only_tags_vec.begin(), fold_only_tags_vec.end()); + if (!fold_only_tags.empty()) { + for (const auto& subgraph : P.subgraphs) { + if (!subgraph._repeated_id.empty() && fold_only_tags.count(subgraph.gettag()) > 0) { + selected_repeated_ids.insert(subgraph._repeated_id); + } + } + } + } + // Collect all groups of function call(s) and process them in groups std::map idx; for (auto&& part_sg : P.subgraphs) { - if (!part_sg._repeated_id.empty()) { + if (!part_sg._repeated_id.empty() && + (selected_repeated_ids.empty() || selected_repeated_ids.count(part_sg._repeated_id) > 0)) { auto pfix = "__" + std::to_string(idx[part_sg._repeated_id]++); const auto& fcid = func_pipeline_type == FunctionPipelineType::FOLD ? part_sg._repeated_id // with folding, functions of the @@ -1658,6 +1778,9 @@ void Partitioner::createFunction(FunctionPipeline& func_ggg) { function._model = func_ggg.mdls.front(); function._param_offset = body_sg._parameters.size(); function.settag(body_sg.gettag()); + if (part_ctx.subgraph_patterns != nullptr) { + part_ctx.subgraph_patterns->apply(function); + } std::size_t new_param_idx = function._param_offset; for (auto&& node_ptr : function._model->get_ordered_ops()) { @@ -1977,85 +2100,6 @@ void Partitioner::attention(const std::string& func_name) { } } -std::shared_ptr Partitioner::getRouterModel() { - // Find and cache router model in context if not already cached - if (part_ctx.router_model != nullptr) { - return part_ctx.router_model; - } - - for (const auto& [name, func] : P.functions) { - LOG_DEBUG("Checking function " << name << " with tag " << func.gettag()); - if (func.gettag() != ov::npuw::patterns::moe::ROUTER_TAG) { - continue; - } - - part_ctx.router_model = func._model; - LOG_INFO("Found router model: " << name); - break; - } - - return part_ctx.router_model; -} - -void Partitioner::transformMoeExperts(const std::string& func_name) { - ov::npuw::Function& f = P.functions.at(func_name); - - // Only process functions tagged as "expert" - if (f.gettag() != ov::npuw::patterns::moe::EXPERT_TAG) { - return; - } - - // Retrieve router model (required for extracting K from TopK node) - auto router_model = getRouterModel(); - if (!router_model) { - LOG_WARN("Router model not available yet, skipping MoE expert transformation for " << func_name); - return; - } - - LOG_DEBUG("Transforming " << func_name << " into MoE expert block in model " << model->get_friendly_name() - << "..."); - LOG_BLOCK(); - - // Determine compilation strategy from configuration: - // - moe_chunk_size = 0 (default): Compile multiple models for various chunk sizes - // {16, 32, 64, 128, 256} to enable dynamic chunk selection at runtime - // - moe_chunk_size > 0: Compile a single model with the specified fixed chunk size - const auto moe_chunk_size = cfg.get<::intel_npu::NPUW_MOE_TOKEN_CHUNK_SIZE>(); - - // Create MoEExperts using factory method: - // - Analyzes expert model structure - // - Extracts K (number of active experts) from router model's TopK node - // - Generates optimized expert models for prefill and/or decoding stages - f._moe_experts = ov::npuw::function::MoEExperts::from(f._model, router_model, moe_chunk_size); -} - -void Partitioner::transformMoeDownstream(const std::string& func_name) { - ov::npuw::Function& f = P.functions.at(func_name); - - // Retrieve router model (required for extracting active expert count) - auto router_model = getRouterModel(); - if (!router_model) { - LOG_WARN("Router model not available, skipping MoE downstream transformation for " << func_name); - return; - } - - LOG_DEBUG("Attempting MoE downstream transformation for " << func_name << "..."); - LOG_BLOCK(); - - // Detect and transform MoE downstream processing pattern: - // Expected pattern: Parameter[total_experts, 1, H, W] -> Convert -> ReduceSum - // - // Transformation: - // - Identifies parameters with expert dimension (first dimension = total_experts) - // - Reshapes from [total_experts, ...] to [active_experts, ...] - // - active_experts (K) is extracted from router model's TopK node - // - // Note: No tag-based filtering - pattern matching determines applicability - // This allows downstream processing to be detected in any function that follows - // the structural pattern, regardless of how it was originally tagged - f._moe_experts_downstream = ov::npuw::function::create_moe_downstream(f._model, router_model); -} - void Partitioner::optimize(const std::string& func_name) { using namespace ov::npuw::weights; @@ -2506,9 +2550,11 @@ void Partitioner::finalizeLinks() { } else { // A function call: find in the prototype subgraph auto& params = P.functions.at(sg_desc._funcall)._model->get_parameters(); - auto& proto = func_pipeline_type == FunctionPipelineType::CWAI - ? ptr // no protos in the CWAI case.. - : all_functions.at(sg_desc._funcall).param_call_to_proto.at(SubgParam(sg_desc, ptr)); + auto& func_group = all_functions.at(sg_desc._funcall); + // Mixed FOLD/CWAI partitionings are resolved per function group: + // FOLD fills param_call_to_proto, while CWAI falls back to the call-local ptr. + auto proto_iter = func_group.param_call_to_proto.find(SubgParam(sg_desc, ptr)); + auto& proto = proto_iter == func_group.param_call_to_proto.end() ? ptr : proto_iter->second; auto param_iter = std::find(params.begin(), params.end(), proto); NPUW_ASSERT(param_iter != params.end()); return std::distance(params.begin(), param_iter); @@ -2527,9 +2573,9 @@ void Partitioner::finalizeLinks() { } else { // A function call: find in the prototype subgraph auto& results = P.functions.at(sg_desc._funcall)._model->get_results(); - auto& proto = func_pipeline_type == FunctionPipelineType::CWAI - ? ptr // no protos in the CWAI case... - : all_functions.at(sg_desc._funcall).result_call_to_proto.at(SubgResult(sg_desc, ptr)); + auto& func_group = all_functions.at(sg_desc._funcall); + auto proto_iter = func_group.result_call_to_proto.find(SubgResult(sg_desc, ptr)); + auto& proto = proto_iter == func_group.result_call_to_proto.end() ? ptr : proto_iter->second; auto result_iter = std::find(results.begin(), results.end(), proto); NPUW_ASSERT(result_iter != results.end()); return std::distance(results.begin(), result_iter); @@ -2573,6 +2619,8 @@ ov::npuw::Partitioning ov::npuw::getPartitioning(const std::shared_ptr(); if (file_path.empty()) { LOG_INFO("No " << ::intel_npu::NPUW_PLAN().key() << " property is provided! Using online partitioning."); - ens = ov::npuw::online::buildPartitioning(model, cfg); + ens = ov::npuw::online::buildPartitioning(model, cfg, effective_ctx.subgraph_patterns); } else { ens = load_groups(model, file_path); } @@ -2653,18 +2701,17 @@ ov::npuw::Partitioning ov::npuw::getPartitioning(const std::shared_ptr()) { - // Do full-featured folding + auto run_fold_pipeline = [&]() { + // Do full-featured folding. auto all_functions = p.initFunctionPipeline(Partitioner::FunctionPipelineType::FOLD); - // Pass 1: Register all functions and apply general transformations + // Pass 1: Register all functions and apply general transformations. // - matchRepeatedSubgraphs() populates P.functions with all function definitions // - Other transformations (spatial, attention, optimize, etc.) can be applied // independently without cross-function dependencies @@ -2681,28 +2728,44 @@ ov::npuw::Partitioning ov::npuw::getPartitioning(const std::shared_ptr( + {[&P, &part_ctx = effective_ctx](const std::string& tag) -> std::shared_ptr { + auto cached = part_ctx.tagged_models.find(tag); + if (cached != part_ctx.tagged_models.end()) { + return cached->second; + } + for (const auto& [name, candidate] : P.functions) { + if (candidate.gettag() == tag) { + part_ctx.tagged_models.emplace(tag, candidate._model); + return candidate._model; + } + } + return nullptr; + }}); + function._pipeline.partition_stage(function, function._pipeline.context); + // The callback captures partitioning state by reference, so keep it scoped to this + // immediate partition-stage invocation and remove it before the context outlives us. + function._pipeline.context.erase(); + } } - } else if (cfg.get<::intel_npu::NPUW_CWAI>()) { + }; + + auto run_cwai_pipeline = [&]() { // Less brutal version - just transform repeated blocks // into the closure forms, but don't do folding. // This path is likely to be removed soon (is here for @@ -2717,9 +2780,51 @@ ov::npuw::Partitioning ov::npuw::getPartitioning(const std::shared_ptrget_friendly_name() - << ", but folding or eager mode are not enabled"); + }; + + auto select_repeated_ids_by_tag = [&](const std::set& tags) { + std::set selected_repeated_ids; + for (const auto& subgraph : P.subgraphs) { + if (!subgraph._repeated_id.empty() && tags.count(subgraph.gettag()) > 0) { + selected_repeated_ids.insert(subgraph._repeated_id); + } + } + return selected_repeated_ids; + }; + + const bool fold_enabled = cfg.get<::intel_npu::NPUW_FOLD>(); + const bool cwai_enabled = cfg.get<::intel_npu::NPUW_CWAI>(); + const auto fold_only_tags_vec = ov::npuw::online::util::splitByComma(cfg.get<::intel_npu::NPUW_FOLD_ONLY>()); + const std::set fold_only_tags(fold_only_tags_vec.begin(), fold_only_tags_vec.end()); + const auto fold_only_repeated_ids = + fold_only_tags.empty() ? std::set{} : select_repeated_ids_by_tag(fold_only_tags); + const bool fold_only_matches = !fold_only_repeated_ids.empty(); + const bool should_run_fold = fold_enabled || fold_only_matches; + const bool should_run_cwai = !fold_enabled && cwai_enabled; + + if (!fold_enabled) { + if (!fold_only_tags.empty()) { + if (cwai_enabled) { + LOG_INFO(::intel_npu::NPUW_FOLD_ONLY().key() + << " is set, so selected-tag FOLD takes precedence over " << ::intel_npu::NPUW_CWAI().key() + << "."); + } + if (!fold_only_matches) { + LOG_INFO("No repeated subgraphs matched " << ::intel_npu::NPUW_FOLD_ONLY().key() + << "; repeated blocks stay unprocessed."); + } + } else if (!cwai_enabled) { + LOG_INFO("Note: Repeated blocks are found in the model " << model->get_friendly_name() + << ", but folding is not enabled"); + } + } + + if (should_run_fold) { + run_fold_pipeline(); + } + if (should_run_cwai) { + // Selected FOLD families are already converted to funcalls, so CWAI only sees the remainder. + run_cwai_pipeline(); } } p.finalizeLinks(); diff --git a/src/plugins/intel_npu/src/plugin/npuw/partitioning/partitioning.hpp b/src/plugins/intel_npu/src/plugin/npuw/partitioning/partitioning.hpp index c8dff61741a6..035c175612ba 100644 --- a/src/plugins/intel_npu/src/plugin/npuw/partitioning/partitioning.hpp +++ b/src/plugins/intel_npu/src/plugin/npuw/partitioning/partitioning.hpp @@ -6,6 +6,7 @@ #include #include +#include #include #include @@ -15,6 +16,7 @@ #include "../moe_transformations/moe_transformation.hpp" #include "../pyramid_attention.hpp" #include "../spatial.hpp" +#include "../v1/subgraph_pipeline.hpp" #include "intel_npu/config/config.hpp" #include "intel_npu/config/npuw.hpp" #include "openvino/openvino.hpp" @@ -73,12 +75,17 @@ struct Subgraph { int64_t idx_idx = -1; }; QuantUnpackGather _quant_unpack_gather; + ov::npuw::v1::subgraphs::FunctionPipeline _pipeline; using Ref = std::reference_wrapper; void settag(const std::string& t) { LOG_DEBUG("Subgraph set-tag=" << t); _tag = t; + _pipeline.registration.patterns.clear(); + if (!t.empty()) { + _pipeline.registration.patterns.push_back(t); + } } std::string gettag() const { return _tag; @@ -107,6 +114,7 @@ struct Function { // MoE expert information - single expert model std::optional _moe_experts; std::optional _moe_experts_downstream; + ov::npuw::v1::subgraphs::FunctionPipeline _pipeline; // FIXME: They should exclude each other (introduce a hierarchy, finally?) // FIXME: shouldn't be here. Needed to not unpack some lazy closures in DCOFF std::set _idx_lazy_unpack; @@ -114,6 +122,10 @@ struct Function { void settag(const std::string& t) { LOG_DEBUG("Function set-tag=" << t); _tag = t; + _pipeline.registration.patterns.clear(); + if (!t.empty()) { + _pipeline.registration.patterns.push_back(t); + } } std::string gettag() const { return _tag; @@ -193,10 +205,8 @@ struct Partitioning { struct PartitioningContext { bool use_host_gather_quant = false; - - // Router model for MoE (shared during partitioning process) - // Will be populated during first pass and used in second pass - mutable std::shared_ptr router_model; + const ov::npuw::v1::subgraphs::PatternRegistry* subgraph_patterns = nullptr; + mutable std::unordered_map> tagged_models; }; Partitioning getPartitioning(const std::shared_ptr& model, diff --git a/src/plugins/intel_npu/src/plugin/npuw/partitioning/patterns/avoid.cpp b/src/plugins/intel_npu/src/plugin/npuw/partitioning/patterns/avoid.cpp index 17906724eaad..1998c95e6f63 100644 --- a/src/plugins/intel_npu/src/plugin/npuw/partitioning/patterns/avoid.cpp +++ b/src/plugins/intel_npu/src/plugin/npuw/partitioning/patterns/avoid.cpp @@ -171,48 +171,6 @@ GemmaRoPE::GemmaRoPE(const std::shared_ptr& snapshot }; register_matcher(std::make_shared(sin_cos, "TagGemmaRoPE"), std::move(callback)); } -//------------------------------------------------------------------------------ -// Pattern: Interpolate in downsampling case -// -// Matches any Interpolate (v4/v11) node. The callback inspects the actual -// partial shapes: if at least one spatial dimension shrinks (input > output), -// the node is marked as "avoid" for the given device. -// -DownsampleInterpolate::DownsampleInterpolate(const std::shared_ptr& snapshot, - const std::string& avoid_device) { - auto interpolate = opp::wrap_type(); - - auto node_to_gptr = snapshot->getNodeToGroupMap(); - - auto callback = [=](ov::pass::pattern::Matcher& m) { - auto& node_to_output = m.get_pattern_value_map(); - auto matched_node = node_to_output.at(interpolate).get_node_shared_ptr(); - - // Compare input and output spatial shapes to detect downsampling - const auto& in_shape = matched_node->get_input_partial_shape(0); - const auto& out_shape = matched_node->get_output_partial_shape(0); - - if (in_shape.rank().is_static() && out_shape.rank().is_static() && - in_shape.rank().get_length() == out_shape.rank().get_length()) { - bool is_downsample = false; - // Check spatial dimensions (skip batch and channel: indices 2..rank-1) - for (auto i = 2; i < in_shape.rank().get_length(); ++i) { - if (in_shape[i].is_static() && out_shape[i].is_static() && - in_shape[i].get_length() > out_shape[i].get_length()) { - is_downsample = true; - break; - } - } - if (is_downsample) { - LOG_DEBUG("Avoiding downsampling Interpolate: " << matched_node->get_friendly_name()); - node_to_gptr->at(matched_node)->avoid(avoid_device); - } - } - - return false; // root hasn't changed - }; - register_matcher(std::make_shared(interpolate, "TagDownsampleInterpolateAvoid"), std::move(callback)); -} //------------------------------------------------------------------------------ // Pattern: FloorMod and its direct input producer diff --git a/src/plugins/intel_npu/src/plugin/npuw/partitioning/patterns/avoid.hpp b/src/plugins/intel_npu/src/plugin/npuw/partitioning/patterns/avoid.hpp index 59a63a091b7e..06cbd18e9dda 100644 --- a/src/plugins/intel_npu/src/plugin/npuw/partitioning/patterns/avoid.hpp +++ b/src/plugins/intel_npu/src/plugin/npuw/partitioning/patterns/avoid.hpp @@ -40,14 +40,6 @@ class GemmaRoPE : public ov::pass::MatcherPass { GemmaRoPE(const std::shared_ptr& snapshot, const std::string& avoid_device); }; -// Pattern: Interpolate in downsampling case (input spatial dims > output spatial dims) -// Matches any Interpolate node and checks at callback time whether it is downsampling. -class DownsampleInterpolate : public ov::pass::MatcherPass { -public: - OPENVINO_MATCHER_PASS_RTTI("npuw::patterns::avoid::DownsampleInterpolate"); - DownsampleInterpolate(const std::shared_ptr& snapshot, const std::string& avoid_device); -}; - // Pattern: FloorMod and its direct input producer — both need FP32 precision. class FloorModFP32 : public ov::pass::MatcherPass { public: diff --git a/src/plugins/intel_npu/src/plugin/npuw/partitioning/patterns/fold_const.cpp b/src/plugins/intel_npu/src/plugin/npuw/partitioning/patterns/fold_const.cpp new file mode 100644 index 000000000000..fa83cd137fd0 --- /dev/null +++ b/src/plugins/intel_npu/src/plugin/npuw/partitioning/patterns/fold_const.cpp @@ -0,0 +1,126 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "fold_const.hpp" + +#include "openvino/core/graph_util.hpp" +#include "openvino/op/concat.hpp" +#include "openvino/op/constant.hpp" +#include "openvino/op/gather.hpp" +#include "openvino/op/shape_of.hpp" +#include "openvino/op/unsqueeze.hpp" +#include "openvino/pass/pattern/op/label.hpp" +#include "openvino/pass/pattern/op/wrap_type.hpp" + +namespace opp = ov::pass::pattern; + +namespace ov { +namespace npuw { +namespace patterns { +namespace util { + +namespace { +// If every input to `node` is an ov::op::v0::Constant and every output shape +// is statically known, evaluate the node and return one folded Constant per +// output port. Returns true on success; `replacements` is populated. +bool fold_if_all_const(const std::shared_ptr& node, ov::OutputVector& replacements) { + ov::TensorVector inputs; + inputs.reserve(node->get_input_size()); + for (size_t i = 0; i < node->get_input_size(); ++i) { + auto c = ov::as_type_ptr(node->get_input_node_shared_ptr(i)); + if (!c) + return false; + inputs.emplace_back(c->get_element_type(), c->get_shape(), const_cast(c->get_data_ptr())); + } + ov::TensorVector outputs; + outputs.reserve(node->get_output_size()); + for (size_t i = 0; i < node->get_output_size(); ++i) { + const auto& pshape = node->get_output_partial_shape(i); + if (pshape.is_dynamic()) + return false; + outputs.emplace_back(node->get_output_element_type(i), pshape.to_shape()); + } + if (!node->evaluate(outputs, inputs)) + return false; + replacements.reserve(outputs.size()); + for (size_t i = 0; i < outputs.size(); ++i) { + auto new_c = std::make_shared(outputs[i]); + new_c->set_friendly_name("NPUW/Folded/" + node->get_friendly_name()); + replacements.push_back(new_c->output(0)); + } + return true; +} +} // namespace + +FoldShapeOf::FoldShapeOf() { + auto shape_of = opp::wrap_type({opp::any_input()}); + + register_matcher(std::make_shared(shape_of, "FoldShapeOf"), [](opp::Matcher& m) { + auto matched_out = m.get_match_root()->output(0); + auto& tensor = matched_out.get_tensor(); + if (!tensor.has_and_set_bound()) + return false; + auto new_c = std::make_shared(tensor.get_upper_value()); + new_c->set_friendly_name("NPUW/Folded/" + m.get_match_root()->get_friendly_name()); + for (auto& input : matched_out.get_target_inputs()) { + input.replace_source_output(new_c); + } + return false; // root itself not replaced, only consumers redirected + }); +} + +FoldGatherOfConst::FoldGatherOfConst() { + auto const_data = opp::wrap_type(); + auto const_idx = opp::wrap_type(); + auto const_axis = opp::wrap_type(); + auto gather = opp::wrap_type({const_data, const_idx, const_axis}); + + register_matcher(std::make_shared(gather, "FoldGatherOfConst"), [](opp::Matcher& m) { + ov::OutputVector replacements; + if (!fold_if_all_const(m.get_match_root(), replacements)) + return false; + ov::replace_node(m.get_match_root(), replacements); + return true; + }); +} + +FoldUnsqueezeOfConst::FoldUnsqueezeOfConst() { + auto const_data = opp::wrap_type(); + auto const_axes = opp::wrap_type(); + auto unsqueeze = opp::wrap_type({const_data, const_axes}); + + register_matcher(std::make_shared(unsqueeze, "FoldUnsqueezeOfConst"), [](opp::Matcher& m) { + ov::OutputVector replacements; + if (!fold_if_all_const(m.get_match_root(), replacements)) + return false; + ov::replace_node(m.get_match_root(), replacements); + return true; + }); +} + +FoldConcatOfConsts::FoldConcatOfConsts() { + auto concat = opp::wrap_type(); + + register_matcher(std::make_shared(concat, "FoldConcatOfConsts"), [](opp::Matcher& m) { + ov::OutputVector replacements; + if (!fold_if_all_const(m.get_match_root(), replacements)) + return false; + ov::replace_node(m.get_match_root(), replacements); + return true; + }); +} + +bool FoldShapeComputeChain::run_on_model(const std::shared_ptr& model) { + ov::pass::GraphRewrite rewr; + rewr.add_matcher(); + rewr.add_matcher(); + rewr.add_matcher(); + rewr.add_matcher(); + return rewr.run_on_model(model); +} + +} // namespace util +} // namespace patterns +} // namespace npuw +} // namespace ov diff --git a/src/plugins/intel_npu/src/plugin/npuw/partitioning/patterns/fold_const.hpp b/src/plugins/intel_npu/src/plugin/npuw/partitioning/patterns/fold_const.hpp new file mode 100644 index 000000000000..9bdf63c3c060 --- /dev/null +++ b/src/plugins/intel_npu/src/plugin/npuw/partitioning/patterns/fold_const.hpp @@ -0,0 +1,68 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#pragma once + +#include "openvino/pass/graph_rewrite.hpp" +#include "openvino/pass/pass.hpp" + +// Lightweight constant-folding passes for shape-compute chains that appear in +// extracted MoE expert subgraphs after RegularizeSDPA folds ShapeOf(param) +// into a Constant. +// +// Target chain (each step enables the next): +// ShapeOf(param) --[RegularizeSDPA/ShapeOfParameter]--> Const +// ShapeOf(any) --[FoldShapeOf]--> Const (bound-based, no input constraint) +// Gather(C,C,C) --[FoldGatherOfConst]--> Const +// Unsqueeze(C,C) --[FoldUnsqueezeOfConst]--> Const +// Concat(C...) --[FoldConcatOfConsts]--> Const + +namespace ov { +namespace npuw { +namespace patterns { +namespace util { + +// Fold ShapeOf(any) → Constant when the output tensor has a known upper bound. +// More general than RegularizeSDPA::ShapeOfParameter: no constraint on input type. +class FoldShapeOf : public ov::pass::MatcherPass { +public: + OPENVINO_MATCHER_PASS_RTTI("npuw::patterns::util::FoldShapeOf"); + FoldShapeOf(); +}; + +// Fold Gather(Constant, Constant, Constant) → Constant. +class FoldGatherOfConst : public ov::pass::MatcherPass { +public: + OPENVINO_MATCHER_PASS_RTTI("npuw::patterns::util::FoldGatherOfConst"); + FoldGatherOfConst(); +}; + +// Fold Unsqueeze(Constant, Constant) → Constant. +class FoldUnsqueezeOfConst : public ov::pass::MatcherPass { +public: + OPENVINO_MATCHER_PASS_RTTI("npuw::patterns::util::FoldUnsqueezeOfConst"); + FoldUnsqueezeOfConst(); +}; + +// Fold Concat whose every input is a Constant → Constant. +// The pattern matches any Concat; the callback enforces the all-constant +// precondition so no variant for each arity is needed. +class FoldConcatOfConsts : public ov::pass::MatcherPass { +public: + OPENVINO_MATCHER_PASS_RTTI("npuw::patterns::util::FoldConcatOfConsts"); + FoldConcatOfConsts(); +}; + +// Runs the full shape-compute-chain folding pipeline in a single pass: +// FoldShapeOf → FoldGatherOfConst → FoldUnsqueezeOfConst → FoldConcatOfConsts. +class FoldShapeComputeChain : public ov::pass::ModelPass { +public: + OPENVINO_RTTI("npuw::patterns::util::FoldShapeComputeChain"); + bool run_on_model(const std::shared_ptr& model) override; +}; + +} // namespace util +} // namespace patterns +} // namespace npuw +} // namespace ov diff --git a/src/plugins/intel_npu/src/plugin/npuw/partitioning/patterns/moe.cpp b/src/plugins/intel_npu/src/plugin/npuw/partitioning/patterns/moe.cpp index 32358073fd97..7b5d4e8d664e 100644 --- a/src/plugins/intel_npu/src/plugin/npuw/partitioning/patterns/moe.cpp +++ b/src/plugins/intel_npu/src/plugin/npuw/partitioning/patterns/moe.cpp @@ -17,6 +17,78 @@ namespace moe { namespace opp = ov::pass::pattern; +namespace { + +using NodeToGroupMapPtr = ov::npuw::online::detail::OVNodeToGroupMapPtr; + +void isolate_node(const std::shared_ptr& node, + const std::string& isol_tag, + const NodeToGroupMapPtr& node_to_gptr) { + if (node && node_to_gptr->count(node)) { + node_to_gptr->at(node)->isolate(isol_tag); + } +} + +// Scan output_multiply's middle dimensions (all dims except dim-0=num_experts and last=hidden). +// If any middle dim > 1 it is the token count (prefill). If all are 1, it is decoding. +// Shape layouts: GPT-OSS [N, token, 1, H] / Qwen3 [N, 1, token, H] +bool is_decoding_stage(const std::shared_ptr& output_multiply) { + const auto shape = output_multiply->get_output_partial_shape(0); + if (!shape.rank().is_static()) { + return false; + } + const auto rank = shape.rank().get_length(); + for (int64_t i = 1; i < rank - 1; ++i) { + const auto& d = shape[i]; + if (d.is_static() && d.get_length() != 1) { + return false; // prefill: found token dim > 1 + } + } + return true; // all middle dims are 1 → decoding +} + +// Find the first consumer of `node` satisfying `pred`. Returns nullptr if not found. +template +std::shared_ptr find_consumer_by_type(const std::shared_ptr& node, Pred&& pred) { + for (auto& output : node->outputs()) { + for (auto& input : output.get_target_inputs()) { + auto consumer = input.get_node()->shared_from_this(); + if (pred(consumer)) { + return consumer; + } + } + } + return nullptr; +} + +// After output_multiply, find the first ReduceSum consumer and isolate it. +void isolate_reduce_sum_after(const std::shared_ptr& output_multiply, + const std::string& isol_tag, + const NodeToGroupMapPtr& node_to_gptr) { + std::shared_ptr reduce_sum; + for (auto& out : output_multiply->outputs()) { + for (auto& in : out.get_target_inputs()) { + auto consumer = in.get_node()->shared_from_this(); + if (std::dynamic_pointer_cast(consumer)) { + reduce_sum = consumer; + break; + } + } + if (reduce_sum) + break; + } + if (reduce_sum && node_to_gptr->count(reduce_sum)) { + node_to_gptr->at(reduce_sum)->isolate(isol_tag); + LOG_DEBUG(" ReduceSum successfully isolated"); + } else if (reduce_sum) { + LOG_WARN(" ReduceSum found but not in node_to_gptr map"); + } else { + LOG_WARN(" No ReduceSum found after Multiply (unexpected for decoding stage)"); + } +} + +} // namespace + /* GPT-OSS Expert Pattern: @@ -98,17 +170,8 @@ GPTOSSExpert::GPTOSSExpert(const std::shared_ptr& sn auto callback = [=](ov::pass::pattern::Matcher& m) { auto& node_to_output = m.get_pattern_value_map(); - auto matched_tile = node_to_output.at(tile).get_node_shared_ptr(); - auto matched_reshape1 = node_to_output.at(reshape1).get_node_shared_ptr(); - auto matched_weights_multiply1 = node_to_output.at(weights_multiply1).get_node_shared_ptr(); - auto matched_weights_convert1 = node_to_output.at(weights_convert1).get_node_shared_ptr(); - auto matched_matmul1 = node_to_output.at(matmul1).get_node_shared_ptr(); - auto matched_add1 = node_to_output.at(add1).get_node_shared_ptr(); - auto matched_slice = node_to_output.at(slice).get_node_shared_ptr(); - auto matched_minimum = node_to_output.at(minimum).get_node_shared_ptr(); - auto matched_swish = node_to_output.at(swish).get_node_shared_ptr(); - // Check if optional AWQ multiply was matched + auto matched_swish = node_to_output.at(swish).get_node_shared_ptr(); std::shared_ptr matched_awq_multiply = nullptr; if (node_to_output.count(awq_multiply) > 0) { auto awq_multiply_value = node_to_output.at(awq_multiply); @@ -121,88 +184,46 @@ GPTOSSExpert::GPTOSSExpert(const std::shared_ptr& sn LOG_DEBUG("Normal model: No AWQ multiply after Swish"); } - auto matched_other_slice = node_to_output.at(other_slice).get_node_shared_ptr(); - auto matched_clamp = node_to_output.at(clamp).get_node_shared_ptr(); - auto matched_add2 = node_to_output.at(add2).get_node_shared_ptr(); - auto matched_multiply1 = node_to_output.at(multiply1).get_node_shared_ptr(); - auto matched_weights_multiply2 = node_to_output.at(weights_multiply2).get_node_shared_ptr(); - auto matched_weights_convert2 = node_to_output.at(weights_convert2).get_node_shared_ptr(); - auto matched_matmul2 = node_to_output.at(matmul2).get_node_shared_ptr(); - auto matched_add3 = node_to_output.at(add3).get_node_shared_ptr(); - auto matched_reshape2 = node_to_output.at(reshape2).get_node_shared_ptr(); auto matched_output_multiply = node_to_output.at(output_multiply).get_node_shared_ptr(); - // Check if this is decoding stage by examining shape[rank-2] - auto output_shape = matched_output_multiply->get_output_partial_shape(0); - LOG_DEBUG("Expert Multiply output_shape: " << output_shape); - bool is_decoding = false; + LOG_DEBUG("Expert Multiply output_shape: " << matched_output_multiply->get_output_partial_shape(0)); + const bool is_decoding = is_decoding_stage(matched_output_multiply); + LOG_DEBUG("GPT-OSS Expert pattern matched (" << (is_decoding ? "Decoding" : "Prefill") << " stage)"); - if (output_shape.rank().is_static() && output_shape.rank().get_length() >= 2) { - auto rank = output_shape.rank().get_length(); - auto token_dim = output_shape[rank - 2]; - - if (token_dim.is_static() && token_dim.get_length() == 1) { - is_decoding = true; - LOG_DEBUG("GPT-OSS Expert pattern matched (Decoding stage): single token"); - } else if (token_dim.is_static()) { - LOG_DEBUG("GPT-OSS Expert pattern matched (Prefill stage): token_count=" << token_dim.get_length()); - } - } + auto isolate = [&](const std::shared_ptr& pattern_node) { + isolate_node(node_to_output.at(pattern_node).get_node_shared_ptr(), isol_tag, node_to_gptr); + }; - // Isolate all common expert nodes - node_to_gptr->at(matched_tile)->isolate(isol_tag); - node_to_gptr->at(matched_reshape1)->isolate(isol_tag); - node_to_gptr->at(matched_weights_multiply1)->isolate(isol_tag); - node_to_gptr->at(matched_weights_convert1)->isolate(isol_tag); - node_to_gptr->at(matched_matmul1)->isolate(isol_tag); - node_to_gptr->at(matched_add1)->isolate(isol_tag); - node_to_gptr->at(matched_slice)->isolate(isol_tag); - node_to_gptr->at(matched_minimum)->isolate(isol_tag); - node_to_gptr->at(matched_swish)->isolate(isol_tag); + isolate(tile); + isolate(reshape1); + isolate(weights_multiply1); + isolate(weights_convert1); + isolate(matmul1); + isolate(add1); + isolate(slice); + isolate(minimum); + isolate_node(matched_swish, isol_tag, node_to_gptr); // Isolate AWQ multiply if it exists - if (matched_awq_multiply && node_to_gptr->count(matched_awq_multiply)) { - node_to_gptr->at(matched_awq_multiply)->isolate(isol_tag); + if (matched_awq_multiply) { + isolate_node(matched_awq_multiply, isol_tag, node_to_gptr); LOG_DEBUG("AWQ multiply after Swish isolated"); } - node_to_gptr->at(matched_other_slice)->isolate(isol_tag); - node_to_gptr->at(matched_clamp)->isolate(isol_tag); - node_to_gptr->at(matched_add2)->isolate(isol_tag); - node_to_gptr->at(matched_multiply1)->isolate(isol_tag); - node_to_gptr->at(matched_weights_multiply2)->isolate(isol_tag); - node_to_gptr->at(matched_weights_convert2)->isolate(isol_tag); - node_to_gptr->at(matched_matmul2)->isolate(isol_tag); - node_to_gptr->at(matched_add3)->isolate(isol_tag); - node_to_gptr->at(matched_reshape2)->isolate(isol_tag); - node_to_gptr->at(matched_output_multiply)->isolate(isol_tag); - - // If decoding stage, find and isolate ReduceSum after Multiply + isolate(other_slice); + isolate(clamp); + isolate(add2); + isolate(multiply1); + isolate(weights_multiply2); + isolate(weights_convert2); + isolate(matmul2); + isolate(add3); + isolate(reshape2); + isolate_node(matched_output_multiply, isol_tag, node_to_gptr); + if (is_decoding) { LOG_DEBUG("Decoding stage detected, searching for ReduceSum to isolate..."); - std::shared_ptr matched_reduce_sum = nullptr; - - for (auto& output : matched_output_multiply->outputs()) { - for (auto& input : output.get_target_inputs()) { - auto consumer = input.get_node()->shared_from_this(); - if (auto reduce_sum = std::dynamic_pointer_cast(consumer)) { - matched_reduce_sum = reduce_sum; - LOG_DEBUG(" Found ReduceSum after Multiply, isolating for decoding stage"); - break; - } - } - if (matched_reduce_sum) - break; - } - - if (matched_reduce_sum && node_to_gptr->count(matched_reduce_sum)) { - node_to_gptr->at(matched_reduce_sum)->isolate(isol_tag); - LOG_DEBUG(" ReduceSum successfully isolated"); - } else if (matched_reduce_sum) { - LOG_WARN(" ReduceSum found but not in node_to_gptr map"); - } else { - LOG_WARN(" No ReduceSum found after Multiply (unexpected for decoding stage)"); - } + isolate_reduce_sum_after(matched_output_multiply, isol_tag, node_to_gptr); } return false; @@ -212,29 +233,39 @@ GPTOSSExpert::GPTOSSExpert(const std::shared_ptr& sn } /* - GPT-OSS Router Pattern: + GPT-OSS Router Pattern (constant-folded variant): + + All ShapeOf nodes in the router are constant-folded before this pattern runs. + The resulting graph is: + + weights -> Multiply -> Convert -> MatMul -> Add -> TopK + |\-> output(0) values -> Softmax -> Slice + \-> output(1) indices -> Convert (indices) + + Slice -> ScatterElementsUpdate -> Transpose -> Reshape -> Unsqueeze + Broadcast (zero base for Scatter, shape fed by folded Const) - Matches MoE router layer (16 nodes: 9 pattern-matched + 7 manual retrieval) - Pattern: weights -> Multiply -> Convert -> MatMul -> Add -> TopK -> Softmax -> Slice - \-> Convert -> ShapeOf -/ - Manual: Add -> ShapeOf -> Broadcast -> Scatter -> Transpose -> Reshape -> Unsqueeze + Pattern-matched (6): Multiply, Convert, MatMul, Add, TopK, Softmax, Slice + Manually retrieved (4): topk_convert (indices Convert), Broadcast, Scatter, + Transpose, Reshape, Unsqueeze */ GPTOSSRouter::GPTOSSRouter(const std::shared_ptr& snapshot, const std::string& isol_tag) { LOG_DEBUG("GPTOSSRouter pattern matcher registered with tag: " << isol_tag); - // Pattern-matched nodes (9 total) + // Pattern-matched nodes (7): Multiply, Convert, MatMul, Add, TopK, Softmax, Slice + // topk_convert (indices Convert) is retrieved manually - TopK has two outputs and + // wrap_type always binds output(0), so output(1)->Convert cannot be expressed inline. auto weights_multiply = opp::wrap_type({opp::any_input(), opp::any_input()}); auto weights_convert2 = opp::wrap_type({weights_multiply}); auto matmul = opp::wrap_type({opp::any_input(), weights_convert2}); auto add = opp::wrap_type({matmul, opp::any_input()}); auto topk = opp::wrap_type({add, opp::any_input()}); - auto softmax = opp::wrap_type({topk}); - auto topk_convert = opp::wrap_type({topk}); - auto shapeof_topk = opp::wrap_type({topk_convert}); + auto softmax = opp::wrap_type({topk}); // connects to topk->output(0) - // Pattern root + // Pattern root: Slice data input is Softmax output; all shape inputs are any_input() + // because ShapeOf nodes are constant-folded before this pattern runs. auto slice = opp::wrap_type( - {softmax, opp::any_input(), shapeof_topk, opp::any_input(), opp::any_input()}); + {softmax, opp::any_input(), opp::any_input(), opp::any_input(), opp::any_input()}); auto node_to_gptr = snapshot->getNodeToGroupMap(); @@ -258,31 +289,21 @@ GPTOSSRouter::GPTOSSRouter(const std::shared_ptr& sn LOG_DEBUG("GPT-OSS Router pattern matched: " << topk_name); - // Get pattern-matched nodes - auto matched_weights_multiply = node_to_output.at(weights_multiply).get_node_shared_ptr(); - auto matched_weights_convert2 = node_to_output.at(weights_convert2).get_node_shared_ptr(); - auto matched_matmul = node_to_output.at(matmul).get_node_shared_ptr(); - auto matched_add = node_to_output.at(add).get_node_shared_ptr(); - auto matched_softmax = node_to_output.at(softmax).get_node_shared_ptr(); - auto matched_topk_convert = node_to_output.at(topk_convert).get_node_shared_ptr(); - auto matched_shapeof_topk = node_to_output.at(shapeof_topk).get_node_shared_ptr(); + // Get pattern-matched nodes needed for manual retrieval auto matched_slice = node_to_output.at(slice).get_node_shared_ptr(); - // Manual retrieval (7 nodes): helper function - auto find_consumer_by_type = - [](const std::shared_ptr& node, - const std::function&)>& pred) -> std::shared_ptr { - for (auto& output : node->outputs()) { - for (auto& input : output.get_target_inputs()) { - auto consumer = input.get_node()->shared_from_this(); - if (pred(consumer)) { - return consumer; - } - } + // topk_convert: Convert on TopK indices output (output(1)). Not in the formal + // pattern because TopK has two outputs and wrap_type always binds output(0). + std::shared_ptr matched_topk_convert = nullptr; + for (auto& target : matched_topk->output(1).get_target_inputs()) { + auto consumer = target.get_node()->shared_from_this(); + if (std::dynamic_pointer_cast(consumer)) { + matched_topk_convert = consumer; + break; } - return nullptr; - }; + } + // Manual retrieval (7 nodes): helper function auto matched_scatter = find_consumer_by_type(matched_slice, [](const std::shared_ptr& n) { return std::dynamic_pointer_cast(n) || std::dynamic_pointer_cast(n); @@ -300,19 +321,6 @@ GPTOSSRouter::GPTOSSRouter(const std::shared_ptr& sn return false; } - std::shared_ptr matched_shapeof = nullptr; - for (size_t i = 0; i < matched_broadcast->inputs().size(); ++i) { - auto input_node = matched_broadcast->input_value(i).get_node_shared_ptr(); - if (std::dynamic_pointer_cast(input_node)) { - matched_shapeof = input_node; - break; - } - } - if (!matched_shapeof) { - LOG_DEBUG("Router pattern: ShapeOf not found"); - return false; - } - // Retrieve output chain (Transpose -> Reshape -> Unsqueeze) auto matched_transpose = find_consumer_by_type(matched_scatter, [](const std::shared_ptr& n) { return std::dynamic_pointer_cast(n) != nullptr; @@ -339,27 +347,23 @@ GPTOSSRouter::GPTOSSRouter(const std::shared_ptr& sn } // Isolate all 16 nodes - auto isolate_if_exists = [&](const std::shared_ptr& node) { - if (node && node_to_gptr->count(node)) { - node_to_gptr->at(node)->isolate(isol_tag); - } + auto isolate = [&](const std::shared_ptr& pattern_node) { + isolate_node(node_to_output.at(pattern_node).get_node_shared_ptr(), isol_tag, node_to_gptr); }; - isolate_if_exists(matched_weights_multiply); - isolate_if_exists(matched_weights_convert2); - isolate_if_exists(matched_matmul); - isolate_if_exists(matched_add); - isolate_if_exists(matched_topk); - isolate_if_exists(matched_softmax); - isolate_if_exists(matched_topk_convert); - isolate_if_exists(matched_shapeof_topk); - isolate_if_exists(matched_slice); - isolate_if_exists(matched_shapeof); - isolate_if_exists(matched_broadcast); - isolate_if_exists(matched_scatter); - isolate_if_exists(matched_transpose); - isolate_if_exists(matched_reshape); - isolate_if_exists(matched_unsqueeze); + isolate(weights_multiply); + isolate(weights_convert2); + isolate(matmul); + isolate(add); + isolate_node(matched_topk, isol_tag, node_to_gptr); + isolate(softmax); + isolate_node(matched_topk_convert, isol_tag, node_to_gptr); + isolate_node(matched_slice, isol_tag, node_to_gptr); + isolate_node(matched_broadcast, isol_tag, node_to_gptr); + isolate_node(matched_scatter, isol_tag, node_to_gptr); + isolate_node(matched_transpose, isol_tag, node_to_gptr); + isolate_node(matched_reshape, isol_tag, node_to_gptr); + isolate_node(matched_unsqueeze, isol_tag, node_to_gptr); LOG_DEBUG("Router pattern isolated"); return false; @@ -368,6 +372,204 @@ GPTOSSRouter::GPTOSSRouter(const std::shared_ptr& sn register_matcher(std::make_shared(slice, "TagGPTOSSRouter"), std::move(callback)); } +/* + Qwen3 Expert Pattern: + + Input preparation: + Tile -> Reshape1 + + Gate projection (SwiGLU gate branch): + Reshape1 -> MatMul_gate (with weights: Multiply -> Convert) -> Swish + + Up projection (SwiGLU up branch): + Reshape1 -> MatMul_up (with weights: Multiply -> Convert) + + SwiGLU merge: + Swish + MatMul_up -> Multiply_swiglu + + Down projection: + Multiply_swiglu -> MatMul_down (with weights: Multiply -> Convert) -> Reshape2 + + Output (scaled by router scores): + Reshape2 * router_score -> Multiply_output <-- pattern root + (router_score = opp::any_input(), produced entirely by Qwen3Router) + + Isolation boundary: + - Expert claims: Tile, Reshape1, gate/up/down MatMuls+weights, SwiGLU Multiply, Reshape2, output Multiply + - Router claims: Softmax, TopK, ReduceSum, Divide, ScatterElementsUpdate, Transpose, Reshape_score, Unsqueeze_score + - Shared shape-compute nodes (ShapeOf->Gather->Unsqueeze->Concat chains) stay outside both, + becoming parameter inputs at subgraph boundaries. +*/ +Qwen3Expert::Qwen3Expert(const std::shared_ptr& snapshot, const std::string& isol_tag) { + LOG_DEBUG("Qwen3Expert pattern matcher registered with tag: " << isol_tag); + + // Input preparation: Tile -> Reshape + auto tile = opp::wrap_type({opp::any_input(), opp::any_input()}); + auto reshape1 = opp::wrap_type({tile, opp::any_input()}); + + // Gate projection weights: Multiply(quantized weight, scale) -> Convert + auto gate_weights_multiply = opp::wrap_type({opp::any_input(), opp::any_input()}); + auto gate_weights_convert = opp::wrap_type({gate_weights_multiply}); + // Gate MatMul + Swish activation + auto matmul_gate = opp::wrap_type({reshape1, gate_weights_convert}); + auto swish = opp::wrap_type({matmul_gate}); + + // Up projection weights: Multiply(quantized weight, scale) -> Convert + auto up_weights_multiply = opp::wrap_type({opp::any_input(), opp::any_input()}); + auto up_weights_convert = opp::wrap_type({up_weights_multiply}); + // Up MatMul + auto matmul_up = opp::wrap_type({reshape1, up_weights_convert}); + + // SwiGLU: gate * up + auto multiply_swiglu = opp::wrap_type({swish, matmul_up}); + + // Down projection weights: Multiply(quantized weight, scale) -> Convert + auto down_weights_multiply = opp::wrap_type({opp::any_input(), opp::any_input()}); + auto down_weights_convert = opp::wrap_type({down_weights_multiply}); + // Down MatMul -> Reshape + auto matmul_down = opp::wrap_type({multiply_swiglu, down_weights_convert}); + auto reshape2 = opp::wrap_type({matmul_down, opp::any_input()}); + + // Pattern root: expert_output * router_score + // The router score (Unsqueeze output) is produced entirely by Qwen3Router and flows + // in as opp::any_input() here to avoid double-claiming shared nodes. + auto output_multiply = opp::wrap_type({reshape2, opp::any_input()}); + + auto node_to_gptr = snapshot->getNodeToGroupMap(); + + auto callback = [=](ov::pass::pattern::Matcher& m) { + auto& node_to_output = m.get_pattern_value_map(); + + auto matched_tile = node_to_output.at(tile).get_node_shared_ptr(); + auto matched_output_multiply = node_to_output.at(output_multiply).get_node_shared_ptr(); + + LOG_DEBUG("Qwen3Expert pattern matched: " << matched_tile->get_friendly_name()); + + LOG_DEBUG("Qwen3 Expert Multiply output_shape: " << matched_output_multiply->get_output_partial_shape(0)); + const bool is_decoding = is_decoding_stage(matched_output_multiply); + LOG_DEBUG("Qwen3 Expert pattern matched (" << (is_decoding ? "Decoding" : "Prefill") << " stage)"); + + auto isolate = [&](const std::shared_ptr& pattern_node) { + isolate_node(node_to_output.at(pattern_node).get_node_shared_ptr(), isol_tag, node_to_gptr); + }; + + isolate(tile); + isolate(reshape1); + isolate(gate_weights_multiply); + isolate(gate_weights_convert); + isolate(matmul_gate); + isolate(swish); + isolate(up_weights_multiply); + isolate(up_weights_convert); + isolate(matmul_up); + isolate(multiply_swiglu); + isolate(down_weights_multiply); + isolate(down_weights_convert); + isolate(matmul_down); + isolate(reshape2); + isolate_node(matched_output_multiply, isol_tag, node_to_gptr); + + if (is_decoding) { + LOG_DEBUG("Decoding stage detected, searching for ReduceSum to isolate..."); + isolate_reduce_sum_after(matched_output_multiply, isol_tag, node_to_gptr); + } + + return false; + }; + + register_matcher(std::make_shared(output_multiply, "TagQwen3Expert"), std::move(callback)); +} + +/* + Qwen3 Router Pattern: + + Router weights (quantized, dequantized via weight chain): + Convert(weight) -> Multiply(weight, scale) -> Convert -> MatMul(input, weight) + + Score computation: + MatMul -> Softmax -> TopK(values, indices) + + Score normalization: + TopK(values) -> ReduceSum -> Divide(values, sum) [renormalize over K selected] + + Scatter to full expert dimension: + TopK(indices) + Divide(scores) -> ScatterElementsUpdate(zero_broadcast, indices, scores) + + Shape to [num_experts, token_count, 1, 1] for expert broadcast: + ScatterElementsUpdate -> Transpose -> Reshape -> Unsqueeze <-- pattern root + + Note: The Unsqueeze output is consumed by Qwen3Expert's Multiply_output node. + Key difference from GPT-OSS: Softmax is BEFORE TopK (not after), + requiring explicit renormalization via ReduceSum->Divide. +*/ +Qwen3Router::Qwen3Router(const std::shared_ptr& snapshot, const std::string& isol_tag) { + LOG_DEBUG("Qwen3Router pattern matcher registered with tag: " << isol_tag); + + // Router weights: Convert(weight) -> Multiply(weight, scale) -> Convert -> MatMul + auto weights_convert_in = opp::wrap_type({opp::any_input()}); + auto weights_multiply = opp::wrap_type({weights_convert_in, opp::any_input()}); + auto weights_convert_out = opp::wrap_type({weights_multiply}); + auto matmul = opp::wrap_type({opp::any_input(), weights_convert_out}); + + // Score: Softmax -> TopK + auto softmax = opp::wrap_type({matmul}); + auto topk = opp::wrap_type({softmax, opp::any_input()}); + + // Renormalization: TopK(values)->ReduceSum, TopK(values)/ReduceSum = Divide + auto reduce_sum = opp::wrap_type({topk, opp::any_input()}); + auto divide = opp::wrap_type({topk, reduce_sum}); + + // Scatter to full expert shape (pattern root = Unsqueeze) + auto scatter = + opp::wrap_type({opp::any_input(), topk, divide, opp::any_input()}); + auto transpose = opp::wrap_type({scatter, opp::any_input()}); + auto reshape = opp::wrap_type({transpose, opp::any_input()}); + auto unsqueeze = opp::wrap_type({reshape, opp::any_input()}); + + auto node_to_gptr = snapshot->getNodeToGroupMap(); + + auto callback = [=](ov::pass::pattern::Matcher& m) { + auto& node_to_output = m.get_pattern_value_map(); + + // Validate: TopK should be MAX mode (selecting top-K experts) + auto matched_topk = node_to_output.at(topk).get_node_shared_ptr(); + auto topk_node = std::dynamic_pointer_cast(matched_topk); + if (!topk_node || topk_node->get_mode() != ov::op::v11::TopK::Mode::MAX) { + return false; + } + + LOG_DEBUG("Qwen3Router pattern matched: " << matched_topk->get_friendly_name()); + + auto matched_scatter = node_to_output.at(scatter).get_node_shared_ptr(); + + // Also isolate Broadcast node that provides zero-filled base for ScatterElementsUpdate + auto broadcast_node = matched_scatter->input_value(0).get_node_shared_ptr(); + auto matched_broadcast = std::dynamic_pointer_cast(broadcast_node); + + auto isolate = [&](const std::shared_ptr& pattern_node) { + isolate_node(node_to_output.at(pattern_node).get_node_shared_ptr(), isol_tag, node_to_gptr); + }; + + isolate(weights_convert_in); + isolate(weights_multiply); + isolate(weights_convert_out); + isolate(matmul); + isolate(softmax); + isolate_node(matched_topk, isol_tag, node_to_gptr); + isolate(reduce_sum); + isolate(divide); + isolate_node(matched_broadcast, isol_tag, node_to_gptr); + isolate_node(matched_scatter, isol_tag, node_to_gptr); + isolate(transpose); + isolate(reshape); + isolate(unsqueeze); + + return false; + }; + + register_matcher(std::make_shared(unsqueeze, "TagQwen3Router"), std::move(callback)); +} + } // namespace moe } // namespace patterns } // namespace npuw diff --git a/src/plugins/intel_npu/src/plugin/npuw/partitioning/patterns/moe.hpp b/src/plugins/intel_npu/src/plugin/npuw/partitioning/patterns/moe.hpp index 91d1bfb104c8..8c3690759669 100644 --- a/src/plugins/intel_npu/src/plugin/npuw/partitioning/patterns/moe.hpp +++ b/src/plugins/intel_npu/src/plugin/npuw/partitioning/patterns/moe.hpp @@ -29,15 +29,63 @@ constexpr const char* MLP_EXPERT_NAME = ".mlp.expert"; class GPTOSSExpert : public ov::pass::MatcherPass { public: OPENVINO_MATCHER_PASS_RTTI("npuw::patterns::moe::GPTOSSExpert"); + static constexpr const char* pattern_name() { + return "GPTOSSExpert"; + } + static constexpr const char* isolation_tag() { + return EXPERT_TAG; + } + static constexpr const char* group_name() { + return "moe"; + } GPTOSSExpert(const std::shared_ptr& snapshot, const std::string& isol_tag); }; class GPTOSSRouter : public ov::pass::MatcherPass { public: OPENVINO_MATCHER_PASS_RTTI("npuw::patterns::moe::GPTOSSRouter"); + static constexpr const char* pattern_name() { + return "GPTOSSRouter"; + } + static constexpr const char* isolation_tag() { + return ROUTER_TAG; + } + static constexpr const char* group_name() { + return "moe"; + } GPTOSSRouter(const std::shared_ptr& snapshot, const std::string& isol_tag); }; +class Qwen3Expert : public ov::pass::MatcherPass { +public: + OPENVINO_MATCHER_PASS_RTTI("npuw::patterns::moe::Qwen3Expert"); + static constexpr const char* pattern_name() { + return "Qwen3Expert"; + } + static constexpr const char* isolation_tag() { + return EXPERT_TAG; + } + static constexpr const char* group_name() { + return "moe"; + } + Qwen3Expert(const std::shared_ptr& snapshot, const std::string& isol_tag); +}; + +class Qwen3Router : public ov::pass::MatcherPass { +public: + OPENVINO_MATCHER_PASS_RTTI("npuw::patterns::moe::Qwen3Router"); + static constexpr const char* pattern_name() { + return "Qwen3Router"; + } + static constexpr const char* isolation_tag() { + return ROUTER_TAG; + } + static constexpr const char* group_name() { + return "moe"; + } + Qwen3Router(const std::shared_ptr& snapshot, const std::string& isol_tag); +}; + } // namespace moe } // namespace patterns } // namespace npuw diff --git a/src/plugins/intel_npu/src/plugin/npuw/partitioning/patterns/opt.cpp b/src/plugins/intel_npu/src/plugin/npuw/partitioning/patterns/opt.cpp index a0a941f55e22..d96a2f37749d 100644 --- a/src/plugins/intel_npu/src/plugin/npuw/partitioning/patterns/opt.cpp +++ b/src/plugins/intel_npu/src/plugin/npuw/partitioning/patterns/opt.cpp @@ -1916,7 +1916,7 @@ CompressDictMatMulf32::CompressDictMatMulf32(Context::Ref ctx) { // Const(W) -> to(f16) -> // Const(Z) -> to(f16) -> Subtract -// Const(S) ---------------------> Multiply -> to(f32) -> MatMul -> Result +// Const(S) ---------------------> Multiply -> [to(f32) ->] MatMul -> Result // ???(Act) --------------------------------------------> PreserveConstDictMatMulAsymm::PreserveConstDictMatMulAsymm(Context::Ref ctx, @@ -1928,7 +1928,8 @@ PreserveConstDictMatMulAsymm::PreserveConstDictMatMulAsymm(Context::Ref ctx, auto qcvtz = opp::wrap_type({qzerop}); auto qsub = opp::wrap_type({qcvtw, qcvtz}); auto qmuls = opp::wrap_type({qsub, qcoeff}); - auto qcvtm = opp::wrap_type({qmuls}); + // The Convert between Multiply and MatMul is optional (some models omit it when Multiply is already f32) + auto qcvtm = opp::optional({qmuls}); auto qmmi = opp::any_input(); auto qmm = opp::wrap_type({qmmi, qcvtm}); std::shared_ptr qres; @@ -1963,8 +1964,13 @@ PreserveConstDictMatMulAsymm::PreserveConstDictMatMulAsymm(Context::Ref ctx, auto qcoeff_shape = matched_qcoeff->output(0).get_shape(); - if (ov::element::u8 == matched_qweight->get_element_type() && qcoeff_shape[1] == 1 && - !matched_matmul->get_transpose_a() && matched_matmul->get_transpose_b()) { + // Standard layout: weight [OC, IC], scale [OC, 1], transpose_b=true + const bool standard_layout = qcoeff_shape.size() == 2 && qcoeff_shape[1] == 1 && + !matched_matmul->get_transpose_a() && matched_matmul->get_transpose_b(); + // Pre-transposed layout: weight [IC, OC], scale [1, OC], transpose_b=false + const bool pretransposed_layout = qcoeff_shape.size() == 2 && qcoeff_shape[0] == 1 && + !matched_matmul->get_transpose_a() && !matched_matmul->get_transpose_b(); + if (ov::element::u8 == matched_qweight->get_element_type() && (standard_layout || pretransposed_layout)) { to_keep.get().push_back(matched_qweight); to_keep.get().push_back(matched_qzerop); to_keep.get().push_back(matched_qcoeff); diff --git a/src/plugins/intel_npu/src/plugin/npuw/partitioning/patterns/pre_compute.cpp b/src/plugins/intel_npu/src/plugin/npuw/partitioning/patterns/pre_compute.cpp index 912091cd9e90..40b9b1bc8d0b 100644 --- a/src/plugins/intel_npu/src/plugin/npuw/partitioning/patterns/pre_compute.cpp +++ b/src/plugins/intel_npu/src/plugin/npuw/partitioning/patterns/pre_compute.cpp @@ -43,6 +43,7 @@ static ov::OutputVector makeCosSinCache(const size_t max_position_embeddings, psin[k + rotary_ndims / 2] = psin[k]; } } + auto Cos = ov::op::v0::Constant::create(ov::element::f16, ov::Shape({1, max_position_embeddings, rotary_ndims}), lut_cos); auto Sin = @@ -51,6 +52,35 @@ static ov::OutputVector makeCosSinCache(const size_t max_position_embeddings, return {Cos, Sin}; } +static ov::NodeVector calculate_freq(const std::shared_ptr short_factor_node, + const std::shared_ptr long_factor_node, + const std::shared_ptr multiply_node, + const std::shared_ptr power_node) { + const auto short_factor = ov::as_type_ptr(short_factor_node)->cast_vector(); + const auto long_factor = ov::as_type_ptr(long_factor_node)->cast_vector(); + const auto multiply_const = ov::as_type_ptr(multiply_node)->cast_vector(); + const auto power_const = ov::as_type_ptr(power_node)->cast_vector(); + auto factor_size = short_factor.size(); + + OPENVINO_ASSERT(short_factor.size() == multiply_const.size() && long_factor.size() == multiply_const.size(), + "Invalid constants for LongRopePatternPhi_v5, expected same size for short_factor, long_factor " + "and multiply_const"); + OPENVINO_ASSERT(power_const.size() == 1, + "Invalid constants for LongRopePatternPhi_v5, expected single value for power_const"); + + std::vector freq(factor_size, 0.0f); + std::vector freq_long(factor_size, 0.0f); + for (size_t i = 0; i < factor_size; i++) { + freq[i] = std::pow(short_factor[i] * multiply_const[i], power_const[0]); + freq_long[i] = std::pow(long_factor[i] * multiply_const[i], power_const[0]); + } + + auto inv_freq = std::make_shared(ov::element::f32, ov::Shape({factor_size}), freq); + auto inv_freq_long = std::make_shared(ov::element::f32, ov::Shape({factor_size}), freq_long); + + return {inv_freq, inv_freq_long}; +} + void replaceSinCosByCache(int max_prompt_len, const ov::OutputVector& cache, const pre_compute::RopePatternDesc* rpe) { auto inv_freq_size = ov::shape_size(rpe->matched_inv_freq->get_shape()); @@ -197,6 +227,85 @@ ov::npuw::patterns::pre_compute::LongRopePatternPhi::LongRopePatternPhi() : matc matcher.register_patterns({output_sin, output_cos}, make_matcher_callback()); } +ov::npuw::patterns::pre_compute::LongRopePatternPhi_v5::LongRopePatternPhi_v5() : matcher("sin-cos-matcher") { + auto MakeConstant = []() { + return opp::wrap_type(); + }; + + auto make_select_pattern = [&](const std::shared_ptr& position_ids, + const std::shared_ptr& short_factor, + const std::shared_ptr& long_factor, + const std::shared_ptr& multiply_const, + const std::shared_ptr& power_const) { + auto red_max = opp::wrap_type({position_ids, MakeConstant()}); + auto add = opp::wrap_type({red_max, MakeConstant()}); + // max(position_ids) + 1 > original_max_position_embeddings + auto greater = opp::wrap_type({add, MakeConstant()}); + + auto short_factor_conv = opp::optional({short_factor->output(0)}); + auto long_factor_conv = opp::optional({long_factor->output(0)}); + + // max(position_ids) + 1 > original_max_position_embeddings ? long_factor : short_factor; + auto select = opp::wrap_type({greater, long_factor_conv, short_factor_conv}); + auto multiply = opp::wrap_type({select, multiply_const}); + auto power = opp::wrap_type({multiply, power_const}); + auto unsqueeze = opp::optional({power, MakeConstant()}); + auto unsqueeze_1 = opp::optional({unsqueeze, MakeConstant()}); + + return std::make_tuple(unsqueeze_1, greater, red_max); + }; + + auto position_ids = opp::wrap_type(); + + auto short_factor = MakeConstant(); + auto long_factor = MakeConstant(); + + auto multiply_const = MakeConstant(); + auto power_const = MakeConstant(); + + auto select_cond_max_pos_id = + make_select_pattern(position_ids, short_factor, long_factor, multiply_const, power_const); + auto select = std::get<0>(select_cond_max_pos_id); + auto cond = std::get<1>(select_cond_max_pos_id); + auto max_pos_id = std::get<2>(select_cond_max_pos_id); + + auto shape_of = opp::wrap_type({opp::any_input()}); + auto gather = opp::wrap_type({shape_of, opp::any_input(), opp::any_input()}); + auto concat_1 = opp::wrap_type({gather, opp::any_input(), opp::any_input()}); + // here we can seen inverse frequencies as a parameter or constant depending on partitioner passes + auto broadcast = opp::wrap_type({select, concat_1}); + auto unsqueeze = opp::wrap_type({position_ids, MakeConstant()}); + auto convert = opp::wrap_type({unsqueeze}); + auto matmul = opp::wrap_type({broadcast, convert}); + auto transpose = opp::wrap_type({matmul, opp::any_input()}); + auto concat_2 = opp::wrap_type({transpose, opp::any_input()}); + auto output_sin = opp::wrap_type({concat_2}); + auto output_cos = opp::wrap_type({concat_2}); + + init_cb = [=](const auto& matches) { + const auto& map_sin = matches.at(output_sin)[0]; + const auto& map_cos = matches.at(output_cos)[0]; + + this->matched_position_ids = map_sin.at(position_ids).get_node_shared_ptr(); + this->matched_concat = map_sin.at(concat_1).get_node_shared_ptr(); + this->matched_short_factor = map_sin.at(short_factor).get_node_shared_ptr(); + this->matched_long_factor = map_sin.at(long_factor).get_node_shared_ptr(); + this->matched_multiply_const = map_sin.at(multiply_const).get_node_shared_ptr(); + this->matched_power_const = map_sin.at(power_const).get_node_shared_ptr(); + this->matched_cond = map_sin.at(cond).get_node_shared_ptr(); + this->max_pos_id = map_sin.at(max_pos_id).get_node_shared_ptr(); + + this->matched_cos = map_cos.at(output_cos).get_node_shared_ptr(); + this->matched_sin = map_sin.at(output_sin).get_node_shared_ptr(); + + LOG_VERB("Rope found : sin=" << matched_sin->get_name() << ", cos=" << matched_cos->get_name()); + + return true; + }; + + matcher.register_patterns({output_sin, output_cos}, make_matcher_callback()); +} + ov::npuw::patterns::pre_compute::RopeCacheMatcher::RopeCacheMatcher(const uint32_t max_prompt_len, const std::shared_ptr& model, const std::string& longrope_input_name) { @@ -227,6 +336,33 @@ ov::npuw::patterns::pre_compute::RopeCacheMatcher::RopeCacheMatcher(const uint32 }; long_rpe->run_on_model(model); + auto long_rpe_v5 = std::make_shared(); + + long_rpe_v5->transform_cb = [&]() { + auto inv_freq = calculate_freq(long_rpe_v5->matched_short_factor, + long_rpe_v5->matched_long_factor, + long_rpe_v5->matched_multiply_const, + long_rpe_v5->matched_power_const); + + auto cache_short = makeCosSinCache(max_prompt_len, inv_freq[0]); + auto cache_long = makeCosSinCache(max_prompt_len, inv_freq[1]); + + auto select_cos = + std::make_shared(long_rpe_v5->matched_cond, cache_long[0], cache_short[0]); + auto select_sin = + std::make_shared(long_rpe_v5->matched_cond, cache_long[1], cache_short[1]); + + // WA: to get correct sin-cos cache size + long_rpe_v5->matched_inv_freq = inv_freq[0]; + replaceSinCosByCache(max_prompt_len, {select_cos, select_sin}, long_rpe_v5.get()); + + auto max_pos_id_out = long_rpe_v5->max_pos_id->output(0); + max_pos_id_param.reset(new ov::op::v0::Parameter(max_pos_id_out.get_element_type(), {1})); + max_pos_id_param->set_friendly_name(longrope_input_name); + max_pos_id_out.replace(max_pos_id_param->output(0)); + }; + long_rpe_v5->run_on_model(model); + if (max_pos_id_param) { model->add_parameters({max_pos_id_param}); for (auto&& input : model->inputs()) { diff --git a/src/plugins/intel_npu/src/plugin/npuw/partitioning/patterns/pre_compute.hpp b/src/plugins/intel_npu/src/plugin/npuw/partitioning/patterns/pre_compute.hpp index 9c8ffb4c4b2c..a1c6419b8617 100644 --- a/src/plugins/intel_npu/src/plugin/npuw/partitioning/patterns/pre_compute.hpp +++ b/src/plugins/intel_npu/src/plugin/npuw/partitioning/patterns/pre_compute.hpp @@ -38,6 +38,16 @@ class LongRopePatternDesc : public RopePatternDesc { std::shared_ptr max_pos_id; }; +class LongRopev5PatternDesc : public RopePatternDesc { +public: + std::shared_ptr matched_short_factor; + std::shared_ptr matched_long_factor; + std::shared_ptr matched_cond; + std::shared_ptr max_pos_id; + std::shared_ptr matched_multiply_const; + std::shared_ptr matched_power_const; +}; + class RopePatternLLama2 : public RopePatternDesc { ov::pass::MultiMatcher matcher; @@ -60,6 +70,17 @@ class LongRopePatternPhi : public LongRopePatternDesc { } }; +class LongRopePatternPhi_v5 : public LongRopev5PatternDesc { + ov::pass::MultiMatcher matcher; + +public: + using LongRopev5PatternDesc::transform_cb; + LongRopePatternPhi_v5(); + bool run_on_model(const std::shared_ptr& m) { + return matcher.run_on_model(m); + } +}; + class RopeCacheMatcher { public: RopeCacheMatcher(const uint32_t max_prompt_len, diff --git a/src/plugins/intel_npu/src/plugin/npuw/partitioning/patterns/sdpa.cpp b/src/plugins/intel_npu/src/plugin/npuw/partitioning/patterns/sdpa.cpp index 708454c20d96..a266598af93c 100644 --- a/src/plugins/intel_npu/src/plugin/npuw/partitioning/patterns/sdpa.cpp +++ b/src/plugins/intel_npu/src/plugin/npuw/partitioning/patterns/sdpa.cpp @@ -24,11 +24,14 @@ namespace opp = ov::pass::pattern; SDPA::SDPA(const std::shared_ptr& snapshot, const std::string& isol_tag) { auto past_k_in = opp::wrap_type(); - auto past_k_cvt = opp::optional({past_k_in->output(0)}); + // Optional beam-search gather: Parameter → Gather(beam_idx) → ... (stateless LLM models) + auto past_k_gather = opp::optional({past_k_in->output(0), opp::any_input(), opp::any_input()}); + auto past_k_cvt = opp::optional({past_k_gather->output(0)}); auto past_k_cat = opp::wrap_type({past_k_cvt, opp::any_input()}); auto past_v_in = opp::wrap_type(); - auto past_v_cvt = opp::optional({past_v_in->output(0)}); + auto past_v_gather = opp::optional({past_v_in->output(0), opp::any_input(), opp::any_input()}); + auto past_v_cvt = opp::optional({past_v_gather->output(0)}); auto past_v_cat = opp::wrap_type({past_v_cvt, opp::any_input()}); // Optional part, probably one of many. Replace by graph traversal! @@ -51,9 +54,11 @@ SDPA::SDPA(const std::shared_ptr& snapshot, const st auto callback = [=](ov::pass::pattern::Matcher& m) { auto& node_to_output = m.get_pattern_value_map(); auto pattern_nodes = std::vector>{past_k_in, + past_k_gather, past_k_cvt, past_k_cat, past_v_in, + past_v_gather, past_v_cvt, past_v_cat, opt_unsq_k, @@ -109,18 +114,24 @@ SDPADecomposed::SDPADecomposed(const std::shared_ptr auto convert1 = opp::wrap_type({opp::any_input()}); auto concat1 = opp::wrap_type({convert1, opp::any_input()}); - // GQA optional nodes - auto unsqueeze1 = opp::optional({concat1, opp::any_input()}); - auto broadcast1 = opp::optional({unsqueeze1, opp::any_input()}); - auto reshape1 = opp::optional({broadcast1, opp::any_input()}); + // GQA optional nodes — require single consumer so shared KV (e.g. Gemma4) does not + // accidentally match: if any expansion node is shared across multiple heads the + // predicate fails and the optional is treated as absent, causing the overall pattern + // to fall through rather than matching an incorrect multi-branch subgraph. + auto single_user = [](const ov::Output& output) { + return output.get_target_inputs().size() == 1; + }; + auto unsqueeze1 = opp::optional({concat1, opp::any_input()}, single_user); + auto broadcast1 = opp::optional({unsqueeze1, opp::any_input()}, single_user); + auto reshape1 = opp::optional({broadcast1, opp::any_input()}, single_user); auto convert2 = opp::wrap_type({opp::any_input()}); auto concat2 = opp::wrap_type({convert2, opp::any_input()}); - // GQA optional nodes - auto unsqueeze2 = opp::optional({concat2, opp::any_input()}); - auto broadcast2 = opp::optional({unsqueeze2, opp::any_input()}); - auto reshape2 = opp::optional({broadcast2, opp::any_input()}); + // GQA optional nodes — same single-consumer guard + auto unsqueeze2 = opp::optional({concat2, opp::any_input()}, single_user); + auto broadcast2 = opp::optional({unsqueeze2, opp::any_input()}, single_user); + auto reshape2 = opp::optional({broadcast2, opp::any_input()}, single_user); auto matmul1 = opp::wrap_type({opp::any_input(), reshape1}); auto add = opp::wrap_type({matmul1, opp::any_input()}); @@ -200,6 +211,11 @@ AttentionBroadcast::AttentionBroadcast() { // Note: Use [=] to make sure the above objects stay alive in the callback auto callback = [=](ov::pass::pattern::Matcher& m) { auto& node_to_output = m.get_pattern_value_map(); + auto matched_gather_out = node_to_output.at(gather); + if (matched_gather_out.get_target_inputs().size() > 1) { + // This pattern is for the Gather that feeds a single Concat. + return false; + } auto matched_concat_out = node_to_output.at(concat); auto& matched_concat_tensor = matched_concat_out.get_tensor(); if (matched_concat_tensor.has_and_set_bound()) { @@ -251,6 +267,49 @@ AttentionBroadcast2::AttentionBroadcast2() { register_matcher(std::make_shared(bcast_kv, "AttentionBroadcast2"), std::move(callback)); } +// FIXME: Same as AttentionBroadcast but Gather connects to multiple Concats +AttentionBroadcast3::AttentionBroadcast3() { + // NB(dm): We've seen cases where this dynamic subgraph is placed on the K-path, + // but I'd expect it could be on the V-path as well - so _kv in the name + auto past_kv_in = opp::wrap_type(); + auto past_kv_cvt = opp::optional({past_kv_in->output(0)}); + auto past_kv_cat = opp::wrap_type({past_kv_cvt, opp::any_input()}); + + // The dynamic shape calculation to be eliminated + // NB: It only works in static shape graphs + auto shape_of = opp::wrap_type({past_kv_cat}); + auto gather = opp::wrap_type({shape_of, opp::any_input(), opp::any_input()}); + auto concat = opp::wrap_type({gather, opp::any_input(), opp::any_input(), opp::any_input()}); + + // Broadcast - the consumer + auto unsq_kv = opp::wrap_type({past_kv_cat, opp::any_input()}); + auto bcast_kv = opp::wrap_type({unsq_kv, concat}); + + // Note: Use [=] to make sure the above objects stay alive in the callback + auto callback = [=](ov::pass::pattern::Matcher& m) { + auto& node_to_output = m.get_pattern_value_map(); + auto matched_gather_out = node_to_output.at(gather); + if (matched_gather_out.get_target_inputs().size() == 1) { + // This pattern only for the Gather feeding multiple Concats. + return false; + } + auto& matched_gather_tensor = matched_gather_out.get_tensor(); + if (matched_gather_tensor.has_and_set_bound()) { + // Replace the dynamic shape calculation with a static constant + // This is bad but it in the current realm it is what it is + auto new_const = std::make_shared(matched_gather_tensor.get_upper_value()); + new_const->set_friendly_name("NPUW/Precalculated/" + + matched_gather_out.get_node_shared_ptr()->get_friendly_name()); + for (auto&& input : matched_gather_out.get_target_inputs()) { + input.replace_source_output(new_const); + } + return true; // root changed + } + return false; // root hasn't changed + }; + register_matcher(std::make_shared(bcast_kv, "AttentionBroadcast3"), std::move(callback)); +} + ShapeOfParameter::ShapeOfParameter() { auto param_in = opp::wrap_type(); auto param_cvt = opp::wrap_type({param_in}); @@ -281,6 +340,7 @@ bool RegularizeSDPA::run_on_model(const std::shared_ptr& model) { ov::pass::GraphRewrite rewr; rewr.add_matcher(); rewr.add_matcher(); + rewr.add_matcher(); model_changed |= rewr.run_on_model(model); } diff --git a/src/plugins/intel_npu/src/plugin/npuw/partitioning/patterns/sdpa.hpp b/src/plugins/intel_npu/src/plugin/npuw/partitioning/patterns/sdpa.hpp index 7bb708270005..9ceac45b47d8 100644 --- a/src/plugins/intel_npu/src/plugin/npuw/partitioning/patterns/sdpa.hpp +++ b/src/plugins/intel_npu/src/plugin/npuw/partitioning/patterns/sdpa.hpp @@ -25,12 +25,30 @@ namespace attn { class SDPA : public ov::pass::MatcherPass { public: OPENVINO_MATCHER_PASS_RTTI("npuw::patterns::attn::SDPA"); + static constexpr const char* pattern_name() { + return "SDPA"; + } + static constexpr const char* isolation_tag() { + return "attn"; + } + static constexpr const char* group_name() { + return "attn"; + } SDPA(const std::shared_ptr& snapshot, const std::string& isol_tag); }; class SDPADecomposed : public ov::pass::MatcherPass { public: OPENVINO_MATCHER_PASS_RTTI("npuw::patterns::attn::SDPADecomposed"); + static constexpr const char* pattern_name() { + return "SDPADecomposed"; + } + static constexpr const char* isolation_tag() { + return "attn"; + } + static constexpr const char* group_name() { + return "attn"; + } SDPADecomposed(const std::shared_ptr& snapshot, const std::string& isol_tag); }; @@ -50,6 +68,12 @@ class AttentionBroadcast2 : public ov::pass::MatcherPass { AttentionBroadcast2(); }; +class AttentionBroadcast3 : public ov::pass::MatcherPass { +public: + OPENVINO_MATCHER_PASS_RTTI("npuw::patterns::attn::AttentionBroadcast3"); + AttentionBroadcast3(); +}; + class ShapeOfParameter : public ov::pass::MatcherPass { public: OPENVINO_MATCHER_PASS_RTTI("npuw::patterns::attn::ShapeOfParameter"); diff --git a/src/plugins/intel_npu/src/plugin/npuw/pipelines/kokoro/kokoro_compiled_model.cpp b/src/plugins/intel_npu/src/plugin/npuw/pipelines/kokoro/kokoro_compiled_model.cpp index bc45b16f7a2e..4dabe8e0821a 100644 --- a/src/plugins/intel_npu/src/plugin/npuw/pipelines/kokoro/kokoro_compiled_model.cpp +++ b/src/plugins/intel_npu/src/plugin/npuw/pipelines/kokoro/kokoro_compiled_model.cpp @@ -13,26 +13,6 @@ #include "plugin.hpp" namespace { -// Remove all options "NPUW_.*" -ov::AnyMap without_npuw_params(const ov::AnyMap& properties) { - ov::AnyMap result; - for (const auto& item : properties) { - if (item.first.find("NPUW") == std::string::npos) { - result.insert(item); - } - } - return result; -} - -// Check properties for NPUW_DEVICES options and return true if only CPU is present -bool is_cpu_only(const ov::AnyMap& properties) { - auto it = properties.find("NPUW_DEVICES"); - if (it != properties.end()) { - return it->second.as() == "CPU"; - } - return false; -} - void split_kokoro_properties(const ov::AnyMap& properties, ov::AnyMap& other_properties, ov::AnyMap& kokoro_properties) { @@ -78,6 +58,7 @@ ov::npuw::KokoroCompiledModel::KokoroCompiledModel(const std::shared_ptrget_core(); - m_model_a_compiled = core->compile_model(split_result.model_a, "CPU", ov::AnyMap{}); - } else { - // Plugin don't have to know about NPUW parameters - ov::AnyMap model_a_properties = without_npuw_params(common_props); - m_model_a_compiled = plugin->compile_model(split_result.model_a, model_a_properties); - } + // Model A (BERT + LSTMs + duration predictor) — compile through NPUW + // so that NPUW_CACHE_DIR caching works for both models. + // NONE pipeline keeps it as a single subgraph (no partitioning overhead). + ov::AnyMap properties_model_a = common_props; + properties_model_a["NPUW_ONLINE_PIPELINE"] = "NONE"; + m_model_a_compiled = std::dynamic_pointer_cast( + ov::npuw::ICompiledModel::create(split_result.model_a, plugin, properties_model_a)); LOG_DEBUG("Compiling kokoro model B..."); ov::AnyMap properties_model_b = common_props; // Enforce offloading to CPU for non-accurate subgraphs if (!properties_model_b.count("NPUW_ONLINE_PIPELINE")) { - // REP mode is giving best compile time / stability results - properties_model_b["NPUW_ONLINE_PIPELINE"] = "REP"; + // Long compilation time, but best performance + properties_model_b["NPUW_ONLINE_PIPELINE"] = "NONE"; } if (!properties_model_b.count("NPUW_ONLINE_AVOID")) { - properties_model_b["NPUW_ONLINE_AVOID"] = "P:DownsampleInterpolate/NPU,P:FloorModFP32/NPU,P:CumSumSinGen/" - "NPU,P:BoxMullerNoise/NPU,P:AngleComplex/NPU,Op:ISTFT/NPU"; + properties_model_b["NPUW_ONLINE_AVOID"] = "P:FloorModFP32/NPU,P:CumSumSinGen/NPU," + "P:BoxMullerNoise/NPU,P:AngleComplex/NPU"; } m_model_b_compiled = std::dynamic_pointer_cast( ov::npuw::ICompiledModel::create(split_result.model_b, plugin, properties_model_b)); diff --git a/src/plugins/intel_npu/src/plugin/npuw/pipelines/kokoro/kokoro_infer_request.cpp b/src/plugins/intel_npu/src/plugin/npuw/pipelines/kokoro/kokoro_infer_request.cpp index 488541cdb225..d12b521233cb 100644 --- a/src/plugins/intel_npu/src/plugin/npuw/pipelines/kokoro/kokoro_infer_request.cpp +++ b/src/plugins/intel_npu/src/plugin/npuw/pipelines/kokoro/kokoro_infer_request.cpp @@ -135,6 +135,12 @@ ov::npuw::KokoroInferRequest::KokoroInferRequest(const std::shared_ptrdata(), seq_len, real_len); + // Also set input_lengths to the real sequence length so that LSTMSequence ops + // (from pack_padded_sequence) only process valid tokens, not padding. + if (m_a_input_lengths.get_node()) { + auto lengths_tensor = m_model_a_request->get_tensor(m_a_input_lengths); + if (!lengths_tensor || lengths_tensor->get_shape() != ov::Shape{1}) { + lengths_tensor = ov::make_tensor(ov::element::i64, ov::Shape{1}); + m_model_a_request->set_tensor(m_a_input_lengths, lengths_tensor); + } + lengths_tensor->data()[0] = static_cast(real_len); + } + m_real_seq_len = real_len; LOG_DEBUG("text_mask filled — real_len=" << real_len << ", seq_len=" << seq_len diff --git a/src/plugins/intel_npu/src/plugin/npuw/pipelines/kokoro/kokoro_infer_request.hpp b/src/plugins/intel_npu/src/plugin/npuw/pipelines/kokoro/kokoro_infer_request.hpp index 7227aa1581f8..04a27eb698a9 100644 --- a/src/plugins/intel_npu/src/plugin/npuw/pipelines/kokoro/kokoro_infer_request.hpp +++ b/src/plugins/intel_npu/src/plugin/npuw/pipelines/kokoro/kokoro_infer_request.hpp @@ -49,7 +49,9 @@ class KokoroInferRequest : public ov::ISyncInferRequest { // Model A attention mask input (auto-filled from input_ids) ov::Output m_a_text_mask; - // Original input_ids port (used to compute text_mask at runtime) + // Model A input_lengths input (auto-filled from input_ids) + ov::Output m_a_input_lengths; + // Original input_ids port (used to compute text_mask and input_lengths at runtime) ov::Output m_orig_input_ids; // Real (unpadded) sequence length — set by fill_text_mask(), used to // truncate pred_dur so Model B only processes valid tokens. diff --git a/src/plugins/intel_npu/src/plugin/npuw/pipelines/kokoro/kokoro_split.cpp b/src/plugins/intel_npu/src/plugin/npuw/pipelines/kokoro/kokoro_split.cpp index 8995a88c9c5e..ab4caa805393 100644 --- a/src/plugins/intel_npu/src/plugin/npuw/pipelines/kokoro/kokoro_split.cpp +++ b/src/plugins/intel_npu/src/plugin/npuw/pipelines/kokoro/kokoro_split.cpp @@ -62,6 +62,69 @@ void replace_text_mask_with_parameter(std::shared_ptr& model) { model->add_parameters({text_mask_param}); model->validate_nodes_and_infer_types(); } + +// TODO Should be replaced with proper LSTMSequence implementation which can accept such inputs. +// +// Replace the input_lengths value (derived from ShapeOf(input_ids)) with a Parameter input. +// In the static model, ShapeOf(input_ids) always returns the padded size (e.g. 512), which +// feeds as sequence_lengths into all LSTMSequence ops (from pack_padded_sequence in PyTorch). +// This makes the LSTMs process all 512 positions including padding, corrupting hidden states +// (especially in bidirectional LSTMs where the backward pass starts from padding). +// By exposing input_lengths as a Parameter, the host can provide the real sequence length. +// +// NOTE: The NPU compiler frontend (IE.cpp) only accepts sequence_lengths that is either +// a Constant or a Parameter node for a static model — any intermediate op (like TopK) causes +// a rejection. Therefore we must connect ALL LSTMSequence port 3 inputs directly to the Parameter. +void replace_input_lengths_with_parameter(std::shared_ptr& model) { + // Find the Broadcast node whose output tensor has the name "input_lengths". + // This is the node that broadcasts ShapeOf(input_ids)[1] → [batch_size]. + std::shared_ptr input_lengths_node; + for (const auto& op : model->get_ops()) { + for (size_t i = 0; i < op->get_output_size(); ++i) { + if (op->output(i).get_names().count("input_lengths")) { + input_lengths_node = op; + break; + } + } + if (input_lengths_node) + break; + } + + if (!input_lengths_node) { + LOG_WARN("input_lengths node not found — skipping replacement"); + return; + } + + LOG_DEBUG("replacing input_lengths (ShapeOf-derived) with Parameter input"); + + // input_lengths shape is [1] (batch_size) with element type I64 + auto input_lengths_param = std::make_shared(input_lengths_node->get_output_element_type(0), + input_lengths_node->get_output_partial_shape(0)); + input_lengths_param->set_friendly_name("input_lengths"); + input_lengths_param->output(0).get_tensor().set_names({"input_lengths"}); + + // Replace the main Broadcast output with the Parameter + input_lengths_node->output(0).replace(input_lengths_param->output(0)); + + // Connect ALL LSTMSequence sequence_lengths (port 3) directly to the Parameter. + // Some LSTMs receive it through TopK (sort for pack_padded_sequence), others + // through a separate ShapeOf chain — but the NPU compiler requires the source + // to be exactly a Parameter or Constant, not an intermediate op. + for (const auto& op : model->get_ops()) { + if (std::string(op->get_type_info().name) != "LSTMSequence") + continue; + + auto source = op->input_value(3).get_node_shared_ptr(); + if (source.get() != input_lengths_param.get()) { + LOG_DEBUG("Redirecting LSTM '" << op->get_friendly_name() + << "' sequence_lengths directly to input_lengths parameter"); + op->input(3).replace_source_output(input_lengths_param->output(0)); + } + } + + model->add_parameters({input_lengths_param}); + model->validate_nodes_and_infer_types(); +} } // namespace // Main logic for kokoro model is splitting it into two parts, @@ -159,6 +222,10 @@ std::shared_ptr KokoroSplit::create_model_a(const std::shared_ptr> _models_to_compile; diff --git a/src/plugins/intel_npu/src/plugin/npuw/serialization.cpp b/src/plugins/intel_npu/src/plugin/npuw/serialization.cpp index e194b5683945..79be4dc96ed3 100644 --- a/src/plugins/intel_npu/src/plugin/npuw/serialization.cpp +++ b/src/plugins/intel_npu/src/plugin/npuw/serialization.cpp @@ -21,13 +21,13 @@ #include "spatial.hpp" #include "util.hpp" -// NOTE: This construtor should only be used when exporting blobs +// NOTE: This constructor should only be used when exporting blobs. ov::npuw::s11n::WeightsContext::WeightsContext(bool _is_weightless, const std::unordered_map& _const_to_offset) : is_weightless(_is_weightless), const_to_offset(_const_to_offset) {} -// NOTE: This construtor can and should only be used when importing blobs +// NOTE: This constructor is used when importing blobs. ov::npuw::s11n::WeightsContext::WeightsContext(const ov::npuw::s11n::WeightsPtr& _weights, const std::string& _weights_path, const s11n::WeightsContext::ConstsCache& _consts_cache, @@ -60,448 +60,166 @@ ov::npuw::s11n::BF16Cache ov::npuw::s11n::get_bf16_consts(const std::shared_ptr< return bf16_cache; } -void ov::npuw::s11n::write(std::ostream& stream, const std::streampos& var) { - stream.write(reinterpret_cast(&var), sizeof var); +void ov::npuw::orc::serialize(Stream& stream, std::streampos& var) { + stream.bytes(&var, sizeof var); } -void ov::npuw::s11n::write(std::ostream& stream, const std::string& var) { - auto var_size = var.size(); - stream.write(reinterpret_cast(&var_size), sizeof var_size); - stream.write(&var[0], var.size()); +void ov::npuw::orc::serialize(Stream& stream, ov::npuw::compiled::Spatial& var) { + stream & var.params & var.range & var.nway & var.out_dim & var.nway_iters & var.tail_size; } -void ov::npuw::s11n::write(std::ostream& stream, const bool& var) { - stream.write(reinterpret_cast(&var), sizeof var); +void ov::npuw::orc::serialize(Stream& stream, ov::npuw::compiled::Spatial::Param& var) { + stream & var.idx & var.dim; } -void ov::npuw::s11n::write(std::ostream& stream, const float& var) { - stream.write(reinterpret_cast(&var), sizeof var); +void ov::npuw::orc::serialize(Stream& stream, ov::npuw::compiled::Attention& var) { + stream & var.query_size & var.context_size & var.params & var.mask_idx & var.attend_all; } -void ov::npuw::s11n::write(std::ostream& stream, const ov::npuw::compiled::Spatial& var) { - using ov::npuw::s11n::write; - - write(stream, var.params.size()); - for (const auto& p : var.params) { - write(stream, p.idx); - write(stream, p.dim); - } - write(stream, var.range); - write(stream, var.nway); - write(stream, var.out_dim); - write(stream, var.nway_iters); - write(stream, var.tail_size); -} - -void ov::npuw::s11n::write(std::ostream& stream, const ov::npuw::compiled::Attention& var) { - using ov::npuw::s11n::write; - - write(stream, var.query_size); - write(stream, var.context_size); - - // NB: This should've been done through a generic vector write! - write(stream, var.params.size()); - for (const auto& p : var.params) { - write(stream, p.idx); - write(stream, p.dim); - } - - write(stream, var.mask_idx); - write(stream, var.attend_all); -} - -void ov::npuw::s11n::write(std::ostream& stream, const ov::npuw::compiled::PyramidAttention& var) { - using ov::npuw::s11n::write; - - write(stream, var.query_size); - write(stream, var.full_context_size); - write(stream, var._context_lengths); - - // Serialize attention infos - write(stream, var._attention_infos.size()); - for (const auto& info : var._attention_infos) { - write(stream, info.params.size()); - for (const auto& p : info.params) { - write(stream, p.idx); - write(stream, p.dim); - } - write(stream, info.mask_idx); - write(stream, info.query_size); - write(stream, info.context_length); - } -} - -void ov::npuw::s11n::write(std::ostream& stream, const ov::npuw::compiled::HostFlashAttention& var) { - using ov::npuw::s11n::write; - - // Serialize basic info from _sdpa_attention_info - write(stream, var._sdpa_attention_info._query_size); - write(stream, var._sdpa_attention_info._context_size); - write(stream, var._sdpa_attention_info._k_seq_dim); - write(stream, var._sdpa_attention_info._v_seq_dim); - - // Serialize SDPA indices from _sdpa_attention_info - write(stream, var._sdpa_attention_info._sdpa_indices.query); - write(stream, var._sdpa_attention_info._sdpa_indices.past_key); - write(stream, var._sdpa_attention_info._sdpa_indices.past_value); - write(stream, var._sdpa_attention_info._sdpa_indices.present_key); - write(stream, var._sdpa_attention_info._sdpa_indices.present_value); - write(stream, var._sdpa_attention_info._sdpa_indices.attention_mask); - - // Serialize tile input indices from _sdpa_attention_info - write(stream, var._sdpa_attention_info._tile_input_indices.q); - write(stream, var._sdpa_attention_info._tile_input_indices.k); - write(stream, var._sdpa_attention_info._tile_input_indices.v); - write(stream, var._sdpa_attention_info._tile_input_indices.mask); - write(stream, var._sdpa_attention_info._tile_input_indices.acc); - write(stream, var._sdpa_attention_info._tile_input_indices.max); - write(stream, var._sdpa_attention_info._tile_input_indices.d); - - // Serialize tile output indices from _sdpa_attention_info - write(stream, var._sdpa_attention_info._tile_output_indices.acc); - write(stream, var._sdpa_attention_info._tile_output_indices.max); - write(stream, var._sdpa_attention_info._tile_output_indices.d); - - // Serialize tile_size - write(stream, var._tile_size); - - // Serialize can_use_tensor_view - write(stream, var._can_use_tensor_view); - - // Note: _tile_model_to_compile and _compiled_tile_model are not serialized here - // They are handled separately in CompiledModelDesc::serialize() -} - -void ov::npuw::s11n::write(std::ostream& stream, const ov::npuw::compiled::MoEExperts& var) { - using ov::npuw::s11n::write; - - // Serialize basic MoE metadata - write(stream, var.num_experts); - write(stream, var.expert_hidden_dim); - write(stream, var.num_active_experts); - write(stream, var.input_token_count); - - // Serialize router scores index - write(stream, var._router_scores_idx.has_value()); - if (var._router_scores_idx.has_value()) { - write(stream, var._router_scores_idx.value()); - } - - // Serialize expert input parameter index - write(stream, var._expert_input_param_idx.has_value()); - if (var._expert_input_param_idx.has_value()) { - write(stream, var._expert_input_param_idx.value()); - } - - // Serialize parameter mapping - write(stream, var._param_mapping); - - // Note: _compiled_models and _models_to_compile are not serialized here - // They are handled separately in CompiledModelDesc::serialize() -} - -void ov::npuw::s11n::write(std::ostream& stream, const ov::npuw::compiled::MoEDownstream& var) { - using ov::npuw::s11n::write; - - // Serialize MoE downstream metadata - write(stream, var.total_experts_num); - write(stream, var.active_experts_num); - write(stream, var.expert_output_param_idx); - - // Note: _compiled_model and _model_to_compile are not serialized here - // They are handled separately in CompiledModelDesc::serialize() -} - -void ov::npuw::s11n::write(std::ostream& stream, const ov::Tensor& var) { - using ov::npuw::s11n::write; - - if (!var) { - write(stream, false); - return; - } - write(stream, true); - - auto type_str = var.get_element_type().to_string(); - write(stream, type_str); - write(stream, var.get_shape()); - write(stream, var.get_byte_size()); - - ov::Tensor tensor; - if (var.is_continuous()) { - tensor = var; - } else { - // Just copy strided tensor to a non-strided one - tensor = ov::Tensor(var.get_element_type(), var.get_shape()); - var.copy_to(tensor); - } - NPUW_ASSERT(tensor); - size_t blob_size = var.get_byte_size(); - if (blob_size > static_cast(std::numeric_limits::max())) { - OPENVINO_THROW("Blob size is too large to be represented on a std::streamsize!"); - } - stream.write(reinterpret_cast(var.data()), static_cast(blob_size)); -} - -void ov::npuw::s11n::write(std::ostream& stream, const ::intel_npu::Config& var) { - write(stream, var.toString()); -} - -void ov::npuw::s11n::write(std::ostream& stream, const ov::Output& var) { - write(stream, var.get_element_type().to_string()); - write(stream, var.get_partial_shape().to_string()); - write(stream, var.get_names()); -} - -void ov::npuw::s11n::write_any(std::ostream& stream, const ov::Any& var) { - auto str = ov::npuw::s11n::anyToString(var); - write(stream, str); -} - -void ov::npuw::s11n::write(std::ostream& stream, const ov::npuw::weights::LazyTensor& var) { - var.serialize(stream); +void ov::npuw::orc::serialize(Stream& stream, ov::npuw::compiled::Attention::Param& var) { + stream & var.idx & var.dim; } -void ov::npuw::s11n::write(std::ostream& stream, const ov::CacheMode& var) { - stream.write(reinterpret_cast(&var), sizeof var); +void ov::npuw::orc::serialize(Stream& stream, ov::npuw::compiled::PyramidAttention& var) { + stream & var.query_size & var.full_context_size & var._context_lengths & var._attention_infos; } -void ov::npuw::s11n::write(std::ostream& stream, const ov::element::Type& var) { - stream.write(reinterpret_cast(&var), sizeof var); +void ov::npuw::orc::serialize(Stream& stream, ov::npuw::compiled::PyramidAttentionInfo& var) { + stream & var.params & var.mask_idx & var.query_size & var.context_length; } -void ov::npuw::s11n::write(std::ostream& stream, const ov::hint::PerformanceMode& var) { - stream.write(reinterpret_cast(&var), sizeof var); +void ov::npuw::orc::serialize(Stream& stream, ov::npuw::compiled::PyramidAttentionInfo::Param& var) { + stream & var.idx & var.dim; } -void ov::npuw::s11n::write(std::ostream& stream, const ov::AnyMap& var) { - auto str = ov::npuw::s11n::anyMapToString(var); - write(stream, str); +void ov::npuw::orc::serialize(Stream& stream, ov::npuw::compiled::HostFlashAttention& var) { + auto& info = var._sdpa_attention_info; + stream & info._query_size & info._context_size & info._k_seq_dim & info._v_seq_dim & info._sdpa_indices.query & + info._sdpa_indices.past_key & info._sdpa_indices.past_value & info._sdpa_indices.present_key & + info._sdpa_indices.present_value & info._sdpa_indices.attention_mask & info._tile_input_indices.q & + info._tile_input_indices.k & info._tile_input_indices.v & info._tile_input_indices.mask & + info._tile_input_indices.acc & info._tile_input_indices.max & info._tile_input_indices.d & + info._tile_output_indices.acc & info._tile_output_indices.max & info._tile_output_indices.d & var._tile_size & + var._can_use_tensor_view; } -void ov::npuw::s11n::read(std::istream& stream, std::streampos& var) { - stream.read(reinterpret_cast(&var), sizeof var); +void ov::npuw::orc::serialize(Stream& stream, ov::npuw::compiled::MoEExperts& var) { + stream & var.num_experts & var.expert_hidden_dim & var.num_active_experts & var.input_token_count & + var._router_scores.original & var._router_scores.compiled & var._expert_input.original & + var._expert_input.compiled & var._param_mapping; } -void ov::npuw::s11n::read(std::istream& stream, std::string& var) { - std::size_t var_size = 0; - stream.read(reinterpret_cast(&var_size), sizeof var_size); - var.resize(var_size); - stream.read(&var[0], var_size); +void ov::npuw::orc::serialize(Stream& stream, ov::npuw::compiled::MoEDownstream& var) { + stream & var.total_experts_num & var.active_experts_num & var.expert_output_param_idx; } -void ov::npuw::s11n::read(std::istream& stream, bool& var) { - stream.read(reinterpret_cast(&var), sizeof var); +void ov::npuw::orc::serialize(Stream& stream, ov::Tensor& var) { + transfer_tensor(stream, var); } -void ov::npuw::s11n::read(std::istream& stream, float& var) { - stream.read(reinterpret_cast(&var), sizeof var); -} - -void ov::npuw::s11n::read(std::istream& stream, ov::npuw::compiled::Spatial& var) { - using ov::npuw::s11n::read; - - std::size_t params_size = 0; - read(stream, params_size); - for (std::size_t i = 0; i < params_size; ++i) { - ov::npuw::compiled::Spatial::Param p; - read(stream, p.idx); - read(stream, p.dim); - var.params.push_back(p); +void ov::npuw::orc::serialize(Stream& stream, ::intel_npu::Config& var) { + std::string str; + if (stream.output()) { + str = var.toString(); } - read(stream, var.range); - read(stream, var.nway); - read(stream, var.out_dim); - read(stream, var.nway_iters); - read(stream, var.tail_size); -} - -void ov::npuw::s11n::read(std::istream& stream, ov::npuw::compiled::Attention& var) { - using ov::npuw::s11n::read; - - read(stream, var.query_size); - read(stream, var.context_size); - - std::size_t params_size = 0; - read(stream, params_size); - for (std::size_t i = 0; i < params_size; ++i) { - ov::npuw::compiled::Attention::Param p; - read(stream, p.idx); - read(stream, p.dim); - var.params.push_back(p); + stream & str; + if (stream.input()) { + var.fromString(str); } - - read(stream, var.mask_idx); - read(stream, var.attend_all); } -void ov::npuw::s11n::read(std::istream& stream, ov::npuw::compiled::PyramidAttention& var) { - using ov::npuw::s11n::read; - - read(stream, var.query_size); - read(stream, var.full_context_size); - read(stream, var._context_lengths); - - // Deserialize attention infos - std::size_t attention_infos_size = 0; - read(stream, attention_infos_size); - var._attention_infos.resize(attention_infos_size); - - for (auto& info : var._attention_infos) { - std::size_t params_size = 0; - read(stream, params_size); - info.params.resize(params_size); - for (auto& p : info.params) { - read(stream, p.idx); - read(stream, p.dim); - } - read(stream, info.mask_idx); - read(stream, info.query_size); - read(stream, info.context_length); +void ov::npuw::orc::serialize(Stream& stream, ov::Output& var) { + if (stream.output()) { + auto elem_type = var.get_element_type().to_string(); + auto shape = var.get_partial_shape().to_string(); + auto names = var.get_names(); + stream & elem_type & shape & names; + } else { + OPENVINO_THROW("ov::Output is write-only in NPUW serialization"); } } -void ov::npuw::s11n::read(std::istream& stream, ov::npuw::compiled::HostFlashAttention& var) { - using ov::npuw::s11n::read; - - // Deserialize basic info into _sdpa_attention_info - read(stream, var._sdpa_attention_info._query_size); - read(stream, var._sdpa_attention_info._context_size); - read(stream, var._sdpa_attention_info._k_seq_dim); - read(stream, var._sdpa_attention_info._v_seq_dim); - - // Deserialize SDPA indices into _sdpa_attention_info - read(stream, var._sdpa_attention_info._sdpa_indices.query); - read(stream, var._sdpa_attention_info._sdpa_indices.past_key); - read(stream, var._sdpa_attention_info._sdpa_indices.past_value); - read(stream, var._sdpa_attention_info._sdpa_indices.present_key); - read(stream, var._sdpa_attention_info._sdpa_indices.present_value); - read(stream, var._sdpa_attention_info._sdpa_indices.attention_mask); - - // Deserialize tile input indices into _sdpa_attention_info - read(stream, var._sdpa_attention_info._tile_input_indices.q); - read(stream, var._sdpa_attention_info._tile_input_indices.k); - read(stream, var._sdpa_attention_info._tile_input_indices.v); - read(stream, var._sdpa_attention_info._tile_input_indices.mask); - read(stream, var._sdpa_attention_info._tile_input_indices.acc); - read(stream, var._sdpa_attention_info._tile_input_indices.max); - read(stream, var._sdpa_attention_info._tile_input_indices.d); - - // Deserialize tile output indices into _sdpa_attention_info - read(stream, var._sdpa_attention_info._tile_output_indices.acc); - read(stream, var._sdpa_attention_info._tile_output_indices.max); - read(stream, var._sdpa_attention_info._tile_output_indices.d); - - // Deserialize tile_size - read(stream, var._tile_size); - - // Deserialize can_use_tensor_view - read(stream, var._can_use_tensor_view); - - // Note: _tile_model_to_compile and _compiled_tile_model are not deserialized here - // They are handled separately in CompiledModelDesc::deserialize() -} +void ov::npuw::orc::transfer_tensor(Stream& stream, ov::Tensor& var, const s11n::TensorAllocator& allocator) { + if (stream.output()) { + bool is_initialized = static_cast(var); + stream & is_initialized; + if (!is_initialized) { + return; + } -void ov::npuw::s11n::read(std::istream& stream, ov::npuw::compiled::MoEExperts& var) { - using ov::npuw::s11n::read; - - // Deserialize basic MoE metadata - read(stream, var.num_experts); - read(stream, var.expert_hidden_dim); - read(stream, var.num_active_experts); - read(stream, var.input_token_count); - - // Deserialize router scores index - bool has_router_scores_idx = false; - read(stream, has_router_scores_idx); - if (has_router_scores_idx) { - size_t value = 0; - read(stream, value); - var._router_scores_idx = value; - } + auto type_str = var.get_element_type().to_string(); + auto shape = var.get_shape(); + auto byte_size = var.get_byte_size(); + stream & type_str & shape & byte_size; - // Deserialize expert input parameter index - bool has_expert_input_param_idx = false; - read(stream, has_expert_input_param_idx); - if (has_expert_input_param_idx) { - size_t value = 0; - read(stream, value); - var._expert_input_param_idx = value; + ov::Tensor tensor = var; + if (!var.is_continuous()) { + tensor = ov::Tensor(var.get_element_type(), var.get_shape()); + var.copy_to(tensor); + } + NPUW_ASSERT(tensor); + stream.bytes(tensor.data(), tensor.get_byte_size()); + return; } - // Deserialize parameter mapping - read(stream, var._param_mapping); - - // Note: _compiled_models and _models_to_compile are not deserialized here - // They are handled separately in CompiledModelDesc::deserialize() -} - -void ov::npuw::s11n::read(std::istream& stream, ov::npuw::compiled::MoEDownstream& var) { - using ov::npuw::s11n::read; - - // Deserialize MoE downstream metadata - read(stream, var.total_experts_num); - read(stream, var.active_experts_num); - read(stream, var.expert_output_param_idx); - - // Note: _compiled_model and _model_to_compile are not deserialized here - // They are handled separately in CompiledModelDesc::deserialize() -} - -void ov::npuw::s11n::read(std::istream& stream, ov::Tensor& var) { bool is_initialized = false; - read(stream, is_initialized); - + stream & is_initialized; if (!is_initialized) { + var = ov::Tensor(); return; } std::string type_str; - read(stream, type_str); + stream & type_str; ov::element::Type type(type_str); ov::Shape shape; - read(stream, shape); + stream & shape; std::size_t byte_size = 0; - read(stream, byte_size); + stream & byte_size; - var = ov::Tensor(type, shape); + if (allocator) { + var = allocator(type, shape); + } else { + var = ov::Tensor(type, shape); + } + NPUW_ASSERT(var && "Tensor allocator returned an empty tensor"); + NPUW_ASSERT(var.get_element_type() == type && var.get_shape() == shape && + "Tensor allocator returned tensor with unexpected type or shape"); + NPUW_ASSERT(var.get_byte_size() == byte_size && "Tensor allocator returned tensor with unexpected byte size"); - stream.read(reinterpret_cast(var.data()), byte_size); + stream.bytes(var.data(), byte_size); } -void ov::npuw::s11n::read(std::istream& stream, ::intel_npu::Config& var) { - std::string str; - read(stream, str); - var.fromString(str); -} +void ov::npuw::orc::serialize(Stream& stream, std::shared_ptr& var) { + if (stream.output()) { + OPENVINO_THROW( + "Serializing shared_ptr is not supported, serialize an output port instead"); + } -void ov::npuw::s11n::read(std::istream& stream, std::shared_ptr& var) { std::string elem_type_str; std::string part_shape_str; std::unordered_set names; - read(stream, elem_type_str); - read(stream, part_shape_str); - read(stream, names); - // NOTE: the code below is taken from NPU plugin's create_dummy_model() + stream & elem_type_str & part_shape_str & names; var = std::make_shared(ov::element::Type(elem_type_str), ov::PartialShape(part_shape_str)); if (!names.empty()) { - var->set_friendly_name(*names.begin()); // FIXME: any_name ? + var->set_friendly_name(*names.begin()); } var->output(0).get_tensor().set_names(names); } -void ov::npuw::s11n::read(std::istream& stream, std::shared_ptr& var) { +void ov::npuw::orc::serialize(Stream& stream, std::shared_ptr& var) { + if (stream.output()) { + OPENVINO_THROW("Serializing shared_ptr is not supported, serialize an output port instead"); + } + std::string elem_type_str; std::string part_shape_str; std::unordered_set names; - read(stream, elem_type_str); - read(stream, part_shape_str); - read(stream, names); - // NOTE: the code below is taken from NPU plugin's create_dummy_model() + stream & elem_type_str & part_shape_str & names; std::shared_ptr res = std::make_shared(ov::element::Type(elem_type_str), std::vector{1}); - // FIXME: serialize names as well? const std::shared_ptr& tensor_dummy = std::make_shared(ov::element::Type(elem_type_str), ov::PartialShape(part_shape_str), @@ -509,90 +227,105 @@ void ov::npuw::s11n::read(std::istream& stream, std::shared_ptr& var) var = std::make_shared(res); var->output(0).set_tensor_ptr(tensor_dummy); if (!names.empty()) { - var->set_friendly_name(*names.begin()); // any_name ? + var->set_friendly_name(*names.begin()); } } -void ov::npuw::s11n::read_any(std::istream& stream, ov::Any& var) { +void ov::npuw::orc::serialize(Stream& stream, ov::Any& var) { std::string str; - read(stream, str); - var = ov::npuw::s11n::stringToAny(str); -} - -void ov::npuw::s11n::read(std::istream& stream, ov::npuw::weights::LazyTensor& var) { - var = ov::npuw::weights::LazyTensor::deserialize(stream); + if (stream.output()) { + str = ov::npuw::s11n::anyToString(var); + } + stream & str; + if (stream.input()) { + var = ov::npuw::s11n::stringToAny(str); + } } -void ov::npuw::s11n::read(std::istream& stream, ov::CacheMode& var) { - stream.read(reinterpret_cast(&var), sizeof var); +void ov::npuw::orc::serialize(Stream& stream, ov::CacheMode& var) { + stream.bytes(&var, sizeof var); } -void ov::npuw::s11n::read(std::istream& stream, ov::element::Type& var) { - stream.read(reinterpret_cast(&var), sizeof var); +void ov::npuw::orc::serialize(Stream& stream, ov::element::Type& var) { + stream.bytes(&var, sizeof var); } -void ov::npuw::s11n::read(std::istream& stream, ov::hint::PerformanceMode& var) { - stream.read(reinterpret_cast(&var), sizeof var); +void ov::npuw::orc::serialize(Stream& stream, ov::hint::PerformanceMode& var) { + stream.bytes(&var, sizeof var); } -void ov::npuw::s11n::read(std::istream& stream, ov::AnyMap& var) { +void ov::npuw::orc::serialize(Stream& stream, ov::AnyMap& var) { std::string str; - read(stream, str); - var = ov::npuw::s11n::stringToAnyMap(str); + if (stream.output()) { + str = ov::npuw::s11n::anyMapToString(var); + } + stream & str; + if (stream.input()) { + var = ov::npuw::s11n::stringToAnyMap(str); + } } // Weightless // FIXME: all serialization needs a good rewriting -void ov::npuw::s11n::write_weightless(std::ostream& stream, - const std::vector& var, - const ov::npuw::s11n::WeightsContext& ctx) { - write(stream, var.size()); - for (const auto& t : var) { - if (!t) { - write(stream, false); - continue; - } - write(stream, true); - auto data = t.data(); - auto iter = ctx.const_to_offset.find(data); - if (iter == ctx.const_to_offset.end()) { - write(stream, false); - write(stream, t); - } else { - write(stream, true); - write(stream, t.get_element_type().to_string()); - write(stream, t.get_shape()); - write(stream, t.get_byte_size()); - write(stream, iter->second); // offset in weights file +void ov::npuw::orc::serialize_weightless(Stream& stream, + std::vector& var, + const ov::npuw::s11n::WeightsContext& ctx) { + if (stream.output()) { + auto size = var.size(); + serialize(stream, size); + for (auto& t : var) { + if (!t) { + bool is_initialized = false; + serialize(stream, is_initialized); + continue; + } + bool is_initialized = true; + serialize(stream, is_initialized); + auto data = t.data(); + auto iter = ctx.const_to_offset.find(data); + if (iter == ctx.const_to_offset.end()) { + bool is_weightless = false; + serialize(stream, is_weightless); + auto tensor = t; + serialize(stream, tensor); + } else { + bool is_weightless = true; + serialize(stream, is_weightless); + auto elem_type = t.get_element_type().to_string(); + serialize(stream, elem_type); + auto shape = t.get_shape(); + serialize(stream, shape); + auto byte_size = t.get_byte_size(); + serialize(stream, byte_size); + auto offset = iter->second; + serialize(stream, offset); + } } + return; } -} -void ov::npuw::s11n::read_weightless(std::istream& stream, - std::vector& var, - const ov::npuw::s11n::WeightsContext& ctx) { var.clear(); std::size_t size; - read(stream, size); + serialize(stream, size); for (std::size_t i = 0; i < size; ++i) { bool is_initialized = false; - read(stream, is_initialized); + serialize(stream, is_initialized); if (!is_initialized) { var.push_back(ov::Tensor()); continue; } bool is_weightless = false; - read(stream, is_weightless); + serialize(stream, is_weightless); if (is_weightless) { std::string type_str; - read(stream, type_str); + serialize(stream, type_str); ov::element::Type type(type_str); ov::Shape shape; - read(stream, shape); + serialize(stream, shape); std::size_t byte_size = 0; - read(stream, byte_size); + serialize(stream, byte_size); std::size_t offset = 0; - read(stream, offset); + serialize(stream, offset); ov::Tensor t(type, shape); if (ctx.weights) { @@ -623,7 +356,7 @@ void ov::npuw::s11n::read_weightless(std::istream& stream, var.push_back(t); } else { ov::Tensor t; - read(stream, t); + serialize(stream, t); var.push_back(t); } } diff --git a/src/plugins/intel_npu/src/plugin/npuw/serialization.hpp b/src/plugins/intel_npu/src/plugin/npuw/serialization.hpp index 99bd1003b871..d5072db64066 100644 --- a/src/plugins/intel_npu/src/plugin/npuw/serialization.hpp +++ b/src/plugins/intel_npu/src/plugin/npuw/serialization.hpp @@ -8,16 +8,24 @@ #include #include #include +#include #include #include #include #include #include +#include #include #include #include +#include "attention.hpp" +#include "host_flash_attention.hpp" +#include "openvino/core/shape.hpp" #include "openvino/runtime/file_handle.hpp" +#include "orc.hpp" +#include "pyramid_attention.hpp" +#include "spatial.hpp" namespace ov { namespace npuw { @@ -36,7 +44,7 @@ const constexpr ov::npuw::s11n::IndicatorType NPUW_COMPILED_MODEL_INDICATOR = const constexpr ov::npuw::s11n::IndicatorType NPUW_LLM_COMPILED_MODEL_INDICATOR = {char{0x4c}, char{0x4c}, char{0x4d}, char{0x43}, char{0x4d}, char{0x4f}}; -const constexpr char* NPUW_SERIALIZATION_VERSION = "0.21"; +const constexpr char* NPUW_SERIALIZATION_VERSION = "0.25"; // Forward declaration namespace intel_npu { @@ -87,8 +95,12 @@ struct MoEDownstream; } // namespace compiled namespace weights { class LazyTensor; +class Bank; } // namespace weights +// -------------------------------------------------------------------------- +// s11n: context types, helper aliases, and utilities +// -------------------------------------------------------------------------- namespace s11n { class PairHash { @@ -129,10 +141,11 @@ struct WeightsContext { WeightsContext() = default; - // NOTE: This construtor should only be used when exporting blobs + // NOTE: This constructor should only be used when exporting blobs. WeightsContext(bool _is_weightless, const std::unordered_map& _const_to_offset); - // NOTE: This construtor can and should only be used when importing weightless blobs + // NOTE: This constructor is used on blob import to carry the resolved weight source + // (embedded weights, mmap'ed weights file, or model-backed constants cache). WeightsContext(const ov::npuw::s11n::WeightsPtr& _weights, const std::string& _weights_path, const ConstsCache& _consts_cache, @@ -168,217 +181,116 @@ struct SubmodelDeserializeCtx { compiled_model(_compiled_model), import_config(_import_config) {} + SubmodelDeserializeCtx(const std::shared_ptr& _plugin, + const ov::SoPtr& _compiled_model, + std::function _device_by_index, + std::function(const std::string&)> _import_config_for_device) + : plugin(_plugin), + compiled_model(_compiled_model), + device_by_index(std::move(_device_by_index)), + import_config_for_device(std::move(_import_config_for_device)) {} + std::shared_ptr plugin; std::string device; const ov::SoPtr& compiled_model; std::map import_config; + std::function device_by_index; + std::function(const std::string&)> import_config_for_device; }; BF16Cache get_bf16_consts(const std::shared_ptr& model); -// Specific type overloads -void write(std::ostream& stream, const std::streampos& var); -void write(std::ostream& stream, const std::string& var); -void write(std::ostream& stream, const bool& var); -void write(std::ostream& stream, const float& var); -void write(std::ostream& stream, const ov::npuw::compiled::Spatial& var); -void write(std::ostream& stream, const ov::npuw::compiled::Attention& var); -void write(std::ostream& stream, const ov::npuw::compiled::PyramidAttention& var); -void write(std::ostream& stream, const ov::npuw::compiled::HostFlashAttention& var); -void write(std::ostream& stream, const ov::npuw::compiled::MoEExperts& var); -void write(std::ostream& stream, const ov::npuw::compiled::MoEDownstream& var); -void write(std::ostream& stream, const ov::Tensor& var); -void write(std::ostream& stream, const ::intel_npu::Config& var); -void write(std::ostream& stream, const ov::Output& var); -void write_any(std::ostream& stream, const ov::Any& var); -void write(std::ostream& stream, const ov::npuw::weights::LazyTensor& var); -void write(std::ostream& stream, const ov::CacheMode& var); -void write(std::ostream& stream, const ov::element::Type& var); -void write(std::ostream& stream, const std::map& var); -void write(std::ostream& stream, const ov::hint::PerformanceMode& var); - -void read(std::istream& stream, std::streampos& var); -void read(std::istream& stream, std::string& var); -void read(std::istream& stream, bool& var); -void read(std::istream& stream, float& var); -void read(std::istream& stream, ov::npuw::compiled::Spatial& var); -void read(std::istream& stream, ov::npuw::compiled::Attention& var); -void read(std::istream& stream, ov::npuw::compiled::PyramidAttention& var); -void read(std::istream& stream, ov::npuw::compiled::HostFlashAttention& var); -void read(std::istream& stream, ov::npuw::compiled::MoEExperts& var); -void read(std::istream& stream, ov::npuw::compiled::MoEDownstream& var); -void read(std::istream& stream, ov::Tensor& var); -void read(std::istream& stream, ::intel_npu::Config& var); -void read(std::istream& stream, std::shared_ptr& var); -void read(std::istream& stream, std::shared_ptr& var); -void read_any(std::istream& stream, ov::Any& var); -void read(std::istream& stream, ov::npuw::weights::LazyTensor& var); -void read(std::istream& stream, ov::CacheMode& var); -void read(std::istream& stream, ov::element::Type& var); -void read(std::istream& stream, std::map& var); -void read(std::istream& stream, ov::hint::PerformanceMode& var); - -// Weightless utils -void write_weightless(std::ostream& stream, const std::vector& var, const WeightsContext& ctx); -// No allocation needed -void read_weightless(std::istream& stream, std::vector& var, const WeightsContext& ctx); - -// Forward declaration -template -void write(std::ostream& stream, const std::pair& var); -template -void write(std::ostream& stream, const std::vector& var); -template -void write(std::ostream& stream, const std::array& var); -template -void read(std::istream& stream, std::pair& var); -template -void read(std::istream& stream, std::vector& var); -template -void read(std::istream& stream, std::array& var); +// The NPUW stream type IS the ORC stream. +using Stream = ::ov::npuw::orc::Stream; -// Serialization -template ::value, bool> = true> -void write(std::ostream& stream, const T& var) { - stream.write(reinterpret_cast(&var), sizeof var); -} +// Bring all orc serialize overloads into s11n namespace so that +// qualified ov::npuw::s11n::serialize(...) call sites continue to work. +using ::ov::npuw::orc::serialize; -template -void write(std::ostream& stream, const std::pair& var) { - write(stream, var.first); - write(stream, var.second); -} +using TensorAllocator = std::function; template -void write(std::ostream& stream, const std::vector& var) { - write(stream, var.size()); - for (const auto& el : var) { - write(stream, el); - } -} - -template -void write(std::ostream& stream, const std::array& var) { - for (const auto& el : var) { - write(stream, el); - } -} - -template -void write(std::ostream& stream, const std::unordered_set& var) { - write(stream, var.size()); - for (const auto& el : var) { - write(stream, el); - } -} - -template -void write(std::ostream& stream, const std::unordered_set& var) { - write(stream, var.size()); - for (const auto& el : var) { - write(stream, el); - } -} - -template -void write(std::ostream& stream, const std::map& var) { - write(stream, var.size()); - for (const auto& el : var) { - write(stream, el); - } +void write(std::ostream& stream, const T& var) { + auto stream_io = Stream::writer(stream); + auto& mutable_var = const_cast&>(var); + stream_io & mutable_var; } template -void write(std::ostream& stream, const std::optional& var) { - if (var) { - write(stream, true); - write(stream, var.value()); - } else { - write(stream, false); - } -} - -// Deserialization -template ::value, bool> = true> void read(std::istream& stream, T& var) { - stream.read(reinterpret_cast(&var), sizeof var); -} - -template -void read(std::istream& stream, std::pair& var) { - read(stream, var.first); - read(stream, var.second); + auto stream_io = Stream::reader(stream); + stream_io & var; } -template -void read(std::istream& stream, std::vector& var) { - var.clear(); - std::size_t var_size = 0; - stream.read(reinterpret_cast(&var_size), sizeof var_size); - var.reserve(var_size); - for (std::size_t i = 0; i < var_size; ++i) { - T elem; - read(stream, elem); - var.push_back(std::move(elem)); - } +inline void write_any(std::ostream& stream, const ov::Any& var) { + write(stream, var); } -template -void read(std::istream& stream, std::array& var) { - for (std::size_t i = 0; i < N; ++i) { - T elem; - read(stream, elem); - var[i] = elem; - } +inline void read_any(std::istream& stream, ov::Any& var) { + read(stream, var); } -template -void read(std::istream& stream, std::unordered_set& var) { - var.clear(); - std::size_t var_size = 0; - stream.read(reinterpret_cast(&var_size), sizeof var_size); - for (std::size_t i = 0; i < var_size; ++i) { - T elem; - read(stream, elem); - var.insert(std::move(elem)); - } -} +} // namespace s11n -template -void read(std::istream& stream, std::unordered_set& var) { - var.clear(); - std::size_t var_size = 0; - stream.read(reinterpret_cast(&var_size), sizeof var_size); - for (std::size_t i = 0; i < var_size; ++i) { - T elem; - read(stream, elem); - var.insert(std::move(elem)); - } -} +// -------------------------------------------------------------------------- +// NPUW-specific orc serializers (in orc namespace for ADL from orc::Stream) +// -------------------------------------------------------------------------- +namespace orc { + +// Raw stream position — used by the compiled model blob header. +void serialize(Stream& stream, std::streampos& var); + +// NPUW compiled structures +void serialize(Stream& stream, ov::npuw::compiled::Spatial& var); +void serialize(Stream& stream, ov::npuw::compiled::Spatial::Param& var); +void serialize(Stream& stream, ov::npuw::compiled::Attention& var); +void serialize(Stream& stream, ov::npuw::compiled::Attention::Param& var); +void serialize(Stream& stream, ov::npuw::compiled::PyramidAttention& var); +void serialize(Stream& stream, ov::npuw::compiled::PyramidAttentionInfo& var); +void serialize(Stream& stream, ov::npuw::compiled::PyramidAttentionInfo::Param& var); +void serialize(Stream& stream, ov::npuw::compiled::HostFlashAttention& var); +void serialize(Stream& stream, ov::npuw::compiled::MoEExperts& var); +void serialize(Stream& stream, ov::npuw::compiled::MoEDownstream& var); + +// OpenVINO runtime types +void serialize(Stream& stream, ov::Tensor& var); +void serialize(Stream& stream, ov::CacheMode& var); +void serialize(Stream& stream, ov::element::Type& var); +void serialize(Stream& stream, ov::hint::PerformanceMode& var); +void serialize(Stream& stream, ov::Any& var); +void serialize(Stream& stream, ov::AnyMap& var); +void serialize(Stream& stream, ov::Output& var); +void serialize(Stream& stream, std::shared_ptr& var); +void serialize(Stream& stream, std::shared_ptr& var); + +// NPU plugin configuration +void serialize(Stream& stream, ::intel_npu::Config& var); + +// NPUW weight types +void serialize(Stream& stream, ov::npuw::weights::LazyTensor& var); +void serialize(Stream& stream, ov::npuw::weights::Bank& var); + +// Weightless tensor utilities +void serialize_weightless(Stream& stream, std::vector& var, const s11n::WeightsContext& ctx); +void transfer_tensor(Stream& stream, ov::Tensor& var, const s11n::TensorAllocator& allocator = {}); + +} // namespace orc + +// Reopen s11n for weightless helpers that need the orc declarations above. +namespace s11n { -template -void read(std::istream& stream, std::map& var) { - var.clear(); - std::size_t var_size = 0; - stream.read(reinterpret_cast(&var_size), sizeof var_size); - for (std::size_t i = 0; i < var_size; ++i) { - std::pair elem; - read(stream, elem); - var[elem.first] = elem.second; - } +inline void write_weightless(std::ostream& stream, const std::vector& var, const WeightsContext& ctx) { + auto stream_io = Stream::writer(stream); + auto& mutable_var = const_cast&>(var); + orc::serialize_weightless(stream_io, mutable_var, ctx); } -template -void read(std::istream& stream, std::optional& var) { - bool has_value = false; - read(stream, has_value); - if (has_value) { - T val; - read(stream, val); - var = val; - } +inline void read_weightless(std::istream& stream, std::vector& var, const WeightsContext& ctx) { + auto stream_io = Stream::reader(stream); + orc::serialize_weightless(stream_io, var, ctx); } } // namespace s11n + } // namespace npuw } // namespace ov diff --git a/src/plugins/intel_npu/src/plugin/npuw/unfold_sync_infer_request.cpp b/src/plugins/intel_npu/src/plugin/npuw/unfold_sync_infer_request.cpp index b850f2ca0255..b02421deee1d 100644 --- a/src/plugins/intel_npu/src/plugin/npuw/unfold_sync_infer_request.cpp +++ b/src/plugins/intel_npu/src/plugin/npuw/unfold_sync_infer_request.cpp @@ -4,6 +4,7 @@ #include "unfold_sync_infer_request.hpp" +#include "attn/attn_subgraph.hpp" #include "compiled_model.hpp" #include "logging.hpp" #include "openvino/core/parallel.hpp" @@ -30,7 +31,7 @@ ov::npuw::UnfoldInferRequest::UnfoldInferRequest(const std::shared_ptr> 4; @@ -913,30 +932,65 @@ bool ov::npuw::util::matchLoRAMatMulAlphaString(const std::string& input) { return ov::npuw::util::matchStringWithLoRAPattern(input, LoRANames::MatMul_alpha); } +bool ov::npuw::util::matchLinCacheString(const std::string& input, const std::string& past_or_present) { + static std::regex past_regex_pattern("^cache_params\\.past\\.(conv|ssm)\\.(\\d+)$"); + static std::regex present_regex_pattern("^cache_params\\.present\\.(conv|ssm)\\.(\\d+)$"); + const std::regex& regex_pattern = (past_or_present == "past") ? past_regex_pattern : present_regex_pattern; + return std::regex_match(input, regex_pattern); +} + +bool ov::npuw::util::starts_with_past_lincache(const std::string& input_name) { + static constexpr const char* past_lin_conv_cache = "cache_params.past.conv"; + static constexpr const char* past_lin_ssm_cache = "cache_params.past.ssm"; + return ov::npuw::util::starts_with(input_name, past_lin_conv_cache) || + ov::npuw::util::starts_with(input_name, past_lin_ssm_cache); +} void ov::npuw::util::fill_tensor_bytes(ov::SoPtr tensor, uint8_t fill_val) { auto* tensor_data = reinterpret_cast(tensor->data()); const size_t byte_size = tensor->get_byte_size(); std::memset(tensor_data, fill_val, byte_size); } -std::optional ov::npuw::util::isPastKeyValuesKey(const std::string& str) { - // Capture the number in parentheses - std::regex pattern(R"(past_key_values\.(\d+)\.key)"); +bool ov::npuw::util::isPastKeyParam(const std::string& str) { + // Match any past key param: contiguous or block-split (e.g. key_block_3, key_block_tail). + static const std::regex pattern(R"(past_key_values\.\d+\.key(_block_(\d+|tail))?)"); + return std::regex_match(str, pattern); +} + +bool ov::npuw::util::isPastValueParam(const std::string& str) { + // Match any past value param: contiguous or block-split. + static const std::regex pattern(R"(past_key_values\.\d+\.value(_block_(\d+|tail))?)"); + return std::regex_match(str, pattern); +} + +bool ov::npuw::util::isRestoredPastKeyValueParam(const std::string& str) { + // Match badly handled KVCache states by StatefulToStateless pass for Whisper. + static const std::regex restored_pattern( + R"((input_restored\.past_key_values\.(\d+)\.decoder\.(key|value))(present\.(\d+)\.decoder\.(key|value)))"); + ; + return std::regex_match(str, restored_pattern); +} + +std::optional ov::npuw::util::isPastKeyValuesKeyContiguous(const std::string& str) { + // Match only the single contiguous past key param (no _block_ suffix). + // Allows optional intermediate parts like "encoder" or "decoder" (for Whisper). + // Returns the layer index if matched. + std::regex pattern(R"(past_key_values\.(\d+)(?:\.[^.]+)*\.key)"); std::smatch match; if (std::regex_match(str, match, pattern)) { - // match[1] contains the number int index = std::stoi(match[1].str()); return index; } return std::nullopt; } -std::optional ov::npuw::util::isPastKeyValuesValue(const std::string& str) { - // Capture the number in parentheses - std::regex pattern(R"(past_key_values\.(\d+)\.value)"); +std::optional ov::npuw::util::isPastKeyValuesValueContiguous(const std::string& str) { + // Match only the single contiguous past value param (no _block_ suffix). + // Allows optional intermediate parts like "encoder" or "decoder" (for Whisper). + // Returns the layer index if matched. + std::regex pattern(R"(past_key_values\.(\d+)(?:\.[^.]+)*\.value)"); std::smatch match; if (std::regex_match(str, match, pattern)) { - // match[1] contains the number int index = std::stoi(match[1].str()); return index; } @@ -944,11 +998,9 @@ std::optional ov::npuw::util::isPastKeyValuesValue(const std::string& str) } std::optional ov::npuw::util::isPresentKeyValuesKey(const std::string& str) { - // Capture the number in parentheses - std::regex pattern(R"(present\.(\d+)\.key)"); + std::regex pattern(R"(present\.(\d+)(?:\.[^.]+)*\.key)"); std::smatch match; if (std::regex_match(str, match, pattern)) { - // match[1] contains the number int index = std::stoi(match[1].str()); return index; } @@ -956,11 +1008,9 @@ std::optional ov::npuw::util::isPresentKeyValuesKey(const std::string& str) } std::optional ov::npuw::util::isPresentKeyValuesValue(const std::string& str) { - // Capture the number in parentheses - std::regex pattern(R"(present\.(\d+)\.value)"); + std::regex pattern(R"(present\.(\d+)(?:\.[^.]+)*\.value)"); std::smatch match; if (std::regex_match(str, match, pattern)) { - // match[1] contains the number int index = std::stoi(match[1].str()); return index; } @@ -1081,4 +1131,4 @@ ov::npuw::util::SDPAPatternNodes ov::npuw::util::find_sdpa_pattern_nodes(const s return {}; } return internal_nodes.front(); -} \ No newline at end of file +} diff --git a/src/plugins/intel_npu/src/plugin/npuw/util.hpp b/src/plugins/intel_npu/src/plugin/npuw/util.hpp index b800a14d477d..2d5073a9c0e8 100644 --- a/src/plugins/intel_npu/src/plugin/npuw/util.hpp +++ b/src/plugins/intel_npu/src/plugin/npuw/util.hpp @@ -37,6 +37,31 @@ bool starts_with(const std::string& str, const std::string& prefix); std::string fmt(std::size_t number, std::size_t total); +// Matches the three DynamicQuantize decomposition implementations declared in +// kv_cache_compressed.hpp. +enum class DynamicQuantDecomposeMode { + // V1: handcrafted symmetric-style path, i8 range [-127, 127]. + HandcraftedSymmetricI8 = 1, + + // V2: ONNX DynamicQuantizeLinear-style path, u8 range [0, 255]. + OnnxDynamicQuantizeLinear = 2, + + // V3: compiler pattern-style path. i8 requests are materialized as u8 + // storage for the asymmetric decomposition branch. + CompilerPatternI8 = 3, +}; + +struct DynamicQuantStorageTypes { + ov::element::Type quantized_data_type = ov::element::dynamic; + ov::element::Type zero_point_type = ov::element::dynamic; + ov::element::Type scale_type = ov::element::f32; +}; + +DynamicQuantStorageTypes resolve_dynamic_quant_storage_types(DynamicQuantDecomposeMode decompose_mode, + bool is_symmetric, + const ov::element::Type& quant_dt, + const ov::element::Type& scale_dt = ov::element::f32); + struct UnpackOptions { bool bUseOvParallelFor; size_t nPartitions; // if 0 we use 64 elements step in parallel for, otherwise target workload is dynamically @@ -139,6 +164,12 @@ struct Impl { const V& at_or_at_or_at(const K& k1, const K& k2, const K& k3) const { return const_cast(this)->at_or_at_or_at(k1, k2, k3); } + + template + V at_or(const K& k, const V& default_val) const { + const auto iter = m->find(k); + return iter != m->end() ? iter->second : default_val; + } }; template @@ -190,6 +221,10 @@ bool matchLoRAMatMulBString(const std::string& input); bool matchLoRAMatMulAlphaString(const std::string& input); +bool matchLinCacheString(const std::string& input, const std::string& past_or_present = "past"); + +bool starts_with_past_lincache(const std::string& input_name); + // Structure to hold SDPA pattern nodes struct SDPAPatternNodes { std::shared_ptr matmul1_node = nullptr; @@ -275,11 +310,32 @@ class Delayed { mutable bool done = false; }; -std::optional isPastKeyValuesKey(const std::string& str); -std::optional isPastKeyValuesValue(const std::string& str); +// Matches only the contiguous (non-block-split) past key param. Returns the layer index if matched. +std::optional isPastKeyValuesKeyContiguous(const std::string& str); +// Matches only the contiguous (non-block-split) past value param. Returns the layer index if matched. +std::optional isPastKeyValuesValueContiguous(const std::string& str); + +// Backward compatibility: Alias for isPastKeyValuesKeyContiguous +inline std::optional isPastKeyValuesKey(const std::string& str) { + return isPastKeyValuesKeyContiguous(str); +} +// Backward compatibility: Alias for isPastKeyValuesValueContiguous +inline std::optional isPastKeyValuesValue(const std::string& str) { + return isPastKeyValuesValueContiguous(str); +} + std::optional isPresentKeyValuesKey(const std::string& str); std::optional isPresentKeyValuesValue(const std::string& str); +// Matches any past key param: contiguous (past_key_values.N.key) or block-split (key_block_M). +bool isPastKeyParam(const std::string& str); +// Matches any past value param: contiguous or block-split. +bool isPastValueParam(const std::string& str); + +// To remove input KV params that got badly matched in StatefulToStateless pass +// in Whisper model. +bool isRestoredPastKeyValueParam(const std::string& str); + } // namespace util } // namespace npuw } // namespace ov diff --git a/src/plugins/intel_npu/src/plugin/npuw/v1/elements/accuracy_checked.cpp b/src/plugins/intel_npu/src/plugin/npuw/v1/elements/accuracy_checked.cpp new file mode 100644 index 000000000000..f713f1f4a1f7 --- /dev/null +++ b/src/plugins/intel_npu/src/plugin/npuw/v1/elements/accuracy_checked.cpp @@ -0,0 +1,250 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "accuracy_checked.hpp" + +#include + +#include "../../logging.hpp" +#include "openvino/core/except.hpp" +#include "openvino/runtime/make_tensor.hpp" + +namespace ov::npuw::accuracy_checked { + +// ============================================================================ +// CompiledModel +// ============================================================================ + +ov::SoPtr CompiledModel::create(const std::shared_ptr& model, + const std::shared_ptr& plugin, + ov::SoPtr main_compiled, + ov::SoPtr ref_compiled, + Checker checker) { + OPENVINO_ASSERT(main_compiled._ptr != nullptr, "AccuracyChecked: main compiled model must not be null"); + OPENVINO_ASSERT(static_cast(checker), "AccuracyChecked: checker function must not be null"); + + if (ref_compiled._ptr == nullptr) { + return main_compiled; + } + + auto cm = std::make_shared(model, + plugin, + std::move(main_compiled), + std::move(ref_compiled), + std::move(checker)); + return {cm, {}}; +} + +CompiledModel::CompiledModel(const std::shared_ptr& model, + const std::shared_ptr& plugin, + ov::SoPtr main_compiled, + ov::SoPtr ref_compiled, + Checker checker) + : ov::ICompiledModel(model, plugin), + m_main_compiled(std::move(main_compiled)), + m_ref_compiled(std::move(ref_compiled)), + m_checker(std::move(checker)) {} + +ov::SoPtr CompiledModel::active_compiled_model_locked() const { + return m_switched_to_reference ? m_ref_compiled : m_main_compiled; +} + +bool CompiledModel::has_switched_to_reference() const { + std::lock_guard lock(m_mutex); + return m_switched_to_reference; +} + +void CompiledModel::export_model(std::ostream& stream) const { + std::lock_guard lock(m_mutex); + active_compiled_model_locked()->export_model(stream); +} + +std::shared_ptr CompiledModel::get_runtime_model() const { + std::lock_guard lock(m_mutex); + return active_compiled_model_locked()->get_runtime_model(); +} + +void CompiledModel::set_property(const ov::AnyMap& properties) { + std::lock_guard lock(m_mutex); + active_compiled_model_locked()->set_property(properties); +} + +ov::Any CompiledModel::get_property(const std::string& name) const { + std::lock_guard lock(m_mutex); + return active_compiled_model_locked()->get_property(name); +} + +std::shared_ptr CompiledModel::create_sync_infer_request() const { + auto self = std::static_pointer_cast(shared_from_this()); + return std::make_shared(std::move(self)); +} + +std::shared_ptr CompiledModel::create_infer_request() const { + return std::make_shared(create_sync_infer_request(), + get_task_executor(), + get_callback_executor()); +} + +// ============================================================================ +// InferRequest +// ============================================================================ + +InferRequest::InferRequest(std::shared_ptr compiled_model) + : ov::ISyncInferRequest(compiled_model), + m_acc_compiled_model(std::move(compiled_model)), + m_using_reference(m_acc_compiled_model->has_switched_to_reference()) {} + +void InferRequest::ensure_main_request_locked() const { + if (m_main_request) { + return; + } + m_main_request = m_acc_compiled_model->m_main_compiled->create_infer_request(); + OPENVINO_ASSERT(m_main_request, "AccuracyChecked: failed to create main infer request"); +} + +void InferRequest::ensure_ref_request_locked() const { + if (m_ref_request) { + return; + } + m_ref_request = m_acc_compiled_model->m_ref_compiled->create_infer_request(); + OPENVINO_ASSERT(m_ref_request, "AccuracyChecked: failed to create reference infer request"); +} + +void InferRequest::infer() { + std::lock_guard lock(m_mutex); + + if (m_using_reference) { + ensure_ref_request_locked(); + m_ref_request->infer(); + return; + } + + ensure_main_request_locked(); + + const auto& main_cm = m_acc_compiled_model->m_main_compiled; + const auto& ref_cm = m_acc_compiled_model->m_ref_compiled; + + // Snapshot current input/output tensors from the main request. + // These are the tensors the user has bound (or the request's defaults). + // We use them to (a) feed the reference request and (b) rebind after a + // permanent switch so that downstream requests keep reading from the + // same memory. + std::vector> main_input_tensors; + std::vector> main_output_tensors; + main_input_tensors.reserve(main_cm->inputs().size()); + main_output_tensors.reserve(main_cm->outputs().size()); + + for (const auto& port : main_cm->inputs()) { + main_input_tensors.push_back(m_main_request->get_tensor(port)); + } + for (const auto& port : main_cm->outputs()) { + main_output_tensors.push_back(m_main_request->get_tensor(port)); + } + + m_main_request->infer(); + + // Accuracy check: feed reference request with the same inputs and run it. + ensure_ref_request_locked(); + for (size_t i = 0; i < ref_cm->inputs().size(); i++) { + m_ref_request->set_tensor(ref_cm->inputs()[i], main_input_tensors[i]); + } + m_ref_request->infer(); + + // Compare outputs using the provided checker. + bool accurate = true; + for (size_t i = 0; i < main_cm->outputs().size(); i++) { + const auto& ref_tensor = m_ref_request->get_tensor(ref_cm->outputs()[i]); + if (!m_acc_compiled_model->m_checker(main_output_tensors[i], ref_tensor)) { + accurate = false; + } + } + + if (!accurate) { + LOG_WARN("Accuracy issue spotted, switching inference to the reference target"); + // Copy reference outputs into the main output buffers so that any + // downstream request already bound to those buffers sees the corrected + // values immediately. + for (size_t i = 0; i < main_cm->outputs().size(); i++) { + const auto& ref_tensor = m_ref_request->get_tensor(ref_cm->outputs()[i]); + ref_tensor->copy_to(main_output_tensors[i]._ptr); + } + + // Rebind the reference request to use the same tensor objects that the + // main request was using. From this point on, the reference request + // writes results directly into those buffers, keeping downstream + // tensor bindings valid without requiring update_subrequest_links(). + for (size_t i = 0; i < ref_cm->inputs().size(); i++) { + m_ref_request->set_tensor(ref_cm->inputs()[i], main_input_tensors[i]); + } + for (size_t i = 0; i < ref_cm->outputs().size(); i++) { + m_ref_request->set_tensor(ref_cm->outputs()[i], main_output_tensors[i]); + } + + m_using_reference = true; + { + std::lock_guard model_lock(m_acc_compiled_model->m_mutex); + m_acc_compiled_model->m_switched_to_reference = true; + } + } +} + +ov::SoPtr InferRequest::get_tensor(const ov::Output& port) const { + std::lock_guard lock(m_mutex); + + if (m_using_reference) { + ensure_ref_request_locked(); + auto found = find_port(port); + OPENVINO_ASSERT(found.found(), "AccuracyChecked: unknown port"); + const auto& ref_cm = m_acc_compiled_model->m_ref_compiled; + return found.is_output() ? m_ref_request->get_tensor(ref_cm->outputs()[found.idx]) + : m_ref_request->get_tensor(ref_cm->inputs()[found.idx]); + } + + ensure_main_request_locked(); + return m_main_request->get_tensor(port); +} + +void InferRequest::set_tensor(const ov::Output& port, const ov::SoPtr& tensor) { + std::lock_guard lock(m_mutex); + + if (m_using_reference) { + ensure_ref_request_locked(); + auto found = find_port(port); + OPENVINO_ASSERT(found.found(), "AccuracyChecked: unknown port"); + const auto& ref_cm = m_acc_compiled_model->m_ref_compiled; + if (found.is_output()) { + m_ref_request->set_tensor(ref_cm->outputs()[found.idx], tensor); + } else { + m_ref_request->set_tensor(ref_cm->inputs()[found.idx], tensor); + } + return; + } + + ensure_main_request_locked(); + m_main_request->set_tensor(port, tensor); +} + +void InferRequest::check_tensors() const {} + +std::vector> InferRequest::query_state() const { + std::lock_guard lock(m_mutex); + if (m_using_reference) { + ensure_ref_request_locked(); + return m_ref_request->query_state(); + } + ensure_main_request_locked(); + return m_main_request->query_state(); +} + +std::vector InferRequest::get_profiling_info() const { + std::lock_guard lock(m_mutex); + if (m_using_reference) { + ensure_ref_request_locked(); + return m_ref_request->get_profiling_info(); + } + ensure_main_request_locked(); + return m_main_request->get_profiling_info(); +} + +} // namespace ov::npuw::accuracy_checked diff --git a/src/plugins/intel_npu/src/plugin/npuw/v1/elements/accuracy_checked.hpp b/src/plugins/intel_npu/src/plugin/npuw/v1/elements/accuracy_checked.hpp new file mode 100644 index 000000000000..6f6c8cca41d7 --- /dev/null +++ b/src/plugins/intel_npu/src/plugin/npuw/v1/elements/accuracy_checked.hpp @@ -0,0 +1,108 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#pragma once + +#include +#include +#include + +#include "openvino/runtime/iasync_infer_request.hpp" +#include "openvino/runtime/icompiled_model.hpp" +#include "openvino/runtime/isync_infer_request.hpp" +#include "openvino/runtime/so_ptr.hpp" + +namespace ov::npuw::accuracy_checked { + +class InferRequest; + +// A compiled-model wrapper that validates inference accuracy against a +// reference device and permanently switches to that reference if the +// normalised RMSE threshold is exceeded. +// +// The wrapper is transparent: it exposes the same I/O as the wrapped +// model and forwards all property queries to the currently active model +// (main until a switch happens, reference afterwards). +// +// Intended to be composed on top of a failsafe::CompiledModel so that +// the full chain becomes: +// +// AccuracyChecked( main = Failsafe(NPU -> CPU), ref = CPU ) +class CompiledModel final : public ov::ICompiledModel { +public: + // Checker: returns true when the output pair is considered accurate. + using Checker = std::function&, const ov::SoPtr&)>; + + // Factory method. Returns main_compiled unwrapped when ref_compiled is + // null (no-op wrapper) to keep the zero-overhead path trivial. + static ov::SoPtr create(const std::shared_ptr& model, + const std::shared_ptr& plugin, + ov::SoPtr main_compiled, + ov::SoPtr ref_compiled, + Checker checker); + + CompiledModel(const std::shared_ptr& model, + const std::shared_ptr& plugin, + ov::SoPtr main_compiled, + ov::SoPtr ref_compiled, + Checker checker); + + void export_model(std::ostream& model) const override; + std::shared_ptr get_runtime_model() const override; + + void set_property(const ov::AnyMap& properties) override; + ov::Any get_property(const std::string& name) const override; + + std::shared_ptr create_sync_infer_request() const override; + std::shared_ptr create_infer_request() const override; + + // Returns true if any InferRequest has triggered a permanent switch + // to the reference compiled model. + bool has_switched_to_reference() const; + +private: + friend class InferRequest; + + ov::SoPtr active_compiled_model_locked() const; + + ov::SoPtr m_main_compiled; + ov::SoPtr m_ref_compiled; + Checker m_checker; + mutable std::mutex m_mutex; + mutable bool m_switched_to_reference = false; +}; + +// Sync infer request wrapper produced by AccuracyChecked::CompiledModel. +// +// On each infer() call it runs the main request, then copies its inputs +// to the reference request and runs that too. Outputs from both are +// compared using the Checker provided at construction. If any output +// fails the check the reference results are copied into the main output +// buffers and this request (and all subsequent requests from the same +// CompiledModel) permanently switch to reference-only inference. +class InferRequest final : public ov::ISyncInferRequest { +public: + explicit InferRequest(std::shared_ptr compiled_model); + + void infer() override; + + ov::SoPtr get_tensor(const ov::Output& port) const override; + void set_tensor(const ov::Output& port, const ov::SoPtr& tensor) override; + void check_tensors() const override; + + std::vector> query_state() const override; + std::vector get_profiling_info() const override; + +private: + void ensure_main_request_locked() const; + void ensure_ref_request_locked() const; + + std::shared_ptr m_acc_compiled_model; + mutable std::mutex m_mutex; + mutable std::shared_ptr m_main_request; + mutable std::shared_ptr m_ref_request; + mutable bool m_using_reference = false; +}; + +} // namespace ov::npuw::accuracy_checked diff --git a/src/plugins/intel_npu/src/plugin/npuw/v1/subgraph_pipeline.hpp b/src/plugins/intel_npu/src/plugin/npuw/v1/subgraph_pipeline.hpp new file mode 100644 index 000000000000..05436febf9b6 --- /dev/null +++ b/src/plugins/intel_npu/src/plugin/npuw/v1/subgraph_pipeline.hpp @@ -0,0 +1,367 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "openvino/core/except.hpp" +#include "openvino/pass/graph_rewrite.hpp" +#include "openvino/runtime/itensor.hpp" +#include "openvino/runtime/so_ptr.hpp" + +namespace ov { +class Model; +class ICompiledModel; +class IAsyncInferRequest; +} // namespace ov + +namespace ov::npuw { +class CompiledModel; +class IBaseInferRequest; +struct Function; +struct Subgraph; +namespace online { +class Snapshot; +} +} // namespace ov::npuw + +namespace ov::npuw::v1::subgraphs { + +class Context { +public: + template + T& put(T value) { + auto [it, inserted] = m_entries.insert_or_assign(std::type_index(typeid(T)), std::any(std::move(value))); + return *std::any_cast(&it->second); + } + + template + T& emplace(Args&&... args) { + auto [it, inserted] = m_entries.insert_or_assign(std::type_index(typeid(T)), + std::any(std::in_place_type, std::forward(args)...)); + return *std::any_cast(&it->second); + } + + template + bool contains() const { + return m_entries.find(std::type_index(typeid(T))) != m_entries.end(); + } + + template + void erase() { + m_entries.erase(std::type_index(typeid(T))); + } + + template + T* get_if() { + auto it = m_entries.find(std::type_index(typeid(T))); + return it == m_entries.end() ? nullptr : std::any_cast(&it->second); + } + + template + const T* get_if() const { + auto it = m_entries.find(std::type_index(typeid(T))); + return it == m_entries.end() ? nullptr : std::any_cast(&it->second); + } + + template + T& get() { + auto* value = get_if(); + if (!value) { + OPENVINO_THROW("NPUW subgraph pipeline context is missing entry for type ", typeid(T).name()); + } + return *value; + } + + template + const T& get() const { + auto* value = get_if(); + if (!value) { + OPENVINO_THROW("NPUW subgraph pipeline context is missing entry for type ", typeid(T).name()); + } + return *value; + } + + bool empty() const { + return m_entries.empty(); + } + + std::size_t size() const { + return m_entries.size(); + } + +private: + std::unordered_map m_entries; +}; + +struct Registration { + std::string group; + std::string name; + std::vector patterns; + std::size_t order = 0u; + + bool empty() const { + return group.empty() && name.empty() && patterns.empty() && order == 0u; + } +}; + +struct CompiledPipeline; +struct InferContext; +struct PartitioningCallbacks { + std::function(const std::string&)> find_tagged_model; +}; + +struct CompileContext { + std::shared_ptr& model; + ov::SoPtr& compiled_model; + const std::vector& devices; + std::function(const std::shared_ptr&, + const std::string&, + const std::vector&)> + compile_model; +}; + +struct FunctionPipeline { + Registration registration; + Context context; + std::function partition_stage; + std::function compile_stage; +}; + +class ISubgraphBehavior { +public: + using Ptr = std::unique_ptr; + + virtual void prepare(InferContext&) {} + virtual bool bind_function_input(InferContext&, std::size_t, const ov::SoPtr&) { + return false; + } + virtual bool bind_function_output(InferContext&, std::size_t, const ov::SoPtr&) { + return false; + } + virtual void prologue(InferContext&) {} + virtual void run(InferContext&) = 0; + virtual void epilogue(InferContext&) {} + + virtual ~ISubgraphBehavior() = default; +}; + +using RuntimeBehaviorFactory = std::function; +using CompileExecutor = std::function; + +struct RuntimeBehaviorSpec { + Registration registration; + Context context; + RuntimeBehaviorFactory factory; + bool handles_function_prologue = false; +}; + +struct CompiledPipeline { + Registration registration; + Context context; + CompileExecutor compile_executor; + std::optional runtime_behavior; + bool is_function_call = false; + std::optional function_body_subgraph_idx; +}; + +struct InferContext { + ov::npuw::CompiledModel& compiled_model; + ov::npuw::IBaseInferRequest& infer_request; + std::size_t subgraph_idx = 0u; + std::size_t real_subgraph_idx = 0u; + ov::SoPtr target_request; + Context* runtime_state = nullptr; + std::function legacy_infer; + std::function opaque_prologue; + std::function opaque_run; +}; + +using PostLegacyHook = std::function; + +class DirectBehavior final : public ISubgraphBehavior { +public: + using Runner = std::function; + + explicit DirectBehavior(Runner runner = {}) : m_runner(std::move(runner)) {} + + void run(InferContext& ctx) override { + if (m_runner) { + m_runner(ctx); + } + } + +private: + Runner m_runner; +}; + +inline ISubgraphBehavior::Ptr make_direct_behavior() { + return std::make_unique(); +} + +struct PatternRegistration { + using PartitionStage = std::function; + using CompileStage = std::function; + using MatcherRegistrar = std::function< + void(ov::pass::GraphRewrite&, const std::shared_ptr&, const std::string&)>; + + std::size_t id = 0u; + std::string pattern; + std::string tag; + std::string group; + std::string name; + PartitionStage partition_stage; + CompileStage compile_stage; + RuntimeBehaviorFactory runtime_factory; + MatcherRegistrar matcher_registrar; +}; + +class PatternRegistry; + +class ScopedPatternRegistration { +public: + ScopedPatternRegistration() = default; + ScopedPatternRegistration(PatternRegistry* registry, std::size_t id) : m_registry(registry), m_id(id) {} + ScopedPatternRegistration(const ScopedPatternRegistration&) = delete; + ScopedPatternRegistration& operator=(const ScopedPatternRegistration&) = delete; + ScopedPatternRegistration(ScopedPatternRegistration&& other) noexcept + : m_registry(std::exchange(other.m_registry, nullptr)), + m_id(std::exchange(other.m_id, 0u)) {} + ScopedPatternRegistration& operator=(ScopedPatternRegistration&& other) noexcept { + if (this != &other) { + reset(); + m_registry = std::exchange(other.m_registry, nullptr); + m_id = std::exchange(other.m_id, 0u); + } + return *this; + } + ~ScopedPatternRegistration() { + reset(); + } + + void reset(); + +private: + PatternRegistry* m_registry = nullptr; + std::size_t m_id = 0u; +}; + +template +class OnPatternBuilder { +public: + explicit OnPatternBuilder(PatternRegistry& registry) : m_registry(registry) { + m_registration.pattern = Pattern::pattern_name(); + m_registration.tag = Pattern::isolation_tag(); + m_registration.group = Pattern::group_name(); + m_registration.name = Pattern::pattern_name(); + m_registration.matcher_registrar = [](ov::pass::GraphRewrite& rewr, + const std::shared_ptr& snapshot, + const std::string& isol_tag) { + rewr.add_matcher(snapshot, isol_tag); + }; + } + + OnPatternBuilder& tag(std::string value) { + m_registration.tag = std::move(value); + return *this; + } + + OnPatternBuilder& group(std::string value) { + m_registration.group = std::move(value); + return *this; + } + + OnPatternBuilder& at_partition(PatternRegistration::PartitionStage stage) { + m_registration.partition_stage = std::move(stage); + return *this; + } + + OnPatternBuilder& at_compile(PatternRegistration::CompileStage stage) { + m_registration.compile_stage = std::move(stage); + return *this; + } + + OnPatternBuilder& at_runtime(RuntimeBehaviorFactory factory) { + m_registration.runtime_factory = std::move(factory); + return *this; + } + + ScopedPatternRegistration scoped(); + +private: + PatternRegistry& m_registry; + PatternRegistration m_registration; +}; + +class PatternRegistry { +public: + template + OnPatternBuilder on() { + return OnPatternBuilder(*this); + } + + ScopedPatternRegistration add(PatternRegistration registration) { + registration.id = ++m_next_id; + m_registrations.push_back(std::move(registration)); + return ScopedPatternRegistration(this, m_registrations.back().id); + } + + void erase(std::size_t id) { + m_registrations.erase(std::remove_if(m_registrations.begin(), + m_registrations.end(), + [id](const PatternRegistration& registration) { + return registration.id == id; + }), + m_registrations.end()); + } + + bool register_matcher(ov::pass::GraphRewrite& rewr, + const std::shared_ptr& snapshot, + const std::string& pattern, + const std::string& isol_tag) const { + bool handled = false; + for (const auto& registration : m_registrations) { + if (registration.pattern != pattern || !registration.matcher_registrar) { + continue; + } + registration.matcher_registrar(rewr, snapshot, isol_tag); + handled = true; + } + return handled; + } + + void apply(ov::npuw::Function& function) const; + void apply(ov::npuw::Subgraph& subgraph) const; + void append_from(const PatternRegistry& other); + +private: + std::vector m_registrations; + std::size_t m_next_id = 0u; +}; + +inline void ScopedPatternRegistration::reset() { + if (m_registry != nullptr) { + m_registry->erase(m_id); + m_registry = nullptr; + m_id = 0u; + } +} + +template +inline ScopedPatternRegistration OnPatternBuilder::scoped() { + return m_registry.add(std::move(m_registration)); +} + +} // namespace ov::npuw::v1::subgraphs diff --git a/src/plugins/intel_npu/src/plugin/npuw/weights_bank.cpp b/src/plugins/intel_npu/src/plugin/npuw/weights_bank.cpp index b43753c360a7..f93760220ad0 100644 --- a/src/plugins/intel_npu/src/plugin/npuw/weights_bank.cpp +++ b/src/plugins/intel_npu/src/plugin/npuw/weights_bank.cpp @@ -196,21 +196,20 @@ bool Bank::is_remote(int64_t uid) const { return false; } -void Bank::serialize(std::ostream& stream) const { - using namespace ov::npuw::s11n; - +void Bank::serialize(ov::npuw::orc::Stream& stream) { LOG_INFO("Serializing weights bank..."); LOG_BLOCK(); std::unique_lock guard(m_mutex); - write(stream, m_device_banks.size()); + std::size_t bank_size = m_device_banks.size(); + stream & bank_size; for (const auto& elem : m_device_banks) { const auto& device = elem.first; const auto& device_bank = elem.second; - write(stream, device); - write(stream, device_bank.storage.size()); + auto storage_size = device_bank.storage.size(); + stream & device & storage_size; // Write tensors sequentially according to sorted uids for better memory allocation and utilization std::set uids; for (const auto& t_pair : device_bank.storage) { @@ -218,47 +217,16 @@ void Bank::serialize(std::ostream& stream) const { } for (const auto& uid : uids) { - write(stream, uid); - write(stream, device_bank.storage.at(uid).tensor); - } - } - - LOG_INFO("DONE."); -} - -std::shared_ptr Bank::deserialize(std::istream& stream, - const std::shared_ptr& core, - const std::string& name) { - using namespace ov::npuw::s11n; - - LOG_INFO("Deserializing weights bank..."); - LOG_BLOCK(); - - auto bank = ov::npuw::weights::bank(name, core, ""); - - std::size_t bank_size = 0; - read(stream, bank_size); - - for (std::size_t i = 0; i < bank_size; ++i) { - std::string device; - read(stream, device); - std::size_t storage_size = 0; - read(stream, storage_size); - for (std::size_t j = 0; j < storage_size; ++j) { - int64_t uid = -1; - read(stream, uid); - bank->read_and_add_tensor(stream, uid, device); + stream & uid; + auto tensor = device_bank.storage.at(uid).tensor; + transfer_tensor(stream, tensor); } } LOG_INFO("DONE."); - - return bank; } -void Bank::read_and_add_tensor(std::istream& stream, int64_t uid, const std::string& device) { - using namespace ov::npuw::s11n; - +void Bank::read_and_add_tensor(ov::npuw::orc::Stream& stream, int64_t uid, const std::string& device) { // This method is supposed to be used only during deserialization std::unique_lock guard(m_mutex); @@ -273,40 +241,52 @@ void Bank::read_and_add_tensor(std::istream& stream, int64_t uid, const std::str if (device == "CPU") { // Just read deserialized tensor into the bank - read(stream, device_bank.storage[uid].tensor); + transfer_tensor(stream, device_bank.storage[uid].tensor); return; } // Need to allocate on device and copy deserialized tensor to that memory - ov::SoPtr remote_tensor; - ov::Tensor allocated_tensor; - - // FIXME: reading not via a dedicated function - bool is_intialized = false; - read(stream, is_intialized); - NPUW_ASSERT(is_intialized); - - std::string type_str; - read(stream, type_str); - ov::element::Type type(type_str); - - ov::Shape shape; - read(stream, shape); - - std::size_t byte_size = 0; - read(stream, byte_size); - auto remote_ctx = m_core->get_default_context(device)._ptr; - remote_tensor = remote_ctx->create_host_tensor(type, shape); - allocated_tensor = ov::make_tensor(remote_tensor); - device_bank.storage[uid] = {LazyTensor(), allocated_tensor}; - stream.read(reinterpret_cast(allocated_tensor.data()), byte_size); + transfer_tensor(stream, + device_bank.storage[uid].tensor, + [&remote_ctx](const ov::element::Type& type, const ov::Shape& shape) { + ov::SoPtr remote_tensor = remote_ctx->create_host_tensor(type, shape); + return ov::make_tensor(remote_tensor); + }); + NPUW_ASSERT(device_bank.storage[uid].tensor && "Remote tensor should be initialized during bank deserialize"); + device_bank.storage[uid].lt = LazyTensor(); } std::string Bank::get_name() const { return m_bank_name; } +void ov::npuw::orc::serialize(Stream& stream, ov::npuw::weights::Bank& var) { + if (stream.output()) { + var.serialize(stream); + } else { + LOG_INFO("Deserializing weights bank..."); + LOG_BLOCK(); + + std::size_t bank_size = 0; + stream & bank_size; + + for (std::size_t i = 0; i < bank_size; ++i) { + std::string device; + stream & device; + std::size_t storage_size = 0; + stream & storage_size; + for (std::size_t j = 0; j < storage_size; ++j) { + int64_t uid = -1; + stream & uid; + var.read_and_add_tensor(stream, uid, device); + } + } + + LOG_INFO("DONE."); + } +} + std::shared_ptr BankManager::getBank(const std::string& bank_name, const std::shared_ptr& core, const std::string& alloc_device) { diff --git a/src/plugins/intel_npu/src/plugin/npuw/weights_bank.hpp b/src/plugins/intel_npu/src/plugin/npuw/weights_bank.hpp index 10efad349b2c..3f8f69876abd 100644 --- a/src/plugins/intel_npu/src/plugin/npuw/weights_bank.hpp +++ b/src/plugins/intel_npu/src/plugin/npuw/weights_bank.hpp @@ -15,6 +15,8 @@ #include "openvino/runtime/iremote_context.hpp" #include "openvino/runtime/make_tensor.hpp" #include "openvino/runtime/tensor.hpp" +#include "orc.hpp" +#include "orc/schema_npuw.hpp" namespace ov { namespace npuw { @@ -25,6 +27,12 @@ namespace weights { class Bank { public: + static constexpr ov::npuw::orc::TypeId kOrcType = + static_cast(ov::npuw::orc::schema_npuw::WeightsBank::ID); + // Version 0 is the frozen baseline on the wire. Any further layout changes + // must be introduced through a new versioned payload rather than by mutating v0. + static constexpr ov::npuw::orc::Version kOrcVersion = 0u; + Bank(const std::shared_ptr& core, const std::string& alloc_device, const std::string& bank_name); // Register LazyTensor in a bank if it's not there. Returns LazyTensor's unique id @@ -43,6 +51,7 @@ class Bank { private: friend class ov::npuw::LLMCompiledModel; friend class ov::npuw::CompiledModel; + friend void ov::npuw::orc::serialize(ov::npuw::orc::Stream& stream, ov::npuw::weights::Bank& var); struct StoredTensor { LazyTensor lt; @@ -60,12 +69,8 @@ class Bank { const std::vector& to_process, const std::string& device); - void serialize(std::ostream& stream) const; - static std::shared_ptr deserialize(std::istream& stream, - const std::shared_ptr& core, - const std::string& name); - // Used during deserialization - void read_and_add_tensor(std::istream& stream, int64_t uid, const std::string& device); + void serialize(ov::npuw::orc::Stream& stream); + void read_and_add_tensor(ov::npuw::orc::Stream& stream, int64_t uid, const std::string& device); mutable std::mutex m_mutex; std::shared_ptr m_core = nullptr; diff --git a/src/plugins/intel_npu/src/plugin/npuw/whisper/prepare_whisper_model.cpp b/src/plugins/intel_npu/src/plugin/npuw/whisper/prepare_whisper_model.cpp index 35bdc392a747..537c5af72d9b 100644 --- a/src/plugins/intel_npu/src/plugin/npuw/whisper/prepare_whisper_model.cpp +++ b/src/plugins/intel_npu/src/plugin/npuw/whisper/prepare_whisper_model.cpp @@ -8,9 +8,11 @@ #include "../llm_compiled_model_utils.hpp" #include "openvino/op/ops.hpp" +#include "openvino/op/scaled_dot_product_attention.hpp" #include "openvino/openvino.hpp" #include "openvino/opsets/opset13.hpp" #include "openvino/pass/graph_rewrite.hpp" +#include "openvino/pass/manager.hpp" #include "openvino/pass/matcher_pass.hpp" #include "openvino/pass/pattern/op/optional.hpp" #include "openvino/pass/pattern/op/or.hpp" @@ -113,7 +115,8 @@ class AttentionMaskInput : public ov::pass::MatcherPass { for (auto node : model->get_ops()) { if (ov::is_type(node)) { if (node->inputs().size() > kAttnMaskPort && - ov::is_type(node->input(kAttnMaskPort).get_source_output().get_node())) { + (ov::is_type(node->input(kAttnMaskPort).get_source_output().get_node()) || + ov::is_type(node->input(kAttnMaskPort).get_source_output().get_node()))) { self_attn_nodes.push_back(node); } else { cross_attn_nodes.push_back(node); @@ -227,6 +230,193 @@ class CachePositionInput : public ov::pass::MatcherPass { } }; +bool can_move_scale_after_matmul(const ov::Output& query, + const ov::Output& kT, + const ov::Output& scale) { + const auto& scale_pshape = scale.get_partial_shape(); + const auto& query_pshape = query.get_partial_shape(); + if (scale_pshape.is_dynamic() || query_pshape.is_dynamic()) { + return false; + } + + // According to the ov SDPA specification, the scale input have to be 1d with 1 element + // or scalar. + if (ov::shape_size(scale_pshape.to_shape()) != 1) { + return false; + } + + // using the original implementation to calculate the shapes. + // we need to move the scale after MatMul only if the tensor after MatMul is smaller. + auto q_scaled = std::make_shared(query, scale); + auto scaled_attn = std::make_shared(q_scaled, kT); + const auto& scaled_attn_pshape = scaled_attn->output(0).get_partial_shape(); + if (scaled_attn_pshape.is_static()) { + return ov::shape_size(query_pshape.to_shape()) > ov::shape_size(scaled_attn_pshape.to_shape()); + } + return false; +} + +// FIXME: Whisper Decompose SDPA +class WhisperScaledDotProductAttentionDecomposition : public ov::pass::MatcherPass { +public: + OPENVINO_MATCHER_PASS_RTTI("WhisperScaledDotProductAttentionDecomposition"); + WhisperScaledDotProductAttentionDecomposition() { + auto pattern_node = ov::pass::pattern::wrap_type(); + + ov::matcher_pass_callback callback = [this, pattern_node](ov::pass::pattern::Matcher& m) { + auto& pattern_to_output = m.get_pattern_value_map(); + + auto node = ov::as_type_ptr( + pattern_to_output.at(pattern_node).get_node_shared_ptr()); + + if (node == nullptr || transformation_callback(node)) { + return false; + } + + const std::string& node_name = node->get_friendly_name(); + if (node_name.find("encoder_attn") == std::string::npos) { + // This pass is only for encoder-decoder cross-attention layers + return false; + } + + auto new_output_node = decompose(node); + ov::replace_node(node, new_output_node); + return true; + }; + + auto m = std::make_shared(pattern_node, + "WhisperScaledDotProductAttentionDecompositionMatcher"); + register_matcher(m, callback); + } + + std::shared_ptr decompose(std::shared_ptr node) { + using namespace ov::op; + using namespace ov; + auto query = node->input_value(0); + auto key = node->input_value(1); + auto value = node->input_value(2); + auto q_shape = register_new_node(query, element::i32); + auto k_shape = register_new_node(key, element::i32); + auto minus_one = register_new_node(v0::Constant::create(element::i32, Shape{}, {-1})); + auto minus_two = register_new_node(v0::Constant::create(element::i32, Shape{}, {-2})); + auto zero_i = register_new_node(v0::Constant::create(element::i32, Shape{}, {0})); + auto one_i = register_new_node(v0::Constant::create(element::i32, Shape{}, {1})); + auto one_f = register_new_node(one_i, query); + auto zero_f = register_new_node(zero_i, query); + + auto build_extract_dim_subgraph = [this, &zero_i](const std::shared_ptr& shape_of, + const int64_t idx) -> std::shared_ptr { + const auto dim_to_extract_const = v0::Constant::create(element::i32, Shape{}, {idx}); + const auto gather = std::make_shared(shape_of, dim_to_extract_const, zero_i); + + register_new_node(dim_to_extract_const); + return register_new_node(gather); + }; + + Output scale; + Output sink; + bool has_sink = false; + if (node->get_input_size() < 5) { + scale = build_extract_dim_subgraph(q_shape, -1); + scale = register_new_node(scale, query); + auto sqrt_scale = register_new_node(scale); + scale = register_new_node(one_f, sqrt_scale); + } else { + scale = node->input_value(4); + if (node->get_input_size() == 6) { + sink = node->input_value(5); + has_sink = true; + } + } + + auto k_rank = register_new_node(k_shape, element::i32)->output(0); + auto k_last_dim = register_new_node(k_rank, minus_one); + auto k_next_dim = register_new_node(k_rank, minus_two)->output(0); + k_rank = register_new_node(k_rank, zero_i); + auto minus_inf = + register_new_node(v0::Constant::create(element::f32, Shape{}, {-std::numeric_limits::infinity()})) + ->output(0); + auto keep_dim_last = register_new_node(k_next_dim, zero_i); + auto k_dims_before_transpose = register_new_node(zero_i, keep_dim_last, one_i, element::i32); + + auto transpose_dims = + register_new_node(OutputVector{k_dims_before_transpose, k_last_dim, k_next_dim}, 0); + auto k_transposed = register_new_node(key, transpose_dims); + + ov::Output scaled_atten; + if (can_move_scale_after_matmul(query, k_transposed, scale)) { + auto atten = register_new_node(query, k_transposed)->output(0); + scaled_atten = register_new_node(atten, scale)->output(0); + } else { + auto q_scaled = register_new_node(query, scale); + scaled_atten = register_new_node(q_scaled, k_transposed)->output(0); + } + + minus_inf = register_new_node(minus_inf, scaled_atten); + + if (node->get_causal() || node->get_input_size() > 3) { + Output mask; + Output atten_mask; + if (!node->get_causal()) { + mask = node->input_value(3); + + // two types of masks are supported. A boolean mask where a value of True indicates that the element + // should take part in attention. A float mask of the same type as query, key, value that is added to + // the attention score. + if (mask.get_element_type() == element::boolean) { + atten_mask = register_new_node(mask, zero_f, minus_inf); + } else { + atten_mask = mask; + } + } else { + auto target_s_len = build_extract_dim_subgraph(q_shape, -2); + auto source_s_len = build_extract_dim_subgraph(k_shape, -2); + auto ssl = register_new_node(source_s_len, zero_i); + auto tsl = register_new_node(target_s_len, zero_i); + auto mask_shape = register_new_node(OutputVector{tsl, ssl}, 0); + mask = register_new_node(minus_inf, mask_shape); + auto horizontal_range = + register_new_node(zero_i, source_s_len, one_i, element::i32)->output(0); + horizontal_range = register_new_node(horizontal_range, zero_i); + auto stop = register_new_node(target_s_len, one_i); + auto vertical_range = register_new_node(one_i, stop, one_i, element::i32)->output(0); + vertical_range = register_new_node(vertical_range, one_i); + auto triu = register_new_node(horizontal_range, vertical_range); + atten_mask = register_new_node(triu, mask, zero_f); + } + scaled_atten = register_new_node(scaled_atten, atten_mask); + } + + scaled_atten.add_names({"cross_attention_qk_scaled_scores"}); + + if (has_sink) { + auto minus_two = register_new_node(v0::Constant::create(element::i32, Shape{1}, {-2})); + auto minus_one = register_new_node(v0::Constant::create(element::i32, Shape{1}, {-1})); + auto zero_i = register_new_node(v0::Constant::create(element::i32, Shape{1}, {0})); + auto one_i = register_new_node(v0::Constant::create(element::i32, Shape{1}, {1})); + + auto q_last_but_one_dim = register_new_node(register_new_node(q_shape), + v0::Constant::create(element::i64, Shape{}, {1})); + auto sink_target_shape_1 = register_new_node(q_shape, zero_i, q_last_but_one_dim, one_i); + auto sink_target_shape = register_new_node(OutputVector{sink_target_shape_1, one_i}, 0); + auto sink_broadcast = register_new_node(sink, sink_target_shape); + + auto scaled_attn_sink = register_new_node(OutputVector{scaled_atten, sink_broadcast}, -1); + scaled_atten = register_new_node(scaled_attn_sink, -1); + + auto prev_seq_len = register_new_node(k_shape, minus_two, zero_i); + scaled_atten = register_new_node(scaled_atten, zero_i, prev_seq_len, one_i, minus_one); + } else { + scaled_atten = register_new_node(scaled_atten, -1); + } + + auto result = register_new_node(scaled_atten, value); + result->set_friendly_name(node->get_friendly_name()); + copy_runtime_info(node, get_new_nodes()); + return result; + } +}; + auto remove_encoder_attn_read_value(const std::shared_ptr& rv_node, const ov::Output& kv_out, const ov::Input& sdpa_in) { @@ -470,6 +660,48 @@ void add_cache_position_input(const std::shared_ptr& model) { ov::pass::Validate().run_on_model(model); } +// FIXME: Whisper Decompose SDPA +void decompose_scaled_dot_product_attention_for_whisper(std::shared_ptr model) { + ov::pass::Manager manager; + manager.register_pass(); + manager.run_passes(model); +} + +// FIXME: Whisper Decompose SDPA +size_t add_cross_attention_qk_scaled_scores_outputs_for_whisper(std::shared_ptr model) { + size_t idx = 0; + for (auto& op : model->get_ordered_ops()) { + if (op->get_type_info().name != std::string("Add")) { + continue; + } + + bool should_skip_op = true; + + for (const auto& output : op->outputs()) { + for (const auto& name : output.get_names()) { + if (name.find("cross_attention_qk_scaled_scores") != std::string::npos) { + should_skip_op = false; + break; + } + } + + // output found, exit outputs loop + if (!should_skip_op) { + break; + } + } + + if (should_skip_op) { + continue; + } + + model->add_output(op->output(0)).set_names({"cross_attention_qk_scaled_scores_" + std::to_string(idx)}); + idx++; + } + + return idx; +} + #ifdef __GNUC__ # pragma GCC diagnostic pop #endif @@ -490,6 +722,12 @@ bool ov::npuw::util::PrepareWhisperPrefillModel::run_on_model(const std::shared_ add_attention_mask_input(model, m_max_prompt_size, m_lhs_seq_size, true); + // FIXME: Whisper Decompose SDPA + if (m_decompose_sdpa) { + decompose_scaled_dot_product_attention_for_whisper(model); + m_decomposed_layers_size = add_cross_attention_qk_scaled_scores_outputs_for_whisper(model); + } + model->validate_nodes_and_infer_types(); return true; diff --git a/src/plugins/intel_npu/src/plugin/npuw/whisper/prepare_whisper_model.hpp b/src/plugins/intel_npu/src/plugin/npuw/whisper/prepare_whisper_model.hpp index 69265be42d49..9a9bafb765a7 100644 --- a/src/plugins/intel_npu/src/plugin/npuw/whisper/prepare_whisper_model.hpp +++ b/src/plugins/intel_npu/src/plugin/npuw/whisper/prepare_whisper_model.hpp @@ -16,14 +16,23 @@ namespace util { class PrepareWhisperPrefillModel : public ov::pass::ModelPass { uint32_t m_max_prompt_size; uint32_t m_lhs_seq_size; + bool m_decompose_sdpa; + size_t m_decomposed_layers_size; public: OPENVINO_MODEL_PASS_RTTI("ov::npuw::PrepareWhisperPrefillModel"); - explicit PrepareWhisperPrefillModel(uint32_t max_prompt_size, uint32_t lhs_seq_size) + explicit PrepareWhisperPrefillModel(uint32_t max_prompt_size, uint32_t lhs_seq_size, bool decompose_sdpa) : m_max_prompt_size(max_prompt_size), - m_lhs_seq_size(lhs_seq_size) {} + m_lhs_seq_size(lhs_seq_size), + m_decompose_sdpa(decompose_sdpa), + m_decomposed_layers_size(0) {} bool run_on_model(const std::shared_ptr& model) override; + + // FIXME: Whisper Decompose SDPA + size_t get_decomposed_sdpa_size() const { + return m_decomposed_layers_size; + } }; class PrepareWhisperKVCacheModel : public ov::pass::ModelPass { diff --git a/src/plugins/intel_npu/src/plugin/npuw/whisper/whisper_infer_request.cpp b/src/plugins/intel_npu/src/plugin/npuw/whisper/whisper_infer_request.cpp index cc11d571560c..0044d1cd4883 100644 --- a/src/plugins/intel_npu/src/plugin/npuw/whisper/whisper_infer_request.cpp +++ b/src/plugins/intel_npu/src/plugin/npuw/whisper/whisper_infer_request.cpp @@ -22,6 +22,8 @@ void ov::npuw::WhisperInferRequest::prepare_for_new_conversation() { ov::npuw::util::fill_tensor(m_prefill_request->get_tensor(m_prefill_in_ports.at("attention_mask")), 0); ov::npuw::util::fill_tensor(m_kvcache_request->get_tensor(m_kvcache_in_ports.at("attention_mask")), 0); m_npuw_llm_compiled_model->m_kvcache_desc.num_stored_tokens = 0u; + + m_alignment_tensors.clear(); } void ov::npuw::WhisperInferRequest::infer_prefill(ov::SoPtr input_ids, @@ -56,6 +58,13 @@ void ov::npuw::WhisperInferRequest::infer_prefill(ov::SoPtr input_i 0u, m_npuw_llm_compiled_model->m_kvcache_desc.num_stored_tokens); + // for word-level timestamps + auto decomposed_sdpa_size = m_npuw_llm_compiled_model->m_decomposed_sdpa_size; + for (size_t idx = 0; idx < decomposed_sdpa_size; idx++) { + auto name = whisper_layer_names::qk_scores_ + std::to_string(idx); + m_alignment_tensors.insert({name, m_prefill_request->get_tensor(m_prefill_out_ports.at(name))}); + } + LOG_DEBUG("Done"); } @@ -210,3 +219,23 @@ void ov::npuw::WhisperInferRequest::infer() { infer_generate(input_ids); } } + +// FIXME: Whisper Decompose SDPA +ov::SoPtr ov::npuw::WhisperInferRequest::get_tensor(const ov::Output& port) const { + const auto& port_names = port.get_names(); + + if (port_names.count(whisper_layer_names::qk_scores) > 0) { + for (auto name : port_names) { + if (name.find(whisper_layer_names::qk_scores_) != std::string::npos) { + auto alignment_tensor = m_alignment_tensors.at(name); + if (!alignment_tensor) { + OPENVINO_THROW( + "Cross-attention qk scaled scores tensor is not available. Please run inference first."); + } + return alignment_tensor; + } + } + } + + return ov::npuw::LLMInferRequest::get_tensor(port); +} diff --git a/src/plugins/intel_npu/src/plugin/npuw/whisper/whisper_infer_request.hpp b/src/plugins/intel_npu/src/plugin/npuw/whisper/whisper_infer_request.hpp index 766a1af6caa0..9479aaa2802b 100644 --- a/src/plugins/intel_npu/src/plugin/npuw/whisper/whisper_infer_request.hpp +++ b/src/plugins/intel_npu/src/plugin/npuw/whisper/whisper_infer_request.hpp @@ -11,11 +11,18 @@ namespace npuw { class WhisperInferRequest final : public LLMInferRequest { public: + struct whisper_layer_names { + static constexpr const char* qk_scores = "cross_attention_qk_scaled_scores"; + static constexpr const char* qk_scores_ = "cross_attention_qk_scaled_scores_"; + }; + explicit WhisperInferRequest(const std::shared_ptr& compiled_model) : LLMInferRequest(compiled_model) {} void infer() override; + ov::SoPtr get_tensor(const ov::Output& port) const override; + protected: void prepare_for_new_conversation() override; @@ -24,6 +31,7 @@ class WhisperInferRequest final : public LLMInferRequest { void infer_generate(ov::SoPtr input_ids); bool m_need_copy_kvcache = false; + std::map> m_alignment_tensors{}; }; } // namespace npuw diff --git a/src/plugins/intel_npu/src/plugin/src/async_infer_request.cpp b/src/plugins/intel_npu/src/plugin/src/async_infer_request.cpp index 622e5a51f1a9..d7a5a8421cd5 100644 --- a/src/plugins/intel_npu/src/plugin/src/async_infer_request.cpp +++ b/src/plugins/intel_npu/src/plugin/src/async_infer_request.cpp @@ -8,19 +8,27 @@ namespace intel_npu { -// clang-format off -AsyncInferRequest::AsyncInferRequest(const std::shared_ptr& syncInferRequest, +AsyncInferRequest::AsyncInferRequest(const std::shared_ptr& inferRequest, const std::shared_ptr& requestExecutor, - const std::shared_ptr& getResultExecutor, + const std::shared_ptr& resultExecutor, const std::shared_ptr& callbackExecutor) - : ov::IAsyncInferRequest(syncInferRequest, requestExecutor, callbackExecutor), - _syncInferRequest(syncInferRequest), _getResultExecutor(getResultExecutor) { - m_pipeline = { - {requestExecutor, [this] { _syncInferRequest->infer_async(); }}, - {getResultExecutor, [this] { _syncInferRequest->get_result(); }} - }; + : ov::IAsyncInferRequest(inferRequest, requestExecutor, callbackExecutor), + _inferRequest(inferRequest), + _resultExecutor(resultExecutor) { + if (_resultExecutor) { + m_pipeline = {{requestExecutor, + [this] { + _inferRequest->infer_async(); + }}, + {_resultExecutor, [this] { + _inferRequest->get_result(); + }}}; + } +} + +void AsyncInferRequest::cancel() { + OPENVINO_THROW("Inference cancellation is not supported by the Intel NPU plugin"); } -// clang-format on AsyncInferRequest::~AsyncInferRequest() { stop_and_wait(); diff --git a/src/plugins/intel_npu/src/plugin/src/compiled_model.cpp b/src/plugins/intel_npu/src/plugin/src/compiled_model.cpp index 3eb3074c52a8..02066a470c6a 100644 --- a/src/plugins/intel_npu/src/plugin/src/compiled_model.cpp +++ b/src/plugins/intel_npu/src/plugin/src/compiled_model.cpp @@ -4,53 +4,58 @@ #include "compiled_model.hpp" +#include #include #include #include "async_infer_request.hpp" +#include "executor.hpp" +#include "intel_npu/common/device_helpers.hpp" #include "intel_npu/common/itt.hpp" #include "intel_npu/config/config.hpp" #include "intel_npu/config/options.hpp" +#include "intel_npu/utils/utils.hpp" #include "metadata.hpp" #include "openvino/pass/constant_folding.hpp" #include "openvino/runtime/properties.hpp" -#include "openvino/runtime/system_conf.hpp" -#include "openvino/runtime/threading/executor_manager.hpp" #include "transformations/utils/utils.hpp" namespace intel_npu { -using intel_npu::envVarStrToBool; - CompiledModel::CompiledModel(const std::shared_ptr& model, const std::shared_ptr& plugin, const std::shared_ptr& device, const std::shared_ptr& graph, const FilteredConfig& config, const std::optional& batchSize) - : ICompiledModel(model, plugin), + : ICompiledModel(model, plugin, nullptr, nullptr), _logger("CompiledModel", config.get()), _device(device), _graph(graph), _batchSize(batchSize) { OV_ITT_SCOPED_TASK(itt::domains::NPUPlugin, "CompiledModel::CompiledModel"); - OV_ITT_TASK_CHAIN(COMPILED_MODEL, itt::domains::NPUPlugin, "CompiledModel::CompiledModel", "initialize_properties"); - _propertiesManager = std::make_unique(PropertiesType::COMPILED_MODEL, config); + // Support for specific properties might depend on the characteristics of the compiled model. + // Adjust lower level config availability to influence the supported properties list if needed + FilteredConfig localConfig = config; + if (!_graph->get_compatibility_descriptor().has_value()) { + _logger.debug("Graph's compatibility descriptor has no value. Disabling RUNTIME_REQUIREMENTS property."); + localConfig.enable(ov::runtime_requirements.name(), false); + } - configure_stream_executors(); + OV_ITT_TASK_CHAIN(COMPILED_MODEL, itt::domains::NPUPlugin, "CompiledModel::CompiledModel", "initialize_properties"); + _propertiesManager = std::make_unique(PropertiesType::COMPILED_MODEL, localConfig); OV_ITT_TASK_SKIP(COMPILED_MODEL); } -CompiledModel::~CompiledModel() { - _logger.debug("~CompiledModel()"); - std::dynamic_pointer_cast(get_task_executor())->cpu_reset(); -} - std::shared_ptr CompiledModel::create_infer_request() const { OV_ITT_SCOPED_TASK(itt::domains::NPUPlugin, "CompiledModel::create_infer_request"); + std::call_once(_streamExecutorsInitFlag, [this] { + const_cast(this)->configure_stream_executors(); + }); + // sanity check OPENVINO_ASSERT(_device != nullptr, "No available devices. Failed to create infer request!"); @@ -64,10 +69,10 @@ std::shared_ptr CompiledModel::create_infer_request() co "Graph is unavailable or failed to initialize. The driver may be missing or too old to run " "inference for this blob."); - const std::shared_ptr& syncInferRequest = + const std::shared_ptr& inferRequest = _device->createInferRequest(shared_from_this(), _propertiesManager->getConfig()); - return std::make_shared(syncInferRequest, + return std::make_shared(inferRequest, get_task_executor(), _resultExecutor, get_callback_executor()); @@ -82,7 +87,37 @@ std::shared_ptr CompiledModel::create_sync_infer_request( void CompiledModel::export_model(std::ostream& stream) const { _logger.debug("CompiledModel::export_model"); - auto [blobSizesBeforeVersioning, initBlobSizes] = _graph->export_blob(stream); + uint64_t blobSizesBeforeVersioning; + std::optional blobSizeAfterEncryption = std::nullopt; + std::optional> initBlobSizes; + + if (_propertiesManager->getConfig().has(CACHE_ENCRYPTION_CALLBACKS::key().data()) && + _propertiesManager->getConfig().get().encrypt != nullptr) { + std::string encryptedBlobStr; + { + std::string tmpBlobStr; + { + std::stringstream tmpStringStream; + std::tie(blobSizesBeforeVersioning, initBlobSizes) = + _graph->export_blob(tmpStringStream); // +1x blob size + tmpBlobStr = tmpStringStream.str(); // +2x blob size + } // -1x blob size when deallocating temporary stringstream + encryptedBlobStr = + _propertiesManager->getConfig().get().encrypt(tmpBlobStr); // +2x blob size + blobSizeAfterEncryption = encryptedBlobStr.size(); + if (blobSizeAfterEncryption.value() % utils::STANDARD_PAGE_SIZE != 0) { + _logger.warning("Encrypted blob size %" PRIu64 + " is not page aligned, memory optimization when reading this blob " + "won't be applied", + blobSizeAfterEncryption.value()); + } + } // -1x blob size when deallocating temporary blob string + stream.write(encryptedBlobStr.c_str(), encryptedBlobStr.size()); + } // -1x blob size when deallocating encrypted blob string + else { + // Write blob directly to user's output stream + std::tie(blobSizesBeforeVersioning, initBlobSizes) = _graph->export_blob(stream); + } if (!_propertiesManager->getConfig().get()) { std::optional> inputLayouts = std::vector(); @@ -97,12 +132,20 @@ void CompiledModel::export_model(std::ostream& stream) const { std::dynamic_pointer_cast(nodeOutput.get_node_shared_ptr())->get_layout()); } + std::optional compilerVersion = std::nullopt; + if (_propertiesManager->getConfig().has(ov::intel_npu::compiler_version.name())) { + compilerVersion = _propertiesManager->getConfig().get(); + } + Metadata(blobSizesBeforeVersioning, CURRENT_OPENVINO_VERSION, - std::move(initBlobSizes), + initBlobSizes, _batchSize, - std::move(inputLayouts), - std::move(outputLayouts)) + inputLayouts, + outputLayouts, + compilerVersion, + blobSizeAfterEncryption, + _graph->get_compatibility_descriptor()) .write(stream); } } @@ -177,10 +220,40 @@ ov::Any CompiledModel::get_property(const std::string& name) const { if (name == ov::model_name.name()) { OPENVINO_ASSERT(_graph != nullptr, "Missing graph"); return _graph->get_metadata().name; - } else { - // default behaviour - return _propertiesManager->getProperty(name); + } else if (name == ov::runtime_requirements.name()) { + // Reading the (dummy) property content to check if it is supported + _propertiesManager->getProperty(name); + + auto compatibilityDescriptor = _graph->get_compatibility_descriptor(); + if (compatibilityDescriptor.has_value()) { + const auto descriptorView = compatibilityDescriptor.value(); + _logger.debug("Runtime requirements from the graph %.*s length: %zu", + static_cast(descriptorView.size()), + descriptorView.data(), + descriptorView.size()); + } + + std::ostringstream requirementsString; + Metadata( + 0, // no real blob + CURRENT_OPENVINO_VERSION, + std::nullopt, // weightless blobs are not supported + _batchSize, + std::nullopt, // input_layouts are not relevant for the compatibility check + std::nullopt, // output_layouts are not relevant for the compatibility check + std::nullopt, // skip compiler version as well since it is already included in runtime requirements string + std::nullopt, // skip encrypted blob size since it is not relevant for the compatibility check + compatibilityDescriptor) + .write_as_text(requirementsString); + _logger.debug("Runtime requirements string: %s length: %zu", + requirementsString.str().c_str(), + requirementsString.str().length()); + + return requirementsString.str(); } + + // default behaviour + return _propertiesManager->getProperty(name); } const std::shared_ptr& CompiledModel::get_graph() const { @@ -198,25 +271,43 @@ void CompiledModel::release_memory() { } void CompiledModel::configure_stream_executors() { - std::shared_ptr task_executor; - if (get_plugin()->get_property(ov::internal::exclusive_async_requests.name(), {}).as()) { - task_executor = ov::threading::executor_manager()->get_executor("NPU"); - } else if (get_property(ov::hint::enable_cpu_pinning.name()).as()) { - auto executor_config = ov::threading::IStreamsExecutor::Config{ - /* name = */ "Intel NPU plugin executor", - /* streams = */ get_plugin()->get_property(ov::num_streams.name(), {}).as(), - /* threads_per_stream = */ 1, - /* thread_preferred_core_type = */ ov::hint::SchedulingCoreType::PCORE_ONLY, - /* cpu_reservation = */ true}; - task_executor = std::make_shared(executor_config); - } else { - task_executor = std::make_shared( - ov::threading::IStreamsExecutor::Config{"NPUPlugin executor"}); + const FilteredConfig& config = get_config(); + + // In case of sequential execution of async requests for the same compiled model, the compiled model must use + // dedicated executors with a single thread to ensure sequential execution of its async requests. + if (config.get()) { + set_task_executor(make_executor("Intel NPU plugin start inferences executor", 1)); + _resultExecutor = make_executor("Intel NPU plugin wait inferences executor", 1); + + return; } - set_task_executor(std::move(task_executor)); - const auto executorId = _graph->get_metadata().name + "_NPUResultExecutor"; - _resultExecutor = ov::threading::executor_manager()->get_executor(executorId); + const auto numStreams = config.get(); + if (numStreams > 0) { + // Use a single thread for start executors to reduce contention on the shared task queue, while scaling wait + // executor workers with num_streams to improve result fetch throughput. Callbacks intentionally run on wait + // threads. + const size_t workers = static_cast(numStreams); + + set_task_executor(make_executor("Intel NPU plugin start inferences executor", 1)); + _resultExecutor = make_executor("Intel NPU plugin wait inferences executor", workers); + } else if (numStreams == 0) { + // For special case when num_streams is explicitly set to 0, start inference will happen in the same thread as + // the call to InferRequest::start_async, while wait executor will still be created with a single worker. + // Callback execution is intentionally done on that wait thread. + set_task_executor(make_executor("Intel NPU plugin start inferences executor", 0)); + _resultExecutor = make_executor("Intel NPU plugin wait inferences executor", 1); + } else { + // Auto mode (default): workers are created on demand. The baseline number of workers that stay alive during + // idle periods (30 s timeout) is derived from the optimal number of parallel infer requests recommended for + // the current platform in THROUGHPUT mode. The pool can then grow dynamically to match runtime workload. + const size_t keepWorkers = static_cast( + utils::getOptimalNumberOfInferRequestsInParallel(config.get(), + ov::hint::PerformanceMode::THROUGHPUT)); + + set_task_executor(make_executor("Intel NPU plugin run inferences executor", keepWorkers, true)); + _resultExecutor = nullptr; + } } } // namespace intel_npu diff --git a/src/plugins/intel_npu/src/plugin/src/executor.cpp b/src/plugins/intel_npu/src/plugin/src/executor.cpp new file mode 100644 index 000000000000..626e90cfc3d0 --- /dev/null +++ b/src/plugins/intel_npu/src/plugin/src/executor.cpp @@ -0,0 +1,234 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "executor.hpp" + +#include +#include +#include +#include +#include +#include + +#include "intel_npu/common/itt.hpp" + +namespace intel_npu { + +namespace { + +/** + * @brief Task executor with adaptive worker growth for NPU plugin jobs. + * + * The executor processes tasks submitted through `run()` using an internal queue + * and a set of worker threads. + * + * Behavior depends on constructor parameters: + * - Fixed-size mode (`allowWorkerGrowth == false`): starts exactly `workers` + * threads during construction. + * - Adaptive mode (`allowWorkerGrowth == true`): starts with one worker and + * grows the pool when queued load exceeds active workers. + * + * In adaptive mode, workers above the baseline (`workers`) can terminate after + * `idleTimeout` if no work arrives, reducing idle thread usage. + * + * The destructor performs a graceful shutdown by signaling stop, waking workers, + * and joining all owned threads. + */ +class AdaptiveThreadExecutor final : public ov::threading::ITaskExecutor { +public: + AdaptiveThreadExecutor(std::string_view name, + size_t workers, + bool allowWorkerGrowth, + std::chrono::milliseconds idleTimeout) + : _name(name), + _workersBaseline(workers), + _allowWorkerGrowth(allowWorkerGrowth), + _idleTimeout(idleTimeout) { + std::lock_guard lock(_mutex); + + if (_allowWorkerGrowth) { + start_worker_locked(); + return; + } + + while (_activeWorkers < _workersBaseline) { + start_worker_locked(); + } + } + + ~AdaptiveThreadExecutor() override { + { + std::lock_guard lock(_mutex); + _stopped = true; + } + _condition.notify_all(); + for (auto& worker : _workers) { + if (worker.thread.joinable()) { + worker.thread.join(); + } + } + } + + void run(ov::threading::Task task) override { + reap_finished_workers(); + + if (_workersBaseline == 0 && !_allowWorkerGrowth) { + try { + task(); + } catch (...) { + } + return; + } + + { + std::lock_guard lock(_mutex); + _tasks.push(TaskEntry{std::move(task)}); + + if (_allowWorkerGrowth) { + while ((_tasks.size() + _busyWorkers) > _activeWorkers) { + start_worker_locked(); + } + } + } + _condition.notify_one(); + } + +private: + struct TaskEntry { + ov::threading::Task task; + }; + + struct WorkerEntry { + std::thread thread; + bool finished = false; + }; + + void reap_finished_workers() { + std::vector threadsToJoin; + std::vector joinedIndices; + + { + std::lock_guard lock(_mutex); + for (const auto index : _retiredWorkerIndices) { + if (_workers[index].finished && _workers[index].thread.joinable()) { + threadsToJoin.emplace_back(std::move(_workers[index].thread)); + joinedIndices.emplace_back(index); + } + } + _retiredWorkerIndices.clear(); + } + + for (auto& thread : threadsToJoin) { + thread.join(); + } + + if (!joinedIndices.empty()) { + std::lock_guard lock(_mutex); + for (const auto index : joinedIndices) { + _workers[index].finished = false; + _freeWorkerIndices.emplace_back(index); + } + } + } + + void start_worker_locked() { + const size_t workerId = _nextWorkerId++; + size_t workerIndex = 0; + const bool reuseSlot = !_freeWorkerIndices.empty(); + if (!_freeWorkerIndices.empty()) { + workerIndex = _freeWorkerIndices.back(); + _freeWorkerIndices.pop_back(); + } else { + workerIndex = _workers.size(); + _workers.emplace_back(); + } + + try { + std::thread workerThread([this, workerId, workerIndex] { + openvino::itt::threadName(_name + "_" + std::to_string(workerId)); + for (;;) { + ov::threading::Task task; + { + std::unique_lock lock(_mutex); + while (!_stopped && _tasks.empty()) { + if (_activeWorkers > _workersBaseline) { + if (!_condition.wait_for(lock, _idleTimeout, [&] { + return _stopped || !_tasks.empty(); + })) { + if (_activeWorkers > _workersBaseline) { + --_activeWorkers; + _workers[workerIndex].finished = true; + _retiredWorkerIndices.emplace_back(workerIndex); + return; + } + + continue; + } + } else { + _condition.wait(lock, [&] { + return _stopped || !_tasks.empty(); + }); + } + } + + if (_stopped && _tasks.empty()) { + --_activeWorkers; + _workers[workerIndex].finished = true; + _retiredWorkerIndices.emplace_back(workerIndex); + return; + } + + task = std::move(_tasks.front().task); + _tasks.pop(); + ++_busyWorkers; + } + + task(); + + { + std::lock_guard lock(_mutex); + --_busyWorkers; + } + } + }); + + _workers[workerIndex].thread = std::move(workerThread); + _workers[workerIndex].finished = false; + ++_activeWorkers; + } catch (...) { + if (reuseSlot) { + _freeWorkerIndices.emplace_back(workerIndex); + } else { + _workers.pop_back(); + } + throw; + } + } + + const std::string _name; + const size_t _workersBaseline; + const bool _allowWorkerGrowth; + const std::chrono::milliseconds _idleTimeout; + std::mutex _mutex; + std::condition_variable _condition; + std::queue _tasks; + std::vector _workers; + std::vector _retiredWorkerIndices; + std::vector _freeWorkerIndices; + size_t _nextWorkerId = 0; + size_t _activeWorkers = 0; + size_t _busyWorkers = 0; + bool _stopped = false; +}; + +} // namespace + +std::shared_ptr make_executor(std::string_view name, + size_t workers, + bool allowWorkerGrowth, + std::chrono::milliseconds idleTimeout) { + return std::make_shared(name, workers, allowWorkerGrowth, idleTimeout); +} + +} // namespace intel_npu diff --git a/src/plugins/intel_npu/src/plugin/src/metadata.cpp b/src/plugins/intel_npu/src/plugin/src/metadata.cpp index 70a446817b70..d7472df7a139 100644 --- a/src/plugins/intel_npu/src/plugin/src/metadata.cpp +++ b/src/plugins/intel_npu/src/plugin/src/metadata.cpp @@ -4,13 +4,55 @@ #include "metadata.hpp" +#include +#include #include -#include #include +#include -#include "intel_npu/utils/utils.hpp" +#include "intel_npu/compat_string_parser.hpp" #include "openvino/runtime/shared_buffer.hpp" -#include "openvino/util/variant_visitor.hpp" + +namespace { + +template +void write_text_field(std::ostream& stream, std::string_view key, const T& value) { + if (stream.tellp() != std::streampos(0)) { + stream << ';'; + } + stream << key << '=' << value; +} + +std::vector parse_version(std::string_view sv) { + const auto hasOnlyDigits = [](std::string_view sv) { + return !sv.empty() && std::all_of(sv.begin(), sv.end(), [](unsigned char c) { + return std::isdigit(c); + }); + }; + + std::vector parts; + std::string_view remaining = sv; + while (true) { + const size_t dot = remaining.find('.'); + const std::string_view part = remaining.substr(0, dot); + if (!hasOnlyDigits(part)) { + OPENVINO_THROW("Invalid version '", + sv, + "': version must meet the format MAJOR.MINOR.PATCH with numeric components"); + } + parts.push_back(static_cast(std::stoul(std::string(part)))); + if (dot == std::string_view::npos) { + break; + } + remaining = remaining.substr(dot + 1); + if (remaining.empty()) { + OPENVINO_THROW("Invalid version '", sv, "': trailing dot"); + } + } + return parts; +} + +} // namespace namespace intel_npu { @@ -77,8 +119,8 @@ Metadata::Metadata(uint64_t blobSize, Metadata::Metadata(uint64_t blobSize, std::optional ovVersion, - const std::optional> initSizes, - const std::optional batchSize) + const std::optional>& initSizes, + const std::optional& batchSize) : Metadata{blobSize, ovVersion, initSizes}, _batchSize{batchSize} { _version = METADATA_VERSION_2_2; @@ -87,7 +129,7 @@ Metadata::Metadata(uint64_t blobSize, Metadata::Metadata(uint64_t blobSize, const std::optional& ovVersion, const std::optional>& initSizes, - const std::optional batchSize, + const std::optional& batchSize, const std::optional>& inputLayouts, const std::optional>& outputLayouts) : Metadata{blobSize, ovVersion, initSizes, batchSize}, @@ -96,6 +138,58 @@ Metadata::Metadata(uint64_t blobSize, _version = METADATA_VERSION_2_3; } +Metadata::Metadata(uint64_t blobSize, + const std::optional& ovVersion, + const std::optional>& initSizes, + const std::optional& batchSize, + const std::optional>& inputLayouts, + const std::optional>& outputLayouts, + const std::optional& compilerVersion) + : Metadata{blobSize, ovVersion, initSizes, batchSize, inputLayouts, outputLayouts}, + _compilerVersion{compilerVersion} { + _version = METADATA_VERSION_2_4; +} + +Metadata::Metadata(uint64_t blobSize, + const std::optional& ovVersion, + const std::optional>& initSizes, + const std::optional& batchSize, + const std::optional>& inputLayouts, + const std::optional>& outputLayouts, + const std::optional& compilerVersion, + const std::optional& blobSizeAfterEncryption) + : Metadata{blobSizeAfterEncryption.has_value() ? blobSizeAfterEncryption.value() : blobSize, + ovVersion, + initSizes, + batchSize, + inputLayouts, + outputLayouts, + compilerVersion}, + _isEncryptedBlob{blobSizeAfterEncryption.has_value()} { + _version = METADATA_VERSION_2_5; +} + +Metadata::Metadata(uint64_t blobSize, + const std::optional& ovVersion, + const std::optional>& initSizes, + const std::optional batchSize, + const std::optional>& inputLayouts, + const std::optional>& outputLayouts, + const std::optional compilerVersion, + const std::optional& blobSizeAfterEncryption, + const std::optional compatibilityDescriptor) + : Metadata{blobSize, + ovVersion, + initSizes, + batchSize, + inputLayouts, + outputLayouts, + compilerVersion, + blobSizeAfterEncryption}, + _compatibilityDescriptor{compatibilityDescriptor} { + _version = METADATA_VERSION_2_6; +} + void MetadataBase::read(std::istream& tensor) { _source = Source(tensor); read(); @@ -106,12 +200,28 @@ void MetadataBase::read(const ov::Tensor& tensor) { read(); } +void MetadataBase::read_as_text(std::map> attrs) { + _textAttrs = std::move(attrs); + read_as_text(); +} + void MetadataBase::read_data_from_source(char* destination, const size_t size) { if (const std::reference_wrapper* stream = std::get_if>(&_source)) { stream->get().read(destination, size); } else if (const std::reference_wrapper* tensor = std::get_if>(&_source)) { + const size_t available = tensor->get().get_byte_size(); + const size_t remaining = (_cursorOffset <= available) ? available - _cursorOffset : 0; + if (size > remaining) { + OPENVINO_THROW("NPU metadata: attempted to read ", + size, + " bytes at offset ", + _cursorOffset, + " but only ", + remaining, + " bytes remain in the metadata buffer."); + } std::memcpy(destination, tensor->get().data() + _cursorOffset, size); _cursorOffset += size; } else { @@ -119,14 +229,7 @@ void MetadataBase::read_data_from_source(char* destination, const size_t size) { } } -void MetadataBase::append_padding_blob_size_and_magic(std::ostream& stream) { - size_t metadataSize = get_metadata_size() + sizeof(_blobDataSize) + MAGIC_BYTES.size(); - size_t size = utils::align_size_to_standard_page_size(metadataSize); - size_t paddingSize = size - metadataSize; - if (paddingSize > 0) { - std::fill_n(std::ostream_iterator(stream), paddingSize, 0); - } - +void MetadataBase::append_blob_size_and_magic(std::ostream& stream) { stream.write(reinterpret_cast(&_blobDataSize), sizeof(_blobDataSize)); stream.write(MAGIC_BYTES.data(), MAGIC_BYTES.size()); } @@ -208,6 +311,89 @@ void Metadata::read() { _outputLayouts = readNLayouts(numberOfOutputLayouts, "Output"); } +void Metadata::read() { + Metadata::read(); + + uint32_t compilerVersion; + read_data_from_source(reinterpret_cast(&compilerVersion), sizeof(compilerVersion)); + _compilerVersion = compilerVersion != 0 ? std::optional(compilerVersion) : std::nullopt; +} + +void Metadata::read() { + Metadata::read(); + + uint8_t isEncryptedBlob; + read_data_from_source(reinterpret_cast(&isEncryptedBlob), sizeof(isEncryptedBlob)); + + _isEncryptedBlob = isEncryptedBlob; +} + +void Metadata::read() { + Metadata::read(); + + uint64_t reqs_len; + read_data_from_source(reinterpret_cast(&reqs_len), sizeof(reqs_len)); + if (reqs_len > 0) { + std::string reqs(reqs_len, '\0'); + read_data_from_source(reqs.data(), reqs_len); + _compatibilityDescriptor = std::move(reqs); + } +} + +void Metadata::read_as_text() { + const auto it = _textAttrs.find(MetadataTextKeys::OV); + if (it == _textAttrs.end()) { + OPENVINO_THROW("Human-readable metadata missing '" + std::string(MetadataTextKeys::OV) + "' field."); + } + const auto ovParts = parse_version(it->second); + if (ovParts.size() != 3) { + OPENVINO_THROW("Human-readable metadata: '" + std::string(MetadataTextKeys::OV) + + "' is not in MAJOR.MINOR.PATCH format: " + it->second); + } + _ovVersion = OpenvinoVersion(ovParts[0], ovParts[1], ovParts[2]); +} + +void Metadata::read_as_text() { + Metadata::read_as_text(); + + const auto it = _textAttrs.find(MetadataTextKeys::WS_INITS); + if (it == _textAttrs.end()) { + return; + } + if (it->second != "1") { + OPENVINO_THROW("Human-readable metadata: '" + std::string(MetadataTextKeys::WS_INITS) + + "' must be '1' when present; got: " + it->second); + } + _initSizes = std::vector{}; +} + +void Metadata::read_as_text() { + Metadata::read_as_text(); + + const auto it = _textAttrs.find(MetadataTextKeys::BATCH); + if (it == _textAttrs.end()) { + return; + } + const int64_t batchValue = std::stoll(it->second); + _batchSize = batchValue != 0 ? std::optional(batchValue) : std::nullopt; +} + +void Metadata::read_as_text() { + Metadata::read_as_text(); + + const auto it = _textAttrs.find(MetadataTextKeys::COMPAT_DESC); + if (it == _textAttrs.end() || it->second.empty()) { + return; + } + + const std::string& v = it->second; + if (v.size() >= 2 && v.front() == '[' && v.back() == ']') { + _compatibilityDescriptor = v.substr(1, v.size() - 2); + } else { + OPENVINO_THROW("Human-readable metadata: 'desc' value is not bracket-enclosed: ", v); + } +} + void Metadata::write(std::ostream& stream) { stream.write(reinterpret_cast(&_version), sizeof(_version)); _ovVersion.write(stream); @@ -254,8 +440,71 @@ void Metadata::write(std::ostream& stream) { writeLayouts(_inputLayouts); writeLayouts(_outputLayouts); +} + +void Metadata::write(std::ostream& stream) { + Metadata::write(stream); + + uint32_t compilerVersion = _compilerVersion.value_or(0); + stream.write(reinterpret_cast(&compilerVersion), sizeof(compilerVersion)); +} + +void Metadata::write(std::ostream& stream) { + Metadata::write(stream); + + const uint8_t isEncryptedBlob = _isEncryptedBlob.value_or(false); + stream.write(reinterpret_cast(&isEncryptedBlob), sizeof(isEncryptedBlob)); +} + +void Metadata::write(std::ostream& stream) { + Metadata::write(stream); + + const std::string& compatDesc = _compatibilityDescriptor.value_or(""); + const uint64_t compatDesc_len = compatDesc.size(); + stream.write(reinterpret_cast(&compatDesc_len), sizeof(compatDesc_len)); + if (compatDesc_len > 0) { + stream.write(compatDesc.data(), static_cast(compatDesc_len)); + } + + append_blob_size_and_magic(stream); +} + +void Metadata::write_as_text(std::ostream& stream) { + const uint16_t meta_major = MetadataBase::get_major(_version); + const uint16_t meta_minor = MetadataBase::get_minor(_version); + write_text_field(stream, MetadataTextKeys::META, std::to_string(meta_major) + "." + std::to_string(meta_minor)); + write_text_field(stream, + MetadataTextKeys::OV, + std::to_string(OPENVINO_VERSION_MAJOR) + "." + std::to_string(OPENVINO_VERSION_MINOR) + "." + + std::to_string(OPENVINO_VERSION_PATCH)); +} + +void Metadata::write_as_text(std::ostream& stream) { + Metadata::write_as_text(stream); - append_padding_blob_size_and_magic(stream); + if (_initSizes.has_value() && !_initSizes->empty()) { + write_text_field(stream, MetadataTextKeys::WS_INITS, "1"); + } +} + +void Metadata::write_as_text(std::ostream& stream) { + Metadata::write_as_text(stream); + + if (_batchSize.has_value() && _batchSize.value() != 0) { + write_text_field(stream, MetadataTextKeys::BATCH, _batchSize.value()); + } +} + +void Metadata::write_as_text(std::ostream& stream) { + Metadata::write_as_text(stream); + + if (_compatibilityDescriptor.has_value() && !_compatibilityDescriptor->empty()) { + std::string desc = _compatibilityDescriptor.value(); + if (!desc.empty() && desc.back() == '\0') { + desc.pop_back(); + } + write_text_field(stream, MetadataTextKeys::COMPAT_DESC, '[' + desc + ']'); + } } std::unique_ptr create_metadata(uint32_t version, uint64_t blobSize) { @@ -280,6 +529,12 @@ std::unique_ptr create_metadata(uint32_t version, uint64_t blobSiz return std::make_unique>(blobSize); case METADATA_VERSION_2_3: return std::make_unique>(blobSize); + case METADATA_VERSION_2_4: + return std::make_unique>(blobSize); + case METADATA_VERSION_2_5: + return std::make_unique>(blobSize); + case METADATA_VERSION_2_6: + return std::make_unique>(blobSize); default: return nullptr; } @@ -379,6 +634,36 @@ std::unique_ptr read_metadata_from(const ov::Tensor& tensor) { return storedMeta; } +std::unique_ptr read_as_text(std::string_view input) { + std::string versionStr; + compat::Parser::attr_map_type attrs; + try { + compat::Parser parser(input, metadataTextAttributes); + versionStr = parser.getAttribute(std::string(MetadataTextKeys::META)); + attrs = parser.getAttributes(); + } catch (const std::exception& ex) { + OPENVINO_THROW("NPU compatibility string is malformed: ", ex.what()); + } + + const auto metaParts = parse_version(versionStr); + if (metaParts.size() != 2) { + OPENVINO_THROW("NPU compatibility string is malformed: 'meta' must be in MAJOR.MINOR format: ", versionStr); + } + const uint32_t metaVersion = MetadataBase::make_version(metaParts[0], metaParts[1]); + + std::unique_ptr storedMeta; + try { + storedMeta = create_metadata(metaVersion, 0); + storedMeta->read_as_text(std::move(attrs)); + } catch (const std::exception& ex) { + OPENVINO_THROW("Can't read NPU human-readable metadata: ", ex.what()); + } catch (...) { + OPENVINO_THROW("Unexpected exception while reading NPU human-readable metadata"); + } + + return storedMeta; +} + uint64_t MetadataBase::get_blob_size() const { return _blobDataSize; } @@ -399,6 +684,18 @@ std::optional> MetadataBase::get_output_layouts() const return std::nullopt; } +std::optional MetadataBase::get_compiler_version() const { + return std::nullopt; +} + +std::optional MetadataBase::is_encrypted_blob() const { + return std::nullopt; +} + +std::optional MetadataBase::get_compatibility_descriptor() const { + return std::nullopt; +} + std::optional> Metadata::get_init_sizes() const { return _initSizes; } @@ -415,44 +712,16 @@ std::optional> Metadata::get_outpu return _outputLayouts; } -size_t Metadata::get_metadata_size() const { - return sizeof(_version) + _ovVersion.get_openvino_version_size(); +std::optional Metadata::get_compiler_version() const { + return _compilerVersion; } -size_t Metadata::get_metadata_size() const { - size_t metadataSize = Metadata::get_metadata_size() + sizeof(_numberOfInits); - - if (_initSizes.has_value()) { - metadataSize += _initSizes->size() * sizeof(uint64_t); - } - - return metadataSize; +std::optional Metadata::is_encrypted_blob() const { + return _isEncryptedBlob; } -size_t Metadata::get_metadata_size() const { - size_t metadataSize = Metadata::get_metadata_size() + sizeof(int64_t); - - return metadataSize; -} - -size_t Metadata::get_metadata_size() const { - size_t metadataSize = Metadata::get_metadata_size(); - // Number of input layouts & number of output layouts - metadataSize += 2 * sizeof(uint64_t); - - if (_inputLayouts.has_value()) { - for (const ov::Layout& layout : _inputLayouts.value()) { - // Length followed by the layout value as string - metadataSize += sizeof(uint16_t) + layout.to_string().size(); - } - } - if (_outputLayouts.has_value()) { - for (const ov::Layout& layout : _outputLayouts.value()) { - metadataSize += sizeof(uint16_t) + layout.to_string().size(); - } - } - - return metadataSize; +std::optional Metadata::get_compatibility_descriptor() const { + return _compatibilityDescriptor; } } // namespace intel_npu diff --git a/src/plugins/intel_npu/src/plugin/src/plugin.cpp b/src/plugins/intel_npu/src/plugin/src/plugin.cpp index 43e9c0843853..286dff6c6458 100644 --- a/src/plugins/intel_npu/src/plugin/src/plugin.cpp +++ b/src/plugins/intel_npu/src/plugin/src/plugin.cpp @@ -5,6 +5,7 @@ #include "plugin.hpp" #include +#include #include "compiled_model.hpp" #include "intel_npu/common/compiler_adapter_factory.hpp" @@ -17,10 +18,10 @@ #include "intel_npu/config/npuw.hpp" #include "intel_npu/config/options.hpp" #include "intel_npu/utils/utils.hpp" -#include "intel_npu/utils/zero/zero_init.hpp" #include "metrics.hpp" #include "npuw/compiled_model.hpp" #include "npuw/llm_compiled_model.hpp" +#include "npuw/orc/schema_npuw.hpp" #include "npuw/serialization.hpp" #include "openvino/core/rt_info/weightless_caching_attributes.hpp" #include "openvino/op/constant.hpp" @@ -148,26 +149,34 @@ void check_weightless_cache_attribute_occurrence(const std::shared_ptr import_model_npuw(std::istream& stream, ov::AnyMap& properties, std::shared_ptr pluginSO) { + if (const auto header = ov::npuw::orc::is_orc(stream); + header.has_value() && header->schema_uuid == ov::npuw::orc::schema_npuw::NPUW_ORC_PARTITIONED_SCHEMA) { + return ov::npuw::CompiledModel::import_model(stream, pluginSO, properties); + } + // If was exported via NPUW auto stream_start_pos = stream.tellg(); ov::npuw::s11n::IndicatorType serialization_indicator; - ov::npuw::s11n::read(stream, serialization_indicator); - if (serialization_indicator == NPUW_SERIALIZATION_INDICATOR) { + if (ov::npuw::orc::try_read_bytes(stream, serialization_indicator.data(), serialization_indicator.size()) && + serialization_indicator == NPUW_SERIALIZATION_INDICATOR) { ov::npuw::s11n::IndicatorType compiled_model_indicator; - ov::npuw::s11n::read(stream, compiled_model_indicator); - stream.seekg(-stream.tellg() + stream_start_pos, std::ios::cur); - - if (compiled_model_indicator == NPUW_LLM_COMPILED_MODEL_INDICATOR) { - // Properties are required for ov::weights_path - return ov::npuw::LLMCompiledModel::import_model(stream, pluginSO, properties); - } else if (compiled_model_indicator == NPUW_COMPILED_MODEL_INDICATOR) { - // Properties are required for ov::weights_path - return ov::npuw::CompiledModel::import_model(stream, pluginSO, properties); - } else { - OPENVINO_THROW("Couldn't deserialize NPUW blob - fatal error!"); + if (ov::npuw::orc::try_read_bytes(stream, compiled_model_indicator.data(), compiled_model_indicator.size())) { + stream.clear(); + stream.seekg(stream_start_pos); + + if (compiled_model_indicator == NPUW_LLM_COMPILED_MODEL_INDICATOR) { + // Properties are required for ov::weights_path + return ov::npuw::LLMCompiledModel::import_model(stream, pluginSO, properties); + } else if (compiled_model_indicator == NPUW_COMPILED_MODEL_INDICATOR) { + OPENVINO_THROW("Legacy flat NPUW CompiledModel blobs are no longer supported. Re-export the model with " + "the current ORC serializer."); + } else { + OPENVINO_THROW("Couldn't deserialize NPUW blob - fatal error!"); + } } } - stream.seekg(-stream.tellg() + stream_start_pos, std::ios::cur); + stream.clear(); + stream.seekg(stream_start_pos); // Drop NPUW properties if there are any for (auto it = properties.begin(); it != properties.end(); ++it) { @@ -221,15 +230,17 @@ void init_config(const IEngineBackend* backend, OptionsDesc& options, FilteredCo REGISTER_OPTION(PERFORMANCE_HINT); REGISTER_OPTION(EXECUTION_MODE_HINT); REGISTER_OPTION(PERFORMANCE_HINT_NUM_REQUESTS); + OPENVINO_SUPPRESS_DEPRECATED_START REGISTER_OPTION(ENABLE_CPU_PINNING); + OPENVINO_SUPPRESS_DEPRECATED_END REGISTER_OPTION(INFERENCE_PRECISION_HINT); REGISTER_OPTION(MODEL_PRIORITY); - REGISTER_OPTION(EXCLUSIVE_ASYNC_REQUESTS); REGISTER_OPTION(COMPILATION_MODE_PARAMS); REGISTER_OPTION(DMA_ENGINES); REGISTER_OPTION(TILES); REGISTER_OPTION(COMPILATION_MODE); REGISTER_OPTION(COMPILER_TYPE); + REGISTER_OPTION(COMPILER_VERSION); REGISTER_OPTION(PLATFORM); REGISTER_OPTION(CREATE_EXECUTOR); REGISTER_OPTION(DYNAMIC_SHAPE_TO_STATIC); @@ -255,6 +266,9 @@ void init_config(const IEngineBackend* backend, OptionsDesc& options, FilteredCo REGISTER_OPTION(MODEL_SERIALIZER_VERSION); REGISTER_OPTION(ENABLE_STRIDES_FOR); REGISTER_OPTION(SHARED_COMMON_QUEUE); + REGISTER_OPTION(CACHE_ENCRYPTION_CALLBACKS); + REGISTER_OPTION(RUNTIME_REQUIREMENTS); + REGISTER_OPTION(COMPATIBILITY_CHECK); if (backend) { // Options registered only if drivers is present and supports the corresponding extension @@ -358,7 +372,76 @@ void Plugin::set_property(const ov::AnyMap& properties) { _propertiesManager->setProperty(properties); } +ov::CompatibilityCheck Plugin::validate_compatibility_descriptor(ov::intel_npu::CompilerType compilerType, + const ov::AnyMap& arguments) const { + if (arguments.empty() || arguments.find(ov::runtime_requirements.name()) == arguments.end()) { + return ov::CompatibilityCheck::NOT_APPLICABLE; + } + + const auto& runtimeRequirements = arguments.at(ov::runtime_requirements.name()).as(); + _logger.debug("Received runtime_requirements: %s length: %zu", + runtimeRequirements.c_str(), + runtimeRequirements.length()); + + // NPU Plugin's runtime requirements are captured in its metadata. + // For now plugin's requirements are met if metadata can be retrieved from the tensor + std::unique_ptr metadata = nullptr; + try { + // The plugin cares only about the string size and the metadata version check for now. Additional checks based + // on other metadata fields can be done following this line. + metadata = read_as_text(runtimeRequirements); + } catch (const std::exception& ex) { + // Unsupported version, could not read the metadata or an unknown error has occured. Report that the + // requirements are not met. + _logger.debug("Failed to read metadata from the runtime requirements. The requirements are not met. %s", + ex.what()); + return ov::CompatibilityCheck::UNSUPPORTED; + } + + const auto descriptorView = metadata->get_compatibility_descriptor(); + std::string compatibilityDescriptor = descriptorView.has_value() ? std::string(descriptorView.value()) : ""; + _logger.debug("Retrieved compatibility descriptor from metadata: %s length: %zu", + compatibilityDescriptor.c_str(), + compatibilityDescriptor.length()); + + // Implement only the fallback path for now through the PLUGIN compiler type + std::unique_ptr compiler = nullptr; + CompilerAdapterFactory factory; + try { + compiler = factory.getCompiler(_backend, compilerType, std::string_view{}); + + // Compiler can validate only if the string describes a blob compatible with the current platform + auto result = compiler->validate_compatibility_descriptor(compatibilityDescriptor); + _logger.debug("Compatibility check result: %s", result ? "met" : "not met"); + if (result) { + return ov::CompatibilityCheck::SUPPORTED; + } else { + return ov::CompatibilityCheck::UNSUPPORTED; + } + } catch (const std::exception&) { + _logger.error("Failed to create the recommended compiler type for the compatibility check %d. The requirements " + "are not met.", + static_cast(compilerType)); + return ov::CompatibilityCheck::NOT_APPLICABLE; + } +} + ov::Any Plugin::get_property(const std::string& name, const ov::AnyMap& arguments) const { + // Special cases that need to be treated outside of the property manager. + // Checking runtime requirements requires access to plugin's metadata + if (name == ov::compatibility_check.name()) { + // Reading the (dummy) property content to check if it is supported + // Expected to throw if the property is not supported + _propertiesManager->getProperty(name); + + // The property was enabled based on the support of the compatibility check in the compiler adapters + // Use the compiler type determined for compatibility check to validate the requirements and return the result + auto compilerType = _propertiesManager->determineCompilerTypeForCompatibilityCheck(); + + // Validates both local (plugin's) requirements and device requirements + return validate_compatibility_descriptor(compilerType, arguments); + } + if (!arguments.empty()) { auto npuPluginArguments = arguments; exclude_model_ptr_from_map(npuPluginArguments); @@ -417,8 +500,8 @@ std::shared_ptr Plugin::compile_model(const std::shared_ptr< } } - // ov::hint::model has no corresponding "Config" implementation thus we need to remove it from the - // list of properties + // ov::hint::model has no corresponding "Config" implementation thus we need to + // remove it from the list of properties if (exclude_model_ptr_from_map(localProperties)) { _logger.warning("Model received in config will be ignored as it was already provided by parameter."); } @@ -453,6 +536,7 @@ std::shared_ptr Plugin::compile_model(const std::shared_ptr< OV_ITT_TASK_CHAIN(PLUGIN_COMPILE_MODEL, itt::domains::NPUPlugin, "Plugin::compile_model", "fork_local_config"); FilteredConfig localConfig = _propertiesManager->getConfigForSpecificCompiler(localProperties, compiler.get()); + localConfig.update({{ov::intel_npu::compiler_version.name(), std::to_string(compiler->get_version())}}); auto updateBatchMode = [&](ov::intel_npu::BatchMode mode) { std::stringstream strStream; @@ -534,11 +618,11 @@ std::shared_ptr Plugin::compile_model(const std::shared_ptr< const bool cacheModeOptimizeSize = (localConfig.get() == ov::CacheMode::OPTIMIZE_SIZE); if (localConfig.get() && !cacheModeOptimizeSize) { _logger.warning( - "The cache mode was not set to \"optimize size\" but the \"ENABLE_WEIGHTLESS\" configuration option" + "The cache mode was not set to \"optimize size\" but the \"ENABLE_WEIGHTLESS\" configuration option " "was set to true. Weights separation WILL NOT be performed in this case."); } else if (!localConfig.get() && cacheModeOptimizeSize) { _logger.warning( - "The cache mode was set to \"optimize size\" but the \"ENABLE_WEIGHTLESS\" configuration option" + "The cache mode was set to \"optimize size\" but the \"ENABLE_WEIGHTLESS\" configuration option " "was set to false. Weights separation WILL be performed in this case."); } @@ -604,6 +688,13 @@ std::shared_ptr Plugin::compile_model(const std::shared_ptr< } } + if (localConfig.has(CACHE_ENCRYPTION_CALLBACKS::key().data()) && + !localConfig.get().encrypt) { + _logger.warning("Encryption callbacks were provided for compiled model creation, but the encrypt " + "callback is null. Proceeding with unencrypted compilation; encrypted blob export " + "will be disabled."); + } + std::shared_ptr compiledModel; try { compiledModel = std::make_shared(model, shared_from_this(), device, graph, localConfig, batch); @@ -682,6 +773,7 @@ std::shared_ptr Plugin::import_model(std::istream& stream, c } else { _logger.info("Blob compatibility check skipped."); } + OPENVINO_ASSERT(blobSize > 0, "Parsed blob size is empty from the given stream!"); ov::Allocator customAllocator{utils::AlignedAllocator{utils::STANDARD_PAGE_SIZE}}; ov::Tensor tensor(ov::element::u8, ov::Shape{blobSize}, customAllocator); @@ -746,6 +838,7 @@ std::shared_ptr Plugin::import_model(const ov::Tensor& compi } else { _logger.info("Blob compatibility check skipped."); } + OPENVINO_ASSERT(blobSize > 0, "Parsed blob size is empty from the given buffer!"); const ov::Tensor roiTensor(compiledBlob, ov::Coordinate{0}, ov::Coordinate{blobSize}); // ROI tensor to skip NPU plugin metadata @@ -817,8 +910,8 @@ std::shared_ptr Plugin::parse(const ov::Tensor& tensorBig, auto localProperties = properties; - // ov::hint::model has no corresponding "Config" implementation thus we need to remove it from the - // list of properties + // ov::hint::model has no corresponding "Config" implementation thus we need to + // remove it from the list of properties auto originalModel = exclude_model_ptr_from_map(localProperties); std::shared_ptr device = @@ -837,22 +930,68 @@ std::shared_ptr Plugin::parse(const ov::Tensor& tensorBig, "The usage of a compiled model can lead to undefined behavior. Please use OpenVINO IR instead!"); } - uint64_t mainSize = tensorBig.get_byte_size(); + const bool isNotNullDecryption = localConfig.has(CACHE_ENCRYPTION_CALLBACKS::key().data()) && + localConfig.get().decrypt != nullptr; + if (!metadata && isNotNullDecryption) { + _logger.warning( + "Received decryption callback, but metadata parsing is skipped and cannot determine if blob was " + "encrypted or not."); + } + + ov::Tensor tensor = tensorBig; + if (isNotNullDecryption && + (metadata == nullptr || (metadata != nullptr && metadata->is_encrypted_blob().value_or(false)))) { + { + std::string decryptedBlobStr; + { + std::string encryptedBlobStr(tensor.data(), tensor.get_byte_size()); // +1x blob size + decryptedBlobStr = + localConfig.get().decrypt(encryptedBlobStr); // +2x blob size + } // -1x blob size when deallocating temporary encrypted blob string + ov::Allocator customAllocator{utils::AlignedAllocator{utils::STANDARD_PAGE_SIZE}}; + size_t alignedSize = utils::align_size_to_standard_page_size(decryptedBlobStr.size()); + size_t paddingSize = alignedSize - decryptedBlobStr.size(); + tensor = ov::Tensor(ov::element::u8, ov::Shape{alignedSize}, + customAllocator); // +1x blob size + std::memcpy(tensor.data(), decryptedBlobStr.c_str(), decryptedBlobStr.size()); + if (paddingSize > 0) { + // If user altered in some way initial blob during encryption, check if its size is still paged aligned + _logger.warning("Decrypted blob size was not page aligned, additional %zu bytes padding will be added", + paddingSize); + std::memset(tensor.data() + decryptedBlobStr.size(), 0, paddingSize); + } + } // -1x blob size when deallocating decrypted blob string + } + + uint64_t mainSize = tensor.get_byte_size(); std::optional> initSizes; std::optional batchSize = std::nullopt; if (metadata) { + if (metadata->is_encrypted_blob().value_or(false) && !isNotNullDecryption) { + OPENVINO_THROW("Blob is encrypted, but no decryption callback was provided!"); + } + size_t accumulator = 0; initSizes = metadata->get_init_sizes(); mainSize = initSizes.has_value() ? metadata->get_blob_size() - std::accumulate(initSizes->begin(), initSizes->end(), accumulator) : metadata->get_blob_size(); batchSize = metadata->get_batch_size(); + + std::optional compilerVersion = metadata->get_compiler_version(); + if (compilerVersion.has_value()) { + localConfig.update({{ov::intel_npu::compiler_version.name(), std::to_string(compilerVersion.value())}}); + _logger.debug("Imported model was compiled with compiler version: %u.%u", + ONEAPI_VERSION_MAJOR(compilerVersion.value()), + ONEAPI_VERSION_MINOR(compilerVersion.value())); + } } else { - _logger.info("Blob compatibility check skipped."); + _logger.warning( + "Metadata parsing is skipped, if this is a weightless blob, init schedules cannot be parsed from it!"); } - const ov::Tensor tensorMain(tensorBig, + const ov::Tensor tensorMain(tensor, ov::Coordinate{0}, ov::Coordinate{mainSize}); // ROI tensor to skip NPU plugin metadata @@ -863,7 +1002,7 @@ std::shared_ptr Plugin::parse(const ov::Tensor& tensorBig, // Read the init compiled models as well size_t cursorPosition = mainSize; for (uint64_t initSize : initSizes.value()) { - const ov::Tensor tensorInit(tensorBig, + const ov::Tensor tensorInit(tensor, ov::Coordinate{cursorPosition}, ov::Coordinate{cursorPosition + initSize}); tensorsInits.push_back(tensorInit); @@ -918,10 +1057,20 @@ std::shared_ptr Plugin::parse(const ov::Tensor& tensorBig, ParserFactory parserFactory; auto parser = parserFactory.getParser(_backend->getInitStructs()); + + // Convert descriptor to an owning string before metadata is potentially destroyed. + std::optional compatibilityDescriptor = std::nullopt; + if (metadata) { + if (const auto descriptorView = metadata->get_compatibility_descriptor(); descriptorView.has_value()) { + compatibilityDescriptor = std::string(descriptorView.value()); + } + } + auto graph = parser->parse(tensorMain, localConfig, initBlobs, - weightsSeparationEnabled ? std::make_optional(std::move(originalModel)) : std::nullopt); + weightsSeparationEnabled ? std::make_optional(std::move(originalModel)) : std::nullopt, + compatibilityDescriptor); graph->update_network_name("net" + std::to_string(_compiledModelLoadCounter++)); const std::shared_ptr modelDummy = diff --git a/src/plugins/intel_npu/src/plugin/src/properties.cpp b/src/plugins/intel_npu/src/plugin/src/properties.cpp index 5087ad818f52..0c4b2b067e90 100644 --- a/src/plugins/intel_npu/src/plugin/src/properties.cpp +++ b/src/plugins/intel_npu/src/plugin/src/properties.cpp @@ -16,10 +16,6 @@ namespace { std::map any_copy(const ov::AnyMap& params) { std::map result; for (auto&& value : params) { - // The value of cache_encryption_callbacks cannot be converted to std::string - if (value.first == ov::cache_encryption_callbacks.name()) { - continue; - } result.emplace(value.first, value.second.as()); } return result; @@ -30,6 +26,12 @@ inline bool isSpecialBothProperty(const std::string& key) { key == ov::log::level.name(); } +inline void logCpuPinningDeprecationWarning(intel_npu::Logger& logger) { + OPENVINO_SUPPRESS_DEPRECATED_START + logger.warning(intel_npu::ENABLE_CPU_PINNING::deprecationMessage()); + OPENVINO_SUPPRESS_DEPRECATED_END +} + void filterPropertiesByCompilerSupport(intel_npu::FilteredConfig& config, const intel_npu::ICompilerAdapter* compiler, const ov::SoPtr& backend, @@ -108,6 +110,14 @@ void filterPropertiesByCompilerSupport(intel_npu::FilteredConfig& config, if (backend && backend->isCommandQueueExtSupported()) { config.enable(ov::intel_npu::turbo.name(), true); } + + if (config.isAvailable(ov::intel_npu::enable_strides_for.name())) { + if (backend && backend->getGraphExtVersion() < ZE_MAKE_VERSION(1, 16)) { + logger.info("Config option %s not supported by the driver! Requirements not met.", + ov::intel_npu::enable_strides_for.name()); + config.enable(ov::intel_npu::enable_strides_for.name(), false); + } + } } void disableCompilerProperties(intel_npu::FilteredConfig& config, const ov::SoPtr& backend) { @@ -131,6 +141,14 @@ void disableCompilerProperties(intel_npu::FilteredConfig& config, const ov::SoPt } } +// Helper function for retrieving the device name +std::string get_specified_device_name(const intel_npu::Config& config) { + if (config.has()) { + return config.get(); + } + return std::string(); +} + } // namespace namespace intel_npu { @@ -401,41 +419,6 @@ namespace intel_npu { std::make_tuple(PROP_VISIBILITY, ov::PropertyMutability::RO, PROP_RETFUNC)); \ } while (0) -// Local helper function for appending platform name to the config -static Config add_platform_to_the_config(Config config, const std::string_view platform) { - config.update({{ov::intel_npu::platform.name(), std::string(platform)}}); - return config; -} - -// Local helper function for retrieving the device name -static auto get_specified_device_name(const Config config) { - if (config.has()) { - return config.get(); - } - return std::string(); -} - -// Heuristically obtained number. Varies depending on the values of PLATFORM and PERFORMANCE_HINT -// Note: this is the value provided by the plugin, application should query and consider it, but may supply its own -// preference for number of parallel requests via dedicated configuration -static int64_t getOptimalNumberOfInferRequestsInParallel(const Config& config) { - const std::string platform = config.get(); - - if (platform == ov::intel_npu::Platform::NPU3720) { - if (config.get() == ov::hint::PerformanceMode::THROUGHPUT) { - return 4; - } else { - return 1; - } - } else { - if (config.get() == ov::hint::PerformanceMode::THROUGHPUT) { - return 8; - } else { - return 1; - } - } -} - Properties::Properties(const PropertiesType pType, const FilteredConfig& config, const std::shared_ptr& metrics, @@ -459,6 +442,7 @@ Properties::Properties(const Properties& other) other._currentlyUsedCompiler, other._currentlyUsedPlatform, other._compilerConfigsFilteredByCompiler, + other._compatibilityCheckFiltered, other._properties, other._supportedProperties}; }()) {} @@ -472,6 +456,7 @@ Properties::Properties(CopyState&& state) _currentlyUsedCompiler(state.currentlyUsedCompiler), _currentlyUsedPlatform(std::move(state.currentlyUsedPlatform)), _compilerConfigsFilteredByCompiler(state.compilerConfigsFilteredByCompiler), + _compatibilityCheckFiltered(state.compatibilityCheckFiltered), _properties(std::move(state.properties)), _supportedProperties(std::move(state.supportedProperties)) {} @@ -526,7 +511,6 @@ void Properties::registerPluginProperties() { TRY_REGISTER_SIMPLE_PROPERTY(ov::device::id, DEVICE_ID); TRY_REGISTER_SIMPLE_PROPERTY(ov::num_streams, NUM_STREAMS); TRY_REGISTER_SIMPLE_PROPERTY(ov::weights_path, WEIGHTS_PATH); - TRY_REGISTER_SIMPLE_PROPERTY(ov::internal::exclusive_async_requests, EXCLUSIVE_ASYNC_REQUESTS); TRY_REGISTER_SIMPLE_PROPERTY(ov::intel_npu::compilation_mode_params, COMPILATION_MODE_PARAMS); TRY_REGISTER_SIMPLE_PROPERTY(ov::intel_npu::dma_engines, DMA_ENGINES); TRY_REGISTER_SIMPLE_PROPERTY(ov::intel_npu::tiles, TILES); @@ -549,7 +533,9 @@ void Properties::registerPluginProperties() { TRY_REGISTER_SIMPLE_PROPERTY(ov::intel_npu::export_raw_blob, EXPORT_RAW_BLOB); TRY_REGISTER_SIMPLE_PROPERTY(ov::intel_npu::import_raw_blob, IMPORT_RAW_BLOB); TRY_REGISTER_SIMPLE_PROPERTY(ov::intel_npu::batch_compiler_mode_settings, BATCH_COMPILER_MODE_SETTINGS); + OPENVINO_SUPPRESS_DEPRECATED_START TRY_REGISTER_SIMPLE_PROPERTY(ov::hint::enable_cpu_pinning, ENABLE_CPU_PINNING); + OPENVINO_SUPPRESS_DEPRECATED_END TRY_REGISTER_SIMPLE_PROPERTY(ov::workload_type, WORKLOAD_TYPE); TRY_REGISTER_SIMPLE_PROPERTY(ov::enable_weightless, ENABLE_WEIGHTLESS); TRY_REGISTER_SIMPLE_PROPERTY(ov::intel_npu::separate_weights_version, SEPARATE_WEIGHTS_VERSION); @@ -589,7 +575,27 @@ void Properties::registerPluginProperties() { } return false; }()); - TRY_REGISTER_SIMPLE_PROPERTY(ov::hint::enable_cpu_pinning, ENABLE_CPU_PINNING); + TRY_REGISTER_CUSTOM_PROPERTY(ov::compatibility_check, + COMPATIBILITY_CHECK, + true, + ov::PropertyMutability::RO, + [](const Config& /* unusedConfig */) { + // This property is implemented in the plugin directly + // This implementation here serves only to publish it in supported_properties + return false; + }); + + TRY_REGISTER_CUSTOM_PROPERTY( + ov::cache_encryption_callbacks, + CACHE_ENCRYPTION_CALLBACKS, + true, + ov::PropertyMutability::WO, + [](const Config& /* unusedConfig */) { + return (ov::EncryptionCallbacks{ + nullptr, + nullptr}); // enclosed in parentheses due to warning C4002 treated as error: too many arguments for + // function-like macro invocation 'TRY_REGISTER_CUSTOM_PROPERTY' + }); FORCE_REGISTER_CUSTOM_PROPERTY(ov::hint::model, MODEL_PTR, @@ -613,24 +619,25 @@ void Properties::registerPluginProperties() { if (_metrics != nullptr) { REGISTER_SIMPLE_METRIC(ov::available_devices, true, _metrics->GetAvailableDevicesNames()); REGISTER_SIMPLE_METRIC(ov::device::capabilities, true, _metrics->GetOptimizationCapabilities()); - REGISTER_SIMPLE_METRIC( - ov::optimal_number_of_infer_requests, - true, - static_cast(getOptimalNumberOfInferRequestsInParallel(add_platform_to_the_config( - config, - utils::getCompilationPlatform( - config.get(), - _backend == nullptr ? config.get() - : _backend->getDevice(config.get())->getName(), - _backend == nullptr ? std::vector() : _backend->getDeviceNames()))))); + REGISTER_SIMPLE_METRIC(ov::optimal_number_of_infer_requests, + true, + utils::getOptimalNumberOfInferRequestsInParallel( + utils::getCompilationPlatform( + config.get(), + _backend == nullptr ? config.get() + : _backend->getDevice(config.get())->getName(), + _backend == nullptr ? std::vector() : _backend->getDeviceNames()), + config.get())); REGISTER_SIMPLE_METRIC(ov::range_for_async_infer_requests, true, _metrics->GetRangeForAsyncInferRequest()); REGISTER_SIMPLE_METRIC(ov::range_for_streams, true, _metrics->GetRangeForStreams()); REGISTER_SIMPLE_METRIC(ov::device::pci_info, true, _metrics->GetPciInfo(get_specified_device_name(config))); REGISTER_SIMPLE_METRIC(ov::device::gops, true, _metrics->GetGops(get_specified_device_name(config))); REGISTER_SIMPLE_METRIC(ov::device::type, true, _metrics->GetDeviceType(get_specified_device_name(config))); - REGISTER_CUSTOM_METRIC(ov::internal::supported_properties, false, [&](const Config&) { - return _internalSupportedProperties; - }); + REGISTER_CUSTOM_METRIC(ov::internal::supported_properties, + false, + [&](const Config&) -> const std::vector& { + return _internalSupportedProperties; + }); REGISTER_SIMPLE_METRIC(ov::internal::cache_header_alignment, false, utils::STANDARD_PAGE_SIZE); REGISTER_SIMPLE_METRIC(ov::intel_npu::device_alloc_mem_size, true, @@ -718,7 +725,9 @@ void Properties::registerCompiledModelProperties() { // FORCE_REGISTER_CUSTOM_PROPERTY format: (property, visibility, mutability, custom_return_lambda_function) // Permanent properties + OPENVINO_SUPPRESS_DEPRECATED_START TRY_REGISTER_SIMPLE_PROPERTY(ov::hint::enable_cpu_pinning, ENABLE_CPU_PINNING); + OPENVINO_SUPPRESS_DEPRECATED_END TRY_REGISTER_SIMPLE_PROPERTY(ov::log::level, LOG_LEVEL); TRY_REGISTER_SIMPLE_PROPERTY(ov::loaded_from_cache, LOADED_FROM_CACHE); TRY_REGISTER_SIMPLE_PROPERTY(ov::hint::performance_mode, PERFORMANCE_HINT); @@ -729,7 +738,9 @@ void Properties::registerCompiledModelProperties() { TRY_REGISTER_SIMPLE_PROPERTY(ov::cache_mode, CACHE_MODE); // Properties we shall only enable if they were set prior-to-compilation + TRY_REGISTER_COMPILEDMODEL_PROPERTY_IFSET(ov::num_streams, NUM_STREAMS); TRY_REGISTER_COMPILEDMODEL_PROPERTY_IFSET(ov::intel_npu::compiler_type, COMPILER_TYPE); + TRY_REGISTER_COMPILEDMODEL_PROPERTY_IFSET(ov::intel_npu::compiler_version, COMPILER_VERSION); TRY_REGISTER_COMPILEDMODEL_PROPERTY_IFSET(ov::weights_path, WEIGHTS_PATH); TRY_REGISTER_COMPILEDMODEL_PROPERTY_IFSET(ov::cache_dir, CACHE_DIR); TRY_REGISTER_COMPILEDMODEL_PROPERTY_IFSET(ov::enable_profiling, PERF_COUNT); @@ -777,6 +788,18 @@ void Properties::registerCompiledModelProperties() { return config.get(); }); + TRY_REGISTER_CUSTOM_PROPERTY( + ov::cache_encryption_callbacks, + CACHE_ENCRYPTION_CALLBACKS, + true, + ov::PropertyMutability::WO, + [](const Config& /* unusedConfig */) { + return (ov::EncryptionCallbacks{ + nullptr, + nullptr}); // enclosed in parentheses due to warning C4002 treated as error: too many arguments for + // function-like macro invocation 'TRY_REGISTER_CUSTOM_PROPERTY' + }); + FORCE_REGISTER_CUSTOM_PROPERTY(ov::hint::model, MODEL_PTR, true, @@ -784,6 +807,15 @@ void Properties::registerCompiledModelProperties() { [](const Config& /* unusedConfig */) { return std::shared_ptr(nullptr); }); + TRY_REGISTER_CUSTOM_PROPERTY(ov::runtime_requirements, + RUNTIME_REQUIREMENTS, + true, + ov::PropertyMutability::RO, + [](const Config& /* unusedConfig */) { + // This property is implemented in compiled model directly + // This implementation here serves only to publish it in supported_properties + return std::string(""); + }); // 2. Metrics (static device and enviroment properties) // ======== @@ -793,22 +825,28 @@ void Properties::registerCompiledModelProperties() { REGISTER_CUSTOM_METRIC(ov::model_name, true, [](const Config&) { // TODO: log an error here as the code shouldn't have gotten here // this property is implemented in compiled model directly - // this implementation here servers only to publish it in supported_properties + // this implementation here serves only to publish it in supported_properties return std::string("invalid"); }); - REGISTER_SIMPLE_METRIC(ov::optimal_number_of_infer_requests, - true, - static_cast(getOptimalNumberOfInferRequestsInParallel(config))); + REGISTER_SIMPLE_METRIC( + ov::optimal_number_of_infer_requests, + true, + utils::getOptimalNumberOfInferRequestsInParallel(config.get(), config.get())); REGISTER_CUSTOM_METRIC(ov::execution_devices, true, [](const Config&) { // TODO: log an error here as the code shouldn't have gotten here // this property is implemented in compiled model directly - // this implementation here servers only to publish it in supported_properties + // this implementation here serves only to publish it in supported_properties return std::string("NPU"); }); } ov::Any Properties::getProperty(const std::string& name) { std::lock_guard lock(_mutex); + + if (name == ov::hint::enable_cpu_pinning.name()) { + logCpuPinningDeprecationWarning(_logger); + } + if (_pType == PropertiesType::PLUGIN) { bool propertyIsCompilerConfig = false; bool propertyIsRegistered = true; @@ -826,6 +864,11 @@ ov::Any Properties::getProperty(const std::string& name) { } } + bool needToResetProperties = false; + if (name == ov::compatibility_check.name() || name == ov::supported_properties.name()) { + // Mark that properties need to be registered again if the internal config is updated + needToResetProperties = disable_compatibility_check_if_needed(); + } // Special case for Supported Properties and Caching Properties as they are compiler dependent. So we need to // check compiler support for those properties on each getProperty call as well. if (propertyIsCompilerConfig || !propertyIsRegistered || name == ov::supported_properties.name() || @@ -861,17 +904,26 @@ ov::Any Properties::getProperty(const std::string& name) { // filter out options again filterPropertiesByCompilerSupport(_config, compiler.get(), _backend, _logger); - // reset properties for the new options - registerProperties(); _compilerConfigsFilteredByCompiler = true; _currentlyUsedCompiler = compilerType; - _currentlyUsedPlatform = compilationPlatform; + _currentlyUsedPlatform = std::move(compilationPlatform); + needToResetProperties = true; } } + + if (needToResetProperties) { + // reset properties for the new options + registerProperties(); + } } auto&& configIterator = _properties.find(name); if (configIterator != _properties.cend()) { + if (std::get<1>(configIterator->second) == ov::PropertyMutability::WO) { + _logger.warning("Trying to get WRITE-ONLY property: %s. Returning empty `ov::Any` object", + name.c_str()); // throw OV exception instead + return ov::Any(); + } return std::get<2>(configIterator->second)(_config); } try { @@ -884,10 +936,14 @@ ov::Any Properties::getProperty(const std::string& name) { void Properties::setProperty(const ov::AnyMap& properties) { std::lock_guard lock(_mutex); - if (properties.count(ov::log::level.name()) != 0) { + if (properties.find(ov::log::level.name()) != properties.end()) { _logger.setLevel(properties.at(ov::log::level.name()).as()); } + if (properties.find(ov::hint::enable_cpu_pinning.name()) != properties.end()) { + logCpuPinningDeprecationWarning(_logger); + } + std::unique_ptr compiler = nullptr; if (_pType == PropertiesType::PLUGIN) { bool propertyIsCompilerConfig = false; @@ -934,12 +990,13 @@ void Properties::setProperty(const ov::AnyMap& properties) { registerProperties(); _compilerConfigsFilteredByCompiler = true; _currentlyUsedCompiler = compilerType; - _currentlyUsedPlatform = compilationPlatform; + _currentlyUsedPlatform = std::move(compilationPlatform); } } } std::map cfgs_to_set; + ov::AnyMap special_cfgs_to_set; for (auto&& value : properties) { if (_properties.find(value.first) == _properties.end()) { // property doesn't exist @@ -957,6 +1014,8 @@ void Properties::setProperty(const ov::AnyMap& properties) { } else { if (std::get<1>(_properties[value.first]) == ov::PropertyMutability::RO) { OPENVINO_THROW("READ-ONLY configuration key: ", value.first); + } else if (value.first == ov::cache_encryption_callbacks.name()) { + special_cfgs_to_set.emplace(value.first, value.second); } else { cfgs_to_set.emplace(value.first, value.second.as()); } @@ -966,10 +1025,18 @@ void Properties::setProperty(const ov::AnyMap& properties) { if (!cfgs_to_set.empty()) { _config.update(cfgs_to_set); } + + if (!special_cfgs_to_set.empty()) { + _config.updateAny(special_cfgs_to_set); + } } bool Properties::isPropertySupported(const std::string& name) { std::lock_guard lock(_mutex); + if (name == ov::hint::enable_cpu_pinning.name()) { + logCpuPinningDeprecationWarning(_logger); + } + if (_pType == PropertiesType::PLUGIN) { const bool isRegistered = isPropertyRegistered(name); const bool isConfigOption = _config.hasOpt(name); @@ -979,6 +1046,14 @@ bool Properties::isPropertySupported(const std::string& name) { return false; } + if (name == ov::compatibility_check.name()) { + bool disabled = disable_compatibility_check_if_needed(); + if (disabled) { + registerProperties(); + return false; + } + } + if (isRegistered) { // Registered and not a config option: always supported. Or it is a special both property which is always // supported. @@ -1056,6 +1131,10 @@ FilteredConfig Properties::getConfigForSpecificCompiler(const ov::AnyMap& proper _logger); }(); + if (properties.find(ov::hint::enable_cpu_pinning.name()) != properties.end()) { + logCpuPinningDeprecationWarning(logger); + } + std::optional propertiesCompilerType = std::nullopt; std::optional propertiesPlatform = std::nullopt; if (compilerConfigsFilteredByCompiler) { @@ -1080,6 +1159,7 @@ FilteredConfig Properties::getConfigForSpecificCompiler(const ov::AnyMap& proper const std::map rawConfig = any_copy(properties); std::map cfgsToSet; + ov::AnyMap specialCfgsToSet; for (const auto& [key, value] : rawConfig) { if (!updatedConfig.hasOpt(key)) { // not a known config key @@ -1088,14 +1168,17 @@ FilteredConfig Properties::getConfigForSpecificCompiler(const ov::AnyMap& proper } else { updatedConfig.addOrUpdateInternal(key, value); } + } else if (key == ov::cache_encryption_callbacks.name()) { + specialCfgsToSet.emplace(key, properties.at(key)); } else { cfgsToSet.emplace(key, value); } } updatedConfig.update(cfgsToSet); + updatedConfig.updateAny(specialCfgsToSet); - return updatedConfig; + return std::move(updatedConfig); } FilteredConfig Properties::getConfigWithCompilerPropertiesDisabled(const ov::AnyMap& properties) { @@ -1108,12 +1191,17 @@ FilteredConfig Properties::getConfigWithCompilerPropertiesDisabled(const ov::Any disableCompilerProperties(updatedConfig, _backend); } + if (properties.find(ov::hint::enable_cpu_pinning.name()) != properties.end()) { + logCpuPinningDeprecationWarning(logger); + } + if (properties.empty()) { return std::move(updatedConfig); } const std::map rawConfig = any_copy(properties); std::map cfgsToSet; + ov::AnyMap specialCfgsToSet; for (const auto& [key, value] : rawConfig) { if (updatedConfig.hasOpt(key)) { const auto optionMode = updatedConfig.getOpt(key).mode(); @@ -1132,10 +1220,15 @@ FilteredConfig Properties::getConfigWithCompilerPropertiesDisabled(const ov::Any } } - cfgsToSet.emplace(key, value); + if (key == ov::cache_encryption_callbacks.name()) { + specialCfgsToSet.emplace(key, properties.at(key)); + } else { + cfgsToSet.emplace(key, value); + } } updatedConfig.update(cfgsToSet); + updatedConfig.updateAny(specialCfgsToSet); return std::move(updatedConfig); } @@ -1167,4 +1260,68 @@ std::string Properties::determineDeviceId(const ov::AnyMap& properties) const { return _config.get(); } +bool Properties::disable_compatibility_check_if_needed() { + // COMPATIBILITY_CHECK is a RunTime option, thus enabled by default + // The property should be supported only if at least one of the compiler adapters support it. + // No need to check again if it was enabled already + // Plugin will prefer the validation to be performed through CID, but it will fallback + // to the CIP validation otherwise. + + if (_compatibilityCheckFiltered) { + // The property was already filtered by compiler support, no need to check again + return false; + } + + // Mark that the property has been filtered by compiler support, regardless of the result + _compatibilityCheckFiltered = true; + + CompilerAdapterFactory factory; + auto compilerType = ov::intel_npu::CompilerType::DRIVER; + try { + auto tempCompiler = factory.getCompiler(_backend, compilerType, std::string_view{}); + // If CID is present but does not support the query, fallback to CIP + if (!tempCompiler->is_option_supported(ov::compatibility_check.name())) { + compilerType = ov::intel_npu::CompilerType::PLUGIN; + try { + tempCompiler = factory.getCompiler(_backend, compilerType, std::string_view{}); + if (!tempCompiler->is_option_supported(ov::compatibility_check.name())) { + // Neither of the compiler adapters support the option, it should be disabled + _logger.debug("Neither CID nor CIP support the compatibility check! Disabling the property."); + _config.enable(ov::compatibility_check.name(), false); + return true; // config was updated + } else { + // CIP is present and supports the option, COMPATIBILITY_CHECK remains enabled + _compilerForCompatibilityCheck = ov::intel_npu::CompilerType::PLUGIN; + } + } catch (const std::exception&) { + // CIP is not present either, the property is not supported + _logger.debug("CIP is not present! Disabling the compatibility check property."); + _config.enable(ov::compatibility_check.name(), false); + return true; // config was updated + } + } else { + // COMPATIBILITY_CHECK remains enabled + _compilerForCompatibilityCheck = ov::intel_npu::CompilerType::DRIVER; + } + } catch (const std::exception&) { + // If CID is not present (driver is not present either) plugin will not be able to retrieve + // the device information required for the CIP check, thus the property should not be supported. + // No need to check the CIP support anymore in this case + _logger.debug("Driver is not present! Disabling the compatibility check property."); + _config.enable(ov::compatibility_check.name(), false); + return true; // config was updated + } + + return false; // config was not updated +} + +ov::intel_npu::CompilerType Properties::determineCompilerTypeForCompatibilityCheck() const { + // The compiler type used for compatibility check is determined based on the support of + // the COMPATIBILITY_CHECK option in the compiler adapters. If the option is supported + // in CID, it will be preferred for compatibility check, otherwise CIP will be used + // if it supports the option. + + return _compilerForCompatibilityCheck; +} + } // namespace intel_npu diff --git a/src/plugins/intel_npu/src/plugin/src/transformations.cpp b/src/plugins/intel_npu/src/plugin/src/transformations.cpp index ce3ff26717bc..9e26373f8892 100644 --- a/src/plugins/intel_npu/src/plugin/src/transformations.cpp +++ b/src/plugins/intel_npu/src/plugin/src/transformations.cpp @@ -189,10 +189,9 @@ std::tuple, bool> handlePluginBatching( const std::function& updateBatchMode, std::optional& originalBatch, Logger logger) { - auto originalModel = std::const_pointer_cast(model); // Keep the original model for all no-op/early-return paths. // A mutable clone is created only when plugin batching is actually about to be applied. - auto reshapedModel = originalModel; + auto resultModel = std::const_pointer_cast(model); auto successfullyDebatched = false; auto batchModeIsAvailable = localConfig.isAvailable(ov::intel_npu::batch_mode.name()); @@ -203,7 +202,7 @@ std::tuple, bool> handlePluginBatching( (batchMode == ov::intel_npu::BatchMode::PLUGIN || batchMode == ov::intel_npu::BatchMode::AUTO); if (!isAutoOrPluginBatch) { - return {reshapedModel, successfullyDebatched}; + return {resultModel, successfullyDebatched}; } } else { // If the compiler doesn't support BATCH_MODE, we can still try using batching @@ -218,23 +217,23 @@ std::tuple, bool> handlePluginBatching( logger.info("Batching will be handled by compiler."); updateBatchMode(ov::intel_npu::BatchMode::COMPILER); } - return {reshapedModel, successfullyDebatched}; + return {resultModel, successfullyDebatched}; } logger.info("Attempting to handle batching on the plugin side."); // Clone right before mutation to avoid extra memory usage when batching is skipped. - reshapedModel = model->clone(); + resultModel = model->clone(); try { - originalBatch = ov::get_batch(reshapedModel); - ov::set_batch(reshapedModel, ov::Dimension(1)); + originalBatch = ov::get_batch(resultModel); + ov::set_batch(resultModel, ov::Dimension(1)); successfullyDebatched = true; } catch (const std::exception& ex) { logger.warning("The plugin couldn't resize a batched model due to exception: %s.\n" "Trying to debatch it...", ex.what()); - if (!deBatchModel(reshapedModel, ov::Dimension(1), originalBatch)) { + if (!deBatchModel(resultModel, ov::Dimension(1), originalBatch)) { OPENVINO_THROW("Cannot debatch a model"); } logger.info("The model has been debatched successfully"); @@ -248,7 +247,7 @@ std::tuple, bool> handlePluginBatching( } } catch (const std::exception& ex) { // If plugin-side transformation failed, keep the original model and drop the clone - reshapedModel = originalModel; + resultModel = std::const_pointer_cast(model); if (batchMode == ov::intel_npu::BatchMode::AUTO) { logger.info("Couldn't validate and reshape the model. Batching will be handled by compiler. Error: %s", ex.what()); @@ -259,11 +258,11 @@ std::tuple, bool> handlePluginBatching( updateBatchMode(ov::intel_npu::BatchMode::COMPILER); } } else { - OPENVINO_THROW("Couldn't validate and reshape the model for PLUGIN batch mode. Error: %s", ex.what()); + OPENVINO_THROW("Couldn't validate and reshape the model for PLUGIN batch mode. Error: ", ex.what()); } } - return {reshapedModel, successfullyDebatched}; + return {resultModel, successfullyDebatched}; } } // namespace batch_helpers diff --git a/src/plugins/intel_npu/src/utils/include/intel_npu/utils/vcl/vcl.h b/src/plugins/intel_npu/src/utils/include/intel_npu/utils/vcl/vcl.h index a7f58240d0e4..47e53d4c8a36 100644 --- a/src/plugins/intel_npu/src/utils/include/intel_npu/utils/vcl/vcl.h +++ b/src/plugins/intel_npu/src/utils/include/intel_npu/utils/vcl/vcl.h @@ -6,49 +6,49 @@ #define VPUX_COMPILER_L0_H #if defined(__cplusplus) -# include -# include +#include +#include #else -# include -# include +#include +#include #endif #if defined(__cplusplus) -# pragma once +#pragma once #endif #if defined(__cplusplus) extern "C" { #endif -#define VCL_COMPILER_VERSION_MAJOR 7 -#define VCL_COMPILER_VERSION_MINOR 6 +#define VCL_COMPILER_VERSION_MAJOR 7 +#define VCL_COMPILER_VERSION_MINOR 7 #define VCL_PROFILING_VERSION_MAJOR 2 #define VCL_PROFILING_VERSION_MINOR 0 #ifndef DEPRECATED -# define DEPRECATED // for documentation only +#define DEPRECATED // for documentation only #endif /////////////////////////////////////////////////////////////////////////////// #ifndef VCL_APICALL -# if defined(_WIN32) +#if defined(_WIN32) /// @brief Calling convention for all API functions -# define VCL_APICALL __cdecl -# else -# define VCL_APICALL -# endif // defined(_WIN32) -#endif // VCL_APICALL +#define VCL_APICALL __cdecl +#else +#define VCL_APICALL +#endif // defined(_WIN32) +#endif // VCL_APICALL /////////////////////////////////////////////////////////////////////////////// #ifndef VCL_APIEXPORT -# if defined(_WIN32) +#if defined(_WIN32) /// @brief Windows-specific dllexport storage-class attribute -# define VCL_APIEXPORT __declspec(dllexport) -# else -# define VCL_APIEXPORT -# endif // defined(_WIN32) -#endif // VCL_APIEXPORT +#define VCL_APIEXPORT __declspec(dllexport) +#else +#define VCL_APIEXPORT +#endif // defined(_WIN32) +#endif // VCL_APIEXPORT /////////////////////////////////////////////////////////////////////////////// /// @brief Compiler handle @@ -214,8 +214,7 @@ VCL_APIEXPORT vcl_result_t VCL_APICALL vclGetVersion(vcl_version_info_t* compile /////////////////////////////////////////////////////////////////////////////// /// @brief Creates a compiler object and returns the compiler handle VCL_APIEXPORT vcl_result_t VCL_APICALL vclCompilerCreate(vcl_compiler_desc_t* compilerDesc, - vcl_device_desc_t* deviceDesc, - vcl_compiler_handle_t* compiler, + vcl_device_desc_t* deviceDesc, vcl_compiler_handle_t* compiler, vcl_log_handle_t* logHandle); /////////////////////////////////////////////////////////////////////////////// @@ -229,8 +228,7 @@ VCL_APIEXPORT vcl_result_t VCL_APICALL vclCompilerGetProperties(vcl_compiler_han /////////////////////////////////////////////////////////////////////////////// /// @brief Create an querynetwork object and return the handle -VCL_APIEXPORT vcl_result_t VCL_APICALL vclQueryNetworkCreate(vcl_compiler_handle_t compiler, - vcl_query_desc_t desc, +VCL_APIEXPORT vcl_result_t VCL_APICALL vclQueryNetworkCreate(vcl_compiler_handle_t compiler, vcl_query_desc_t desc, vcl_query_handle_t* query); /////////////////////////////////////////////////////////////////////////////// @@ -245,8 +243,7 @@ VCL_APIEXPORT vcl_result_t VCL_APICALL vclQueryNetworkDestroy(vcl_query_handle_t /////////////////////////////////////////////////////////////////////////////// /// @brief Creates an executable object and returns the executable handle. /// Parse modelIRData in the executable descriptor to blob and store it in the executable. -VCL_APIEXPORT vcl_result_t VCL_APICALL vclExecutableCreate(vcl_compiler_handle_t compiler, - vcl_executable_desc_t desc, +VCL_APIEXPORT vcl_result_t VCL_APICALL vclExecutableCreate(vcl_compiler_handle_t compiler, vcl_executable_desc_t desc, vcl_executable_handle_t* executable); DEPRECATED typedef struct __vcl_allocator_t { @@ -261,15 +258,23 @@ typedef struct __vcl_allocator2_t { DEPRECATED VCL_APIEXPORT vcl_result_t VCL_APICALL vclAllocatedExecutableCreate(vcl_compiler_handle_t compiler, vcl_executable_desc_t desc, - const vcl_allocator_t* allocator, + vcl_allocator_t const* allocator, uint8_t** blobBuffer, uint64_t* blobSize); -VCL_APIEXPORT vcl_result_t VCL_APICALL vclAllocatedExecutableCreate2(vcl_compiler_handle_t compiler, +DEPRECATED VCL_APIEXPORT vcl_result_t VCL_APICALL vclAllocatedExecutableCreate2(vcl_compiler_handle_t compiler, + vcl_executable_desc_t desc, + vcl_allocator2_t* allocator, + uint8_t** blobBuffer, + uint64_t* blobSize); + +VCL_APIEXPORT vcl_result_t VCL_APICALL vclAllocatedExecutableCreate3(vcl_compiler_handle_t compiler, vcl_executable_desc_t desc, vcl_allocator2_t* allocator, uint8_t** blobBuffer, - uint64_t* blobSize); + uint64_t* blobSize, + uint8_t** compatibilityStringBuffer, + uint64_t* compatibilityStringSize); VCL_APIEXPORT vcl_result_t VCL_APICALL vclAllocatedExecutableCreateWSOneShot(vcl_compiler_handle_t compiler, vcl_executable_desc_t desc, @@ -283,8 +288,7 @@ VCL_APIEXPORT vcl_result_t VCL_APICALL vclExecutableDestroy(vcl_executable_handl /// @brief If blobBuffer is null, the function returns the size of the blob stored in the executable. /// Otherwise the function copies the executable cached blob to the blobBuffer provided by the caller. VCL_APIEXPORT vcl_result_t VCL_APICALL vclExecutableGetSerializableBlob(vcl_executable_handle_t executable, - uint8_t* blobBuffer, - uint64_t* blobSize); + uint8_t* blobBuffer, uint64_t* blobSize); /////////////////////////////////////////////////////////////////////////////// /// @brief Creates a buffer with decoded profiling info. @@ -325,15 +329,13 @@ VCL_APIEXPORT vcl_result_t VCL_APICALL vclLogHandleGetString(vcl_log_handle_t lo /////////////////////////////////////////////////////////////////////////////// /// @brief Retrieve the list of supported compiler options /// @attention Should be called twice, first time to retrieve data size, second time to get data. -VCL_APIEXPORT vcl_result_t VCL_APICALL vclGetCompilerSupportedOptions(vcl_compiler_handle_t compiler, - char* result, +VCL_APIEXPORT vcl_result_t VCL_APICALL vclGetCompilerSupportedOptions(vcl_compiler_handle_t compiler, char* result, uint64_t* size); /////////////////////////////////////////////////////////////////////////////// /// @brief Verifies if a given config option (or option-value pair) is supported by the compiler VCL_APIEXPORT vcl_result_t VCL_APICALL vclGetCompilerIsOptionSupported(vcl_compiler_handle_t compiler, - const char* option, - const char* value); + const char* option, const char* value); #if defined(__cplusplus) } // extern "C" diff --git a/src/plugins/intel_npu/src/utils/include/intel_npu/utils/vcl/vcl_allocator.hpp b/src/plugins/intel_npu/src/utils/include/intel_npu/utils/vcl/vcl_allocator.hpp new file mode 100644 index 000000000000..4eacb1b74a1a --- /dev/null +++ b/src/plugins/intel_npu/src/utils/include/intel_npu/utils/vcl/vcl_allocator.hpp @@ -0,0 +1,99 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#pragma once + +#include +#include +#include +#include +#include + +#include "intel_npu/utils/utils.hpp" +#include "intel_npu/utils/vcl/vcl.h" +#include "openvino/core/except.hpp" +#include "openvino/runtime/make_tensor.hpp" +#include "openvino/runtime/tensor.hpp" + +namespace intel_npu { + +struct vcl_allocator_3 : vcl_allocator2_t { + vcl_allocator_3() : vcl_allocator2_t{allocate, deallocate}, m_allocator(intel_npu::utils::STANDARD_PAGE_SIZE) {} + + ~vcl_allocator_3() { + for (auto& item : m_info) { + if (item.first) { + m_allocator.deallocate(item.first, item.second, intel_npu::utils::STANDARD_PAGE_SIZE); + } + } + m_info.clear(); + } + + static uint8_t* allocate(vcl_allocator2_t* allocator, size_t size) noexcept { + vcl_allocator_3* vclAllocator = static_cast(allocator); + size_t alignedSize = intel_npu::utils::align_size_to_standard_page_size(size); + + // Prevent integer wraparound on extremely large allocation sizes + if (alignedSize < size) { + return nullptr; + } + + uint8_t* allocatedPtr = nullptr; + try { + allocatedPtr = static_cast( + vclAllocator->m_allocator.allocate(alignedSize, intel_npu::utils::STANDARD_PAGE_SIZE)); + + if (allocatedPtr == nullptr) { + return nullptr; + } + std::memset(allocatedPtr + size, 0, alignedSize - size); + + vclAllocator->m_info.emplace_back(std::make_pair(allocatedPtr, alignedSize)); + return allocatedPtr; + } catch (...) { + if (allocatedPtr != nullptr) { + vclAllocator->m_allocator.deallocate(allocatedPtr, alignedSize, intel_npu::utils::STANDARD_PAGE_SIZE); + } + return nullptr; + } + } + + static void deallocate(vcl_allocator2_t* allocator, uint8_t* ptr) noexcept { + if (ptr == nullptr) { + return; + } + vcl_allocator_3* vclAllocator = static_cast(allocator); + + auto it = std::find_if(vclAllocator->m_info.begin(), + vclAllocator->m_info.end(), + [ptr](const std::pair& item) { + return item.first == ptr; + }); + if (it != vclAllocator->m_info.end()) { + vclAllocator->m_info.erase(it); + } + + // 1 is the placeholder value, as size is not needed in deallocate + vclAllocator->m_allocator.deallocate(ptr, 1, intel_npu::utils::STANDARD_PAGE_SIZE); + } + intel_npu::utils::AlignedAllocator m_allocator; + std::vector> m_info; +}; + +inline ov::Tensor make_tensor_from_aligned_addr(uint8_t* allocated, + size_t size, + std::shared_ptr sourceAllocator) { + auto tensor = ov::Tensor(ov::element::u8, ov::Shape{size}, allocated); + auto impl = ov::get_tensor_impl(std::move(tensor)); + std::shared_ptr ptr(allocated, [sourceAllocator](uint8_t* p) noexcept { + if (p == nullptr) { + return; + } + vcl_allocator_3::deallocate(sourceAllocator.get(), p); + }); + impl._so = std::move(ptr); + return ov::make_tensor(impl); +} + +} // namespace intel_npu diff --git a/src/plugins/intel_npu/src/utils/include/intel_npu/utils/vcl/vcl_api.hpp b/src/plugins/intel_npu/src/utils/include/intel_npu/utils/vcl/vcl_api.hpp index 8e22dbab0912..83a480fd64c5 100644 --- a/src/plugins/intel_npu/src/utils/include/intel_npu/utils/vcl/vcl_api.hpp +++ b/src/plugins/intel_npu/src/utils/include/intel_npu/utils/vcl/vcl_api.hpp @@ -6,9 +6,9 @@ #include -#include "vcl.h" #include "intel_npu/utils/logger/logger.hpp" #include "openvino/core/except.hpp" +#include "vcl.h" namespace intel_npu { // clang-format off @@ -28,7 +28,7 @@ namespace intel_npu { vcl_symbol_statement(vclProfilingDestroy) \ vcl_symbol_statement(vclProfilingGetProperties) \ vcl_symbol_statement(vclLogHandleGetString) \ - vcl_symbol_statement(vclAllocatedExecutableCreate2) \ + vcl_symbol_statement(vclAllocatedExecutableCreate3) \ vcl_symbol_statement(vclGetCompilerSupportedOptions) \ vcl_symbol_statement(vclGetCompilerIsOptionSupported) \ @@ -36,18 +36,19 @@ namespace intel_npu { // symbols that may not be supported in older versions of vcl #define vcl_weak_symbols_list() \ vcl_symbol_statement(vclAllocatedExecutableCreate) \ + vcl_symbol_statement(vclAllocatedExecutableCreate2) \ vcl_symbol_statement(vclAllocatedExecutableCreateWSOneShot) // clang-format on class VCLApi { public: - VCLApi(); + VCLApi(const std::string& library_dir); VCLApi(const VCLApi& other) = delete; VCLApi(VCLApi&& other) = delete; void operator=(const VCLApi&) = delete; void operator=(VCLApi&&) = delete; - static const std::shared_ptr getInstance(); + static const std::shared_ptr getInstance(const std::string& library_dir = std::string()); std::shared_ptr getLibrary() const { return lib; } diff --git a/src/plugins/intel_npu/src/compiler_adapter/include/npu_vm_runtime_api.hpp b/src/plugins/intel_npu/src/utils/include/intel_npu/utils/vm/npu_vm_runtime_api.hpp similarity index 84% rename from src/plugins/intel_npu/src/compiler_adapter/include/npu_vm_runtime_api.hpp rename to src/plugins/intel_npu/src/utils/include/intel_npu/utils/vm/npu_vm_runtime_api.hpp index 321ddfa6fb74..046a1570c772 100644 --- a/src/plugins/intel_npu/src/compiler_adapter/include/npu_vm_runtime_api.hpp +++ b/src/plugins/intel_npu/src/utils/include/intel_npu/utils/vm/npu_vm_runtime_api.hpp @@ -40,6 +40,15 @@ class NPUVMRuntimeApi { ~NPUVMRuntimeApi() = default; + // Must be called before the first getInstance() invocation. + // Re-initialization with the same library is a no-op after the singleton has been created. + // Throws only if re-initialized with a different library after creation. + static void initialize(std::string_view libName); + + // Inspects the blob header to select the appropriate runtime library and calls initialize(). + // Selects "npu_interpreter_runtime" for NPUByte blobs, "npu_mlir_runtime" otherwise. + static void initializeFromBlob(const void* data, size_t size); + static const std::shared_ptr& getInstance(); #define nmr_symbol_statement(symbol) decltype(&::symbol) symbol; diff --git a/src/plugins/intel_npu/src/utils/include/intel_npu/utils/zero/zero_init.hpp b/src/plugins/intel_npu/src/utils/include/intel_npu/utils/zero/zero_init.hpp index 766efc2cc8c6..4fc676a30b62 100644 --- a/src/plugins/intel_npu/src/utils/include/intel_npu/utils/zero/zero_init.hpp +++ b/src/plugins/intel_npu/src/utils/include/intel_npu/utils/zero/zero_init.hpp @@ -6,6 +6,7 @@ #include +#include #include #include #include @@ -41,7 +42,7 @@ class ZeroInitStructsHolder final { return _device_handle; } inline ze_context_handle_t getContext() const { - return _context; + return _context.load(); } inline ze_graph_dditable_ext_curr_t& getGraphDdiTable() const { return *_graph_dditable_ext_decorator; @@ -97,6 +98,8 @@ class ZeroInitStructsHolder final { void initCompilerPropertiesLocked(); void getExtensionFunctionAddress(const std::string& name, const uint32_t version, void** function_address); void setContextProperties(); + void destroyContextLocked(); + static void destroyContextForInstance(std::shared_ptr& instance); inline ZeroMemPool& getZeroMemPool() { return _zero_mem_pool; @@ -107,7 +110,7 @@ class ZeroInitStructsHolder final { Logger _log; - ze_context_handle_t _context = nullptr; + std::atomic _context{nullptr}; ze_driver_handle_t _driver_handle = nullptr; ze_device_handle_t _device_handle = nullptr; diff --git a/src/plugins/intel_npu/src/utils/include/intel_npu/utils/zero/zero_types.hpp b/src/plugins/intel_npu/src/utils/include/intel_npu/utils/zero/zero_types.hpp index a5119d9e0dea..fed548d6dd92 100644 --- a/src/plugins/intel_npu/src/utils/include/intel_npu/utils/zero/zero_types.hpp +++ b/src/plugins/intel_npu/src/utils/include/intel_npu/utils/zero/zero_types.hpp @@ -43,7 +43,7 @@ struct ze_graph_dditable_ext_decorator final { } public: - ze_graph_dditable_ext_t* const getImpl() { + ze_graph_dditable_ext_t* getImpl() { return _impl; } diff --git a/src/plugins/intel_npu/src/utils/include/intel_npu/utils/zero/zero_wrappers.hpp b/src/plugins/intel_npu/src/utils/include/intel_npu/utils/zero/zero_wrappers.hpp index 81beb30eae4d..622f49478a07 100644 --- a/src/plugins/intel_npu/src/utils/include/intel_npu/utils/zero/zero_wrappers.hpp +++ b/src/plugins/intel_npu/src/utils/include/intel_npu/utils/zero/zero_wrappers.hpp @@ -148,6 +148,7 @@ class CommandQueue { CommandQueue& operator=(const CommandQueue&) = delete; CommandQueue& operator=(CommandQueue&&) = delete; + void setWorkloadType(ze_command_queue_workload_type_t workloadType) const; void executeCommandList(CommandList& command_list) const; void executeCommandList(CommandList& command_list, Fence& fence) const; ~CommandQueue(); @@ -157,21 +158,23 @@ class CommandQueue { inline CommandQueueDesc desc() const { return _desc; } + inline ze_context_handle_t getZeroContext() const { + return _init_structs->getContext(); + } private: std::shared_ptr _init_structs; CommandQueueDesc _desc; + ze_command_queue_handle_t _handle = nullptr; Logger _log; - - ze_command_queue_handle_t _handle = nullptr; }; class EventPool { public: EventPool() = delete; - EventPool(ze_device_handle_t device_handle, const ze_context_handle_t& context, uint32_t event_count); + EventPool(const std::shared_ptr& init_structs, uint32_t event_count); EventPool(const EventPool&) = delete; EventPool(EventPool&&) = delete; EventPool& operator=(const EventPool&) = delete; @@ -180,8 +183,13 @@ class EventPool { inline ze_event_pool_handle_t handle() const { return _handle; } + inline ze_context_handle_t getZeroContext() const { + return _init_structs->getContext(); + } private: + std::shared_ptr _init_structs; + ze_event_pool_handle_t _handle = nullptr; Logger _log; diff --git a/src/plugins/intel_npu/src/utils/src/CMakeLists.txt b/src/plugins/intel_npu/src/utils/src/CMakeLists.txt index ec6493205c16..434523deda87 100644 --- a/src/plugins/intel_npu/src/utils/src/CMakeLists.txt +++ b/src/plugins/intel_npu/src/utils/src/CMakeLists.txt @@ -7,4 +7,5 @@ add_subdirectory(logger) if(ENABLE_NPU_PLUGIN_ENGINE) add_subdirectory(vcl) add_subdirectory(zero) + add_subdirectory(vm) endif() diff --git a/src/plugins/intel_npu/src/utils/src/vcl/CMakeLists.txt b/src/plugins/intel_npu/src/utils/src/vcl/CMakeLists.txt index ee78f0c0597d..7d712cae9692 100644 --- a/src/plugins/intel_npu/src/utils/src/vcl/CMakeLists.txt +++ b/src/plugins/intel_npu/src/utils/src/vcl/CMakeLists.txt @@ -12,8 +12,6 @@ set_target_properties( ${TARGET_NAME} PROPERTIES INTERPROCEDURAL_OPTIMIZATION_RELEASE ${ENABLE_LTO}) ov_add_clang_format_target(${TARGET_NAME}_clang FOR_TARGETS ${TARGET_NAME}) -add_library(openvino::npu_vcl_utils ALIAS ${TARGET_NAME}) - target_include_directories(${TARGET_NAME} PUBLIC $ diff --git a/src/plugins/intel_npu/src/utils/src/vcl/vcl_api.cpp b/src/plugins/intel_npu/src/utils/src/vcl/vcl_api.cpp index 86c8b2ba922b..b1eedc0e8a34 100644 --- a/src/plugins/intel_npu/src/utils/src/vcl/vcl_api.cpp +++ b/src/plugins/intel_npu/src/utils/src/vcl/vcl_api.cpp @@ -10,27 +10,16 @@ #include "openvino/util/shared_object.hpp" namespace intel_npu { -VCLApi::VCLApi() : _logger("VCLApi", Logger::global().level()) { - std::filesystem::path baseName = "openvino_intel_npu_compiler_loader"; - std::filesystem::path libpath{}; +VCLApi::VCLApi(const std::string& library_dir) : _logger("VCLApi", Logger::global().level()) { + const auto baseName = "openvino_intel_npu_compiler_loader"; try { - libpath = ov::util::make_plugin_library_name(ov::util::get_ov_lib_path(), baseName); - _logger.debug("Try to load %s", baseName.string().c_str()); + const auto libpath = ov::util::make_plugin_library_name(std::filesystem::path(library_dir), baseName); + _logger.debug("Try to load: %s", ov::util::path_to_string(libpath).c_str()); this->lib = ov::util::load_shared_object(libpath); } catch (const std::runtime_error& error) { - _logger.debug("Failed to load %s: %s", baseName.string().c_str(), error.what()); - - // TODO: remove fallback loading logic after all components switch to "openvino_intel_npu_compiler_loader" - baseName = "openvino_intel_npu_compiler"; - _logger.debug("Trying to load %s", baseName.string().c_str()); - try { - libpath = ov::util::make_plugin_library_name(ov::util::get_ov_lib_path(), baseName); - this->lib = ov::util::load_shared_object(libpath); - } catch (const std::runtime_error& error) { - _logger.debug("Failed to load a fallback library: %s", baseName.string().c_str()); - OPENVINO_THROW(error.what()); - } + _logger.debug("Failed to load %s: %s", baseName, error.what()); + OPENVINO_THROW(error.what()); } try { @@ -39,7 +28,7 @@ VCLApi::VCLApi() : _logger("VCLApi", Logger::global().level()) { vcl_symbols_list(); #undef vcl_symbol_statement } catch (const std::runtime_error& error) { - _logger.debug("Failed to get formal symbols from %s", baseName.string().c_str()); + _logger.debug("Failed to get formal symbols from %s", baseName); OPENVINO_THROW(error.what()); } @@ -47,7 +36,7 @@ VCLApi::VCLApi() : _logger("VCLApi", Logger::global().level()) { try { \ this->vcl_symbol = reinterpret_cast(ov::util::get_symbol(lib, #vcl_symbol)); \ } catch (const std::runtime_error&) { \ - _logger.debug("Failed to get %s from %s", #vcl_symbol, baseName.string().c_str()); \ + _logger.debug("Failed to get %s from %s", #vcl_symbol, baseName); \ this->vcl_symbol = nullptr; \ } vcl_weak_symbols_list(); @@ -59,8 +48,29 @@ VCLApi::VCLApi() : _logger("VCLApi", Logger::global().level()) { #undef vcl_symbol_statement } -const std::shared_ptr VCLApi::getInstance() { - static std::shared_ptr instance = std::make_shared(); +const std::shared_ptr VCLApi::getInstance(const std::string& library_dir) { + static std::mutex mtx; + std::lock_guard lock(mtx); + + static std::string initialized_dir; + static std::shared_ptr instance = nullptr; + + if (!instance) { + if (library_dir.empty()) { + OPENVINO_THROW("VCLApi instance has not been loaded yet, and no valid path was provided to load it."); + } + initialized_dir = library_dir; + instance = std::make_shared(library_dir); + } else { + if (!library_dir.empty() && library_dir != initialized_dir) { + OPENVINO_THROW("VCLApi has already been initialized with path: '", + initialized_dir, + "'. Dynamic switching to a new compiler path: '", + library_dir, + "' in the same process is not supported."); + } + } + return instance; } diff --git a/src/plugins/intel_npu/src/utils/src/vm/CMakeLists.txt b/src/plugins/intel_npu/src/utils/src/vm/CMakeLists.txt new file mode 100644 index 000000000000..6c077c0b1853 --- /dev/null +++ b/src/plugins/intel_npu/src/utils/src/vm/CMakeLists.txt @@ -0,0 +1,30 @@ +# Copyright (C) 2018-2026 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +set(TARGET_NAME openvino_npu_vm_utils) + +file(GLOB_RECURSE SOURCES *.cpp) +source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${SOURCES}) + +add_library(${TARGET_NAME} OBJECT ${SOURCES}) + +set_target_properties(${TARGET_NAME} PROPERTIES + INTERPROCEDURAL_OPTIMIZATION_RELEASE ${ENABLE_LTO} + EXPORT_NAME npu_vm_utils) + +ov_add_clang_format_target(${TARGET_NAME}_clang FOR_TARGETS ${TARGET_NAME}) + +target_include_directories(${TARGET_NAME} + PUBLIC + $ + $ + $ + $ + $ + $ +) + +if(NOT BUILD_SHARED_LIBS) + target_compile_definitions(${TARGET_NAME} PRIVATE OPENVINO_STATIC_LIBRARY) +endif() diff --git a/src/plugins/intel_npu/src/compiler_adapter/src/npu_vm_runtime_api.cpp b/src/plugins/intel_npu/src/utils/src/vm/npu_vm_runtime_api.cpp similarity index 58% rename from src/plugins/intel_npu/src/compiler_adapter/src/npu_vm_runtime_api.cpp rename to src/plugins/intel_npu/src/utils/src/vm/npu_vm_runtime_api.cpp index 4ae9f6dccff0..222f995c399e 100644 --- a/src/plugins/intel_npu/src/compiler_adapter/src/npu_vm_runtime_api.cpp +++ b/src/plugins/intel_npu/src/utils/src/vm/npu_vm_runtime_api.cpp @@ -2,13 +2,20 @@ // SPDX-License-Identifier: Apache-2.0 // -#include "npu_vm_runtime_api.hpp" +#include "intel_npu/utils/vm/npu_vm_runtime_api.hpp" + +#include #include "openvino/util/file_util.hpp" #include "openvino/util/shared_object.hpp" namespace intel_npu { +namespace { +std::string g_libName{"npu_mlir_runtime"}; +bool g_instanceCreated{false}; +} // namespace + NPUVMRuntimeApi::NPUVMRuntimeApi(std::string_view libName) { const std::string baseName = libName.empty() ? "npu_mlir_runtime" : std::string(libName); try { @@ -40,8 +47,33 @@ NPUVMRuntimeApi::NPUVMRuntimeApi(std::string_view libName) { #undef nmr_symbol_statement } +void NPUVMRuntimeApi::initializeFromBlob(const void* data, size_t size) { + const size_t headerSize = std::min(size, size_t{20}); + const std::string_view header(static_cast(data), headerSize); + const std::string_view libName = + (header.find("NPUByte\x00") != std::string_view::npos) ? "npu_interpreter_runtime" : "npu_mlir_runtime"; + initialize(libName); +} + +void NPUVMRuntimeApi::initialize(std::string_view libName) { + const std::string resolvedName = libName.empty() ? "npu_mlir_runtime" : std::string(libName); + if (g_instanceCreated) { + if (g_libName != resolvedName) { + OPENVINO_THROW("NPUVMRuntimeApi is already initialized with '", + g_libName, + "', cannot reinitialize with '", + resolvedName, + "'"); + } + // Same library — idempotent, nothing to do. + return; + } + g_libName = resolvedName; +} + const std::shared_ptr& NPUVMRuntimeApi::getInstance() { - static std::shared_ptr instance = std::make_shared(); + static std::shared_ptr instance = std::make_shared(g_libName); + g_instanceCreated = true; return instance; } diff --git a/src/plugins/intel_npu/src/utils/src/zero/zero_init.cpp b/src/plugins/intel_npu/src/utils/src/zero/zero_init.cpp index 47f0d9840a17..a5248e94f157 100644 --- a/src/plugins/intel_npu/src/utils/src/zero/zero_init.cpp +++ b/src/plugins/intel_npu/src/utils/src/zero/zero_init.cpp @@ -20,7 +20,7 @@ constexpr uint32_t WIN_DRIVER_NO_MCL_SUPPORT = 2688; #endif constexpr uint32_t TARGET_ZE_DRIVER_NPU_EXT_VERSION = ZE_DRIVER_NPU_EXT_VERSION_1_0; -constexpr uint32_t TARGET_ZE_GRAPH_NPU_EXT_VERSION = ZE_GRAPH_EXT_VERSION_1_16; +constexpr uint32_t TARGET_ZE_GRAPH_NPU_EXT_VERSION = ZE_GRAPH_EXT_VERSION_1_18; constexpr uint32_t TARGET_ZE_COMMAND_QUEUE_NPU_EXT_VERSION = ZE_COMMAND_QUEUE_NPU_EXT_VERSION_1_1; constexpr uint32_t TARGET_ZE_PROFILING_NPU_EXT_VERSION = ZE_PROFILING_DATA_EXT_VERSION_1_0; constexpr uint32_t TARGET_ZE_CONTEXT_NPU_EXT_VERSION = ZE_CONTEXT_NPU_EXT_VERSION_1_0; @@ -369,7 +369,9 @@ ZeroInitStructsHolder::ZeroInitStructsHolder() // Create context - share between the compiler and the backend ze_context_desc_t context_desc = {ZE_STRUCTURE_TYPE_CONTEXT_DESC, 0, 0}; - THROW_ON_FAIL_FOR_LEVELZERO("zeContextCreate", zeContextCreate(_driver_handle, &context_desc, &_context)); + ze_context_handle_t ctx = nullptr; + THROW_ON_FAIL_FOR_LEVELZERO("zeContextCreate", zeContextCreate(_driver_handle, &context_desc, &ctx)); + _context.store(ctx); _log.debug("ZeroInitStructsHolder initialize complete"); // Discover if standard allocation is supported @@ -418,13 +420,34 @@ const std::shared_ptr ZeroInitStructsHolder::getInstance( std::lock_guard lock(mutex); auto instance = weak_instance.lock(); + + if (instance) { + // Check if device is still valid + auto res = zeDeviceGetStatus(instance->getDevice()); + if (res == ZE_RESULT_ERROR_DEVICE_LOST) { + destroyContextForInstance(instance); + weak_instance.reset(); + instance.reset(); + } + } + if (!instance) { instance = std::make_shared(); weak_instance = instance; } + return instance; } +void ZeroInitStructsHolder::destroyContextForInstance(std::shared_ptr& instance) { + if (!instance) { + return; + } + + std::lock_guard lock(instance->_mutex); + instance->destroyContextLocked(); +} + ze_device_graph_properties_t ZeroInitStructsHolder::getCompilerProperties() { std::lock_guard lock(_mutex); initCompilerPropertiesLocked(); @@ -439,8 +462,9 @@ void ZeroInitStructsHolder::initCompilerPropertiesLocked() { // Keep optional disengaged unless driver query succeeds. ze_device_graph_properties_t compiler_properties = {}; compiler_properties.stype = ZE_STRUCTURE_TYPE_DEVICE_GRAPH_PROPERTIES; - auto result = _graph_dditable_ext_decorator->pfnDeviceGetGraphProperties(_device_handle, &compiler_properties); - THROW_ON_FAIL_FOR_LEVELZERO("pfnDeviceGetGraphProperties", result); + THROW_ON_FAIL_FOR_LEVELZERO( + "pfnDeviceGetGraphProperties", + _graph_dditable_ext_decorator->pfnDeviceGetGraphProperties(_device_handle, &compiler_properties)); _compiler_properties = compiler_properties; } @@ -475,7 +499,9 @@ void ZeroInitStructsHolder::setContextProperties() { ze_context_properties_npu_ext_t context_properties_npu_ext = {ZE_STRUCTURE_TYPE_CONTEXT_PROPERTIES_NPU_EXT, nullptr, _context_options}; - _context_npu_dditable_ext_decorator->pfnSetProperties(_context, &context_properties_npu_ext); + THROW_ON_FAIL_FOR_LEVELZERO( + "pfnSetProperties", + _context_npu_dditable_ext_decorator->pfnSetProperties(_context.load(), &context_properties_npu_ext)); } void ZeroInitStructsHolder::clearContextOptions(const uint32_t options) { @@ -490,20 +516,27 @@ void ZeroInitStructsHolder::setContextOptions(const uint32_t options) { setContextProperties(); } -ZeroInitStructsHolder::~ZeroInitStructsHolder() { - if (_context) { - _log.debug("ZeroInitStructsHolder - performing zeContextDestroy"); - auto result = zeContextDestroy(_context); - _context = nullptr; - if (result != ZE_RESULT_SUCCESS) { - if (result == ZE_RESULT_ERROR_UNINITIALIZED) { - _log.warning( - "zeContextDestroy failed to destroy the context; Level zero context was already destroyed"); - } else { - _log.error("zeContextDestroy failed %#X", uint64_t(result)); - } - } +void ZeroInitStructsHolder::destroyContextLocked() { + if (!_context.load()) { + return; } + + _log.debug("ZeroInitStructsHolder - performing zeContextDestroy"); + auto result = zeContextDestroy(_context.load()); + if (result == ZE_RESULT_SUCCESS) { + _context.store(nullptr); + _log.debug("ZeroInitStructsHolder - zeContextDestroy succeeded"); + } else if (result == ZE_RESULT_ERROR_UNINITIALIZED) { + _context.store(nullptr); + _log.warning("zeContextDestroy failed to destroy the context; Level zero context was already destroyed"); + } else { + _log.error("zeContextDestroy failed %#X", uint64_t(result)); + } +} + +ZeroInitStructsHolder::~ZeroInitStructsHolder() { + std::lock_guard lock(_mutex); + destroyContextLocked(); } } // namespace intel_npu diff --git a/src/plugins/intel_npu/src/utils/src/zero/zero_mem.cpp b/src/plugins/intel_npu/src/utils/src/zero/zero_mem.cpp index 495385ba58c2..6f85f468272d 100644 --- a/src/plugins/intel_npu/src/utils/src/zero/zero_mem.cpp +++ b/src/plugins/intel_npu/src/utils/src/zero/zero_mem.cpp @@ -48,11 +48,13 @@ ZeroMem::ZeroMem(const std::shared_ptr& init_structs, "Importing standard allocation is not supported if memory is not aligned to standard page size"); } - // We need to check if the end of the current region is part of a previous imported region - // The other cases are handled by the driver - if (zeroUtils::get_l0_context_memory_allocation_id( - _init_structs->getContext(), - static_cast(static_cast(const_cast(data)) + _size)) > 0) { + // Reject the import only when the region genuinely overlaps a previously imported + // allocation. Probe the last valid byte (data + _size - 1) so that a buffer whose end + // merely abuts an adjacent allocation is still importable. Other cases are handled by + // the driver. + if (_size > 0 && zeroUtils::get_l0_context_memory_allocation_id( + _init_structs->getContext(), + static_cast(static_cast(const_cast(data)) + _size - 1)) > 0) { throw ZeroMemException("Can not import a memory which is part of an existing allocation"); } @@ -111,7 +113,14 @@ uint64_t ZeroMem::id() { } ZeroMem::~ZeroMem() { - auto result = zeMemFree(_init_structs->getContext(), _ptr); + auto ze_context = _init_structs->getContext(); + if (ze_context == nullptr) { + _logger.warning("Context is null while trying to free memory with id %llu. Memory might be already freed.", + _id); + return; + } + + auto result = zeMemFree(ze_context, _ptr); if (ZE_RESULT_SUCCESS != result) { _logger.error("L0 zeMemFree result: %s, code %#X - %s", ze_result_to_string(result).c_str(), diff --git a/src/plugins/intel_npu/src/utils/src/zero/zero_wrappers.cpp b/src/plugins/intel_npu/src/utils/src/zero/zero_wrappers.cpp index fc7ae9a94096..dd8f441c31b1 100644 --- a/src/plugins/intel_npu/src/utils/src/zero/zero_wrappers.cpp +++ b/src/plugins/intel_npu/src/utils/src/zero/zero_wrappers.cpp @@ -76,22 +76,31 @@ void CommandQueueDesc::update_key() { _key = hash; } -EventPool::EventPool(ze_device_handle_t device_handle, const ze_context_handle_t& context, uint32_t event_count) - : _log("EventPool", Logger::global().level()) { +EventPool::EventPool(const std::shared_ptr& init_structs, uint32_t event_count) + : _init_structs(init_structs), + _log("EventPool", Logger::global().level()) { ze_event_pool_desc_t event_pool_desc = {ZE_STRUCTURE_TYPE_EVENT_POOL_DESC, nullptr, ZE_EVENT_POOL_FLAG_HOST_VISIBLE, event_count}; - THROW_ON_FAIL_FOR_LEVELZERO("zeEventPoolCreate", - zeEventPoolCreate(context, &event_pool_desc, 1, &device_handle, &_handle)); + auto device_handle = _init_structs->getDevice(); + THROW_ON_FAIL_FOR_LEVELZERO( + "zeEventPoolCreate", + zeEventPoolCreate(_init_structs->getContext(), &event_pool_desc, 1, &device_handle, &_handle)); } EventPool::~EventPool() { + if (_init_structs->getContext() == nullptr || _handle == nullptr) { + _log.warning("Context or EventPool handle is null during destruction. EventPool might be already destroyed."); + _handle = nullptr; + return; + } + auto result = zeEventPoolDestroy(_handle); - if (ZE_RESULT_SUCCESS != result) { + if (ZE_RESULT_SUCCESS == result) { + _handle = nullptr; + } else { _log.error("zeEventPoolDestroy failed %#X", uint64_t(result)); } - - _handle = nullptr; } Event::Event(const std::shared_ptr& event_pool, uint32_t event_index) @@ -119,12 +128,17 @@ void Event::reset() const { THROW_ON_FAIL_FOR_LEVELZERO("zeEventHostReset", zeEventHostReset(_handle)); } Event::~Event() { + if (_event_pool->getZeroContext() == nullptr || _handle == nullptr) { + _log.warning("Context or Event handle is null during destruction. Event might be already destroyed."); + return; + } + auto result = zeEventDestroy(_handle); - if (ZE_RESULT_SUCCESS != result) { + if (ZE_RESULT_SUCCESS == result) { + _handle = nullptr; + } else { _log.error("zeEventDestroy failed %#X", uint64_t(result)); } - - _handle = nullptr; } CommandList::CommandList(const std::shared_ptr& init_structs) @@ -204,12 +218,19 @@ void CommandList::close() const { THROW_ON_FAIL_FOR_LEVELZERO("zeCommandListClose", zeCommandListClose(_handle)); } CommandList::~CommandList() { + if (_init_structs->getContext() == nullptr || _handle == nullptr) { + _log.warning( + "Context or CommandList handle is null during destruction. CommandList might be already destroyed."); + _handle = nullptr; + return; + } + auto result = zeCommandListDestroy(_handle); - if (ZE_RESULT_SUCCESS != result) { + if (ZE_RESULT_SUCCESS == result) { + _handle = nullptr; + } else { _log.error("zeCommandListDestroy failed %#X", uint64_t(result)); } - - _handle = nullptr; } void CommandList::updateMutableCommandList(uint32_t index, const void* data) const { ze_mutable_graph_argument_exp_desc_t desc = {}; @@ -303,13 +324,7 @@ CommandQueue::CommandQueue(const std::shared_ptr& init_st if (_desc.workload().has_value()) { try { - if (_init_structs->getCommandQueueDdiTable().version() >= ZE_MAKE_VERSION(1, 0)) { - THROW_ON_FAIL_FOR_LEVELZERO( - "zeSetWorkloadType", - _init_structs->getCommandQueueDdiTable().pfnSetWorkloadType(_handle, _desc.workload().value())); - } else { - OPENVINO_THROW("The WorkloadType property is not supported by the current Driver Version!"); - } + setWorkloadType(_desc.workload().value()); } catch (...) { auto result = zeCommandQueueDestroy(_handle); _handle = nullptr; @@ -321,6 +336,15 @@ CommandQueue::CommandQueue(const std::shared_ptr& init_st } } } +void CommandQueue::setWorkloadType(ze_command_queue_workload_type_t workloadType) const { + if (_init_structs->getCommandQueueDdiTable().version() >= ZE_MAKE_VERSION(1, 0)) { + THROW_ON_FAIL_FOR_LEVELZERO("zeSetWorkloadType", + _init_structs->getCommandQueueDdiTable().pfnSetWorkloadType(_handle, workloadType)); + } else { + OPENVINO_THROW("The WorkloadType property is not supported by the current Driver Version!"); + } +} + void CommandQueue::executeCommandList(CommandList& command_list) const { THROW_ON_FAIL_FOR_LEVELZERO("zeCommandQueueExecuteCommandLists", zeCommandQueueExecuteCommandLists(_handle, 1, &command_list._handle, nullptr)); @@ -330,12 +354,19 @@ void CommandQueue::executeCommandList(CommandList& command_list, Fence& fence) c zeCommandQueueExecuteCommandLists(_handle, 1, &command_list._handle, fence.handle())); } CommandQueue::~CommandQueue() { + if (_init_structs->getContext() == nullptr || _handle == nullptr) { + _log.warning( + "Context or CommandQueue handle is null during destruction. CommandQueue might be already destroyed."); + _handle = nullptr; + return; + } + auto result = zeCommandQueueDestroy(_handle); - if (ZE_RESULT_SUCCESS != result) { + if (ZE_RESULT_SUCCESS == result) { + _handle = nullptr; + } else { _log.error("zeCommandQueueDestroy failed %#X", uint64_t(result)); } - - _handle = nullptr; } Fence::Fence(const std::shared_ptr& command_queue) @@ -351,12 +382,18 @@ void Fence::hostSynchronize() const { THROW_ON_FAIL_FOR_LEVELZERO("zeFenceHostSynchronize", zeFenceHostSynchronize(_handle, UINT64_MAX)); } Fence::~Fence() { + if (_command_queue->getZeroContext() == nullptr || _handle == nullptr) { + _log.warning("Context or Fence handle is null during destruction. Fence might be already destroyed."); + _handle = nullptr; + return; + } + auto result = zeFenceDestroy(_handle); - if (ZE_RESULT_SUCCESS != result) { + if (ZE_RESULT_SUCCESS == result) { + _handle = nullptr; + } else { _log.error("zeFenceDestroy failed %#X", uint64_t(result)); } - - _handle = nullptr; } } // namespace intel_npu diff --git a/src/plugins/intel_npu/tests/functional/CMakeLists.txt b/src/plugins/intel_npu/tests/functional/CMakeLists.txt index 3287750219b5..9341a4e5bcfa 100644 --- a/src/plugins/intel_npu/tests/functional/CMakeLists.txt +++ b/src/plugins/intel_npu/tests/functional/CMakeLists.txt @@ -36,6 +36,7 @@ ov_add_test_target( # OpenVINO headers need to be added before the NPU headers # Otherwise, the compiler may mistake OpenVINO headers for the NPU headers "${OpenVINO_SOURCE_DIR}/src/tests/functional/base_func_tests/include" + "${OpenVINO_SOURCE_DIR}/src/tests/functional/plugin/shared/include" ${CMAKE_CURRENT_SOURCE_DIR} ${OPTIONAL_FUNC_TESTS_INCLUDES} "${CMAKE_CURRENT_SOURCE_DIR}/shared_tests_instances" @@ -49,6 +50,7 @@ ov_add_test_target( "${OpenVINO_SOURCE_DIR}/src/plugins/intel_npu/src/plugin/src/transformations.cpp" "${OpenVINO_SOURCE_DIR}/src/plugins/intel_npu/src/plugin/src/properties.cpp" "${OpenVINO_SOURCE_DIR}/src/plugins/intel_npu/src/plugin/src/metrics.cpp" + "${OpenVINO_SOURCE_DIR}/src/plugins/intel_npu/src/plugin/src/executor.cpp" LINK_LIBRARIES ${OPTIONAL_FUNC_TESTS_LIBS} openvino::func_test_utils diff --git a/src/plugins/intel_npu/tests/functional/behavior/compiled_model/compatibility_string.cpp b/src/plugins/intel_npu/tests/functional/behavior/compiled_model/compatibility_string.cpp new file mode 100644 index 000000000000..f3781431e0f4 --- /dev/null +++ b/src/plugins/intel_npu/tests/functional/behavior/compiled_model/compatibility_string.cpp @@ -0,0 +1,16 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "common/npu_test_env_cfg.hpp" +#include "compatibility_string.hpp" + +using namespace ov::test::behavior; + +namespace { + +INSTANTIATE_TEST_SUITE_P(smoke_BehaviorTests, + ClassCompatibilityStringTestSuite, + ::testing::Values(ov::test::utils::DEVICE_NPU), + ClassCompatibilityStringTestSuite::getTestCaseName); +} // namespace diff --git a/src/plugins/intel_npu/tests/functional/behavior/compiled_model/compatibility_string.hpp b/src/plugins/intel_npu/tests/functional/behavior/compiled_model/compatibility_string.hpp new file mode 100644 index 000000000000..2fc82bf5da12 --- /dev/null +++ b/src/plugins/intel_npu/tests/functional/behavior/compiled_model/compatibility_string.hpp @@ -0,0 +1,206 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#pragma once + +#include +#include +#include + +#include + +#include "behavior/compiled_model/properties.hpp" +#include "common/npu_test_env_cfg.hpp" +#include "common_test_utils/subgraph_builders/conv_pool_relu.hpp" +#include "openvino/pass/serialize.hpp" +#include "shared_test_classes/base/ov_behavior_test_utils.hpp" + + +using namespace ov::test::behavior; + +namespace { + +// Tests specific for RUNTIME_REQUIREMENTS and COMPATIBILITY_CHECK properties +class ClassCompatibilityStringTestNPU + : public OVCompiledModelPropertiesBase, + public ::testing::WithParamInterface { +protected: + std::string deviceName; + ov::Core core; + +public: + void SetUp() override { + SKIP_IF_CURRENT_TEST_IS_DISABLED(); + OVCompiledModelPropertiesBase::SetUp(); + deviceName = GetParam(); + } + static std::string getTestCaseName(testing::TestParamInfo obj) { + auto targetDevice = obj.param; + std::replace(targetDevice.begin(), targetDevice.end(), ':', '_'); + std::ostringstream result; + static uint8_t testCounter = 0; + result << "_testCounter=" + << std::to_string(testCounter++) + "_"; // used to avoid same names for different tests + result << "targetDevice=" << ov::test::utils::getDeviceNameTestCase(targetDevice) << "_"; + result << "_targetPlatform=" + ov::test::utils::getTestsPlatformFromEnvironmentOr(ov::test::utils::DEVICE_NPU); + return result.str(); + } +}; + +using ClassCompatibilityStringTestSuite = ClassCompatibilityStringTestNPU; + +TEST_P(ClassCompatibilityStringTestSuite, CompatibilityCheckIsSupported) { + std::vector properties; + + // Forcing CIP as the current compiler type + core.set_property(deviceName, ov::intel_npu::compiler_type(ov::intel_npu::CompilerType::PLUGIN)); + + { + OV_ASSERT_NO_THROW(properties = core.get_property(deviceName, ov::supported_properties)); + auto it = find(properties.cbegin(), properties.cend(), ov::compatibility_check); + ASSERT_TRUE(it != properties.cend()); + ASSERT_FALSE(it->is_mutable()); + } + + // Forcing CID as the current compiler type + core.set_property(deviceName, ov::intel_npu::compiler_type(ov::intel_npu::CompilerType::DRIVER)); + + // Test that COMPATIBILITY_CHECK is still present in supported properties when CID is used as the current compiler type + // Even if CID does not support the option, the property should be marked as supported since the plugin will fallback to CIP + { + OV_ASSERT_NO_THROW(properties = core.get_property(deviceName, ov::supported_properties)); + auto it = find(properties.cbegin(), properties.cend(), ov::compatibility_check); + ASSERT_TRUE(it != properties.cend()); + } +} + +TEST_P(ClassCompatibilityStringTestSuite, CompatibilityCheckInvalidArgument) { + // Forcing CIP as the current compiler type + ov::CompatibilityCheck result = ov::CompatibilityCheck::NOT_APPLICABLE; + OV_ASSERT_NO_THROW(result = core.get_property(deviceName, ov::compatibility_check)); + ASSERT_TRUE(result == ov::CompatibilityCheck::NOT_APPLICABLE); + + // Provide an argument without runtime_requirements + OV_ASSERT_NO_THROW(result = core.get_property(deviceName, ov::compatibility_check, ov::log::level(ov::log::Level::ERR))); + ASSERT_TRUE(result == ov::CompatibilityCheck::NOT_APPLICABLE); + + // An incorrect runtime_requirements argument should return UNSUPPORTED + OV_ASSERT_NO_THROW(result = core.get_property(deviceName, ov::compatibility_check, std::make_pair(ov::runtime_requirements.name(), "invalid_string"))); + ASSERT_TRUE(result == ov::CompatibilityCheck::UNSUPPORTED); +} + +TEST_P(ClassCompatibilityStringTestSuite, RuntimeRequirementsIsSupported) { + // Forcing CIP as the current compiler type + auto model = ov::test::utils::make_conv_pool_relu(); + ov::CompiledModel compiledModel; + OV_ASSERT_NO_THROW(compiledModel = core.compile_model( + model, deviceName, + {ov::intel_npu::compiler_type(ov::intel_npu::CompilerType::PLUGIN), + ov::intel_npu::platform(ov::intel_npu::Platform::standardize( + ov::test::utils::getTestsPlatformFromEnvironmentOr(ov::test::utils::DEVICE_NPU)))})); + + std::vector properties; + // Test that RUNTIME_REQUIREMENTS is supported for a model compiled with CIP + OV_ASSERT_NO_THROW(properties = compiledModel.get_property(ov::supported_properties)); + { + auto it = find(properties.cbegin(), properties.cend(), ov::runtime_requirements); + ASSERT_TRUE(it != properties.cend()); + ASSERT_FALSE(it->is_mutable()); + } + OV_ASSERT_NO_THROW(auto requirements = compiledModel.get_property(ov::runtime_requirements)); + + OV_ASSERT_NO_THROW(compiledModel = core.compile_model(model, deviceName, ov::intel_npu::compiler_type(ov::intel_npu::CompilerType::DRIVER))); + // Test that RUNTIME_REQUIREMENTS is not supported for a model compiled with CID + // This check should be conditioned by the compiler/driver version once support is added in L0 + OV_ASSERT_NO_THROW(properties = compiledModel.get_property(ov::supported_properties)); + { + auto it = find(properties.cbegin(), properties.cend(), ov::runtime_requirements); + ASSERT_TRUE(it == properties.cend()); + } + OV_EXPECT_THROW(auto requirements = compiledModel.get_property(ov::runtime_requirements), ov::Exception, testing::HasSubstr("Unsupported configuration key: RUNTIME_REQUIREMENTS")); + +} + +TEST_P(ClassCompatibilityStringTestSuite, RuntimeRequirementsIsNotSupportedForWS) { + // Preparing the model for the test + std::stringstream model_xml, model_bin; + { + // Serialize generated model into stringstream to later populate `WeightlessCacheAttribute` runtime information + // of constant nodes + auto model = ov::test::utils::make_conv_pool_relu(); + ov::pass::Serialize serializer(model_xml, model_bin); + serializer.run_on_model(model); + } + auto model_bin_str = model_bin.str(); + ov::Tensor model_weights(ov::element::u8, ov::Shape{model_bin_str.size()}); + std::memcpy(model_weights.data(), model_bin_str.data(), model_bin_str.size()); + auto model = core.read_model(model_xml.str(), model_weights); + + ov::CompiledModel compiledModel; + OV_ASSERT_NO_THROW(compiledModel = core.compile_model( + model, deviceName, + {ov::intel_npu::compiler_type(ov::intel_npu::CompilerType::PLUGIN), + ov::intel_npu::platform(ov::intel_npu::Platform::standardize( + ov::test::utils::getTestsPlatformFromEnvironmentOr(ov::test::utils::DEVICE_NPU))), + ov::enable_weightless(true)})); + + std::vector properties; + // Test that RUNTIME_REQUIREMENTS is not supported for a weightless model + OV_EXPECT_THROW(auto requirements = compiledModel.get_property(ov::runtime_requirements), ov::Exception, testing::HasSubstr("Unsupported configuration key: RUNTIME_REQUIREMENTS")); + + // Test that RUNTIME_REQUIREMENTS is not in the list of supported properties either + OV_ASSERT_NO_THROW(properties = compiledModel.get_property(ov::supported_properties)); + auto it = find(properties.cbegin(), properties.cend(), ov::runtime_requirements); + ASSERT_TRUE(it == properties.cend()); +} + +TEST_P(ClassCompatibilityStringTestSuite, RuntimeRequirementsExportImport) { + // Forcing CIP as the current compiler type + auto model = ov::test::utils::make_conv_pool_relu(); + ov::CompiledModel compiledModel; + OV_ASSERT_NO_THROW(compiledModel = core.compile_model( + model, deviceName, + {ov::intel_npu::compiler_type(ov::intel_npu::CompilerType::PLUGIN), + ov::intel_npu::platform(ov::intel_npu::Platform::standardize( + ov::test::utils::getTestsPlatformFromEnvironmentOr(ov::test::utils::DEVICE_NPU)))})); + std::string reference_requirements; + OV_ASSERT_NO_THROW(reference_requirements = compiledModel.get_property(ov::runtime_requirements)); + + std::stringstream compiled_blob; + OV_ASSERT_NO_THROW(compiledModel.export_model(compiled_blob)); + + OV_ASSERT_NO_THROW(compiledModel = {}); + OV_ASSERT_NO_THROW(compiledModel = core.import_model(compiled_blob, deviceName)); + + std::vector properties; + // Test that RUNTIME_REQUIREMENTS is supported for an imported model as well + OV_ASSERT_NO_THROW(properties = compiledModel.get_property(ov::supported_properties)); + auto it = find(properties.cbegin(), properties.cend(), ov::runtime_requirements); + ASSERT_TRUE(it != properties.cend()); + std::string imported_requirements; + OV_ASSERT_NO_THROW(imported_requirements = compiledModel.get_property(ov::runtime_requirements)); + + // The equality must be guaranteed for a given openvino version + // If the blob was exported with a different OV version, requirements might differ + ASSERT_EQ(reference_requirements, imported_requirements); +} + +TEST_P(ClassCompatibilityStringTestSuite, CompatibilityStringGenerateAndCheck) { + // Forcing CIP as the current compiler type + auto model = ov::test::utils::make_conv_pool_relu(); + ov::CompiledModel compiledModel; + OV_ASSERT_NO_THROW(compiledModel = core.compile_model( + model, deviceName, + {ov::intel_npu::compiler_type(ov::intel_npu::CompilerType::PLUGIN), + ov::intel_npu::platform(ov::intel_npu::Platform::standardize( + ov::test::utils::getTestsPlatformFromEnvironmentOr(ov::test::utils::DEVICE_NPU)))})); + + std::string requirements; + OV_ASSERT_NO_THROW(requirements = compiledModel.get_property(ov::runtime_requirements)); + ov::CompatibilityCheck result = ov::CompatibilityCheck::NOT_APPLICABLE; + OV_ASSERT_NO_THROW(result = core.get_property(deviceName, ov::compatibility_check, std::make_pair(ov::runtime_requirements.name(), requirements))); + ASSERT_TRUE(result == ov::CompatibilityCheck::SUPPORTED); +} + +} // namespace diff --git a/src/plugins/intel_npu/tests/functional/behavior/compiled_model/import_export.cpp b/src/plugins/intel_npu/tests/functional/behavior/compiled_model/import_export.cpp index aa39e52b2210..3be9382431aa 100644 --- a/src/plugins/intel_npu/tests/functional/behavior/compiled_model/import_export.cpp +++ b/src/plugins/intel_npu/tests/functional/behavior/compiled_model/import_export.cpp @@ -6,6 +6,8 @@ #include +#include + #include "common/utils.hpp" using namespace ov::test::behavior; diff --git a/src/plugins/intel_npu/tests/functional/behavior/compiled_model/import_export.hpp b/src/plugins/intel_npu/tests/functional/behavior/compiled_model/import_export.hpp index 3e2312991183..014fb764ab54 100644 --- a/src/plugins/intel_npu/tests/functional/behavior/compiled_model/import_export.hpp +++ b/src/plugins/intel_npu/tests/functional/behavior/compiled_model/import_export.hpp @@ -7,9 +7,11 @@ #include #include +#include "common/npu_test_env_cfg.hpp" #include "common_test_utils/subgraph_builders/conv_pool_relu.hpp" #include "intel_npu/npu_private_properties.hpp" #include "openvino/runtime/make_tensor.hpp" +#include "openvino/util/codec_xor.hpp" namespace ov { @@ -68,20 +70,23 @@ TEST_P(OVCompiledGraphImportExportTestNPU, CanImportModelWithApplicationHeaderAn } } -TEST_P(OVCompiledGraphImportExportTestNPU, CheckSizeOfExportedModelIfMultipleOfPageSize) { +TEST_P(OVCompiledGraphImportExportTestNPU, CheckSizeOfRawBlobIfMultipleOfPageSize) { ov::Core core; std::stringstream sstream; + auto rawBlobConfig = configuration; + rawBlobConfig.emplace(ov::intel_npu::export_raw_blob(true)); + auto model = ov::test::utils::make_conv_pool_relu(); - core.compile_model(model, target_device, configuration).export_model(sstream); + core.compile_model(model, target_device, rawBlobConfig).export_model(sstream); std::size_t size = sstream.str().size(); - ASSERT_TRUE(size != 0) << "Size of the exported model shall be different from 0"; - ASSERT_TRUE(size % 4096 == 0) << "Size of the exported model shall be multiple of 4096"; + ASSERT_TRUE(size != 0) << "Size of the blob should be different from 0"; + ASSERT_TRUE(size % 4096 == 0) << "Size of the blob should be multiple of 4096"; } -TEST_P(OVCompiledGraphImportExportTestNPU, CheckSizeOfBlobIfMultipleOfPageSize) { +TEST_P(OVCompiledGraphImportExportTestNPU, CheckSizeOfExportedModelIfMultipleOfPageSize) { ov::Core core; std::stringstream sstream; @@ -98,6 +103,143 @@ TEST_P(OVCompiledGraphImportExportTestNPU, CheckSizeOfBlobIfMultipleOfPageSize) ASSERT_TRUE(size_of_blob % 4096 == 0) << "Size of the blob shall be multiple of 4096"; } +TEST_P(OVCompiledGraphImportExportTestNPU, SameBlobAfterImportExport) { + ov::Core core; + std::stringstream blob_stream, test_blob_stream_1, test_blob_stream_2; + + auto model = ov::test::utils::make_conv_pool_relu(); + core.compile_model(model, target_device, configuration).export_model(blob_stream); + configuration.insert(ov::intel_npu::defer_weights_load(true)); + + core.import_model(blob_stream, target_device, configuration).export_model(test_blob_stream_1); + ASSERT_EQ(blob_stream.str(), test_blob_stream_1.str()); + + auto blob_str = blob_stream.str(); + ov::Tensor blob_tensor(ov::element::u8, ov::Shape{blob_str.size()}, blob_str.c_str()); + core.import_model(blob_tensor, target_device, configuration).export_model(test_blob_stream_2); + ASSERT_EQ(blob_stream.str(), test_blob_stream_2.str()); +} + +TEST_P(OVCompiledGraphImportExportTestNPU, ImportingEncryptedBlobThrows) { + ov::Core core; + std::stringstream encrypted_blob_stream; + + auto model = ov::test::utils::make_conv_pool_relu(); + configuration.insert(ov::cache_encryption_callbacks(ov::EncryptionCallbacks{ov::util::codec_xor, nullptr})); + core.compile_model(model, target_device, configuration).export_model(encrypted_blob_stream); + auto encrypted_blob_str = encrypted_blob_stream.str(); + ov::Tensor encrypted_blob_tensor(ov::element::u8, ov::Shape{encrypted_blob_str.size()}, encrypted_blob_str.c_str()); + configuration.erase(ov::cache_encryption_callbacks.name()); + + OV_EXPECT_THROW(core.import_model(encrypted_blob_stream, target_device, configuration), + ov::Exception, + ::testing::HasSubstr("Blob is encrypted, but no decryption callback was provided")); + + OV_EXPECT_THROW(core.import_model(encrypted_blob_tensor, target_device, configuration), + ov::Exception, + ::testing::HasSubstr("Blob is encrypted, but no decryption callback was provided")); + + encrypted_blob_stream.seekg(0, std::ios::beg); + + // Parsing corrupted blob on MTL will throw Access Violation 0xC0000005 SEH exceptions + if (ov::intel_npu::Platform::standardize(ov::test::utils::getTestPlatform()) != ov::intel_npu::Platform::NPU3720) { + configuration.insert(ov::intel_npu::import_raw_blob(true)); + OV_EXPECT_THROW(core.import_model(encrypted_blob_stream, target_device, configuration), + ov::Exception, + ::testing::HasSubstr("ZE_RESULT_ERROR_INVALID_NATIVE_BINARY")); + + OV_EXPECT_THROW(core.import_model(encrypted_blob_tensor, target_device, configuration), + ov::Exception, + ::testing::HasSubstr("ZE_RESULT_ERROR_INVALID_NATIVE_BINARY")); + } +} + +TEST_P(OVCompiledGraphImportExportTestNPU, SameUnencryptedBlobAfterDecryption) { + ov::Core core; + std::stringstream unencrypted_blob_stream, encrypted_blob_stream, decrypted_blob_stream; + + auto model = ov::test::utils::make_conv_pool_relu(); + core.compile_model(model, target_device, configuration).export_model(unencrypted_blob_stream); + configuration.insert( + ov::cache_encryption_callbacks(ov::EncryptionCallbacks{ov::util::codec_xor, ov::util::codec_xor})); + configuration.insert(ov::intel_npu::defer_weights_load(true)); + core.import_model(unencrypted_blob_stream, target_device, configuration).export_model(encrypted_blob_stream); + configuration.erase(ov::cache_encryption_callbacks.name()); + configuration.insert(ov::cache_encryption_callbacks(ov::EncryptionCallbacks{nullptr, ov::util::codec_xor})); + + core.import_model(encrypted_blob_stream, target_device, configuration).export_model(decrypted_blob_stream); + ASSERT_EQ(unencrypted_blob_stream.str(), decrypted_blob_stream.str()); + + decrypted_blob_stream.str(std::string()); + auto encrypted_blob_str = encrypted_blob_stream.str(); + ov::Tensor encrypted_blob_tensor(ov::element::u8, ov::Shape{encrypted_blob_str.size()}, encrypted_blob_str.c_str()); + core.import_model(encrypted_blob_tensor, target_device, configuration).export_model(decrypted_blob_stream); + ASSERT_EQ(unencrypted_blob_stream.str(), decrypted_blob_stream.str()); +} + +TEST_P(OVCompiledGraphImportExportTestNPU, SameEncryptedBlobViaExportAndManualFunctionCall) { + ov::Core core; + std::stringstream unencrypted_blob_stream, encrypted_blob_stream; + + auto model = ov::test::utils::make_conv_pool_relu(); + // metadata is not encrypted, exclude it from blob + configuration.insert(ov::intel_npu::import_raw_blob(true)); + configuration.insert(ov::intel_npu::export_raw_blob(true)); + + configuration.insert(ov::intel_npu::defer_weights_load(true)); + + core.compile_model(model, target_device, configuration).export_model(unencrypted_blob_stream); + configuration.insert(ov::cache_encryption_callbacks(ov::EncryptionCallbacks{ov::util::codec_xor, nullptr})); + core.import_model(unencrypted_blob_stream, target_device, configuration).export_model(encrypted_blob_stream); + + std::string manual_encrypted_blob_str = ov::util::codec_xor(unencrypted_blob_stream.str()); + std::string encrypted_blob_str = encrypted_blob_stream.str(); + + ASSERT_EQ(manual_encrypted_blob_str, encrypted_blob_str); +} + +TEST_P(OVCompiledGraphImportExportTestNPU, DifferentSizesOfEncryptedVsDecryptedBlobWorks) { + ov::Core core; + std::stringstream encrypted_blob_stream; + + std::stringstream model_xml, model_bin; + { + // Serialize generated model into stringstream to later populate `WeightlessCacheAttribute` runtime information + // of constant nodes + auto model = ov::test::utils::make_conv_pool_relu(); + ov::pass::Serialize serializer(model_xml, model_bin); + serializer.run_on_model(model); + } + auto model_bin_str = model_bin.str(); + ov::Tensor model_weights(ov::element::u8, ov::Shape{model_bin_str.size()}); + std::memcpy(model_weights.data(), model_bin_str.data(), model_bin_str.size()); + auto model = core.read_model(model_xml.str(), model_weights); + + configuration.insert(ov::cache_encryption_callbacks( + ov::EncryptionCallbacks{[](const std::string& unencrypted_blob) { + std::string copy_blob = unencrypted_blob; + copy_blob += ""; + return ov::util::codec_xor(copy_blob); + }, + [](const std::string& encrypted_blob) { + std::string decrypted_blob = ov::util::codec_xor(encrypted_blob); + decrypted_blob += ""; + return decrypted_blob; + }})); + + auto supported_properties = core.get_property(target_device, ov::supported_properties); + if (std::find(supported_properties.begin(), supported_properties.end(), ov::enable_weightless.name()) != + supported_properties.end()) { + configuration.insert(ov::enable_weightless(true)); + } + OV_ASSERT_NO_THROW(core.compile_model(model, target_device, configuration).export_model(encrypted_blob_stream)); + + auto encrypted_blob_str = encrypted_blob_stream.str(); + ov::Tensor encrypted_blob_tensor(ov::element::u8, ov::Shape{encrypted_blob_str.size()}, encrypted_blob_str.c_str()); + OV_ASSERT_NO_THROW(core.import_model(encrypted_blob_stream, target_device, configuration)); + OV_ASSERT_NO_THROW(core.import_model(encrypted_blob_tensor, target_device, configuration)); +} + } // namespace behavior } // namespace test diff --git a/src/plugins/intel_npu/tests/functional/behavior/compiled_model/model_cache.cpp b/src/plugins/intel_npu/tests/functional/behavior/compiled_model/model_cache.cpp new file mode 100644 index 000000000000..9137a48c5c5b --- /dev/null +++ b/src/plugins/intel_npu/tests/functional/behavior/compiled_model/model_cache.cpp @@ -0,0 +1,66 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "model_cache.hpp" + +#include + +#include "common/utils.hpp" + +using namespace ov::test::behavior; + +namespace { + +std::vector config = { + {}, + {ov::enable_weightless(true), + ov::cache_mode(ov::CacheMode::OPTIMIZE_SIZE), + ov::intel_npu::compiler_type(ov::intel_npu::CompilerType::PLUGIN), // only CIP is allowed for ONE_SHOT + ov::intel_npu::platform(ov::intel_npu::Platform::standardize( + ov::test::utils::getTestsPlatformFromEnvironmentOr(ov::test::utils::DEVICE_NPU))), // platform needed for NPU_COMPILER_TYPE_PLUGIN + ov::intel_npu::separate_weights_version(ov::intel_npu::WSVersion::ONE_SHOT), + ov::enable_mmap(false)}, // enable_mmap for both `import_model(stream)` and `import_model(tensor)` paths + {ov::enable_weightless(true), + ov::cache_mode(ov::CacheMode::OPTIMIZE_SIZE), + ov::intel_npu::compiler_type(ov::intel_npu::CompilerType::PLUGIN), + ov::intel_npu::platform(ov::intel_npu::Platform::standardize( + ov::test::utils::getTestsPlatformFromEnvironmentOr(ov::test::utils::DEVICE_NPU))), // platform needed for NPU_COMPILER_TYPE_PLUGIN + ov::intel_npu::separate_weights_version(ov::intel_npu::WSVersion::ONE_SHOT), + ov::enable_mmap(true)}, + {ov::enable_weightless(true), + ov::cache_mode(ov::CacheMode::OPTIMIZE_SIZE), + ov::intel_npu::compiler_type(ov::intel_npu::CompilerType::DRIVER), + ov::intel_npu::separate_weights_version(ov::intel_npu::WSVersion::ITERATIVE), + ov::enable_mmap(false)}, + {ov::enable_weightless(true), + ov::cache_mode(ov::CacheMode::OPTIMIZE_SIZE), + ov::intel_npu::compiler_type(ov::intel_npu::CompilerType::DRIVER), + ov::intel_npu::separate_weights_version(ov::intel_npu::WSVersion::ITERATIVE), + ov::enable_mmap(true)}, + {ov::enable_weightless(true), + ov::cache_mode(ov::CacheMode::OPTIMIZE_SIZE), + ov::intel_npu::compiler_type(ov::intel_npu::CompilerType::PLUGIN), + ov::intel_npu::platform(ov::intel_npu::Platform::standardize( + ov::test::utils::getTestsPlatformFromEnvironmentOr(ov::test::utils::DEVICE_NPU))), // platform needed for NPU_COMPILER_TYPE_PLUGIN + ov::intel_npu::separate_weights_version(ov::intel_npu::WSVersion::ITERATIVE), + ov::enable_mmap(false)}, + {ov::enable_weightless(true), + ov::cache_mode(ov::CacheMode::OPTIMIZE_SIZE), + ov::intel_npu::compiler_type(ov::intel_npu::CompilerType::PLUGIN), + ov::intel_npu::platform(ov::intel_npu::Platform::standardize( + ov::test::utils::getTestsPlatformFromEnvironmentOr(ov::test::utils::DEVICE_NPU))), // platform needed for NPU_COMPILER_TYPE_PLUGIN + ov::intel_npu::separate_weights_version(ov::intel_npu::WSVersion::ITERATIVE), + ov::enable_mmap(true)}}; + +} // namespace + +INSTANTIATE_TEST_SUITE_P(smoke_BehaviorTests, + OVWeightlessCacheAccuracyNPU, + ::testing::Combine(::testing::ValuesIn({true, false}), // m_use_compile_model_api + ::testing::Values(true), // m_do_encryption + ::testing::Values(ov::element::f16), // m_inference_mode + ::testing::Values(ov::element::f16), // m_model_dtype + ::testing::ValuesIn(config), // config parsed with std::ignore + ::testing::Values(ov::test::utils::DEVICE_NPU)), // m_target_device + ov::test::utils::appendPlatformTypeTestName); diff --git a/src/plugins/intel_npu/tests/functional/behavior/compiled_model/model_cache.hpp b/src/plugins/intel_npu/tests/functional/behavior/compiled_model/model_cache.hpp new file mode 100644 index 000000000000..56da09e5276f --- /dev/null +++ b/src/plugins/intel_npu/tests/functional/behavior/compiled_model/model_cache.hpp @@ -0,0 +1,192 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#pragma once + +#include +#include + +#include "common_test_utils/test_assertions.hpp" +#include "functional_test_utils/skip_tests_config.hpp" +#include "openvino/core/model_util.hpp" +#include "openvino/core/rt_info/weightless_caching_attributes.hpp" +#include "openvino/op/add.hpp" +#include "openvino/op/constant.hpp" +#include "openvino/op/multiply.hpp" +#include "openvino/op/parameter.hpp" +#include "openvino/op/reshape.hpp" +#include "openvino/openvino.hpp" + +namespace { + +std::shared_ptr createTestModel(const bool addWeightlessCacheAttribute = true, + const bool alternativeWeights = false) { + constexpr auto precision = ov::element::f32; + + const float weightsValue = !alternativeWeights ? 1.0f : 2.0f; + auto weights = std::make_shared(precision, ov::Shape{5}, std::vector{weightsValue}); + auto input = std::make_shared(precision, ov::Shape{1}); + auto add = std::make_shared(input, weights); + + weights->set_friendly_name("weights"); + input->set_friendly_name("input"); + add->set_friendly_name("add"); + + if (addWeightlessCacheAttribute) { + weights->get_rt_info()[ov::WeightlessCacheAttribute::get_type_info_static()] = + ov::WeightlessCacheAttribute(weights->get_byte_size(), 0, weights->get_element_type()); + } + + auto model = std::make_shared(ov::OutputVector{add}, ov::ParameterVector{input}, "Simple with weights"); + ov::util::set_tensors_names(ov::AUTO, *model, {}, {{0, {"add"}}}); + return model; +} + +// This is a special model that has weightless constants that are guaranteed +// to be skipped by weights schedule. This tests cases where compiler +// produces "blob with weights" when "weightless blob" is requested: in +// theory, this may happen, and must not cause any errors. +std::shared_ptr createTestModelWeightlessWithDummyConstants() { + constexpr auto precision = ov::element::f32; + + const auto reshapeWeights = + std::make_shared(ov::element::i64, ov::Shape{3}, std::vector{1, 2, 3}); + + const auto input1 = std::make_shared(precision, ov::Shape{6}); + const auto input2 = std::make_shared(precision, ov::Shape{1, 2, 3}); + const auto reshapedInput1 = std::make_shared(input1, reshapeWeights, /*special_zero=*/false); + auto add = std::make_shared(reshapedInput1, input2); + + reshapeWeights->set_friendly_name("weights"); + input1->set_friendly_name("input1"); + input2->set_friendly_name("input2"); + reshapedInput1->set_friendly_name("reshapedInput1"); + add->set_friendly_name("add"); + + // Note: Reshape weights with weightless cache attribute satisfy the + // basic requirement to create weights schedule. However, since this is + // a static reshape, these weights would "disappear" during compilation, + // causing the compiler to put nothing into the weights schedule. + reshapeWeights->get_rt_info()[ov::WeightlessCacheAttribute::get_type_info_static()] = + ov::WeightlessCacheAttribute(reshapeWeights->get_byte_size(), 0, reshapeWeights->get_element_type()); + + auto model = std::make_shared(ov::OutputVector{add}, + ov::ParameterVector{input1, input2}, + "Dummy weightless model"); + ov::util::set_tensors_names(ov::AUTO, *model, {}, {{0, {"add"}}}); + return model; +} + +/** + * @brief This model was fine-tuned in order to compile fast and yield a light init schedule. + */ +std::shared_ptr createTestModelLightInitSchedule() { + ov::ParameterVector parameter_vector; + auto data = std::make_shared(ov::element::f16, ov::Shape{1}); + parameter_vector.push_back(data); + auto add_constant = ov::op::v0::Constant::create(ov::element::f16, ov::Shape{50}, {0.5}); + auto add = std::make_shared(data, add_constant); + + for (int i = 0; i < 10; i++) { + auto intermediate_input = std::make_shared(ov::element::f16, ov::Shape{1}); + parameter_vector.push_back(intermediate_input); + add_constant = ov::op::v0::Constant::create(ov::element::f16, ov::Shape{10, 10, 20, 50}, {i}); + add_constant->get_rt_info()[ov::WeightlessCacheAttribute::get_type_info_static()] = + ov::WeightlessCacheAttribute(add_constant->get_byte_size(), i, add_constant->get_element_type()); + add = std::make_shared(add, intermediate_input); + add = std::make_shared(add, add_constant); + } + + return std::make_shared(ov::OutputVector{add}, parameter_vector); +} + +// This is a special test model with multiple constants using the same weights buffer. +std::shared_ptr createTestModelWeightlessWithDuplicateConstants() { + const std::vector sharedData{1.0f, 2.0f, 3.0f, 4.0f}; + constexpr auto precision = ov::element::f32; + + auto input1 = std::make_shared(precision, ov::Shape{1, 1, 4}); + auto input2 = std::make_shared(precision, ov::Shape{1, 4, 1}); + + auto weights1 = std::make_shared(precision, ov::Shape{1, 1, 4}, sharedData); + auto multiply1 = std::make_shared(input1, weights1); + + auto weights2 = std::make_shared(precision, ov::Shape{1, 4, 1}, sharedData); + auto multiply2 = std::make_shared(input2, weights2); + + auto reshapeWeights = + std::make_shared(ov::element::i64, ov::Shape{3}, std::vector{1, 4, 1}); + auto reshape = std::make_shared(multiply1, reshapeWeights, false); + + auto add = std::make_shared(reshape, multiply2); + + input1->set_friendly_name("input1"); + input2->set_friendly_name("input2"); + weights1->set_friendly_name("weights"); + multiply1->set_friendly_name("multiply1"); + weights2->set_friendly_name("weights_new_shape"); + multiply2->set_friendly_name("multiply2"); + reshapeWeights->set_friendly_name("reshapeWeights"); + reshape->set_friendly_name("reshape"); + add->set_friendly_name("add"); + + // Note: if this offset is changed, compiled_model->export_model() => + // core->import_model() would fail as the weightless bin offset is + // "reset" through this boundary; right now this is by design + constexpr size_t theOnlyFunctioningBinOffset = 0; + weights1->get_rt_info()[ov::WeightlessCacheAttribute::get_type_info_static()] = + ov::WeightlessCacheAttribute(weights1->get_byte_size(), + theOnlyFunctioningBinOffset, + weights1->get_element_type()); + weights2->get_rt_info()[ov::WeightlessCacheAttribute::get_type_info_static()] = + ov::WeightlessCacheAttribute(weights2->get_byte_size(), + theOnlyFunctioningBinOffset, + weights2->get_element_type()); + + auto model = std::make_shared(ov::OutputVector{add}, + ov::ParameterVector{input1, input2}, + "duplicate_weights_model"); + ov::util::set_tensors_names(ov::AUTO, *model, {}, {{0, {"add"}}}); + return model; +} + +} // namespace + +namespace ov { + +namespace test { + +namespace behavior { + +using OVWeightlessCacheAccuracyNPU = WeightlessCacheAccuracy; + +TEST_P(OVWeightlessCacheAccuracyNPU, SimpleTestModel) { + SKIP_IF_CURRENT_TEST_IS_DISABLED() + OV_ASSERT_NO_THROW(m_model = createTestModel()); + OV_ASSERT_NO_THROW(run()); +} + +TEST_P(OVWeightlessCacheAccuracyNPU, TestModelWeightlessWithDummyConstants) { + SKIP_IF_CURRENT_TEST_IS_DISABLED() + OV_ASSERT_NO_THROW(m_model = createTestModelWeightlessWithDummyConstants()); + OV_ASSERT_NO_THROW(run()); +} + +TEST_P(OVWeightlessCacheAccuracyNPU, TestModelLightInitSchedule) { + SKIP_IF_CURRENT_TEST_IS_DISABLED() + OV_ASSERT_NO_THROW(m_model = createTestModelLightInitSchedule()); + OV_ASSERT_NO_THROW(run()); +} + +TEST_P(OVWeightlessCacheAccuracyNPU, TestModelWeightlessWithDuplicateConstants) { + SKIP_IF_CURRENT_TEST_IS_DISABLED() + OV_ASSERT_NO_THROW(m_model = createTestModelWeightlessWithDuplicateConstants()); + OV_ASSERT_NO_THROW(run()); +} + +} // namespace behavior + +} // namespace test + +} // namespace ov diff --git a/src/plugins/intel_npu/tests/functional/behavior/compiled_model/property.cpp b/src/plugins/intel_npu/tests/functional/behavior/compiled_model/property.cpp index 2de8235f0067..3734e18a68f3 100644 --- a/src/plugins/intel_npu/tests/functional/behavior/compiled_model/property.cpp +++ b/src/plugins/intel_npu/tests/functional/behavior/compiled_model/property.cpp @@ -5,6 +5,7 @@ #include "property.hpp" #include +#include #include #include "common/npu_test_env_cfg.hpp" @@ -15,14 +16,16 @@ namespace { std::vector> exe_network_supported_properties = { {ov::hint::num_requests.name(), ov::Any(8)}, - {ov::hint::enable_cpu_pinning.name(), ov::Any(true)}, {ov::hint::performance_mode.name(), ov::Any(ov::hint::PerformanceMode::THROUGHPUT)}, {ov::optimal_number_of_infer_requests.name(), ov::Any(2)}, }; +std::vector> exe_network_public_mutable_properties = { + {ov::cache_encryption_callbacks.name(), ov::Any(ov::EncryptionCallbacks{ov::util::codec_xor, ov::util::codec_xor})}, +}; + std::vector> exe_network_immutable_properties = { {std::make_pair(ov::optimal_number_of_infer_requests.name(), ov::Any(2))}, - {std::make_pair(ov::hint::enable_cpu_pinning.name(), ov::Any(false))}, {std::make_pair(ov::supported_properties.name(), ov::Any("deadbeef"))}, {std::make_pair(ov::model_name.name(), ov::Any("deadbeef"))}, {ov::hint::model.name(), ov::Any(std::shared_ptr(nullptr))}, @@ -35,7 +38,6 @@ std::vector> plugin_public_mutable_properties = {ov::enable_profiling.name(), ov::Any(true)}, {ov::compilation_num_threads.name(), ov::Any(1)}, {ov::hint::performance_mode.name(), ov::Any(ov::hint::PerformanceMode::THROUGHPUT)}, - {ov::hint::enable_cpu_pinning.name(), ov::Any(true)}, {ov::log::level.name(), ov::Any(ov::log::Level::ERR)}, {ov::device::id.name(), ov::Any(ov::test::utils::getDeviceNameID(ov::test::utils::getDeviceName()))}, }; @@ -49,13 +51,11 @@ std::vector> compat_plugin_internal_mutable_prop }; std::vector> plugin_internal_mutable_properties = { - {ov::intel_npu::stepping.name(), ov::Any(4)} -}; + {ov::intel_npu::stepping.name(), ov::Any(4)}}; std::vector> plugin_public_immutable_properties = { {ov::device::uuid.name(), ov::Any("deadbeef")}, {ov::supported_properties.name(), {ov::device::full_name.name()}}, - {ov::num_streams.name(), ov::Any(ov::streams::Num(4))}, {ov::available_devices.name(), ov::Any(std::vector{"deadbeef"})}, {ov::device::capabilities.name(), ov::Any(std::vector{"deadbeef"})}, {ov::range_for_async_infer_requests.name(), @@ -64,8 +64,7 @@ std::vector> plugin_public_immutable_properties {ov::optimal_number_of_infer_requests.name(), ov::Any(4)}, {ov::intel_npu::device_alloc_mem_size.name(), ov::Any(2)}, {ov::intel_npu::device_total_mem_size.name(), ov::Any(2)}, - {ov::intel_npu::max_tiles.name(), ov::Any(9999)} -}; + {ov::intel_npu::max_tiles.name(), ov::Any(9999)}}; std::vector> invalid_device_ids = { {ov::device::id.name(), "NPU.1"}, @@ -130,6 +129,12 @@ INSTANTIATE_TEST_SUITE_P( ov::Any("THISCONFIGVALUENOTEXIST"))})), ClassPluginPropertiesTestSuite4NPU::getTestCaseName); +INSTANTIATE_TEST_SUITE_P(smoke_BehaviorTests_ClassExecutableNetworkSetPropertiesTestNPU, + ClassPluginPropertiesTestSuite5NPU, + ::testing::Combine(::testing::Values(ov::test::utils::getDeviceName()), + ::testing::ValuesIn(exe_network_public_mutable_properties)), + ClassPluginPropertiesTestSuite5NPU::getTestCaseName); + INSTANTIATE_TEST_SUITE_P(smoke_BehaviorTests_ClassExecutableNetworkInvalidDeviceIDTestNPU, ClassExecutableNetworkInvalidDeviceIDTestSuite, ::testing::Combine(::testing::Values(ov::test::utils::getDeviceName()), @@ -150,6 +155,24 @@ INSTANTIATE_TEST_SUITE_P(smoke_BehaviorTests_CheckCompilerType, CheckCompilerPropertyWhenImporting, ::testing::Combine(::testing::Values(ov::test::utils::DEVICE_NPU), ::testing::ValuesIn(valid_device_ids)), - CheckCompilerTypeProperty::getTestCaseName); + CheckCompilerPropertyWhenImporting::getTestCaseName); + +INSTANTIATE_TEST_SUITE_P(smoke_BehaviorTests_CheckCompilerType, + CheckCpuPinning, + ::testing::Combine(::testing::Values(ov::test::utils::DEVICE_NPU), + ::testing::ValuesIn(valid_device_ids)), + CheckCpuPinning::getTestCaseName); + +INSTANTIATE_TEST_SUITE_P(smoke_BehaviorTests_CheckCompilerVersion, + CheckCompilerVersionProperty, + ::testing::Combine(::testing::Values(ov::test::utils::DEVICE_NPU), + ::testing::ValuesIn(valid_device_ids)), + CheckCompilerVersionProperty::getTestCaseName); + +INSTANTIATE_TEST_SUITE_P(compatibility_smoke_BehaviorTests_CheckSecureCompilationFlag, + CheckSecureCompilationFlag, + ::testing::Combine(::testing::Values(ov::test::utils::DEVICE_NPU), + ::testing::ValuesIn(exe_network_public_mutable_properties)), + CheckSecureCompilationFlag::getTestCaseName); } // namespace diff --git a/src/plugins/intel_npu/tests/functional/behavior/compiled_model/property.hpp b/src/plugins/intel_npu/tests/functional/behavior/compiled_model/property.hpp index 62a43b9ffc9d..f5dde63e26cc 100644 --- a/src/plugins/intel_npu/tests/functional/behavior/compiled_model/property.hpp +++ b/src/plugins/intel_npu/tests/functional/behavior/compiled_model/property.hpp @@ -10,30 +10,16 @@ #include "behavior/compiled_model/properties.hpp" #include "common/npu_test_env_cfg.hpp" +#include "common/utils.hpp" #include "common_test_utils/subgraph_builders/conv_pool_relu.hpp" #include "intel_npu/npu_private_properties.hpp" #include "openvino/core/log.hpp" #include "zero_backend.hpp" -#include "zero_device.hpp" using namespace ov::test::behavior; namespace { -class LogCallbackGuard { -public: - explicit LogCallbackGuard(const std::function& callback) { - ov::util::set_log_callback(callback); - } - - ~LogCallbackGuard() { - ov::util::reset_log_callback(); - } - - LogCallbackGuard(const LogCallbackGuard&) = delete; - LogCallbackGuard& operator=(const LogCallbackGuard&) = delete; -}; - bool has_non_negative_numeric_suffix(const std::string& value) { const auto dot_pos = value.find('.'); if (dot_pos == std::string::npos || dot_pos + 1 >= value.size()) { @@ -251,6 +237,27 @@ TEST_P(ClassPluginPropertiesTestSuite4NPU, CanNotSetGetInexistentProperty) { ov::Exception); // Expect to throw due to unsupported config } +using ClassPluginPropertiesTestSuite5NPU = ClassExecutableNetworkGetPropertiesTestNPU; + +TEST_P(ClassPluginPropertiesTestSuite5NPU, CanSetMutablePropertiesToCompiledModel) { + // ie.set_property won't call plugin Engine::SetConfig due to empty string-ov::Plugin map from core_impl + // workaround to overcome this is to call first ie.get_property which calls get_plugin() from core_impl and + // populates plugin map + std::vector properties; + OV_ASSERT_NO_THROW(properties = ie.get_property(deviceName, ov::supported_properties)); + + OV_ASSERT_NO_THROW(ie.set_property(deviceName, {{configKey, configValue}})); + + OV_ASSERT_NO_THROW(ov::CompiledModel compiled_model1 = + ie.compile_model(model, deviceName, {{configKey, configValue}})); + + ov::CompiledModel compiled_model2; + + OV_ASSERT_NO_THROW(compiled_model2 = ie.compile_model(model, deviceName)); + + OV_ASSERT_NO_THROW(compiled_model2.set_property({{configKey, configValue}})); +} + using ClassExecutableNetworkInvalidDeviceIDTestSuite = ClassExecutableNetworkGetPropertiesTestNPU; TEST_P(ClassExecutableNetworkInvalidDeviceIDTestSuite, InvalidNPUdeviceIDTest) { @@ -369,7 +376,7 @@ TEST_P(CheckCompilerTypeProperty, CheckLogAfterSettingExtraConfigToGetProperty) auto compiler_type = ov::intel_npu::CompilerType::PLUGIN; { - LogCallbackGuard log_callback_guard(log_cb); + ov::test::utils::LogCallbackGuard log_callback_guard(log_cb); compiler_type = core.get_property( deviceName, ov::intel_npu::compiler_type, @@ -398,7 +405,7 @@ TEST_P(CheckCompilerTypeProperty, CheckLogAfterGettingPropertyWithExtraConfig) { }; { - LogCallbackGuard log_callback_guard(log_cb); + ov::test::utils::LogCallbackGuard log_callback_guard(log_cb); OV_ASSERT_NO_THROW(core.get_property(deviceName, ov::intel_npu::defer_weights_load, {ov::intel_npu::compiler_type(ov::intel_npu::CompilerType::DRIVER)})); @@ -409,7 +416,7 @@ TEST_P(CheckCompilerTypeProperty, CheckLogAfterGettingPropertyWithExtraConfig) { logs.clear(); { - LogCallbackGuard log_callback_guard(log_cb); + ov::test::utils::LogCallbackGuard log_callback_guard(log_cb); OV_ASSERT_NO_THROW(core.get_property(deviceName, ov::intel_npu::defer_weights_load, {ov::intel_npu::compiler_type(ov::intel_npu::CompilerType::DRIVER), @@ -434,7 +441,7 @@ TEST_P(CheckCompilerTypeProperty, SetRuntimeProperty) { }; { - LogCallbackGuard log_callback_guard(log_cb); + ov::test::utils::LogCallbackGuard log_callback_guard(log_cb); OV_ASSERT_NO_THROW( core.set_property(deviceName, ov::intel_npu::compiler_type(ov::intel_npu::CompilerType::DRIVER))); OV_ASSERT_NO_THROW(core.get_property(deviceName, ov::intel_npu::defer_weights_load)); @@ -445,7 +452,7 @@ TEST_P(CheckCompilerTypeProperty, SetRuntimeProperty) { logs.clear(); { - LogCallbackGuard log_callback_guard(log_cb); + ov::test::utils::LogCallbackGuard log_callback_guard(log_cb); OV_ASSERT_NO_THROW(core.set_property(deviceName, ov::intel_npu::qdq_optimization(true))); } @@ -467,7 +474,7 @@ TEST_P(CheckCompilerTypeProperty, SetCompilerPropertyForDifferentCompiler) { }; { - LogCallbackGuard log_callback_guard(log_cb); + ov::test::utils::LogCallbackGuard log_callback_guard(log_cb); OV_ASSERT_NO_THROW( core.set_property(deviceName, ov::intel_npu::compiler_type(ov::intel_npu::CompilerType::DRIVER))); OV_ASSERT_NO_THROW(core.get_property(deviceName, ov::intel_npu::defer_weights_load)); @@ -477,7 +484,7 @@ TEST_P(CheckCompilerTypeProperty, SetCompilerPropertyForDifferentCompiler) { logs.clear(); { - LogCallbackGuard log_callback_guard(log_cb); + ov::test::utils::LogCallbackGuard log_callback_guard(log_cb); OV_ASSERT_NO_THROW(core.set_property(deviceName, ov::intel_npu::qdq_optimization(true))); } ASSERT_NE(logs.find("initialize DriverCompilerAdapter start"), std::string::npos); @@ -485,7 +492,7 @@ TEST_P(CheckCompilerTypeProperty, SetCompilerPropertyForDifferentCompiler) { logs.clear(); { - LogCallbackGuard log_callback_guard(log_cb); + ov::test::utils::LogCallbackGuard log_callback_guard(log_cb); OV_ASSERT_NO_THROW( core.set_property(deviceName, ov::intel_npu::compiler_type(ov::intel_npu::CompilerType::PLUGIN))); } @@ -495,7 +502,7 @@ TEST_P(CheckCompilerTypeProperty, SetCompilerPropertyForDifferentCompiler) { logs.clear(); { - LogCallbackGuard log_callback_guard(log_cb); + ov::test::utils::LogCallbackGuard log_callback_guard(log_cb); OV_ASSERT_NO_THROW(core.set_property(deviceName, ov::intel_npu::qdq_optimization(true))); } ASSERT_NE(logs.find("initialize PluginCompilerAdapter start"), std::string::npos); @@ -518,7 +525,7 @@ TEST_P(CheckCompilerTypeProperty, GetCompilerVersion) { OV_ASSERT_NO_THROW( core.set_property(deviceName, ov::intel_npu::compiler_type(ov::intel_npu::CompilerType::DRIVER))); { - LogCallbackGuard log_callback_guard(log_cb); + ov::test::utils::LogCallbackGuard log_callback_guard(log_cb); OV_ASSERT_NO_THROW(core.get_property(deviceName, ov::intel_npu::compiler_version)); } ASSERT_NE(logs.find("initialize DriverCompilerAdapter start"), std::string::npos); @@ -529,7 +536,7 @@ TEST_P(CheckCompilerTypeProperty, GetCompilerVersion) { OV_ASSERT_NO_THROW( core.set_property(deviceName, ov::intel_npu::compiler_type(ov::intel_npu::CompilerType::PLUGIN))); { - LogCallbackGuard log_callback_guard(log_cb); + ov::test::utils::LogCallbackGuard log_callback_guard(log_cb); OV_ASSERT_NO_THROW(core.get_property(deviceName, ov::intel_npu::compiler_version)); } ASSERT_EQ(logs.find("initialize DriverCompilerAdapter start"), std::string::npos); @@ -538,7 +545,7 @@ TEST_P(CheckCompilerTypeProperty, GetCompilerVersion) { logs.clear(); { - LogCallbackGuard log_callback_guard(log_cb); + ov::test::utils::LogCallbackGuard log_callback_guard(log_cb); OV_ASSERT_NO_THROW(core.get_property(deviceName, ov::intel_npu::compiler_version, {ov::intel_npu::compiler_type(ov::intel_npu::CompilerType::DRIVER)})); @@ -549,6 +556,41 @@ TEST_P(CheckCompilerTypeProperty, GetCompilerVersion) { logs.clear(); } +using CheckCompilerVersionProperty = ClassExecutableNetworkGetPropertiesTestNPU; + +TEST_P(CheckCompilerVersionProperty, GetCompilerVersionFromCompiledModel) { + ov::Core core; + ov::CompiledModel compiled_model; + OV_ASSERT_NO_THROW(compiled_model = core.compile_model(model, deviceName)); + + uint32_t compiled_model_version = 0; + OV_ASSERT_NO_THROW(compiled_model_version = compiled_model.get_property(ov::intel_npu::compiler_version)); + + uint32_t plugin_version = 0; + OV_ASSERT_NO_THROW(plugin_version = core.get_property(deviceName, ov::intel_npu::compiler_version)); + ASSERT_EQ(compiled_model_version, plugin_version); +} + +TEST_P(CheckCompilerVersionProperty, CompilerVersionAvailableAfterImport) { + ov::Core core_compile, core_import; + ov::CompiledModel compiled_model, imported_model; + std::stringstream export_stream; + + OV_ASSERT_NO_THROW(compiled_model = core_compile.compile_model(model, deviceName)); + + uint32_t compiled_version = 0; + OV_ASSERT_NO_THROW(compiled_version = compiled_model.get_property(ov::intel_npu::compiler_version)); + + OV_ASSERT_NO_THROW(compiled_model.export_model(export_stream)); + compiled_model = {}; + + OV_ASSERT_NO_THROW(imported_model = core_import.import_model(export_stream, deviceName)); + + uint32_t imported_version = 0; + OV_ASSERT_NO_THROW(imported_version = imported_model.get_property(ov::intel_npu::compiler_version)); + ASSERT_EQ(imported_version, compiled_version); +} + using CheckCompilerPropertyWhenImporting = ClassExecutableNetworkGetPropertiesTestNPU; TEST_P(CheckCompilerPropertyWhenImporting, ExpectedThrowFromImportWithUnsupportedProperty) { @@ -583,7 +625,7 @@ TEST_P(CheckCompilerPropertyWhenImporting, ExpectedNoThrowFromImportWithCompiler }; { - LogCallbackGuard log_callback_guard(log_cb); + ov::test::utils::LogCallbackGuard log_callback_guard(log_cb); core_import.set_property(deviceName, ov::log::level(ov::log::Level::INFO)); OV_ASSERT_NO_THROW( core_import.import_model(export_stream, deviceName, {{ov::intel_npu::qdq_optimization(true)}})); @@ -667,7 +709,7 @@ TEST_P(CheckCompilerPropertyWhenImporting, CheckImportWithCompilerProperty) { }; { - LogCallbackGuard log_callback_guard(log_cb); + ov::test::utils::LogCallbackGuard log_callback_guard(log_cb); OV_ASSERT_NO_THROW( core_for_importing.import_model(export_stream, deviceName, @@ -707,7 +749,7 @@ TEST_P(CheckCompilerPropertyWhenImporting, CheckImportWithCompilerPropertyAfterC }; { - LogCallbackGuard log_callback_guard(log_cb); + ov::test::utils::LogCallbackGuard log_callback_guard(log_cb); OV_ASSERT_NO_THROW(core.import_model(export_stream, deviceName, {{ov::intel_npu::compiler_type(ov::intel_npu::CompilerType::DRIVER), @@ -725,4 +767,121 @@ TEST_P(CheckCompilerPropertyWhenImporting, CheckImportWithCompilerPropertyAfterC std::string::npos); } +using CheckCpuPinning = ClassExecutableNetworkGetPropertiesTestNPU; + +TEST_P(CheckCpuPinning, CheckCompileModelWithCpuPinningFromSetProperty) { + std::string logs; + std::mutex logs_mutex; + ov::Core core; + ov::CompiledModel compiled_model; + + ov::log::Level previous_log_level = ov::log::Level::NO; + OV_ASSERT_NO_THROW(previous_log_level = core.get_property(deviceName, ov::log::level)); + core.set_property(deviceName, ov::log::level(ov::log::Level::INFO)); + + // Keep this std::function alive while logging is active. + std::function log_cb = [&](std::string_view msg) { + std::lock_guard lock(logs_mutex); + logs.append(msg); + logs.push_back('\n'); + }; + + { + ov::test::utils::LogCallbackGuard log_callback_guard(log_cb); + OV_ASSERT_NO_THROW(core.set_property(deviceName, ov::hint::enable_cpu_pinning(true))); + OV_ASSERT_NO_THROW(compiled_model = core.compile_model(model, deviceName)); + } + OV_ASSERT_NO_THROW(core.set_property(deviceName, ov::log::level(previous_log_level))); + + bool enable_cpu_pinning = false; + OV_ASSERT_NO_THROW(enable_cpu_pinning = compiled_model.get_property(ov::hint::enable_cpu_pinning)); + ASSERT_TRUE(enable_cpu_pinning); + + std::vector core_supported_properties; + OV_ASSERT_NO_THROW(core_supported_properties = core.get_property(deviceName, ov::supported_properties)); + ASSERT_EQ(std::find(core_supported_properties.cbegin(), + core_supported_properties.cend(), + ov::hint::enable_cpu_pinning.name()), + core_supported_properties.cend()); + + std::vector compiled_supported_properties; + OV_ASSERT_NO_THROW(compiled_supported_properties = compiled_model.get_property(ov::supported_properties)); + ASSERT_EQ(std::find(compiled_supported_properties.cbegin(), + compiled_supported_properties.cend(), + ov::hint::enable_cpu_pinning.name()), + compiled_supported_properties.cend()); + + ASSERT_NE(logs.find("The \"ENABLE_CPU_PINNING\" property is deprecated and has no effect on the NPU Plugin."), + std::string::npos); +} + +TEST_P(CheckCpuPinning, CheckCompileModelWithCpuPinningFromCompileProperty) { + std::string logs; + std::mutex logs_mutex; + ov::Core core; + ov::CompiledModel compiled_model; + + ov::log::Level previous_log_level = ov::log::Level::NO; + OV_ASSERT_NO_THROW(previous_log_level = core.get_property(deviceName, ov::log::level)); + core.set_property(deviceName, ov::log::level(ov::log::Level::INFO)); + + // Keep this std::function alive while logging is active. + std::function log_cb = [&](std::string_view msg) { + std::lock_guard lock(logs_mutex); + logs.append(msg); + logs.push_back('\n'); + }; + + { + ov::test::utils::LogCallbackGuard log_callback_guard(log_cb); + OV_ASSERT_NO_THROW(compiled_model = + core.compile_model(model, deviceName, {ov::hint::enable_cpu_pinning(true)})); + } + OV_ASSERT_NO_THROW(core.set_property(deviceName, ov::log::level(previous_log_level))); + + bool enable_cpu_pinning = false; + OV_ASSERT_NO_THROW(enable_cpu_pinning = compiled_model.get_property(ov::hint::enable_cpu_pinning)); + ASSERT_TRUE(enable_cpu_pinning); + + std::vector core_supported_properties; + OV_ASSERT_NO_THROW(core_supported_properties = core.get_property(deviceName, ov::supported_properties)); + ASSERT_EQ(std::find(core_supported_properties.cbegin(), + core_supported_properties.cend(), + ov::hint::enable_cpu_pinning.name()), + core_supported_properties.cend()); + + std::vector compiled_supported_properties; + OV_ASSERT_NO_THROW(compiled_supported_properties = compiled_model.get_property(ov::supported_properties)); + ASSERT_EQ(std::find(compiled_supported_properties.cbegin(), + compiled_supported_properties.cend(), + ov::hint::enable_cpu_pinning.name()), + compiled_supported_properties.cend()); + + ASSERT_NE(logs.find("The \"ENABLE_CPU_PINNING\" property is deprecated and has no effect on the NPU Plugin."), + std::string::npos); +} + +using CheckSecureCompilationFlag = ClassExecutableNetworkGetPropertiesTestNPU; + +TEST_P(CheckSecureCompilationFlag, EncryptionCallbacksForOlderDriverThrows) { + ov::Core core; + ov::AnyMap configuration = {{configKey, configValue}, + ov::intel_npu::compiler_type(ov::intel_npu::CompilerType::DRIVER)}; + if (::intel_npu::ZeroInitStructsHolder::getInstance()->getGraphDdiTable().version() < ZE_MAKE_VERSION(1, 17)) { + OV_EXPECT_THROW(core.compile_model(model, deviceName, configuration), + ov::Exception, + testing::HasSubstr( + "Secure compilation was requested, but the current driver version does not support it.")); + core.set_property(deviceName, configuration); + OV_EXPECT_THROW(core.compile_model(model, deviceName, {}), + ov::Exception, + testing::HasSubstr( + "Secure compilation was requested, but the current driver version does not support it.")); + } else { + OV_ASSERT_NO_THROW(core.compile_model(model, deviceName, configuration)); + core.set_property(deviceName, configuration); + OV_ASSERT_NO_THROW(core.compile_model(model, deviceName, {})); + } +} + } // namespace diff --git a/src/plugins/intel_npu/tests/functional/behavior/dynamic_host_pipeline/infer_with_host_compile.cpp b/src/plugins/intel_npu/tests/functional/behavior/dynamic_host_pipeline/infer_with_host_compile.cpp index 238d7f546852..1edd68bb34cd 100644 --- a/src/plugins/intel_npu/tests/functional/behavior/dynamic_host_pipeline/infer_with_host_compile.cpp +++ b/src/plugins/intel_npu/tests/functional/behavior/dynamic_host_pipeline/infer_with_host_compile.cpp @@ -9,17 +9,15 @@ #include "intel_npu/config/options.hpp" #include "intel_npu/npu_private_properties.hpp" -const std::vector configs = {{{"NPU_COMPILER_TYPE", "PLUGIN"}, - {"NPU_PLATFORM", "NPU4000"}, - {"NPU_COMPILATION_MODE", "HostCompile"}, - {"NPU_CREATE_EXECUTOR", "0"}}, - {{"NPU_COMPILER_TYPE", "PLUGIN"}, - {"NPU_PLATFORM", "NPU5010"}, - {"NPU_COMPILATION_MODE", "HostCompile"}, - {"NPU_CREATE_EXECUTOR", "0"}}}; +const std::vector devices = {"NPU.4000", "NPU.5010"}; + +const std::vector configs = { + {{"NPU_COMPILER_TYPE", "PLUGIN"}, + {"NPU_COMPILATION_MODE", "HostCompile"}, + {"NPU_CREATE_EXECUTOR", "0"}, + }}; INSTANTIATE_TEST_SUITE_P(smoke_BehaviorTests, InferWithHostCompileTests, - ::testing::Combine(::testing::Values(ov::test::utils::DEVICE_NPU), - ::testing::ValuesIn(configs)), + ::testing::Combine(::testing::ValuesIn(devices), ::testing::ValuesIn(configs)), ov::test::utils::appendPlatformTypeTestName); diff --git a/src/plugins/intel_npu/tests/functional/behavior/dynamic_host_pipeline/infer_with_host_compile.hpp b/src/plugins/intel_npu/tests/functional/behavior/dynamic_host_pipeline/infer_with_host_compile.hpp index 582b4ff1c461..d326fc26f997 100644 --- a/src/plugins/intel_npu/tests/functional/behavior/dynamic_host_pipeline/infer_with_host_compile.hpp +++ b/src/plugins/intel_npu/tests/functional/behavior/dynamic_host_pipeline/infer_with_host_compile.hpp @@ -14,14 +14,16 @@ #include "openvino/opsets/opset6.hpp" #include "openvino/pass/manager.hpp" #include "openvino/pass/serialize.hpp" +#include "openvino/runtime/make_tensor.hpp" #include "shared_test_classes/base/ov_behavior_test_utils.hpp" + namespace ov { namespace test { namespace behavior { inline std::shared_ptr createMaxPoolModel() { - auto input = - std::make_shared(element::f16, PartialShape{1, 16, 720, ov::Dimension(10, 1280)}); + auto input = std::make_shared(ov::element::f16, + ov::PartialShape{1, 16, ov::Dimension(10, 720), ov::Dimension(10, 1280)}); input->set_friendly_name("input1"); auto maxpool = std::make_shared(input, @@ -35,8 +37,18 @@ inline std::shared_ptr createMaxPoolModel() { auto result = std::make_shared(maxpool); result->set_friendly_name("output"); + auto model = std::make_shared(ResultVector{result}, ParameterVector{input}, "MaxPool"); + + // making input and output to be NHWC + auto preProc = ov::preprocess::PrePostProcessor(model); + preProc.input(0).tensor().set_layout("NHWC"); + preProc.input(0).model().set_layout("NCHW"); + preProc.output(0).tensor().set_layout("NHWC"); + preProc.output(0).model().set_layout("NCHW"); + + model = preProc.build(); - return std::make_shared(ResultVector{result}, ParameterVector{input}, "MaxPool"); + return model; } using InferWithHostCompileParams = std::tuple, public OVInferRequestTestBase { public: + enum class RuntimeCompareStatus { + ready, + skip, + fail, + }; + + struct ScopedLogCapture { + ScopedLogCapture(); + ~ScopedLogCapture(); + + void clear(); + std::string str() const; + + private: + std::stringstream stream; + std::function callback; + + friend class InferWithHostCompileTests; + }; + + struct RuntimeCompareContext { + std::shared_ptr model; + ov::CompiledModel compiledModel; + ov::CompiledModel referenceCompiledModel; + ov::InferRequest reqDynamic; + ov::InferRequest reqReference; + }; + + struct RuntimeCompareSetupResult { + RuntimeCompareStatus status = RuntimeCompareStatus::ready; + std::string message; + RuntimeCompareContext context; + }; + static std::string getTestCaseName(testing::TestParamInfo obj) { std::string target_device; ov::AnyMap configuration; @@ -71,36 +117,152 @@ class InferWithHostCompileTests : public testing::WithParamInterfaceGetParam(); + std::vector deviceNames = + core->get_property("NPU", ov::available_devices.name()).as>(); + for (auto name : deviceNames) { + if (target_device.find(name) != std::string::npos) { + isTargetDevice = true; + break; + } + } + originalLogLevel = core->get_property("NPU", ov::log::level.name()).as(); + APIBaseTest::SetUp(); } - bool isLLVMFormat(std::stringstream& modelStream) { - auto pos = modelStream.tellg(); - if (pos == std::streampos(-1)) { - return false; - } + void TearDown() { + core->set_property("NPU", ov::log::level(originalLogLevel)); + } - modelStream.seekg(0, std::ios::beg); + static void compareInferenceResult(const std::shared_ptr& model, + ov::InferRequest& reqDynamic, + ov::InferRequest& reqReference); - // Assume the first 20 char of LLVM blob shall have key word 'llvm' - std::string region(20, '\0'); - modelStream.read(®ion[0], 20); - region.resize(modelStream.gcount()); + static void inferAndCompare(const std::shared_ptr& model, + ov::InferRequest& reqDynamic, + ov::InferRequest& reqReference, + const std::string& dumpPrefix); - modelStream.clear(); - modelStream.seekg(pos); + static void setInputInferAndCompare(const std::shared_ptr& model, + ov::InferRequest& reqDynamic, + ov::InferRequest& reqReference, + const ov::Tensor& inputTensor, + const std::string& dumpPrefix); - return region.find("llvm") != std::string::npos; - } + static bool logContains(const ScopedLogCapture& logCapture, const std::string& expectedEntry); + + RuntimeCompareSetupResult prepareRuntimeCompareContext(const std::shared_ptr& model); protected: std::shared_ptr core = utils::PluginCache::get().core(); ov::AnyMap configuration; + bool isTargetDevice = false; + ov::log::Level originalLogLevel = ov::log::Level::ERR; }; -TEST_P(InferWithHostCompileTests, Compile) { +InferWithHostCompileTests::ScopedLogCapture::ScopedLogCapture() + : callback([this](std::string_view s) { + stream << s << std::endl; + }) { + ov::util::set_log_callback(callback); +} + +InferWithHostCompileTests::ScopedLogCapture::~ScopedLogCapture() { + ov::util::reset_log_callback(); +} + +void InferWithHostCompileTests::ScopedLogCapture::clear() { + stream.str(""); + stream.clear(); +} + +std::string InferWithHostCompileTests::ScopedLogCapture::str() const { + return stream.str(); +} + +void InferWithHostCompileTests::compareInferenceResult(const std::shared_ptr& model, + ov::InferRequest& reqDynamic, + ov::InferRequest& reqReference) { + const auto inputTensor = reqDynamic.get_input_tensor(0); + const auto npuOutputTensor = reqDynamic.get_tensor(model->output()); + const auto referenceOutputTensor = reqReference.get_tensor(model->output()); + + ov::test::utils::compare(referenceOutputTensor, npuOutputTensor, npuOutputTensor.get_element_type()); +} + +void InferWithHostCompileTests::inferAndCompare(const std::shared_ptr& model, + ov::InferRequest& reqDynamic, + ov::InferRequest& reqReference, + const std::string& stage) { + OV_ASSERT_NO_THROW(reqDynamic.infer()); + OV_ASSERT_NO_THROW(reqReference.infer()); + try { + compareInferenceResult(model, reqDynamic, reqReference); + } catch (const ov::Exception& e) { + FAIL() << "Inference result comparison failed at stage " << stage << ": " << e.what(); + } +} + +void InferWithHostCompileTests::setInputInferAndCompare(const std::shared_ptr& model, + ov::InferRequest& reqDynamic, + ov::InferRequest& reqReference, + const ov::Tensor& inputTensor, + const std::string& stage) { + OV_ASSERT_NO_THROW(reqDynamic.set_input_tensor(0, inputTensor)); + OV_ASSERT_NO_THROW(reqReference.set_input_tensor(0, inputTensor)); + inferAndCompare(model, reqDynamic, reqReference, stage); +} + +bool InferWithHostCompileTests::logContains(const ScopedLogCapture& logCapture, const std::string& expectedEntry) { + return logCapture.str().find(expectedEntry) != std::string::npos; +} + +InferWithHostCompileTests::RuntimeCompareSetupResult InferWithHostCompileTests::prepareRuntimeCompareContext( + const std::shared_ptr& model) { + RuntimeCompareSetupResult result; + result.context.model = model; + + try { + result.context.compiledModel = core->compile_model(model, target_device, configuration); + } catch (const ov::Exception& e) { + result.status = RuntimeCompareStatus::fail; + result.message = std::string("Failed to compile model for target device: ") + e.what(); + return result; + } + + try { + result.context.referenceCompiledModel = core->compile_model(model, ov::test::utils::DEVICE_TEMPLATE); + } catch (const ov::Exception& e) { + result.status = RuntimeCompareStatus::skip; + result.message = std::string("CPU plugin is not available for reference comparison: ") + e.what(); + return result; + } + + try { + result.context.reqDynamic = result.context.compiledModel.create_infer_request(); + } catch (const ov::Exception& e) { + if (std::string(e.what()).find("Cannot load library") == std::string::npos) { + result.status = RuntimeCompareStatus::fail; + result.message = + std::string("Expected exception message to contain 'Cannot load library', but got: ") + e.what(); + return result; + } + + result.status = RuntimeCompareStatus::skip; + result.message = "Cannot load library, skip test."; + return result; + } + + result.context.reqReference = result.context.referenceCompiledModel.create_infer_request(); + return result; +} + +TEST_P(InferWithHostCompileTests, CompileAndImportAndInfer) { // Skip test according to plugin specific disabledTestPatterns() (if any) SKIP_IF_CURRENT_TEST_IS_DISABLED() + if (!isTargetDevice) { + GTEST_SKIP() << "Skip test for current device"; + } auto model = createMaxPoolModel(); ov::CompiledModel compiledModel; @@ -110,35 +272,284 @@ TEST_P(InferWithHostCompileTests, Compile) { std::stringstream modelStream; OV_ASSERT_NO_THROW(compiledModel.export_model(modelStream)); - // With HostCompile, the modelStream shall contain "llvm.func" - ASSERT_TRUE(isLLVMFormat(modelStream)) << "CompiledStream from HostCompile mode shall has 'llvm.func' inside it"; - ov::InferRequest reqDynamic; - // Add shape check once npu_mlir_runtime is inside test package - EXPECT_THROW(reqDynamic = compiledModel.create_infer_request(), ov::Exception); + try { + ov::CompiledModel importedModel = core->import_model(modelStream, target_device); + reqDynamic = importedModel.create_infer_request(); + } catch (const ov::Exception& e) { + if (std::string(e.what()).find("Cannot load library") == std::string::npos) { + FAIL() << "Expected exception message to contain 'Cannot load library', but got: " << e.what(); + } else { + GTEST_SKIP() << "Cannot load library, skip test."; + } + } + + OV_ASSERT_NO_THROW(reqDynamic.infer()); } -TEST_P(InferWithHostCompileTests, CompileAndImport) { +// Compile, infer with a large shape, then shrink the input shape and verify both output correctness and command-list +// reuse behavior. +TEST_P(InferWithHostCompileTests, CompileAndInferWithDecreasedSize) { // Skip test according to plugin specific disabledTestPatterns() (if any) SKIP_IF_CURRENT_TEST_IS_DISABLED() + if (!isTargetDevice) { + GTEST_SKIP() << "Skip test for current device"; + } + auto model = createMaxPoolModel(); + ScopedLogCapture logCapture; - ov::CompiledModel compiledModel; - // Compilation shall pass since load of npu_mlir_runtime is deffered with NPU_CREATE_EXECUTOR=0 - OV_ASSERT_NO_THROW(compiledModel = core->compile_model(model, target_device, configuration)); + core->set_property("NPU", ov::log::level(ov::log::Level::DEBUG)); + auto setupResult = prepareRuntimeCompareContext(model); + if (setupResult.status == RuntimeCompareStatus::fail) { + FAIL() << setupResult.message; + } + if (setupResult.status == RuntimeCompareStatus::skip) { + GTEST_SKIP() << setupResult.message; + } + auto& testContext = setupResult.context; - std::stringstream modelStream; - OV_ASSERT_NO_THROW(compiledModel.export_model(modelStream)); + // Start with the largest shape in the dynamic range. + ov::Shape shape = {1, 720, 1280, 16}; + ov::Tensor inTensor = ov::test::utils::create_and_fill_tensor(model->input().get_element_type(), shape, 100, 0); + setInputInferAndCompare(model, + testContext.reqDynamic, + testContext.reqReference, + inTensor, + "CompileAndInferWithDecreasedSize_first"); + // The first run materializes runtime state for the initial shape. + ASSERT_TRUE(logContains(logCapture, "Reset command list to run with runtime")) + << "Expected log to contain 'Reset command list to run with runtime', but got: " << logCapture.str(); - // With HostCompile, the modelStream shall contain "llvm.func" - ASSERT_TRUE(isLLVMFormat(modelStream)) << "CompiledStream from HostCompile mode shall has 'llvm.func' inside it"; + logCapture.clear(); + inferAndCompare(model, testContext.reqDynamic, testContext.reqReference, "CompileAndInferWithDecreasedSize_second"); + // Reusing the same input should keep the existing command list intact. + ASSERT_TRUE(logContains(logCapture, "Reuse command list without update since no tensor change detected")) + << "Expected log to contain 'Reuse command list without update since no tensor change detected' for second " + "inference, but got: " + << logCapture.str(); - ov::CompiledModel importedModel; - OV_ASSERT_NO_THROW(core->import_model(modelStream, target_device, configuration)); + logCapture.clear(); + ov::Tensor inTensor1 = ov::test::utils::create_and_fill_tensor(model->input().get_element_type(), shape, 100, 0); + setInputInferAndCompare(model, + testContext.reqDynamic, + testContext.reqReference, + inTensor1, + "CompileAndInferWithDecreasedSize_third"); + // A new host tensor with the same shape should still reuse the command list. + ASSERT_TRUE(logContains(logCapture, "Reuse command list without update since no tensor change detected")) + << "Expected log to contain 'Reuse command list without update since no tensor change detected' for third " + "inference, but got: " + << logCapture.str(); - ov::InferRequest reqDynamic; - // Add shape check once npu_mlir_runtime is inside test package - EXPECT_THROW(reqDynamic = importedModel.create_infer_request(), ov::Exception); + logCapture.clear(); + ov::Shape shape2 = {1, 720, 720, 16}; + ov::Tensor inTensor3 = ov::test::utils::create_and_fill_tensor(model->input().get_element_type(), shape2, 100, 0); + setInputInferAndCompare(model, + testContext.reqDynamic, + testContext.reqReference, + inTensor3, + "CompileAndInferWithDecreasedSize_fourth"); + // Shrinking the shape should force runtime reconfiguration for the new tensor layout. + ASSERT_TRUE(logContains(logCapture, "Reset command list to run with runtime")) + << "Expected log to contain 'Reset command list to run with runtime' for fourth inference with new shape, but " + "got: " + << logCapture.str(); +} + +// Compile, infer with a small shape, then grow the input shape and verify both output correctness and command-list +// reuse behavior. +TEST_P(InferWithHostCompileTests, CompileAndInferWithIncreasedSize) { + // Skip test according to plugin specific disabledTestPatterns() (if any) + SKIP_IF_CURRENT_TEST_IS_DISABLED() + if (!isTargetDevice) { + GTEST_SKIP() << "Skip test for current device"; + } + + auto model = createMaxPoolModel(); + ScopedLogCapture logCapture; + + core->set_property("NPU", ov::log::level(ov::log::Level::DEBUG)); + auto setupResult = prepareRuntimeCompareContext(model); + if (setupResult.status == RuntimeCompareStatus::fail) { + FAIL() << setupResult.message; + } + if (setupResult.status == RuntimeCompareStatus::skip) { + GTEST_SKIP() << setupResult.message; + } + + auto& testContext = setupResult.context; + + // Start with a smaller valid dynamic shape. + ov::Shape shape = {1, 720, 720, 16}; + ov::Tensor inTensor = ov::test::utils::create_and_fill_tensor(model->input().get_element_type(), shape, 100, 0); + setInputInferAndCompare(model, + testContext.reqDynamic, + testContext.reqReference, + inTensor, + "CompileAndInferWithIncreasedSize_first"); + // The first run materializes runtime state for the initial shape. + ASSERT_TRUE(logContains(logCapture, "Reset command list to run with runtime")) + << "Expected log to contain 'Reset command list to run with runtime', but got: " << logCapture.str(); + + logCapture.clear(); + inferAndCompare(model, testContext.reqDynamic, testContext.reqReference, "CompileAndInferWithIncreasedSize_second"); + // Reusing the same input should keep the existing command list intact. + ASSERT_TRUE(logContains(logCapture, "Reuse command list without update since no tensor change detected")) + << "Expected log to contain 'Reuse command list without update since no tensor change detected' for second " + "inference, but got: " + << logCapture.str(); + + logCapture.clear(); + ov::Tensor inTensor1 = ov::test::utils::create_and_fill_tensor(model->input().get_element_type(), shape, 100, 0); + setInputInferAndCompare(model, + testContext.reqDynamic, + testContext.reqReference, + inTensor1, + "CompileAndInferWithIncreasedSize_third"); + // A new host tensor with the same shape should still reuse the command list. + ASSERT_TRUE(logContains(logCapture, "Reuse command list without update since no tensor change detected")) + << "Expected log to contain 'Reuse command list without update since no tensor change detected' for third " + "inference, but got: " + << logCapture.str(); + + logCapture.clear(); + ov::Shape shape2 = {1, 720, 1280, 16}; + ov::Tensor inTensor3 = ov::test::utils::create_and_fill_tensor(model->input().get_element_type(), shape2, 100, 0); + setInputInferAndCompare(model, + testContext.reqDynamic, + testContext.reqReference, + inTensor3, + "CompileAndInferWithIncreasedSize_fourth"); + // Growing the shape should force runtime reconfiguration for the new tensor layout. + ASSERT_TRUE(logContains(logCapture, "Reset command list to run with runtime")) + << "Expected log to contain 'Reset command list to run with runtime' for fourth inference with new shape, but " + "got: " + << logCapture.str(); +} + +// Exercise imported Level Zero tensors and verify both output correctness and command-list pointer updates. +TEST_P(InferWithHostCompileTests, CompileAndInferWithZeroTensor) { + // Skip test according to plugin specific disabledTestPatterns() (if any) + SKIP_IF_CURRENT_TEST_IS_DISABLED() + if (!isTargetDevice) { + GTEST_SKIP() << "Skip test for current device"; + } + + auto model = createMaxPoolModel(); + ScopedLogCapture logCapture; + + core->set_property("NPU", ov::log::level(ov::log::Level::DEBUG)); + auto setupResult = prepareRuntimeCompareContext(model); + if (setupResult.status == RuntimeCompareStatus::fail) { + FAIL() << setupResult.message; + } + if (setupResult.status == RuntimeCompareStatus::skip) { + GTEST_SKIP() << setupResult.message; + } + auto& testContext = setupResult.context; + + // Start from a regular host tensor. + ov::Shape shape = {1, 720, 1280, 16}; + ov::Tensor inTensor = ov::test::utils::create_and_fill_tensor(model->input().get_element_type(), shape, 100, 0); + setInputInferAndCompare(model, + testContext.reqDynamic, + testContext.reqReference, + inTensor, + "CompileAndInferWithZeroTensor_first"); + + // The first run materializes runtime state for the initial shape. + ASSERT_TRUE(logContains(logCapture, "Reset command list to run with runtime")) + << "Expected log to contain 'Reset command list to run with runtime', but got: " << logCapture.str(); + + logCapture.clear(); + ov::InferRequest reqDynamic1 = testContext.compiledModel.create_infer_request(); + ov::InferRequest reqReference1 = testContext.referenceCompiledModel.create_infer_request(); + setInputInferAndCompare(model, reqDynamic1, reqReference1, inTensor, "CompileAndInferWithZeroTensor_second"); + // A fresh infer request rebuilds runtime state on its first execution. + ASSERT_TRUE(logContains(logCapture, "Reset command list to run with runtime")) + << "Expected log to contain 'Reset command list to run with runtime', but got: " << logCapture.str(); + + logCapture.clear(); + auto outputTensorFromReq = testContext.reqDynamic.get_tensor(model->output()); + setInputInferAndCompare(model, + reqDynamic1, + reqReference1, + outputTensorFromReq, + "CompileAndInferWithZeroTensor_third"); + // Feeding an imported output tensor, ptr change detected and rebuild runtime + // TODO: Update commandlist once dynamic stride supported + ASSERT_TRUE(logContains(logCapture, "Reset command list to run with runtime")) + << "Expected log to contain 'Reset command list to run with runtime' for third inference, but got: " + << logCapture.str(); + + logCapture.clear(); + auto zeroContext = core->get_default_context(target_device); + auto inputHostTensorForForthInfer = zeroContext.create_host_tensor(model->input().get_element_type(), shape); + auto hostTensorSourceForForthInfer = + ov::test::utils::create_and_fill_tensor(model->input().get_element_type(), shape, 100, 0); + ASSERT_EQ(hostTensorSourceForForthInfer.get_byte_size(), inputHostTensorForForthInfer.get_byte_size()) + << "Source and destination tensors must have identical byte sizes for copy"; + std::memcpy(inputHostTensorForForthInfer.data(), + hostTensorSourceForForthInfer.data(), + hostTensorSourceForForthInfer.get_byte_size()); + setInputInferAndCompare(model, + reqDynamic1, + reqReference1, + inputHostTensorForForthInfer, + "CompileAndInferWithZeroTensor_fourth"); + // Feeding a context-allocated host tensor, ptr change detected and rebuild runtime + // TODO: Update commandlist once dynamic stride supported + ASSERT_TRUE(logContains(logCapture, "Reset command list to run with runtime")) + << "Expected log to contain 'Reset command list to run with runtime' for fourth inference, but got: " + << logCapture.str(); + + logCapture.clear(); + auto outputShape = reqDynamic1.get_tensor(model->output()).get_shape(); + auto zeroOutputTensorForFifthInfer = zeroContext.create_host_tensor(model->input().get_element_type(), outputShape); + auto hostTensorSourceForOutputForFifthInfer = + ov::test::utils::create_and_fill_tensor(model->input().get_element_type(), outputShape, 100, 0); + ASSERT_EQ(hostTensorSourceForOutputForFifthInfer.get_byte_size(), zeroOutputTensorForFifthInfer.get_byte_size()) + << "Source and destination tensors must have identical byte sizes for copy"; + std::memcpy(zeroOutputTensorForFifthInfer.data(), + hostTensorSourceForOutputForFifthInfer.data(), + hostTensorSourceForOutputForFifthInfer.get_byte_size()); + OV_ASSERT_NO_THROW(reqDynamic1.set_tensor(model->output(), zeroOutputTensorForFifthInfer)); + inferAndCompare(model, reqDynamic1, reqReference1, "CompileAndInferWithZeroTensor_fifth"); + // Feeding a context-allocated host tensor as output, ptr change detected and rebuild runtime + // TODO: Update commandlist once dynamic stride supported + ASSERT_TRUE(logContains(logCapture, "Reset command list to run with runtime")) + << "Expected log to contain 'Reset command list to run with runtime' for fifth inference, but got: " + << logCapture.str(); + + logCapture.clear(); + auto inputTensorForSixthInfer = + ov::test::utils::create_and_fill_tensor(model->input().get_element_type(), + reqDynamic1.get_tensor(model->input()).get_shape(), + 100, + 0); + + auto outputShapeForSixthInfer = reqDynamic1.get_tensor(model->output()).get_shape(); + auto zeroOutputTensorForSixthInfer = + zeroContext.create_host_tensor(model->input().get_element_type(), outputShapeForSixthInfer); + auto hostTensorSourceForOutputForSixthInfer = + ov::test::utils::create_and_fill_tensor(model->input().get_element_type(), outputShapeForSixthInfer, 100, 0); + ASSERT_EQ(hostTensorSourceForOutputForSixthInfer.get_byte_size(), zeroOutputTensorForSixthInfer.get_byte_size()) + << "Source and destination tensors must have identical byte sizes for copy"; + std::memcpy(zeroOutputTensorForSixthInfer.data(), + hostTensorSourceForOutputForSixthInfer.data(), + hostTensorSourceForOutputForSixthInfer.get_byte_size()); + OV_ASSERT_NO_THROW(reqDynamic1.set_tensor(model->output(), zeroOutputTensorForSixthInfer)); + setInputInferAndCompare(model, + reqDynamic1, + reqReference1, + inputTensorForSixthInfer, + "CompileAndInferWithZeroTensor_sixth"); + // Feeding a context-allocated host tensor, ptr change detected and rebuild runtime + // TODO: Update commandlist once dynamic stride supported + ASSERT_TRUE(logContains(logCapture, "Reset command list to run with runtime")) + << "Expected log to contain 'Reset command list to run with runtime' for sixth inference, but got: " + << logCapture.str(); } } // namespace behavior diff --git a/src/plugins/intel_npu/tests/functional/behavior/npuw/serialization.cpp b/src/plugins/intel_npu/tests/functional/behavior/npuw/serialization.cpp index 1c80b96f5c3e..d6f755e139d1 100644 --- a/src/plugins/intel_npu/tests/functional/behavior/npuw/serialization.cpp +++ b/src/plugins/intel_npu/tests/functional/behavior/npuw/serialization.cpp @@ -4,14 +4,112 @@ #include +#include + #include "npuw/test_engine/models/model_builder.hpp" #include "openvino/core/parallel.hpp" +#include "openvino/pass/stateful_to_stateless.hpp" +#include "shared_test_classes/base/ov_behavior_test_utils.hpp" +using ov::test::npuw::LLMConfig; using ov::test::npuw::ModelBuilder; +namespace { + +LLMConfig make_llm_config() { + LLMConfig cfg; + cfg.num_layers = 2; + cfg.hidden_size = 64; + cfg.num_heads = 4; + cfg.head_dim = 16; + cfg.num_kv_heads = 4; + cfg.vocab_size = 256; + return cfg; +} + +std::shared_ptr build_chunked_prefill_model() { + auto cfg = make_llm_config(); + cfg.num_kv_heads = 2; + cfg.force_gqa_broadcast = true; + + ModelBuilder mb; + auto model = mb.build_llm(cfg); + ov::pass::StatefulToStateless().run_on_model(model); + model = model->clone(); + + constexpr std::size_t kSeq = 8; + constexpr std::size_t kPast = 8; + + std::map new_shapes; + for (const auto& input : model->inputs()) { + const auto& name = input.get_any_name(); + auto pshape = input.get_partial_shape(); + const auto rank = pshape.rank(); + + if (name.find("input_ids") != std::string::npos || name.find("token_type_ids") != std::string::npos) { + new_shapes[name] = ov::PartialShape{1, kSeq}; + } else if (name.find("inputs_embeds") != std::string::npos && rank.is_static() && rank.get_length() == 3) { + new_shapes[name] = ov::PartialShape{1, kSeq, pshape[2]}; + } else if (name.find("attention_mask") != std::string::npos) { + new_shapes[name] = ov::PartialShape{1, kSeq + kPast}; + } else if (name.find("position_ids") != std::string::npos) { + new_shapes[name] = + rank.get_length() == 3 ? ov::PartialShape{3, 1, kSeq} : ov::PartialShape{1, kSeq}; + } else if (name.find("beam_idx") != std::string::npos) { + new_shapes[name] = ov::PartialShape{1}; + } else if (rank.is_static() && rank.get_length() > 2) { + pshape[0] = 1; + pshape[2] = kPast; + new_shapes[name] = pshape; + } else { + new_shapes[name] = pshape; + } + } + + model->reshape(new_shapes); + model->validate_nodes_and_infer_types(); + return model; +} + +ov::AnyMap make_phase0_decode_npu_opts() { + return { + {"NPU_USE_NPUW", "YES"}, + {"NPUW_DEVICES", "NPU"}, + {"NPUW_WEIGHTS_BANK", "shared"}, + {"NPUW_FUNCALL_FOR_ALL", "YES"}, + {"NPUW_CWAI", "YES"}, + {"NPUW_ONLINE_PIPELINE", "NONE"}, + {"NPU_COMPILER_DYNAMIC_QUANTIZATION", "YES"}, + }; +} + +ov::AnyMap make_phase0_base_config() { + auto config = make_phase0_decode_npu_opts(); + config["NPUW_ENSURE_COMPATIBILITY"] = "YES"; + return config; +} + +ov::AnyMap make_phase0_cpu_subgraph_config() { + auto config = make_phase0_base_config(); + config["NPUW_DEVICES"] = "NPU,CPU"; + config["NPUW_SUBMODEL_DEVICE"] = "0:CPU"; + return config; +} + +void skip_if_no_npu(ov::Core& core) { + const auto devices = core.get_available_devices(); + if (std::find(devices.begin(), devices.end(), "NPU") == devices.end()) { + GTEST_SKIP() << "No available devices."; + } +} + +} // namespace + // FIXME: parametrize all the tests below TEST(SerializationTestNPUW, Stress_ParallelImport) { + SKIP_IF_CURRENT_TEST_IS_DISABLED(); + // Only run this test on NPU device ov::Core ov_core; auto core_devices = ov_core.get_available_devices(); @@ -79,3 +177,32 @@ TEST(SerializationTestNPUW, Stress_ParallelImport) { } } } + +TEST(SerializationTestNPUW, CompiledModelPhase0CompatibilityExportSucceedsWithStaticAttention) { + SKIP_IF_CURRENT_TEST_IS_DISABLED(); + + ov::Core ov_core; + skip_if_no_npu(ov_core); + + auto compiled = ov_core.compile_model(build_chunked_prefill_model(), "NPU", make_phase0_base_config()); + std::stringstream blob; + EXPECT_NO_THROW(compiled.export_model(blob)); +} + +TEST(SerializationTestNPUW, CompiledModelPhase0CompatibilityRejectsCpuPinnedSubgraphExport) { + SKIP_IF_CURRENT_TEST_IS_DISABLED(); + + ov::Core ov_core; + skip_if_no_npu(ov_core); + + auto compiled = ov_core.compile_model(build_chunked_prefill_model(), "NPU", make_phase0_cpu_subgraph_config()); + std::stringstream blob; + + try { + compiled.export_model(blob); + FAIL() << "Expected phase-0 compatibility export to reject a CPU-pinned subgraph"; + } catch (const ov::Exception& ex) { + EXPECT_NE(std::string(ex.what()).find("device \"CPU\""), std::string::npos) << ex.what(); + } +} + diff --git a/src/plugins/intel_npu/tests/functional/behavior/npuw/test_engine/models/model_builder.cpp b/src/plugins/intel_npu/tests/functional/behavior/npuw/test_engine/models/model_builder.cpp index 6610c8486725..eaead45711ef 100644 --- a/src/plugins/intel_npu/tests/functional/behavior/npuw/test_engine/models/model_builder.cpp +++ b/src/plugins/intel_npu/tests/functional/behavior/npuw/test_engine/models/model_builder.cpp @@ -5,8 +5,10 @@ #include "model_builder.hpp" #include +#include #include +#include "model_builder_internal.hpp" #include "openvino/core/rt_info/weightless_caching_attributes.hpp" #include "openvino/op/assign.hpp" #include "openvino/op/ops.hpp" @@ -35,470 +37,6 @@ void annotate_constants_with_weightless_cache(const std::shared_ptr& } } // namespace -// Named constants for magic values used throughout model construction. -constexpr float kRoPEBaseFrequency = 10000.0f; -constexpr float kAttentionMaskPadding = -10000.0f; -constexpr float kAttentionMaskPaddingFP16Min = -65504.0f; - -// Deterministic single fill value from tensor name (for scalars, norms, biases). -static float fill_value_from_name(const std::string& name) { - size_t h = std::hash{}(name); - return 0.01f + static_cast(h % 100000u) / 100000.0f; // [0.01, 1.01) -} - -// Deterministic xorshift32 PRNG — produces pseudo-random per-element values -// that are reproducible from the tensor name alone. -static uint32_t xorshift32(uint32_t& state) { - state ^= state << 13; - state ^= state >> 17; - state ^= state << 5; - return state; -} - -static uint32_t seed_from_name(const std::string& name) { - // Ensure non-zero seed (xorshift requires it) - uint32_t s = static_cast(std::hash{}(name)); - return s ? s : 1u; -} - -ov::Output FloatWeight::operator()(const std::string& name, - const ov::Shape& shape, - ov::element::Type compute_precision) const { - // Deterministic pseudo-random fill: each element gets a unique value derived - // from the tensor name via xorshift32. Same name always produces the same - // weights, but values look random and span a wide enough range ([-0.5, 0.5)) - // to survive FP16 quantisation through NPUW. This prevents CSE from merging - // same-shape projections and produces diverse logits in the LM head. - uint32_t state = seed_from_name(name); - size_t total = ov::shape_size(shape); - std::vector data(total); - for (size_t i = 0; i < total; ++i) { - uint32_t r = xorshift32(state); - data[i] = static_cast(r % 10000u) / 10000.0f - 0.5f; // [-0.5, 0.5) - } - auto weight = ov::opset11::Constant::create(storage_type, shape, data); - weight->set_friendly_name(name); - - if (storage_type == compute_precision) { - return weight->output(0); - } - auto convert = std::make_shared(weight, compute_precision); - convert->set_friendly_name(name + "_convert"); - return convert->output(0); -} - -ov::Output CompressedWeight::operator()(const std::string& name, - const ov::Shape& shape, - ov::element::Type compute_precision) const { - OPENVINO_ASSERT(shape.size() == 2, "CompressedWeight expects 2D shape, got ", shape.size(), "D"); - const size_t rows = shape[0]; - const size_t cols = shape[1]; - - // --- Validate pattern constraints --- - const bool has_zp = - (pattern == DCOffPattern::SYMM_ZP || pattern == DCOffPattern::GPTQ || pattern == DCOffPattern::ASYMM_ZP); - if (has_zp) { - OPENVINO_ASSERT(storage_type == ov::element::u4, - "Zero-point patterns require u4 storage type, got ", - storage_type); - } - - // Decomp element type: f32 for SYMM_NO_ZP_F32 and GPTQ, f16 otherwise. - const bool decomp_f32 = (pattern == DCOffPattern::SYMM_NO_ZP_F32 || pattern == DCOffPattern::GPTQ); - const auto decomp_et = decomp_f32 ? ov::element::f32 : ov::element::f16; - - // --- Weight value range --- - int8_t lo = 0, hi = 0; - if (storage_type == ov::element::i4) { - lo = -7; // Symmetric range [-7, 7] (not [-8, 7]) — no zero point needed. - hi = 7; - } else if (storage_type == ov::element::u4) { - lo = 1; - hi = 15; - } else { - lo = -100; - hi = 100; - } - - // --- Group quantization setup --- - const bool has_groups = group_size > 0; - size_t num_groups = 1; - if (has_groups) { - OPENVINO_ASSERT(group_size >= 64 && group_size % 64 == 0, - "Group size must be >= 64 and a multiple of 64 " - "(DCOFF AVX2 unpack constraint), got ", - group_size); - OPENVINO_ASSERT(cols >= group_size && cols % group_size == 0, - "Group quantization requires cols (", - cols, - ") >= group_size (", - group_size, - ") and evenly divisible"); - num_groups = cols / group_size; - } - - // GPTQ and ASYMM_ZP only have group-quant DCOFF patterns (Reshape2 / AsymmZP::Reshape). - // No per-channel (group_size=0) DCOFF pass exists for these pattern types. - if (pattern == DCOffPattern::GPTQ || pattern == DCOffPattern::ASYMM_ZP) { - OPENVINO_ASSERT(has_groups, - "DCOffPattern::", - (pattern == DCOffPattern::GPTQ ? "GPTQ" : "ASYMM_ZP"), - " requires group_size > 0 (no per-channel DCOFF pass exists)"); - } - - // Weight shape: 3D [rows, num_groups, group_size] for group quant, 2D [rows, cols] - // for per-channel. DCOFF patterns expect the weight Parameter to already be in the - // correct shape — no leading Reshape is recognized. - const ov::Shape weight_shape = has_groups ? ov::Shape{rows, num_groups, group_size} : ov::Shape{rows, cols}; - uint32_t w_state = seed_from_name(name); - std::vector w_data(rows * cols); - for (size_t i = 0; i < w_data.size(); ++i) { - uint32_t r = xorshift32(w_state); - int val = static_cast(lo) + static_cast(r % static_cast(hi - lo + 1)); - w_data[i] = static_cast(val); - } - auto weight = ov::opset11::Constant::create(storage_type, weight_shape, w_data); - weight->set_friendly_name(name); - - ov::Output decomp_input = weight->output(0); - - // --- Convert weight to decomp element type --- - auto convert = std::make_shared(decomp_input, decomp_et); - convert->set_friendly_name(name + "_convert"); - - ov::Output multiply_input = convert->output(0); - - // --- Zero-point subtraction (SYMM_ZP, GPTQ, ASYMM_ZP) --- - if (has_zp) { - const ov::Shape zp_shape = has_groups ? ov::Shape{rows, num_groups, 1} : ov::Shape{rows, 1}; - const size_t zp_count = has_groups ? rows * num_groups : rows; - const int mid = (static_cast(lo) + static_cast(hi)) / 2; - - if (pattern == DCOffPattern::GPTQ) { - // GPTQ: ZP is f32 Constant fed directly to Subtract (no Convert). - // Uniform value so it stays Constant after partitioning. - std::vector zp_f32(zp_count, static_cast(mid)); - auto zp_const = ov::opset11::Constant::create(ov::element::f32, zp_shape, zp_f32); - zp_const->set_friendly_name(name + "_zp"); - - auto subtract = std::make_shared(convert, zp_const); - subtract->set_friendly_name(name + "_subtract"); - multiply_input = subtract->output(0); - - } else if (pattern == DCOffPattern::SYMM_ZP) { - // SymmZP: u4 ZP Constant → Convert(f16) → Subtract. - // Uniform value across all layers so it stays Constant after partitioning. - std::vector zp_data(zp_count, static_cast(mid)); - auto zp_const = ov::opset11::Constant::create(storage_type, zp_shape, zp_data); - zp_const->set_friendly_name(name + "_zp"); - - auto zp_convert = std::make_shared(zp_const, ov::element::f16); - zp_convert->set_friendly_name(name + "_zp_convert"); - - auto subtract = std::make_shared(convert, zp_convert); - subtract->set_friendly_name(name + "_subtract"); - multiply_input = subtract->output(0); - - } else { - // AsymmZP: u4 ZP Constant → Convert(f16) → Subtract. - // Per-layer varying values (seeded from name) so NPUW promotes - // it to a Parameter after partitioning. - uint32_t zp_state = seed_from_name(name + "_zp"); - std::vector zp_data(zp_count); - for (size_t i = 0; i < zp_data.size(); ++i) { - uint32_t r = xorshift32(zp_state); - int zp_val = mid + static_cast(r % 3u) - 1; - zp_data[i] = static_cast(zp_val); - } - auto zp_const = ov::opset11::Constant::create(storage_type, zp_shape, zp_data); - zp_const->set_friendly_name(name + "_zp"); - - auto zp_convert = std::make_shared(zp_const, ov::element::f16); - zp_convert->set_friendly_name(name + "_zp_convert"); - - auto subtract = std::make_shared(convert, zp_convert); - subtract->set_friendly_name(name + "_subtract"); - multiply_input = subtract->output(0); - } - } - - // --- Scale: per-group [rows, num_groups, 1] or per-channel [rows, 1] --- - // Magnitude kept small (scale_range ≈ 1/hi) so decompressed values stay moderate - // (roughly ±1), preventing hidden state overflow. - const ov::Shape scale_shape = has_groups ? ov::Shape{rows, num_groups, 1} : ov::Shape{rows, 1}; - const size_t scale_count = has_groups ? rows * num_groups : rows; - const float scale_range = 1.0f / static_cast(hi); - uint32_t s_state = seed_from_name(name + "_scale"); - std::vector scale_data(scale_count); - for (size_t i = 0; i < scale_data.size(); ++i) { - uint32_t r = xorshift32(s_state); - scale_data[i] = scale_range * (0.1f + static_cast(r % 1000u) / 1000.0f); - } - auto scale = ov::opset11::Constant::create(decomp_et, scale_shape, scale_data); - scale->set_friendly_name(name + "_scale"); - - auto scaled = std::make_shared(multiply_input, scale); - scaled->set_friendly_name(name + "_decompress"); - - ov::Output decompressed = scaled->output(0); - - // --- Group quant: Reshape 3D → 2D [rows, cols] --- - if (has_groups) { - auto out_shape = - ov::opset11::Constant::create(ov::element::i64, - ov::Shape{2}, - std::vector{static_cast(rows), static_cast(cols)}); - auto reshaped = std::make_shared(decompressed, out_shape, false); - reshaped->set_friendly_name(name + "_reshape"); - decompressed = reshaped->output(0); - } - - // --- Convert to compute precision if needed --- - if (decomp_et != compute_precision) { - auto to_compute = std::make_shared(decompressed, compute_precision); - to_compute->set_friendly_name(name + "_to_compute"); - return to_compute->output(0); - } - return decompressed; -} - -ov::Output LayerNorm::operator()(const ov::Output& input, const std::string& name) const { - float w_val = 1.0f + fill_value_from_name(name + ".weight") * 0.1f; - float b_val = fill_value_from_name(name + ".bias") * 0.01f; - auto weight = - ov::opset11::Constant::create(precision, ov::Shape{hidden_size}, std::vector(hidden_size, w_val)); - weight->set_friendly_name(name + ".weight"); - - auto bias = - ov::opset11::Constant::create(precision, ov::Shape{hidden_size}, std::vector(hidden_size, b_val)); - bias->set_friendly_name(name + ".bias"); - - auto axes = ov::opset11::Constant::create(ov::element::i64, ov::Shape{1}, {-1}); - - auto mvn = std::make_shared(input, axes, true, eps, ov::op::MVNEpsMode::INSIDE_SQRT); - mvn->set_friendly_name(name + "_mvn"); - - auto mul = std::make_shared(mvn, weight); - - auto add = std::make_shared(mul, bias); - add->set_friendly_name(name); - - return add->output(0); -} - -ov::Output RMSNorm::operator()(const ov::Output& input, const std::string& name) const { - float w_val = 1.0f + fill_value_from_name(name + ".weight") * 0.1f; - auto weight = - ov::opset11::Constant::create(precision, ov::Shape{hidden_size}, std::vector(hidden_size, w_val)); - weight->set_friendly_name(name + ".weight"); - - auto squared = std::make_shared(input, input); - - auto axes = ov::opset11::Constant::create(ov::element::i64, ov::Shape{1}, {-1}); - - auto mean = std::make_shared(squared, axes, true); - - auto eps_const = ov::opset11::Constant::create(precision, ov::Shape{}, {eps}); - - auto mean_eps = std::make_shared(mean, eps_const); - - auto rsqrt = std::make_shared(mean_eps); - - auto normalized = std::make_shared(input, rsqrt); - - auto scaled = std::make_shared(normalized, weight); - scaled->set_friendly_name(name); - - return scaled->output(0); -} - -/// Builds frequency cos/sin chain matching NPUW's AddPositionIdsNode pattern. -struct RoPEFrequencies { - ov::Output cos, sin; -}; - -static RoPEFrequencies build_rope_frequencies(size_t head_dim, - ov::element::Type precision, - const ov::Output& position_ids, - const std::string& prefix = "model.rope") { - const size_t half_dim = head_dim / 2; - - std::vector inv_freq_data(half_dim); - for (size_t i = 0; i < half_dim; ++i) { - inv_freq_data[i] = - 1.0f / std::pow(kRoPEBaseFrequency, static_cast(2 * i) / static_cast(head_dim)); - } - auto inv_freq = ov::opset11::Constant::create(ov::element::f32, ov::Shape{1, half_dim, 1}, inv_freq_data); - inv_freq->set_friendly_name(prefix + ".inv_freq"); - - // position_ids [batch, seq] -> Unsqueeze -> Convert(f32) -> MatMul(inv_freq) - // -> Transpose -> Concat(self,self) -> Sin/Cos -> Unsqueeze [batch, 1, seq, head_dim] - auto unsq_axis = ov::opset11::Constant::create(ov::element::i64, ov::Shape{1}, {1}); - auto unsqueezed = std::make_shared(position_ids, unsq_axis); - unsqueezed->set_friendly_name(prefix + ".pos_unsqueeze"); - - auto converted = std::make_shared(unsqueezed, ov::element::f32); - converted->set_friendly_name(prefix + ".pos_convert"); - - auto matmul = std::make_shared(inv_freq, converted, false, false); - matmul->set_friendly_name(prefix + ".freq_matmul"); - - auto perm = ov::opset11::Constant::create(ov::element::i64, ov::Shape{3}, std::vector{0, 2, 1}); - auto transposed = std::make_shared(matmul, perm); - transposed->set_friendly_name(prefix + ".freq_transpose"); - - auto concat = - std::make_shared(ov::OutputVector{transposed->output(0), transposed->output(0)}, -1); - concat->set_friendly_name(prefix + ".freq_concat"); - - auto sin_node = std::make_shared(concat); - sin_node->set_friendly_name(prefix + ".sin"); - auto cos_node = std::make_shared(concat); - cos_node->set_friendly_name(prefix + ".cos"); - - auto head_axis = ov::opset11::Constant::create(ov::element::i64, ov::Shape{1}, {1}); - - auto cos_unsq = std::make_shared(cos_node, head_axis); - cos_unsq->set_friendly_name(prefix + ".cos_unsqueeze"); - - auto sin_unsq = std::make_shared(sin_node, head_axis); - sin_unsq->set_friendly_name(prefix + ".sin_unsqueeze"); - - ov::Output cos_out = cos_unsq->output(0); - ov::Output sin_out = sin_unsq->output(0); - if (precision != ov::element::f32) { - auto cos_cvt = std::make_shared(cos_unsq, precision); - cos_cvt->set_friendly_name(prefix + ".cos_convert"); - cos_out = cos_cvt->output(0); - - auto sin_cvt = std::make_shared(sin_unsq, precision); - sin_cvt->set_friendly_name(prefix + ".sin_convert"); - sin_out = sin_cvt->output(0); - } - - return {cos_out, sin_out}; -} - -HalfRotationRoPE::HalfRotationRoPE(size_t hd, ov::element::Type precision, const ov::Output& position_ids) - : head_dim(hd) { - auto freq = build_rope_frequencies(hd, precision, position_ids); - cos_freq = freq.cos; - sin_freq = freq.sin; -} - -ov::Output HalfRotationRoPE::operator()(const ov::Output& input, const std::string& name) const { - const int64_t half = static_cast(head_dim / 2); - - auto zero = ov::opset11::Constant::create(ov::element::i64, ov::Shape{1}, {0}); - auto half_const = ov::opset11::Constant::create(ov::element::i64, ov::Shape{1}, {half}); - auto full_const = ov::opset11::Constant::create(ov::element::i64, ov::Shape{1}, {static_cast(head_dim)}); - auto step = ov::opset11::Constant::create(ov::element::i64, ov::Shape{1}, {1}); - auto last_axis = ov::opset11::Constant::create(ov::element::i64, ov::Shape{1}, {-1}); - - auto first_half = std::make_shared(input, zero, half_const, step, last_axis); - first_half->set_friendly_name(name + "_first_half"); - - auto second_half = std::make_shared(input, half_const, full_const, step, last_axis); - second_half->set_friendly_name(name + "_second_half"); - - auto neg_one = ov::opset11::Constant::create(input.get_element_type(), ov::Shape{}, {-1.0f}); - auto neg_second = std::make_shared(second_half, neg_one); - neg_second->set_friendly_name(name + "_neg_second"); - - auto rotated = - std::make_shared(ov::OutputVector{neg_second->output(0), first_half->output(0)}, -1); - rotated->set_friendly_name(name + "_rotated"); - - auto input_cos = std::make_shared(input, cos_freq); - input_cos->set_friendly_name(name + "_input_cos"); - - auto rotated_sin = std::make_shared(rotated, sin_freq); - rotated_sin->set_friendly_name(name + "_rotated_sin"); - - auto output = std::make_shared(input_cos, rotated_sin); - output->set_friendly_name(name); - - return output->output(0); -} - -InterleavedRoPE::InterleavedRoPE(size_t hd, ov::element::Type precision, const ov::Output& position_ids) - : head_dim(hd) { - auto freq = build_rope_frequencies(hd, precision, position_ids); - cos_freq = freq.cos; - sin_freq = freq.sin; -} - -ov::Output InterleavedRoPE::operator()(const ov::Output& input, const std::string& name) const { - const int64_t half_dim = static_cast(head_dim / 2); - - auto reshape_5d = - ov::opset11::Constant::create(ov::element::i64, ov::Shape{5}, std::vector{0, 0, 0, half_dim, 2}); - - auto reshaped = std::make_shared(input, reshape_5d, true); - reshaped->set_friendly_name(name + "_reshape_5d"); - - auto zero = ov::opset11::Constant::create(ov::element::i64, ov::Shape{1}, {0}); - auto one = ov::opset11::Constant::create(ov::element::i64, ov::Shape{1}, {1}); - auto two = ov::opset11::Constant::create(ov::element::i64, ov::Shape{1}, {2}); - auto step = ov::opset11::Constant::create(ov::element::i64, ov::Shape{1}, {1}); - auto last_axis = ov::opset11::Constant::create(ov::element::i64, ov::Shape{1}, {-1}); - - auto x_even = std::make_shared(reshaped, zero, one, step, last_axis); - x_even->set_friendly_name(name + "_x_even"); - - auto x_odd = std::make_shared(reshaped, one, two, step, last_axis); - x_odd->set_friendly_name(name + "_x_odd"); - - auto neg_one = ov::opset11::Constant::create(input.get_element_type(), ov::Shape{}, {-1.0f}); - - auto neg_x_odd = std::make_shared(x_odd, neg_one); - neg_x_odd->set_friendly_name(name + "_neg_x_odd"); - - auto rotated_pairs = std::make_shared(ov::OutputVector{neg_x_odd, x_even}, -1); - rotated_pairs->set_friendly_name(name + "_rotated_pairs"); - - auto reshape_4d = ov::opset11::Constant::create(ov::element::i64, - ov::Shape{4}, - std::vector{0, 0, 0, static_cast(head_dim)}); - - auto rotated = std::make_shared(rotated_pairs, reshape_4d, true); - rotated->set_friendly_name(name + "_rotated"); - - auto input_cos = std::make_shared(input, cos_freq); - input_cos->set_friendly_name(name + "_input_cos"); - - auto rotated_sin = std::make_shared(rotated, sin_freq); - rotated_sin->set_friendly_name(name + "_rotated_sin"); - - auto output = std::make_shared(input_cos, rotated_sin); - output->set_friendly_name(name); - - return output->output(0); -} - -ov::Output make_position_ids_2d() { - auto param = std::make_shared(ov::element::i64, ov::PartialShape{-1, -1}); - param->set_friendly_name("position_ids"); - param->output(0).set_names({"position_ids"}); - return param->output(0); -} - -ov::Output make_position_ids_3d() { - auto param = std::make_shared(ov::element::i64, ov::PartialShape{3, -1, -1}); - param->set_friendly_name("position_ids"); - param->output(0).set_names({"position_ids"}); - - auto indices = ov::opset11::Constant::create(ov::element::i64, ov::Shape{1}, {0}); - auto axis = ov::opset11::Constant::create(ov::element::i64, ov::Shape{}, {0}); - auto gather = std::make_shared(param, indices, axis); - - auto squeeze_axes = ov::opset11::Constant::create(ov::element::i64, ov::Shape{1}, {0}); - auto squeeze = std::make_shared(gather, squeeze_axes); - squeeze->set_friendly_name("position_ids_2d"); - - return squeeze->output(0); -} - ov::Output make_linear(const ov::Output& input, size_t in_features, size_t out_features, @@ -521,195 +59,6 @@ ov::Output make_linear(const ov::Output& input, return matmul->output(0); } -ov::Output make_multihead_reshape(const ov::Output& input, - size_t num_heads, - size_t head_dim, - const std::string& name) { - auto shape = ov::opset11::Constant::create( - ov::element::i64, - ov::Shape{4}, - std::vector{0, -1, static_cast(num_heads), static_cast(head_dim)}); - - auto reshape = std::make_shared(input, shape, true); - reshape->set_friendly_name(name); - - return reshape->output(0); -} - -ov::Output make_attention_transpose(const ov::Output& input, const std::string& name) { - auto order_const = ov::opset11::Constant::create(ov::element::i64, ov::Shape{4}, {0, 2, 1, 3}); - - auto transpose = std::make_shared(input, order_const); - transpose->set_friendly_name(name); - - return transpose->output(0); -} - -ov::Output make_repeat_kv(const ov::Output& kv, - size_t num_heads, - size_t num_kv_heads, - size_t head_dim, - const std::string& name, - const ov::Output& shared_broadcast_shape) { - const size_t actual_kv_heads = (num_kv_heads == 0) ? num_heads : num_kv_heads; - const size_t n_rep = num_heads / actual_kv_heads; - - if (!shared_broadcast_shape.get_node() && n_rep == 1) { - return kv; - } - - OPENVINO_ASSERT(num_heads % actual_kv_heads == 0, - "num_heads (", - num_heads, - ") must be divisible by num_kv_heads (", - actual_kv_heads, - ")"); - - ov::Output broadcast_shape_output; - if (shared_broadcast_shape.get_node()) { - broadcast_shape_output = shared_broadcast_shape; - } else { - auto shape_of_kv = std::make_shared(kv, ov::element::i64); - auto gather_axis = ov::opset11::Constant::create(ov::element::i64, ov::Shape{}, {0}); - auto idx_01 = ov::opset11::Constant::create(ov::element::i64, ov::Shape{2}, {0, 1}); - auto batch_kv_heads = std::make_shared(shape_of_kv, idx_01, gather_axis); - auto n_rep_const = ov::opset11::Constant::create(ov::element::i64, ov::Shape{1}, {static_cast(n_rep)}); - auto idx_23 = ov::opset11::Constant::create(ov::element::i64, ov::Shape{2}, {2, 3}); - auto seq_head_dim = std::make_shared(shape_of_kv, idx_23, gather_axis); - auto broadcast_shape = - std::make_shared(ov::OutputVector{batch_kv_heads, n_rep_const, seq_head_dim}, 0); - broadcast_shape->set_friendly_name(name + "_broadcast_shape"); - broadcast_shape_output = broadcast_shape->output(0); - } - - auto unsqueeze_axis = ov::opset11::Constant::create(ov::element::i64, ov::Shape{1}, {2}); - - auto unsqueezed = std::make_shared(kv, unsqueeze_axis); - unsqueezed->set_friendly_name(name + "_unsqueeze"); - - auto broadcasted = std::make_shared(unsqueezed, - broadcast_shape_output, - ov::op::BroadcastType::BIDIRECTIONAL); - broadcasted->set_friendly_name(name + "_broadcast"); - - auto new_shape = ov::opset11::Constant::create( - ov::element::i64, - ov::Shape{4}, - std::vector{0, static_cast(num_heads), -1, static_cast(head_dim)}); - new_shape->set_friendly_name(name + "_shape"); - - auto reshaped = std::make_shared(broadcasted, new_shape, true); - reshaped->set_friendly_name(name); - - return reshaped->output(0); -} - -KVCacheReadState make_kv_cache_read(const ov::Output& batch_source, - const ov::Output& beam_idx, - size_t num_heads, - size_t head_dim, - const std::string& name, - ov::element::Type precision) { - auto var_shape = ov::PartialShape{-1, static_cast(num_heads), -1, static_cast(head_dim)}; - auto variable = std::make_shared(ov::op::util::VariableInfo{var_shape, precision, name}); - - auto shape_of = std::make_shared(batch_source, ov::element::i64); - shape_of->set_friendly_name(name + "_shapeof"); - - auto zero_idx = ov::opset11::Constant::create(ov::element::i64, ov::Shape{1}, std::vector{0}); - auto gather_axis = ov::opset11::Constant::create(ov::element::i64, ov::Shape{}, std::vector{0}); - - auto batch_dim = std::make_shared(shape_of, zero_idx, gather_axis); - batch_dim->set_friendly_name(name + "_batch_dim"); - - auto num_heads_const = ov::opset11::Constant::create(ov::element::i64, - ov::Shape{1}, - std::vector{static_cast(num_heads)}); - auto zero_seq = ov::opset11::Constant::create(ov::element::i64, ov::Shape{1}, std::vector{0}); - auto head_dim_const = ov::opset11::Constant::create(ov::element::i64, - ov::Shape{1}, - std::vector{static_cast(head_dim)}); - - auto init_shape = std::make_shared(ov::OutputVector{batch_dim->output(0), - num_heads_const->output(0), - zero_seq->output(0), - head_dim_const->output(0)}, - 0); - init_shape->set_friendly_name(name + "_init_shape"); - - auto zero_scalar = ov::opset11::Constant::create(precision, ov::Shape{}, std::vector{0.0f}); - - auto init_value = std::make_shared(zero_scalar, init_shape); - init_value->set_friendly_name(name + "_init"); - - auto read_value = std::make_shared(init_value, variable); - read_value->set_friendly_name(name + "_read"); - - auto beam_gather_axis = ov::opset11::Constant::create(ov::element::i64, ov::Shape{}, std::vector{0}); - - auto beam_gather = std::make_shared(read_value, beam_idx, beam_gather_axis); - beam_gather->set_friendly_name(name + "_beam_gather"); - - return {variable, beam_gather->output(0)}; -} - -KVCacheResult make_kv_cache_concat(const ov::Output& current_kv, - const ov::Output& batch_source, - const ov::Output& beam_idx, - size_t num_heads, - size_t head_dim, - const std::string& name, - ov::element::Type precision) { - auto read_state = make_kv_cache_read(batch_source, beam_idx, num_heads, head_dim, name, precision); - - auto concat = std::make_shared(ov::OutputVector{read_state.beam_gather, current_kv}, 2); - concat->set_friendly_name(name + "_concat"); - - auto assign = std::make_shared(concat, read_state.variable); - assign->set_friendly_name(name + "_assign"); - - return {concat->output(0), read_state.beam_gather, assign}; -} - -ov::Output make_sdpa(const ov::Output& q, - const ov::Output& k, - const ov::Output& v, - const std::string& name, - const ov::Output& attention_mask, - size_t head_dim_for_scale) { - std::shared_ptr sdpa; - if (head_dim_for_scale > 0 && attention_mask.get_node()) { - // 5-input SDPA: Q, K, V, mask, scale (required for embedding model pattern matching) - auto scale_val = 1.0f / std::sqrt(static_cast(head_dim_for_scale)); - auto scale = ov::opset11::Constant::create(ov::element::f32, ov::Shape{}, {scale_val}); - scale->set_friendly_name(name + ".scale"); - sdpa = std::make_shared(q, k, v, attention_mask, scale, false); - } else if (attention_mask.get_node()) { - sdpa = std::make_shared(q, k, v, attention_mask, false); - } else { - sdpa = std::make_shared(q, k, v, false); - } - sdpa->set_friendly_name(name + ".sdpa"); - - return sdpa->output(0); -} - -ov::Output make_attention_output(const ov::Output& sdpa_output, - size_t hidden_size, - const std::string& name, - ov::element::Type precision, - const WeightFn& weight_fn, - const WeightFn& bias_fn) { - auto attn_trans = make_attention_transpose(sdpa_output, name + "_transpose"); - - auto reshape_shape = ov::opset11::Constant::create(ov::element::i64, - ov::Shape{3}, - std::vector{0, -1, static_cast(hidden_size)}); - auto attn_reshaped = std::make_shared(attn_trans, reshape_shape, true); - attn_reshaped->set_friendly_name(name + "_reshape"); - - return make_linear(attn_reshaped->output(0), hidden_size, hidden_size, name, precision, weight_fn, bias_fn); -} ov::Output make_embedding(const ov::Output& input_ids, size_t vocab_size, @@ -778,51 +127,6 @@ ov::Output make_conv1d(const ov::Output& input, return add->output(0); } -KVCacheResult make_encoder_kv_cache(const ov::Output& encoder_kv, - size_t num_heads, - size_t head_dim, - const std::string& name, - ov::element::Type precision) { - auto var_shape = ov::PartialShape{-1, static_cast(num_heads), -1, static_cast(head_dim)}; - auto variable = std::make_shared(ov::op::util::VariableInfo{var_shape, precision, name}); - - // No Gather/beam reorder — encoder KV is identical across beams - auto read_value = std::make_shared(encoder_kv, variable); - read_value->set_friendly_name(name + "_read"); - - auto assign = std::make_shared(read_value, variable); - assign->set_friendly_name(name + "_assign"); - - return {read_value->output(0), {}, assign}; -} - -ov::Output SwiGLU::operator()(const ov::Output& input, const std::string& name) const { - auto gate = make_linear(input, hidden_size, intermediate_size, name + ".gate_proj", precision, weight_fn); - auto up = make_linear(input, hidden_size, intermediate_size, name + ".up_proj", precision, weight_fn); - - auto sigmoid = std::make_shared(gate); - - auto silu = std::make_shared(gate, sigmoid); - silu->set_friendly_name(name + "_silu"); - - auto gate_up = std::make_shared(silu, up); - gate_up->set_friendly_name(name + "_gate_up"); - - auto down = make_linear(gate_up, intermediate_size, hidden_size, name + ".down_proj", precision, weight_fn); - - return down; -} - -ov::Output GELU::operator()(const ov::Output& input, const std::string& name) const { - auto up = make_linear(input, hidden_size, intermediate_size, name + ".up_proj", precision, weight_fn, bias_fn); - - auto gelu = std::make_shared(up); - gelu->set_friendly_name(name + "_gelu"); - - auto down = make_linear(gelu, intermediate_size, hidden_size, name + ".down_proj", precision, weight_fn, bias_fn); - - return down; -} ov::Output make_transformer_layers(const ov::Output& initial, size_t num_layers, @@ -836,179 +140,7 @@ ov::Output make_transformer_layers(const ov::Output& initial return current; } -/// Shared GQA broadcast shape — ReConstructEmbeddingModel requires pointer equality across SDPAs. -static ov::Output make_shared_gqa_broadcast(const ov::Output& shape_source, - size_t kv_heads, - size_t num_heads, - size_t head_dim) { - const size_t n_rep = num_heads / kv_heads; - auto shape_of = std::make_shared(shape_source, ov::element::i64); - auto axis0 = ov::opset11::Constant::create(ov::element::i64, ov::Shape{}, {0}); - auto idx0 = ov::opset11::Constant::create(ov::element::i64, ov::Shape{1}, {0}); - auto batch_dim = std::make_shared(shape_of, idx0, axis0); - auto idx1 = ov::opset11::Constant::create(ov::element::i64, ov::Shape{1}, {1}); - auto seq_dim = std::make_shared(shape_of, idx1, axis0); - auto kv_heads_const = - ov::opset11::Constant::create(ov::element::i64, ov::Shape{1}, {static_cast(kv_heads)}); - auto n_rep_const = ov::opset11::Constant::create(ov::element::i64, ov::Shape{1}, {static_cast(n_rep)}); - auto head_dim_const = - ov::opset11::Constant::create(ov::element::i64, ov::Shape{1}, {static_cast(head_dim)}); - auto shared_concat = std::make_shared( - ov::OutputVector{batch_dim, kv_heads_const, n_rep_const, seq_dim, head_dim_const}, - 0); - shared_concat->set_friendly_name("model.shared_gqa_broadcast_shape"); - return shared_concat->output(0); -} - -/// Missing separator (e.g. "keypresent") is intentional — matches OV's StatefulToStateless pass. -static std::string make_kv_var_id(const std::string& layer, const std::string& infix, const std::string& kv_type) { - return "past_key_values." + layer + infix + kv_type + "present." + layer + infix + kv_type; -} - -ov::Output Attention::operator()(const ov::Output& q, - const ov::Output& k, - const ov::Output& v, - const std::string& prefix, - size_t layer_idx) const { - auto q_reshaped = make_multihead_reshape(q, num_heads, head_dim, prefix + attn_prefix + "q_reshape"); - auto k_reshaped = make_multihead_reshape(k, num_kv_heads, head_dim, prefix + attn_prefix + "k_reshape"); - auto v_reshaped = make_multihead_reshape(v, num_kv_heads, head_dim, prefix + attn_prefix + "v_reshape"); - - ov::Output q_normed = q_reshaped; - ov::Output k_normed = k_reshaped; - if (qk_norm) { - q_normed = qk_norm(q_reshaped, prefix + attn_prefix + "q_norm"); - k_normed = qk_norm(k_reshaped, prefix + attn_prefix + "k_norm"); - } - - // [batch, seq, heads, dim] -> [batch, heads, seq, dim] - auto q_trans = make_attention_transpose(q_normed, prefix + attn_prefix + "q_transpose"); - auto k_trans = make_attention_transpose(k_normed, prefix + attn_prefix + "k_transpose"); - auto v_trans = make_attention_transpose(v_reshaped, prefix + attn_prefix + "v_transpose"); - - ov::Output q_roped = q_trans; - ov::Output k_roped = k_trans; - if (rope_fn) { - q_roped = rope_fn(q_trans, prefix + "q_rope"); - k_roped = rope_fn(k_trans, prefix + "k_rope"); - } - - ov::Output k_for_attn = k_roped; - ov::Output v_for_attn = v_trans; - if (kv_cache_fn) { - std::tie(k_for_attn, v_for_attn) = kv_cache_fn(k_roped, v_trans, layer_idx); - } - - auto k_expanded = - make_repeat_kv(k_for_attn, num_heads, num_kv_heads, head_dim, prefix + "k_repeat", shared_broadcast_shape); - auto v_expanded = - make_repeat_kv(v_for_attn, num_heads, num_kv_heads, head_dim, prefix + "v_repeat", shared_broadcast_shape); - - // 5-input SDPA with explicit scale needed for embedding model pattern matching - size_t sdpa_scale_dim = shared_broadcast_shape.get_node() ? head_dim : 0; - auto attn_output = - make_sdpa(q_roped, k_expanded, v_expanded, prefix + attn_prefix + "attn", sdpa_mask, sdpa_scale_dim); - - return make_attention_output(attn_output, hidden_size, prefix + o_proj_name, precision, weight_fn, bias_fn); -} - -ov::Output Attention::operator()(const ov::Output& input, - const ov::Output& kv_input, - const std::string& prefix, - size_t layer_idx) const { - auto kv_src = kv_input.get_node() ? kv_input : input; - size_t kv_dim = num_kv_heads * head_dim; - auto q = - make_linear(input, hidden_size, hidden_size, prefix + attn_prefix + "q_proj", precision, weight_fn, bias_fn); - auto k = make_linear(kv_src, hidden_size, kv_dim, prefix + attn_prefix + "k_proj", precision, weight_fn, bias_fn); - auto v = make_linear(kv_src, hidden_size, kv_dim, prefix + attn_prefix + "v_proj", precision, weight_fn, bias_fn); - return (*this)(q, k, v, prefix, layer_idx); -} - -/// Padding-only mask: [batch, seq] -> [batch, 1, 1, seq] float (0.0=attend, -10000.0=pad) -static ov::Output make_padding_mask(const ov::Output& attention_mask_output, - ov::element::Type prec) { - auto mask_float = std::make_shared(attention_mask_output, prec); - mask_float->set_friendly_name("model.mask_convert"); - - auto one_const = ov::opset11::Constant::create(prec, ov::Shape{}, {1.0f}); - auto inv_mask = std::make_shared(one_const, mask_float); - inv_mask->set_friendly_name("model.mask_invert"); - - auto neg_inf = ov::opset11::Constant::create(prec, ov::Shape{}, {kAttentionMaskPadding}); - auto padding_mask = std::make_shared(inv_mask, neg_inf); - padding_mask->set_friendly_name("model.padding_mask"); - - auto pad_shape = ov::opset11::Constant::create(ov::element::i64, ov::Shape{4}, std::vector{0, 1, 1, -1}); - auto padding_4d = std::make_shared(padding_mask, pad_shape, true); - padding_4d->set_friendly_name("model.padding_mask_4d"); - - return padding_4d->output(0); -} - -static ov::Output make_causal_mask(const ov::Output& input_ids_output, - const ov::Output& attention_mask_output, - ov::element::Type prec) { - auto padding_4d = make_padding_mask(attention_mask_output, prec); - - auto ids_shape = std::make_shared(input_ids_output, ov::element::i64); - ids_shape->set_friendly_name("model.ids_shape"); - - auto mask_shape_node = std::make_shared(attention_mask_output, ov::element::i64); - mask_shape_node->set_friendly_name("model.mask_shape"); - - auto idx1 = ov::opset11::Constant::create(ov::element::i64, ov::Shape{}, {1}); - auto gather_axis = ov::opset11::Constant::create(ov::element::i64, ov::Shape{}, {0}); - auto seq_len_s = std::make_shared(ids_shape, idx1, gather_axis); - seq_len_s->set_friendly_name("model.seq_len"); - - auto total_seq_s = std::make_shared(mask_shape_node, idx1, gather_axis); - total_seq_s->set_friendly_name("model.total_seq"); - - auto offset = std::make_shared(total_seq_s, seq_len_s); - offset->set_friendly_name("model.causal_offset"); - - auto range_start = ov::opset11::Constant::create(ov::element::i64, ov::Shape{}, {0}); - auto range_step = ov::opset11::Constant::create(ov::element::i64, ov::Shape{}, {1}); - - auto kv_range = std::make_shared(range_start, total_seq_s, range_step, ov::element::i64); - kv_range->set_friendly_name("model.kv_range"); - - auto q_range = std::make_shared(range_start, seq_len_s, range_step, ov::element::i64); - q_range->set_friendly_name("model.q_range"); - - auto q_abs = std::make_shared(q_range, offset); - q_abs->set_friendly_name("model.q_abs_positions"); - - auto axis_last = ov::opset11::Constant::create(ov::element::i64, ov::Shape{1}, {1}); - auto axis_first = ov::opset11::Constant::create(ov::element::i64, ov::Shape{1}, {0}); - - auto q_col = std::make_shared(q_abs, axis_last); - q_col->set_friendly_name("model.q_col"); - - auto kv_row = std::make_shared(kv_range, axis_first); - kv_row->set_friendly_name("model.kv_row"); - - auto causal_bool = std::make_shared(kv_row, q_col); - causal_bool->set_friendly_name("model.causal_bool"); - - auto select_true = ov::opset11::Constant::create(prec, ov::Shape{}, {0.0f}); - auto select_false = ov::opset11::Constant::create(prec, ov::Shape{}, {kAttentionMaskPadding}); - - auto causal_float = std::make_shared(causal_bool, select_true, select_false); - causal_float->set_friendly_name("model.causal_mask"); - - auto unsqueeze_axes = ov::opset11::Constant::create(ov::element::i64, ov::Shape{2}, {0, 1}); - - auto causal_4d = std::make_shared(causal_float, unsqueeze_axes); - causal_4d->set_friendly_name("model.causal_mask_4d"); - - auto combined = std::make_shared(padding_4d, causal_4d); - combined->set_friendly_name("model.mask_4d"); - - return combined->output(0); -} std::shared_ptr ModelBuilder::get_model_with_one_op() { auto param = std::make_shared(ov::element::i64, ov::PartialShape{1, 3, 2, 2}); @@ -1544,8 +676,23 @@ std::shared_ptr ModelBuilder::build_llm(const LLMConfig& config_in) { LLMConfig config = config_in; if (!config.norm) config.norm = LayerNorm(config.hidden_size, config.precision); - if (!config.ffn) - config.ffn = SwiGLU(config.hidden_size, config.intermediate_size, config.precision, config.weight); + if (!config.ffn) { + if (config.num_experts > 0) { + size_t moe_inter = config.moe_intermediate_size > 0 + ? config.moe_intermediate_size + : config.intermediate_size; + size_t moe_k = config.num_experts_per_tok > 0 + ? config.num_experts_per_tok + : std::min(2, config.num_experts); + OPENVINO_ASSERT(moe_k >= 1 && moe_k <= config.num_experts, + "Invalid MoE config: num_experts_per_tok (", + moe_k, ") must be in [1, num_experts (", config.num_experts, ")]"); + config.ffn = MoEFFN(config.hidden_size, moe_inter, config.num_experts, + moe_k, config.precision); + } else { + config.ffn = SwiGLU(config.hidden_size, config.intermediate_size, config.precision, config.weight); + } + } const auto prec = config.precision; auto attention_mask = parameter(ov::element::i64, ov::PartialShape{-1, -1}, "attention_mask"); @@ -1577,7 +724,7 @@ std::shared_ptr ModelBuilder::build_llm(const LLMConfig& config_in) { // Shared GQA broadcast shape (embedding models only) ov::Output shared_broadcast; - if (!config.use_kv_cache && !config.lm_head_weight) { + if ((!config.use_kv_cache && !config.lm_head_weight) || config.force_gqa_broadcast) { shared_broadcast = make_shared_gqa_broadcast(attention_mask->output(0), config.get_kv_heads(), config.num_heads, @@ -1745,176 +892,6 @@ std::shared_ptr ModelBuilder::build_whisper_encoder(const WhisperConf return make_model(encoder_output, "last_hidden_state", "synthetic_whisper_encoder"); } -struct CachePositionResult { - ov::Output position_ids; // [batch, seq] - ov::Output total_seq_len; - ov::Output seq_len; - ov::Output cache_pos_unsq; // [1, seq] (needed for causal mask) - ov::Output ids_shape; // ShapeOf(input_ids) -}; - -static CachePositionResult make_cache_position_ids(const ov::Output& input_ids, - const ov::Output& kv_cache_beam_gather, - const std::string& prefix) { - auto ids_shape = std::make_shared(input_ids, ov::element::i64); - ids_shape->set_friendly_name(prefix + "ids_shape"); - auto seq_len = - std::make_shared(ids_shape, - ov::opset11::Constant::create(ov::element::i64, ov::Shape{}, {1}), - ov::opset11::Constant::create(ov::element::i64, ov::Shape{}, {0})); - seq_len->set_friendly_name(prefix + "seq_len"); - - // kv_seq_len = ShapeOf(beam_gather)[2] — root of the CachePositionInput pattern - auto kv_shape = std::make_shared(kv_cache_beam_gather, ov::element::i64); - kv_shape->set_friendly_name(prefix + "kv_shape"); - auto kv_seq_len = - std::make_shared(kv_shape, - ov::opset11::Constant::create(ov::element::i64, ov::Shape{}, {2}), - ov::opset11::Constant::create(ov::element::i64, ov::Shape{}, {0})); - kv_seq_len->set_friendly_name(prefix + "kv_seq_len"); - - // CachePositionInput pattern: Gather -> Add -> Range -> Unsqueeze -> Tile - auto total_seq_len = std::make_shared(kv_seq_len, seq_len); - total_seq_len->set_friendly_name(prefix + "total_seq_len"); - - auto cache_positions = std::make_shared( - kv_seq_len->output(0), - total_seq_len->output(0), - ov::opset11::Constant::create(ov::element::i64, ov::Shape{}, {1})->output(0), - ov::element::i64); - cache_positions->set_friendly_name(prefix + "cache_positions"); - - auto cache_pos_unsq = - std::make_shared(cache_positions, - ov::opset11::Constant::create(ov::element::i64, ov::Shape{}, {0})); - cache_pos_unsq->set_friendly_name(prefix + "cache_pos_unsq"); - - auto batch_dim_for_tile = - std::make_shared(ids_shape, - ov::opset11::Constant::create(ov::element::i64, ov::Shape{1}, {0}), - ov::opset11::Constant::create(ov::element::i64, ov::Shape{}, {0})); - batch_dim_for_tile->set_friendly_name(prefix + "batch_for_tile"); - - auto tile_repeats = std::make_shared( - ov::OutputVector{batch_dim_for_tile->output(0), - ov::opset11::Constant::create(ov::element::i64, ov::Shape{1}, {1})->output(0)}, - 0); - tile_repeats->set_friendly_name(prefix + "tile_repeats"); - - auto position_ids = std::make_shared(cache_pos_unsq, tile_repeats); - position_ids->set_friendly_name(prefix + "position_ids"); - - return {position_ids->output(0), - total_seq_len->output(0), - seq_len->output(0), - cache_pos_unsq->output(0), - ids_shape->output(0)}; -} - -static ov::Output make_whisper_causal_mask(const CachePositionResult& cache_pos, const std::string& prefix) { - // kv_idx: Range -> 3x Unsqueeze -> [1, 1, 1, total_seq] - auto mask_range = std::make_shared( - ov::opset11::Constant::create(ov::element::i64, ov::Shape{}, {0})->output(0), - cache_pos.total_seq_len, - ov::opset11::Constant::create(ov::element::i64, ov::Shape{}, {1})->output(0), - ov::element::i64); - mask_range->set_friendly_name(prefix + "mask_range"); - - auto kv_unsq1 = - std::make_shared(mask_range, - ov::opset11::Constant::create(ov::element::i64, ov::Shape{}, {0})); - kv_unsq1->set_friendly_name(prefix + "kv_unsq1"); - auto kv_unsq2 = - std::make_shared(kv_unsq1, - ov::opset11::Constant::create(ov::element::i64, ov::Shape{}, {1})); - kv_unsq2->set_friendly_name(prefix + "kv_unsq2"); - auto kv_unsq3 = - std::make_shared(kv_unsq2, - ov::opset11::Constant::create(ov::element::i64, ov::Shape{}, {2})); - kv_unsq3->set_friendly_name(prefix + "kv_unsq3"); - - // q_idx: cache_pos_unsq -> 2x Unsqueeze -> [1, 1, seq, 1] - auto q_unsq1 = - std::make_shared(cache_pos.cache_pos_unsq, - ov::opset11::Constant::create(ov::element::i64, ov::Shape{}, {1})); - q_unsq1->set_friendly_name(prefix + "q_unsq1"); - auto q_unsq2 = - std::make_shared(q_unsq1, - ov::opset11::Constant::create(ov::element::i64, ov::Shape{}, {3})); - q_unsq2->set_friendly_name(prefix + "q_unsq2"); - - auto causal_bool = std::make_shared(kv_unsq3, q_unsq2); - causal_bool->set_friendly_name(prefix + "causal_mask_bool"); - - auto batch_dim_b = - std::make_shared(cache_pos.ids_shape, - ov::opset11::Constant::create(ov::element::i64, ov::Shape{1}, {0}), - ov::opset11::Constant::create(ov::element::i64, ov::Shape{}, {0})); - auto seq_len_1d = - std::make_shared(cache_pos.seq_len, - ov::opset11::Constant::create(ov::element::i64, ov::Shape{1}, {1}), - false); - auto total_seq_1d = - std::make_shared(cache_pos.total_seq_len, - ov::opset11::Constant::create(ov::element::i64, ov::Shape{}, {0})); - auto broadcast_shape = std::make_shared( - ov::OutputVector{batch_dim_b->output(0), - ov::opset11::Constant::create(ov::element::i64, ov::Shape{1}, {1})->output(0), - seq_len_1d->output(0), - total_seq_1d->output(0)}, - 0); - auto causal_broadcast = - std::make_shared(causal_bool, broadcast_shape, ov::op::BroadcastType::BIDIRECTIONAL); - causal_broadcast->set_friendly_name(prefix + "causal_mask_broadcast"); - - // Always f32 — NPUW's AttentionMask matchers inject f32 nodes - auto select_true = ov::opset11::Constant::create(ov::element::f32, ov::Shape{}, {0.0f}); - auto select_false = ov::opset11::Constant::create(ov::element::f32, ov::Shape{}, {kAttentionMaskPaddingFP16Min}); - auto causal_float = std::make_shared(causal_broadcast, select_true, select_false); - causal_float->set_friendly_name(prefix + "causal_mask"); - - // Structural no-op Slice — AttentionMaskInput (prefill) needs Slice -> SDPA input[3] - auto slice_start = ov::opset11::Constant::create(ov::element::i64, ov::Shape{1}, {0}); - auto slice_stop = - std::make_shared(cache_pos.total_seq_len, - ov::opset11::Constant::create(ov::element::i64, ov::Shape{1}, {1}), - false); - auto slice_step = ov::opset11::Constant::create(ov::element::i64, ov::Shape{1}, {1}); - auto slice_axes = ov::opset11::Constant::create(ov::element::i64, ov::Shape{1}, {3}); - auto causal_sliced = - std::make_shared(causal_float, slice_start, slice_stop, slice_step, slice_axes); - causal_sliced->set_friendly_name(prefix + "causal_mask_sliced"); - - return causal_sliced->output(0); -} - -static ov::Output make_whisper_positional_embedding(const ov::Output& token_embed, - const ov::Output& position_ids, - size_t max_target_positions, - size_t hidden_size, - ov::element::Type precision, - const std::string& prefix) { - float pos_fill = fill_value_from_name(prefix + "embed_positions.weight"); - auto pos_embed_table = - ov::opset11::Constant::create(precision, - ov::Shape{max_target_positions, hidden_size}, - std::vector(max_target_positions * hidden_size, pos_fill)); - pos_embed_table->set_friendly_name(prefix + "embed_positions.weight"); - - auto pos_ids_i32 = std::make_shared(position_ids, ov::element::i32); - pos_ids_i32->set_friendly_name(prefix + "pos_ids_convert"); - auto pos_embed = - std::make_shared(pos_embed_table, - pos_ids_i32, - ov::opset11::Constant::create(ov::element::i64, ov::Shape{}, {0}), - 0); - pos_embed->set_friendly_name(prefix + "pos_embed_gather"); - - auto hidden_states = std::make_shared(token_embed, pos_embed); - hidden_states->set_friendly_name(prefix + "embed_add"); - - return hidden_states->output(0); -} std::shared_ptr ModelBuilder::build_whisper_decoder(const WhisperConfig& config_in) { clear(); @@ -1953,7 +930,7 @@ std::shared_ptr ModelBuilder::build_whisper_decoder(const WhisperConf prec); auto cache_pos = make_cache_position_ids(input_ids->output(0), layer0_k_read.beam_gather, "model.decoder."); - auto hidden_states = make_whisper_positional_embedding(token_embed, + auto hidden_states = make_learned_positional_embedding(token_embed, cache_pos.position_ids, config.max_target_positions, d, diff --git a/src/plugins/intel_npu/tests/functional/behavior/npuw/test_engine/models/model_builder.hpp b/src/plugins/intel_npu/tests/functional/behavior/npuw/test_engine/models/model_builder.hpp index 3a3d2671935e..c0b314ca09eb 100644 --- a/src/plugins/intel_npu/tests/functional/behavior/npuw/test_engine/models/model_builder.hpp +++ b/src/plugins/intel_npu/tests/functional/behavior/npuw/test_engine/models/model_builder.hpp @@ -9,6 +9,13 @@ #include #include +#include "model_builder_attention.hpp" +#include "model_builder_ffn.hpp" +#include "model_builder_masks.hpp" +#include "model_builder_norm.hpp" +#include "model_builder_rope.hpp" +#include "model_builder_types.hpp" +#include "model_builder_weights.hpp" #include "openvino/openvino.hpp" #include "openvino/opsets/opset11.hpp" @@ -16,177 +23,6 @@ namespace ov { namespace test { namespace npuw { -struct KVCacheResult { - ov::Output concatenated; - ov::Output beam_gather; - std::shared_ptr assign; -}; - -struct KVCacheReadState { - std::shared_ptr variable; - ov::Output beam_gather; -}; - -using WeightFn = std::function(const std::string&, const ov::Shape&, ov::element::Type)>; -using NormFn = std::function(const ov::Output&, const std::string&)>; -using FFNFn = std::function(const ov::Output&, const std::string&)>; -using RoPEFn = std::function(const ov::Output&, const std::string&)>; -using LayerFn = std::function(const ov::Output&, const std::string&, size_t)>; - -/// (projected_k, projected_v, layer_idx) -> (cached_k, cached_v). Empty = no cache. -using KVCacheFn = - std::function, - ov::Output>(const ov::Output&, const ov::Output&, size_t)>; - -struct FloatWeight { - ov::element::Type storage_type; - - FloatWeight(ov::element::Type st = ov::element::f32) : storage_type(st) {} - - ov::Output operator()(const std::string& name, - const ov::Shape& shape, - ov::element::Type compute_precision) const; -}; - -using FP32Weight = FloatWeight; -struct FP16Weight : FloatWeight { - FP16Weight() : FloatWeight(ov::element::f16) {} -}; - -/// Decompression pattern for CompressedWeight, matching DCOFF recognition. -/// -/// After NPUW partitioning, Constants become Parameters. The patterns below -/// describe the graph that DCOFF will see *before* partitioning transforms it. -/// -/// Pattern | Chain (f16/f32 = decomp type) | DCOFF class matched -/// ----------------+-------------------------------------------+------------------------------ -/// SYMM_NO_ZP | Cvt(f16) → Mul(f16 scale) [→ Reshape] | Reshape3 / Reshape4 -/// SYMM_NO_ZP_F32 | Cvt(f32) → Mul(f32 scale) [→ Reshape] | SymmNoZP::MatMul / Reshape4 -/// SYMM_ZP | Cvt(f16) → Sub(Const u4→Cvt f16) → Mul | Reshape1 / Convert1 -/// GPTQ | Cvt(f32) → Sub(Const f32) → Mul(f32) | Reshape2 -/// ASYMM_ZP | Cvt(f16) → Sub(varying u4→Cvt f16) → Mul | AsymmZP::Reshape -enum class DCOffPattern { - SYMM_NO_ZP, ///< f16 chain, no zero point. i4/i8/u4 storage. - SYMM_NO_ZP_F32, ///< f32 chain, no zero point. i4/i8/nf4 storage. - SYMM_ZP, ///< f16 chain, uniform u4 zero point (Constant after partitioning). u4 storage. - GPTQ, ///< f32 chain, uniform f32 zero point (no Convert on ZP). u4 storage. - ASYMM_ZP, ///< f16 chain, per-layer varying u4 zero point (Parameter after partitioning). u4 storage. -}; - -/// Compressed (quantized) weight with configurable DCOFF decompression pattern. -/// group_size > 0 = per-group scale (3D weight → decompress → Reshape 2D). -/// group_size = 0 = per-channel scale (2D weight, no Reshape). -/// Note: GPTQ and ASYMM_ZP require group_size > 0 (no per-channel DCOFF pass exists). -struct CompressedWeight { - ov::element::Type storage_type; - size_t group_size; ///< 0 = per-channel scale, >0 = per-group scale - DCOffPattern pattern; ///< Decompression pattern to generate - - explicit CompressedWeight(ov::element::Type st, size_t gs = 0, DCOffPattern pat = DCOffPattern::SYMM_NO_ZP) - : storage_type(st), - group_size(gs), - pattern(pat) {} - - ov::Output operator()(const std::string& name, - const ov::Shape& shape, - ov::element::Type compute_precision) const; -}; - -struct INT8Weight : CompressedWeight { - INT8Weight() : CompressedWeight(ov::element::i8) {} -}; - -struct INT4Weight : CompressedWeight { - INT4Weight() : CompressedWeight(ov::element::i4) {} -}; - -struct INT4GroupWeight : CompressedWeight { - explicit INT4GroupWeight(size_t gs = 128) : CompressedWeight(ov::element::i4, gs) {} -}; - -struct LayerNorm { - size_t hidden_size; - ov::element::Type precision; - float eps; - - LayerNorm(size_t hs, ov::element::Type prec = ov::element::f32, float e = 1e-5f) - : hidden_size(hs), - precision(prec), - eps(e) {} - - ov::Output operator()(const ov::Output& input, const std::string& name) const; -}; - -struct RMSNorm { - size_t hidden_size; - ov::element::Type precision; - float eps; - - RMSNorm(size_t hs, ov::element::Type prec = ov::element::f32, float e = 1e-5f) - : hidden_size(hs), - precision(prec), - eps(e) {} - - ov::Output operator()(const ov::Output& input, const std::string& name) const; -}; - -/// Position IDs baked in at construction, cos/sin shared across layers. -struct HalfRotationRoPE { - size_t head_dim; - ov::Output cos_freq, sin_freq; - - HalfRotationRoPE(size_t head_dim, ov::element::Type precision, const ov::Output& position_ids); - - ov::Output operator()(const ov::Output& input, const std::string& name) const; -}; - -struct InterleavedRoPE { - size_t head_dim; - ov::Output cos_freq, sin_freq; - - InterleavedRoPE(size_t head_dim, ov::element::Type precision, const ov::Output& position_ids); - - ov::Output operator()(const ov::Output& input, const std::string& name) const; -}; - -/// [batch, seq] position_ids Parameter. -ov::Output make_position_ids_2d(); - -/// [3, batch, seq] position_ids Parameter for m-rope. Returns [batch, seq] slice. -ov::Output make_position_ids_3d(); - -struct SwiGLU { - size_t hidden_size; - size_t intermediate_size; - ov::element::Type precision; - WeightFn weight_fn; - - SwiGLU(size_t hs, size_t is, ov::element::Type prec, WeightFn wf) - : hidden_size(hs), - intermediate_size(is), - precision(prec), - weight_fn(std::move(wf)) {} - - ov::Output operator()(const ov::Output& input, const std::string& name) const; -}; - -struct GELU { - size_t hidden_size; - size_t intermediate_size; - ov::element::Type precision; - WeightFn weight_fn; - WeightFn bias_fn; - - GELU(size_t hs, size_t is, ov::element::Type prec, WeightFn wf, WeightFn bf = {}) - : hidden_size(hs), - intermediate_size(is), - precision(prec), - weight_fn(std::move(wf)), - bias_fn(std::move(bf)) {} - - ov::Output operator()(const ov::Output& input, const std::string& name) const; -}; - ov::Output make_linear(const ov::Output& input, size_t in_features, size_t out_features, @@ -195,50 +31,6 @@ ov::Output make_linear(const ov::Output& input, const WeightFn& weight_fn = FP32Weight{}, const WeightFn& bias_fn = {}); -ov::Output make_multihead_reshape(const ov::Output& input, - size_t num_heads, - size_t head_dim, - const std::string& name); - -ov::Output make_attention_transpose(const ov::Output& input, const std::string& name); - -ov::Output make_repeat_kv(const ov::Output& kv, - size_t num_heads, - size_t num_kv_heads, - size_t head_dim, - const std::string& name, - const ov::Output& shared_broadcast_shape = {}); - -KVCacheReadState make_kv_cache_read(const ov::Output& batch_source, - const ov::Output& beam_idx, - size_t num_heads, - size_t head_dim, - const std::string& name, - ov::element::Type precision = ov::element::f32); - -KVCacheResult make_kv_cache_concat(const ov::Output& current_kv, - const ov::Output& batch_source, - const ov::Output& beam_idx, - size_t num_heads, - size_t head_dim, - const std::string& name, - ov::element::Type precision = ov::element::f32); - -/// head_dim_for_scale > 0 creates 5-input SDPA (needed for ReConstructEmbeddingModel matching) -ov::Output make_sdpa(const ov::Output& q, - const ov::Output& k, - const ov::Output& v, - const std::string& name, - const ov::Output& attention_mask = ov::Output(), - size_t head_dim_for_scale = 0); - -ov::Output make_attention_output(const ov::Output& sdpa_output, - size_t hidden_size, - const std::string& name, - ov::element::Type precision, - const WeightFn& weight_fn, - const WeightFn& bias_fn = {}); - ov::Output make_embedding(const ov::Output& input_ids, size_t vocab_size, size_t hidden_size, @@ -261,47 +53,11 @@ ov::Output make_conv1d(const ov::Output& input, const std::string& name, ov::element::Type precision = ov::element::f32); -/// Store-only KV cache for cross-attention. No beam gather — encoder KV is identical across beams. -KVCacheResult make_encoder_kv_cache(const ov::Output& encoder_kv, - size_t num_heads, - size_t head_dim, - const std::string& name, - ov::element::Type precision = ov::element::f32); - ov::Output make_transformer_layers(const ov::Output& initial, size_t num_layers, const std::string& prefix_base, const LayerFn& layer_fn); -/// Takes pre-projected Q, K, V. Handles reshape, QK-norm, RoPE, KV cache, GQA, SDPA, O proj. -struct Attention { - size_t hidden_size, num_heads, num_kv_heads, head_dim; - ov::element::Type precision; - WeightFn weight_fn; - WeightFn bias_fn; - NormFn qk_norm; - RoPEFn rope_fn; - KVCacheFn kv_cache_fn; - - ov::Output sdpa_mask; - ov::Output shared_broadcast_shape; - - std::string o_proj_name = "self_attn.o_proj"; - std::string attn_prefix = "self_attn."; - - ov::Output operator()(const ov::Output& q, - const ov::Output& k, - const ov::Output& v, - const std::string& prefix, - size_t layer_idx = 0) const; - - /// Convenience: project Q/K/V from input (and optionally kv_input for K/V), then attend. - ov::Output operator()(const ov::Output& input, - const ov::Output& kv_input, - const std::string& prefix, - size_t layer_idx = 0) const; -}; - template ov::Output make_cross_attn_decoder_layer(const ov::Output& input, const Norm& norm, @@ -403,6 +159,12 @@ struct LLMConfig : public BaseModelConfig { bool use_inputs_embeds = false; bool internal_position_ids = false; ///< embedding model bool pre_norm = true; + bool force_gqa_broadcast = false; ///< force 5-input SDPA (needed for SDPA isolation pattern matching) + + // MoE configuration (num_experts=0 means dense, no MoE) + size_t num_experts = 0; ///< Total experts. 0 = dense model. + size_t num_experts_per_tok = 0; ///< Top-K. 0 = default to 2. + size_t moe_intermediate_size = 0; ///< Expert FFN intermediate size. 0 = use intermediate_size. }; struct WhisperConfig : public BaseModelConfig { diff --git a/src/plugins/intel_npu/tests/functional/behavior/npuw/test_engine/models/model_builder_attention.cpp b/src/plugins/intel_npu/tests/functional/behavior/npuw/test_engine/models/model_builder_attention.cpp new file mode 100644 index 000000000000..8f1511e235ac --- /dev/null +++ b/src/plugins/intel_npu/tests/functional/behavior/npuw/test_engine/models/model_builder_attention.cpp @@ -0,0 +1,320 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "model_builder_attention.hpp" + +#include +#include + +#include "model_builder.hpp" +#include "openvino/op/ops.hpp" +#include "openvino/op/util/variable.hpp" +#include "openvino/opsets/opset11.hpp" + +namespace ov { +namespace test { +namespace npuw { + +ov::Output make_multihead_reshape(const ov::Output& input, + size_t num_heads, + size_t head_dim, + const std::string& name) { + auto shape = ov::opset11::Constant::create( + ov::element::i64, + ov::Shape{4}, + std::vector{0, -1, static_cast(num_heads), static_cast(head_dim)}); + + auto reshape = std::make_shared(input, shape, true); + reshape->set_friendly_name(name); + + return reshape->output(0); +} + +ov::Output make_attention_transpose(const ov::Output& input, const std::string& name) { + auto order_const = ov::opset11::Constant::create(ov::element::i64, ov::Shape{4}, {0, 2, 1, 3}); + + auto transpose = std::make_shared(input, order_const); + transpose->set_friendly_name(name); + + return transpose->output(0); +} + +ov::Output make_repeat_kv(const ov::Output& kv, + size_t num_heads, + size_t num_kv_heads, + size_t head_dim, + const std::string& name, + const ov::Output& shared_broadcast_shape) { + const size_t actual_kv_heads = (num_kv_heads == 0) ? num_heads : num_kv_heads; + const size_t n_rep = num_heads / actual_kv_heads; + + if (!shared_broadcast_shape.get_node() && n_rep == 1) { + return kv; + } + + OPENVINO_ASSERT(num_heads % actual_kv_heads == 0, + "num_heads (", + num_heads, + ") must be divisible by num_kv_heads (", + actual_kv_heads, + ")"); + + ov::Output broadcast_shape_output; + if (shared_broadcast_shape.get_node()) { + broadcast_shape_output = shared_broadcast_shape; + } else { + // 4-input Concat {Gather, any, any, any} required by NPUW's AttentionBroadcast + // pattern (sdpa.cpp); a 3-input form only hits the AttentionBroadcast2 fallback. + auto shape_of_kv = std::make_shared(kv, ov::element::i64); + shape_of_kv->set_friendly_name(name + "_shapeof"); + auto gather_axis = ov::opset11::Constant::create(ov::element::i64, ov::Shape{}, {0}); + auto idx_01 = ov::opset11::Constant::create(ov::element::i64, ov::Shape{2}, {0, 1}); + auto batch_kv_heads = std::make_shared(shape_of_kv, idx_01, gather_axis); + batch_kv_heads->set_friendly_name(name + "_batch_kv_heads"); + auto n_rep_const = ov::opset11::Constant::create(ov::element::i64, ov::Shape{1}, {static_cast(n_rep)}); + auto idx_2 = ov::opset11::Constant::create(ov::element::i64, ov::Shape{1}, {2}); + auto seq_dim = std::make_shared(shape_of_kv, idx_2, gather_axis); + seq_dim->set_friendly_name(name + "_seq_dim"); + auto head_dim_const = + ov::opset11::Constant::create(ov::element::i64, ov::Shape{1}, {static_cast(head_dim)}); + auto broadcast_shape = std::make_shared( + ov::OutputVector{batch_kv_heads, n_rep_const, seq_dim, head_dim_const}, + 0); + broadcast_shape->set_friendly_name(name + "_broadcast_shape"); + broadcast_shape_output = broadcast_shape->output(0); + } + + auto unsqueeze_axis = ov::opset11::Constant::create(ov::element::i64, ov::Shape{1}, {2}); + + auto unsqueezed = std::make_shared(kv, unsqueeze_axis); + unsqueezed->set_friendly_name(name + "_unsqueeze"); + + auto broadcasted = std::make_shared(unsqueezed, + broadcast_shape_output, + ov::op::BroadcastType::BIDIRECTIONAL); + broadcasted->set_friendly_name(name + "_broadcast"); + + auto new_shape = ov::opset11::Constant::create( + ov::element::i64, + ov::Shape{4}, + std::vector{0, static_cast(num_heads), -1, static_cast(head_dim)}); + new_shape->set_friendly_name(name + "_shape"); + + auto reshaped = std::make_shared(broadcasted, new_shape, true); + reshaped->set_friendly_name(name); + + return reshaped->output(0); +} + +KVCacheReadState make_kv_cache_read(const ov::Output& batch_source, + const ov::Output& beam_idx, + size_t num_heads, + size_t head_dim, + const std::string& name, + ov::element::Type precision) { + auto var_shape = ov::PartialShape{-1, static_cast(num_heads), -1, static_cast(head_dim)}; + auto variable = std::make_shared(ov::op::util::VariableInfo{var_shape, precision, name}); + + auto shape_of = std::make_shared(batch_source, ov::element::i64); + shape_of->set_friendly_name(name + "_shapeof"); + + auto zero_idx = ov::opset11::Constant::create(ov::element::i64, ov::Shape{1}, std::vector{0}); + auto gather_axis = ov::opset11::Constant::create(ov::element::i64, ov::Shape{}, std::vector{0}); + + auto batch_dim = std::make_shared(shape_of, zero_idx, gather_axis); + batch_dim->set_friendly_name(name + "_batch_dim"); + + auto num_heads_const = ov::opset11::Constant::create(ov::element::i64, + ov::Shape{1}, + std::vector{static_cast(num_heads)}); + auto zero_seq = ov::opset11::Constant::create(ov::element::i64, ov::Shape{1}, std::vector{0}); + auto head_dim_const = ov::opset11::Constant::create(ov::element::i64, + ov::Shape{1}, + std::vector{static_cast(head_dim)}); + + auto init_shape = std::make_shared(ov::OutputVector{batch_dim->output(0), + num_heads_const->output(0), + zero_seq->output(0), + head_dim_const->output(0)}, + 0); + init_shape->set_friendly_name(name + "_init_shape"); + + auto zero_scalar = ov::opset11::Constant::create(precision, ov::Shape{}, std::vector{0.0f}); + + auto init_value = std::make_shared(zero_scalar, init_shape); + init_value->set_friendly_name(name + "_init"); + + auto read_value = std::make_shared(init_value, variable); + read_value->set_friendly_name(name + "_read"); + + auto beam_gather_axis = ov::opset11::Constant::create(ov::element::i64, ov::Shape{}, std::vector{0}); + + auto beam_gather = std::make_shared(read_value, beam_idx, beam_gather_axis); + beam_gather->set_friendly_name(name + "_beam_gather"); + + return {variable, beam_gather->output(0)}; +} + +KVCacheResult make_kv_cache_concat(const ov::Output& current_kv, + const ov::Output& batch_source, + const ov::Output& beam_idx, + size_t num_heads, + size_t head_dim, + const std::string& name, + ov::element::Type precision) { + auto read_state = make_kv_cache_read(batch_source, beam_idx, num_heads, head_dim, name, precision); + + auto concat = std::make_shared(ov::OutputVector{read_state.beam_gather, current_kv}, 2); + concat->set_friendly_name(name + "_concat"); + + auto assign = std::make_shared(concat, read_state.variable); + assign->set_friendly_name(name + "_assign"); + + return {concat->output(0), read_state.beam_gather, assign}; +} + +KVCacheResult make_encoder_kv_cache(const ov::Output& encoder_kv, + size_t num_heads, + size_t head_dim, + const std::string& name, + ov::element::Type precision) { + auto var_shape = ov::PartialShape{-1, static_cast(num_heads), -1, static_cast(head_dim)}; + auto variable = std::make_shared(ov::op::util::VariableInfo{var_shape, precision, name}); + + // No Gather/beam reorder — encoder KV is identical across beams + auto read_value = std::make_shared(encoder_kv, variable); + read_value->set_friendly_name(name + "_read"); + + auto assign = std::make_shared(read_value, variable); + assign->set_friendly_name(name + "_assign"); + + return {read_value->output(0), {}, assign}; +} + +ov::Output make_shared_gqa_broadcast(const ov::Output& shape_source, + size_t kv_heads, + size_t num_heads, + size_t head_dim) { + const size_t n_rep = num_heads / kv_heads; + auto shape_of = std::make_shared(shape_source, ov::element::i64); + auto axis0 = ov::opset11::Constant::create(ov::element::i64, ov::Shape{}, {0}); + auto idx0 = ov::opset11::Constant::create(ov::element::i64, ov::Shape{1}, {0}); + auto batch_dim = std::make_shared(shape_of, idx0, axis0); + auto idx1 = ov::opset11::Constant::create(ov::element::i64, ov::Shape{1}, {1}); + auto seq_dim = std::make_shared(shape_of, idx1, axis0); + auto kv_heads_const = + ov::opset11::Constant::create(ov::element::i64, ov::Shape{1}, {static_cast(kv_heads)}); + auto n_rep_const = ov::opset11::Constant::create(ov::element::i64, ov::Shape{1}, {static_cast(n_rep)}); + auto head_dim_const = + ov::opset11::Constant::create(ov::element::i64, ov::Shape{1}, {static_cast(head_dim)}); + auto shared_concat = std::make_shared( + ov::OutputVector{batch_dim, kv_heads_const, n_rep_const, seq_dim, head_dim_const}, + 0); + shared_concat->set_friendly_name("model.shared_gqa_broadcast_shape"); + return shared_concat->output(0); +} + +ov::Output make_sdpa(const ov::Output& q, + const ov::Output& k, + const ov::Output& v, + const std::string& name, + const ov::Output& attention_mask, + size_t head_dim_for_scale) { + std::shared_ptr sdpa; + if (head_dim_for_scale > 0 && attention_mask.get_node()) { + // 5-input SDPA: Q, K, V, mask, scale (required for embedding model pattern matching) + auto scale_val = 1.0f / std::sqrt(static_cast(head_dim_for_scale)); + auto scale = ov::opset11::Constant::create(ov::element::f32, ov::Shape{}, {scale_val}); + scale->set_friendly_name(name + ".scale"); + sdpa = std::make_shared(q, k, v, attention_mask, scale, false); + } else if (attention_mask.get_node()) { + sdpa = std::make_shared(q, k, v, attention_mask, false); + } else { + sdpa = std::make_shared(q, k, v, false); + } + sdpa->set_friendly_name(name + ".sdpa"); + + return sdpa->output(0); +} + +ov::Output make_attention_output(const ov::Output& sdpa_output, + size_t hidden_size, + const std::string& name, + ov::element::Type precision, + const WeightFn& weight_fn, + const WeightFn& bias_fn) { + auto attn_trans = make_attention_transpose(sdpa_output, name + "_transpose"); + + auto reshape_shape = ov::opset11::Constant::create(ov::element::i64, + ov::Shape{3}, + std::vector{0, -1, static_cast(hidden_size)}); + auto attn_reshaped = std::make_shared(attn_trans, reshape_shape, true); + attn_reshaped->set_friendly_name(name + "_reshape"); + + return make_linear(attn_reshaped->output(0), hidden_size, hidden_size, name, precision, weight_fn, bias_fn); +} + +ov::Output Attention::operator()(const ov::Output& q, + const ov::Output& k, + const ov::Output& v, + const std::string& prefix, + size_t layer_idx) const { + auto q_reshaped = make_multihead_reshape(q, num_heads, head_dim, prefix + attn_prefix + "q_reshape"); + auto k_reshaped = make_multihead_reshape(k, num_kv_heads, head_dim, prefix + attn_prefix + "k_reshape"); + auto v_reshaped = make_multihead_reshape(v, num_kv_heads, head_dim, prefix + attn_prefix + "v_reshape"); + + ov::Output q_normed = q_reshaped; + ov::Output k_normed = k_reshaped; + if (qk_norm) { + q_normed = qk_norm(q_reshaped, prefix + attn_prefix + "q_norm"); + k_normed = qk_norm(k_reshaped, prefix + attn_prefix + "k_norm"); + } + + // [batch, seq, heads, dim] -> [batch, heads, seq, dim] + auto q_trans = make_attention_transpose(q_normed, prefix + attn_prefix + "q_transpose"); + auto k_trans = make_attention_transpose(k_normed, prefix + attn_prefix + "k_transpose"); + auto v_trans = make_attention_transpose(v_reshaped, prefix + attn_prefix + "v_transpose"); + + ov::Output q_roped = q_trans; + ov::Output k_roped = k_trans; + if (rope_fn) { + q_roped = rope_fn(q_trans, prefix + "q_rope"); + k_roped = rope_fn(k_trans, prefix + "k_rope"); + } + + ov::Output k_for_attn = k_roped; + ov::Output v_for_attn = v_trans; + if (kv_cache_fn) { + std::tie(k_for_attn, v_for_attn) = kv_cache_fn(k_roped, v_trans, layer_idx); + } + + auto k_expanded = + make_repeat_kv(k_for_attn, num_heads, num_kv_heads, head_dim, prefix + "k_repeat", shared_broadcast_shape); + auto v_expanded = + make_repeat_kv(v_for_attn, num_heads, num_kv_heads, head_dim, prefix + "v_repeat", shared_broadcast_shape); + + // 5-input SDPA with explicit scale needed for embedding model pattern matching + size_t sdpa_scale_dim = shared_broadcast_shape.get_node() ? head_dim : 0; + auto attn_output = + make_sdpa(q_roped, k_expanded, v_expanded, prefix + attn_prefix + "attn", sdpa_mask, sdpa_scale_dim); + + return make_attention_output(attn_output, hidden_size, prefix + o_proj_name, precision, weight_fn, bias_fn); +} + +ov::Output Attention::operator()(const ov::Output& input, + const ov::Output& kv_input, + const std::string& prefix, + size_t layer_idx) const { + auto kv_src = kv_input.get_node() ? kv_input : input; + size_t kv_dim = num_kv_heads * head_dim; + auto q = + make_linear(input, hidden_size, hidden_size, prefix + attn_prefix + "q_proj", precision, weight_fn, bias_fn); + auto k = make_linear(kv_src, hidden_size, kv_dim, prefix + attn_prefix + "k_proj", precision, weight_fn, bias_fn); + auto v = make_linear(kv_src, hidden_size, kv_dim, prefix + attn_prefix + "v_proj", precision, weight_fn, bias_fn); + return (*this)(q, k, v, prefix, layer_idx); +} + +} // namespace npuw +} // namespace test +} // namespace ov diff --git a/src/plugins/intel_npu/tests/functional/behavior/npuw/test_engine/models/model_builder_attention.hpp b/src/plugins/intel_npu/tests/functional/behavior/npuw/test_engine/models/model_builder_attention.hpp new file mode 100644 index 000000000000..7de2409082a9 --- /dev/null +++ b/src/plugins/intel_npu/tests/functional/behavior/npuw/test_engine/models/model_builder_attention.hpp @@ -0,0 +1,106 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#pragma once + +#include + +#include "model_builder_types.hpp" +#include "openvino/core/node.hpp" +#include "openvino/core/type/element_type.hpp" + +namespace ov { +namespace test { +namespace npuw { + +ov::Output make_multihead_reshape(const ov::Output& input, + size_t num_heads, + size_t head_dim, + const std::string& name); + +ov::Output make_attention_transpose(const ov::Output& input, const std::string& name); + +ov::Output make_repeat_kv(const ov::Output& kv, + size_t num_heads, + size_t num_kv_heads, + size_t head_dim, + const std::string& name, + const ov::Output& shared_broadcast_shape = {}); + +KVCacheReadState make_kv_cache_read(const ov::Output& batch_source, + const ov::Output& beam_idx, + size_t num_heads, + size_t head_dim, + const std::string& name, + ov::element::Type precision = ov::element::f32); + +KVCacheResult make_kv_cache_concat(const ov::Output& current_kv, + const ov::Output& batch_source, + const ov::Output& beam_idx, + size_t num_heads, + size_t head_dim, + const std::string& name, + ov::element::Type precision = ov::element::f32); + +/// Store-only KV cache for cross-attention. No beam gather — encoder KV is identical across beams. +KVCacheResult make_encoder_kv_cache(const ov::Output& encoder_kv, + size_t num_heads, + size_t head_dim, + const std::string& name, + ov::element::Type precision = ov::element::f32); + +/// Shared GQA broadcast shape — ReConstructEmbeddingModel requires pointer +/// equality across SDPAs. Build once per model and feed into Attention. +ov::Output make_shared_gqa_broadcast(const ov::Output& shape_source, + size_t kv_heads, + size_t num_heads, + size_t head_dim); + +/// head_dim_for_scale > 0 creates 5-input SDPA (needed for ReConstructEmbeddingModel matching). +ov::Output make_sdpa(const ov::Output& q, + const ov::Output& k, + const ov::Output& v, + const std::string& name, + const ov::Output& attention_mask = ov::Output(), + size_t head_dim_for_scale = 0); + +ov::Output make_attention_output(const ov::Output& sdpa_output, + size_t hidden_size, + const std::string& name, + ov::element::Type precision, + const WeightFn& weight_fn, + const WeightFn& bias_fn = {}); + +/// Takes pre-projected Q, K, V. Handles reshape, QK-norm, RoPE, KV cache, GQA, SDPA, O proj. +struct Attention { + size_t hidden_size, num_heads, num_kv_heads, head_dim; + ov::element::Type precision; + WeightFn weight_fn; + WeightFn bias_fn; + NormFn qk_norm; + RoPEFn rope_fn; + KVCacheFn kv_cache_fn; + + ov::Output sdpa_mask; + ov::Output shared_broadcast_shape; + + std::string o_proj_name = "self_attn.o_proj"; + std::string attn_prefix = "self_attn."; + + ov::Output operator()(const ov::Output& q, + const ov::Output& k, + const ov::Output& v, + const std::string& prefix, + size_t layer_idx = 0) const; + + /// Convenience: project Q/K/V from input (and optionally kv_input for K/V), then attend. + ov::Output operator()(const ov::Output& input, + const ov::Output& kv_input, + const std::string& prefix, + size_t layer_idx = 0) const; +}; + +} // namespace npuw +} // namespace test +} // namespace ov diff --git a/src/plugins/intel_npu/tests/functional/behavior/npuw/test_engine/models/model_builder_ffn.cpp b/src/plugins/intel_npu/tests/functional/behavior/npuw/test_engine/models/model_builder_ffn.cpp new file mode 100644 index 000000000000..6c9d83c82393 --- /dev/null +++ b/src/plugins/intel_npu/tests/functional/behavior/npuw/test_engine/models/model_builder_ffn.cpp @@ -0,0 +1,199 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "model_builder_ffn.hpp" + +#include + +#include "model_builder.hpp" +#include "openvino/op/ops.hpp" +#include "openvino/opsets/opset11.hpp" + +namespace ov { +namespace test { +namespace npuw { + +ov::Output SwiGLU::operator()(const ov::Output& input, const std::string& name) const { + auto gate = make_linear(input, hidden_size, intermediate_size, name + ".gate_proj", precision, weight_fn); + auto up = make_linear(input, hidden_size, intermediate_size, name + ".up_proj", precision, weight_fn); + + auto sigmoid = std::make_shared(gate); + + auto silu = std::make_shared(gate, sigmoid); + silu->set_friendly_name(name + "_silu"); + + auto gate_up = std::make_shared(silu, up); + gate_up->set_friendly_name(name + "_gate_up"); + + auto down = make_linear(gate_up, intermediate_size, hidden_size, name + ".down_proj", precision, weight_fn); + + return down; +} + +ov::Output GELU::operator()(const ov::Output& input, const std::string& name) const { + auto up = make_linear(input, hidden_size, intermediate_size, name + ".up_proj", precision, weight_fn, bias_fn); + + auto gelu = std::make_shared(up); + gelu->set_friendly_name(name + "_gelu"); + + auto down = make_linear(gelu, intermediate_size, hidden_size, name + ".down_proj", precision, weight_fn, bias_fn); + + return down; +} + +MoEFFN::MoEFFN(size_t hs, size_t is, size_t ne, size_t k, ov::element::Type prec, WeightFn wf) + : hidden_size(hs), + intermediate_size(is), + num_experts(ne), + num_experts_per_tok(k), + precision(prec), + weight_fn(std::move(wf)) { + // Default to i4 CompressedWeight if no weight function provided. + // i4 (not nf4) because NPUW's nf4 unpack doesn't handle 3D batched scales. + if (!weight_fn) { + weight_fn = CompressedWeight{ov::element::i4, 0, DCOffPattern::SYMM_NO_ZP}; + } + using C = ov::opset11::Constant; + int32_t is_i = static_cast(is); + int32_t ne_i = static_cast(ne); + int32_t k_i = static_cast(k); + + // Use i32 for shape/axis/step constants matching real GPT-OSS HuggingFace export. + tile_repeats = C::create(ov::element::i32, ov::Shape{2}, std::vector{ne_i, 1}); + slice_step = C::create(ov::element::i32, ov::Shape{1}, std::vector{1}); + slice_axis2 = C::create(ov::element::i32, ov::Shape{1}, std::vector{2}); + slice_start_0 = C::create(ov::element::i32, ov::Shape{1}, std::vector{0}); + slice_stop_is = C::create(ov::element::i32, ov::Shape{1}, std::vector{is_i}); + slice_start_is = C::create(ov::element::i32, ov::Shape{1}, std::vector{is_i}); + slice_stop_2is = C::create(ov::element::i32, ov::Shape{1}, std::vector{static_cast(2 * is_i)}); + min_const = C::create(prec, ov::Shape{1}, std::vector{20.0f}); + swish_beta = C::create(prec, ov::Shape{}, std::vector{1.0f}); + clamp_add_zero = C::create(prec, ov::Shape{1}, std::vector{0.0f}); + topk_k_const = C::create(ov::element::i32, ov::Shape{}, std::vector{k_i}); + sl_start = C::create(ov::element::i32, ov::Shape{2}, std::vector{0, 0}); + sl_step_r = C::create(ov::element::i32, ov::Shape{2}, std::vector{1, 1}); + sl_axes = C::create(ov::element::i32, ov::Shape{2}, std::vector{0, 1}); + scatter_axis = C::create(ov::element::i32, ov::Shape{}, std::vector{1}); + tp_order = C::create(ov::element::i32, ov::Shape{2}, std::vector{1, 0}); + unsq_axis = C::create(ov::element::i32, ov::Shape{}, std::vector{3}); + reduce_axis = C::create(ov::element::i32, ov::Shape{1}, std::vector{0}); +} + +ov::Output MoEFFN::operator()(const ov::Output& input, const std::string& name) const { + using C = ov::opset11::Constant; + const auto prec = precision; + const int32_t ne_i = static_cast(num_experts); + const int32_t hs_i = static_cast(hidden_size); + auto mk = [](std::vector v) { + ov::OutputVector p; + for (auto x : v) + p.push_back(C::create(ov::element::i32, ov::Shape{1}, std::vector{x})->output(0)); + return std::make_shared(p, 0); + }; + + auto original_shape = std::make_shared(input, ov::element::i32); + original_shape->set_friendly_name(name + ".original_shape"); + + auto input_2d = std::make_shared(input, mk({-1, hs_i}), false); + input_2d->set_friendly_name(name + ".input_2d"); + + // Router uses i8 weights matching real GPT-OSS two-pass quantization + // (router excluded from 4-bit, gets 8-bit instead). + static const CompressedWeight router_wt{ov::element::i8, 0, DCOffPattern::SYMM_NO_ZP}; + auto rw = router_wt(name + ".expert.router.weight", ov::Shape{num_experts, hidden_size}, prec); + auto r_mm = std::make_shared(input_2d, rw, false, true); + r_mm->set_friendly_name(name + ".expert.router.matmul"); + auto r_bias = C::create(prec, ov::Shape{1, num_experts}, std::vector(num_experts, 0.0f)); + r_bias->set_friendly_name(name + ".expert.router.bias"); + auto r_add = std::make_shared(r_mm, r_bias); + r_add->set_friendly_name(name + ".expert.router.add"); + + auto topk = std::make_shared(r_add, topk_k_const, 1, "max", "value", ov::element::i64); + topk->set_friendly_name(name + ".expert.router.topk"); + auto softmax = std::make_shared(topk->output(0), 1); + softmax->set_friendly_name(name + ".expert.router.softmax"); + auto topk_cvt = std::make_shared(topk->output(1), ov::element::i32); + topk_cvt->set_friendly_name(name + ".expert.router.topk_convert"); + auto topk_shape = std::make_shared(topk_cvt, ov::element::i32); + topk_shape->set_friendly_name(name + ".expert.router.topk_shapeof"); + + auto r_slice = std::make_shared(softmax, sl_start, topk_shape, sl_step_r, sl_axes); + r_slice->set_friendly_name(name + ".expert.router.slice"); + auto add_shape = std::make_shared(r_add, ov::element::i32); + add_shape->set_friendly_name(name + ".expert.router.add_shapeof"); + auto zeros = std::make_shared(C::create(prec, ov::Shape{}, std::vector{0.0f}), + add_shape, + ov::op::BroadcastType::NUMPY); + zeros->set_friendly_name(name + ".expert.router.zeros"); + auto scatter = std::make_shared(zeros, topk_cvt, r_slice, scatter_axis); + scatter->set_friendly_name(name + ".expert.router.scatter"); + + auto r_tp = std::make_shared(scatter, tp_order); + r_tp->set_friendly_name(name + ".expert.router.transpose"); + auto r_reshape = std::make_shared(r_tp, mk({ne_i, 1, -1}), false); + r_reshape->set_friendly_name(name + ".expert.router.reshape"); + auto router_scores = std::make_shared(r_reshape, unsq_axis); + router_scores->set_friendly_name(name + ".expert.router.unsqueeze"); + + // Expert: Tile → Reshape → MatMul → Add → dual-branch → MatMul → Add → Reshape → Multiply → ReduceSum + auto tiled = std::make_shared(input_2d, tile_repeats); + tiled->set_friendly_name(name + ".expert.tile"); + auto expert_3d = std::make_shared(tiled, mk({ne_i, -1, hs_i}), false); + expert_3d->set_friendly_name(name + ".expert.reshape_in"); + + auto gu_w = weight_fn(name + ".expert.gate_up_proj.weight", + ov::Shape{num_experts, 2 * intermediate_size, hidden_size}, + prec); + auto gu_mm = std::make_shared(expert_3d, gu_w, false, true); + gu_mm->set_friendly_name(name + ".expert.gate_up_matmul"); + auto gu_bias = C::create(prec, + ov::Shape{num_experts, 1, 2 * intermediate_size}, + std::vector(num_experts * 2 * intermediate_size, 0.0f)); + gu_bias->set_friendly_name(name + ".expert.gate_up_bias"); + auto gu_add = std::make_shared(gu_mm, gu_bias); + gu_add->set_friendly_name(name + ".expert.gate_up_add"); + + auto act_slice = std::make_shared(gu_add, slice_start_0, slice_stop_is, slice_step, slice_axis2); + act_slice->set_friendly_name(name + ".expert.slice_act"); + auto act_min = std::make_shared(act_slice, min_const); + act_min->set_friendly_name(name + ".expert.minimum"); + auto act_swish = std::make_shared(act_min, swish_beta); + act_swish->set_friendly_name(name + ".expert.swish"); + + auto gate_slice = + std::make_shared(gu_add, slice_start_is, slice_stop_2is, slice_step, slice_axis2); + gate_slice->set_friendly_name(name + ".expert.slice_gate"); + auto gate_clamp = std::make_shared(gate_slice, -20.0f, 20.0f); + gate_clamp->set_friendly_name(name + ".expert.clamp"); + auto gate_add = std::make_shared(gate_clamp, clamp_add_zero); + gate_add->set_friendly_name(name + ".expert.gate_add"); + + auto merged = std::make_shared(act_swish, gate_add); + merged->set_friendly_name(name + ".expert.merge"); + + auto dn_w = + weight_fn(name + ".expert.down_proj.weight", ov::Shape{num_experts, hidden_size, intermediate_size}, prec); + auto dn_mm = std::make_shared(merged, dn_w, false, true); + dn_mm->set_friendly_name(name + ".expert.down_matmul"); + auto dn_bias = + C::create(prec, ov::Shape{num_experts, 1, hidden_size}, std::vector(num_experts * hidden_size, 0.0f)); + dn_bias->set_friendly_name(name + ".expert.down_bias"); + auto dn_add = std::make_shared(dn_mm, dn_bias); + dn_add->set_friendly_name(name + ".expert.down_add"); + + auto expert_out = std::make_shared(dn_add, mk({ne_i, 1, -1, hs_i}), false); + expert_out->set_friendly_name(name + ".expert.reshape_out"); + auto weighted = std::make_shared(expert_out, router_scores); + weighted->set_friendly_name(name + ".expert.weighted"); + auto reduced = std::make_shared(weighted, reduce_axis, false); + reduced->set_friendly_name(name + ".expert.reduced"); + + auto output = std::make_shared(reduced, original_shape, false); + output->set_friendly_name(name + ".output"); + return output->output(0); +} + +} // namespace npuw +} // namespace test +} // namespace ov diff --git a/src/plugins/intel_npu/tests/functional/behavior/npuw/test_engine/models/model_builder_ffn.hpp b/src/plugins/intel_npu/tests/functional/behavior/npuw/test_engine/models/model_builder_ffn.hpp new file mode 100644 index 000000000000..905e11e7eb90 --- /dev/null +++ b/src/plugins/intel_npu/tests/functional/behavior/npuw/test_engine/models/model_builder_ffn.hpp @@ -0,0 +1,76 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#pragma once + +#include +#include + +#include "model_builder_types.hpp" +#include "openvino/core/node.hpp" +#include "openvino/core/type/element_type.hpp" + +namespace ov { +namespace test { +namespace npuw { + +struct SwiGLU { + size_t hidden_size; + size_t intermediate_size; + ov::element::Type precision; + WeightFn weight_fn; + + SwiGLU(size_t hs, size_t is, ov::element::Type prec, WeightFn wf) + : hidden_size(hs), + intermediate_size(is), + precision(prec), + weight_fn(std::move(wf)) {} + + ov::Output operator()(const ov::Output& input, const std::string& name) const; +}; + +struct GELU { + size_t hidden_size; + size_t intermediate_size; + ov::element::Type precision; + WeightFn weight_fn; + WeightFn bias_fn; + + GELU(size_t hs, size_t is, ov::element::Type prec, WeightFn wf, WeightFn bf = {}) + : hidden_size(hs), + intermediate_size(is), + precision(prec), + weight_fn(std::move(wf)), + bias_fn(std::move(bf)) {} + + ov::Output operator()(const ov::Output& input, const std::string& name) const; +}; + +/// GPT-OSS style batched MoE FFN matching NPUW's GPTOSSExpert + GPTOSSRouter patterns. +/// All experts compute on all tokens via Tile + 3D batched MatMul (no NonZero). +/// Weight function must produce a Multiply→Convert→MatMul chain (default: i4 CompressedWeight) +/// for the isolation patterns to match. Shared constants across layers enable repeating +/// block detection. Conforms to FFNFn for drop-in use in transformer layer templates. +struct MoEFFN { + size_t hidden_size, intermediate_size, num_experts, num_experts_per_tok; + ov::element::Type precision; + WeightFn weight_fn; + + /// Default weight_fn: CompressedWeight{i4, 0, SYMM_NO_ZP}. + MoEFFN(size_t hs, size_t is, size_t ne, size_t k, ov::element::Type prec, WeightFn wf = {}); + + ov::Output operator()(const ov::Output& input, const std::string& name) const; + +private: + // Shared across layers for matchRepeatedSubgraphs (created once in ctor) + std::shared_ptr tile_repeats, topk_k_const; + std::shared_ptr slice_step, slice_axis2, slice_start_0, slice_stop_is, slice_start_is, slice_stop_2is; + std::shared_ptr min_const, swish_beta, clamp_add_zero; + std::shared_ptr sl_start, sl_step_r, sl_axes, scatter_axis; + std::shared_ptr tp_order, unsq_axis, reduce_axis; +}; + +} // namespace npuw +} // namespace test +} // namespace ov diff --git a/src/plugins/intel_npu/tests/functional/behavior/npuw/test_engine/models/model_builder_internal.hpp b/src/plugins/intel_npu/tests/functional/behavior/npuw/test_engine/models/model_builder_internal.hpp new file mode 100644 index 000000000000..95f07774d6d0 --- /dev/null +++ b/src/plugins/intel_npu/tests/functional/behavior/npuw/test_engine/models/model_builder_internal.hpp @@ -0,0 +1,54 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// +// Internal helpers shared between model_builder.cpp and the per-component +// split files (model_builder_norm.cpp, model_builder_rope.cpp, ...). +// Not part of the public test_engine API. +// + +#pragma once + +#include +#include +#include + +namespace ov { +namespace test { +namespace npuw { + +// Named constants for magic values used throughout model construction. +inline constexpr float kRoPEBaseFrequency = 10000.0f; +inline constexpr float kAttentionMaskPaddingFP16Min = -65504.0f; + +/// Deterministic single fill value from tensor name (for scalars, norms, biases). +inline float fill_value_from_name(const std::string& name) { + size_t h = std::hash{}(name); + return 0.01f + static_cast(h % 100000u) / 100000.0f; // [0.01, 1.01) +} + +/// Deterministic xorshift32 PRNG — produces pseudo-random per-element values +/// that are reproducible from the tensor name alone. +inline uint32_t xorshift32(uint32_t& state) { + state ^= state << 13; + state ^= state >> 17; + state ^= state << 5; + return state; +} + +inline uint32_t seed_from_name(const std::string& name) { + // Ensure non-zero seed (xorshift requires it) + uint32_t s = static_cast(std::hash{}(name)); + return s ? s : 1u; +} + +/// KV cache variable id. Missing separator (e.g. "keypresent") is intentional +/// — matches OV's StatefulToStateless pass regex. +inline std::string make_kv_var_id(const std::string& layer, + const std::string& infix, + const std::string& kv_type) { + return "past_key_values." + layer + infix + kv_type + "present." + layer + infix + kv_type; +} + +} // namespace npuw +} // namespace test +} // namespace ov diff --git a/src/plugins/intel_npu/tests/functional/behavior/npuw/test_engine/models/model_builder_masks.cpp b/src/plugins/intel_npu/tests/functional/behavior/npuw/test_engine/models/model_builder_masks.cpp new file mode 100644 index 000000000000..6c3ecf38b53e --- /dev/null +++ b/src/plugins/intel_npu/tests/functional/behavior/npuw/test_engine/models/model_builder_masks.cpp @@ -0,0 +1,236 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "model_builder_masks.hpp" + +#include "model_builder_internal.hpp" +#include "openvino/op/ops.hpp" +#include "openvino/opsets/opset11.hpp" + +namespace ov { +namespace test { +namespace npuw { + +ov::Output make_padding_mask(const ov::Output& attention_mask_output, + ov::element::Type prec) { + auto mask_float = std::make_shared(attention_mask_output, prec); + mask_float->set_friendly_name("model.mask_convert"); + + auto one_const = ov::opset11::Constant::create(prec, ov::Shape{}, {1.0f}); + auto inv_mask = std::make_shared(one_const, mask_float); + inv_mask->set_friendly_name("model.mask_invert"); + + auto neg_inf = ov::opset11::Constant::create(prec, ov::Shape{}, {kAttentionMaskPadding}); + auto padding_mask = std::make_shared(inv_mask, neg_inf); + padding_mask->set_friendly_name("model.padding_mask"); + + auto pad_shape = ov::opset11::Constant::create(ov::element::i64, ov::Shape{4}, std::vector{0, 1, 1, -1}); + auto padding_4d = std::make_shared(padding_mask, pad_shape, true); + padding_4d->set_friendly_name("model.padding_mask_4d"); + + return padding_4d->output(0); +} + +ov::Output make_causal_mask(const ov::Output& input_ids_output, + const ov::Output& attention_mask_output, + ov::element::Type prec) { + auto padding_4d = make_padding_mask(attention_mask_output, prec); + + auto ids_shape = std::make_shared(input_ids_output, ov::element::i64); + ids_shape->set_friendly_name("model.ids_shape"); + + auto mask_shape_node = std::make_shared(attention_mask_output, ov::element::i64); + mask_shape_node->set_friendly_name("model.mask_shape"); + + auto idx1 = ov::opset11::Constant::create(ov::element::i64, ov::Shape{}, {1}); + auto gather_axis = ov::opset11::Constant::create(ov::element::i64, ov::Shape{}, {0}); + + auto seq_len_s = std::make_shared(ids_shape, idx1, gather_axis); + seq_len_s->set_friendly_name("model.seq_len"); + + auto total_seq_s = std::make_shared(mask_shape_node, idx1, gather_axis); + total_seq_s->set_friendly_name("model.total_seq"); + + auto offset = std::make_shared(total_seq_s, seq_len_s); + offset->set_friendly_name("model.causal_offset"); + + auto range_start = ov::opset11::Constant::create(ov::element::i64, ov::Shape{}, {0}); + auto range_step = ov::opset11::Constant::create(ov::element::i64, ov::Shape{}, {1}); + + auto kv_range = std::make_shared(range_start, total_seq_s, range_step, ov::element::i64); + kv_range->set_friendly_name("model.kv_range"); + + auto q_range = std::make_shared(range_start, seq_len_s, range_step, ov::element::i64); + q_range->set_friendly_name("model.q_range"); + + auto q_abs = std::make_shared(q_range, offset); + q_abs->set_friendly_name("model.q_abs_positions"); + + auto axis_last = ov::opset11::Constant::create(ov::element::i64, ov::Shape{1}, {1}); + auto axis_first = ov::opset11::Constant::create(ov::element::i64, ov::Shape{1}, {0}); + + auto q_col = std::make_shared(q_abs, axis_last); + q_col->set_friendly_name("model.q_col"); + + auto kv_row = std::make_shared(kv_range, axis_first); + kv_row->set_friendly_name("model.kv_row"); + + auto causal_bool = std::make_shared(kv_row, q_col); + causal_bool->set_friendly_name("model.causal_bool"); + + auto select_true = ov::opset11::Constant::create(prec, ov::Shape{}, {0.0f}); + auto select_false = ov::opset11::Constant::create(prec, ov::Shape{}, {kAttentionMaskPadding}); + + auto causal_float = std::make_shared(causal_bool, select_true, select_false); + causal_float->set_friendly_name("model.causal_mask"); + + auto unsqueeze_axes = ov::opset11::Constant::create(ov::element::i64, ov::Shape{2}, {0, 1}); + + auto causal_4d = std::make_shared(causal_float, unsqueeze_axes); + causal_4d->set_friendly_name("model.causal_mask_4d"); + + auto combined = std::make_shared(padding_4d, causal_4d); + combined->set_friendly_name("model.mask_4d"); + + return combined->output(0); +} + +CachePositionResult make_cache_position_ids(const ov::Output& input_ids, + const ov::Output& kv_cache_beam_gather, + const std::string& prefix) { + auto ids_shape = std::make_shared(input_ids, ov::element::i64); + ids_shape->set_friendly_name(prefix + "ids_shape"); + auto seq_len = + std::make_shared(ids_shape, + ov::opset11::Constant::create(ov::element::i64, ov::Shape{}, {1}), + ov::opset11::Constant::create(ov::element::i64, ov::Shape{}, {0})); + seq_len->set_friendly_name(prefix + "seq_len"); + + // kv_seq_len = ShapeOf(beam_gather)[2] — root of the CachePositionInput pattern + auto kv_shape = std::make_shared(kv_cache_beam_gather, ov::element::i64); + kv_shape->set_friendly_name(prefix + "kv_shape"); + auto kv_seq_len = + std::make_shared(kv_shape, + ov::opset11::Constant::create(ov::element::i64, ov::Shape{}, {2}), + ov::opset11::Constant::create(ov::element::i64, ov::Shape{}, {0})); + kv_seq_len->set_friendly_name(prefix + "kv_seq_len"); + + // CachePositionInput pattern: Gather -> Add -> Range -> Unsqueeze -> Tile + auto total_seq_len = std::make_shared(kv_seq_len, seq_len); + total_seq_len->set_friendly_name(prefix + "total_seq_len"); + + auto cache_positions = std::make_shared( + kv_seq_len->output(0), + total_seq_len->output(0), + ov::opset11::Constant::create(ov::element::i64, ov::Shape{}, {1})->output(0), + ov::element::i64); + cache_positions->set_friendly_name(prefix + "cache_positions"); + + auto cache_pos_unsq = + std::make_shared(cache_positions, + ov::opset11::Constant::create(ov::element::i64, ov::Shape{}, {0})); + cache_pos_unsq->set_friendly_name(prefix + "cache_pos_unsq"); + + auto batch_dim_for_tile = + std::make_shared(ids_shape, + ov::opset11::Constant::create(ov::element::i64, ov::Shape{1}, {0}), + ov::opset11::Constant::create(ov::element::i64, ov::Shape{}, {0})); + batch_dim_for_tile->set_friendly_name(prefix + "batch_for_tile"); + + auto tile_repeats = std::make_shared( + ov::OutputVector{batch_dim_for_tile->output(0), + ov::opset11::Constant::create(ov::element::i64, ov::Shape{1}, {1})->output(0)}, + 0); + tile_repeats->set_friendly_name(prefix + "tile_repeats"); + + auto position_ids = std::make_shared(cache_pos_unsq, tile_repeats); + position_ids->set_friendly_name(prefix + "position_ids"); + + return {position_ids->output(0), + total_seq_len->output(0), + seq_len->output(0), + cache_pos_unsq->output(0), + ids_shape->output(0)}; +} + +ov::Output make_whisper_causal_mask(const CachePositionResult& cache_pos, const std::string& prefix) { + // kv_idx: Range -> 3x Unsqueeze -> [1, 1, 1, total_seq] + auto mask_range = std::make_shared( + ov::opset11::Constant::create(ov::element::i64, ov::Shape{}, {0})->output(0), + cache_pos.total_seq_len, + ov::opset11::Constant::create(ov::element::i64, ov::Shape{}, {1})->output(0), + ov::element::i64); + mask_range->set_friendly_name(prefix + "mask_range"); + + auto kv_unsq1 = + std::make_shared(mask_range, + ov::opset11::Constant::create(ov::element::i64, ov::Shape{}, {0})); + kv_unsq1->set_friendly_name(prefix + "kv_unsq1"); + auto kv_unsq2 = + std::make_shared(kv_unsq1, + ov::opset11::Constant::create(ov::element::i64, ov::Shape{}, {1})); + kv_unsq2->set_friendly_name(prefix + "kv_unsq2"); + auto kv_unsq3 = + std::make_shared(kv_unsq2, + ov::opset11::Constant::create(ov::element::i64, ov::Shape{}, {2})); + kv_unsq3->set_friendly_name(prefix + "kv_unsq3"); + + // q_idx: cache_pos_unsq -> 2x Unsqueeze -> [1, 1, seq, 1] + auto q_unsq1 = + std::make_shared(cache_pos.cache_pos_unsq, + ov::opset11::Constant::create(ov::element::i64, ov::Shape{}, {1})); + q_unsq1->set_friendly_name(prefix + "q_unsq1"); + auto q_unsq2 = + std::make_shared(q_unsq1, + ov::opset11::Constant::create(ov::element::i64, ov::Shape{}, {3})); + q_unsq2->set_friendly_name(prefix + "q_unsq2"); + + auto causal_bool = std::make_shared(kv_unsq3, q_unsq2); + causal_bool->set_friendly_name(prefix + "causal_mask_bool"); + + auto batch_dim_b = + std::make_shared(cache_pos.ids_shape, + ov::opset11::Constant::create(ov::element::i64, ov::Shape{1}, {0}), + ov::opset11::Constant::create(ov::element::i64, ov::Shape{}, {0})); + auto seq_len_1d = + std::make_shared(cache_pos.seq_len, + ov::opset11::Constant::create(ov::element::i64, ov::Shape{1}, {1}), + false); + auto total_seq_1d = + std::make_shared(cache_pos.total_seq_len, + ov::opset11::Constant::create(ov::element::i64, ov::Shape{}, {0})); + auto broadcast_shape = std::make_shared( + ov::OutputVector{batch_dim_b->output(0), + ov::opset11::Constant::create(ov::element::i64, ov::Shape{1}, {1})->output(0), + seq_len_1d->output(0), + total_seq_1d->output(0)}, + 0); + auto causal_broadcast = + std::make_shared(causal_bool, broadcast_shape, ov::op::BroadcastType::BIDIRECTIONAL); + causal_broadcast->set_friendly_name(prefix + "causal_mask_broadcast"); + + // Always f32 — NPUW's AttentionMask matchers inject f32 nodes + auto select_true = ov::opset11::Constant::create(ov::element::f32, ov::Shape{}, {0.0f}); + auto select_false = ov::opset11::Constant::create(ov::element::f32, ov::Shape{}, {kAttentionMaskPaddingFP16Min}); + auto causal_float = std::make_shared(causal_broadcast, select_true, select_false); + causal_float->set_friendly_name(prefix + "causal_mask"); + + // Structural no-op Slice — AttentionMaskInput (prefill) needs Slice -> SDPA input[3] + auto slice_start = ov::opset11::Constant::create(ov::element::i64, ov::Shape{1}, {0}); + auto slice_stop = + std::make_shared(cache_pos.total_seq_len, + ov::opset11::Constant::create(ov::element::i64, ov::Shape{1}, {1}), + false); + auto slice_step = ov::opset11::Constant::create(ov::element::i64, ov::Shape{1}, {1}); + auto slice_axes = ov::opset11::Constant::create(ov::element::i64, ov::Shape{1}, {3}); + auto causal_sliced = + std::make_shared(causal_float, slice_start, slice_stop, slice_step, slice_axes); + causal_sliced->set_friendly_name(prefix + "causal_mask_sliced"); + + return causal_sliced->output(0); +} + +} // namespace npuw +} // namespace test +} // namespace ov diff --git a/src/plugins/intel_npu/tests/functional/behavior/npuw/test_engine/models/model_builder_masks.hpp b/src/plugins/intel_npu/tests/functional/behavior/npuw/test_engine/models/model_builder_masks.hpp new file mode 100644 index 000000000000..1c315e381751 --- /dev/null +++ b/src/plugins/intel_npu/tests/functional/behavior/npuw/test_engine/models/model_builder_masks.hpp @@ -0,0 +1,51 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#pragma once + +#include + +#include "openvino/core/node.hpp" +#include "openvino/core/type/element_type.hpp" + +namespace ov { +namespace test { +namespace npuw { + +/// Sentinel value used in additive float attention masks for "do not attend" positions. +constexpr float kAttentionMaskPadding = -10000.0f; + +/// Padding-only mask: [batch, seq] -> [batch, 1, 1, seq] float (0.0=attend, kAttentionMaskPadding=pad). +ov::Output make_padding_mask(const ov::Output& attention_mask, + ov::element::Type prec); + +/// Standard causal mask combined with padding. Returns 4D float mask +/// [batch, 1, seq, total_seq] with 0.0=attend, kAttentionMaskPadding=masked. +ov::Output make_causal_mask(const ov::Output& seq_source, + const ov::Output& attention_mask, + ov::element::Type prec); + +/// Cache-position-derived position_ids + total_seq_len, used by Whisper-style +/// decoders that anchor the causal mask on Gather(ShapeOf(beam_gather), 2) +/// — root of the CachePositionInput pattern. +struct CachePositionResult { + ov::Output position_ids; ///< [batch, seq] + ov::Output total_seq_len; + ov::Output seq_len; + ov::Output cache_pos_unsq; ///< [1, seq] (needed for causal mask) + ov::Output ids_shape; ///< ShapeOf(input_ids) +}; + +CachePositionResult make_cache_position_ids(const ov::Output& input_ids, + const ov::Output& kv_cache_beam_gather, + const std::string& prefix); + +/// Whisper-style decoder self-attn causal mask. Uses cache_pos based ranges +/// (different shape from make_causal_mask). Includes the structural Slice that +/// AttentionMaskInput (prefill) requires on SDPA input[3]. +ov::Output make_whisper_causal_mask(const CachePositionResult& cache_pos, const std::string& prefix); + +} // namespace npuw +} // namespace test +} // namespace ov diff --git a/src/plugins/intel_npu/tests/functional/behavior/npuw/test_engine/models/model_builder_norm.cpp b/src/plugins/intel_npu/tests/functional/behavior/npuw/test_engine/models/model_builder_norm.cpp new file mode 100644 index 000000000000..d2a1caad7d24 --- /dev/null +++ b/src/plugins/intel_npu/tests/functional/behavior/npuw/test_engine/models/model_builder_norm.cpp @@ -0,0 +1,67 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "model_builder_norm.hpp" + +#include "model_builder_internal.hpp" +#include "openvino/op/ops.hpp" +#include "openvino/opsets/opset11.hpp" + +namespace ov { +namespace test { +namespace npuw { + +ov::Output LayerNorm::operator()(const ov::Output& input, const std::string& name) const { + float w_val = 1.0f + fill_value_from_name(name + ".weight") * 0.1f; + float b_val = fill_value_from_name(name + ".bias") * 0.01f; + auto weight = + ov::opset11::Constant::create(precision, ov::Shape{hidden_size}, std::vector(hidden_size, w_val)); + weight->set_friendly_name(name + ".weight"); + + auto bias = + ov::opset11::Constant::create(precision, ov::Shape{hidden_size}, std::vector(hidden_size, b_val)); + bias->set_friendly_name(name + ".bias"); + + auto axes = ov::opset11::Constant::create(ov::element::i64, ov::Shape{1}, {-1}); + + auto mvn = std::make_shared(input, axes, true, eps, ov::op::MVNEpsMode::INSIDE_SQRT); + mvn->set_friendly_name(name + "_mvn"); + + auto mul = std::make_shared(mvn, weight); + + auto add = std::make_shared(mul, bias); + add->set_friendly_name(name); + + return add->output(0); +} + +ov::Output RMSNorm::operator()(const ov::Output& input, const std::string& name) const { + float w_val = 1.0f + fill_value_from_name(name + ".weight") * 0.1f; + auto weight = + ov::opset11::Constant::create(precision, ov::Shape{hidden_size}, std::vector(hidden_size, w_val)); + weight->set_friendly_name(name + ".weight"); + + auto squared = std::make_shared(input, input); + + auto axes = ov::opset11::Constant::create(ov::element::i64, ov::Shape{1}, {-1}); + + auto mean = std::make_shared(squared, axes, true); + + auto eps_const = ov::opset11::Constant::create(precision, ov::Shape{}, {eps}); + + auto mean_eps = std::make_shared(mean, eps_const); + + auto rsqrt = std::make_shared(mean_eps); + + auto normalized = std::make_shared(input, rsqrt); + + auto scaled = std::make_shared(normalized, weight); + scaled->set_friendly_name(name); + + return scaled->output(0); +} + +} // namespace npuw +} // namespace test +} // namespace ov diff --git a/src/plugins/intel_npu/tests/functional/behavior/npuw/test_engine/models/model_builder_norm.hpp b/src/plugins/intel_npu/tests/functional/behavior/npuw/test_engine/models/model_builder_norm.hpp new file mode 100644 index 000000000000..bfacf8a4c1a7 --- /dev/null +++ b/src/plugins/intel_npu/tests/functional/behavior/npuw/test_engine/models/model_builder_norm.hpp @@ -0,0 +1,44 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#pragma once + +#include + +#include "openvino/core/node.hpp" +#include "openvino/core/type/element_type.hpp" + +namespace ov { +namespace test { +namespace npuw { + +struct LayerNorm { + size_t hidden_size; + ov::element::Type precision; + float eps; + + LayerNorm(size_t hs, ov::element::Type prec = ov::element::f32, float e = 1e-5f) + : hidden_size(hs), + precision(prec), + eps(e) {} + + ov::Output operator()(const ov::Output& input, const std::string& name) const; +}; + +struct RMSNorm { + size_t hidden_size; + ov::element::Type precision; + float eps; + + RMSNorm(size_t hs, ov::element::Type prec = ov::element::f32, float e = 1e-5f) + : hidden_size(hs), + precision(prec), + eps(e) {} + + ov::Output operator()(const ov::Output& input, const std::string& name) const; +}; + +} // namespace npuw +} // namespace test +} // namespace ov diff --git a/src/plugins/intel_npu/tests/functional/behavior/npuw/test_engine/models/model_builder_rope.cpp b/src/plugins/intel_npu/tests/functional/behavior/npuw/test_engine/models/model_builder_rope.cpp new file mode 100644 index 000000000000..8c116610c9d3 --- /dev/null +++ b/src/plugins/intel_npu/tests/functional/behavior/npuw/test_engine/models/model_builder_rope.cpp @@ -0,0 +1,267 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "model_builder_rope.hpp" + +#include +#include + +#include "model_builder_internal.hpp" +#include "openvino/op/ops.hpp" +#include "openvino/opsets/opset11.hpp" + +namespace ov { +namespace test { +namespace npuw { + +namespace { + +/// Builds frequency cos/sin chain matching NPUW's AddPositionIdsNode pattern. +struct RoPEFrequencies { + ov::Output cos, sin; +}; + +RoPEFrequencies build_rope_frequencies(size_t head_dim, + ov::element::Type precision, + const ov::Output& position_ids, + const ov::Output& shape_source = {}, + const std::string& prefix = "model.rope") { + const size_t half_dim = head_dim / 2; + + std::vector inv_freq_data(half_dim); + for (size_t i = 0; i < half_dim; ++i) { + inv_freq_data[i] = + 1.0f / std::pow(kRoPEBaseFrequency, static_cast(2 * i) / static_cast(head_dim)); + } + auto inv_freq = ov::opset11::Constant::create(ov::element::f32, ov::Shape{1, half_dim, 1}, inv_freq_data); + inv_freq->set_friendly_name(prefix + ".inv_freq"); + + // Broadcast inv_freq to [batch, half_dim, 1] using batch dim from shape_source. + // This ShapeOf -> Gather -> Concat -> Broadcast chain matches NPUW's RopePatternLLama2. + const auto& batch_source = shape_source.get_node() ? shape_source : position_ids; + auto shape_of = std::make_shared(batch_source, ov::element::i64); + shape_of->set_friendly_name(prefix + ".shapeof"); + auto batch_dim = std::make_shared( + shape_of, + ov::opset11::Constant::create(ov::element::i64, ov::Shape{1}, {0}), + ov::opset11::Constant::create(ov::element::i64, ov::Shape{}, {0})); + batch_dim->set_friendly_name(prefix + ".batch_gather"); + auto broadcast_shape = std::make_shared( + ov::OutputVector{ + batch_dim->output(0), + ov::opset11::Constant::create(ov::element::i64, ov::Shape{1}, {static_cast(half_dim)})->output(0), + ov::opset11::Constant::create(ov::element::i64, ov::Shape{1}, {1})->output(0)}, + 0); + broadcast_shape->set_friendly_name(prefix + ".broadcast_shape"); + auto inv_freq_broadcast = + std::make_shared(inv_freq, broadcast_shape, ov::op::BroadcastType::BIDIRECTIONAL); + inv_freq_broadcast->set_friendly_name(prefix + ".inv_freq_broadcast"); + + // position_ids [batch, seq] -> Unsqueeze -> Convert(f32) -> MatMul(broadcast_inv_freq) + // -> Transpose -> Concat(self,self) -> Sin/Cos -> Unsqueeze [batch, 1, seq, head_dim] + auto unsq_axis = ov::opset11::Constant::create(ov::element::i64, ov::Shape{1}, {1}); + auto unsqueezed = std::make_shared(position_ids, unsq_axis); + unsqueezed->set_friendly_name(prefix + ".pos_unsqueeze"); + + auto converted = std::make_shared(unsqueezed, ov::element::f32); + converted->set_friendly_name(prefix + ".pos_convert"); + + auto matmul = std::make_shared(inv_freq_broadcast, converted, false, false); + matmul->set_friendly_name(prefix + ".freq_matmul"); + + auto perm = ov::opset11::Constant::create(ov::element::i64, ov::Shape{3}, std::vector{0, 2, 1}); + auto transposed = std::make_shared(matmul, perm); + transposed->set_friendly_name(prefix + ".freq_transpose"); + + auto concat = + std::make_shared(ov::OutputVector{transposed->output(0), transposed->output(0)}, -1); + concat->set_friendly_name(prefix + ".freq_concat"); + + auto sin_node = std::make_shared(concat); + sin_node->set_friendly_name(prefix + ".sin"); + auto cos_node = std::make_shared(concat); + cos_node->set_friendly_name(prefix + ".cos"); + + auto head_axis = ov::opset11::Constant::create(ov::element::i64, ov::Shape{1}, {1}); + + auto cos_unsq = std::make_shared(cos_node, head_axis); + cos_unsq->set_friendly_name(prefix + ".cos_unsqueeze"); + + auto sin_unsq = std::make_shared(sin_node, head_axis); + sin_unsq->set_friendly_name(prefix + ".sin_unsqueeze"); + + ov::Output cos_out = cos_unsq->output(0); + ov::Output sin_out = sin_unsq->output(0); + if (precision != ov::element::f32) { + auto cos_cvt = std::make_shared(cos_unsq, precision); + cos_cvt->set_friendly_name(prefix + ".cos_convert"); + cos_out = cos_cvt->output(0); + + auto sin_cvt = std::make_shared(sin_unsq, precision); + sin_cvt->set_friendly_name(prefix + ".sin_convert"); + sin_out = sin_cvt->output(0); + } + + return {cos_out, sin_out}; +} + +} // namespace + +HalfRotationRoPE::HalfRotationRoPE(size_t hd, + ov::element::Type precision, + const ov::Output& position_ids, + const ov::Output& shape_source) + : head_dim(hd) { + auto freq = build_rope_frequencies(hd, precision, position_ids, shape_source); + cos_freq = freq.cos; + sin_freq = freq.sin; +} + +ov::Output HalfRotationRoPE::operator()(const ov::Output& input, const std::string& name) const { + const int64_t half = static_cast(head_dim / 2); + + auto zero = ov::opset11::Constant::create(ov::element::i64, ov::Shape{1}, {0}); + auto half_const = ov::opset11::Constant::create(ov::element::i64, ov::Shape{1}, {half}); + auto full_const = ov::opset11::Constant::create(ov::element::i64, ov::Shape{1}, {static_cast(head_dim)}); + auto step = ov::opset11::Constant::create(ov::element::i64, ov::Shape{1}, {1}); + auto last_axis = ov::opset11::Constant::create(ov::element::i64, ov::Shape{1}, {-1}); + + auto first_half = std::make_shared(input, zero, half_const, step, last_axis); + first_half->set_friendly_name(name + "_first_half"); + + auto second_half = std::make_shared(input, half_const, full_const, step, last_axis); + second_half->set_friendly_name(name + "_second_half"); + + auto neg_one = ov::opset11::Constant::create(input.get_element_type(), ov::Shape{}, {-1.0f}); + auto neg_second = std::make_shared(second_half, neg_one); + neg_second->set_friendly_name(name + "_neg_second"); + + auto rotated = + std::make_shared(ov::OutputVector{neg_second->output(0), first_half->output(0)}, -1); + rotated->set_friendly_name(name + "_rotated"); + + auto input_cos = std::make_shared(input, cos_freq); + input_cos->set_friendly_name(name + "_input_cos"); + + auto rotated_sin = std::make_shared(rotated, sin_freq); + rotated_sin->set_friendly_name(name + "_rotated_sin"); + + auto output = std::make_shared(input_cos, rotated_sin); + output->set_friendly_name(name); + + return output->output(0); +} + +InterleavedRoPE::InterleavedRoPE(size_t hd, + ov::element::Type precision, + const ov::Output& position_ids, + const ov::Output& shape_source) + : head_dim(hd) { + auto freq = build_rope_frequencies(hd, precision, position_ids, shape_source); + cos_freq = freq.cos; + sin_freq = freq.sin; +} + +ov::Output InterleavedRoPE::operator()(const ov::Output& input, const std::string& name) const { + const int64_t half_dim = static_cast(head_dim / 2); + + auto reshape_5d = + ov::opset11::Constant::create(ov::element::i64, ov::Shape{5}, std::vector{0, 0, 0, half_dim, 2}); + + auto reshaped = std::make_shared(input, reshape_5d, true); + reshaped->set_friendly_name(name + "_reshape_5d"); + + auto zero = ov::opset11::Constant::create(ov::element::i64, ov::Shape{1}, {0}); + auto one = ov::opset11::Constant::create(ov::element::i64, ov::Shape{1}, {1}); + auto two = ov::opset11::Constant::create(ov::element::i64, ov::Shape{1}, {2}); + auto step = ov::opset11::Constant::create(ov::element::i64, ov::Shape{1}, {1}); + auto last_axis = ov::opset11::Constant::create(ov::element::i64, ov::Shape{1}, {-1}); + + auto x_even = std::make_shared(reshaped, zero, one, step, last_axis); + x_even->set_friendly_name(name + "_x_even"); + + auto x_odd = std::make_shared(reshaped, one, two, step, last_axis); + x_odd->set_friendly_name(name + "_x_odd"); + + auto neg_one = ov::opset11::Constant::create(input.get_element_type(), ov::Shape{}, {-1.0f}); + + auto neg_x_odd = std::make_shared(x_odd, neg_one); + neg_x_odd->set_friendly_name(name + "_neg_x_odd"); + + auto rotated_pairs = std::make_shared(ov::OutputVector{neg_x_odd, x_even}, -1); + rotated_pairs->set_friendly_name(name + "_rotated_pairs"); + + auto reshape_4d = ov::opset11::Constant::create(ov::element::i64, + ov::Shape{4}, + std::vector{0, 0, 0, static_cast(head_dim)}); + + auto rotated = std::make_shared(rotated_pairs, reshape_4d, true); + rotated->set_friendly_name(name + "_rotated"); + + auto input_cos = std::make_shared(input, cos_freq); + input_cos->set_friendly_name(name + "_input_cos"); + + auto rotated_sin = std::make_shared(rotated, sin_freq); + rotated_sin->set_friendly_name(name + "_rotated_sin"); + + auto output = std::make_shared(input_cos, rotated_sin); + output->set_friendly_name(name); + + return output->output(0); +} + +ov::Output make_position_ids_2d() { + auto param = std::make_shared(ov::element::i64, ov::PartialShape{-1, -1}); + param->set_friendly_name("position_ids"); + param->output(0).set_names({"position_ids"}); + return param->output(0); +} + +ov::Output make_position_ids_3d() { + auto param = std::make_shared(ov::element::i64, ov::PartialShape{3, -1, -1}); + param->set_friendly_name("position_ids"); + param->output(0).set_names({"position_ids"}); + + auto indices = ov::opset11::Constant::create(ov::element::i64, ov::Shape{1}, {0}); + auto axis = ov::opset11::Constant::create(ov::element::i64, ov::Shape{}, {0}); + auto gather = std::make_shared(param, indices, axis); + + auto squeeze_axes = ov::opset11::Constant::create(ov::element::i64, ov::Shape{1}, {0}); + auto squeeze = std::make_shared(gather, squeeze_axes); + squeeze->set_friendly_name("position_ids_2d"); + + return squeeze->output(0); +} + +ov::Output make_learned_positional_embedding(const ov::Output& token_embed, + const ov::Output& position_ids, + size_t max_target_positions, + size_t hidden_size, + ov::element::Type precision, + const std::string& prefix) { + float pos_fill = fill_value_from_name(prefix + "embed_positions.weight"); + auto pos_embed_table = + ov::opset11::Constant::create(precision, + ov::Shape{max_target_positions, hidden_size}, + std::vector(max_target_positions * hidden_size, pos_fill)); + pos_embed_table->set_friendly_name(prefix + "embed_positions.weight"); + + auto pos_ids_i32 = std::make_shared(position_ids, ov::element::i32); + pos_ids_i32->set_friendly_name(prefix + "pos_ids_convert"); + auto pos_embed = + std::make_shared(pos_embed_table, + pos_ids_i32, + ov::opset11::Constant::create(ov::element::i64, ov::Shape{}, {0}), + 0); + pos_embed->set_friendly_name(prefix + "pos_embed_gather"); + + auto hidden_states = std::make_shared(token_embed, pos_embed); + hidden_states->set_friendly_name(prefix + "embed_add"); + + return hidden_states->output(0); +} + +} // namespace npuw +} // namespace test +} // namespace ov diff --git a/src/plugins/intel_npu/tests/functional/behavior/npuw/test_engine/models/model_builder_rope.hpp b/src/plugins/intel_npu/tests/functional/behavior/npuw/test_engine/models/model_builder_rope.hpp new file mode 100644 index 000000000000..2b0d2bfbba65 --- /dev/null +++ b/src/plugins/intel_npu/tests/functional/behavior/npuw/test_engine/models/model_builder_rope.hpp @@ -0,0 +1,60 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#pragma once + +#include + +#include "openvino/core/node.hpp" +#include "openvino/core/type/element_type.hpp" + +namespace ov { +namespace test { +namespace npuw { + +/// Position IDs baked in at construction, cos/sin shared across layers. +/// shape_source provides batch dim for inv_freq Broadcast (matches NPUW RopeCache pattern). +/// Defaults to position_ids when not specified. +struct HalfRotationRoPE { + size_t head_dim; + ov::Output cos_freq, sin_freq; + + HalfRotationRoPE(size_t head_dim, + ov::element::Type precision, + const ov::Output& position_ids, + const ov::Output& shape_source = {}); + + ov::Output operator()(const ov::Output& input, const std::string& name) const; +}; + +struct InterleavedRoPE { + size_t head_dim; + ov::Output cos_freq, sin_freq; + + InterleavedRoPE(size_t head_dim, + ov::element::Type precision, + const ov::Output& position_ids, + const ov::Output& shape_source = {}); + + ov::Output operator()(const ov::Output& input, const std::string& name) const; +}; + +/// [batch, seq] position_ids Parameter. +ov::Output make_position_ids_2d(); + +/// [3, batch, seq] position_ids Parameter for m-rope. Returns [batch, seq] slice. +ov::Output make_position_ids_3d(); + +/// Learned absolute positional embedding lookup (no RoPE — used by Whisper). +/// Adds a Gather'd row from a per-position embedding table to the token embeddings. +ov::Output make_learned_positional_embedding(const ov::Output& token_embed, + const ov::Output& position_ids, + size_t max_target_positions, + size_t hidden_size, + ov::element::Type precision, + const std::string& prefix); + +} // namespace npuw +} // namespace test +} // namespace ov diff --git a/src/plugins/intel_npu/tests/functional/behavior/npuw/test_engine/models/model_builder_types.hpp b/src/plugins/intel_npu/tests/functional/behavior/npuw/test_engine/models/model_builder_types.hpp new file mode 100644 index 000000000000..fd089945d04a --- /dev/null +++ b/src/plugins/intel_npu/tests/functional/behavior/npuw/test_engine/models/model_builder_types.hpp @@ -0,0 +1,47 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// +// Common typedefs and small structs shared across all model_builder_* headers. +// + +#pragma once + +#include +#include +#include +#include + +#include "openvino/core/node.hpp" +#include "openvino/core/shape.hpp" +#include "openvino/core/type/element_type.hpp" +#include "openvino/op/util/variable.hpp" + +namespace ov { +namespace test { +namespace npuw { + +struct KVCacheResult { + ov::Output concatenated; + ov::Output beam_gather; + std::shared_ptr assign; +}; + +struct KVCacheReadState { + std::shared_ptr variable; + ov::Output beam_gather; +}; + +using WeightFn = std::function(const std::string&, const ov::Shape&, ov::element::Type)>; +using NormFn = std::function(const ov::Output&, const std::string&)>; +using FFNFn = std::function(const ov::Output&, const std::string&)>; +using RoPEFn = std::function(const ov::Output&, const std::string&)>; +using LayerFn = std::function(const ov::Output&, const std::string&, size_t)>; + +/// (projected_k, projected_v, layer_idx) -> (cached_k, cached_v). Empty = no cache. +using KVCacheFn = + std::function, + ov::Output>(const ov::Output&, const ov::Output&, size_t)>; + +} // namespace npuw +} // namespace test +} // namespace ov diff --git a/src/plugins/intel_npu/tests/functional/behavior/npuw/test_engine/models/model_builder_weights.cpp b/src/plugins/intel_npu/tests/functional/behavior/npuw/test_engine/models/model_builder_weights.cpp new file mode 100644 index 000000000000..1456d49d37da --- /dev/null +++ b/src/plugins/intel_npu/tests/functional/behavior/npuw/test_engine/models/model_builder_weights.cpp @@ -0,0 +1,250 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "model_builder_weights.hpp" + +#include +#include + +#include "model_builder_internal.hpp" +#include "openvino/op/ops.hpp" +#include "openvino/opsets/opset11.hpp" + +namespace ov { +namespace test { +namespace npuw { + +ov::Output FloatWeight::operator()(const std::string& name, + const ov::Shape& shape, + ov::element::Type compute_precision) const { + // Deterministic pseudo-random fill: each element gets a unique value derived + // from the tensor name via xorshift32. Same name always produces the same + // weights, but values look random and span a wide enough range ([-0.5, 0.5)) + // to survive FP16 quantisation through NPUW. This prevents CSE from merging + // same-shape projections and produces diverse logits in the LM head. + uint32_t state = seed_from_name(name); + size_t total = ov::shape_size(shape); + std::vector data(total); + for (size_t i = 0; i < total; ++i) { + uint32_t r = xorshift32(state); + data[i] = static_cast(r % 10000u) / 10000.0f - 0.5f; // [-0.5, 0.5) + } + auto weight = ov::opset11::Constant::create(storage_type, shape, data); + weight->set_friendly_name(name); + + if (storage_type == compute_precision) { + return weight->output(0); + } + auto convert = std::make_shared(weight, compute_precision); + convert->set_friendly_name(name + "_convert"); + return convert->output(0); +} + +ov::Output CompressedWeight::operator()(const std::string& name, + const ov::Shape& shape, + ov::element::Type compute_precision) const { + OPENVINO_ASSERT(shape.size() == 2 || shape.size() == 3, + "CompressedWeight expects 2D or 3D shape, got ", + shape.size(), + "D"); + // 3D [batch, rows, cols]: batched expert weights (MoE). + // 2D [rows, cols]: standard linear projection. + const bool is_3d = (shape.size() == 3); + const size_t batch = is_3d ? shape[0] : 1; + const size_t rows = is_3d ? shape[1] : shape[0]; + const size_t cols = is_3d ? shape[2] : shape[1]; + + // --- Validate pattern constraints --- + const bool has_zp = + (pattern == DCOffPattern::SYMM_ZP || pattern == DCOffPattern::GPTQ || pattern == DCOffPattern::ASYMM_ZP); + if (has_zp) { + OPENVINO_ASSERT(storage_type == ov::element::u4, + "Zero-point patterns require u4 storage type, got ", + storage_type); + } + + // Decomp element type: f32 for SYMM_NO_ZP_F32 and GPTQ, f16 otherwise. + const bool decomp_f32 = (pattern == DCOffPattern::SYMM_NO_ZP_F32 || pattern == DCOffPattern::GPTQ); + const auto decomp_et = decomp_f32 ? ov::element::f32 : ov::element::f16; + + // --- Weight value range --- + int8_t lo = 0, hi = 0; + if (storage_type == ov::element::i4) { + lo = -7; // Symmetric range [-7, 7] (not [-8, 7]) — no zero point needed. + hi = 7; + } else if (storage_type == ov::element::u4) { + lo = 1; + hi = 15; + } else if (storage_type == ov::element::nf4) { + lo = 0; + hi = 15; + } else { + lo = -100; + hi = 100; + } + + // --- Group quantization setup --- + const bool has_groups = group_size > 0; + size_t num_groups = 1; + if (has_groups) { + OPENVINO_ASSERT(group_size >= 64 && group_size % 64 == 0, + "Group size must be >= 64 and a multiple of 64 " + "(DCOFF AVX2 unpack constraint), got ", + group_size); + OPENVINO_ASSERT(cols >= group_size && cols % group_size == 0, + "Group quantization requires cols (", + cols, + ") >= group_size (", + group_size, + ") and evenly divisible"); + num_groups = cols / group_size; + } + + // GPTQ and ASYMM_ZP only have group-quant DCOFF patterns (Reshape2 / AsymmZP::Reshape). + // No per-channel (group_size=0) DCOFF pass exists for these pattern types. + if (pattern == DCOffPattern::GPTQ || pattern == DCOffPattern::ASYMM_ZP) { + OPENVINO_ASSERT(has_groups, + "DCOffPattern::", + (pattern == DCOffPattern::GPTQ ? "GPTQ" : "ASYMM_ZP"), + " requires group_size > 0 (no per-channel DCOFF pass exists)"); + } + + // Weight shape includes batch dim for 3D (MoE batched experts). + // Group quant adds an extra group dim inside each [rows, cols] slice. + ov::Shape weight_shape; + if (is_3d) { + weight_shape = has_groups ? ov::Shape{batch, rows, num_groups, group_size} : ov::Shape{batch, rows, cols}; + } else { + weight_shape = has_groups ? ov::Shape{rows, num_groups, group_size} : ov::Shape{rows, cols}; + } + uint32_t w_state = seed_from_name(name); + std::vector w_data(batch * rows * cols); + for (size_t i = 0; i < w_data.size(); ++i) { + uint32_t r = xorshift32(w_state); + int val = static_cast(lo) + static_cast(r % static_cast(hi - lo + 1)); + w_data[i] = static_cast(val); + } + auto weight = ov::opset11::Constant::create(storage_type, weight_shape, w_data); + weight->set_friendly_name(name); + + ov::Output decomp_input = weight->output(0); + + // --- Convert weight to decomp element type (skip if already matching) --- + ov::Output multiply_input; + if (storage_type != decomp_et) { + auto convert = std::make_shared(decomp_input, decomp_et); + convert->set_friendly_name(name + "_convert"); + multiply_input = convert->output(0); + } else { + multiply_input = decomp_input; + } + + // --- Zero-point subtraction (SYMM_ZP, GPTQ, ASYMM_ZP) --- + if (has_zp) { + // ZP shape: always 2D (same reasoning as scale — MoE executor doesn't slice ZP) + ov::Shape zp_shape; + zp_shape = has_groups ? ov::Shape{rows, num_groups, 1} : ov::Shape{rows, 1}; + const size_t zp_count = has_groups ? rows * num_groups : rows; + const int mid = (static_cast(lo) + static_cast(hi)) / 2; + + if (pattern == DCOffPattern::GPTQ) { + // GPTQ: ZP is f32 Constant fed directly to Subtract (no Convert). + // Uniform value so it stays Constant after partitioning. + std::vector zp_f32(zp_count, static_cast(mid)); + auto zp_const = ov::opset11::Constant::create(ov::element::f32, zp_shape, zp_f32); + zp_const->set_friendly_name(name + "_zp"); + + auto subtract = std::make_shared(multiply_input, zp_const); + subtract->set_friendly_name(name + "_subtract"); + multiply_input = subtract->output(0); + + } else if (pattern == DCOffPattern::SYMM_ZP) { + // SymmZP: u4 ZP Constant → Convert(f16) → Subtract. + // Uniform value across all layers so it stays Constant after partitioning. + std::vector zp_data(zp_count, static_cast(mid)); + auto zp_const = ov::opset11::Constant::create(storage_type, zp_shape, zp_data); + zp_const->set_friendly_name(name + "_zp"); + + auto zp_convert = std::make_shared(zp_const, ov::element::f16); + zp_convert->set_friendly_name(name + "_zp_convert"); + + auto subtract = std::make_shared(multiply_input, zp_convert); + subtract->set_friendly_name(name + "_subtract"); + multiply_input = subtract->output(0); + + } else { + // AsymmZP: u4 ZP Constant → Convert(f16) → Subtract. + // Per-layer varying values (seeded from name) so NPUW promotes + // it to a Parameter after partitioning. + uint32_t zp_state = seed_from_name(name + "_zp"); + std::vector zp_data(zp_count); + for (size_t i = 0; i < zp_data.size(); ++i) { + uint32_t r = xorshift32(zp_state); + int zp_val = mid + static_cast(r % 3u) - 1; + zp_data[i] = static_cast(zp_val); + } + auto zp_const = ov::opset11::Constant::create(storage_type, zp_shape, zp_data); + zp_const->set_friendly_name(name + "_zp"); + + auto zp_convert = std::make_shared(zp_const, ov::element::f16); + zp_convert->set_friendly_name(name + "_zp_convert"); + + auto subtract = std::make_shared(multiply_input, zp_convert); + subtract->set_friendly_name(name + "_subtract"); + multiply_input = subtract->output(0); + } + } + + // --- Scale: per-group or per-channel, with optional batch dim --- + // Magnitude kept small (scale_range ≈ 1/hi) so decompressed values stay moderate + // (roughly ±1), preventing hidden state overflow. + // 3D batched weights (MoE) use 3D scale [batch, rows, 1] so DEVICE_ROUTED transform + // can identify the expert dimension (shape[0] == num_experts). + ov::Shape scale_shape; + if (is_3d) { + scale_shape = has_groups ? ov::Shape{batch, rows, num_groups, 1} : ov::Shape{batch, rows, 1}; + } else { + scale_shape = has_groups ? ov::Shape{rows, num_groups, 1} : ov::Shape{rows, 1}; + } + const size_t scale_count = batch * (has_groups ? rows * num_groups : rows); + const float scale_range = 1.0f / static_cast(hi); + uint32_t s_state = seed_from_name(name + "_scale"); + std::vector scale_data(scale_count); + for (size_t i = 0; i < scale_data.size(); ++i) { + uint32_t r = xorshift32(s_state); + scale_data[i] = scale_range * (0.1f + static_cast(r % 1000u) / 1000.0f); + } + auto scale = ov::opset11::Constant::create(decomp_et, scale_shape, scale_data); + scale->set_friendly_name(name + "_scale"); + + auto scaled = std::make_shared(multiply_input, scale); + scaled->set_friendly_name(name + "_decompress"); + + ov::Output decompressed = scaled->output(0); + + // --- Group quant: collapse group dims back to original shape --- + if (has_groups) { + std::vector out_dims; + if (is_3d) + out_dims = {static_cast(batch), static_cast(rows), static_cast(cols)}; + else + out_dims = {static_cast(rows), static_cast(cols)}; + auto out_shape = ov::opset11::Constant::create(ov::element::i64, ov::Shape{out_dims.size()}, out_dims); + auto reshaped = std::make_shared(decompressed, out_shape, false); + reshaped->set_friendly_name(name + "_reshape"); + decompressed = reshaped->output(0); + } + + // --- Convert to compute precision if needed --- + if (decomp_et != compute_precision) { + auto to_compute = std::make_shared(decompressed, compute_precision); + to_compute->set_friendly_name(name + "_to_compute"); + return to_compute->output(0); + } + return decompressed; +} + +} // namespace npuw +} // namespace test +} // namespace ov diff --git a/src/plugins/intel_npu/tests/functional/behavior/npuw/test_engine/models/model_builder_weights.hpp b/src/plugins/intel_npu/tests/functional/behavior/npuw/test_engine/models/model_builder_weights.hpp new file mode 100644 index 000000000000..4908f7a83a2a --- /dev/null +++ b/src/plugins/intel_npu/tests/functional/behavior/npuw/test_engine/models/model_builder_weights.hpp @@ -0,0 +1,85 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#pragma once + +#include + +#include "openvino/core/node.hpp" +#include "openvino/core/shape.hpp" +#include "openvino/core/type/element_type.hpp" + +namespace ov { +namespace test { +namespace npuw { + +struct FloatWeight { + ov::element::Type storage_type; + + FloatWeight(ov::element::Type st = ov::element::f32) : storage_type(st) {} + + ov::Output operator()(const std::string& name, + const ov::Shape& shape, + ov::element::Type compute_precision) const; +}; + +using FP32Weight = FloatWeight; +struct FP16Weight : FloatWeight { + FP16Weight() : FloatWeight(ov::element::f16) {} +}; + +/// Decompression pattern for CompressedWeight, matching DCOFF recognition. +/// +/// After NPUW partitioning, Constants become Parameters. The patterns below +/// describe the graph that DCOFF will see *before* partitioning transforms it. +/// +/// Pattern | Chain (f16/f32 = decomp type) | DCOFF class matched +/// ----------------+-------------------------------------------+------------------------------ +/// SYMM_NO_ZP | Cvt(f16) → Mul(f16 scale) [→ Reshape] | Reshape3 / Reshape4 +/// SYMM_NO_ZP_F32 | Cvt(f32) → Mul(f32 scale) [→ Reshape] | SymmNoZP::MatMul / Reshape4 +/// SYMM_ZP | Cvt(f16) → Sub(Const u4→Cvt f16) → Mul | Reshape1 / Convert1 +/// GPTQ | Cvt(f32) → Sub(Const f32) → Mul(f32) | Reshape2 +/// ASYMM_ZP | Cvt(f16) → Sub(varying u4→Cvt f16) → Mul | AsymmZP::Reshape +enum class DCOffPattern { + SYMM_NO_ZP, ///< f16 chain, no zero point. i4/i8/u4 storage. + SYMM_NO_ZP_F32, ///< f32 chain, no zero point. i4/i8/nf4 storage. + SYMM_ZP, ///< f16 chain, uniform u4 zero point (Constant after partitioning). u4 storage. + GPTQ, ///< f32 chain, uniform f32 zero point (no Convert on ZP). u4 storage. + ASYMM_ZP, ///< f16 chain, per-layer varying u4 zero point (Parameter after partitioning). u4 storage. +}; + +/// Compressed (quantized) weight with configurable DCOFF decompression pattern. +/// group_size > 0 = per-group scale (3D weight → decompress → Reshape 2D). +/// group_size = 0 = per-channel scale (2D weight, no Reshape). +/// Note: GPTQ and ASYMM_ZP require group_size > 0 (no per-channel DCOFF pass exists). +struct CompressedWeight { + ov::element::Type storage_type; + size_t group_size; ///< 0 = per-channel scale, >0 = per-group scale + DCOffPattern pattern; ///< Decompression pattern to generate + + explicit CompressedWeight(ov::element::Type st, size_t gs = 0, DCOffPattern pat = DCOffPattern::SYMM_NO_ZP) + : storage_type(st), + group_size(gs), + pattern(pat) {} + + ov::Output operator()(const std::string& name, + const ov::Shape& shape, + ov::element::Type compute_precision) const; +}; + +struct INT8Weight : CompressedWeight { + INT8Weight() : CompressedWeight(ov::element::i8) {} +}; + +struct INT4Weight : CompressedWeight { + INT4Weight() : CompressedWeight(ov::element::i4) {} +}; + +struct INT4GroupWeight : CompressedWeight { + explicit INT4GroupWeight(size_t gs = 128) : CompressedWeight(ov::element::i4, gs) {} +}; + +} // namespace npuw +} // namespace test +} // namespace ov diff --git a/src/plugins/intel_npu/tests/functional/behavior/ov_infer_request/callback.cpp b/src/plugins/intel_npu/tests/functional/behavior/ov_infer_request/callback.cpp index 1b6cb530e80a..8b93aca861b7 100644 --- a/src/plugins/intel_npu/tests/functional/behavior/ov_infer_request/callback.cpp +++ b/src/plugins/intel_npu/tests/functional/behavior/ov_infer_request/callback.cpp @@ -9,27 +9,10 @@ namespace { const std::vector configs = {{}}; -const std::vector multiConfigs = {{ov::device::priorities(ov::test::utils::DEVICE_NPU), - ov::device::properties(ov::test::utils::DEVICE_NPU, ov::AnyMap{})}}; - -const std::vector autoConfigs = {{ov::device::priorities(ov::test::utils::DEVICE_NPU), - ov::device::properties(ov::test::utils::DEVICE_NPU, ov::AnyMap{})}}; - INSTANTIATE_TEST_SUITE_P(compatibility_smoke_BehaviorTests, OVInferRequestCallbackTestsNPU, ::testing::Combine(::testing::Values(ov::test::utils::DEVICE_NPU), ::testing::ValuesIn(configs)), InferRequestParamsAnyMapTestName::getTestCaseName); -INSTANTIATE_TEST_SUITE_P(compatibility_smoke_Multi_BehaviorTests, - OVInferRequestCallbackTestsNPU, - ::testing::Combine(::testing::Values(ov::test::utils::DEVICE_MULTI), - ::testing::ValuesIn(multiConfigs)), - InferRequestParamsAnyMapTestName::getTestCaseName); - -INSTANTIATE_TEST_SUITE_P(compatibility_smoke_Auto_BehaviorTests, - OVInferRequestCallbackTestsNPU, - ::testing::Combine(::testing::Values(ov::test::utils::DEVICE_AUTO), - ::testing::ValuesIn(autoConfigs)), - InferRequestParamsAnyMapTestName::getTestCaseName); } // namespace diff --git a/src/plugins/intel_npu/tests/functional/behavior/ov_infer_request/callback.hpp b/src/plugins/intel_npu/tests/functional/behavior/ov_infer_request/callback.hpp index 97fb23046688..e25e524b7d42 100644 --- a/src/plugins/intel_npu/tests/functional/behavior/ov_infer_request/callback.hpp +++ b/src/plugins/intel_npu/tests/functional/behavior/ov_infer_request/callback.hpp @@ -4,7 +4,10 @@ #pragma once +#include +#include #include +#include #include "common/utils.hpp" @@ -13,112 +16,60 @@ namespace test { namespace behavior { using OVInferRequestCallbackTestsNPU = OVInferRequestTestsNPU; -TEST_P(OVInferRequestCallbackTestsNPU, canCallAsyncWithCompletionCallback) { +TEST_P(OVInferRequestCallbackTestsNPU, callbackCanCallInfer) { ov::InferRequest req; OV_ASSERT_NO_THROW(req = execNet.create_infer_request()); - bool is_called = false; - OV_ASSERT_NO_THROW(req.set_callback([&](std::exception_ptr exception_ptr) { - // HSD_1805940120: Wait on starting callback return HDDL_ERROR_INVAL_TASK_HANDLE - ASSERT_EQ(exception_ptr, nullptr); - is_called = true; - })); - OV_ASSERT_NO_THROW(req.start_async()); - OV_ASSERT_NO_THROW(req.wait()); - ASSERT_TRUE(is_called); -} - -TEST_P(OVInferRequestCallbackTestsNPU, syncInferDoesNotCallCompletionCallback) { - ov::InferRequest req; - OV_ASSERT_NO_THROW(req = execNet.create_infer_request()); - bool is_called = false; - req.set_callback([&](std::exception_ptr exception_ptr) { - ASSERT_EQ(nullptr, exception_ptr); - is_called = true; - }); - req.infer(); - ASSERT_FALSE(is_called); -} -// test that can wait all callbacks on dtor -TEST_P(OVInferRequestCallbackTestsNPU, canStartSeveralAsyncInsideCompletionCallbackWithSafeDtor) { - const int NUM_ITER = 10; - struct TestUserData { - std::atomic numIter = {0}; - std::promise promise; - }; - TestUserData data; + std::atomic callback_calls{0}; + std::promise done; + auto done_future = done.get_future(); + std::atomic_bool done_signaled{false}; - ov::InferRequest req; - OV_ASSERT_NO_THROW(req = execNet.create_infer_request()); OV_ASSERT_NO_THROW(req.set_callback([&](std::exception_ptr exception_ptr) { - if (exception_ptr) { - data.promise.set_exception(exception_ptr); - } else { - if (data.numIter.fetch_add(1) != NUM_ITER) { - req.start_async(); - } else { - data.promise.set_value(true); - } + ASSERT_EQ(nullptr, exception_ptr); + callback_calls.fetch_add(1, std::memory_order_relaxed); + req.infer(); + + if (!done_signaled.exchange(true, std::memory_order_relaxed)) { + done.set_value(); } })); - auto future = data.promise.get_future(); + OV_ASSERT_NO_THROW(req.start_async()); OV_ASSERT_NO_THROW(req.wait()); - future.wait(); - auto callbackStatus = future.get(); - ASSERT_TRUE(callbackStatus); - auto dataNumIter = data.numIter - 1; - ASSERT_EQ(NUM_ITER, dataNumIter); -} -TEST_P(OVInferRequestCallbackTestsNPU, returnGeneralErrorIfCallbackThrowException) { - ov::InferRequest req; - OV_ASSERT_NO_THROW(req = execNet.create_infer_request()); - OV_ASSERT_NO_THROW(req.set_callback([](std::exception_ptr) { - OPENVINO_THROW("Throw"); - })); - OV_ASSERT_NO_THROW(req.start_async()); - ASSERT_THROW(req.wait(), ov::Exception); + ASSERT_EQ(done_future.wait_for(std::chrono::seconds(30)), std::future_status::ready); + ASSERT_EQ(callback_calls.load(std::memory_order_relaxed), 1); } -TEST_P(OVInferRequestCallbackTestsNPU, ReturnResultNotReadyFromWaitInAsyncModeForTooSmallTimeout) { +TEST_P(OVInferRequestCallbackTestsNPU, callbackCanCallStartAsyncAndWait) { ov::InferRequest req; OV_ASSERT_NO_THROW(req = execNet.create_infer_request()); - std::promise callbackTimeStamp; - auto callbackTimeStampFuture = callbackTimeStamp.get_future(); - // add a callback to the request and capture the timestamp + + std::atomic callback_calls{0}; + std::promise nested_done; + auto nested_done_future = nested_done.get_future(); + std::atomic_bool nested_done_signaled{false}; + OV_ASSERT_NO_THROW(req.set_callback([&](std::exception_ptr exception_ptr) { - if (exception_ptr) { - callbackTimeStamp.set_exception(exception_ptr); - } else { - callbackTimeStamp.set_value(std::chrono::system_clock::now()); + ASSERT_EQ(nullptr, exception_ptr); + const auto call_index = callback_calls.fetch_add(1, std::memory_order_relaxed); + + if (call_index == 0) { + req.start_async(); + req.wait(); + } else if (call_index == 1) { + if (!nested_done_signaled.exchange(true, std::memory_order_relaxed)) { + nested_done.set_value(); + } } })); - OV_ASSERT_NO_THROW(req.start_async()); - bool ready = false; - OV_ASSERT_NO_THROW(ready = req.wait_for({})); - // get timestamp taken AFTER return from the wait(STATUS_ONLY) - const auto afterWaitTimeStamp = std::chrono::system_clock::now(); - // IF the callback timestamp is larger than the afterWaitTimeStamp - // then we should observe false ready result - if (afterWaitTimeStamp < callbackTimeStampFuture.get()) { - ASSERT_FALSE(ready); - } - OV_ASSERT_NO_THROW(req.wait()); -} -TEST_P(OVInferRequestCallbackTestsNPU, ImplDoesNotCopyCallback) { - ov::InferRequest req; - OV_ASSERT_NO_THROW(req = execNet.create_infer_request()); - { - auto somePtr = std::make_shared(42); - OV_ASSERT_NO_THROW(req.set_callback([somePtr](std::exception_ptr exception_ptr) { - ASSERT_EQ(nullptr, exception_ptr); - ASSERT_EQ(1, somePtr.use_count()); - })); - } OV_ASSERT_NO_THROW(req.start_async()); OV_ASSERT_NO_THROW(req.wait()); + + ASSERT_EQ(nested_done_future.wait_for(std::chrono::seconds(30)), std::future_status::ready); + ASSERT_GE(callback_calls.load(std::memory_order_relaxed), 2); } } // namespace behavior diff --git a/src/plugins/intel_npu/tests/functional/behavior/ov_infer_request/cancellation.cpp b/src/plugins/intel_npu/tests/functional/behavior/ov_infer_request/cancellation.cpp deleted file mode 100644 index c80b5db8bb8b..000000000000 --- a/src/plugins/intel_npu/tests/functional/behavior/ov_infer_request/cancellation.cpp +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (C) 2018-2026 Intel Corporation -// SPDX-License-Identifier: Apache-2.0 -// - -#include "cancellation.hpp" - -#include "common/npu_test_env_cfg.hpp" - -namespace { -const std::vector configs = {{}}; - -INSTANTIATE_TEST_SUITE_P(compatibility_smoke_BehaviorTests, - OVInferRequestCancellationTestsNPU, - ::testing::Combine(::testing::Values(ov::test::utils::DEVICE_NPU), - ::testing::ValuesIn(configs)), - InferRequestParamsAnyMapTestName::getTestCaseName); -} // namespace diff --git a/src/plugins/intel_npu/tests/functional/behavior/ov_infer_request/cancellation.hpp b/src/plugins/intel_npu/tests/functional/behavior/ov_infer_request/cancellation.hpp deleted file mode 100644 index 10c3c49f44f1..000000000000 --- a/src/plugins/intel_npu/tests/functional/behavior/ov_infer_request/cancellation.hpp +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (C) 2018-2026 Intel Corporation -// SPDX-License-Identifier: Apache-2.0 -// - -#pragma once - -#include - -#include "common/utils.hpp" -#include "openvino/runtime/exception.hpp" - -namespace ov { -namespace test { -namespace behavior { -using OVInferRequestCancellationTestsNPU = OVInferRequestTestsNPU; - -TEST_P(OVInferRequestCancellationTestsNPU, canCancelAsyncRequest) { - ov::InferRequest req; - OV_ASSERT_NO_THROW(req = execNet.create_infer_request()); - OV_ASSERT_NO_THROW(req.start_async()); - OV_ASSERT_NO_THROW(req.cancel()); - try { - req.wait(); - } catch (const ov::Cancelled&) { - SUCCEED(); - } -} - -TEST_P(OVInferRequestCancellationTestsNPU, CanResetAfterCancelAsyncRequest) { - ov::InferRequest req; - OV_ASSERT_NO_THROW(req = execNet.create_infer_request()); - OV_ASSERT_NO_THROW(req.start_async()); - OV_ASSERT_NO_THROW(req.cancel()); - try { - req.wait(); - } catch (const ov::Cancelled&) { - SUCCEED(); - } - OV_ASSERT_NO_THROW(req.start_async()); - OV_ASSERT_NO_THROW(req.wait()); -} - -TEST_P(OVInferRequestCancellationTestsNPU, canCancelBeforeAsyncRequest) { - ov::InferRequest req; - OV_ASSERT_NO_THROW(req = execNet.create_infer_request()); - OV_ASSERT_NO_THROW(req.cancel()); -} - -TEST_P(OVInferRequestCancellationTestsNPU, canCancelInferRequest) { - ov::InferRequest req; - OV_ASSERT_NO_THROW(req = execNet.create_infer_request()); - auto infer = std::async(std::launch::async, [&req] { - req.infer(); - }); - while (!req.wait_for({})) { - } - OV_ASSERT_NO_THROW(req.cancel()); - try { - infer.get(); - } catch (const ov::Cancelled&) { - SUCCEED(); - } -} -} // namespace behavior -} // namespace test -} // namespace ov diff --git a/src/plugins/intel_npu/tests/functional/behavior/ov_infer_request/infer_request_run.cpp b/src/plugins/intel_npu/tests/functional/behavior/ov_infer_request/infer_request_run.cpp index d679b2e0e8d1..8386c0b3c074 100644 --- a/src/plugins/intel_npu/tests/functional/behavior/ov_infer_request/infer_request_run.cpp +++ b/src/plugins/intel_npu/tests/functional/behavior/ov_infer_request/infer_request_run.cpp @@ -12,6 +12,7 @@ using namespace ov::test::behavior; namespace { const std::vector configsInferRequestRunTests = {{}}; + const std::vector configsBooleanPrecisionInferRequestRunTests = { {{ov::intel_npu::compiler_type(ov::intel_npu::CompilerType::PLUGIN), ov::intel_npu::batch_mode(ov::intel_npu::BatchMode::PLUGIN), @@ -28,12 +29,22 @@ const std::vector configsBooleanPrecisionInferRequestRunTests = { {{ov::intel_npu::compiler_type(ov::intel_npu::CompilerType::DRIVER), ov::intel_npu::batch_mode(ov::intel_npu::BatchMode::COMPILER)}}}; +const std::vector configsWithDifferentNumStreamsTest = {{{ov::num_streams(0)}}, + {{ov::num_streams(2)}}, + {{ov::num_streams(8)}}}; + INSTANTIATE_TEST_SUITE_P(compatibility_smoke_BehaviorTest, InferRequestRunTests, ::testing::Combine(::testing::Values(ov::test::utils::DEVICE_NPU), ::testing::ValuesIn(configsInferRequestRunTests)), InferRequestRunTests::getTestCaseName); +INSTANTIATE_TEST_SUITE_P(compatibility_smoke_BehaviorTest, + NumStreamsTests, + ::testing::Combine(::testing::Values(ov::test::utils::DEVICE_NPU), + ::testing::ValuesIn(configsWithDifferentNumStreamsTest)), + InferRequestRunTests::getTestCaseName); + INSTANTIATE_TEST_SUITE_P(compatibility_smoke_BehaviorTest, BooleanPrecisionInferRequestRunTests, ::testing::Combine(::testing::Values(ov::test::utils::DEVICE_NPU), @@ -76,6 +87,12 @@ INSTANTIATE_TEST_SUITE_P(smoke_BehaviorTest, ::testing::ValuesIn(configsInferRequestRunTests)), InferRequestRunTests::getTestCaseName); +INSTANTIATE_TEST_SUITE_P(smoke_BehaviorTest, + DynamicBoundsTests, + ::testing::Combine(::testing::Values(ov::test::utils::DEVICE_NPU), + ::testing::ValuesIn(configsInferRequestRunTests)), + InferRequestRunTests::getTestCaseName); + const std::vector batchingConfigs = {{ov::intel_npu::batch_mode(ov::intel_npu::BatchMode::PLUGIN)}, {ov::intel_npu::batch_mode(ov::intel_npu::BatchMode::COMPILER)}, {ov::intel_npu::batch_mode(ov::intel_npu::BatchMode::AUTO)}}; diff --git a/src/plugins/intel_npu/tests/functional/behavior/ov_infer_request/infer_request_run.hpp b/src/plugins/intel_npu/tests/functional/behavior/ov_infer_request/infer_request_run.hpp index 3c4d5053795a..10ba7a2180ae 100644 --- a/src/plugins/intel_npu/tests/functional/behavior/ov_infer_request/infer_request_run.hpp +++ b/src/plugins/intel_npu/tests/functional/behavior/ov_infer_request/infer_request_run.hpp @@ -17,6 +17,7 @@ #include "behavior/ov_infer_request/inference.hpp" #include "common/npu_test_env_cfg.hpp" #include "common/utils.hpp" +#include "common/zero_init_mock.hpp" #include "common_test_utils/ov_tensor_utils.hpp" #include "intel_npu/npu_private_properties.hpp" #include "intel_npu/utils/zero/zero_init.hpp" @@ -37,22 +38,6 @@ using CompilationParams = std::tuple& callback) { - ov::util::set_log_callback(callback); - } - - ~LogCallbackGuard() { - ov::util::reset_log_callback(); - } - - LogCallbackGuard(const LogCallbackGuard&) = delete; - LogCallbackGuard& operator=(const LogCallbackGuard&) = delete; -}; -} // namespace - namespace ov { namespace test { namespace behavior { @@ -974,7 +959,7 @@ TEST_P(RunSeqTests, CheckMultipleRunsSeq0) { for (uint32_t i = 0; i < inferences; i++) { auto* output_tensor_data = reinterpret_cast(output_tensor[i].data()); for (size_t j = 0; j < shape_size; ++j) { - EXPECT_NEAR(output_tensor_data[j], expected_result, 1e-5) + ASSERT_NEAR(output_tensor_data[j], expected_result, 1e-5) << "Run=" << z << "Output=" << i << " Expected=" << expected_result << ", actual=" << output_tensor_data[j] << " for index " << j; } @@ -1034,7 +1019,7 @@ TEST_P(RunSeqTests, CheckMultipleRunsSeq1) { for (int i = inferences - 1; i >= 0; i--) { auto* output_tensor_data = reinterpret_cast(output_tensor[i].data()); for (size_t j = 0; j < shape_size; ++j) { - EXPECT_NEAR(output_tensor_data[j], expected_result, 1e-5) + ASSERT_NEAR(output_tensor_data[j], expected_result, 1e-5) << "Run=" << z << "Output=" << i << " Expected=" << expected_result << ", actual=" << output_tensor_data[j] << " for index " << j; } @@ -1191,7 +1176,7 @@ TEST_P(RunSeqTests, CheckMultipleRunsSeq4) { for (int i = inferences - 1; i >= 0; i--) { auto* output_tensor_data = reinterpret_cast(output_tensor[i].data()); for (size_t j = 0; j < shape_size; ++j) { - EXPECT_NEAR(output_tensor_data[j], expected_result, 1e-5) + ASSERT_NEAR(output_tensor_data[j], expected_result, 1e-5) << "Run=" << z << "Output=" << i << " Expected=" << expected_result << ", actual=" << output_tensor_data[j] << " for index " << j; } @@ -1217,7 +1202,7 @@ TEST_P(RunSeqTests, CheckMultipleRunsSeq4) { for (uint32_t i = 0; i < inferences; i++) { auto* output_tensor_data = reinterpret_cast(output_tensor[i].data()); for (size_t j = 0; j < shape_size; ++j) { - EXPECT_NEAR(output_tensor_data[j], expected_result, 1e-5) + ASSERT_NEAR(output_tensor_data[j], expected_result, 1e-5) << "Run=" << z << "Output=" << i << " Expected=" << expected_result << ", actual=" << output_tensor_data[j] << " for index " << j; } @@ -1282,7 +1267,7 @@ TEST_P(RunSeqTests, CheckTurboWithMultipleRunsSeq) { for (int i = inferences - 1; i >= 0; i--) { auto* output_tensor_data = reinterpret_cast(output_tensor[i].data()); for (size_t j = 0; j < shape_size; ++j) { - EXPECT_NEAR(output_tensor_data[j], expected_result, 1e-5) + ASSERT_NEAR(output_tensor_data[j], expected_result, 1e-5) << "Run=" << z << "Output=" << i << " Expected=" << expected_result << ", actual=" << output_tensor_data[j] << " for index " << j; } @@ -1343,7 +1328,7 @@ TEST_P(BatchingRunSeqTests, CheckMultipleBatchingRunsSeq) { for (uint32_t i = 0; i < inferences; i++) { auto* output_tensor_data = reinterpret_cast(output_tensor[i].data()); for (size_t j = 0; j < shape_size; ++j) { - EXPECT_NEAR(output_tensor_data[j], expected_result, 1e-5) + ASSERT_NEAR(output_tensor_data[j], expected_result, 1e-5) << "Run=" << z << "Output=" << i << " Expected=" << expected_result << ", actual=" << output_tensor_data[j] << " for index " << j; } @@ -1386,7 +1371,7 @@ TEST_P(DynamicBatchingTests, DynamicCheckMultipleBatchingRun0) { float expected_result = static_cast(batch_size) + 1.f; auto* output_tensor_data = reinterpret_cast(output_tensor.data()); for (size_t j = 0; j < shape_size; ++j) { - EXPECT_NEAR(output_tensor_data[j], expected_result, 1e-5) + ASSERT_NEAR(output_tensor_data[j], expected_result, 1e-5) << "Run=" << batch_size << " Expected=" << expected_result << ", actual=" << output_tensor_data[j] << " for index " << j; } @@ -1412,7 +1397,7 @@ TEST_P(DynamicBatchingTests, DynamicCheckMultipleBatchingRun0) { EXPECT_EQ(output_test_tensor.get_size(), input_host_tensor.get_size()); for (size_t j = 0; j < output_test_tensor.get_size(); ++j) { - EXPECT_NEAR(output_tensor_data[j], expected_result, 1e-5) + ASSERT_NEAR(output_tensor_data[j], expected_result, 1e-5) << "Run=" << z << " Expected=" << expected_result << ", actual=" << output_tensor_data[j] << " for index " << j; } @@ -1451,7 +1436,7 @@ TEST_P(DynamicBatchingTests, DynamicCheckMultipleBatchingRun1) { float expected_result = static_cast(batch_size) + 1.f; auto* output_tensor_data = reinterpret_cast(output_tensor.data()); for (size_t j = 0; j < shape_size; ++j) { - EXPECT_NEAR(output_tensor_data[j], expected_result, 1e-5) + ASSERT_NEAR(output_tensor_data[j], expected_result, 1e-5) << "Run=" << batch_size << " Expected=" << expected_result << ", actual=" << output_tensor_data[j] << " for index " << j; } @@ -1477,7 +1462,7 @@ TEST_P(DynamicBatchingTests, DynamicCheckMultipleBatchingRun1) { EXPECT_EQ(output_test_tensor.get_size(), input_host_tensor.get_size()); for (size_t j = 0; j < output_test_tensor.get_size(); ++j) { - EXPECT_NEAR(output_tensor_data[j], expected_result, 1e-5) + ASSERT_NEAR(output_tensor_data[j], expected_result, 1e-5) << "Run=" << z << " Expected=" << expected_result << ", actual=" << output_tensor_data[j] << " for index " << j; } @@ -1514,7 +1499,7 @@ TEST_P(DynamicBatchingTests, DynamicCheckMultipleBatchingRun2) { float expected_result = static_cast(batch_size) + 1.f; auto* output_tensor_data = reinterpret_cast(output_tensor.data()); for (size_t j = 0; j < shape_size; ++j) { - EXPECT_NEAR(output_tensor_data[j], expected_result, 1e-5) + ASSERT_NEAR(output_tensor_data[j], expected_result, 1e-5) << "Run=" << batch_size << " Expected=" << expected_result << ", actual=" << output_tensor_data[j] << " for index " << j; } @@ -1569,7 +1554,7 @@ TEST_P(DynamicBatchingTests, DynamicCheckMultipleBatchingRunsSeq) { for (uint32_t i = 0; i < inferences; i++) { auto* output_tensor_data = reinterpret_cast(output_tensor[i].data()); for (size_t j = 0; j < shape_size; ++j) { - EXPECT_NEAR(output_tensor_data[j], expected_result, 1e-5) + ASSERT_NEAR(output_tensor_data[j], expected_result, 1e-5) << "Run=" << z << "Output=" << i << " Expected=" << expected_result << ", actual=" << output_tensor_data[j] << " for index " << j; } @@ -1636,7 +1621,7 @@ TEST_P(SetShapeInferRunTests, checkResultsAfterIOBlobReallocation) { auto* actual = first_output_tensor.data(); for (size_t i = 0; i < shape_size; ++i) { - EXPECT_NEAR(actual[i], 6.f, 1e-5) << "Expected=6, actual=" << actual[i] << " for index " << i; + ASSERT_NEAR(actual[i], 6.f, 1e-5) << "Expected=6, actual=" << actual[i] << " for index " << i; } // imitates blob reallocation @@ -1658,7 +1643,7 @@ TEST_P(SetShapeInferRunTests, checkResultsAfterIOBlobReallocation) { actual = second_output_tensor.data(); for (size_t i = 0; i < shape_size; ++i) { - EXPECT_NEAR(actual[i], 10.f, 1e-5) << "Expected=10, actual=" << actual[i] << " for index " << i; + ASSERT_NEAR(actual[i], 10.f, 1e-5) << "Expected=10, actual=" << actual[i] << " for index " << i; } } @@ -1695,7 +1680,7 @@ TEST_P(SetShapeInferRunTests, checkResultsAfterStateTensorsReallocation) { auto output_tensor = inference_request.get_tensor("sigmod_state"); auto output_data = output_tensor.data(); for (size_t i = 0; i < output_tensor.get_size(); i++) { - EXPECT_NEAR(0.5f, output_data[i], 1e-5); + ASSERT_NEAR(0.5f, output_data[i], 1e-5); } auto states = inference_request.query_state(); @@ -1707,7 +1692,7 @@ TEST_P(SetShapeInferRunTests, checkResultsAfterStateTensorsReallocation) { ASSERT_TRUE(last_state_size != 0) << "State size should not be 0"; for (size_t i = 0; i < last_state_size; ++i) { - EXPECT_NEAR(0.0, last_state_data[i], 1e-5); + ASSERT_NEAR(0.0, last_state_data[i], 1e-5); } } @@ -1754,7 +1739,7 @@ TEST_P(SetShapeInferRunTests, checkResultsAfterStateTensorsReallocation) { ASSERT_TRUE(last_state_size != 0) << "State size should not be 0"; for (size_t i = 0; i < last_state_size; ++i) { - EXPECT_NEAR(input_data[i], last_state_data[i], 1e-5); + ASSERT_NEAR(input_data[i], last_state_data[i], 1e-5); } } } @@ -1863,7 +1848,7 @@ TEST_P(CpuVaTensorsTests, SetMultiplePageAllignedTensors) { auto* output_tensor_data = reinterpret_cast(output_tensor[i].data()); EXPECT_EQ(output_tensor_data, output_data[i]); for (size_t j = 0; j < shape_size; ++j) { - EXPECT_NEAR(output_tensor_data[j], expected_result, 1e-5) + ASSERT_NEAR(output_tensor_data[j], expected_result, 1e-5) << "Output=" << i << " Expected=" << expected_result << ", actual=" << output_tensor_data[j] << " for index " << j; } @@ -1935,7 +1920,7 @@ TEST_P(CpuVaTensorsTests, SetMultipleAllignedAndNotAllignedTensors) { for (int i = 0; i < inferences; i++) { auto* output_tensor_data = reinterpret_cast(output_tensor[i].data()); for (size_t j = 0; j < shape_size; ++j) { - EXPECT_NEAR(output_tensor_data[j], expected_result, 1e-5) + ASSERT_NEAR(output_tensor_data[j], expected_result, 1e-5) << "Output=" << i << " Expected=" << expected_result << ", actual=" << output_tensor_data[j] << " for index " << j; } @@ -2018,7 +2003,7 @@ TEST_P(CpuVaTensorsTests, SetMultipleRemoteAllignedAndNotAllignedTensors) { for (int i = 0; i < inferences; i++) { auto* output_tensor_data = reinterpret_cast(output_tensor[i].data()); for (size_t j = 0; j < shape_size; ++j) { - EXPECT_NEAR(output_tensor_data[j], expected_result, 1e-5) + ASSERT_NEAR(output_tensor_data[j], expected_result, 1e-5) << "Output=" << i << " Expected=" << expected_result << ", actual=" << output_tensor_data[j] << " for index " << j; } @@ -2084,13 +2069,13 @@ TEST_P(CpuVaTensorsTests, SetAndDestroyDifferentAlignedTensors) { auto* output_tensor_data = reinterpret_cast(output_tensor0.data()); for (size_t j = 0; j < shape_size; ++j) { - EXPECT_NEAR(output_tensor_data[j], expected_result, 1e-5) + ASSERT_NEAR(output_tensor_data[j], expected_result, 1e-5) << " Expected=" << expected_result << ", actual=" << output_tensor_data[j] << " for index " << j; } output_tensor_data = reinterpret_cast(output_tensor1.data()); for (size_t j = 0; j < shape_size; ++j) { - EXPECT_NEAR(output_tensor_data[j], expected_result, 1e-5) + ASSERT_NEAR(output_tensor_data[j], expected_result, 1e-5) << " Expected=" << expected_result << ", actual=" << output_tensor_data[j] << " for index " << j; } @@ -2151,7 +2136,7 @@ TEST_P(CpuVaTensorsTests, checkResultsAfterStateTensorsUseImportCpuVa0) { auto output_tensor = inference_request.get_tensor("sigmod_state"); auto output_data = output_tensor.data(); for (size_t i = 0; i < output_tensor.get_size(); i++) { - EXPECT_NEAR(0.5f, output_data[i], 1e-5); + ASSERT_NEAR(0.5f, output_data[i], 1e-5); } states = inference_request.query_state(); @@ -2181,11 +2166,11 @@ TEST_P(CpuVaTensorsTests, checkResultsAfterStateTensorsUseImportCpuVa0) { auto get_state_data = static_cast(l0_host_tensor.data()); for (size_t i = 0; i < get_tensor_state.get_size(); ++i) { - EXPECT_NEAR(0.0, get_state_data[i], 1e-5); - EXPECT_NEAR(0.0, state_data[0][i], 1e-5); + ASSERT_NEAR(0.0, get_state_data[i], 1e-5); + ASSERT_NEAR(0.0, state_data[0][i], 1e-5); - EXPECT_NEAR(input_data[i], state_data[1][i], 1e-5); - EXPECT_NEAR(input_data[i], state_data[2][i], 1e-5); + ASSERT_NEAR(input_data[i], state_data[1][i], 1e-5); + ASSERT_NEAR(input_data[i], state_data[2][i], 1e-5); } inference_request = {}; @@ -2249,7 +2234,7 @@ TEST_P(CpuVaTensorsTests, checkResultsAfterStateTensorsUseImportCpuVa1) { auto output_tensor = inference_request.get_tensor("sigmod_state"); auto output_data = output_tensor.data(); for (size_t i = 0; i < output_tensor.get_size(); i++) { - EXPECT_NEAR(0.5f, output_data[i], 1e-5); + ASSERT_NEAR(0.5f, output_data[i], 1e-5); } states = inference_request.query_state(); @@ -2279,11 +2264,11 @@ TEST_P(CpuVaTensorsTests, checkResultsAfterStateTensorsUseImportCpuVa1) { auto get_state_data = static_cast(l0_host_tensor.data()); for (size_t i = 0; i < get_tensor_state.get_size(); ++i) { - EXPECT_NEAR(0.0, get_state_data[i], 1e-5); - EXPECT_NEAR(0.0, state_data[0][i], 1e-5); + ASSERT_NEAR(0.0, get_state_data[i], 1e-5); + ASSERT_NEAR(0.0, state_data[0][i], 1e-5); - EXPECT_NEAR(input_data[i], state_data[1][i], 1e-5); - EXPECT_NEAR(input_data[i], state_data[2][i], 1e-5); + ASSERT_NEAR(input_data[i], state_data[1][i], 1e-5); + ASSERT_NEAR(input_data[i], state_data[2][i], 1e-5); } inference_request = {}; @@ -2323,7 +2308,7 @@ TEST_P(CpuVaTensorsTests, checkResultsAfterRawMemoryIsDestroyedAndReallocatedAft internal_core.set_property(target_device, ov::log::level(ov::log::Level::DEBUG)); { // don't flood console with messages from model compilation - LogCallbackGuard log_callback_guard(log_cb); + utils::LogCallbackGuard log_callback_guard(log_cb); ov::CompiledModel compiled_model = internal_core.compile_model(model, target_device, configuration); inference_request = compiled_model.create_infer_request(); } @@ -2337,7 +2322,7 @@ TEST_P(CpuVaTensorsTests, checkResultsAfterRawMemoryIsDestroyedAndReallocatedAft } { - LogCallbackGuard log_callback_guard(log_cb); + utils::LogCallbackGuard log_callback_guard(log_cb); inference_request.set_input_tensor(ov::Tensor{ov::element::f32, shape, input_data}); inference_request.set_output_tensor(ov::Tensor{ov::element::f32, shape, output_data}); inference_request.infer(); @@ -2348,7 +2333,7 @@ TEST_P(CpuVaTensorsTests, checkResultsAfterRawMemoryIsDestroyedAndReallocatedAft logs.clear(); for (size_t j = 0; j < shape_size; ++j) { - EXPECT_NEAR(output_data[j], input_data[j] + 1.0f, 1e-5) + ASSERT_NEAR(output_data[j], input_data[j] + 1.0f, 1e-5) << "Run " << i << ": Expected=" << input_data[j] + 1.0f << ", actual=" << output_data[j] << " for index " << j; } @@ -2387,7 +2372,7 @@ TEST_P(CpuVaTensorsTests, checkResultsAfterRunningWithSameRawMemoryMultipleTimes internal_core.set_property(target_device, ov::log::level(ov::log::Level::DEBUG)); { // don't flood console with messages from model compilation - LogCallbackGuard log_callback_guard(log_cb); + utils::LogCallbackGuard log_callback_guard(log_cb); ov::CompiledModel compiled_model = internal_core.compile_model(model, target_device, configuration); inference_request = compiled_model.create_infer_request(); inference_request.set_input_tensor(ov::Tensor{ov::element::f32, shape, input_data}); @@ -2401,7 +2386,7 @@ TEST_P(CpuVaTensorsTests, checkResultsAfterRunningWithSameRawMemoryMultipleTimes } { - LogCallbackGuard log_callback_guard(log_cb); + utils::LogCallbackGuard log_callback_guard(log_cb); inference_request.infer(); } @@ -2415,7 +2400,7 @@ TEST_P(CpuVaTensorsTests, checkResultsAfterRunningWithSameRawMemoryMultipleTimes logs.clear(); for (size_t j = 0; j < shape_size; ++j) { - EXPECT_NEAR(output_data[j], input_data[j] + 1.0f, 1e-5) + ASSERT_NEAR(output_data[j], input_data[j] + 1.0f, 1e-5) << "Run " << i << ": Expected=" << input_data[j] + 1.0f << ", actual=" << output_data[j] << " for index " << j; } @@ -2453,7 +2438,7 @@ TEST_P(CpuVaTensorsTests, checkResultsAfterRunningWithSameZeroTensorMultipleTime internal_core.set_property(target_device, ov::log::level(ov::log::Level::DEBUG)); { // don't flood console with messages from model compilation - LogCallbackGuard log_callback_guard(log_cb); + utils::LogCallbackGuard log_callback_guard(log_cb); ov::CompiledModel compiled_model = internal_core.compile_model(model, target_device, configuration); inference_request = compiled_model.create_infer_request(); input_tensor = inference_request.get_input_tensor(); @@ -2469,7 +2454,7 @@ TEST_P(CpuVaTensorsTests, checkResultsAfterRunningWithSameZeroTensorMultipleTime } { - LogCallbackGuard log_callback_guard(log_cb); + utils::LogCallbackGuard log_callback_guard(log_cb); inference_request.infer(); } @@ -2479,7 +2464,7 @@ TEST_P(CpuVaTensorsTests, checkResultsAfterRunningWithSameZeroTensorMultipleTime logs.clear(); for (size_t j = 0; j < shape_size; ++j) { - EXPECT_NEAR(output_data[j], input_data[j] + 1.0f, 1e-5) + ASSERT_NEAR(output_data[j], input_data[j] + 1.0f, 1e-5) << "Run " << i << ": Expected=" << input_data[j] + 1.0f << ", actual=" << output_data[j] << " for index " << j; } @@ -2518,7 +2503,7 @@ TEST_P(CpuVaTensorsTests, checkResultsAfterRunningWithSameZeroHostTensorMultiple internal_core.set_property(target_device, ov::log::level(ov::log::Level::DEBUG)); { // don't flood console with messages from model compilation - LogCallbackGuard log_callback_guard(log_cb); + utils::LogCallbackGuard log_callback_guard(log_cb); ov::CompiledModel compiled_model = internal_core.compile_model(model, target_device, configuration); inference_request = compiled_model.create_infer_request(); inference_request.set_input_tensor(input_tensor); @@ -2532,7 +2517,7 @@ TEST_P(CpuVaTensorsTests, checkResultsAfterRunningWithSameZeroHostTensorMultiple } { - LogCallbackGuard log_callback_guard(log_cb); + utils::LogCallbackGuard log_callback_guard(log_cb); inference_request.infer(); } @@ -2542,7 +2527,7 @@ TEST_P(CpuVaTensorsTests, checkResultsAfterRunningWithSameZeroHostTensorMultiple logs.clear(); for (size_t j = 0; j < shape_size; ++j) { - EXPECT_NEAR(output_data[j], input_data[j] + 1.0f, 1e-5) + ASSERT_NEAR(output_data[j], input_data[j] + 1.0f, 1e-5) << "Run " << i << ": Expected=" << input_data[j] + 1.0f << ", actual=" << output_data[j] << " for index " << j; } @@ -2585,7 +2570,7 @@ TEST_P(CpuVaTensorsTests, checkResultsAfterRunningWithSameRawMemoryMultipleTimes OV_ASSERT_NO_THROW(inference_request.infer()); for (size_t j = 0; j < shape_size; ++j) { - EXPECT_NEAR(output_data[j], input_data[j] + 1.0f, 1e-5) + ASSERT_NEAR(output_data[j], input_data[j] + 1.0f, 1e-5) << "Run " << i << ": Expected=" << input_data[j] + 1.0f << ", actual=" << output_data[j] << " for index " << j; } @@ -2595,6 +2580,287 @@ TEST_P(CpuVaTensorsTests, checkResultsAfterRunningWithSameRawMemoryMultipleTimes ::operator delete(output_data, std::align_val_t(4096)); } +using DynamicBoundsTests = InferRequestRunTests; + +TEST_P(DynamicBoundsTests, ChangeShapeAfterTensorIsSet) { + SKIP_IF_CURRENT_TEST_IS_DISABLED(); + + auto model_shape = PartialShape{1, ov::Dimension(1, 10), 2, 2}; + auto model = createModel(element::f32, model_shape, "N..."); + + auto compiled_model = core->compile_model(model, target_device, configuration); + // Create InferRequest + ov::InferRequest req; + req = compiled_model.create_infer_request(); + + auto shape = Shape{1, 4, 2, 2}; + auto shape_size = ov::shape_size(shape); + auto input_tensor = ov::Tensor{ov::element::f32, shape}; + auto output_tensor = ov::Tensor{ov::element::f32, shape}; + + ASSERT_EQ(shape_size * sizeof(float), input_tensor.get_byte_size()); + ASSERT_EQ(shape_size * sizeof(float), output_tensor.get_byte_size()); + ASSERT_EQ(shape, input_tensor.get_shape()); + ASSERT_EQ(shape, output_tensor.get_shape()); + + req.set_input_tensor(input_tensor); + req.set_output_tensor(output_tensor); + + auto input_data = input_tensor.data(); + auto output_data = output_tensor.data(); + for (size_t i = 0; i < shape_size; ++i) { + input_data[i] = 21.f; + } + + req.infer(); // Adds '1' to each element + + auto expected = 22.f; + for (size_t i = 0; i < shape_size; ++i) { + ASSERT_EQ(output_data[i], expected) + << ": Expected=" << expected << ", actual=" << output_data[i] << " for index " << i; + } + + auto input_data_after_run = input_tensor.data(); + auto output_data_after_run = output_tensor.data(); + + ASSERT_EQ(input_data, input_data_after_run); + ASSERT_EQ(output_data, output_data_after_run); + + auto new_shape = Shape{1, 6, 2, 2}; + auto new_shape_size = ov::shape_size(new_shape); + input_tensor.set_shape(new_shape); + output_tensor.set_shape(new_shape); + + ASSERT_EQ(new_shape_size * sizeof(float), input_tensor.get_byte_size()); + ASSERT_EQ(new_shape_size * sizeof(float), output_tensor.get_byte_size()); + ASSERT_EQ(new_shape, input_tensor.get_shape()); + ASSERT_EQ(new_shape, output_tensor.get_shape()); + + input_data = input_tensor.data(); + output_data = output_tensor.data(); + + for (size_t i = 0; i < new_shape_size; ++i) { + input_data[i] = 25.f; + } + + req.infer(); // Adds '1' to each element + + expected = 26.f; + for (size_t i = 0; i < new_shape_size; ++i) { + ASSERT_EQ(output_data[i], expected) + << ": Expected=" << expected << ", actual=" << output_data[i] << " for index " << i; + } + + ASSERT_EQ(new_shape_size * sizeof(float), input_tensor.get_byte_size()); + ASSERT_EQ(new_shape_size * sizeof(float), output_tensor.get_byte_size()); + ASSERT_EQ(new_shape, input_tensor.get_shape()); + ASSERT_EQ(new_shape, output_tensor.get_shape()); + + input_data_after_run = input_tensor.data(); + output_data_after_run = output_tensor.data(); + + ASSERT_EQ(input_data, input_data_after_run); + ASSERT_EQ(output_data, output_data_after_run); +} + +TEST_P(DynamicBoundsTests, ChangeShapeAfterTensorIsSetUsingAlignedExternalMemory) { + SKIP_IF_CURRENT_TEST_IS_DISABLED(); + + auto model_shape = PartialShape{1, ov::Dimension(1, 10), 4, 64}; + auto model = createModel(element::f32, model_shape, "N..."); + + auto compiled_model = core->compile_model(model, target_device, configuration); + // Create InferRequest + ov::InferRequest req; + req = compiled_model.create_infer_request(); + + ov::Allocator allocator{ov::test::utils::DefaultAllocatorAligned{}}; + + auto shape = Shape{1, 8, 4, 64}; + auto shape_size = ov::shape_size(shape); + auto input_tensor = ov::Tensor{ov::element::f32, shape, allocator}; + auto output_tensor = ov::Tensor{ov::element::f32, shape, allocator}; + + ASSERT_EQ(shape_size * sizeof(float), input_tensor.get_byte_size()); + ASSERT_EQ(shape_size * sizeof(float), output_tensor.get_byte_size()); + ASSERT_EQ(shape, input_tensor.get_shape()); + ASSERT_EQ(shape, output_tensor.get_shape()); + + req.set_input_tensor(input_tensor); + req.set_output_tensor(output_tensor); + + auto* input_data = input_tensor.data(); + for (size_t i = 0; i < shape_size; ++i) { + input_data[i] = 21.f; + } + + req.infer(); // Adds '1' to each element + + auto expected = 22.f; + auto* output_data = output_tensor.data(); + for (size_t i = 0; i < shape_size; ++i) { + ASSERT_EQ(output_data[i], expected) + << ": Expected=" << expected << ", actual=" << output_data[i] << " for index " << i; + } + + auto new_shape = Shape{1, 6, 4, 64}; + auto new_shape_size = ov::shape_size(new_shape); + input_tensor.set_shape(new_shape); + output_tensor.set_shape(new_shape); + + ASSERT_EQ(new_shape_size * sizeof(float), input_tensor.get_byte_size()); + ASSERT_EQ(new_shape_size * sizeof(float), output_tensor.get_byte_size()); + ASSERT_EQ(new_shape, input_tensor.get_shape()); + ASSERT_EQ(new_shape, output_tensor.get_shape()); + + input_data = input_tensor.data(); + output_data = output_tensor.data(); + + for (size_t i = 0; i < new_shape_size; ++i) { + input_data[i] = 25.f; + } + + req.infer(); // Adds '1' to each element + + expected = 26.f; + for (size_t i = 0; i < new_shape_size; ++i) { + ASSERT_EQ(output_data[i], expected) + << ": Expected=" << expected << ", actual=" << output_data[i] << " for index " << i; + } + + ASSERT_EQ(new_shape_size * sizeof(float), input_tensor.get_byte_size()); + ASSERT_EQ(new_shape_size * sizeof(float), output_tensor.get_byte_size()); + ASSERT_EQ(new_shape, input_tensor.get_shape()); + ASSERT_EQ(new_shape, output_tensor.get_shape()); +} + +TEST_P(DynamicBoundsTests, RunningTwiceWithRemoteTensor) { + SKIP_IF_CURRENT_TEST_IS_DISABLED(); + + auto model_shape = PartialShape{1, ov::Dimension(1, 10), 4, 64}; + auto model = createModel(element::f32, model_shape, "N..."); + + auto compiled_model = core->compile_model(model, target_device, configuration); + // Create InferRequest + ov::InferRequest req; + req = compiled_model.create_infer_request(); + + auto context = core->get_default_context(target_device); + + auto shape = Shape{1, 8, 4, 64}; + auto shape_size = ov::shape_size(shape); + auto input_tensor = context.create_host_tensor(ov::element::f32, shape); + auto output_tensor = context.create_host_tensor(ov::element::f32, shape); + + ASSERT_EQ(shape_size * sizeof(float), input_tensor.get_byte_size()); + ASSERT_EQ(shape_size * sizeof(float), output_tensor.get_byte_size()); + ASSERT_EQ(shape, input_tensor.get_shape()); + ASSERT_EQ(shape, output_tensor.get_shape()); + + req.set_input_tensor(input_tensor); + req.set_output_tensor(output_tensor); + + auto* input_data = input_tensor.data(); + for (size_t i = 0; i < shape_size; ++i) { + input_data[i] = 21.f; + } + + req.infer(); // Adds '1' to each element + + auto expected = 22.f; + auto* output_data = output_tensor.data(); + for (size_t i = 0; i < shape_size; ++i) { + ASSERT_EQ(output_data[i], expected) + << ": Expected=" << expected << ", actual=" << output_data[i] << " for index " << i; + } + + for (size_t i = 0; i < shape_size; ++i) { + input_data[i] = 25.f; + } + + req.infer(); // Adds '1' to each element + + expected = 26.f; + for (size_t i = 0; i < shape_size; ++i) { + ASSERT_EQ(output_data[i], expected) + << ": Expected=" << expected << ", actual=" << output_data[i] << " for index " << i; + } + + ASSERT_EQ(shape_size * sizeof(float), input_tensor.get_byte_size()); + ASSERT_EQ(shape_size * sizeof(float), output_tensor.get_byte_size()); + ASSERT_EQ(shape, input_tensor.get_shape()); + ASSERT_EQ(shape, output_tensor.get_shape()); +} + +TEST_P(DynamicBoundsTests, ExpectErrorFromWrongTensorShape) { + SKIP_IF_CURRENT_TEST_IS_DISABLED(); + + auto model_shape = PartialShape{1, ov::Dimension(1, 10), 4, 64}; + auto model = createModel(element::f32, model_shape, "N..."); + + auto compiled_model = core->compile_model(model, target_device, configuration); + // Create InferRequest + ov::InferRequest req; + req = compiled_model.create_infer_request(); + + auto context = core->get_default_context(target_device); + auto input_tensor = context.create_host_tensor(ov::element::f32, Shape{1, 8, 2, 64}); + OV_EXPECT_THROW(req.set_input_tensor(input_tensor), + ov::Exception, + HasSubstr("The tensor shape is not compatible with the model input/output shape")); + + auto output_tensor = context.create_host_tensor(ov::element::f32, Shape{1, 8, 12, 64}); + OV_EXPECT_THROW(req.set_output_tensor(output_tensor), + ov::Exception, + HasSubstr("The tensor shape is not compatible with the model input/output shape")); +} + +using NumStreamsTests = InferRequestRunTests; + +TEST_P(NumStreamsTests, RunWithDifferentNumStreamsCheckResults) { + SKIP_IF_CURRENT_TEST_IS_DISABLED(); + auto shape = Shape{1, 2, 64, 64}; + auto shape_size = ov::shape_size(shape); + auto model = createModel(element::f32, shape, "N..."); + + auto compiled_model = core->compile_model(model, target_device, configuration); + const uint32_t inferences = 32; + std::array inference_request; + + for (uint32_t i = 0; i < inferences; i++) { + inference_request[i] = compiled_model.create_infer_request(); + + auto input_tensor = inference_request[i].get_input_tensor(); + auto* input_data = reinterpret_cast(input_tensor.data()); + for (size_t j = 0; j < shape_size; ++j) { + input_data[j] = static_cast(i); + } + } + + for (uint32_t i = 0; i < inferences; i++) { + OV_ASSERT_NO_THROW(inference_request[i].infer()); + } + + for (uint32_t i = 0; i < inferences; i++) { + OV_ASSERT_NO_THROW(inference_request[i].start_async()); + } + + for (uint32_t i = 0; i < inferences; i++) { + OV_ASSERT_NO_THROW(inference_request[i].wait()); + } + + for (uint32_t i = 0; i < inferences; i++) { + float expected_result = static_cast(i) + 1.f; + auto output_tensor = inference_request[i].get_output_tensor(); + auto* output_tensor_data = reinterpret_cast(output_tensor.data()); + for (size_t j = 0; j < shape_size; ++j) { + ASSERT_NEAR(output_tensor_data[j], expected_result, 1e-5) + << "Inference=" << i << " Expected=" << expected_result << ", actual=" << output_tensor_data[j] + << " for index " << j; + } + } +} + } // namespace behavior } // namespace test } // namespace ov diff --git a/src/plugins/intel_npu/tests/functional/behavior/ov_plugin/caching_tests.hpp b/src/plugins/intel_npu/tests/functional/behavior/ov_plugin/caching_tests.hpp index 88bf49922415..c37574412a27 100644 --- a/src/plugins/intel_npu/tests/functional/behavior/ov_plugin/caching_tests.hpp +++ b/src/plugins/intel_npu/tests/functional/behavior/ov_plugin/caching_tests.hpp @@ -4,12 +4,11 @@ #pragma once -#include // not redundant, needed for `ze_structure_type_t` structure - #include #include "intel_npu/utils/zero/zero_init.hpp" #include "openvino/core/log_util.hpp" +#include "openvino/runtime/intel_npu/properties.hpp" namespace ov { namespace test { @@ -20,7 +19,7 @@ using OVCompileModelLoadFromFileTestBaseNPU = CompileModelLoadFromFileTestBase; TEST_P(OVCompileModelLoadFromFileTestBaseNPU, BlobWithOVHeaderAligmentCanBeImported) { core->set_property(ov::cache_dir(m_cacheFolderName)); - if (!intel_npu::ZeroInitStructsHolder::getInstance()->isExternalMemoryStandardAllocationSupported()) { + if (!::intel_npu::ZeroInitStructsHolder::getInstance()->isExternalMemoryStandardAllocationSupported()) { GTEST_SKIP() << "Standard allocation is not supported by the current configuration."; } diff --git a/src/plugins/intel_npu/tests/functional/behavior/ov_plugin/internal_properties_tests.cpp b/src/plugins/intel_npu/tests/functional/behavior/ov_plugin/internal_properties_tests.cpp index bd376c22c847..639c9c2d03fb 100644 --- a/src/plugins/intel_npu/tests/functional/behavior/ov_plugin/internal_properties_tests.cpp +++ b/src/plugins/intel_npu/tests/functional/behavior/ov_plugin/internal_properties_tests.cpp @@ -1,13 +1,35 @@ -// -// Copyright (C) Intel Corporation +// Copyright (C) 2018-2026 Intel Corporation // SPDX-License-Identifier: Apache-2.0 // #include "internal_properties_tests.hpp" +#include + #include "common/utils.hpp" +#include "intel_npu/config/options.hpp" #include "intel_npu/npu_private_properties.hpp" +namespace { + +void set_env(const std::string& name, const std::string& value) { +#ifdef _WIN32 + _putenv_s(name.c_str(), value.c_str()); +#else + ::setenv(name.c_str(), value.c_str(), 1); +#endif +} + +void unset_env(const std::string& name) { +#ifdef _WIN32 + _putenv_s(name.c_str(), ""); +#else + ::unsetenv(name.c_str()); +#endif +} + +} // namespace + namespace ov::test::behavior { std::string OVPropertiesTestsNPU::getTestCaseName(const testing::TestParamInfo& obj) { @@ -18,7 +40,7 @@ std::string OVPropertiesTestsNPU::getTestCaseName(const testing::TestParamInfo

get_available_devices(); + core->unload_plugin(target_device); + for (const auto& property_item : properties) { + if (std::getenv(property_item.second.as().c_str()) == nullptr) { + set_env(property_item.second.as(), wrong_env_var_value); + std::vector plugins; + try { + plugins = core->get_available_devices(); + } catch (const std::exception& e) { + FAIL() << "Failed to get available devices due to error: " << e.what(); + } + unset_env(property_item.second.as()); + if (!plugins.empty()) { + auto it = std::find_if(plugins.begin(), + plugins.end(), + [&target_device = std::as_const(target_device)](const std::string& plugin) { + return plugin.find(target_device) != std::string::npos; + }); + ASSERT_TRUE(it != plugins.end()) << "Plugin was not loaded with wrong environment value for " + << property_item.second.as(); + } + } + } +} + TEST_P(OVCheckSetSupportedRWMetricsPropsTestsNPU, ChangeCorrectProperties) { std::vector supported_properties; OV_ASSERT_NO_THROW(supported_properties = core->get_property(target_device, ov::supported_properties)); @@ -138,7 +187,6 @@ TEST_P(OVCheckSetSupportedRWMetricsPropsTestsNPU, ChangeCorrectProperties) { } const std::vector compat_CorrectPluginMutableProperties = { - {{ov::internal::exclusive_async_requests.name(), true}}, {{ov::intel_npu::dma_engines.name(), 1}}, {{ov::intel_npu::compilation_mode.name(), "DefaultHW"}}, {{ov::intel_npu::profiling_type.name(), ov::intel_npu::ProfilingType::INFER}}}; @@ -157,6 +205,23 @@ const std::vector IncorrectMutablePropertiesWrongValueTypes = { {{ov::intel_npu::stepping.name(), "V1"}}, }; +const std::vector PropsWithEnvVars{ + {{ov::log::level.name(), ::intel_npu::LOG_LEVEL::envVar()}}, +#ifdef NPU_PLUGIN_DEVELOPER_BUILD + {{ov::intel_npu::platform.name(), ::intel_npu::PLATFORM::envVar()}}, + {{ov::intel_npu::create_executor.name(), ::intel_npu::CREATE_EXECUTOR::envVar()}}, + {{ov::intel_npu::defer_weights_load.name(), ::intel_npu::DEFER_WEIGHTS_LOAD::envVar()}}, + {{ov::intel_npu::compiler_type.name(), ::intel_npu::COMPILER_TYPE::envVar()}}, + {{ov::intel_npu::compilation_mode.name(), ::intel_npu::COMPILATION_MODE::envVar()}}, + {{ov::intel_npu::dynamic_shape_to_static.name(), ::intel_npu::DYNAMIC_SHAPE_TO_STATIC::envVar()}}, + {{ov::intel_npu::tiles.name(), ::intel_npu::TILES::envVar()}}, + {{ov::intel_npu::dma_engines.name(), ::intel_npu::DMA_ENGINES::envVar()}}, + {{ov::intel_npu::disable_version_check.name(), ::intel_npu::DISABLE_VERSION_CHECK::envVar()}}, + {{ov::intel_npu::export_raw_blob.name(), ::intel_npu::EXPORT_RAW_BLOB::envVar()}}, + {{ov::intel_npu::import_raw_blob.name(), ::intel_npu::IMPORT_RAW_BLOB::envVar()}}, +#endif +}; + INSTANTIATE_TEST_SUITE_P(compatibility_smoke_BehaviorTests, OVPropertiesTestsNPU, ::testing::Combine(::testing::Values(ov::test::utils::DEVICE_NPU), @@ -169,6 +234,12 @@ INSTANTIATE_TEST_SUITE_P(compatibility_smoke_BehaviorTests, ::testing::ValuesIn(compat_IncorrectMutablePropertiesWrongValueTypes)), (ov::test::utils::appendPlatformTypeTestName)); +INSTANTIATE_TEST_SUITE_P(compatibility_smoke_BehaviorTests, + OVPropertiesEnvVarTestsNPU, + ::testing::Combine(::testing::Values(ov::test::utils::DEVICE_NPU), + ::testing::ValuesIn(PropsWithEnvVars)), + (ov::test::utils::appendPlatformTypeTestName)); + INSTANTIATE_TEST_SUITE_P(smoke_BehaviorTests, OVCheckSetSupportedRWMetricsPropsTestsNPU, ::testing::Combine(::testing::Values(ov::test::utils::DEVICE_NPU), ::testing::ValuesIn([] { diff --git a/src/plugins/intel_npu/tests/functional/behavior/ov_plugin/internal_properties_tests.hpp b/src/plugins/intel_npu/tests/functional/behavior/ov_plugin/internal_properties_tests.hpp index dd087baaf670..52918a8dc8c8 100644 --- a/src/plugins/intel_npu/tests/functional/behavior/ov_plugin/internal_properties_tests.hpp +++ b/src/plugins/intel_npu/tests/functional/behavior/ov_plugin/internal_properties_tests.hpp @@ -1,17 +1,16 @@ -// // Copyright (C) 2018-2026 Intel Corporation // SPDX-License-Identifier: Apache-2.0 // #pragma once -#include "shared_test_classes/base/ov_behavior_test_utils.hpp" #include "behavior/ov_plugin/properties_tests.hpp" #include "common_test_utils/file_utils.hpp" #include "common_test_utils/test_assertions.hpp" #include "common_test_utils/unicode_utils.hpp" #include "openvino/runtime/properties.hpp" #include "openvino/util/common_util.hpp" +#include "shared_test_classes/base/ov_behavior_test_utils.hpp" namespace ov::test::behavior { @@ -28,6 +27,8 @@ class OVPropertiesTestsNPU : public testing::WithParamInterface; class OVPropertiesTestsWithCompileModelPropsNPU : public testing::WithParamInterface, diff --git a/src/plugins/intel_npu/tests/functional/behavior/ov_plugin/life_time.hpp b/src/plugins/intel_npu/tests/functional/behavior/ov_plugin/life_time.hpp index 16b8cdc4b861..3e7f18316706 100644 --- a/src/plugins/intel_npu/tests/functional/behavior/ov_plugin/life_time.hpp +++ b/src/plugins/intel_npu/tests/functional/behavior/ov_plugin/life_time.hpp @@ -9,6 +9,7 @@ #include "common_test_utils/subgraph_builders/conv_pool_relu.hpp" #include "intel_npu/utils/zero/zero_init.hpp" #include "life_time.hpp" +#include "openvino/util/codec_xor.hpp" using CompilationParams = std::tuple { protected: diff --git a/src/plugins/intel_npu/tests/functional/common/utils.cpp b/src/plugins/intel_npu/tests/functional/common/utils.cpp index ee903aba7f6a..5b5288cec084 100644 --- a/src/plugins/intel_npu/tests/functional/common/utils.cpp +++ b/src/plugins/intel_npu/tests/functional/common/utils.cpp @@ -48,65 +48,6 @@ std::string removeDeviceNameOnlyID(const std::string& device_name_id) { return std::string(first_digit, last_digit + 1); } -std::vector getRWMandatoryPropertiesValues(std::vector props) { - ov::hint::PerformanceMode performanceModes[] = {ov::hint::PerformanceMode::LATENCY, - ov::hint::PerformanceMode::THROUGHPUT, - ov::hint::PerformanceMode::CUMULATIVE_THROUGHPUT}; - for (auto& performanceMode : performanceModes) { - if (std::find(props.begin(), - props.end(), - ov::AnyMap{{ov::hint::performance_mode.name(), ov::Any(performanceMode)}}) != props.end()) { - continue; - } - props.push_back({{ov::hint::performance_mode(performanceMode)}}); - } - - if (std::find(props.begin(), props.end(), ov::AnyMap{{ov::hint::num_requests.name(), ov::Any(1)}}) == props.end()) { - props.push_back({{ov::hint::num_requests(1)}}); - } - - ov::hint::ExecutionMode executionModes[] = {ov::hint::ExecutionMode::PERFORMANCE, - ov::hint::ExecutionMode::ACCURACY}; - for (auto& executionMode : executionModes) { - if (std::find(props.begin(), - props.end(), - ov::AnyMap{{ov::hint::execution_mode.name(), ov::Any(executionMode)}}) != props.end()) { - continue; - } - props.push_back({{ov::hint::execution_mode(executionMode)}}); - } - - bool enableProfilingValues[] = {true, false}; - for (auto enableProfilingValue : enableProfilingValues) { - if (std::find(props.begin(), - props.end(), - ov::AnyMap{{ov::enable_profiling.name(), ov::Any(enableProfilingValue)}}) != props.end()) { - continue; - } - props.push_back({{ov::enable_profiling(enableProfilingValue)}}); - } - - ov::log::Level logLevels[] = {ov::log::Level::NO, - ov::log::Level::ERR, - ov::log::Level::WARNING, - ov::log::Level::INFO, - ov::log::Level::DEBUG, - ov::log::Level::TRACE}; - for (auto& logLevel : logLevels) { - if (std::find(props.begin(), props.end(), ov::AnyMap{{ov::log::level.name(), ov::Any(logLevel)}}) != - props.end()) { - continue; - } - props.push_back({ov::log::level(logLevel)}); - } - - if (std::find(props.begin(), props.end(), ov::AnyMap{{ov::streams::num.name(), ov::Any(ov::streams::num(3))}}) != - props.end()) { - props.push_back({ov::streams::num(3)}); - } - return props; -} - std::shared_ptr createModelWithStates(ov::element::Type type, const ov::Shape& shape) { auto input = std::make_shared(type, shape); auto mem_i1 = std::make_shared(type, shape, 0); diff --git a/src/plugins/intel_npu/tests/functional/common/utils.hpp b/src/plugins/intel_npu/tests/functional/common/utils.hpp index 5199d41f11e7..1499cc58d3dd 100644 --- a/src/plugins/intel_npu/tests/functional/common/utils.hpp +++ b/src/plugins/intel_npu/tests/functional/common/utils.hpp @@ -4,14 +4,16 @@ #pragma once -#include #include -#include -#include -#include +#include "common_test_utils/subgraph_builders/conv_pool_relu.hpp" #include "common_test_utils/unicode_utils.hpp" +#include "intel_npu/utils/logger/logger.hpp" #include "npu_test_env_cfg.hpp" +#include "openvino/core/log.hpp" +#include "openvino/runtime/core.hpp" +#include "openvino/runtime/intel_npu/properties.hpp" +#include "shared_test_classes/base/ov_behavior_test_utils.hpp" std::string getBackendName(const ov::Core& core); @@ -21,8 +23,6 @@ std::string modelPriorityToString(const ov::hint::Priority priority); std::string removeDeviceNameOnlyID(const std::string& device_name_id); -std::vector getRWMandatoryPropertiesValues(std::vector props); - std::shared_ptr createModelWithStates(ov::element::Type type, const ov::Shape& shape); template static constexpr bool hasGetTestCaseName = false; + template + static constexpr bool has_get_test_case_name = false; + template static std::string getTestCaseName(const testing::TestParamInfo& obj) { if constexpr (hasGetTestCaseName) { return T::getTestCaseName(obj); + } else if constexpr (has_get_test_case_name) { + return T::get_test_case_name(obj); } else { std::ostringstream result; ::testing::PrintToStringParamName printToStringParamName; @@ -79,11 +84,14 @@ struct GenericTestCaseNameClass { }; template -constexpr bool - GenericTestCaseNameClass::hasGetTestCaseName().getTestCaseName( - std::declval>()))>> = - true; +constexpr bool GenericTestCaseNameClass::hasGetTestCaseName< + T, + std::void_t>()))>> = true; + +template +constexpr bool GenericTestCaseNameClass::has_get_test_case_name< + T, + std::void_t>()))>> = true; namespace ov::test::behavior { inline std::shared_ptr getDefaultNGraphFunctionForTheDeviceNPU( @@ -134,7 +142,7 @@ class DefaultAllocatorNotAligned final { void deallocate(void* handle, const size_t bytes, size_t alignment = 4096) noexcept { ::operator delete(static_cast(handle) - _offset, std::align_val_t(alignment)); } - bool is_equal(const DefaultAllocatorNotAligned& other) const { + bool is_equal(const DefaultAllocatorNotAligned&) const { return false; } @@ -142,6 +150,19 @@ class DefaultAllocatorNotAligned final { size_t _offset = 16; }; +class DefaultAllocatorAligned final { +public: + void* allocate(const size_t bytes, const size_t) { + return ::operator new(bytes, std::align_val_t(4096)); + } + void deallocate(void* handle, const size_t, size_t) noexcept { + ::operator delete(static_cast(handle), std::align_val_t(4096)); + } + bool is_equal(const DefaultAllocatorAligned&) const { + return false; + } +}; + std::tuple& infer_request, } }; +class LogCallbackGuard { +public: + explicit LogCallbackGuard(const std::function& callback) { + ov::util::set_log_callback(callback); + } + + ~LogCallbackGuard() { + ov::util::reset_log_callback(); + } + + LogCallbackGuard(const LogCallbackGuard&) = delete; + LogCallbackGuard& operator=(const LogCallbackGuard&) = delete; +}; + +class LoggerLevelGuard { +public: + explicit LoggerLevelGuard(ov::log::Level level) : _previousLevel(::intel_npu::Logger::global().level()) { + ::intel_npu::Logger::global().setLevel(level); + } + + ~LoggerLevelGuard() { + ::intel_npu::Logger::global().setLevel(_previousLevel); + } + + LoggerLevelGuard(const LoggerLevelGuard&) = delete; + LoggerLevelGuard& operator=(const LoggerLevelGuard&) = delete; + +private: + ov::log::Level _previousLevel; +}; + } // namespace utils } // namespace test diff --git a/src/plugins/intel_npu/tests/functional/internal/compiler_adapter/zero_init_mock.cpp b/src/plugins/intel_npu/tests/functional/common/zero_init_mock.cpp similarity index 94% rename from src/plugins/intel_npu/tests/functional/internal/compiler_adapter/zero_init_mock.cpp rename to src/plugins/intel_npu/tests/functional/common/zero_init_mock.cpp index 27540b676930..d8ccc6e1e33d 100644 --- a/src/plugins/intel_npu/tests/functional/internal/compiler_adapter/zero_init_mock.cpp +++ b/src/plugins/intel_npu/tests/functional/common/zero_init_mock.cpp @@ -322,7 +322,9 @@ ZeroInitStructsMock::ZeroInitStructsMock(uint32_t zeDriverNpuExtVersion, // Create context - share between the compiler and the backend ze_context_desc_t context_desc = {ZE_STRUCTURE_TYPE_CONTEXT_DESC, 0, 0}; - THROW_ON_FAIL_FOR_LEVELZERO("zeContextCreate", zeContextCreate(_driver_handle, &context_desc, &_context)); + ze_context_handle_t ctx = nullptr; + THROW_ON_FAIL_FOR_LEVELZERO("zeContextCreate", zeContextCreate(_driver_handle, &context_desc, &ctx)); + _context.store(ctx); _log.debug("ZeroInitStructsHolder initialize complete"); // Discover if standard allocation is supported @@ -385,20 +387,36 @@ void ZeroInitStructsMock::getExtensionFunctionAddress(const std::string& name, } } -ZeroInitStructsMock::~ZeroInitStructsMock() { - if (_context) { - _log.debug("ZeroInitStructsHolder - performing zeContextDestroy"); - auto result = zeContextDestroy(_context); - _context = nullptr; - if (result != ZE_RESULT_SUCCESS) { - if (result == ZE_RESULT_ERROR_UNINITIALIZED) { - _log.warning( - "zeContextDestroy failed to destroy the context; Level zero context was already destroyed"); - } else { - _log.error("zeContextDestroy failed %#X", uint64_t(result)); - } - } +void ZeroInitStructsMock::destroyContextForInstance(std::shared_ptr& instance) { + if (!instance) { + return; + } + + std::lock_guard lock(instance->_mutex); + instance->destroyContextLocked(); +} + +void ZeroInitStructsMock::destroyContextLocked() { + if (!_context.load()) { + return; + } + + _log.debug("ZeroInitStructsHolder - performing zeContextDestroy"); + auto result = zeContextDestroy(_context.load()); + if (result == ZE_RESULT_SUCCESS) { + _context.store(nullptr); + _log.debug("ZeroInitStructsHolder - zeContextDestroy succeeded"); + } else if (result == ZE_RESULT_ERROR_UNINITIALIZED) { + _context.store(nullptr); + _log.warning("zeContextDestroy failed to destroy the context; Level zero context was already destroyed"); + } else { + _log.error("zeContextDestroy failed %#X", uint64_t(result)); } } +ZeroInitStructsMock::~ZeroInitStructsMock() { + std::lock_guard lock(_mutex); + destroyContextLocked(); +} + } // namespace intel_npu diff --git a/src/plugins/intel_npu/tests/functional/internal/compiler_adapter/zero_init_mock.hpp b/src/plugins/intel_npu/tests/functional/common/zero_init_mock.hpp similarity index 94% rename from src/plugins/intel_npu/tests/functional/internal/compiler_adapter/zero_init_mock.hpp rename to src/plugins/intel_npu/tests/functional/common/zero_init_mock.hpp index e95d95c4dfa0..e3fd98d37a59 100644 --- a/src/plugins/intel_npu/tests/functional/internal/compiler_adapter/zero_init_mock.hpp +++ b/src/plugins/intel_npu/tests/functional/common/zero_init_mock.hpp @@ -19,7 +19,7 @@ namespace intel_npu { namespace test_constants { inline constexpr uint32_t TARGET_ZE_DRIVER_NPU_EXT_VERSION = ZE_DRIVER_NPU_EXT_VERSION_1_0; -inline constexpr uint32_t TARGET_ZE_GRAPH_NPU_EXT_VERSION = ZE_GRAPH_EXT_VERSION_1_16; +inline constexpr uint32_t TARGET_ZE_GRAPH_NPU_EXT_VERSION = ZE_GRAPH_EXT_VERSION_1_17; inline constexpr uint32_t TARGET_ZE_COMMAND_QUEUE_NPU_EXT_VERSION = ZE_COMMAND_QUEUE_NPU_EXT_VERSION_1_1; inline constexpr uint32_t TARGET_ZE_PROFILING_NPU_EXT_VERSION = ZE_PROFILING_DATA_EXT_VERSION_1_0; inline constexpr uint32_t TARGET_ZE_CONTEXT_NPU_EXT_VERSION = ZE_CONTEXT_NPU_EXT_VERSION_1_0; @@ -45,15 +45,18 @@ struct ZeroInitStructsMock { return _zero_mem_pool; } + static void destroyContextForInstance(std::shared_ptr& instance); + private: void initNpuDriver(); void getExtensionFunctionAddress(const std::string& name, const uint32_t version, void** function_address); + void destroyContextLocked(); std::shared_ptr _zero_api; Logger _log; - ze_context_handle_t _context = nullptr; + std::atomic _context{nullptr}; ze_driver_handle_t _driver_handle = nullptr; ze_device_handle_t _device_handle = nullptr; diff --git a/src/plugins/intel_npu/tests/functional/internal/backend/zero_infer_request_tests.cpp b/src/plugins/intel_npu/tests/functional/internal/backend/zero_infer_request_tests.cpp index 7de01aa89d25..43b466c131a1 100644 --- a/src/plugins/intel_npu/tests/functional/internal/backend/zero_infer_request_tests.cpp +++ b/src/plugins/intel_npu/tests/functional/internal/backend/zero_infer_request_tests.cpp @@ -6,14 +6,22 @@ using namespace ov::test::behavior; -const std::vector configs = {{{ov::intel_npu::compiler_type(ov::intel_npu::CompilerType::PLUGIN), - ov::intel_npu::batch_mode(ov::intel_npu::BatchMode::PLUGIN)}}, - {{ov::intel_npu::compiler_type(ov::intel_npu::CompilerType::PLUGIN), - ov::intel_npu::batch_mode(ov::intel_npu::BatchMode::COMPILER)}}, - {{ov::intel_npu::compiler_type(ov::intel_npu::CompilerType::DRIVER), - ov::intel_npu::batch_mode(ov::intel_npu::BatchMode::PLUGIN)}}, - {{ov::intel_npu::compiler_type(ov::intel_npu::CompilerType::DRIVER), - ov::intel_npu::batch_mode(ov::intel_npu::BatchMode::COMPILER)}}}; +const std::vector configs = { + {{ov::intel_npu::compiler_type(ov::intel_npu::CompilerType::PLUGIN), + ov::intel_npu::batch_mode(ov::intel_npu::BatchMode::PLUGIN), + // platform needed only for NPU_COMPILER_TYPE_PLUGIN + ov::intel_npu::platform(ov::intel_npu::Platform::standardize( + ov::test::utils::getTestsPlatformFromEnvironmentOr(ov::test::utils::DEVICE_NPU)))}}, + {{ov::intel_npu::compiler_type(ov::intel_npu::CompilerType::PLUGIN), + ov::intel_npu::batch_mode(ov::intel_npu::BatchMode::COMPILER), + // platform needed only for NPU_COMPILER_TYPE_PLUGIN + ov::intel_npu::platform(ov::intel_npu::Platform::standardize( + ov::test::utils::getTestsPlatformFromEnvironmentOr(ov::test::utils::DEVICE_NPU)))}}, + {{ov::intel_npu::compiler_type(ov::intel_npu::CompilerType::DRIVER), + ov::intel_npu::batch_mode(ov::intel_npu::BatchMode::PLUGIN)}}, + {{ov::intel_npu::compiler_type(ov::intel_npu::CompilerType::DRIVER), + ov::intel_npu::batch_mode(ov::intel_npu::BatchMode::COMPILER)}}}; + const std::vector specialTensorDataTypes = {ov::element::boolean}; INSTANTIATE_TEST_SUITE_P( diff --git a/src/plugins/intel_npu/tests/functional/internal/backend/zero_infer_request_tests.hpp b/src/plugins/intel_npu/tests/functional/internal/backend/zero_infer_request_tests.hpp index cd74c4f40258..6ac8fa24bf86 100644 --- a/src/plugins/intel_npu/tests/functional/internal/backend/zero_infer_request_tests.hpp +++ b/src/plugins/intel_npu/tests/functional/internal/backend/zero_infer_request_tests.hpp @@ -6,9 +6,9 @@ #include -#include "../compiler_adapter/zero_init_mock.hpp" #include "common/npu_test_env_cfg.hpp" #include "common/utils.hpp" +#include "common/zero_init_mock.hpp" #include "common_test_utils/file_utils.hpp" #include "common_test_utils/ov_plugin_cache.hpp" #include "common_test_utils/ov_tensor_utils.hpp" @@ -158,14 +158,10 @@ class ZeroInferRequestTests : public ov::test::behavior::OVPluginTestBase, options->add<::intel_npu::COMPILER_TYPE>(); options->add<::intel_npu::BATCH_MODE>(); options->add<::intel_npu::MODEL_SERIALIZER_VERSION>(); - options->add<::intel_npu::ENABLE_CPU_PINNING>(); npu_config = std::make_unique<::intel_npu::FilteredConfig>(options); - ::intel_npu::Config::ConfigMap configMap{ - {::intel_npu::PLATFORM::key().data(), - ov::intel_npu::Platform::standardize(ov::test::utils::getTestsPlatformFromEnvironmentOr(target_device))}}; + ::intel_npu::Config::ConfigMap configMap; npu_config->enable(::intel_npu::PLATFORM::key().data(), true); npu_config->enable(::intel_npu::MODEL_SERIALIZER_VERSION::key().data(), true); - npu_config->enable(::intel_npu::ENABLE_CPU_PINNING::key().data(), true); for (const auto& [propertyName, propertyValue] : configuration) { configMap[propertyName] = propertyValue.as(); npu_config->enable(propertyName, true); @@ -227,6 +223,8 @@ TEST_P(ZeroInferRequestTests, BooleanSetTensorSetTensorsWork) { npu_config->update({{::intel_npu::MODEL_SERIALIZER_VERSION::key().data(), ::intel_npu::MODEL_SERIALIZER_VERSION::toString( ov::intel_npu::ModelSerializerVersion::ALL_WEIGHTS_COPY)}}); + } else { + npu_config->enable(::intel_npu::MODEL_SERIALIZER_VERSION::key().data(), false); } // logic for batch @@ -253,13 +251,13 @@ TEST_P(ZeroInferRequestTests, BooleanSetTensorSetTensorsWork) { graph->set_batch_size(batch.value()); } - auto compiledModel = - std::make_shared(ov_model, - std::make_shared(), - device, - graph, - *npu_config, - batch); // MockPlugin needed only to avoid throw for nullptr + auto compiledModel = std::make_shared( + ov_model, + std::make_shared(), // MockPlugin needed only to avoid throw for nullptr + device, + graph, + *npu_config, + batch); OPENVINO_ASSERT(compiledModel->inputs()[0].get_element_type() == element_type); OPENVINO_ASSERT(compiledModel->inputs()[1].get_element_type() == element_type); diff --git a/src/plugins/intel_npu/tests/functional/internal/compiler_adapter/vcl_allocator_test.cpp b/src/plugins/intel_npu/tests/functional/internal/compiler_adapter/vcl_allocator_test.cpp new file mode 100644 index 000000000000..da95d1328a2a --- /dev/null +++ b/src/plugins/intel_npu/tests/functional/internal/compiler_adapter/vcl_allocator_test.cpp @@ -0,0 +1,17 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "vcl_allocator_test.hpp" + +#include "common/npu_test_env_cfg.hpp" + +namespace { + +using ov::test::behavior::VclAllocatorFuncTests; + +INSTANTIATE_TEST_SUITE_P(smoke_BehaviorTest, + VclAllocatorFuncTests, + ::testing::Values(ov::test::utils::DEVICE_NPU), + VclAllocatorFuncTests::getTestCaseName); +} // namespace diff --git a/src/plugins/intel_npu/tests/functional/internal/compiler_adapter/vcl_allocator_test.hpp b/src/plugins/intel_npu/tests/functional/internal/compiler_adapter/vcl_allocator_test.hpp new file mode 100644 index 000000000000..006cbb22032c --- /dev/null +++ b/src/plugins/intel_npu/tests/functional/internal/compiler_adapter/vcl_allocator_test.hpp @@ -0,0 +1,335 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#pragma once + +#include + +#include +#include +#include + +#include "common/npu_test_env_cfg.hpp" +#include "common_test_utils/file_utils.hpp" +#include "common_test_utils/subgraph_builders/conv_pool_relu.hpp" +#include "intel_npu/utils/utils.hpp" +#include "intel_npu/utils/vcl/vcl_allocator.hpp" +#include "intel_npu/utils/vcl/vcl_api.hpp" +#include "model_serializer.hpp" +#include "openvino/core/except.hpp" +#include "openvino/runtime/make_tensor.hpp" +#include "openvino/runtime/tensor.hpp" +#include "shared_test_classes/base/ov_behavior_test_utils.hpp" + +namespace ov::test::behavior { + +class VclAllocatorFuncTests : public ::testing::TestWithParam { +public: + static std::string getTestCaseName(testing::TestParamInfo obj) { + auto targetDevice = obj.param; + std::replace(targetDevice.begin(), targetDevice.end(), ':', '_'); + std::ostringstream result; + result << "targetDevice=" << ov::test::utils::getDeviceNameTestCase(targetDevice) << "_"; + result << "targetPlatform=" + ov::test::utils::getTestsPlatformFromEnvironmentOr(ov::test::utils::DEVICE_NPU); + return result.str(); + } + +protected: + std::string targetDevice; + std::shared_ptr model; + std::shared_ptr<::intel_npu::vcl_allocator_3> allocator; + + void SetUp() override { + targetDevice = GetParam(); + model = ov::test::utils::make_conv_pool_relu(); + allocator = std::make_shared<::intel_npu::vcl_allocator_3>(); + + try { + std::string ov_lib_dir = ov::test::utils::getOpenvinoLibDirectory(); + ::intel_npu::VCLApi::getInstance(ov_lib_dir); + } catch (const std::exception&) { + GTEST_SKIP() << "Couldn't load compiler library"; + } + } + + // Helper struct and function to reduce code duplication + struct CompilerSetupState { + std::string buildFlags; + ::intel_npu::SerializedIR serializedIR; + vcl_compiler_handle_t compiler = nullptr; + vcl_log_handle_t logHandle = nullptr; + + CompilerSetupState() = default; + CompilerSetupState(const CompilerSetupState&) = delete; + CompilerSetupState& operator=(const CompilerSetupState&) = delete; + CompilerSetupState(CompilerSetupState&& other) noexcept + : buildFlags(std::move(other.buildFlags)), + serializedIR(std::move(other.serializedIR)), + compiler(other.compiler), + logHandle(other.logHandle) { + other.compiler = nullptr; + other.logHandle = nullptr; + } + CompilerSetupState& operator=(CompilerSetupState&& other) noexcept { + if (this != &other) { + release(); + buildFlags = std::move(other.buildFlags); + serializedIR = std::move(other.serializedIR); + compiler = other.compiler; + logHandle = other.logHandle; + other.compiler = nullptr; + other.logHandle = nullptr; + } + return *this; + } + + ~CompilerSetupState() { + release(); + } + + private: + void release() { + if (compiler != nullptr) { + ::intel_npu::vclCompilerDestroy(compiler); + compiler = nullptr; + } + } + }; + + CompilerSetupState createCompilerAndDescriptor() { + CompilerSetupState state; + + state.buildFlags = ::intel_npu::compiler_utils::serializeIOInfo(model, true); + + auto platform = ov::test::utils::getTestsPlatformFromEnvironmentOr(ov::test::utils::DEVICE_NPU); + if (platform.find("NPU") == 0) { + platform = platform.substr(3); // Remove "NPU" prefix if present + } + + state.buildFlags += + " --config NPU_PLATFORM=\"" + platform + "\" NPU_COMPILATION_MODE_PARAMS=\"optimization-level=0\""; + + vcl_version_info_t vclVersion = {}; + vcl_version_info_t vclProfilingVersion = {}; + ::intel_npu::vclGetVersion(&vclVersion, &vclProfilingVersion); + + vcl_compiler_desc_t compilerDesc = {}; + compilerDesc.version = vclVersion; + compilerDesc.debugLevel = + static_cast<__vcl_log_level_t>(static_cast(::intel_npu::Logger::global().level()) + 1); + + uint32_t defaultTileCount = std::numeric_limits::max(); + if (vclVersion.major == 7 && vclVersion.minor < 6) { + defaultTileCount = std::numeric_limits::max(); + } + + vcl_device_desc_t deviceDesc = {sizeof(vcl_device_desc_t), + 0x00, + std::numeric_limits::max(), + defaultTileCount}; + + ::intel_npu::vclCompilerCreate(&compilerDesc, &deviceDesc, &state.compiler, &state.logHandle); + + if (state.compiler == nullptr) { + ADD_FAILURE() << "vclCompilerCreate failed"; + return state; + } + + ze_graph_compiler_version_info_t vclVersionInfo = {0, 0}; + vcl_compiler_properties_t compilerProp = {}; + ::intel_npu::vclCompilerGetProperties(state.compiler, &compilerProp); + vclVersionInfo.major = compilerProp.version.major; + vclVersionInfo.minor = compilerProp.version.minor; + + auto isOptionValueSupportedByCompiler = [](const std::string&, const std::optional&) { + return true; + }; + + state.serializedIR = + ::intel_npu::compiler_utils::serializeIR(model, + vclVersionInfo, + 10, + ::intel_npu::MODEL_SERIALIZER_VERSION::defaultValue(), + isOptionValueSupportedByCompiler); + + state.buildFlags += " NPU_MODEL_SERIALIZER_VERSION=\"" + + ::intel_npu::MODEL_SERIALIZER_VERSION::toString(state.serializedIR.serializerVersion) + + "\""; + + return state; + } +}; + +TEST_P(VclAllocatorFuncTests, VerifyAllocation) { + uint8_t* blobBuffer = nullptr; + uint64_t blobSize = 0; + uint8_t* compatibilityReqBuffer = nullptr; + uint64_t compatibilityReqSize = 0; + + auto setup = createCompilerAndDescriptor(); + ASSERT_NE(setup.compiler, nullptr); + + vcl_executable_desc_t desc = {setup.serializedIR.buffer.get(), + setup.serializedIR.size, + setup.buildFlags.c_str(), + setup.buildFlags.size()}; + + auto result = ::intel_npu::vclAllocatedExecutableCreate3(setup.compiler, + desc, + allocator.get(), + &blobBuffer, + &blobSize, + &compatibilityReqBuffer, + &compatibilityReqSize); + + EXPECT_EQ(result, VCL_RESULT_SUCCESS); + EXPECT_NE(blobBuffer, nullptr); + EXPECT_NE(compatibilityReqBuffer, nullptr); + + // Verify it tracked exactly these two buffers + // 2 allocations: blob & compatibility string + EXPECT_EQ(allocator->m_info.size(), 2); + + auto blobIt = std::find_if(allocator->m_info.begin(), + allocator->m_info.end(), + [blobBuffer](const std::pair& item) { + return item.first == blobBuffer; + }); + EXPECT_NE(blobIt, allocator->m_info.end()); + + auto compatIt = std::find_if(allocator->m_info.begin(), + allocator->m_info.end(), + [compatibilityReqBuffer](const std::pair& item) { + return item.first == compatibilityReqBuffer; + }); + EXPECT_NE(compatIt, allocator->m_info.end()); +} + +TEST_P(VclAllocatorFuncTests, VerifyDeallocation) { + uint8_t* blobBuffer = nullptr; + uint64_t blobSize = 0; + uint8_t* compatibilityReqBuffer = nullptr; + uint64_t compatibilityReqSize = 0; + + auto setup = createCompilerAndDescriptor(); + ASSERT_NE(setup.compiler, nullptr); + + vcl_executable_desc_t desc = {setup.serializedIR.buffer.get(), + setup.serializedIR.size, + setup.buildFlags.c_str(), + setup.buildFlags.size()}; + + auto result = ::intel_npu::vclAllocatedExecutableCreate3(setup.compiler, + desc, + allocator.get(), + &blobBuffer, + &blobSize, + &compatibilityReqBuffer, + &compatibilityReqSize); + + EXPECT_EQ(result, VCL_RESULT_SUCCESS); + + // Clean up compatibility string + if (compatibilityReqBuffer != nullptr) { + allocator->deallocate(allocator.get(), compatibilityReqBuffer); + } + + // Verify compatibility buffer was erased from tracking vector + auto compatIt = std::find_if(allocator->m_info.begin(), + allocator->m_info.end(), + [compatibilityReqBuffer](const std::pair& item) { + return item.first == compatibilityReqBuffer; + }); + EXPECT_EQ(compatIt, allocator->m_info.end()); + EXPECT_EQ(allocator->m_info.size(), 1); +} + +TEST_P(VclAllocatorFuncTests, VerifyOwnershipTransferToTensor) { + uint8_t* blobBuffer = nullptr; + uint64_t blobSize = 0; + uint8_t* compatibilityReqBuffer = nullptr; + uint64_t compatibilityReqSize = 0; + + auto setup = createCompilerAndDescriptor(); + ASSERT_NE(setup.compiler, nullptr); + + vcl_executable_desc_t desc = {setup.serializedIR.buffer.get(), + setup.serializedIR.size, + setup.buildFlags.c_str(), + setup.buildFlags.size()}; + + auto result = ::intel_npu::vclAllocatedExecutableCreate3(setup.compiler, + desc, + allocator.get(), + &blobBuffer, + &blobSize, + &compatibilityReqBuffer, + &compatibilityReqSize); + + EXPECT_EQ(result, VCL_RESULT_SUCCESS); + + // Retrieve the real allocated size for the blob from the allocator (mirroring compiler_impl.cpp) + auto it = std::find_if(allocator->m_info.begin(), + allocator->m_info.end(), + [blobBuffer](const std::pair& item) { + return item.first == blobBuffer; + }); + ASSERT_NE(it, allocator->m_info.end()); + size_t alignedBlobSize = it->second; + + // Wrap Blob in ov::Tensor to test ownership transfer + { + ov::Tensor tensor = ::intel_npu::make_tensor_from_aligned_addr(blobBuffer, alignedBlobSize, allocator); + EXPECT_EQ(tensor.data(), blobBuffer); + EXPECT_EQ(tensor.get_byte_size(), alignedBlobSize); + + // Remove only the transferred blob, leaving any possible temporary allocations + // to be safely freed by ~vcl_allocator_3 (mirroring compiler_impl.cpp) + allocator->m_info.erase(it); + } // tensor goes out of scope here: custom deleter correctly handles the physical memory free + + // Clean up compatibility string manually + if (compatibilityReqBuffer != nullptr) { + allocator->deallocate(allocator.get(), compatibilityReqBuffer); + } + + // After erasing the blob and deallocating the compatibility string, + // the tracking info in the allocator should be empty. + EXPECT_EQ(allocator->m_info.size(), 0); +} + +TEST_P(VclAllocatorFuncTests, VerifyOrphanMemoryCleanupOnDestruction) { + uint8_t* blobBuffer = nullptr; + uint64_t blobSize = 0; + uint8_t* compatibilityReqBuffer = nullptr; + uint64_t compatibilityReqSize = 0; + + auto setup = createCompilerAndDescriptor(); + ASSERT_NE(setup.compiler, nullptr); + + vcl_executable_desc_t desc = {setup.serializedIR.buffer.get(), + setup.serializedIR.size, + setup.buildFlags.c_str(), + setup.buildFlags.size()}; + + auto result = ::intel_npu::vclAllocatedExecutableCreate3(setup.compiler, + desc, + allocator.get(), + &blobBuffer, + &blobSize, + &compatibilityReqBuffer, + &compatibilityReqSize); + + EXPECT_EQ(result, VCL_RESULT_SUCCESS); + EXPECT_EQ(allocator->m_info.size(), 2); // 2 allocations: blob & compatibility string + + // Purpose: Simulate VCL crash or early return where the memory is temporarily tracked as an orphan leak + // When the allocator goes out of scope and gets destroyed, it should cleanly free all tracked memory + + allocator.reset(); + + EXPECT_EQ(allocator, nullptr); +} + +} // namespace ov::test::behavior diff --git a/src/plugins/intel_npu/tests/functional/internal/compiler_adapter/zero_graph.cpp b/src/plugins/intel_npu/tests/functional/internal/compiler_adapter/zero_graph.cpp index 9276f4e474a0..1c280430f489 100644 --- a/src/plugins/intel_npu/tests/functional/internal/compiler_adapter/zero_graph.cpp +++ b/src/plugins/intel_npu/tests/functional/internal/compiler_adapter/zero_graph.cpp @@ -4,6 +4,8 @@ #include "zero_graph.hpp" +#include "openvino/util/codec_xor.hpp" + namespace { const std::vector configsGraphCompilationTests = {{}, {ov::cache_dir("test")}, @@ -35,4 +37,11 @@ INSTANTIATE_TEST_SUITE_P(smoke_BehaviorTest, graphExtVersions), ZeroGraphTest::getTestCaseName); +INSTANTIATE_TEST_SUITE_P(smoke_BehaviorTest, + EncryptionCallbacks, + ::testing::Combine(::testing::Values(ov::test::utils::DEVICE_NPU), + ::testing::ValuesIn(emptyConfigsTests), + graphExtVersions), + ZeroGraphTest::getTestCaseName); + } // namespace diff --git a/src/plugins/intel_npu/tests/functional/internal/compiler_adapter/zero_graph.hpp b/src/plugins/intel_npu/tests/functional/internal/compiler_adapter/zero_graph.hpp index 44be378a849d..a0ec07d87bf9 100644 --- a/src/plugins/intel_npu/tests/functional/internal/compiler_adapter/zero_graph.hpp +++ b/src/plugins/intel_npu/tests/functional/internal/compiler_adapter/zero_graph.hpp @@ -10,15 +10,18 @@ #include #include "common/npu_test_env_cfg.hpp" +#include "common/utils.hpp" +#include "common/zero_init_mock.hpp" #include "common_test_utils/subgraph_builders/multi_single_conv.hpp" +#include "driver_compiler_adapter.hpp" #include "intel_npu/utils/utils.hpp" #include "intel_npu/utils/zero/zero_mem.hpp" #include "intel_npu/utils/zero/zero_mem_pool.hpp" #include "intel_npu/utils/zero/zero_utils.hpp" #include "model_serializer.hpp" #include "openvino/runtime/intel_npu/properties.hpp" +#include "plugin_compiler_adapter.hpp" #include "ze_graph_ext_wrappers.hpp" -#include "zero_init_mock.hpp" using namespace intel_npu; @@ -93,7 +96,9 @@ class ZeroGraphTest : public ::testing::TestWithParamdestroyGraph(graphDescriptor); + if (zeGraphExt != nullptr) { + zeGraphExt->destroyGraph(graphDescriptor); + } if (blob) { ::operator delete(blob, std::align_val_t(::utils::STANDARD_PAGE_SIZE)); } @@ -328,6 +333,45 @@ TEST_P(ZeroGraphTest, CheckNoThrowOnUnsupportedFeature) { } #endif +TEST_P(ZeroGraphTest, DontDestroyZeroGraphWhenZeroContextIsDestroyed) { + SKIP_IF_CURRENT_TEST_IS_DISABLED() + + std::string logs; + std::mutex logsMutex; + + std::function logCb = [&](std::string_view msg) { + std::lock_guard lock(logsMutex); + logs.append(msg); + logs.push_back('\n'); + }; + + auto localZeroInitMock = std::make_shared<::intel_npu::ZeroInitStructsMock>(); + + std::shared_ptr<::intel_npu::ZeroInitStructsHolder> localZeroInitStruct = + std::reinterpret_pointer_cast<::intel_npu::ZeroInitStructsHolder>(localZeroInitMock); + + { + utils::LogCallbackGuard log_callback_guard(logCb); + utils::LoggerLevelGuard logger_level_guard(ov::log::Level::WARNING); + + auto localZeroGraphExt = std::make_shared(localZeroInitStruct); + serializeIR(); + GraphDescriptor localGraphDescriptor; + OV_ASSERT_NO_THROW(localGraphDescriptor = localZeroGraphExt->getGraphDescriptor(serializedIR, "")); + OV_ASSERT_NO_THROW(localZeroGraphExt->initializeGraph(localGraphDescriptor)); + + ::intel_npu::ZeroInitStructsMock::destroyContextForInstance(localZeroInitMock); + + try { + localZeroGraphExt->destroyGraph(localGraphDescriptor); + } catch (const std::exception& ex) { + ASSERT_FALSE(true) << ex.what(); + } + } + ASSERT_NE(logs.find("Context or graph is null while trying to destroy graph."), std::string::npos); + ASSERT_EQ(logs.find("failed to destroy graph handle"), std::string::npos); +} + using IsOptionSupported = ZeroGraphTest; TEST_P(IsOptionSupported, GetCompilerSupportedOptions) { @@ -362,4 +406,21 @@ TEST_P(IsOptionSupported, PropertySupportedByDriver) { } } +using EncryptionCallbacks = ZeroGraphTest; + +TEST_P(EncryptionCallbacks, EncryptionCallbacksSetSecureCompileFlag) { + auto localZeGraphExt = std::make_shared(zeroInitStruct); + serializeIR(); + if (zeroInitStruct->getGraphDdiTable().version() < ZE_MAKE_VERSION(1, 17)) { + OV_EXPECT_THROW( + localZeGraphExt->getGraphDescriptor(serializedIR, "", bypassUmdCache(), /* secureCompile = */ true), + ov::Exception, + testing::HasSubstr( + "Secure compilation was requested, but the current driver version does not support it.")); + } else { + OV_ASSERT_NO_THROW( + localZeGraphExt->getGraphDescriptor(serializedIR, "", bypassUmdCache(), /* secureCompile = */ true)); + } +} + } // namespace ov::test::behavior diff --git a/src/plugins/intel_npu/tests/functional/internal/plugin/test_properties.hpp b/src/plugins/intel_npu/tests/functional/internal/plugin/test_properties.hpp index 1be5b8e1f5e9..c3e0afc72f0e 100644 --- a/src/plugins/intel_npu/tests/functional/internal/plugin/test_properties.hpp +++ b/src/plugins/intel_npu/tests/functional/internal/plugin/test_properties.hpp @@ -40,39 +40,6 @@ using ::testing::HasSubstr; using ConfigParams = std::tuple; // Config name -namespace { -class LogCallbackGuard { -public: - explicit LogCallbackGuard(const std::function& callback) { - ov::util::set_log_callback(callback); - } - - ~LogCallbackGuard() { - ov::util::reset_log_callback(); - } - - LogCallbackGuard(const LogCallbackGuard&) = delete; - LogCallbackGuard& operator=(const LogCallbackGuard&) = delete; -}; - -class LoggerLevelGuard { -public: - explicit LoggerLevelGuard(ov::log::Level level) : _previousLevel(::intel_npu::Logger::global().level()) { - ::intel_npu::Logger::global().setLevel(level); - } - - ~LoggerLevelGuard() { - ::intel_npu::Logger::global().setLevel(_previousLevel); - } - - LoggerLevelGuard(const LoggerLevelGuard&) = delete; - LoggerLevelGuard& operator=(const LoggerLevelGuard&) = delete; - -private: - ov::log::Level _previousLevel; -}; -} // namespace - namespace ov { namespace test { namespace behavior { @@ -123,7 +90,6 @@ class PropertiesManagerTests : public ov::test::behavior::OVPluginTestBase, options->add(); \ npu_config.enable(std::move(o_name), false); \ } while (0) - REGISTER_OPTION(LOG_LEVEL); REGISTER_OPTION(CACHE_DIR); REGISTER_OPTION(CACHE_MODE); @@ -136,10 +102,11 @@ class PropertiesManagerTests : public ov::test::behavior::OVPluginTestBase, REGISTER_OPTION(PERFORMANCE_HINT); REGISTER_OPTION(EXECUTION_MODE_HINT); REGISTER_OPTION(PERFORMANCE_HINT_NUM_REQUESTS); + OPENVINO_SUPPRESS_DEPRECATED_START REGISTER_OPTION(ENABLE_CPU_PINNING); + OPENVINO_SUPPRESS_DEPRECATED_END REGISTER_OPTION(INFERENCE_PRECISION_HINT); REGISTER_OPTION(MODEL_PRIORITY); - REGISTER_OPTION(EXCLUSIVE_ASYNC_REQUESTS); REGISTER_OPTION(COMPILATION_MODE_PARAMS); REGISTER_OPTION(DMA_ENGINES); REGISTER_OPTION(TILES); @@ -235,8 +202,8 @@ TEST_P(PropertiesManagerTests, ExpectRunTimeSpecialBothPropertyIsSupported) { }; { - LogCallbackGuard log_callback_guard(log_cb); - LoggerLevelGuard logger_level_guard(ov::log::Level::INFO); + utils::LogCallbackGuard log_callback_guard(log_cb); + utils::LoggerLevelGuard logger_level_guard(ov::log::Level::INFO); propertiesManager->setProperty({{ov::log::level(ov::log::Level::INFO)}}); propertiesManager->setProperty({{ov::intel_npu::compiler_type(ov::intel_npu::CompilerType::DRIVER)}}); isSupported = propertiesManager->isPropertySupported(configuration); @@ -263,8 +230,8 @@ TEST_P(PropertiesManagerTests, ExpectArgumentIsNotSupported) { {"DUMMY_PROPERTY", "DUMMY_VALUE"}}; { - LogCallbackGuard log_callback_guard(log_cb); - LoggerLevelGuard logger_level_guard(ov::log::Level::INFO); + utils::LogCallbackGuard log_callback_guard(log_cb); + utils::LoggerLevelGuard logger_level_guard(ov::log::Level::INFO); try { propertiesManager->setProperty(arguments); @@ -294,8 +261,8 @@ TEST_P(ExpectLoadingCompilerPropertySupported, ExpectCompilerPropertyIsSupported }; { - LogCallbackGuard log_callback_guard(log_cb); - LoggerLevelGuard logger_level_guard(ov::log::Level::INFO); + utils::LogCallbackGuard log_callback_guard(log_cb); + utils::LoggerLevelGuard logger_level_guard(ov::log::Level::INFO); propertiesManager->setProperty({{ov::intel_npu::compiler_type(ov::intel_npu::CompilerType::DRIVER)}}); isSupported = propertiesManager->isPropertySupported(configuration); } @@ -320,8 +287,8 @@ TEST_P(ExpectLoadingCompilerPropertyNotSupported, ExpectCompilerPropertyIsNotSup }; { - LogCallbackGuard log_callback_guard(log_cb); - LoggerLevelGuard logger_level_guard(ov::log::Level::INFO); + utils::LogCallbackGuard log_callback_guard(log_cb); + utils::LoggerLevelGuard logger_level_guard(ov::log::Level::INFO); propertiesManager->setProperty({{ov::intel_npu::compiler_type(ov::intel_npu::CompilerType::DRIVER)}}); isSupported = propertiesManager->isPropertySupported(configuration); } diff --git a/src/plugins/intel_npu/tests/functional/internal/utils/zero/zero_cmd_queue_pool_tests.hpp b/src/plugins/intel_npu/tests/functional/internal/utils/zero/zero_cmd_queue_pool_tests.hpp index 66f0518b6e03..8ed99daf1bce 100644 --- a/src/plugins/intel_npu/tests/functional/internal/utils/zero/zero_cmd_queue_pool_tests.hpp +++ b/src/plugins/intel_npu/tests/functional/internal/utils/zero/zero_cmd_queue_pool_tests.hpp @@ -93,13 +93,38 @@ TEST_P(ZeroCmdQueuePoolTests, SetWorkloadType) { } } -TEST_P(ZeroCmdQueuePoolTests, PoolReusabilityTest) { - // Test that the pool correctly reuses queues after weak_ptr cleanup +TEST_P(ZeroCmdQueuePoolTests, SetWorkloadTypeOnExistingQueue) { + if (init_struct->getCommandQueueDdiTable().version() < ZE_MAKE_VERSION(1, 0)) { + GTEST_SKIP() << "The WorkloadType property is not supported by the current driver.\n"; + } + + int owner = 1; ::intel_npu::CommandQueueDesc command_queue_desc{ZE_COMMAND_QUEUE_PRIORITY_NORMAL, - std::nullopt, + ZE_WORKLOAD_TYPE_DEFAULT, 0, - nullptr, - true}; + &owner, + false}; + + auto cmd_queue = ::intel_npu::ZeroCmdQueuePool::getInstance().getCommandQueue(init_struct, command_queue_desc); + ASSERT_NE(cmd_queue, nullptr); + + OV_ASSERT_NO_THROW(cmd_queue->setWorkloadType(ZE_WORKLOAD_TYPE_BACKGROUND)); + OV_ASSERT_NO_THROW(cmd_queue->setWorkloadType(ZE_WORKLOAD_TYPE_DEFAULT)); +} + +TEST_P(ZeroCmdQueuePoolTests, SharedCommonQueueDisabledRequiresOwnerTag) { + OV_EXPECT_THROW_HAS_SUBSTRING((::intel_npu::CommandQueueDesc{ZE_COMMAND_QUEUE_PRIORITY_NORMAL, + std::nullopt, + 0, + nullptr, + false}), + ov::Exception, + "owner_tag must not be null"); +} + +TEST_P(ZeroCmdQueuePoolTests, PoolReusabilityTest) { + // Test that the pool correctly reuses queues after weak_ptr cleanup + ::intel_npu::CommandQueueDesc command_queue_desc{ZE_COMMAND_QUEUE_PRIORITY_NORMAL, std::nullopt, 0, nullptr, true}; // First allocation std::shared_ptr<::intel_npu::CommandQueue> queue1 = @@ -171,6 +196,60 @@ TEST_P(ZeroCmdQueuePoolTests, PoolReusabilityDisabledTest) { queue3.reset(); } +TEST_P(ZeroCmdQueuePoolTests, SharedCommonQueueDisabledUsesNewQueueWhenDescriptorChanges) { + int owner = 1; + + ::intel_npu::CommandQueueDesc base_desc{ZE_COMMAND_QUEUE_PRIORITY_NORMAL, std::nullopt, 0, &owner, false}; + auto base_queue = ::intel_npu::ZeroCmdQueuePool::getInstance().getCommandQueue(init_struct, base_desc); + auto same_base_queue = ::intel_npu::ZeroCmdQueuePool::getInstance().getCommandQueue(init_struct, base_desc); + + ASSERT_NE(base_queue, nullptr); + ASSERT_EQ(base_queue.get(), same_base_queue.get()) << "Same descriptor and owner_tag should reuse the same queue"; + + ::intel_npu::CommandQueueDesc priority_desc{ + ZE_COMMAND_QUEUE_PRIORITY_PRIORITY_HIGH, + std::nullopt, + 0, + &owner, + false, + }; + auto priority_queue = ::intel_npu::ZeroCmdQueuePool::getInstance().getCommandQueue(init_struct, priority_desc); + + ASSERT_NE(priority_queue, nullptr); + EXPECT_NE(base_queue.get(), priority_queue.get()) + << "Changing model priority should create a different queue for shared_common_queue=false"; + + if (init_struct->getCommandQueueDdiTable().version() >= ZE_MAKE_VERSION(1, 0)) { + ::intel_npu::CommandQueueDesc workload_desc{ + ZE_COMMAND_QUEUE_PRIORITY_NORMAL, + ZE_WORKLOAD_TYPE_BACKGROUND, + 0, + &owner, + false, + }; + auto workload_queue = ::intel_npu::ZeroCmdQueuePool::getInstance().getCommandQueue(init_struct, workload_desc); + + ASSERT_NE(workload_queue, nullptr); + EXPECT_NE(base_queue.get(), workload_queue.get()) + << "Changing workload type should create a different queue for shared_common_queue=false"; + } + + if (init_struct->getCommandQueueDdiTable().version() >= ZE_MAKE_VERSION(1, 1)) { + ::intel_npu::CommandQueueDesc option_desc{ + ZE_COMMAND_QUEUE_PRIORITY_NORMAL, + std::nullopt, + ZE_NPU_COMMAND_QUEUE_OPTION_DEVICE_SYNC, + &owner, + false, + }; + auto option_queue = ::intel_npu::ZeroCmdQueuePool::getInstance().getCommandQueue(init_struct, option_desc); + + ASSERT_NE(option_queue, nullptr); + EXPECT_NE(base_queue.get(), option_queue.get()) + << "Changing command queue options should create a different queue for shared_common_queue=false"; + } +} + TEST_P(ZeroCmdQueuePoolTests, AllCommandQueueOptionsCombinations) { if (init_struct->getCommandQueueDdiTable().version() < ZE_MAKE_VERSION(1, 1)) { GTEST_SKIP() << "Not all the command queue options are supported by the current driver.\n"; @@ -319,7 +398,11 @@ TEST_P(ZeroCmdQueuePoolTests, MultiThreadingTest) { } // else: no options (commandQueueOptions = 0) - ::intel_npu::CommandQueueDesc command_queue_desc{priority, workload_type, commandQueueOptions, nullptr, true}; + ::intel_npu::CommandQueueDesc command_queue_desc{priority, + workload_type, + commandQueueOptions, + nullptr, + true}; auto cmd_queue = ::intel_npu::ZeroCmdQueuePool::getInstance().getCommandQueue(init_struct, command_queue_desc); @@ -387,10 +470,10 @@ TEST_P(ZeroCmdQueuePoolTests, MultiThreadingTestWithDestroyCmdQueue) { // else: no options (commandQueueOptions = 0) ::intel_npu::CommandQueueDesc command_queue_desc{priority, - workload_type, - commandQueueOptions, - nullptr, - true}; + workload_type, + commandQueueOptions, + nullptr, + true}; auto cmd_queue = ::intel_npu::ZeroCmdQueuePool::getInstance().getCommandQueue(init_struct, command_queue_desc); ASSERT_NE(cmd_queue, nullptr); diff --git a/src/plugins/intel_npu/tests/functional/internal/utils/zero/zero_mem_pool_tests.hpp b/src/plugins/intel_npu/tests/functional/internal/utils/zero/zero_mem_pool_tests.hpp index 6962c3f8d898..f11547382bb7 100644 --- a/src/plugins/intel_npu/tests/functional/internal/utils/zero/zero_mem_pool_tests.hpp +++ b/src/plugins/intel_npu/tests/functional/internal/utils/zero/zero_mem_pool_tests.hpp @@ -17,6 +17,7 @@ #include "common/npu_test_env_cfg.hpp" #include "common/utils.hpp" +#include "common/zero_init_mock.hpp" #include "functional_test_utils/ov_plugin_cache.hpp" #include "intel_npu/common/npu.hpp" #include "intel_npu/config/config.hpp" @@ -25,8 +26,8 @@ #include "intel_npu/utils/zero/zero_mem.hpp" #include "intel_npu/utils/zero/zero_mem_pool.hpp" #include "intel_npu/utils/zero/zero_utils.hpp" -#include "internal/compiler_adapter/zero_init_mock.hpp" #include "openvino/core/any.hpp" +#include "openvino/core/log.hpp" #include "openvino/runtime/core.hpp" #include "shared_test_classes/base/ov_behavior_test_utils.hpp" @@ -367,6 +368,73 @@ TEST_P(ZeroMemPoolTests, MultiThreadingImportMemoryReUseAndDestroyItWithMultiple } } +TEST_P(ZeroMemPoolTests, ImportMemoryAdjacentToAlreadyImportedAllocation) { + SKIP_IF_CURRENT_TEST_IS_DISABLED() + + if (!init_struct->isExternalMemoryStandardAllocationSupported()) { + GTEST_SKIP() << "Test requires support for importing standard allocated memory as external memory, which is " + "not available on this platform."; + } + + // Allocate a single, contiguous two-page block so the two halves are guaranteed to be page-adjacent. + constexpr size_t page = 4096; + void* block = ::operator new(2 * page, std::align_val_t(page)); + auto* lower = static_cast(block); + auto* upper = lower + page; + + // Import the upper page first so that it is a registered allocation in the L0 context. + std::shared_ptr<::intel_npu::ZeroMem> upper_mem; + OV_ASSERT_NO_THROW(upper_mem = ::intel_npu::zero_mem::import_standard_allocation_memory(init_struct, upper, page)); + + // The lower page's exclusive end pointer (lower + page) aliases the base of the already-imported upper page. + // This must NOT be treated as an overlap: the regions merely abut, they do not overlap. Importing the lower + // page must succeed. + std::shared_ptr<::intel_npu::ZeroMem> lower_mem; + OV_ASSERT_NO_THROW(lower_mem = ::intel_npu::zero_mem::import_standard_allocation_memory(init_struct, lower, page)); + + ASSERT_TRUE(::intel_npu::zeroUtils::get_l0_context_memory_allocation_id(init_struct->getContext(), lower)); + ASSERT_TRUE(::intel_npu::zeroUtils::get_l0_context_memory_allocation_id(init_struct->getContext(), upper)); + + lower_mem = {}; + upper_mem = {}; + ::operator delete(block, std::align_val_t(page)); +} + +TEST_P(ZeroMemPoolTests, DontDestroyZeroMemoryWhenZeroContextIsDestroyed) { + SKIP_IF_CURRENT_TEST_IS_DISABLED() + + std::string logs; + std::mutex logs_mutex; + + // Keep this std::function alive while logging is active. + std::function log_cb = [&](std::string_view msg) { + std::lock_guard lock(logs_mutex); + logs.append(msg); + logs.push_back('\n'); + }; + + auto zero_init_mock = std::make_shared<::intel_npu::ZeroInitStructsMock>(); + + std::shared_ptr<::intel_npu::ZeroInitStructsHolder> zero_init_struct = + std::reinterpret_pointer_cast<::intel_npu::ZeroInitStructsHolder>(zero_init_mock); + + { + utils::LogCallbackGuard log_callback_guard(log_cb); + utils::LoggerLevelGuard logger_level_guard(ov::log::Level::WARNING); + + auto zero_memory = ::intel_npu::zero_mem::allocate_memory(zero_init_struct, 4096, 4096); + ::intel_npu::ZeroInitStructsMock::destroyContextForInstance(zero_init_mock); + + try { + zero_memory = {}; + } catch (const std::exception& ex) { + ASSERT_FALSE(true) << ex.what(); + } + } + ASSERT_NE(logs.find("Context is null while trying to free memory with id"), std::string::npos); + ASSERT_EQ(logs.find("L0 zeMemFree result:"), std::string::npos); +} + } // namespace behavior } // namespace test } // namespace ov diff --git a/src/plugins/intel_npu/tests/functional/internal/utils/zero/zero_wrappers_tests.cpp b/src/plugins/intel_npu/tests/functional/internal/utils/zero/zero_wrappers_tests.cpp new file mode 100644 index 000000000000..ecbd805397b1 --- /dev/null +++ b/src/plugins/intel_npu/tests/functional/internal/utils/zero/zero_wrappers_tests.cpp @@ -0,0 +1,17 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "internal/utils/zero/zero_wrappers_tests.hpp" + +#include "common/npu_test_env_cfg.hpp" +#include "common/utils.hpp" +#include "intel_npu/config/options.hpp" +#include "intel_npu/npu_private_properties.hpp" + +using namespace ov::test::behavior; + +INSTANTIATE_TEST_SUITE_P(compatibility_smoke_BehaviorTest, + ZeroWrappersTests, + ::testing::Values(ov::test::utils::DEVICE_NPU), + ZeroWrappersTests::getTestCaseName); diff --git a/src/plugins/intel_npu/tests/functional/internal/utils/zero/zero_wrappers_tests.hpp b/src/plugins/intel_npu/tests/functional/internal/utils/zero/zero_wrappers_tests.hpp new file mode 100644 index 000000000000..19a379daef43 --- /dev/null +++ b/src/plugins/intel_npu/tests/functional/internal/utils/zero/zero_wrappers_tests.hpp @@ -0,0 +1,250 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#pragma once + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "common/npu_test_env_cfg.hpp" +#include "common/utils.hpp" +#include "common/zero_init_mock.hpp" +#include "functional_test_utils/ov_plugin_cache.hpp" +#include "intel_npu/utils/zero/zero_init.hpp" +#include "intel_npu/utils/zero/zero_wrappers.hpp" +#include "openvino/core/any.hpp" +#include "openvino/core/log.hpp" +#include "openvino/runtime/core.hpp" +#include "shared_test_classes/base/ov_behavior_test_utils.hpp" + +namespace ov { +namespace test { +namespace behavior { +using CompilationParams = std::string; + +using ::testing::AllOf; +using ::testing::HasSubstr; +class ZeroWrappersTests : public ov::test::behavior::OVPluginTestBase, + public testing::WithParamInterface { +public: + static std::string getTestCaseName(const testing::TestParamInfo& obj) { + std::string target_device; + ov::AnyMap configuration; + target_device = obj.param; + std::replace(target_device.begin(), target_device.end(), ':', '_'); + target_device = ov::test::utils::getTestsPlatformFromEnvironmentOr(ov::test::utils::DEVICE_NPU); + + std::ostringstream result; + result << "targetDevice=" << target_device << "_"; + result << "targetPlatform=" << ov::test::utils::getTestsPlatformFromEnvironmentOr(target_device) << "_"; + if (!configuration.empty()) { + for (auto& configItem : configuration) { + result << "configItem=" << configItem.first << "_"; + configItem.second.print(result); + } + } + + return result.str(); + } + + void SetUp() override { + target_device = this->GetParam(); + + SKIP_IF_CURRENT_TEST_IS_DISABLED() + OVPluginTestBase::SetUp(); + } + + void TearDown() override { + APIBaseTest::TearDown(); + } +}; + +TEST_P(ZeroWrappersTests, DontDestroyZeroCommandListWhenZeroContextIsDestroyed) { + SKIP_IF_CURRENT_TEST_IS_DISABLED() + + std::string logs; + std::mutex logs_mutex; + + // Keep this std::function alive while logging is active. + std::function log_cb = [&](std::string_view msg) { + std::lock_guard lock(logs_mutex); + logs.append(msg); + logs.push_back('\n'); + }; + + auto zero_init_mock = std::make_shared<::intel_npu::ZeroInitStructsMock>(); + + std::shared_ptr<::intel_npu::ZeroInitStructsHolder> zero_init_struct = + std::reinterpret_pointer_cast<::intel_npu::ZeroInitStructsHolder>(zero_init_mock); + + { + utils::LogCallbackGuard log_callback_guard(log_cb); + utils::LoggerLevelGuard logger_level_guard(ov::log::Level::WARNING); + + auto command_list = std::make_shared<::intel_npu::CommandList>(zero_init_struct); + ::intel_npu::ZeroInitStructsMock::destroyContextForInstance(zero_init_mock); + + try { + command_list = {}; + } catch (const std::exception& ex) { + ASSERT_FALSE(true) << ex.what(); + } + } + ASSERT_NE(logs.find("Context or CommandList handle is null during destruction."), std::string::npos); + ASSERT_EQ(logs.find("zeCommandListDestroy failed"), std::string::npos); +} + +TEST_P(ZeroWrappersTests, DontDestroyZeroCommandQueueWhenZeroContextIsDestroyed) { + SKIP_IF_CURRENT_TEST_IS_DISABLED() + + std::string logs; + std::mutex logs_mutex; + + std::function log_cb = [&](std::string_view msg) { + std::lock_guard lock(logs_mutex); + logs.append(msg); + logs.push_back('\n'); + }; + + auto zero_init_mock = std::make_shared<::intel_npu::ZeroInitStructsMock>(); + + std::shared_ptr<::intel_npu::ZeroInitStructsHolder> zero_init_struct = + std::reinterpret_pointer_cast<::intel_npu::ZeroInitStructsHolder>(zero_init_mock); + + { + utils::LogCallbackGuard log_callback_guard(log_cb); + utils::LoggerLevelGuard logger_level_guard(ov::log::Level::WARNING); + + auto command_queue = + std::make_shared<::intel_npu::CommandQueue>(zero_init_struct, ::intel_npu::CommandQueueDesc{}); + ::intel_npu::ZeroInitStructsMock::destroyContextForInstance(zero_init_mock); + + try { + command_queue = {}; + } catch (const std::exception& ex) { + ASSERT_FALSE(true) << ex.what(); + } + } + ASSERT_NE(logs.find("Context or CommandQueue handle is null during destruction."), std::string::npos); + ASSERT_EQ(logs.find("zeCommandQueueDestroy failed"), std::string::npos); +} + +TEST_P(ZeroWrappersTests, DontDestroyZeroFenceWhenZeroContextIsDestroyed) { + SKIP_IF_CURRENT_TEST_IS_DISABLED() + + std::string logs; + std::mutex logs_mutex; + + std::function log_cb = [&](std::string_view msg) { + std::lock_guard lock(logs_mutex); + logs.append(msg); + logs.push_back('\n'); + }; + + auto zero_init_mock = std::make_shared<::intel_npu::ZeroInitStructsMock>(); + + std::shared_ptr<::intel_npu::ZeroInitStructsHolder> zero_init_struct = + std::reinterpret_pointer_cast<::intel_npu::ZeroInitStructsHolder>(zero_init_mock); + + { + utils::LogCallbackGuard log_callback_guard(log_cb); + utils::LoggerLevelGuard logger_level_guard(ov::log::Level::WARNING); + + auto command_queue = + std::make_shared<::intel_npu::CommandQueue>(zero_init_struct, ::intel_npu::CommandQueueDesc{}); + auto fence = std::make_shared<::intel_npu::Fence>(command_queue); + ::intel_npu::ZeroInitStructsMock::destroyContextForInstance(zero_init_mock); + + try { + fence = {}; + command_queue = {}; + } catch (const std::exception& ex) { + ASSERT_FALSE(true) << ex.what(); + } + } + ASSERT_NE(logs.find("Context or Fence handle is null during destruction."), std::string::npos); + ASSERT_EQ(logs.find("zeFenceDestroy failed"), std::string::npos); +} + +TEST_P(ZeroWrappersTests, DontDestroyZeroEventPoolWhenZeroContextIsDestroyed) { + SKIP_IF_CURRENT_TEST_IS_DISABLED() + + std::string logs; + std::mutex logs_mutex; + + std::function log_cb = [&](std::string_view msg) { + std::lock_guard lock(logs_mutex); + logs.append(msg); + logs.push_back('\n'); + }; + + auto zero_init_mock = std::make_shared<::intel_npu::ZeroInitStructsMock>(); + + std::shared_ptr<::intel_npu::ZeroInitStructsHolder> zero_init_struct = + std::reinterpret_pointer_cast<::intel_npu::ZeroInitStructsHolder>(zero_init_mock); + + { + utils::LogCallbackGuard log_callback_guard(log_cb); + utils::LoggerLevelGuard logger_level_guard(ov::log::Level::WARNING); + + auto event_pool = std::make_shared<::intel_npu::EventPool>(zero_init_struct, 1); + ::intel_npu::ZeroInitStructsMock::destroyContextForInstance(zero_init_mock); + + try { + event_pool = {}; + } catch (const std::exception& ex) { + ASSERT_FALSE(true) << ex.what(); + } + } + ASSERT_NE(logs.find("Context or EventPool handle is null during destruction."), std::string::npos); + ASSERT_EQ(logs.find("zeEventPoolDestroy failed"), std::string::npos); +} + +TEST_P(ZeroWrappersTests, DontDestroyZeroEventWhenZeroContextIsDestroyed) { + SKIP_IF_CURRENT_TEST_IS_DISABLED() + + std::string logs; + std::mutex logs_mutex; + + std::function log_cb = [&](std::string_view msg) { + std::lock_guard lock(logs_mutex); + logs.append(msg); + logs.push_back('\n'); + }; + + auto zero_init_mock = std::make_shared<::intel_npu::ZeroInitStructsMock>(); + + std::shared_ptr<::intel_npu::ZeroInitStructsHolder> zero_init_struct = + std::reinterpret_pointer_cast<::intel_npu::ZeroInitStructsHolder>(zero_init_mock); + + { + utils::LogCallbackGuard log_callback_guard(log_cb); + utils::LoggerLevelGuard logger_level_guard(ov::log::Level::WARNING); + + auto event_pool = std::make_shared<::intel_npu::EventPool>(zero_init_struct, 1); + auto event = std::make_shared<::intel_npu::Event>(event_pool, 0); + ::intel_npu::ZeroInitStructsMock::destroyContextForInstance(zero_init_mock); + + try { + event = {}; + event_pool = {}; + } catch (const std::exception& ex) { + ASSERT_FALSE(true) << ex.what(); + } + } + ASSERT_NE(logs.find("Context or Event handle is null during destruction."), std::string::npos); + ASSERT_EQ(logs.find("zeEventDestroy failed"), std::string::npos); +} + +} // namespace behavior +} // namespace test +} // namespace ov diff --git a/src/plugins/intel_npu/tests/functional/main.cpp b/src/plugins/intel_npu/tests/functional/main.cpp index 5ae7b65b0efb..8c8b85b76e37 100644 --- a/src/plugins/intel_npu/tests/functional/main.cpp +++ b/src/plugins/intel_npu/tests/functional/main.cpp @@ -113,7 +113,7 @@ int main(int argc, char** argv, char** envp) { << "'; Full device name: '" << full << "'" << std::endl; } - std::string dTest = ::testing::internal::GTEST_FLAG(internal_run_death_test); + std::string dTest = ::testing::GTEST_FLAG(internal_run_death_test); if (dTest.empty()) { std::cout << oss.str() << std::endl; } else { diff --git a/src/plugins/intel_npu/tests/functional/shared_tests_instances/behavior/compiled_model/model_cache.cpp b/src/plugins/intel_npu/tests/functional/shared_tests_instances/behavior/compiled_model/model_cache.cpp new file mode 100644 index 000000000000..2e0993173030 --- /dev/null +++ b/src/plugins/intel_npu/tests/functional/shared_tests_instances/behavior/compiled_model/model_cache.cpp @@ -0,0 +1,67 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "behavior/compiled_model/model_cache.hpp" + +#include + +#include "common/npu_test_env_cfg.hpp" +#include "common/utils.hpp" + +using namespace ov::test::behavior; + +namespace { + +std::vector config = { + {}, + {ov::enable_weightless(true), + ov::cache_mode(ov::CacheMode::OPTIMIZE_SIZE), + ov::intel_npu::compiler_type(ov::intel_npu::CompilerType::PLUGIN), // only CIP is allowed for ONE_SHOT + ov::intel_npu::platform(ov::intel_npu::Platform::standardize( + ov::test::utils::getTestsPlatformFromEnvironmentOr(ov::test::utils::DEVICE_NPU))), // platform needed for NPU_COMPILER_TYPE_PLUGIN + ov::intel_npu::separate_weights_version(ov::intel_npu::WSVersion::ONE_SHOT), + ov::enable_mmap(false)}, // enable_mmap for both `import_model(stream)` and `import_model(tensor)` paths + {ov::enable_weightless(true), + ov::cache_mode(ov::CacheMode::OPTIMIZE_SIZE), + ov::intel_npu::compiler_type(ov::intel_npu::CompilerType::PLUGIN), + ov::intel_npu::platform(ov::intel_npu::Platform::standardize( + ov::test::utils::getTestsPlatformFromEnvironmentOr(ov::test::utils::DEVICE_NPU))), // platform needed for NPU_COMPILER_TYPE_PLUGIN + ov::intel_npu::separate_weights_version(ov::intel_npu::WSVersion::ONE_SHOT), + ov::enable_mmap(true)}, + {ov::enable_weightless(true), + ov::cache_mode(ov::CacheMode::OPTIMIZE_SIZE), + ov::intel_npu::compiler_type(ov::intel_npu::CompilerType::DRIVER), + ov::intel_npu::separate_weights_version(ov::intel_npu::WSVersion::ITERATIVE), + ov::enable_mmap(false)}, + {ov::enable_weightless(true), + ov::cache_mode(ov::CacheMode::OPTIMIZE_SIZE), + ov::intel_npu::compiler_type(ov::intel_npu::CompilerType::DRIVER), + ov::intel_npu::separate_weights_version(ov::intel_npu::WSVersion::ITERATIVE), + ov::enable_mmap(true)}, + {ov::enable_weightless(true), + ov::cache_mode(ov::CacheMode::OPTIMIZE_SIZE), + ov::intel_npu::compiler_type(ov::intel_npu::CompilerType::PLUGIN), + ov::intel_npu::platform(ov::intel_npu::Platform::standardize( + ov::test::utils::getTestsPlatformFromEnvironmentOr(ov::test::utils::DEVICE_NPU))), // platform needed for NPU_COMPILER_TYPE_PLUGIN + ov::intel_npu::separate_weights_version(ov::intel_npu::WSVersion::ITERATIVE), + ov::enable_mmap(false)}, + {ov::enable_weightless(true), + ov::cache_mode(ov::CacheMode::OPTIMIZE_SIZE), + ov::intel_npu::compiler_type(ov::intel_npu::CompilerType::PLUGIN), + ov::intel_npu::platform(ov::intel_npu::Platform::standardize( + ov::test::utils::getTestsPlatformFromEnvironmentOr(ov::test::utils::DEVICE_NPU))), // platform needed for NPU_COMPILER_TYPE_PLUGIN + ov::intel_npu::separate_weights_version(ov::intel_npu::WSVersion::ITERATIVE), + ov::enable_mmap(true)}}; + +INSTANTIATE_TEST_SUITE_P(smoke_BehaviorTests, + WeightlessCacheAccuracy, + ::testing::Combine(::testing::ValuesIn({true, false}), // m_use_compile_model_api + ::testing::Values(true), // m_do_encryption + ::testing::Values(ov::element::f16), // m_inference_mode + ::testing::Values(ov::element::f16), // m_model_dtype + ::testing::ValuesIn(config), // config parsed with std::ignore + ::testing::Values(ov::test::utils::DEVICE_NPU)), // m_target_device + ov::test::utils::appendPlatformTypeTestName); + +} // namespace diff --git a/src/plugins/intel_npu/tests/functional/shared_tests_instances/behavior/compiled_model/properties.cpp b/src/plugins/intel_npu/tests/functional/shared_tests_instances/behavior/compiled_model/properties.cpp index d0a3aafe920a..c4048a858088 100644 --- a/src/plugins/intel_npu/tests/functional/shared_tests_instances/behavior/compiled_model/properties.cpp +++ b/src/plugins/intel_npu/tests/functional/shared_tests_instances/behavior/compiled_model/properties.cpp @@ -4,6 +4,8 @@ #include "behavior/compiled_model/properties.hpp" +#include + #include "common/functions.hpp" #include "common/npu_test_env_cfg.hpp" #include "common/utils.hpp" @@ -50,21 +52,25 @@ const std::vector> compiledModelProperties = { {ov::hint::model_priority.name(), ov::Any(ov::hint::Priority::HIGH)}, {ov::intel_npu::tiles.name(), ov::Any(2)}, {ov::intel_npu::profiling_type.name(), ov::Any(ov::intel_npu::ProfilingType::INFER)}, - {ov::intel_npu::defer_weights_load.name(), ov::Any(false)}}; + {ov::intel_npu::defer_weights_load.name(), ov::Any(false)}, + {ov::cache_encryption_callbacks.name(), ov::Any(ov::EncryptionCallbacks{ov::util::codec_xor, ov::util::codec_xor})}, +}; const std::string& expectedModelName = []() -> std::string { return ov::test::behavior::getDefaultNGraphFunctionForTheDevice()->get_friendly_name(); }(); const std::vector compatibilityPublicCompiledModelConfigs = { - {{ov::hint::enable_cpu_pinning.name(), ov::Any(false)}}, {{ov::hint::model_priority.name(), ov::Any(ov::hint::Priority::MEDIUM)}}, {{ov::execution_devices.name(), ov::Any(ov::test::utils::DEVICE_NPU)}}, {{ov::loaded_from_cache.name(), ov::Any(false)}}, {{ov::model_name.name(), ov::Any(expectedModelName)}}, {{ov::optimal_number_of_infer_requests.name(), ov::Any(1u)}}, {{ov::hint::performance_mode.name(), ov::Any(ov::hint::PerformanceMode::LATENCY)}}, - {{ov::hint::num_requests.name(), ov::Any(1u)}}}; + {{ov::hint::num_requests.name(), ov::Any(1u)}}, + {{ov::cache_encryption_callbacks.name(), + ov::Any(ov::EncryptionCallbacks{ov::util::codec_xor, ov::util::codec_xor})}}, +}; const std::vector publicCompiledModelConfigs = { // execution_mode isn't supported with PV driver. diff --git a/src/plugins/intel_npu/tests/functional/shared_tests_instances/behavior/ov_infer_request/cancellation.cpp b/src/plugins/intel_npu/tests/functional/shared_tests_instances/behavior/ov_infer_request/cancellation.cpp deleted file mode 100644 index ce930c01cc37..000000000000 --- a/src/plugins/intel_npu/tests/functional/shared_tests_instances/behavior/ov_infer_request/cancellation.cpp +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (C) 2018-2026 Intel Corporation -// SPDX-License-Identifier: Apache-2.0 -// - -#include - -#include "common/npu_test_env_cfg.hpp" - -using namespace ov::test::behavior; - -namespace { -const std::vector configs = {{}}; - -INSTANTIATE_TEST_SUITE_P(compatibility_smoke_BehaviorTests, - OVInferRequestCancellationTests, - ::testing::Combine(::testing::Values(ov::test::utils::DEVICE_NPU), - ::testing::ValuesIn(configs)), - InferRequestParamsAnyMapTestName::getTestCaseName); - -} // namespace diff --git a/src/plugins/intel_npu/tests/functional/shared_tests_instances/behavior/ov_infer_request/properties_tests.cpp b/src/plugins/intel_npu/tests/functional/shared_tests_instances/behavior/ov_infer_request/properties_tests.cpp index ccc62fb40b33..5f3d521dabf6 100644 --- a/src/plugins/intel_npu/tests/functional/shared_tests_instances/behavior/ov_infer_request/properties_tests.cpp +++ b/src/plugins/intel_npu/tests/functional/shared_tests_instances/behavior/ov_infer_request/properties_tests.cpp @@ -13,7 +13,7 @@ namespace { INSTANTIATE_TEST_SUITE_P(compatibility_smoke_BehaviorTests, InferRequestPropertiesTest, - ::testing::Combine(::testing::Values(2u), + ::testing::Combine(::testing::Values(0u), ::testing::Values(ov::test::utils::DEVICE_NPU), ::testing::ValuesIn(std::vector{{}})), ov::test::utils::appendPlatformTypeTestName); diff --git a/src/plugins/intel_npu/tests/functional/shared_tests_instances/behavior/ov_plugin/caching_tests.cpp b/src/plugins/intel_npu/tests/functional/shared_tests_instances/behavior/ov_plugin/caching_tests.cpp index e35701b19a6f..fd2247ccd8bf 100644 --- a/src/plugins/intel_npu/tests/functional/shared_tests_instances/behavior/ov_plugin/caching_tests.cpp +++ b/src/plugins/intel_npu/tests/functional/shared_tests_instances/behavior/ov_plugin/caching_tests.cpp @@ -145,4 +145,9 @@ INSTANTIATE_TEST_SUITE_P(smoke_BehaviorTests_CachingSupportCase_NPU_Check_Config ::testing::ValuesIn(cachingProperties)), ov::test::utils::appendPlatformTypeTestName); +INSTANTIATE_TEST_SUITE_P(smoke_BehaviorTests_CompileModelWithCacheEncryptionTest_NPU, + CompileModelWithCacheEncryptionTest, + ::testing::Values(ov::test::utils::DEVICE_NPU), + ov::test::utils::appendPlatformTypeTestName); + } // namespace diff --git a/src/plugins/intel_npu/tests/functional/shared_tests_instances/behavior/ov_plugin/properties_tests.cpp b/src/plugins/intel_npu/tests/functional/shared_tests_instances/behavior/ov_plugin/properties_tests.cpp index 96d100a21ee0..16136939ab10 100644 --- a/src/plugins/intel_npu/tests/functional/shared_tests_instances/behavior/ov_plugin/properties_tests.cpp +++ b/src/plugins/intel_npu/tests/functional/shared_tests_instances/behavior/ov_plugin/properties_tests.cpp @@ -5,6 +5,7 @@ #include "behavior/ov_plugin/properties_tests.hpp" #include +#include #include "common/npu_test_env_cfg.hpp" #include "common/utils.hpp" @@ -26,16 +27,32 @@ constexpr std::vector operator+(const std::vector& vector1, const std::vec ov::log::Level getTestsLogLevelFromEnvironmentOr(ov::log::Level instead) { if (auto var = std::getenv("OV_NPU_LOG_LEVEL")) { - std::istringstream stringStream = std::istringstream(var); - ov::log::Level level; + try { + std::istringstream stringStream = std::istringstream(var); + ov::log::Level level; - stringStream >> level; + stringStream >> level; - return level; + return level; + } catch (...) { + // ignore parsing errors and return default log level + } } return instead; } +const std::vector anyMapVecToStringVec(const std::vector& anyMaps) { + std::vector result; + for (const auto& anyMap : anyMaps) { + for (const auto& keyValue : anyMap) { + if (std::find(result.begin(), result.end(), keyValue.first) == result.end()) { + result.push_back(keyValue.first); + } + } + } + return result; +} + const std::vector compat_CorrectPluginMutableProperties = { // OV {{ov::hint::performance_mode.name(), ov::hint::PerformanceMode::THROUGHPUT}}, @@ -46,6 +63,7 @@ const std::vector compat_CorrectPluginMutableProperties = { {{ov::log::level.name(), ov::log::Level::ERR}}, {{ov::device::id.name(), removeDeviceNameOnlyID(ov::test::utils::getTestsPlatformFromEnvironmentOr("3720"))}}, {{ov::enable_profiling.name(), true}}, + {{ov::cache_encryption_callbacks.name(), ov::EncryptionCallbacks{ov::util::codec_xor, ov::util::codec_xor}}}, }; const std::vector CorrectPluginMutableProperties = { @@ -60,7 +78,7 @@ const std::vector compat_CorrectPluginDefaultMutableProperties = { {{ov::hint::num_requests.name(), 1u}}, {{ov::log::level.name(), getTestsLogLevelFromEnvironmentOr(ov::log::Level::WARNING)}}, {{ov::device::id.name(), ""}}, - {{ov::num_streams.name(), ov::streams::Num(1)}}, + {{ov::num_streams.name(), ov::streams::AUTO}}, }; const std::vector CorrectPluginDefaultMutableProperties = { @@ -99,7 +117,6 @@ const std::vector CorrectCompiledModelProperties = { }; const std::vector IncorrectImmutableProperties = { - {{ov::streams::num.name(), ov::streams::Num(2)}}, {{ov::optimal_number_of_infer_requests.name(), 4}}, {{ov::intel_npu::device_alloc_mem_size.name(), 1024}}, {{ov::intel_npu::device_total_mem_size.name(), 2048}}, @@ -198,7 +215,8 @@ INSTANTIATE_TEST_SUITE_P( smoke_BehaviorTests_OVCheckSetSupportedRWMetricsPropsTests, OVCheckSetSupportedRWMetricsPropsTests, ::testing::Combine(::testing::Values(ov::test::utils::DEVICE_NPU), - ::testing::ValuesIn(getRWMandatoryPropertiesValues(compat_CorrectPluginMutableProperties))), + ::testing::ValuesIn(OVCheckSetSupportedRWMetricsPropsTests::getRWMandatoryPropertiesValues( + anyMapVecToStringVec(compat_CorrectPluginMutableProperties)))), (ov::test::utils::appendPlatformTypeTestName)); INSTANTIATE_TEST_SUITE_P( diff --git a/src/plugins/intel_npu/tests/functional/shared_tests_instances/npu_skip_func_tests.xml b/src/plugins/intel_npu/tests/functional/shared_tests_instances/npu_skip_func_tests.xml index a02375f18b64..c81cf10cdbe9 100644 --- a/src/plugins/intel_npu/tests/functional/shared_tests_instances/npu_skip_func_tests.xml +++ b/src/plugins/intel_npu/tests/functional/shared_tests_instances/npu_skip_func_tests.xml @@ -186,7 +186,6 @@ Example: TensorIterator layer is not supported - .*ReturnResultNotReadyFromWaitInAsyncModeForTooSmallTimeout.* .*OVInferRequestDynamicTests.* .*OVInferenceChaining.* @@ -400,17 +399,6 @@ Example: - - - Runs only on NPU3720 with Level Zero enabled #85493 - - !3720 - - - .*InferRequestRunTests.MultipleExecutorStreamsTestsSyncInfers.* - - - Other devices than NPU doesn't allow to set NPU properties with OV1.0 and CACHE_DIR + PLUGIN is not supported @@ -595,6 +583,16 @@ Example: + + Output I4 / U4 precision is not currently supported. + + .*smoke.*OVInferRequestCheckTensorPrecision.*type=i4.* + .*smoke.*OVInferRequestCheckTensorPrecision.*type=u4.* + .*smoke.*OVInferRequestIOTensorSetPrecisionTestNPU.*type=i4.* + .*smoke.*OVInferRequestIOTensorSetPrecisionTestNPU.*type=u4.* + + + Disabled tests for NPU3720 @@ -739,7 +737,7 @@ Example: - NPU cannot set properties for compiled models + NPU cannot set properties for compiled models except CACHE_ENCRYPTION_CALLBACKS, but is not convertible to string .*OVClassCompiledModelSetCorrectConfigTest.canSetConfig.* @@ -877,7 +875,75 @@ Example: 3720 - .*ZeroGraphTest.SetAlignedBlob.*zeGraphNpuExtVersion=(1.13|1.14|1.15|1.16).* + .*ZeroGraphTest.SetAlignedBlob.* + + + + + + TiWithLstmCell model won't provide `WeightlessCacheAttribute` in constants' rt_info + + LEVEL0 + + + .*WeightlessCacheAccuracy.TiWithLstmCell.*ENABLE_WEIGHTLESS.* + + + + + + Write only (WO) properties will return empty ov::Any objects + + LEVEL0 + + + .*OVClassCompiledModelPropertiesTests.canCompileModelWithPropertiesAndCheckGetProperty.*CACHE_ENCRYPTION_CALLBACKS.* + .*OVPropertiesTests.canSetPropertyAndCheckGetProperty.*CACHE_ENCRYPTION_CALLBACKS.* + .*OVPropertiesDefaultSupportedTests.CanSetDefaultValueBackToPlugin.* + .*OVClassCompiledModelGetConfigTest.GetConfigNoThrow.* + .*OVSpecificDeviceGetConfigTest.GetConfigSpecificDeviceNoThrow.* + + + + + PV driver cannot compile models securely + + LEVEL0 + 1688 + + + .*OVHoldersTestNPU.CompileModelWithEncryptionWorksAfterConfigDeallocate.* + + + + + Exclusive Async Request is not supported by NPU + + .*InferRequestPropertiesTest.canSetExclusiveAsyncRequests.* + .*InferRequestPropertiesTest.ReusableCPUStreamsExecutor.* + + + + + + Test failed with compile error. + + LEVEL0 + 3720 + + + .*SerializationTestNPUW.CompiledModelPhase0CompatibilityExportSucceedsWithStaticAttention.* + + + + + + The tests crash when compiling model on NPU5010 + + 5010 + + + .*InferWithHostCompileTests.* diff --git a/src/plugins/intel_npu/tests/unit/CMakeLists.txt b/src/plugins/intel_npu/tests/unit/CMakeLists.txt index 9989274e3495..f0c1d728de10 100644 --- a/src/plugins/intel_npu/tests/unit/CMakeLists.txt +++ b/src/plugins/intel_npu/tests/unit/CMakeLists.txt @@ -40,6 +40,7 @@ ov_add_test_target( ${OpenVINO_SOURCE_DIR}/thirdparty/level_zero/level-zero/include OBJECT_FILES # plugin/src + ${OpenVINO_SOURCE_DIR}/src/plugins/intel_npu/src/plugin/src/executor.cpp ${OpenVINO_SOURCE_DIR}/src/plugins/intel_npu/src/plugin/src/metadata.cpp # npuw core ${OpenVINO_SOURCE_DIR}/src/plugins/intel_npu/src/plugin/npuw/compiled_model.cpp @@ -60,11 +61,15 @@ ov_add_test_target( ${OpenVINO_SOURCE_DIR}/src/plugins/intel_npu/src/plugin/npuw/logging.cpp ${OpenVINO_SOURCE_DIR}/src/plugins/intel_npu/src/plugin/npuw/llm_prefix_caching.cpp ${OpenVINO_SOURCE_DIR}/src/plugins/intel_npu/src/plugin/npuw/infer_request_utils.cpp + ${OpenVINO_SOURCE_DIR}/src/plugins/intel_npu/src/plugin/npuw/orc.cpp ${OpenVINO_SOURCE_DIR}/src/plugins/intel_npu/src/plugin/npuw/serialization.cpp ${OpenVINO_SOURCE_DIR}/src/plugins/intel_npu/src/plugin/npuw/util.cpp ${OpenVINO_SOURCE_DIR}/src/plugins/intel_npu/src/plugin/npuw/util_xarch.cpp + ${OpenVINO_SOURCE_DIR}/src/plugins/intel_npu/src/plugin/npuw/v1/elements/accuracy_checked.cpp ${OpenVINO_SOURCE_DIR}/src/plugins/intel_npu/src/plugin/npuw/v1/elements/failsafe.cpp ${OpenVINO_SOURCE_DIR}/src/plugins/intel_npu/src/plugin/npuw/lazy_tensor.cpp + ${OpenVINO_SOURCE_DIR}/src/plugins/intel_npu/src/plugin/npuw/npuw_transformations/split_kvcache_into_blocks.cpp + ${OpenVINO_SOURCE_DIR}/src/plugins/intel_npu/src/plugin/npuw/kv_cache_block_manager.cpp ${OpenVINO_SOURCE_DIR}/src/plugins/intel_npu/src/plugin/npuw/attention.cpp ${OpenVINO_SOURCE_DIR}/src/plugins/intel_npu/src/plugin/npuw/pyramid_attention.cpp ${OpenVINO_SOURCE_DIR}/src/plugins/intel_npu/src/plugin/npuw/host_flash_attention.cpp @@ -73,8 +78,10 @@ ov_add_test_target( ${OpenVINO_SOURCE_DIR}/src/plugins/intel_npu/src/plugin/npuw/spatial.cpp ${OpenVINO_SOURCE_DIR}/src/plugins/intel_npu/src/plugin/npuw/weights_bank.cpp ${OpenVINO_SOURCE_DIR}/src/plugins/intel_npu/src/plugin/npuw/accuracy/comparator.cpp + ${OpenVINO_SOURCE_DIR}/src/plugins/intel_npu/src/plugin/npuw/partitioning/patterns/fold_const.cpp # moe ${OpenVINO_SOURCE_DIR}/src/plugins/intel_npu/src/plugin/npuw/moe/moe_executor.cpp + ${OpenVINO_SOURCE_DIR}/src/plugins/intel_npu/src/plugin/npuw/moe/moe_subgraph.cpp ${OpenVINO_SOURCE_DIR}/src/plugins/intel_npu/src/plugin/npuw/moe/moe_resources.cpp ${OpenVINO_SOURCE_DIR}/src/plugins/intel_npu/src/plugin/npuw/moe/moe_infer_utils.cpp ${OpenVINO_SOURCE_DIR}/src/plugins/intel_npu/src/plugin/npuw/moe_transformations/moe_transformation.cpp @@ -82,13 +89,16 @@ ov_add_test_target( ${OpenVINO_SOURCE_DIR}/src/plugins/intel_npu/src/plugin/npuw/moe_transformations/device_routed_moe_transform.cpp ${OpenVINO_SOURCE_DIR}/src/plugins/intel_npu/src/plugin/npuw/moe_transformations/gather_to_2d_gather.cpp ${OpenVINO_SOURCE_DIR}/src/plugins/intel_npu/src/plugin/npuw/moe_transformations/apply_moe_device_routed_transforms.cpp + # attn + ${OpenVINO_SOURCE_DIR}/src/plugins/intel_npu/src/plugin/npuw/attn/attn_subgraph.cpp # npuw_transformations + ${OpenVINO_SOURCE_DIR}/src/plugins/intel_npu/src/plugin/npuw/npuw_transformations/add_position_ids_param.cpp ${OpenVINO_SOURCE_DIR}/src/plugins/intel_npu/src/plugin/npuw/npuw_transformations/convert_kvcache_to_precision.cpp ${OpenVINO_SOURCE_DIR}/src/plugins/intel_npu/src/plugin/npuw/npuw_transformations/decompose_gqa.cpp ${OpenVINO_SOURCE_DIR}/src/plugins/intel_npu/src/plugin/npuw/npuw_transformations/lora_stateful_to_stateless.cpp ${OpenVINO_SOURCE_DIR}/src/plugins/intel_npu/src/plugin/npuw/npuw_transformations/optimize_value_tensors.cpp - ${OpenVINO_SOURCE_DIR}/src/plugins/intel_npu/src/plugin/npuw/npuw_transformations/patch_phi3_sliding_mask.cpp - ${OpenVINO_SOURCE_DIR}/src/plugins/intel_npu/src/plugin/npuw/npuw_transformations/phi3_sliding_mask.cpp + ${OpenVINO_SOURCE_DIR}/src/plugins/intel_npu/src/plugin/npuw/npuw_transformations/patch_sliding_window_mask.cpp + ${OpenVINO_SOURCE_DIR}/src/plugins/intel_npu/src/plugin/npuw/npuw_transformations/sliding_window_mask.cpp ${OpenVINO_SOURCE_DIR}/src/plugins/intel_npu/src/plugin/npuw/npuw_transformations/reshape_sliced_head_to_static.cpp ${OpenVINO_SOURCE_DIR}/src/plugins/intel_npu/src/plugin/npuw/npuw_transformations/reshape_to_static.cpp ${OpenVINO_SOURCE_DIR}/src/plugins/intel_npu/src/plugin/npuw/npuw_transformations/slice_out_embeds.cpp @@ -100,6 +110,12 @@ ov_add_test_target( ${OpenVINO_SOURCE_DIR}/src/plugins/intel_npu/src/plugin/npuw/pipelines/kokoro/kokoro_utils.cpp # model builder (test utility) ${OpenVINO_SOURCE_DIR}/src/plugins/intel_npu/tests/functional/behavior/npuw/test_engine/models/model_builder.cpp + ${OpenVINO_SOURCE_DIR}/src/plugins/intel_npu/tests/functional/behavior/npuw/test_engine/models/model_builder_attention.cpp + ${OpenVINO_SOURCE_DIR}/src/plugins/intel_npu/tests/functional/behavior/npuw/test_engine/models/model_builder_ffn.cpp + ${OpenVINO_SOURCE_DIR}/src/plugins/intel_npu/tests/functional/behavior/npuw/test_engine/models/model_builder_masks.cpp + ${OpenVINO_SOURCE_DIR}/src/plugins/intel_npu/tests/functional/behavior/npuw/test_engine/models/model_builder_norm.cpp + ${OpenVINO_SOURCE_DIR}/src/plugins/intel_npu/tests/functional/behavior/npuw/test_engine/models/model_builder_rope.cpp + ${OpenVINO_SOURCE_DIR}/src/plugins/intel_npu/tests/functional/behavior/npuw/test_engine/models/model_builder_weights.cpp LINK_LIBRARIES ${MANDATORY_UNIT_TESTS_LIBS} LABELS @@ -119,9 +135,20 @@ install(TARGETS ${TARGET_NAME} ) target_sources(${TARGET_NAME} PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/npu/executor_test.cpp ${CMAKE_CURRENT_SOURCE_DIR}/npuw/llm_compiled_model_graph_options_test.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/npuw/stored_tokens_state_test.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/npuw/lincache_utils_test.cpp ${CMAKE_CURRENT_SOURCE_DIR}/npuw/partitioning_options_test.cpp ${CMAKE_CURRENT_SOURCE_DIR}/npuw/npuw_options_smoke_test.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/npuw/pre_compute_test.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/npuw/resolve_dynamic_quant_storage_types_test.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/npuw/orc.cpp ${CMAKE_CURRENT_SOURCE_DIR}/npuw/sdpa_pattern_nodes_test.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/npuw/subgraph_pipeline.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/npuw/subgraph_behavior_infer_test.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/npuw/preserve_const_matmul_pattern_test.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/npuw/pipeline_passes/add_position_ids_param_test.cpp ${CMAKE_CURRENT_SOURCE_DIR}/npuw/pipeline_passes/remove_empty_kv_inputs_test.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/npuw/pipeline_passes/stateful_to_stateless_test.cpp ) diff --git a/src/plugins/intel_npu/tests/unit/main.cpp b/src/plugins/intel_npu/tests/unit/main.cpp index 46eac4072eaf..f43877f9d4ff 100644 --- a/src/plugins/intel_npu/tests/unit/main.cpp +++ b/src/plugins/intel_npu/tests/unit/main.cpp @@ -62,7 +62,7 @@ int main(int argc, char** argv, char** envp) { ::testing::InitGoogleTest(&argc, argv); ::testing::AddGlobalTestEnvironment(new testing::Environment()); - std::string dTest = ::testing::internal::GTEST_FLAG(internal_run_death_test); + std::string dTest = ::testing::GTEST_FLAG(internal_run_death_test); if (!dTest.empty()) { std::cout << "gtest death test process is running" << std::endl; } diff --git a/src/plugins/intel_npu/tests/unit/npu/executor_test.cpp b/src/plugins/intel_npu/tests/unit/npu/executor_test.cpp new file mode 100644 index 000000000000..977541abdfcb --- /dev/null +++ b/src/plugins/intel_npu/tests/unit/npu/executor_test.cpp @@ -0,0 +1,595 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "executor.hpp" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "openvino/openvino.hpp" +#include "openvino/runtime/common.hpp" +#include "openvino/runtime/iasync_infer_request.hpp" +#include "openvino/runtime/icompiled_model.hpp" +#include "openvino/runtime/isync_infer_request.hpp" +#include "openvino/runtime/ivariable_state.hpp" + +namespace { + +using namespace std::chrono_literals; + +class SharedState { +public: + std::atomic inferCalls{0}; + std::atomic failInfer{false}; +}; + +class TestPlugin final : public ov::IPlugin { +public: + std::shared_ptr compile_model(const std::shared_ptr&, + const ov::AnyMap&) const override { + OPENVINO_THROW("Not implemented in unit test plugin"); + } + + std::shared_ptr compile_model(const std::shared_ptr&, + const ov::AnyMap&, + const ov::SoPtr&) const override { + OPENVINO_THROW("Not implemented in unit test plugin"); + } + + void set_property(const ov::AnyMap&) override {} + + ov::Any get_property(const std::string&, const ov::AnyMap&) const override { + return {}; + } + + ov::SoPtr create_context(const ov::AnyMap&) const override { + return {}; + } + + ov::SoPtr get_default_context(const ov::AnyMap&) const override { + return {}; + } + + std::shared_ptr import_model(std::istream&, const ov::AnyMap&) const override { + OPENVINO_THROW("Not implemented in unit test plugin"); + } + + std::shared_ptr import_model(std::istream&, + const ov::SoPtr&, + const ov::AnyMap&) const override { + OPENVINO_THROW("Not implemented in unit test plugin"); + } + + std::shared_ptr import_model(const ov::Tensor&, const ov::AnyMap&) const override { + OPENVINO_THROW("Not implemented in unit test plugin"); + } + + std::shared_ptr import_model(const ov::Tensor&, + const ov::SoPtr&, + const ov::AnyMap&) const override { + OPENVINO_THROW("Not implemented in unit test plugin"); + } + + ov::SupportedOpsMap query_model(const std::shared_ptr&, const ov::AnyMap&) const override { + return {}; + } +}; + +class TestCompiledModel; + +class TestSyncInferRequest final : public ov::ISyncInferRequest { +public: + TestSyncInferRequest(std::shared_ptr compiled_model, std::shared_ptr state) + : ov::ISyncInferRequest(std::move(compiled_model)), + m_state(std::move(state)) {} + + void infer() override { + m_state->inferCalls.fetch_add(1); + if (m_state->failInfer.exchange(false)) { + OPENVINO_THROW("sync infer failure"); + } + } + + std::vector get_profiling_info() const override { + return {}; + } + + std::vector> query_state() const override { + return {}; + } + +protected: + void check_tensors() const override { + // This test request does not consume model tensors; it validates async callback flow only. + } + +private: + std::shared_ptr m_state; +}; + +class TestCompiledModel final : public ov::ICompiledModel { +public: + explicit TestCompiledModel(std::shared_ptr state) + : ov::ICompiledModel(build_model(), make_test_plugin()), + m_state(std::move(state)), + m_model(build_model()) {} + + std::shared_ptr create_sync_infer_request() const override { + auto self = std::static_pointer_cast(shared_from_this()); + return std::make_shared(std::move(self), m_state); + } + + std::shared_ptr create_infer_request() const override { + return std::make_shared(create_sync_infer_request(), + intel_npu::make_executor("executor_test_task", 1), + intel_npu::make_executor("executor_test_callback", 1)); + } + + void export_model(std::ostream&) const override {} + + std::shared_ptr get_runtime_model() const override { + return m_model; + } + + void set_property(const ov::AnyMap&) override {} + + ov::Any get_property(const std::string& name) const override { + if (name == ov::execution_devices.name()) { + return std::vector{"NPU"}; + } + OPENVINO_THROW("Unsupported property: ", name); + } + +private: + static std::shared_ptr make_test_plugin() { + static const auto plugin = std::make_shared(); + return plugin; + } + + static std::shared_ptr build_model() { + auto param = std::make_shared(ov::element::f32, ov::Shape{1}); + auto result = std::make_shared(param); + return std::make_shared(ov::ResultVector{result}, ov::ParameterVector{param}); + } + + std::shared_ptr m_state; + std::shared_ptr m_model; +}; + +bool wait_until(const std::function& pred) { + while (!pred()) { + std::this_thread::yield(); + } + return true; +} + +TEST(NPUExecutorTests, InlineModeRunsOnCallerThread) { + auto exec = intel_npu::make_executor("inline", 0, false, 10ms); + + std::thread::id taskThread{}; + exec->run([&] { + taskThread = std::this_thread::get_id(); + }); + + EXPECT_EQ(taskThread, std::this_thread::get_id()); +} + +TEST(NPUExecutorTests, InlineModeSwallowsTaskExceptions) { + auto exec = intel_npu::make_executor("inline_throw", 0, false, 10ms); + + std::atomic taskRan{false}; + + EXPECT_NO_THROW(exec->run([&] { + taskRan = true; + throw std::runtime_error("inline_failure"); + })); + + EXPECT_TRUE(taskRan.load()); + + std::atomic followUpRan{false}; + EXPECT_NO_THROW(exec->run([&] { + followUpRan = true; + })); + EXPECT_TRUE(followUpRan.load()); +} + +TEST(NPUExecutorTests, AdaptiveModeGrowsForQueuedWork) { + auto exec = intel_npu::make_executor("grow", 0, true, 200ms); + + std::mutex m; + std::condition_variable cv; + bool release = false; + int started = 0; + + auto blockingTask = [&] { + { + std::lock_guard lock(m); + ++started; + } + cv.notify_all(); + + std::unique_lock lock(m); + cv.wait(lock, [&] { + return release; + }); + }; + + exec->run(blockingTask); + exec->run(blockingTask); + + // EXPECT (non-fatal) so the release block always runs, + // preventing the destructor from deadlocking on blocked worker threads. + EXPECT_TRUE(wait_until([&] { + std::lock_guard lock(m); + return started == 2; + })); + + { + std::lock_guard lock(m); + release = true; + } + cv.notify_all(); +} + +TEST(NPUExecutorTests, FixedModeDoesNotUseMoreThanConfiguredWorkers) { + constexpr int workers = 2; + constexpr int tasksCount = 6; + auto exec = intel_npu::make_executor("fixed_workers", workers, false, 200ms); + + std::mutex m; + std::condition_variable cv; + bool release = false; + int started = 0; + int completed = 0; + std::set workerThreads; + + for (int i = 0; i < tasksCount; ++i) { + exec->run([&] { + { + std::lock_guard lock(m); + ++started; + workerThreads.insert(std::this_thread::get_id()); + } + cv.notify_all(); + + std::unique_lock lock(m); + cv.wait(lock, [&] { + return release; + }); + + ++completed; + cv.notify_all(); + }); + } + + // EXPECT (non-fatal) so the release block always runs, + // preventing the destructor from deadlocking on 6 blocked worker tasks. + EXPECT_TRUE(wait_until([&] { + std::lock_guard lock(m); + return started >= workers; + })); + + { + std::lock_guard lock(m); + release = true; + } + cv.notify_all(); + + EXPECT_TRUE(wait_until([&] { + std::lock_guard lock(m); + return completed == tasksCount; + })); + + std::lock_guard lock(m); + EXPECT_LE(workerThreads.size(), static_cast(workers)); +} + +TEST(NPUExecutorTests, AdaptiveModeGrowShrinkAcrossMultipleBursts) { + auto exec = intel_npu::make_executor("grow_shrink_cycles", 1, true, 30ms); + + for (int cycle = 0; cycle < 5; ++cycle) { + std::mutex m; + std::condition_variable cv; + bool release = false; + int started = 0; + int completed = 0; + + auto blockingTask = [&] { + { + std::lock_guard lock(m); + ++started; + } + cv.notify_all(); + + std::unique_lock lock(m); + cv.wait(lock, [&] { + return release; + }); + + ++completed; + cv.notify_all(); + }; + + exec->run(blockingTask); + exec->run(blockingTask); + + // EXPECT (non-fatal) so the release block always runs, + // preventing the destructor from deadlocking on blocked worker threads. + EXPECT_TRUE(wait_until([&] { + std::lock_guard lock(m); + return started == 2; + })); + + { + std::lock_guard lock(m); + release = true; + } + cv.notify_all(); + + EXPECT_TRUE(wait_until([&] { + std::lock_guard lock(m); + return completed == 2; + })); + + std::this_thread::yield(); + } +} + +TEST(NPUExecutorTests, DestructorWaitsForInFlightTaskCompletion) { + auto exec = intel_npu::make_executor("destroy_waits", 1, false, 200ms); + + std::mutex m; + std::condition_variable cv; + bool started = false; + bool release = false; + std::atomic finished{false}; + std::atomic destructorReturned{false}; + + exec->run([&] { + { + std::lock_guard lock(m); + started = true; + } + cv.notify_all(); + + std::unique_lock lock(m); + cv.wait(lock, [&] { + return release; + }); + finished = true; + }); + + ASSERT_TRUE(wait_until([&] { + std::lock_guard lock(m); + return started; + })); + + std::thread destroyer([&] { + exec.reset(); + destructorReturned = true; + }); + + // Do not use sleep_for to assert destructorReturned is still false — that + // is a racy negative assertion that fails on a loaded CI machine where the + // destroyer thread may not be scheduled within the sleep window. + // The deterministic guarantee is captured below: after destroyer.join(), + // finished must be true, proving the destructor waited for the task. + { + std::lock_guard lock(m); + release = true; + } + cv.notify_all(); + + destroyer.join(); + EXPECT_TRUE(finished.load()); + EXPECT_TRUE(destructorReturned.load()); +} + +TEST(NPUExecutorTests, AdaptiveModeWithZeroBaselineRunsOffCallerThread) { + auto exec = intel_npu::make_executor("adaptive_zero_baseline", 0, true, 200ms); + + const auto callerId = std::this_thread::get_id(); + std::thread::id workerId{}; + std::promise done; + auto doneFuture = done.get_future(); + + exec->run([&] { + workerId = std::this_thread::get_id(); + done.set_value(); + }); + + doneFuture.wait(); + EXPECT_NE(workerId, std::thread::id{}); + EXPECT_NE(workerId, callerId); +} + +TEST(NPUExecutorTests, ReentrantSubmissionFromWorkerDoesNotDeadlock) { + auto exec = intel_npu::make_executor("reentrant_submit", 1, false, 200ms); + + std::mutex sequenceMutex; + std::vector sequence; + + std::promise parentDone; + std::promise childDone; + auto parentFuture = parentDone.get_future(); + auto childFuture = childDone.get_future(); + + exec->run([&] { + { + std::lock_guard lock(sequenceMutex); + sequence.emplace_back("parent_start"); + } + + exec->run([&] { + { + std::lock_guard lock(sequenceMutex); + sequence.emplace_back("child"); + } + childDone.set_value(); + }); + + { + std::lock_guard lock(sequenceMutex); + sequence.emplace_back("parent_end"); + } + parentDone.set_value(); + }); + + parentFuture.wait(); + childFuture.wait(); + + ASSERT_EQ(sequence.size(), 3U); + EXPECT_EQ(sequence[0], "parent_start"); + EXPECT_EQ(sequence[1], "parent_end"); + EXPECT_EQ(sequence[2], "child"); +} + +TEST(NPUExecutorTests, StressManyProducersNoTaskLoss) { + auto exec = intel_npu::make_executor("stress_many_submitters", 4, false, 200ms); + + constexpr int producers = 4; + constexpr int tasksPerProducer = 80; + constexpr int expectedSuccessesPerProducer = tasksPerProducer; + + std::atomic successfulTasks{0}; + + std::vector submitters; + submitters.reserve(producers); + + for (int submitterIdx = 0; submitterIdx < producers; ++submitterIdx) { + submitters.emplace_back([&] { + for (int i = 0; i < tasksPerProducer; ++i) { + exec->run([&] { + successfulTasks.fetch_add(1); + }); + } + }); + } + + for (auto& th : submitters) { + th.join(); + } + + ASSERT_TRUE(wait_until([&] { + return successfulTasks.load() == producers * expectedSuccessesPerProducer; + })); + + EXPECT_EQ(successfulTasks.load(), producers * expectedSuccessesPerProducer); +} + +TEST(NPUExecutorTests, InlineModeSwallowsCustomExceptionType) { + class CustomDeferredError final : public std::runtime_error { + public: + explicit CustomDeferredError(const std::string& whatArg) : std::runtime_error(whatArg) {} + }; + + auto exec = intel_npu::make_executor("typed_exception", 0, false, 200ms); + + std::atomic taskRan{false}; + EXPECT_NO_THROW(exec->run([&] { + taskRan = true; + throw CustomDeferredError("typed_error"); + })); + + EXPECT_TRUE(taskRan.load()); +} + +TEST(NPUExecutorTests, AdaptiveModeTinyIdleTimeoutRemainsStable) { + auto exec = intel_npu::make_executor("tiny_idle_timeout", 1, true, 1ms); + + constexpr int totalTasks = 200; + std::atomic completed{0}; + + for (int i = 0; i < totalTasks; ++i) { + exec->run([&, i] { + if (i % 3 == 0) { + std::this_thread::yield(); + } + completed.fetch_add(1); + }); + } + + ASSERT_TRUE(wait_until([&] { + return completed.load() == totalTasks; + })); +} + +TEST(NPUExecutorTests, ResettingOneOwnerDoesNotStopOtherOwners) { + auto ownerA = intel_npu::make_executor("shared_owner_reset", 1, false, 200ms); + auto ownerB = ownerA; + + std::thread releaser([&] { + ownerA.reset(); + }); + releaser.join(); + + std::atomic ran{false}; + ownerB->run([&] { + ran = true; + }); + + ASSERT_TRUE(wait_until([&] { + return ran.load(); + })); +} + +TEST(NPUExecutorTests, AsyncInferRequestPipelineSuccessCallback) { + auto state = std::make_shared(); + auto compiledModel = std::make_shared(state); + auto asyncRequest = compiledModel->create_infer_request(); + + std::atomic callbackCalled{false}; + std::atomic callbackHadException{true}; + + asyncRequest->set_callback([&](std::exception_ptr ex) { + callbackCalled = true; + callbackHadException = (ex != nullptr); + }); + + asyncRequest->start_async(); + asyncRequest->wait(); + + EXPECT_TRUE(callbackCalled.load()); + EXPECT_FALSE(callbackHadException.load()); + EXPECT_EQ(state->inferCalls.load(), 1); +} + +TEST(NPUExecutorTests, AsyncInferRequestPipelineFailureCallback) { + auto state = std::make_shared(); + auto compiledModel = std::make_shared(state); + auto asyncRequest = compiledModel->create_infer_request(); + + state->failInfer = true; + + std::atomic callbackCalled{false}; + std::atomic callbackHadException{false}; + + asyncRequest->set_callback([&](std::exception_ptr ex) { + callbackCalled = true; + callbackHadException = (ex != nullptr); + }); + + asyncRequest->start_async(); + ASSERT_THROW(asyncRequest->wait(), std::exception); + + ASSERT_TRUE(wait_until([&] { + return callbackCalled.load(); + })); + EXPECT_TRUE(callbackHadException.load()); + EXPECT_EQ(state->inferCalls.load(), 1); +} + +} // namespace diff --git a/src/plugins/intel_npu/tests/unit/npu/metadata/metadata_text.cpp b/src/plugins/intel_npu/tests/unit/npu/metadata/metadata_text.cpp new file mode 100644 index 000000000000..0ff480b41d1b --- /dev/null +++ b/src/plugins/intel_npu/tests/unit/npu/metadata/metadata_text.cpp @@ -0,0 +1,103 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "metadata_text.hpp" + +namespace { + +const auto make_key_value_field = [](std::string_view key, std::string_view val) -> std::string { + return std::string(key) + '=' + std::string(val); +}; + +const std::vector inputs = { + {make_key_value_field(MetadataTextKeys::META, "2.0") + ";" + make_key_value_field(MetadataTextKeys::OV, "2026.1.0"), + true}, + {make_key_value_field(MetadataTextKeys::META, "2.1") + ";" + + make_key_value_field(MetadataTextKeys::OV, "2026.1.0") + ";" + + make_key_value_field(MetadataTextKeys::WS_INITS, "1"), + true}, + {make_key_value_field(MetadataTextKeys::META, "2.2") + ";" + + make_key_value_field(MetadataTextKeys::OV, "2026.1.0") + ";" + + make_key_value_field(MetadataTextKeys::BATCH, "4"), + true}, + {make_key_value_field(MetadataTextKeys::META, "2.2") + ";" + + make_key_value_field(MetadataTextKeys::OV, "2026.1.0") + ";" + + make_key_value_field(MetadataTextKeys::BATCH, "4") + ";" + + make_key_value_field(MetadataTextKeys::WS_INITS, "1"), + true}, + {make_key_value_field(MetadataTextKeys::META, "2.2") + ";" + + make_key_value_field(MetadataTextKeys::OV, "2026.1.0") + ";" + + make_key_value_field(MetadataTextKeys::BATCH, "-1"), + false}, + // order independent: metadata version field is not first + {make_key_value_field(MetadataTextKeys::OV, "2026.1.0") + ";" + make_key_value_field(MetadataTextKeys::META, "2.0"), + true}, + // optional (unknown) fields are ignored + {make_key_value_field(MetadataTextKeys::META, "2.0") + ";" + + make_key_value_field(MetadataTextKeys::OV, "2026.1.0") + ";{future_field=FUTURE_VAL}", + true}, + {make_key_value_field(MetadataTextKeys::META, "2.0") + ";" + + make_key_value_field(MetadataTextKeys::OV, "2026.1.0") + ";future_field=FUTURE_VAL", + false}, + {"", false}, + {make_key_value_field(MetadataTextKeys::OV, "2026.1.0"), false}, + // metadata version malformed + {make_key_value_field(MetadataTextKeys::META, "20") + ";" + make_key_value_field(MetadataTextKeys::OV, "2026.1.0"), + false}, + {make_key_value_field(MetadataTextKeys::META, "2X.0") + ";" + + make_key_value_field(MetadataTextKeys::OV, "2026.1.0"), + false}, + {make_key_value_field(MetadataTextKeys::META, "2.0X") + ";" + + make_key_value_field(MetadataTextKeys::OV, "2026.1.0"), + false}, + {make_key_value_field(MetadataTextKeys::META, "2.0.1") + ";" + + make_key_value_field(MetadataTextKeys::OV, "2026.1.0"), + false}, + // unsupported metadata major version + {make_key_value_field(MetadataTextKeys::META, std::to_string(CURRENT_METADATA_MAJOR_VERSION + 1) + ".0") + ";" + + make_key_value_field(MetadataTextKeys::OV, "2026.1.0"), + false}, + // minor version exceeds current maximum + {make_key_value_field(MetadataTextKeys::META, "2.99") + ";" + + make_key_value_field(MetadataTextKeys::OV, "2026.1.0"), + false}, + {make_key_value_field(MetadataTextKeys::META, "2.0"), false}, + {make_key_value_field(MetadataTextKeys::META, "2.1") + ";" + + make_key_value_field(MetadataTextKeys::OV, "2026.1.0") + ";" + + make_key_value_field(MetadataTextKeys::WS_INITS, "0"), + false}, + // compatibility descriptor value is not bracket-enclosed + {make_key_value_field(MetadataTextKeys::META, "2.6") + ";" + + make_key_value_field(MetadataTextKeys::OV, "2026.1.0") + ";" + + make_key_value_field(MetadataTextKeys::COMPAT_DESC, "platform=NPU3720"), + false}, + // unmatched closing bracket in value + {make_key_value_field(MetadataTextKeys::META, "2.0") + ";" + + make_key_value_field(MetadataTextKeys::OV, "2026.1.0") + ";bad=]VALUE", + false}, + // unclosed opening bracket in value + {make_key_value_field(MetadataTextKeys::META, "2.0") + ";" + + make_key_value_field(MetadataTextKeys::OV, "2026.1.0") + ";bad=[VALUE", + false}, + {make_key_value_field(MetadataTextKeys::META, "2.0") + ";" + make_key_value_field(MetadataTextKeys::OV, "2026.1"), + false}, + {make_key_value_field(MetadataTextKeys::META, "2.0") + ";" + make_key_value_field(MetadataTextKeys::OV, "20261"), + false}, + // too many ov version components + {make_key_value_field(MetadataTextKeys::META, "2.0") + ";" + + make_key_value_field(MetadataTextKeys::OV, "2026.1.0.0"), + false}, + {make_key_value_field(MetadataTextKeys::META, "2.6") + ";;" + + make_key_value_field(MetadataTextKeys::OV, "2026.1.0") + ";", + false}, + {"key=VALUE;key=VALUE2", false}, + {"notavalidformat", false}, +}; + +} // namespace + +INSTANTIATE_TEST_SUITE_P(FormatValidation, + MetadataTextTest, + ::testing::ValuesIn(inputs), + MetadataTextTest::getTestCaseName); diff --git a/src/plugins/intel_npu/tests/unit/npu/metadata/metadata_text.hpp b/src/plugins/intel_npu/tests/unit/npu/metadata/metadata_text.hpp new file mode 100644 index 000000000000..e861ee8c1a6c --- /dev/null +++ b/src/plugins/intel_npu/tests/unit/npu/metadata/metadata_text.hpp @@ -0,0 +1,172 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#pragma once + +#include + +#include + +#include "common_test_utils/test_assertions.hpp" +#include "metadata.hpp" +#include "metadata_wrappers.hpp" + +using namespace intel_npu; + +using MetadataUnitTests = ::testing::Test; + +namespace { + +std::string make_text_string(MetadataBase& meta) { + std::ostringstream stream; + meta.write_as_text(stream); + return stream.str(); +} + +} // namespace + +using MetadataHumanReadableTests = ::testing::Test; + +TEST_F(MetadataHumanReadableTests, minimalMetadata) { + auto meta = Metadata(0, CURRENT_OPENVINO_VERSION); + const auto text = make_text_string(meta); + + std::unique_ptr storedMeta; + OV_ASSERT_NO_THROW(storedMeta = read_as_text(text)); + + ASSERT_FALSE(storedMeta->get_init_sizes().has_value()); + ASSERT_FALSE(storedMeta->get_batch_size().has_value()); + ASSERT_FALSE(storedMeta->get_input_layouts().has_value()); + ASSERT_FALSE(storedMeta->get_output_layouts().has_value()); + ASSERT_FALSE(storedMeta->get_compiler_version().has_value()); +} + +TEST_F(MetadataHumanReadableTests, initSizes) { + const std::vector initSizes{16, 32, 64}; + auto meta = Metadata(0, CURRENT_OPENVINO_VERSION, initSizes); + const auto text = make_text_string(meta); + + std::unique_ptr storedMeta; + OV_ASSERT_NO_THROW(storedMeta = read_as_text(text)); + + ASSERT_TRUE(storedMeta->get_init_sizes().has_value()); +} + +TEST_F(MetadataHumanReadableTests, emptyInitSizes) { + auto meta = Metadata(0, CURRENT_OPENVINO_VERSION, std::vector{}); + const auto text = make_text_string(meta); + + std::unique_ptr storedMeta; + OV_ASSERT_NO_THROW(storedMeta = read_as_text(text)); + + ASSERT_FALSE(storedMeta->get_init_sizes().has_value()); +} + +TEST_F(MetadataHumanReadableTests, batchSize) { + const int64_t batchSize = 4; + auto meta = Metadata(0, CURRENT_OPENVINO_VERSION, std::nullopt, batchSize); + const auto text = make_text_string(meta); + + std::unique_ptr storedMeta; + OV_ASSERT_NO_THROW(storedMeta = read_as_text(text)); + + ASSERT_TRUE(storedMeta->get_batch_size().has_value()); + EXPECT_EQ(storedMeta->get_batch_size().value(), batchSize); +} + +TEST_F(MetadataHumanReadableTests, zeroBatchSize) { + auto meta = Metadata(0, CURRENT_OPENVINO_VERSION, std::nullopt, std::nullopt); + const auto text = make_text_string(meta); + + std::unique_ptr storedMeta; + OV_ASSERT_NO_THROW(storedMeta = read_as_text(text)); + + ASSERT_FALSE(storedMeta->get_batch_size().has_value()); +} + +TEST_F(MetadataHumanReadableTests, noLayouts) { + auto meta = Metadata(0, CURRENT_OPENVINO_VERSION, std::nullopt, std::nullopt, std::nullopt); + const auto text = make_text_string(meta); + + std::unique_ptr storedMeta; + OV_ASSERT_NO_THROW(storedMeta = read_as_text(text)); + + ASSERT_FALSE(storedMeta->get_input_layouts().has_value()); + ASSERT_FALSE(storedMeta->get_output_layouts().has_value()); +} + +TEST_F(MetadataHumanReadableTests, compatibilityDescriptor) { + const std::string compatDesc = "platform=NPU3720;tiles=2;something=ELSE"; + auto meta = Metadata(0, + CURRENT_OPENVINO_VERSION, + std::nullopt, + std::nullopt, + std::nullopt, + std::nullopt, + std::nullopt, + std::nullopt, + compatDesc); + const auto text = make_text_string(meta); + + std::unique_ptr storedMeta; + OV_ASSERT_NO_THROW(storedMeta = read_as_text(text)); + + ASSERT_TRUE(storedMeta->get_compatibility_descriptor().has_value()); + EXPECT_EQ(storedMeta->get_compatibility_descriptor().value(), compatDesc); +} + +TEST_F(MetadataHumanReadableTests, compilerVersion) { + const uint32_t compilerVersion = 0xCAFECAFE; + auto meta = Metadata(0, + CURRENT_OPENVINO_VERSION, + std::nullopt, + std::nullopt, + std::nullopt, + std::nullopt, + compilerVersion); + const auto text = make_text_string(meta); + + std::unique_ptr storedMeta; + OV_ASSERT_NO_THROW(storedMeta = read_as_text(text)); + + ASSERT_FALSE(storedMeta->get_compiler_version().has_value()); +} + +TEST_F(MetadataHumanReadableTests, allTextFields) { + const std::vector initSizes{10, 20, 30}; + const int64_t batchSize = 8; + const uint32_t compilerVersion = 0xDEADBEEF; + const std::string compatDesc = "platform=NPU3720;tiles=2;something=ELSE"; + auto meta = Metadata(0, + CURRENT_OPENVINO_VERSION, + initSizes, + batchSize, + std::nullopt, + std::nullopt, + compilerVersion, + std::nullopt, + compatDesc); + const auto text = make_text_string(meta); + + std::unique_ptr storedMeta; + OV_ASSERT_NO_THROW(storedMeta = read_as_text(text)); + + ASSERT_TRUE(storedMeta->get_init_sizes().has_value()); + ASSERT_TRUE(storedMeta->get_batch_size().has_value()); + EXPECT_EQ(storedMeta->get_batch_size().value(), batchSize); + ASSERT_FALSE(storedMeta->get_input_layouts().has_value()); + ASSERT_FALSE(storedMeta->get_output_layouts().has_value()); + ASSERT_FALSE(storedMeta->get_compiler_version().has_value()); + ASSERT_TRUE(storedMeta->get_compatibility_descriptor().has_value()); + EXPECT_EQ(storedMeta->get_compatibility_descriptor().value(), compatDesc); +} + +TEST_P(MetadataTextTest, Format) { + std::unique_ptr meta; + if (isValid) { + OV_ASSERT_NO_THROW(meta = ::read_as_text(compatibilityString)); + } else { + ASSERT_ANY_THROW(meta = ::read_as_text(compatibilityString)); + } +} diff --git a/src/plugins/intel_npu/tests/unit/npu/metadata_version.cpp b/src/plugins/intel_npu/tests/unit/npu/metadata/metadata_version.cpp similarity index 76% rename from src/plugins/intel_npu/tests/unit/npu/metadata_version.cpp rename to src/plugins/intel_npu/tests/unit/npu/metadata/metadata_version.cpp index b7a099bf58ce..34d26d1380d8 100644 --- a/src/plugins/intel_npu/tests/unit/npu/metadata_version.cpp +++ b/src/plugins/intel_npu/tests/unit/npu/metadata/metadata_version.cpp @@ -6,26 +6,12 @@ #include "common_test_utils/test_assertions.hpp" #include "metadata.hpp" -#include "openvino/core/version.hpp" +#include "metadata_wrappers.hpp" using namespace intel_npu; using MetadataUnitTests = ::testing::Test; -struct MetadataTest : Metadata { - MetadataTest(uint64_t blobSize, - const std::optional& ovVersion, - const std::optional>& initSizes = std::nullopt, - const std::optional batchSize = std::nullopt, - const std::optional>& inputLayouts = std::nullopt, - const std::optional>& outputLayouts = std::nullopt) - : Metadata(blobSize, ovVersion, initSizes, batchSize, inputLayouts, outputLayouts) {} - - void set_version(uint32_t newVersion) { - _version = newVersion; - } -}; - TEST_F(MetadataUnitTests, readUnversionedBlob) { std::stringstream blob("this_is an_unversioned bl0b"); @@ -154,6 +140,91 @@ TEST_F(MetadataUnitTests, writeAndReadCurrentMetadataFromBlobWithContentAllAttri } } +TEST_F(MetadataUnitTests, writeAndReadCurrentMetadataFromBlobWithCompatibilityDesc) { + uint64_t blobSize = 64; + const std::string compatDesc = "platform=NPU3720;tiles=2;etc=..."; + std::stringstream stream; + std::vector content(blobSize, 0); + stream.write(reinterpret_cast(content.data()), static_cast(blobSize)); + + auto meta = MetadataTest(blobSize, + CURRENT_OPENVINO_VERSION, + std::nullopt, + std::nullopt, + std::nullopt, + std::nullopt, + std::nullopt, + std::nullopt, + compatDesc); + OV_ASSERT_NO_THROW(meta.write(stream)); + + std::unique_ptr storedMeta; + OV_ASSERT_NO_THROW(storedMeta = read_metadata_from(stream)); + ASSERT_TRUE(storedMeta->get_blob_size() == blobSize); + ASSERT_TRUE(storedMeta->get_compatibility_descriptor().has_value()); + ASSERT_EQ(storedMeta->get_compatibility_descriptor().value(), compatDesc); + + stream.seekg(0, std::ios::beg); + size_t streamSize = MetadataBase::getFileSize(stream); + auto tensor = ov::Tensor(ov::element::u8, ov::Shape{streamSize}); + stream.read(tensor.data(), tensor.get_byte_size()); + OV_ASSERT_NO_THROW(storedMeta = read_metadata_from(tensor)); + ASSERT_TRUE(storedMeta->get_blob_size() == blobSize); + ASSERT_TRUE(storedMeta->get_compatibility_descriptor().has_value()); + ASSERT_EQ(storedMeta->get_compatibility_descriptor().value(), compatDesc); +} + +TEST_F(MetadataUnitTests, writeAndReadCurrentMetadataFromBlobWithEmptyCompatibilityDescriptor) { + uint64_t blobSize = 0; + std::stringstream stream; + + auto meta = MetadataTest(blobSize, + CURRENT_OPENVINO_VERSION, + std::nullopt, + std::nullopt, + std::nullopt, + std::nullopt, + std::nullopt, + std::nullopt, + std::string{}); + OV_ASSERT_NO_THROW(meta.write(stream)); + + std::unique_ptr storedMeta; + OV_ASSERT_NO_THROW(storedMeta = read_metadata_from(stream)); + ASSERT_FALSE(storedMeta->get_compatibility_descriptor().has_value()); + + stream.seekg(0, std::ios::beg); + size_t streamSize = MetadataBase::getFileSize(stream); + auto tensor = ov::Tensor(ov::element::u8, ov::Shape{streamSize}); + stream.read(tensor.data(), tensor.get_byte_size()); + OV_ASSERT_NO_THROW(storedMeta = read_metadata_from(tensor)); + ASSERT_FALSE(storedMeta->get_compatibility_descriptor().has_value()); +} + +TEST_F(MetadataUnitTests, compatibilityDescriptorLenExceedsTensorBounds) { + std::stringstream stream; + const std::string compatDesc = "platform=NPU3720;tiles=2;etc=..."; + auto meta = Metadata(0, + std::nullopt, + std::nullopt, + std::nullopt, + std::nullopt, + std::nullopt, + std::nullopt, + std::nullopt, + compatDesc); + meta.write(stream); + std::string blob = stream.str(); + + const size_t compatDescLenOffset = blob.size() - MAGIC_BYTES.size() - sizeof(uint64_t) - compatDesc.size() - sizeof(uint64_t); + const uint64_t badLen = compatDesc.size() + 0xFF; + std::memcpy(&blob[compatDescLenOffset], &badLen, sizeof(badLen)); + + auto tensor = ov::Tensor(ov::element::u8, ov::Shape{blob.size()}); + std::memcpy(tensor.data(), blob.data(), blob.size()); + ASSERT_ANY_THROW(read_metadata_from(tensor)); +} + TEST_F(MetadataUnitTests, writeAndReadInvalidMetadataVersion) { uint64_t blobSize = 0; std::stringstream stream; @@ -192,32 +263,6 @@ TEST_F(MetadataUnitTests, writeAndReadMetadataWithNewerMinorVersion) { ASSERT_ANY_THROW(storedMeta = read_metadata_from(tensor)); } -struct MetadataVersionTestFixture : Metadata, ::testing::TestWithParam { -public: - std::stringstream blob; - - void set_version(uint32_t newVersion) { - _version = newVersion; - } - - MetadataVersionTestFixture() : Metadata(0, std::nullopt) {} - - MetadataVersionTestFixture(uint64_t blobSize, std::optional ovVersion) - : Metadata(blobSize, ovVersion) {} - - void TestBody() override {} - - static std::string getTestCaseName(const testing::TestParamInfo& info); -}; - -std::string MetadataVersionTestFixture::getTestCaseName( - const testing::TestParamInfo& info) { - std::ostringstream result; - result << "major version=" << MetadataBase::get_major(info.param) - << ", minor version=" << MetadataBase::get_minor(info.param); - return result.str(); -} - TEST_P(MetadataVersionTestFixture, writeAndReadInvalidMetadataVersion) { uint32_t metaVersion = GetParam(); if (CURRENT_METADATA_MAJOR_VERSION == MetadataBase::get_major(metaVersion) && CURRENT_METADATA_MINOR_VERSION == 0) { diff --git a/src/plugins/intel_npu/tests/unit/npu/metadata/metadata_wrappers.hpp b/src/plugins/intel_npu/tests/unit/npu/metadata/metadata_wrappers.hpp new file mode 100644 index 000000000000..f29e3672c4fc --- /dev/null +++ b/src/plugins/intel_npu/tests/unit/npu/metadata/metadata_wrappers.hpp @@ -0,0 +1,77 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#pragma once + +#include + +#include "metadata.hpp" + +using namespace intel_npu; + +struct MetadataTest : Metadata { + MetadataTest(uint64_t blobSize, + const std::optional& ovVersion, + const std::optional>& initSizes = std::nullopt, + const std::optional& batchSize = std::nullopt, + const std::optional>& inputLayouts = std::nullopt, + const std::optional>& outputLayouts = std::nullopt, + const std::optional& compilerVersion = std::nullopt, + const std::optional& blobSizeAfterEncryption = std::nullopt, + const std::optional& compatibilityDescriptor = std::nullopt) + : Metadata(blobSize, + ovVersion, + initSizes, + batchSize, + inputLayouts, + outputLayouts, + compilerVersion, + blobSizeAfterEncryption, + compatibilityDescriptor) {} + + void set_version(uint32_t newVersion) { + _version = newVersion; + } +}; + +struct MetadataVersionTestFixture : Metadata, ::testing::TestWithParam { +public: + void set_version(uint32_t newVersion) { + _version = newVersion; + } + + MetadataVersionTestFixture() : Metadata(0, std::nullopt) {} + + MetadataVersionTestFixture(uint64_t blobSize, std::optional ovVersion) + : Metadata(blobSize, ovVersion) {} + + void TestBody() override {} + + static std::string getTestCaseName(const testing::TestParamInfo& info) { + std::ostringstream result; + result << "majorVersion=" << MetadataBase::get_major(info.param) + << "_minorVersion=" << MetadataBase::get_minor(info.param); + return result.str(); + } + + std::stringstream blob; +}; + +struct MetadataTextTest : Metadata, ::testing::TestWithParam> { +public: + MetadataTextTest() : Metadata(0, std::nullopt) {} + + void SetUp() override { + isValid = std::get<1>(GetParam()); + compatibilityString = std::get<0>(GetParam()); + } + + static std::string getTestCaseName(const testing::TestParamInfo& info) { + return "compatibilityString=\"" + std::get<0>(info.param) + + "\"_isValid=" + (std::get<1>(info.param) ? "true" : "false"); + } + + std::string compatibilityString; + bool isValid; +}; diff --git a/src/plugins/intel_npu/tests/unit/npuw/accuracy_checked.cpp b/src/plugins/intel_npu/tests/unit/npuw/accuracy_checked.cpp new file mode 100644 index 000000000000..708c7bdc166d --- /dev/null +++ b/src/plugins/intel_npu/tests/unit/npuw/accuracy_checked.cpp @@ -0,0 +1,505 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include + +#include +#include +#include + +#include "v1/elements/accuracy_checked.hpp" +#include "v1/elements/failsafe.hpp" +#include "openvino/opsets/opset10.hpp" +#include "openvino/runtime/make_tensor.hpp" +#include "openvino/runtime/properties.hpp" + +namespace { + +// --------------------------------------------------------------------------- +// Shared test infrastructure (mirrored from failsafe.cpp) +// --------------------------------------------------------------------------- + +std::shared_ptr make_test_model() { + auto input = std::make_shared(ov::element::f32, ov::Shape{1}); + input->set_friendly_name("input"); + auto zero = ov::opset10::Constant::create(ov::element::f32, ov::Shape{1}, {0.f}); + auto add = std::make_shared(input, zero); + add->set_friendly_name("output_add"); + auto result = std::make_shared(add); + result->set_friendly_name("output"); + return std::make_shared(ov::ResultVector{result}, ov::ParameterVector{input}, "AccTestModel"); +} + +class NullPlugin final : public ov::IPlugin { +public: + std::shared_ptr compile_model(const std::shared_ptr&, + const ov::AnyMap&) const override { return {}; } + std::shared_ptr compile_model(const std::shared_ptr&, + const ov::AnyMap&, + const ov::SoPtr&) const override { return {}; } + std::shared_ptr import_model(std::istream&, const ov::AnyMap&) const override { return {}; } + std::shared_ptr import_model(std::istream&, + const ov::SoPtr&, + const ov::AnyMap&) const override { return {}; } + std::shared_ptr import_model(const ov::Tensor&, const ov::AnyMap&) const override { return {}; } + std::shared_ptr import_model(const ov::Tensor&, + const ov::SoPtr&, + const ov::AnyMap&) const override { return {}; } + ov::SupportedOpsMap query_model(const std::shared_ptr&, const ov::AnyMap&) const override { return {}; } + void set_property(const ov::AnyMap&) override {} + ov::Any get_property(const std::string&, const ov::AnyMap&) const override { return {}; } + ov::SoPtr create_context(const ov::AnyMap&) const override { return {}; } + ov::SoPtr get_default_context(const ov::AnyMap&) const override { return {}; } +}; + +// Compiled model that adds a fixed bias to its single input and writes the +// result to its single output. Optionally tracks which events occurred. +struct ModelState { + std::string name; + std::vector* events = nullptr; + float output_bias = 0.f; + float last_input_value = 0.f; + float last_output_value = 0.f; + // Optional glitch: from call number glitch_at_call onwards, output_bias is + // replaced with glitch_bias. Set glitch_at_call <= 0 to disable. + int infer_count = 0; + int glitch_at_call = 0; + float glitch_bias = 0.f; +}; + +class TestCompiledModel; + +class TestInferRequest final : public ov::ISyncInferRequest { +public: + TestInferRequest(std::shared_ptr cm, std::shared_ptr state); + + void infer() override; + ov::SoPtr get_tensor(const ov::Output& port) const override { + return ov::ISyncInferRequest::get_tensor(port); + } + void set_tensor(const ov::Output& port, const ov::SoPtr& tensor) override { + ov::ISyncInferRequest::set_tensor(port, tensor); + } + void check_tensors() const override {} + std::vector> query_state() const override { return {}; } + std::vector get_profiling_info() const override { return {}; } + +private: + std::shared_ptr m_state; +}; + +class TestCompiledModel final : public ov::ICompiledModel { +public: + TestCompiledModel(const std::shared_ptr& model, + const std::shared_ptr& plugin, + std::shared_ptr state) + : ov::ICompiledModel(model, plugin), m_model(model), m_state(std::move(state)) {} + + void export_model(std::ostream&) const override {} + std::shared_ptr get_runtime_model() const override { return m_model; } + void set_property(const ov::AnyMap&) override {} + ov::Any get_property(const std::string& name) const override { + if (name == ov::execution_devices.name()) { + return std::vector{m_state->name}; + } + OPENVINO_THROW("Unsupported property: ", name); + } + std::shared_ptr create_sync_infer_request() const override { + if (m_state->events) { + m_state->events->push_back("create-request:" + m_state->name); + } + auto self = std::static_pointer_cast(shared_from_this()); + return std::make_shared(std::move(self), m_state); + } + std::shared_ptr create_infer_request() const override { + return std::make_shared(create_sync_infer_request(), + get_task_executor(), + get_callback_executor()); + } + +private: + std::shared_ptr m_model; + std::shared_ptr m_state; +}; + +TestInferRequest::TestInferRequest(std::shared_ptr cm, std::shared_ptr state) + : ov::ISyncInferRequest(cm), m_state(std::move(state)) { + for (const auto& input : get_compiled_model()->inputs()) { + ov::ISyncInferRequest::set_tensor( + input, ov::get_tensor_impl(ov::Tensor(input.get_element_type(), input.get_shape()))); + } + for (const auto& output : get_compiled_model()->outputs()) { + ov::ISyncInferRequest::set_tensor( + output, ov::get_tensor_impl(ov::Tensor(output.get_element_type(), output.get_shape()))); + } +} + +void TestInferRequest::infer() { + if (m_state->events) { + m_state->events->push_back("infer:" + m_state->name); + } + ++m_state->infer_count; + const float bias = (m_state->glitch_at_call > 0 && m_state->infer_count >= m_state->glitch_at_call) + ? m_state->glitch_bias + : m_state->output_bias; + const auto& input_port = get_compiled_model()->inputs().front(); + const auto& output_port = get_compiled_model()->outputs().front(); + const auto& in_tensor = ov::ISyncInferRequest::get_tensor(input_port); + const auto& out_tensor = ov::ISyncInferRequest::get_tensor(output_port); + m_state->last_input_value = in_tensor->data()[0]; + m_state->last_output_value = m_state->last_input_value + bias; + out_tensor->data()[0] = m_state->last_output_value; +} + +// Helper to build an ov::SoPtr backed by a TestCompiledModel. +ov::SoPtr make_test_compiled_model(const std::shared_ptr& model, + const std::shared_ptr& plugin, + std::shared_ptr state) { + return {std::make_shared(model, plugin, std::move(state)), {}}; +} + +// A checker that passes when |actual - reference| <= threshold for a scalar f32 tensor. +ov::npuw::accuracy_checked::CompiledModel::Checker make_threshold_checker(float threshold) { + return [threshold](const ov::SoPtr& actual, + const ov::SoPtr& reference) -> bool { + const float a = actual->data()[0]; + const float r = reference->data()[0]; + return std::abs(a - r) <= threshold; + }; +} + +} // namespace + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +TEST(AccuracyCheckedCompiledModelTest, NullRefReturnsMainUnwrapped) { + auto model = make_test_model(); + auto plugin = std::make_shared(); + auto main_state = std::make_shared(ModelState{"main", nullptr, 0.f}); + auto main_cm = make_test_compiled_model(model, plugin, main_state); + + auto so = ov::npuw::accuracy_checked::CompiledModel::create(model, plugin, main_cm, {}, make_threshold_checker(0.f)); + + // When ref is null create() must return the unwrapped main model. + EXPECT_EQ(std::dynamic_pointer_cast(so._ptr), nullptr); + ASSERT_NE(so._ptr, nullptr); +} + +TEST(AccuracyCheckedCompiledModelTest, AccurateInferencePassesThrough) { + auto model = make_test_model(); + auto plugin = std::make_shared(); + std::vector events; + + auto main_state = std::make_shared(ModelState{"main", &events, 10.f}); + auto ref_state = std::make_shared(ModelState{"ref", &events, 10.f}); // same bias → same output + + auto main_cm = make_test_compiled_model(model, plugin, main_state); + auto ref_cm = make_test_compiled_model(model, plugin, ref_state); + + auto so = ov::npuw::accuracy_checked::CompiledModel::create( + model, plugin, main_cm, ref_cm, make_threshold_checker(0.1f)); + auto compiled = std::dynamic_pointer_cast(so._ptr); + ASSERT_NE(compiled, nullptr); + + auto request = compiled->create_sync_infer_request(); + auto input = ov::get_tensor_impl(ov::Tensor(ov::element::f32, ov::Shape{1})); + input->data()[0] = 5.f; + request->set_tensor(model->inputs().front(), input); + + ASSERT_NO_THROW(request->infer()); + + // Both main and ref should have been inferred. + EXPECT_EQ(main_state->last_input_value, 5.f); + EXPECT_EQ(ref_state->last_input_value, 5.f); + // Output = input + bias = 5 + 10 = 15. + EXPECT_FLOAT_EQ(request->get_tensor(model->outputs().front())->data()[0], 15.f); + + // Model should NOT have switched. + EXPECT_FALSE(compiled->has_switched_to_reference()); +} + +TEST(AccuracyCheckedCompiledModelTest, InaccurateInferenceSwitchesToReference) { + auto model = make_test_model(); + auto plugin = std::make_shared(); + std::vector events; + + // main bias=10, ref bias=11 → difference=1 > threshold=0.5 → fail + auto main_state = std::make_shared(ModelState{"main", &events, 10.f}); + auto ref_state = std::make_shared(ModelState{"ref", &events, 11.f}); + + auto main_cm = make_test_compiled_model(model, plugin, main_state); + auto ref_cm = make_test_compiled_model(model, plugin, ref_state); + + auto so = ov::npuw::accuracy_checked::CompiledModel::create( + model, plugin, main_cm, ref_cm, make_threshold_checker(0.5f)); + auto compiled = std::dynamic_pointer_cast(so._ptr); + ASSERT_NE(compiled, nullptr); + + auto request = compiled->create_sync_infer_request(); + auto input = ov::get_tensor_impl(ov::Tensor(ov::element::f32, ov::Shape{1})); + input->data()[0] = 3.f; + request->set_tensor(model->inputs().front(), input); + + ASSERT_NO_THROW(request->infer()); + + // Output should be the reference value (3 + 11 = 14), copied into the main output buffer. + EXPECT_FLOAT_EQ(request->get_tensor(model->outputs().front())->data()[0], 14.f); + EXPECT_TRUE(compiled->has_switched_to_reference()); +} + +TEST(AccuracyCheckedCompiledModelTest, PermanentSwitchSkipsMainOnSubsequentInfers) { + auto model = make_test_model(); + auto plugin = std::make_shared(); + std::vector events; + + auto main_state = std::make_shared(ModelState{"main", &events, 10.f}); + auto ref_state = std::make_shared(ModelState{"ref", &events, 11.f}); + + auto main_cm = make_test_compiled_model(model, plugin, main_state); + auto ref_cm = make_test_compiled_model(model, plugin, ref_state); + + auto so = ov::npuw::accuracy_checked::CompiledModel::create( + model, plugin, main_cm, ref_cm, make_threshold_checker(0.5f)); + auto compiled = std::dynamic_pointer_cast(so._ptr); + auto request = compiled->create_sync_infer_request(); + + auto input = ov::get_tensor_impl(ov::Tensor(ov::element::f32, ov::Shape{1})); + input->data()[0] = 1.f; + request->set_tensor(model->inputs().front(), input); + + // First infer triggers the switch. + ASSERT_NO_THROW(request->infer()); + ASSERT_TRUE(compiled->has_switched_to_reference()); + + events.clear(); // Reset event log. + + input->data()[0] = 2.f; + ASSERT_NO_THROW(request->infer()); + + // After permanent switch the main model must NOT be invoked. + for (const auto& ev : events) { + EXPECT_NE(ev, "infer:main") << "main should not be inferred after switch"; + } + // Reference should have been inferred with the new input. + EXPECT_FLOAT_EQ(ref_state->last_input_value, 2.f); + EXPECT_FLOAT_EQ(request->get_tensor(model->outputs().front())->data()[0], 2.f + 11.f); +} + +TEST(AccuracyCheckedCompiledModelTest, UserOutputBufferReceivesReferenceValueOnSwitch) { + auto model = make_test_model(); + auto plugin = std::make_shared(); + + auto main_state = std::make_shared(ModelState{"main", nullptr, 10.f}); + auto ref_state = std::make_shared(ModelState{"ref", nullptr, 20.f}); + + auto main_cm = make_test_compiled_model(model, plugin, main_state); + auto ref_cm = make_test_compiled_model(model, plugin, ref_state); + + auto so = ov::npuw::accuracy_checked::CompiledModel::create( + model, plugin, main_cm, ref_cm, make_threshold_checker(0.5f)); + auto compiled = std::dynamic_pointer_cast(so._ptr); + auto request = compiled->create_sync_infer_request(); + + auto input = ov::get_tensor_impl(ov::Tensor(ov::element::f32, ov::Shape{1})); + auto output = ov::get_tensor_impl(ov::Tensor(ov::element::f32, ov::Shape{1})); + input->data()[0] = 4.f; + output->data()[0] = -1.f; // sentinel + request->set_tensor(model->inputs().front(), input); + request->set_tensor(model->outputs().front(), output); + + ASSERT_NO_THROW(request->infer()); + + // The user-provided output tensor must hold the reference result (4 + 20 = 24). + EXPECT_FLOAT_EQ(output->data()[0], 24.f); + // And get_tensor() must return the same object. + EXPECT_EQ(request->get_tensor(model->outputs().front())._ptr, output._ptr); +} + +TEST(AccuracyCheckedCompiledModelTest, NewRequestFromSwitchedModelStartsOnReference) { + auto model = make_test_model(); + auto plugin = std::make_shared(); + std::vector events; + + auto main_state = std::make_shared(ModelState{"main", &events, 10.f}); + auto ref_state = std::make_shared(ModelState{"ref", &events, 20.f}); + + auto main_cm = make_test_compiled_model(model, plugin, main_state); + auto ref_cm = make_test_compiled_model(model, plugin, ref_state); + + auto so = ov::npuw::accuracy_checked::CompiledModel::create( + model, plugin, main_cm, ref_cm, make_threshold_checker(0.5f)); + auto compiled = std::dynamic_pointer_cast(so._ptr); + + // Trigger switch via first request. + { + auto req1 = compiled->create_sync_infer_request(); + auto in1 = ov::get_tensor_impl(ov::Tensor(ov::element::f32, ov::Shape{1})); + in1->data()[0] = 1.f; + req1->set_tensor(model->inputs().front(), in1); + req1->infer(); + ASSERT_TRUE(compiled->has_switched_to_reference()); + } + + events.clear(); + + // A second request created after the switch should use reference directly. + auto req2 = compiled->create_sync_infer_request(); + auto in2 = ov::get_tensor_impl(ov::Tensor(ov::element::f32, ov::Shape{1})); + in2->data()[0] = 5.f; + req2->set_tensor(model->inputs().front(), in2); + ASSERT_NO_THROW(req2->infer()); + + for (const auto& ev : events) { + EXPECT_NE(ev, "infer:main") << "new request after switch must not use main"; + } + EXPECT_FLOAT_EQ(ref_state->last_input_value, 5.f); + EXPECT_FLOAT_EQ(req2->get_tensor(model->outputs().front())->data()[0], 5.f + 20.f); +} + +TEST(AccuracyCheckedCompiledModelTest, ChainedWithFailsafeModel) { + auto model = make_test_model(); + auto plugin = std::make_shared(); + std::vector events; + + // Failsafe inner layer: NPU fails, falls back to CPU. + auto npu_state = std::make_shared(ModelState{"NPU", &events, 10.f}); + auto cpu_state = std::make_shared(ModelState{"CPU", &events, 10.f}); + + auto failsafe_factory = [&](const std::string& device) -> ov::SoPtr { + if (device == "NPU") { + OPENVINO_THROW("NPU not available"); + } + if (device == "CPU") { + return make_test_compiled_model(model, plugin, cpu_state); + } + OPENVINO_THROW("Unknown device: ", device); + }; + + auto failsafe_cm = ov::npuw::failsafe::CompiledModel::create(model, plugin, {"NPU", "CPU"}, failsafe_factory); + + // Reference is a separate CPU model with a different bias (simulate inaccuracy). + auto ref_state = std::make_shared(ModelState{"ref_cpu", &events, 20.f}); + auto ref_cm = make_test_compiled_model(model, plugin, ref_state); + + auto so = ov::npuw::accuracy_checked::CompiledModel::create( + model, plugin, failsafe_cm, ref_cm, make_threshold_checker(0.5f)); + auto acc_compiled = std::dynamic_pointer_cast(so._ptr); + ASSERT_NE(acc_compiled, nullptr); + + auto request = acc_compiled->create_sync_infer_request(); + auto input = ov::get_tensor_impl(ov::Tensor(ov::element::f32, ov::Shape{1})); + input->data()[0] = 2.f; + request->set_tensor(model->inputs().front(), input); + + // Failsafe uses CPU (bias=10), ref uses ref_cpu (bias=20). diff=10 > 0.5 → switch. + ASSERT_NO_THROW(request->infer()); + + EXPECT_TRUE(acc_compiled->has_switched_to_reference()); + // Output should be ref result: 2 + 20 = 22. + EXPECT_FLOAT_EQ(request->get_tensor(model->outputs().front())->data()[0], 22.f); +} + +TEST(AccuracyCheckedCompiledModelTest, ExecutionDevicesReflectsActiveModel) { + auto model = make_test_model(); + auto plugin = std::make_shared(); + + auto main_state = std::make_shared(ModelState{"NPU", nullptr, 10.f}); + auto ref_state = std::make_shared(ModelState{"CPU", nullptr, 11.f}); + + auto main_cm = make_test_compiled_model(model, plugin, main_state); + auto ref_cm = make_test_compiled_model(model, plugin, ref_state); + + auto so = ov::npuw::accuracy_checked::CompiledModel::create( + model, plugin, main_cm, ref_cm, make_threshold_checker(0.5f)); + auto compiled = std::dynamic_pointer_cast(so._ptr); + ASSERT_NE(compiled, nullptr); + + // Before switch: reports main device. + auto devs_before = compiled->get_property(ov::execution_devices.name()).as>(); + EXPECT_EQ(devs_before, (std::vector{"NPU"})); + + // Trigger switch. + auto request = compiled->create_sync_infer_request(); + auto input = ov::get_tensor_impl(ov::Tensor(ov::element::f32, ov::Shape{1})); + input->data()[0] = 1.f; + request->set_tensor(model->inputs().front(), input); + request->infer(); + ASSERT_TRUE(compiled->has_switched_to_reference()); + + // After switch: reports reference device. + auto devs_after = compiled->get_property(ov::execution_devices.name()).as>(); + EXPECT_EQ(devs_after, (std::vector{"CPU"})); +} + +TEST(AccuracyCheckedCompiledModelTest, RepeatingBlockAccuracyFailsAtThirdCall) { + // In NPUW a function body (repeating block) is compiled once and its infer + // request pair (main + ref) is *reused* for every call-site invocation + // within a single forward pass. The AccuracyChecked::InferRequest wraps + // that pair and is called once per instance. The shared + // m_switched_to_reference flag ensures that once any instance detects an + // accuracy failure, all subsequent invocations automatically use reference. + // + // Scenario: main is accurate on calls 1 and 2, but "glitches" from call 3 + // onwards (output bias shifts by 1). Reference is always stable. + // Threshold = 0.5, so |glitch| = 1 triggers the permanent switch on call 3. + auto model = make_test_model(); + auto plugin = std::make_shared(); + std::vector events; + + auto main_state = std::make_shared(ModelState{"main", &events, 10.f}); + main_state->glitch_at_call = 3; + main_state->glitch_bias = 11.f; // diverges from ref by 1 on call 3+ + + auto ref_state = std::make_shared(ModelState{"ref", &events, 10.f}); + + auto main_cm = make_test_compiled_model(model, plugin, main_state); + auto ref_cm = make_test_compiled_model(model, plugin, ref_state); + + auto so = ov::npuw::accuracy_checked::CompiledModel::create( + model, plugin, main_cm, ref_cm, make_threshold_checker(0.5f)); + auto compiled = std::dynamic_pointer_cast(so._ptr); + ASSERT_NE(compiled, nullptr); + + // One AccuracyChecked::InferRequest reused for all N instances of the block. + auto request = compiled->create_sync_infer_request(); + auto input = ov::get_tensor_impl(ov::Tensor(ov::element::f32, ov::Shape{1})); + request->set_tensor(model->inputs().front(), input); + + // -- Forward pass 1, instance 1: main call #1, bias=10. Accurate. --------- + input->data()[0] = 1.f; + ASSERT_NO_THROW(request->infer()); + EXPECT_FALSE(compiled->has_switched_to_reference()); + EXPECT_FLOAT_EQ(request->get_tensor(model->outputs().front())->data()[0], 11.f); // 1+10 + + // -- Forward pass 1, instance 2: main call #2, bias=10. Accurate. --------- + input->data()[0] = 2.f; + ASSERT_NO_THROW(request->infer()); + EXPECT_FALSE(compiled->has_switched_to_reference()); + EXPECT_FLOAT_EQ(request->get_tensor(model->outputs().front())->data()[0], 12.f); // 2+10 + + // -- Forward pass 1, instance 3: main call #3, bias glitches to 11. + // main=3+11=14, ref=3+10=13, |14-13|=1 > 0.5 → permanent switch. ------ + input->data()[0] = 3.f; + ASSERT_NO_THROW(request->infer()); + EXPECT_TRUE(compiled->has_switched_to_reference()); + // Output must be the reference result (3+10=13), not the glitched main (14). + EXPECT_FLOAT_EQ(request->get_tensor(model->outputs().front())->data()[0], 13.f); + + // -- Forward pass 2: all instances use reference directly. ----------------- + events.clear(); + input->data()[0] = 10.f; + ASSERT_NO_THROW(request->infer()); // instance 1 + input->data()[0] = 20.f; + ASSERT_NO_THROW(request->infer()); // instance 2 + input->data()[0] = 30.f; + ASSERT_NO_THROW(request->infer()); // instance 3 + + for (const auto& ev : events) { + EXPECT_NE(ev, "infer:main") << "main must not be invoked after permanent reference switch"; + } + // Last output via reference: 30+10=40. + EXPECT_FLOAT_EQ(request->get_tensor(model->outputs().front())->data()[0], 40.f); +} diff --git a/src/plugins/intel_npu/tests/unit/npuw/device_routed_moe_transform_test.cpp b/src/plugins/intel_npu/tests/unit/npuw/device_routed_moe_transform_test.cpp index 6b254878186b..b71d5d6680f8 100644 --- a/src/plugins/intel_npu/tests/unit/npuw/device_routed_moe_transform_test.cpp +++ b/src/plugins/intel_npu/tests/unit/npuw/device_routed_moe_transform_test.cpp @@ -1,4 +1,4 @@ -// Copyright (C) 2026 Intel Corporation +// Copyright (C) 2018-2026 Intel Corporation // SPDX-License-Identifier: Apache-2.0 // @@ -15,10 +15,15 @@ /* * Test suite for Device-Routed MoE Transformation * - * Testing Strategy: - * - BasicTransformation: Verify Gather insertion in quantized weights and shape updates - * - MultiLayerMoE: Test independent transformation of multiple layers - * - AWQActivationMultiply: Test AWQ activation scaling support + * GPT-OSS tests (create_complete_moe_graph): + * - BasicTransformation: Gather insertion in quantized weights; shape updates + * - MultiLayerMoE: Independent transformation of multiple layers + * - AWQActivationMultiply: AWQ activation-scale Multiply support + * - NegativeNoSoftmax: Transformation skipped when Softmax is absent + * + * Qwen3 tests (create_qwen3_moe_graph): + * - Qwen3BasicTransformation: Gather insertion for Qwen3 SwiGLU expert + * - RouterBroadcastChainShapeUpdate: Reshape shape[0] updated after transpose replacement */ // Uncomment to save debug XML files during test execution @@ -283,8 +288,12 @@ std::shared_ptr create_complete_moe_graph(size_t num_experts = 32, auto output_multiply = std::make_shared(reshape2, router_scores_output); output_multiply->set_friendly_name("__module.model." + layer_id + "mlp.experts/aten::mul/Multiply"); - // Result - auto result = std::make_shared(output_multiply); + // ReduceSum over expert dim — required by detect_router_by_topology step 3. + auto reduce_axis = op::v0::Constant::create(element::i64, Shape{1}, {0}); + auto reduce_sum = std::make_shared(output_multiply, reduce_axis); + reduce_sum->set_friendly_name("__module.model." + layer_id + "mlp.experts/aten::sum/ReduceSum"); + + auto result = std::make_shared(reduce_sum); result->set_friendly_name(layer_id + "output"); return std::make_shared(ResultVector{result}, ParameterVector{shared_input}); @@ -539,4 +548,221 @@ TEST_F(DeviceRoutedMoETransformTest, NegativeNoSoftmax) { EXPECT_EQ(repeats_data[0], num_experts) << "Tile repeats should remain unchanged"; } +// ============================================================================ +// Qwen3 graph builder +// ============================================================================ + +// Qwen3-style MoE graph (decoding stage, token_count=1). +// Router: Input -> MatMul -> Softmax -> TopK -> ReduceSum -> Divide +// -> Scatter -> Transpose -> Reshape([N,1,1]) -> Unsqueeze -> [N,1,1,1] +// Expert: Input -> Tile -> Reshape([N,1,H]) -> gate/up/down MatMuls (SwiGLU) +// -> Reshape([N,1,1,H]) -> Multiply(router) -> ReduceSum +// Weights: Const(i4) -> Convert(f16) -> Multiply(scale) -> Convert(f32) -> MatMul +// 2 Gathers per MatMul (weight + scale) x 3 MatMuls = 6 total. +std::shared_ptr create_qwen3_moe_graph(size_t num_experts = 4, + int64_t k_value = 2, + size_t hidden_dim = 8, + size_t intermediate_dim = 4) { + auto input = std::make_shared(element::f32, Shape{1, hidden_dim}); + input->set_friendly_name("qwen3_input"); + + // --- Router --- + // NF4 weight chain: i4 [N,H] -> f16 -> Multiply(scale[N,1]) -> f32 -> MatMul + auto rw = op::v0::Constant::create(element::i4, + Shape{num_experts, hidden_dim}, + std::vector(num_experts * hidden_dim, 1)); + auto rw_f16 = std::make_shared(rw, element::f16); + auto rs = op::v0::Constant::create(element::f16, Shape{num_experts, 1}, std::vector(num_experts, 1.0f)); + auto rw_scaled = std::make_shared(rw_f16, rs); + auto rw_f32 = std::make_shared(rw_scaled, element::f32); + auto router_mm = std::make_shared(input, rw_f32, false, true); + + // Softmax -> TopK (Softmax BEFORE TopK, Qwen3 style) + auto softmax = std::make_shared(router_mm, 1); + auto k_c = op::v0::Constant::create(element::i64, Shape{}, {k_value}); + auto topk = + std::make_shared(softmax, k_c, -1, op::v11::TopK::Mode::MAX, op::v11::TopK::SortType::NONE); + + // Renormalize: ReduceSum(values) -> Divide + auto rs_ax = op::v0::Constant::create(element::i64, Shape{1}, {1}); + auto reduce_router = std::make_shared(topk->output(0), rs_ax, true); + auto divide = std::make_shared(topk->output(0), reduce_router); + + // Scatter back to full expert dimension + auto zero = op::v0::Constant::create(element::f32, Shape{}, {0.0f}); + auto bcast_shp = op::v0::Constant::create(element::i64, Shape{2}, std::vector{1LL, (int64_t)num_experts}); + auto broadcast = std::make_shared(zero, bcast_shp); + auto sc_ax = op::v0::Constant::create(element::i64, Shape{1}, {1}); + auto scatter = std::make_shared(broadcast, topk->output(1), divide, sc_ax); + + // Transpose [1,N] -> [N,1] + auto tp_ord = op::v0::Constant::create(element::i32, Shape{2}, {1, 0}); + auto transpose = std::make_shared(scatter, tp_ord); + + // Reshape [N,1] -> [N,1,1] (shape[0] = num_experts, fixed by transform_router_broadcast_chain) + auto r_shp = op::v0::Constant::create(element::i64, Shape{3}, std::vector{(int64_t)num_experts, 1LL, 1LL}); + auto router_reshape = std::make_shared(transpose, r_shp, false); + + // Unsqueeze dim 3: [N,1,1] -> [N,1,1,1] + auto us_ax = op::v0::Constant::create(element::i64, Shape{1}, {3}); + auto unsqueeze = std::make_shared(router_reshape, us_ax); + + // --- Expert --- + // Helper: build INT4 weight dequantization chain: Const(i4,[d0,d1,d2])->f16->Multiply(scale)->f32 + auto make_weight = [](size_t d0, size_t d1, size_t d2) { + auto w = op::v0::Constant::create(element::i4, Shape{d0, d1, d2}, std::vector(d0 * d1 * d2, 1)); + auto wf = std::make_shared(w, element::f16); + auto sc = op::v0::Constant::create(element::f16, Shape{d0, d1, 1}, std::vector(d0 * d1, 1.0f)); + auto ws = std::make_shared(wf, sc); + return std::make_shared(ws, element::f32); + }; + + // Tile [1,H] -> [N,H] + auto tile_rep = op::v0::Constant::create(element::i64, Shape{2}, std::vector{(int64_t)num_experts, 1LL}); + auto tile = std::make_shared(input, tile_rep); + + // Reshape [N,H] -> [N,1,H] (shape[0] = num_experts) + auto rs1_shp = op::v0::Constant::create(element::i64, + Shape{3}, + std::vector{(int64_t)num_experts, 1LL, (int64_t)hidden_dim}); + auto reshape1 = std::make_shared(tile, rs1_shp, false); + + // Gate: [N,I,H] weights -> [N,1,H] x [N,I,H]^T -> [N,1,I] -> Swish + auto gate_w = make_weight(num_experts, intermediate_dim, hidden_dim); + auto matmul_gate = std::make_shared(reshape1, gate_w, false, true); + auto swish = std::make_shared(matmul_gate); + + // Up: [N,I,H] weights -> [N,1,H] x [N,I,H]^T -> [N,1,I] + auto up_w = make_weight(num_experts, intermediate_dim, hidden_dim); + auto matmul_up = std::make_shared(reshape1, up_w, false, true); + + // SwiGLU + auto swiglu = std::make_shared(swish, matmul_up); + + // Down: [N,H,I] weights -> [N,1,I] x [N,H,I]^T -> [N,1,H] + auto down_w = make_weight(num_experts, hidden_dim, intermediate_dim); + auto matmul_down = std::make_shared(swiglu, down_w, false, true); + + // Reshape [N,1,H] -> [N,1,1,H] (shape[0] = num_experts) + auto rs2_shp = op::v0::Constant::create(element::i64, + Shape{4}, + std::vector{(int64_t)num_experts, 1LL, 1LL, (int64_t)hidden_dim}); + auto reshape2 = std::make_shared(matmul_down, rs2_shp, false); + + // Output multiply: [N,1,1,H] * [N,1,1,1] -> [N,1,1,H] + auto output_multiply = std::make_shared(reshape2, unsqueeze); + + // ReduceSum over expert dim — required by detect_router_by_topology step 3. + auto final_ax = op::v0::Constant::create(element::i64, Shape{1}, {0}); + auto final_reduce = std::make_shared(output_multiply, final_ax); + + auto result = std::make_shared(final_reduce); + return std::make_shared(ResultVector{result}, ParameterVector{input}); +} + +// ============================================================================ +// Qwen3 tests +// ============================================================================ + +// Test 5: Qwen3 basic transformation +// Verifies 6 Gather nodes inserted, Tile repeats[0] and Reshape dim[0] updated to k_value. +TEST_F(DeviceRoutedMoETransformTest, Qwen3BasicTransformation) { + constexpr size_t num_experts = 4; + constexpr int64_t k_value = 2; + constexpr size_t hidden_dim = 8; + constexpr size_t intermediate_dim = 4; + + auto model = create_qwen3_moe_graph(num_experts, k_value, hidden_dim, intermediate_dim); + save_model(model, "qwen3_moe_basic_before"); + + EXPECT_EQ(count_nodes(model), 0u) << "No Gather before transformation"; + EXPECT_EQ(count_nodes(model), 1u) << "One Tile before transformation"; + + ov::pass::Manager manager; + manager.register_pass(); + manager.run_passes(model); + + EXPECT_NO_THROW(model->validate_nodes_and_infer_types()); + save_model(model, "qwen3_moe_basic_after"); + + // 2 Gathers per MatMul (weight + scale) x 3 MatMuls = 6 + auto gathers = find_gather_nodes(model); + EXPECT_EQ(gathers.size(), 6u) << "Expected 6 Gather nodes (weight+scale per gate/up/down MatMul)"; + + // Tile repeats[0] must be updated to k_value + for (const auto& node : model->get_ordered_ops()) { + if (auto tile = std::dynamic_pointer_cast(node)) { + auto rep = std::dynamic_pointer_cast(tile->input_value(1).get_node_shared_ptr()); + ASSERT_NE(rep, nullptr); + EXPECT_EQ(rep->cast_vector()[0], k_value) << "Tile repeats[0] should be updated to k=" << k_value; + } + } + + // All Reshape shape constants where dim[0] > 1 must be updated to k_value + for (const auto& node : model->get_ordered_ops()) { + if (auto reshape = std::dynamic_pointer_cast(node)) { + auto shp = std::dynamic_pointer_cast(reshape->input_value(1).get_node_shared_ptr()); + if (!shp) + continue; + auto d = shp->cast_vector(); + if (!d.empty() && d[0] > 1) { + EXPECT_EQ(d[0], k_value) << "Reshape expert dim should be updated to k=" << k_value; + } + } + } +} + +// Test 6: Router broadcast chain Reshape shape fix (Qwen3-specific) +// After transform_transpose replaces Scatter->Transpose with router_scores [1,k], +// transform_router_broadcast_chain must update Reshape shape[0] from num_experts to k_value. +TEST_F(DeviceRoutedMoETransformTest, RouterBroadcastChainShapeUpdate) { + constexpr size_t num_experts = 4; + constexpr int64_t k_value = 2; + constexpr size_t hidden_dim = 8; + constexpr size_t intermediate_dim = 4; + + auto model = create_qwen3_moe_graph(num_experts, k_value, hidden_dim, intermediate_dim); + + // Before: locate the 3-element Reshape [num_experts, 1, 1] in the router broadcast chain + auto find_router_reshape_shape = [&]() -> std::shared_ptr { + for (const auto& node : model->get_ordered_ops()) { + if (auto reshape = std::dynamic_pointer_cast(node)) { + auto shp = std::dynamic_pointer_cast(reshape->input_value(1).get_node_shared_ptr()); + if (!shp) + continue; + auto d = shp->cast_vector(); + if (d.size() == 3 && d[0] == (int64_t)num_experts && d[1] == 1 && d[2] == 1) + return shp; + } + } + return nullptr; + }; + + auto before_shp = find_router_reshape_shape(); + ASSERT_NE(before_shp, nullptr) << "Router broadcast Reshape [N,1,1] should exist before transform"; + EXPECT_EQ(before_shp->cast_vector()[0], (int64_t)num_experts); + + ov::pass::Manager manager; + manager.register_pass(); + manager.run_passes(model); + + EXPECT_NO_THROW(model->validate_nodes_and_infer_types()); + + // After: the Reshape in the chain must now have shape [k_value, 1, 1] + bool found = false; + for (const auto& node : model->get_ordered_ops()) { + if (auto reshape = std::dynamic_pointer_cast(node)) { + auto shp = std::dynamic_pointer_cast(reshape->input_value(1).get_node_shared_ptr()); + if (!shp) + continue; + auto d = shp->cast_vector(); + if (d.size() == 3 && d[0] == k_value && d[1] == 1 && d[2] == 1) { + found = true; + } + } + } + EXPECT_TRUE(found) << "Router broadcast Reshape shape[0] must be updated from " << num_experts + << " to k=" << k_value; +} + } // namespace diff --git a/src/plugins/intel_npu/tests/unit/npuw/dynamic_quantize_accuracy_test.cpp b/src/plugins/intel_npu/tests/unit/npuw/dynamic_quantize_accuracy_test.cpp index 8df3e8de40c7..d5c9bcdb2202 100644 --- a/src/plugins/intel_npu/tests/unit/npuw/dynamic_quantize_accuracy_test.cpp +++ b/src/plugins/intel_npu/tests/unit/npuw/dynamic_quantize_accuracy_test.cpp @@ -14,7 +14,7 @@ // Parameter(f16) -> Convert(f32) -> DynamicQuantize -> [q, scale, zp] Results // // 2. Dequantize model (applied when reading cached tokens for attention): -// Parameters(q_i8, scale_f32, zp_i8) -> dequant chain -> Result(f16) +// Parameters(q_storage, scale_f32, zp_storage) -> dequant chain -> Result(f16) // // Test cases: // RoundtripAccuracy - single fused roundtrip model via evaluate() @@ -53,10 +53,12 @@ #include "openvino/pass/manager.hpp" #include "openvino/runtime/intel_npu/properties.hpp" #include "ov_ops/dynamic_quantize.hpp" +#include "util.hpp" namespace { using namespace ov; +using DQMode = ov::npuw::util::DynamicQuantDecomposeMode; // ============================================================================ // Accuracy metrics @@ -79,7 +81,8 @@ AccuracyMetrics compute_metrics(const float* original, const float* reconstructe sum_sq_err += diff * diff; double abs_diff = std::abs(diff); sum_abs_err += abs_diff; - if (abs_diff > max_abs) max_abs = abs_diff; + if (abs_diff > max_abs) + max_abs = abs_diff; sum_sq_orig += static_cast(original[i]) * static_cast(original[i]); } @@ -97,7 +100,7 @@ AccuracyMetrics compute_metrics(const float* original, const float* reconstructe enum class DistributionKind { Uniform, GenGaussian }; struct DQTestParams { - int decompose_version; + DQMode decompose_mode; Shape shape; unsigned seed; DistributionKind dist_kind; @@ -107,16 +110,20 @@ struct DQTestParams { float ggd_alpha; float ggd_beta; // Quantization config overrides. - // quant_dt == element::dynamic → infer from decompose_version (u8 for v2, i8 otherwise) - element::Type quant_dt = element::dynamic; - bool is_symmetric = false; // true = symmetric (no ZP, 2 DQ outputs) - double threshold = 0.02; // rel-L2 pass threshold + // quant_dt == element::dynamic → infer from decompose mode (u8 for v2, i8 otherwise) + element::Type quant_dt = element::dynamic; + bool is_symmetric = false; // true = symmetric (no ZP, 2 DQ outputs) + double threshold = 0.02; // rel-L2 pass threshold }; +static int decompose_version(const DQTestParams& p) { + return static_cast(p.decompose_mode); +} + static element::Type resolve_quant_dt(const DQTestParams& p) { return (p.quant_dt != element::dynamic) ? p.quant_dt - : (p.decompose_version == 2 ? element::u8 : element::i8); + : (p.decompose_mode == DQMode::OnnxDynamicQuantizeLinear ? element::u8 : element::i8); } static std::string dist_to_token(DistributionKind kind) { @@ -164,17 +171,14 @@ static std::string sanitize_gtest_name(std::string name) { static std::string make_test_case_name(const DQTestParams& p) { std::ostringstream os; - os << "V" << p.decompose_version - << "_" << dist_to_token(p.dist_kind) - << "_" << (p.is_symmetric ? "Sym" : "Asym") - << "_" << type_to_token(resolve_quant_dt(p)); + os << "V" << decompose_version(p) << "_" << dist_to_token(p.dist_kind) << "_" + << (p.is_symmetric ? "Sym" : "Asym") << "_" << type_to_token(resolve_quant_dt(p)); if (p.dist_kind == DistributionKind::Uniform) { os << "_R" << float_to_token(p.uniform_min) << "_" << float_to_token(p.uniform_max); } else { - os << "_Mu" << float_to_token(p.ggd_mu) - << "_A" << float_to_token(p.ggd_alpha) - << "_B" << float_to_token(p.ggd_beta); + os << "_Mu" << float_to_token(p.ggd_mu) << "_A" << float_to_token(p.ggd_alpha) << "_B" + << float_to_token(p.ggd_beta); } os << "_S"; @@ -192,46 +196,44 @@ std::ostream& operator<<(std::ostream& os, const DQTestParams& p) { return os << make_test_case_name(p); } - // ============================================================================ // Shared helpers for DQ config and decompose pass // ============================================================================ -op::internal::DynamicQuantize::Attributes make_dq_config(const Shape& shape, - const DQTestParams& p) { +op::internal::DynamicQuantize::Attributes make_dq_config(const Shape& shape, const DQTestParams& p) { using QT = op::internal::DynamicQuantize::QuantizationType; // Resolve effective quantization dtype: explicit override beats version default. const element::Type quant_dt = resolve_quant_dt(p); op::internal::DynamicQuantize::Attributes config; - config.scale_dt = element::f32; + config.scale_dt = element::f32; config.quantization_dt = quant_dt; config.group_sizes = std::vector(shape.size(), 1); if (p.is_symmetric) { config.quantization_type = QT::Symmetric; - config.zp_dt = element::dynamic; // no zero-point + config.zp_dt = element::dynamic; // no zero-point } else { config.quantization_type = QT::Asymmetric; - config.zp_dt = quant_dt; + config.zp_dt = quant_dt; } return config; } -void run_decompose_pass(const std::shared_ptr& model, int decompose_version) { +void run_decompose_pass(const std::shared_ptr& model, DQMode decompose_mode) { pass::Manager manager("decompose_dq"); - switch (decompose_version) { - case 1: + switch (decompose_mode) { + case DQMode::HandcraftedSymmetricI8: manager.register_pass(); break; - case 2: + case DQMode::OnnxDynamicQuantizeLinear: manager.register_pass(); break; - case 3: + case DQMode::CompilerPatternI8: manager.register_pass(); break; default: - OPENVINO_THROW("Unknown decompose version: ", decompose_version); + OPENVINO_THROW("Unknown decompose mode: ", static_cast(decompose_mode)); } manager.run_passes(model); model->validate_nodes_and_infer_types(); @@ -244,8 +246,7 @@ void run_decompose_pass(const std::shared_ptr& model, int decompose_versi // The DynamicQuantize node is named with "key" so the decomposition pass // selects reduction_axis=3 (the key-cache embedding dimension). // ============================================================================ -std::shared_ptr build_roundtrip_model(const Shape& shape, - const DQTestParams& p) { +std::shared_ptr build_roundtrip_model(const Shape& shape, const DQTestParams& p) { auto input = std::make_shared(element::f16, shape); input->set_friendly_name("input"); @@ -276,13 +277,12 @@ std::shared_ptr build_roundtrip_model(const Shape& shape, auto result = std::make_shared(output_cvt); result->set_friendly_name("result_reconstructed"); - auto model = std::make_shared( - ResultVector{result}, - ParameterVector{input}, - "roundtrip_fp16_v" + std::to_string(p.decompose_version)); + auto model = std::make_shared(ResultVector{result}, + ParameterVector{input}, + "roundtrip_fp16_v" + std::to_string(decompose_version(p))); model->validate_nodes_and_infer_types(); - run_decompose_pass(model, p.decompose_version); + run_decompose_pass(model, p.decompose_mode); return model; } @@ -294,8 +294,7 @@ std::shared_ptr build_roundtrip_model(const Shape& shape, // failure here (which has been observed) would go unnoticed with a single // fused roundtrip model. // ============================================================================ -std::shared_ptr build_quantize_model(const Shape& shape, - const DQTestParams& p) { +std::shared_ptr build_quantize_model(const Shape& shape, const DQTestParams& p) { auto input = std::make_shared(element::f16, shape); input->set_friendly_name("input"); @@ -320,29 +319,29 @@ std::shared_ptr build_quantize_model(const Shape& shape, results.push_back(result_zp); } - auto model = std::make_shared( - results, - ParameterVector{input}, - "quantize_fp16_v" + std::to_string(p.decompose_version)); + auto model = std::make_shared(results, + ParameterVector{input}, + "quantize_fp16_v" + std::to_string(decompose_version(p))); model->validate_nodes_and_infer_types(); - run_decompose_pass(model, p.decompose_version); + run_decompose_pass(model, p.decompose_mode); return model; } // ============================================================================ // Build a dequantize-only model (matches the real "read cached tokens" path): -// Parameters(q, scale, zp) -> (convert(q,f32) - convert(zp,f32)) * scale +// Parameters(q_storage, scale, zp_storage) -> (convert(q,f32) - convert(zp,f32)) * scale // -> Convert(f16) -> Result // // This mirrors create_dequant_nodes() in the production code. // No decompose pass needed -- the dequant chain is plain arithmetic. // ============================================================================ -std::shared_ptr build_dequantize_model(const Shape& shape, - const DQTestParams& p) { - auto config = make_dq_config(shape, p); +std::shared_ptr build_dequantize_model(const Shape& shape, const DQTestParams& p) { + const auto storage_types = ov::npuw::util::resolve_dynamic_quant_storage_types(p.decompose_mode, + p.is_symmetric, + resolve_quant_dt(p)); - auto param_q = std::make_shared(config.quantization_dt, shape); + auto param_q = std::make_shared(storage_types.quantized_data_type, shape); param_q->set_friendly_name("quantized_data"); // Scale/ZP shape: same as data but with embedding dim (last) collapsed to 1 @@ -364,7 +363,7 @@ std::shared_ptr build_dequantize_model(const Shape& shape, if (p.is_symmetric) { dequant_input = q_f32; } else { - auto param_zp = std::make_shared(config.zp_dt, scale_shape); + auto param_zp = std::make_shared(storage_types.zero_point_type, scale_shape); param_zp->set_friendly_name("zero_point"); params.push_back(param_zp); @@ -384,10 +383,9 @@ std::shared_ptr build_dequantize_model(const Shape& shape, auto result = std::make_shared(output_cvt); result->set_friendly_name("result_dequantized"); - auto model = std::make_shared( - ResultVector{result}, - params, - "dequantize_fp16_v" + std::to_string(p.decompose_version)); + auto model = std::make_shared(ResultVector{result}, + params, + "dequantize_fp16_v" + std::to_string(decompose_version(p))); model->validate_nodes_and_infer_types(); return model; @@ -431,9 +429,7 @@ std::vector generate_gen_gaussian_data(size_t count, float mu, float alph // ============================================================================ // Helper: evaluate a roundtrip model via the reference interpreter. // ============================================================================ -AccuracyMetrics evaluate_roundtrip(const std::shared_ptr& model, - const float* input_data, - size_t element_count) { +AccuracyMetrics evaluate_roundtrip(const std::shared_ptr& model, const float* input_data, size_t element_count) { auto input_tensor = Tensor(element::f16, model->get_parameters()[0]->get_shape()); auto* dst = input_tensor.data(); for (size_t i = 0; i < element_count; ++i) { @@ -530,7 +526,7 @@ AccuracyMetrics evaluate_split_roundtrip_on_device(const std::shared_ptr& quant_request.set_input_tensor(quant_input); quant_request.infer(); - auto tensor_q = quant_request.get_output_tensor(0); + auto tensor_q = quant_request.get_output_tensor(0); auto tensor_scale = quant_request.get_output_tensor(1); // Step 2: compile and run dequantize model @@ -558,7 +554,6 @@ AccuracyMetrics evaluate_split_roundtrip_on_device(const std::shared_ptr& return compute_metrics(input_data, output_f32.data(), element_count); } - // ============================================================================ // Helper: get the target device from the environment, empty if not set // ============================================================================ @@ -567,7 +562,6 @@ std::string get_test_device() { return env ? std::string(env) : std::string{}; } - // ============================================================================ // Parameterized test fixture // ============================================================================ @@ -575,9 +569,7 @@ class DynamicQuantizeAccuracyTest : public ::testing::TestWithParam 1e-9) { double ratio = dev_metrics.rel_l2_norm / ref_metrics.rel_l2_norm; - EXPECT_LT(ratio, 4.0) - << "V" << p.decompose_version << " on " << device - << ": device accuracy is " << ratio << "x worse than interpreter"; + EXPECT_LT(ratio, 4.0) << "V" << decompose_version(p) << " on " << device + << ": device accuracy is " << ratio << "x worse than interpreter"; } } @@ -721,23 +709,23 @@ static std::string get_test_case_name(const ::testing::TestParamInfo make_uniform_cases(int version) { +static std::vector make_uniform_cases(DQMode mode) { return { - make_uniform(version, {1, 8, 64, 128}, -1.0f, 1.0f, 42), - make_uniform(version, {1, 8, 64, 128}, -10.0f, 10.0f, 77), - make_uniform(version, {1, 8, 64, 128}, 0.0f, 5.0f, 999), - make_uniform(version, {1, 8, 1024, 128}, -3.0f, 3.0f, 314), + make_uniform(mode, {1, 8, 64, 128}, -1.0f, 1.0f, 42), + make_uniform(mode, {1, 8, 64, 128}, -10.0f, 10.0f, 77), + make_uniform(mode, {1, 8, 64, 128}, 0.0f, 5.0f, 999), + make_uniform(mode, {1, 8, 1024, 128}, -3.0f, 3.0f, 314), }; } -static std::vector make_ggd_cases(int version) { +static std::vector make_ggd_cases(DQMode mode) { return { - make_ggd(version, {1, 8, 1024, 128}, 0.018f, 1.121f, 0.923f, 42), - make_ggd(version, {1, 8, 1024, 128}, 0.032f, 1.388f, 1.016f, 77), - make_ggd(version, {1, 8, 1024, 128}, -0.027f, 1.830f, 1.271f, 123), + make_ggd(mode, {1, 8, 1024, 128}, 0.018f, 1.121f, 0.923f, 42), + make_ggd(mode, {1, 8, 1024, 128}, 0.032f, 1.388f, 1.016f, 77), + make_ggd(mode, {1, 8, 1024, 128}, -0.027f, 1.830f, 1.271f, 123), }; } @@ -793,62 +781,54 @@ static std::vector make_sym_i4_ggd_cases() { // ============================================================================ // Test instantiation -- V1: handcrafted symmetric i8 [-127, 127] // ============================================================================ -INSTANTIATE_TEST_SUITE_P( - V1_Uniform, - DynamicQuantizeAccuracyTest, - ::testing::ValuesIn(make_uniform_cases(1)), - get_test_case_name); +INSTANTIATE_TEST_SUITE_P(V1_Uniform, + DynamicQuantizeAccuracyTest, + ::testing::ValuesIn(make_uniform_cases(DQMode::HandcraftedSymmetricI8)), + get_test_case_name); -INSTANTIATE_TEST_SUITE_P( - V1_GenGaussian, - DynamicQuantizeAccuracyTest, - ::testing::ValuesIn(make_ggd_cases(1)), - get_test_case_name); +INSTANTIATE_TEST_SUITE_P(V1_GenGaussian, + DynamicQuantizeAccuracyTest, + ::testing::ValuesIn(make_ggd_cases(DQMode::HandcraftedSymmetricI8)), + get_test_case_name); // ============================================================================ // Test instantiation -- V2: ONNX DynamicQuantizeLinear u8 [0, 255] // ============================================================================ -INSTANTIATE_TEST_SUITE_P( - V2_Uniform, - DynamicQuantizeAccuracyTest, - ::testing::ValuesIn(make_uniform_cases(2)), - get_test_case_name); +INSTANTIATE_TEST_SUITE_P(V2_Uniform, + DynamicQuantizeAccuracyTest, + ::testing::ValuesIn(make_uniform_cases(DQMode::OnnxDynamicQuantizeLinear)), + get_test_case_name); -INSTANTIATE_TEST_SUITE_P( - V2_GenGaussian, - DynamicQuantizeAccuracyTest, - ::testing::ValuesIn(make_ggd_cases(2)), - get_test_case_name); +INSTANTIATE_TEST_SUITE_P(V2_GenGaussian, + DynamicQuantizeAccuracyTest, + ::testing::ValuesIn(make_ggd_cases(DQMode::OnnxDynamicQuantizeLinear)), + get_test_case_name); // ============================================================================ // Test instantiation -- V3: compiler pattern style i8 [-128, 127] // ============================================================================ -INSTANTIATE_TEST_SUITE_P( - V3_Uniform, - DynamicQuantizeAccuracyTest, - ::testing::ValuesIn(make_uniform_cases(3)), - get_test_case_name); +INSTANTIATE_TEST_SUITE_P(V3_Uniform, + DynamicQuantizeAccuracyTest, + ::testing::ValuesIn(make_uniform_cases(DQMode::CompilerPatternI8)), + get_test_case_name); -INSTANTIATE_TEST_SUITE_P( - V3_GenGaussian, - DynamicQuantizeAccuracyTest, - ::testing::ValuesIn(make_ggd_cases(3)), - get_test_case_name); +INSTANTIATE_TEST_SUITE_P(V3_GenGaussian, + DynamicQuantizeAccuracyTest, + ::testing::ValuesIn(make_ggd_cases(DQMode::CompilerPatternI8)), + get_test_case_name); // ============================================================================ // Test instantiation -- V1 symmetric i4: handcrafted symmetric i4 [-7, 7] // ============================================================================ -INSTANTIATE_TEST_SUITE_P( - V1_Symmetric_i4_Uniform, - DynamicQuantizeAccuracyTest, - ::testing::ValuesIn(make_sym_i4_uniform_cases()), - get_test_case_name); +INSTANTIATE_TEST_SUITE_P(V1_Symmetric_i4_Uniform, + DynamicQuantizeAccuracyTest, + ::testing::ValuesIn(make_sym_i4_uniform_cases()), + get_test_case_name); -INSTANTIATE_TEST_SUITE_P( - V1_Symmetric_i4_GenGaussian, - DynamicQuantizeAccuracyTest, - ::testing::ValuesIn(make_sym_i4_ggd_cases()), - get_test_case_name); +INSTANTIATE_TEST_SUITE_P(V1_Symmetric_i4_GenGaussian, + DynamicQuantizeAccuracyTest, + ::testing::ValuesIn(make_sym_i4_ggd_cases()), + get_test_case_name); // ============================================================================ // Test instantiation -- asymmetric i8 offset distributions for all versions. @@ -859,24 +839,25 @@ INSTANTIATE_TEST_SUITE_P( // This can fail until V1 formula is corrected to use -min/scale. // ============================================================================ static std::vector make_asym_i8_offset_cases() { - static constexpr std::array versions = {1, 2, 3}; + static constexpr std::array versions = {DQMode::HandcraftedSymmetricI8, + DQMode::OnnxDynamicQuantizeLinear, + DQMode::CompilerPatternI8}; std::vector cases; cases.reserve(versions.size() * 3u); - for (const int version : versions) { - cases.push_back(make_uniform(version, {1, 8, 64, 128}, 0.5f, 5.0f, 42)); - cases.push_back(make_uniform(version, {1, 8, 64, 128}, 1.0f, 5.0f, 77)); - cases.push_back(make_uniform(version, {1, 8, 64, 128}, -5.0f, -0.5f, 99)); + for (const auto mode : versions) { + cases.push_back(make_uniform(mode, {1, 8, 64, 128}, 0.5f, 5.0f, 42)); + cases.push_back(make_uniform(mode, {1, 8, 64, 128}, 1.0f, 5.0f, 77)); + cases.push_back(make_uniform(mode, {1, 8, 64, 128}, -5.0f, -0.5f, 99)); } return cases; } -INSTANTIATE_TEST_SUITE_P( - Asymmetric_i8_OffsetCoverage, - DynamicQuantizeAccuracyTest, - ::testing::ValuesIn(make_asym_i8_offset_cases()), - get_test_case_name); +INSTANTIATE_TEST_SUITE_P(Asymmetric_i8_OffsetCoverage, + DynamicQuantizeAccuracyTest, + ::testing::ValuesIn(make_asym_i8_offset_cases()), + get_test_case_name); } // namespace diff --git a/src/plugins/intel_npu/tests/unit/npuw/fold_const_test.cpp b/src/plugins/intel_npu/tests/unit/npuw/fold_const_test.cpp new file mode 100644 index 000000000000..86938722299e --- /dev/null +++ b/src/plugins/intel_npu/tests/unit/npuw/fold_const_test.cpp @@ -0,0 +1,223 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "partitioning/patterns/fold_const.hpp" + +#include + +#include "openvino/op/ops.hpp" + +// Tests for ov::npuw::patterns::util::FoldShapeComputeChain. +// +// The graph built here mirrors Model0_prefill_01_REP00DE.xml – the simplified +// GPT-OSS MoE router subgraph after ReshapeToStatic has been applied. All +// tensor shapes are fully static (seq_len=1024, hidden=2880, experts=32, k=4). +// +// Two ShapeOf nodes are present in the pre-folded graph: +// +// ShapeOf_A : ShapeOf(Add[1024,32]) → Broadcast.shape (zeros-like template) +// ShapeOf_B : ShapeOf(Convert[1024,4]) → Slice.stop (scatter index bounds) +// +// Full edge topology (matches the XML edge list): +// +// Param[1024,2880] ──Convert──► MatMul ──► Add ──► TopK ──values──► Softmax ──► Slice ──► Scatter +// Const[32,2880]f32 ────────────────────────┘ │ ▲ +// │ ShapeOf_B(Convert[1024,4]) │ +// ├──► ShapeOf_A(Add[1024,32]) │ +// │ └──► Broadcast(zeros) ──► Scatter │ +// └──► TopK.indices ──► Convert ─────────── ┘ +// +// FoldShapeComputeChain must replace both ShapeOf nodes with Constants. + +namespace { + +using namespace ov; + +static std::shared_ptr make_gptoss_router_model() { + constexpr size_t seq_len = 1024; + constexpr size_t hidden_dim = 2880; + constexpr size_t num_experts = 32; + constexpr int64_t k = 4; + + // ── Router input: [seq_len, hidden_dim] ────────────────────────────────── + auto router_input = std::make_shared(element::f32, Shape{seq_len, hidden_dim}); + router_input->set_friendly_name("router_input"); + + // ── Weight: f32 Const [32, 2880] (mirrors the quantised-then-dequantised path) ── + auto weight = op::v0::Constant::create(element::f32, + Shape{num_experts, hidden_dim}, + std::vector(num_experts * hidden_dim, 0.0f)); + weight->set_friendly_name("router/weight"); + + // Convert input to f32 (matches Convert_f16ic_6 in XML) + auto input_convert = std::make_shared(router_input, element::f32); + input_convert->set_friendly_name("router/Convert_input"); + + // MatMul: [1024,2880] × [32,2880]ᵀ → [1024,32] + auto matmul = std::make_shared(input_convert, weight, false, true); + matmul->set_friendly_name("router/MatMul"); + + // Add bias: [1,32] → broadcast gives [1024,32] + auto bias = op::v0::Constant::create(element::f32, Shape{1, num_experts}, std::vector(num_experts, 0.0f)); + bias->set_friendly_name("router/bias"); + auto add = std::make_shared(matmul, bias); + add->set_friendly_name("router/Add"); + + // ── ShapeOf chain A: ShapeOf(Add[1024,32]) → Broadcast.shape ───────────── + // Mirrors XML nodes 12 (ShapeOf) and 13 (Broadcast). + // Creates the zeros-like template of shape [1024, 32]. + auto shape_of_a = std::make_shared(add, element::i64); + shape_of_a->set_friendly_name("router/zeros_like/ShapeOf"); + + auto zeros_scalar = op::v0::Constant::create(element::f32, Shape{}, std::vector{0.0f}); + zeros_scalar->set_friendly_name("router/zeros_like/scalar"); + + // v3::Broadcast(scalar, target_shape) with NUMPY mode → zeros[1024,32] + auto broadcast = std::make_shared(zeros_scalar, shape_of_a); + broadcast->set_friendly_name("router/zeros_like/Broadcast"); + + // ── TopK: top-k expert selection ───────────────────────────────────────── + auto k_const = op::v0::Constant::create(element::i64, Shape{}, std::vector{k}); + k_const->set_friendly_name("router/k"); + auto topk = std::make_shared(add, + k_const, + /*axis=*/-1, + op::v11::TopK::Mode::MAX, + op::v11::TopK::SortType::NONE); + topk->set_friendly_name("router/TopK"); + // topk->output(0) = values [1024,4] + // topk->output(1) = indices [1024,4] + + auto softmax = std::make_shared(topk->output(0), /*axis=*/-1); + softmax->set_friendly_name("router/Softmax"); + + // Convert TopK indices (mirrors XML node 16: Convert → i64) + auto topk_idx_convert = std::make_shared(topk->output(1), element::i64); + topk_idx_convert->set_friendly_name("router/scatter_/Convert"); + + // ── ShapeOf chain B: ShapeOf(Convert[1024,4]) → Slice.stop ─────────────── + // Mirrors XML nodes 19 (ShapeOf) and 22 (Slice). + auto shape_of_b = std::make_shared(topk_idx_convert, element::i64); + shape_of_b->set_friendly_name("router/scatter_/ShapeOf"); + + // Slice the softmax values [0:1024, 0:4] – bounds are given by ShapeOf_B. + // After folding, ShapeOf_B becomes Const([1024, 4]) and the slice is trivially static. + auto slice_start = op::v0::Constant::create(element::i64, Shape{2}, std::vector{0, 0}); + auto slice_step = op::v0::Constant::create(element::i64, Shape{2}, std::vector{1, 1}); + auto slice_axes = op::v0::Constant::create(element::i64, Shape{2}, std::vector{0, 1}); + // v8::Slice(data, start, stop, step, axes) + auto slice = std::make_shared(softmax, slice_start, shape_of_b, slice_step, slice_axes); + slice->set_friendly_name("router/scatter_/Slice"); + + // ── ScatterElementsUpdate: scatter softmax scores back into [1024,32] ──── + // data=broadcast(zeros), indices=topk_idx_convert, updates=slice, axis=1 + auto scatter_axis = op::v0::Constant::create(element::i64, Shape{}, std::vector{1}); + scatter_axis->set_friendly_name("router/scatter_axis"); + auto scatter = std::make_shared(broadcast, topk_idx_convert, slice, scatter_axis); + scatter->set_friendly_name("router/ScatterElementsUpdate"); + + // ── Transpose [1024,32] → [32,1024] ────────────────────────────────────── + auto transpose_order = op::v0::Constant::create(element::i32, Shape{2}, std::vector{1, 0}); + auto transpose = std::make_shared(scatter, transpose_order); + transpose->set_friendly_name("experts/Transpose"); + + // ── Reshape [32,1024] → [32,1,1024] ────────────────────────────────────── + auto reshape_shape = op::v0::Constant::create( + element::i64, + Shape{3}, + std::vector{static_cast(num_experts), 1LL, static_cast(seq_len)}); + auto reshape = std::make_shared(transpose, reshape_shape, false); + reshape->set_friendly_name("experts/Reshape"); + + // ── Unsqueeze [32,1,1024] → [32,1,1024,1] ──────────────────────────────── + auto unsqueeze_axis = op::v0::Constant::create(element::i64, Shape{}, std::vector{3}); + auto unsqueeze = std::make_shared(reshape, unsqueeze_axis); + unsqueeze->set_friendly_name("experts/Unsqueeze"); + + auto result = std::make_shared(unsqueeze); + return std::make_shared(ov::ResultVector{result}, + ov::ParameterVector{router_input}, + "gptoss_router_test"); +} + +// Count nodes of a specific op type in the model. +template +static size_t count_ops_of_type(const std::shared_ptr& model) { + size_t count = 0; + for (const auto& op : model->get_ordered_ops()) { + if (ov::is_type(op)) + ++count; + } + return count; +} + +// ───────────────────────────────────────────────────────────────────────────── +// Test 1: The model has exactly 2 ShapeOf nodes before folding and 0 after. +// ───────────────────────────────────────────────────────────────────────────── +TEST(FoldConstTest, RouterShapeOfNodesRemovedAfterFolding) { + auto model = make_gptoss_router_model(); + + ASSERT_EQ(count_ops_of_type(model), 2u) + << "Precondition: ShapeOf_A (on Add) and ShapeOf_B (on TopK-indices-Convert)"; + + ov::npuw::patterns::util::FoldShapeComputeChain().run_on_model(model); + + EXPECT_EQ(count_ops_of_type(model), 0u) + << "FoldShapeComputeChain must replace all ShapeOf nodes with Constants"; +} + +// ───────────────────────────────────────────────────────────────────────────── +// Test 2: After folding, the Broadcast shape input (was ShapeOf_A) is a +// Constant carrying value [1024, 32]. +// ───────────────────────────────────────────────────────────────────────────── +TEST(FoldConstTest, RouterBroadcastShapeInputFoldedToConst) { + auto model = make_gptoss_router_model(); + ov::npuw::patterns::util::FoldShapeComputeChain().run_on_model(model); + + bool found_broadcast = false; + for (const auto& op : model->get_ordered_ops()) { + if (!ov::is_type(op)) + continue; + found_broadcast = true; + // Input port 1 = target_shape (was ShapeOf_A → Const after folding). + auto shape_input = op->input_value(1).get_node_shared_ptr(); + EXPECT_TRUE(ov::is_type(shape_input)) + << "Broadcast target-shape input must be a Constant after folding"; + auto c = ov::as_type_ptr(shape_input); + ASSERT_NE(c, nullptr); + auto vals = c->cast_vector(); + ASSERT_EQ(vals.size(), 2u); + EXPECT_EQ(vals[0], 1024) << "dim[0] should equal seq_len=1024"; + EXPECT_EQ(vals[1], 32) << "dim[1] should equal num_experts=32"; + } + EXPECT_TRUE(found_broadcast) << "Broadcast node not found in model"; +} + +// ───────────────────────────────────────────────────────────────────────────── +// Test 3: After folding, the Slice stop input (was ShapeOf_B) is a Constant +// carrying value [1024, 4]. +// ───────────────────────────────────────────────────────────────────────────── +TEST(FoldConstTest, RouterSliceStopInputFoldedToConst) { + auto model = make_gptoss_router_model(); + ov::npuw::patterns::util::FoldShapeComputeChain().run_on_model(model); + + bool found_slice = false; + for (const auto& op : model->get_ordered_ops()) { + if (!ov::is_type(op)) + continue; + found_slice = true; + // Input port 2 = stop (was ShapeOf_B → Const after folding). + auto stop_input = op->input_value(2).get_node_shared_ptr(); + EXPECT_TRUE(ov::is_type(stop_input)) << "Slice stop input must be a Constant after folding"; + auto c = ov::as_type_ptr(stop_input); + ASSERT_NE(c, nullptr); + auto vals = c->cast_vector(); + ASSERT_EQ(vals.size(), 2u); + EXPECT_EQ(vals[0], 1024) << "dim[0] should equal seq_len=1024"; + EXPECT_EQ(vals[1], 4) << "dim[1] should equal k=4"; + } + EXPECT_TRUE(found_slice) << "Slice node not found in model"; +} + +} // namespace diff --git a/src/plugins/intel_npu/tests/unit/npuw/import_non_llm_blob.cpp b/src/plugins/intel_npu/tests/unit/npuw/import_non_llm_blob.cpp index e0bbd93a9cb0..9e3d7c3d37cc 100644 --- a/src/plugins/intel_npu/tests/unit/npuw/import_non_llm_blob.cpp +++ b/src/plugins/intel_npu/tests/unit/npuw/import_non_llm_blob.cpp @@ -11,6 +11,9 @@ #include "intel_npu/config/npuw.hpp" #include "model_builder.hpp" #include "openvino/openvino.hpp" +#include "openvino/util/codec_xor.hpp" +#include "orc.hpp" +#include "serialization.hpp" using ov::test::npuw::ModelBuilder; @@ -23,6 +26,68 @@ class ImportNonLLMBlobTestNPUW : public ::testing::TestWithParam { } protected: + static ov::EncryptionCallbacks xor_callbacks() { + return {ov::util::codec_xor, ov::util::codec_xor}; + } + + static ov::Tensor make_input_tensor(const ov::Output& input) { + ov::Tensor tensor(input.get_element_type(), input.get_shape()); + + if (input.get_element_type() == ov::element::i32) { + auto* data = tensor.data(); + for (std::size_t i = 0; i < tensor.get_size(); ++i) { + data[i] = static_cast(i % 17); + } + } else if (input.get_element_type() == ov::element::i64) { + auto* data = tensor.data(); + for (std::size_t i = 0; i < tensor.get_size(); ++i) { + data[i] = static_cast(i % 17); + } + } else if (input.get_element_type() == ov::element::f32) { + auto* data = tensor.data(); + for (std::size_t i = 0; i < tensor.get_size(); ++i) { + data[i] = static_cast(i) * 0.25f; + } + } else if (input.get_element_type() == ov::element::f16) { + auto* data = tensor.data(); + for (std::size_t i = 0; i < tensor.get_size(); ++i) { + data[i] = ov::float16(static_cast(i) * 0.25f); + } + } else { + std::memset(tensor.data(), 0, tensor.get_byte_size()); + } + + return tensor; + } + + static std::vector infer_outputs(ov::CompiledModel& model) { + auto request = model.create_infer_request(); + for (const auto& input : model.inputs()) { + request.set_tensor(input, make_input_tensor(input)); + } + request.infer(); + + std::vector outputs; + outputs.reserve(model.outputs().size()); + for (const auto& output : model.outputs()) { + auto result = request.get_tensor(output); + ov::Tensor copy(result.get_element_type(), result.get_shape()); + result.copy_to(copy); + outputs.push_back(copy); + } + return outputs; + } + + static void expect_outputs_equal(const std::vector& expected, const std::vector& actual) { + ASSERT_EQ(expected.size(), actual.size()); + for (std::size_t i = 0; i < expected.size(); ++i) { + EXPECT_EQ(expected[i].get_element_type(), actual[i].get_element_type()); + EXPECT_EQ(expected[i].get_shape(), actual[i].get_shape()); + ASSERT_EQ(expected[i].get_byte_size(), actual[i].get_byte_size()); + EXPECT_EQ(std::memcmp(expected[i].data(), actual[i].data(), expected[i].get_byte_size()), 0); + } + } + ModelBuilder model_builder; std::shared_ptr m_ov_model; ov::AnyMap m_props; @@ -35,34 +100,140 @@ TEST_P(ImportNonLLMBlobTestNPUW, CacheModeOptimizeSpeed) { m_props["CACHE_MODE"] = "OPTIMIZE_SPEED"; auto compiled = m_core.compile_model(m_ov_model, "NPU", m_props); + auto compiled_outputs = infer_outputs(compiled); + + std::stringstream blob; + compiled.export_model(blob); + EXPECT_TRUE(ov::npuw::orc::is_orc(blob).has_value()); + + EXPECT_NO_THROW({ + auto imported = m_core.import_model(blob, "NPU", m_props); + auto imported_outputs = infer_outputs(imported); + expect_outputs_equal(compiled_outputs, imported_outputs); + }); +} + +TEST_P(ImportNonLLMBlobTestNPUW, CacheModeOptimizeSpeedEncrypted) { + ov::AnyMap wai_props = GetParam(); + m_props.insert(wai_props.begin(), wai_props.end()); + m_props["CACHE_MODE"] = "OPTIMIZE_SPEED"; + m_props[ov::cache_encryption_callbacks.name()] = xor_callbacks(); + + auto compiled = m_core.compile_model(m_ov_model, "NPU", m_props); + auto compiled_outputs = infer_outputs(compiled); std::stringstream blob; compiled.export_model(blob); + EXPECT_TRUE(ov::npuw::orc::is_orc(blob).has_value()); EXPECT_NO_THROW({ auto imported = m_core.import_model(blob, "NPU", m_props); - imported.create_infer_request(); + auto imported_outputs = infer_outputs(imported); + expect_outputs_equal(compiled_outputs, imported_outputs); }); } +TEST_P(ImportNonLLMBlobTestNPUW, CacheModeOptimizeSpeedEncryptedRequiresDecryptCallback) { + ov::AnyMap wai_props = GetParam(); + m_props.insert(wai_props.begin(), wai_props.end()); + m_props["CACHE_MODE"] = "OPTIMIZE_SPEED"; + m_props[ov::cache_encryption_callbacks.name()] = ov::EncryptionCallbacks{ov::util::codec_xor, nullptr}; + + auto compiled = m_core.compile_model(m_ov_model, "NPU", m_props); + + std::stringstream blob; + compiled.export_model(blob); + EXPECT_TRUE(ov::npuw::orc::is_orc(blob).has_value()); + + auto import_props = m_props; + import_props.erase(ov::cache_encryption_callbacks.name()); + try { + auto imported = m_core.import_model(blob, "NPU", import_props); + (void)imported; + FAIL() << "Expected encrypted ORC import to require a decrypt callback"; + } catch (const ov::Exception& ex) { + EXPECT_NE(std::string(ex.what()).find("Blob is encrypted, but no decryption callback was provided"), + std::string::npos) + << ex.what(); + } +} + +TEST_P(ImportNonLLMBlobTestNPUW, CacheModeOptimizeSpeedEnsureCompatibility) { + ov::AnyMap wai_props = GetParam(); + m_props.insert(wai_props.begin(), wai_props.end()); + m_props["CACHE_MODE"] = "OPTIMIZE_SPEED"; + m_props["NPUW_ENSURE_COMPATIBILITY"] = "YES"; + + auto compiled = m_core.compile_model(m_ov_model, "NPU", m_props); + std::stringstream blob; + // This synthetic repeated-block model does not satisfy the phase-0 compatibility contract: + // even with NPU-only placement it still exercises non-versioned behavior-owned state today, + // so ENSURE_COMPATIBILITY is expected to reject export rather than produce a misleading blob. + EXPECT_THROW(compiled.export_model(blob), ov::Exception); +} + TEST_P(ImportNonLLMBlobTestNPUW, CacheModeOptimizeSizeWithModelPtr) { ov::AnyMap wai_props = GetParam(); m_props.insert(wai_props.begin(), wai_props.end()); m_props["CACHE_MODE"] = "OPTIMIZE_SIZE"; auto compiled = m_core.compile_model(m_ov_model, "NPU", m_props); + auto compiled_outputs = infer_outputs(compiled); std::stringstream blob; compiled.export_model(blob); + EXPECT_TRUE(ov::npuw::orc::is_orc(blob).has_value()); EXPECT_NO_THROW({ auto import_props = m_props; import_props[ov::hint::model.name()] = std::static_pointer_cast(m_ov_model); auto imported = m_core.import_model(blob, "NPU", import_props); - imported.create_infer_request(); + auto imported_outputs = infer_outputs(imported); + expect_outputs_equal(compiled_outputs, imported_outputs); + }); +} + +TEST_P(ImportNonLLMBlobTestNPUW, CacheModeOptimizeSizeWithModelPtrEncryptedDifferentDecryptSize) { + ov::AnyMap wai_props = GetParam(); + m_props.insert(wai_props.begin(), wai_props.end()); + m_props["CACHE_MODE"] = "OPTIMIZE_SIZE"; + m_props[ov::cache_encryption_callbacks.name()] = + ov::EncryptionCallbacks{[](const std::string& unencrypted_blob) { + std::string copy_blob = unencrypted_blob; + copy_blob += ""; + return ov::util::codec_xor(copy_blob); + }, + [](const std::string& encrypted_blob) { + std::string decrypted_blob = ov::util::codec_xor(encrypted_blob); + decrypted_blob += ""; + return decrypted_blob; + }}; + + auto compiled = m_core.compile_model(m_ov_model, "NPU", m_props); + + std::stringstream blob; + compiled.export_model(blob); + EXPECT_TRUE(ov::npuw::orc::is_orc(blob).has_value()); + + auto import_props = m_props; + import_props[ov::hint::model.name()] = std::static_pointer_cast(m_ov_model); + EXPECT_NO_THROW({ + auto imported = m_core.import_model(blob, "NPU", import_props); + (void)imported; }); } +TEST_P(ImportNonLLMBlobTestNPUW, CacheModeOptimizeSizeWithModelPtrEnsureCompatibility) { + ov::AnyMap wai_props = GetParam(); + m_props.insert(wai_props.begin(), wai_props.end()); + m_props["CACHE_MODE"] = "OPTIMIZE_SIZE"; + m_props["NPUW_ENSURE_COMPATIBILITY"] = "YES"; + + auto compiled = m_core.compile_model(m_ov_model, "NPU", m_props); + std::stringstream blob; + EXPECT_THROW(compiled.export_model(blob), ov::Exception); +} + using ImportNonLLMNonWAIBlobTestNPUW = ImportNonLLMBlobTestNPUW; TEST_P(ImportNonLLMNonWAIBlobTestNPUW, CacheModeOptimizeSizeNoModelPtr) { ov::AnyMap wai_props = GetParam(); @@ -70,16 +241,30 @@ TEST_P(ImportNonLLMNonWAIBlobTestNPUW, CacheModeOptimizeSizeNoModelPtr) { m_props["CACHE_MODE"] = "OPTIMIZE_SIZE"; auto compiled = m_core.compile_model(m_ov_model, "NPU", m_props); + auto compiled_outputs = infer_outputs(compiled); std::stringstream blob; compiled.export_model(blob); + EXPECT_TRUE(ov::npuw::orc::is_orc(blob).has_value()); EXPECT_NO_THROW({ auto imported = m_core.import_model(blob, "NPU", m_props); - imported.create_infer_request(); + auto imported_outputs = infer_outputs(imported); + expect_outputs_equal(compiled_outputs, imported_outputs); }); } +TEST_P(ImportNonLLMNonWAIBlobTestNPUW, CacheModeOptimizeSizeNoModelPtrEnsureCompatibility) { + ov::AnyMap wai_props = GetParam(); + m_props.insert(wai_props.begin(), wai_props.end()); + m_props["CACHE_MODE"] = "OPTIMIZE_SIZE"; + m_props["NPUW_ENSURE_COMPATIBILITY"] = "YES"; + + auto compiled = m_core.compile_model(m_ov_model, "NPU", m_props); + std::stringstream blob; + EXPECT_THROW(compiled.export_model(blob), ov::Exception); +} + using ImportNonLLMWAIBlobTestNPUW = ImportNonLLMBlobTestNPUW; TEST_P(ImportNonLLMWAIBlobTestNPUW, CacheModeOptimizeSizeNoModelPtr) { ov::AnyMap wai_props = GetParam(); @@ -90,27 +275,38 @@ TEST_P(ImportNonLLMWAIBlobTestNPUW, CacheModeOptimizeSizeNoModelPtr) { std::stringstream blob; compiled.export_model(blob); + EXPECT_TRUE(ov::npuw::orc::is_orc(blob).has_value()); try { auto imported = m_core.import_model(blob, "NPU", m_props); FAIL() << "Expected import to throw when WAI weightless blob is imported without MODEL_PTR/WEIGHTS_PATH"; } catch (const ov::Exception& ex) { const std::string what = ex.what(); - const bool has_expected_text = - what.find("Blob is weightless") != std::string::npos && - what.find("WEIGHTS_PATH") != std::string::npos && - what.find("MODEL_PTR") != std::string::npos; + const bool has_expected_text = what.find("Blob is weightless") != std::string::npos && + what.find("WEIGHTS_PATH") != std::string::npos && + what.find("MODEL_PTR") != std::string::npos; EXPECT_TRUE(has_expected_text) << "Unexpected exception message: " << what; } } -INSTANTIATE_TEST_SUITE_P(Only_NPU_USE_NPUW, ImportNonLLMBlobTestNPUW, - testing::Values(ov::AnyMap{})); -INSTANTIATE_TEST_SUITE_P(NPUW_FOLD, ImportNonLLMBlobTestNPUW, - testing::Values(ov::AnyMap{{"NPUW_FOLD", "YES"}})); -INSTANTIATE_TEST_SUITE_P(NPUW_CWAI, ImportNonLLMBlobTestNPUW, - testing::Values(ov::AnyMap{{"NPUW_CWAI", "YES"}})); - -INSTANTIATE_TEST_SUITE_P(NPUW_NON_WAI, ImportNonLLMNonWAIBlobTestNPUW, - testing::Values(ov::AnyMap{}, ov::AnyMap{{"NPUW_FUNCALL_FOR_ALL", "YES"}})); -INSTANTIATE_TEST_SUITE_P(NPUW_WAI, ImportNonLLMWAIBlobTestNPUW, - testing::Values(ov::AnyMap{{"NPUW_FOLD", "YES"}}, ov::AnyMap{{"NPUW_CWAI", "YES"}})); +TEST_P(ImportNonLLMWAIBlobTestNPUW, CacheModeOptimizeSizeNoModelPtrEnsureCompatibility) { + ov::AnyMap wai_props = GetParam(); + m_props.insert(wai_props.begin(), wai_props.end()); + m_props["CACHE_MODE"] = "OPTIMIZE_SIZE"; + m_props["NPUW_ENSURE_COMPATIBILITY"] = "YES"; + + auto compiled = m_core.compile_model(m_ov_model, "NPU", m_props); + + std::stringstream blob; + EXPECT_THROW(compiled.export_model(blob), ov::Exception); +} + +INSTANTIATE_TEST_SUITE_P(Only_NPU_USE_NPUW, ImportNonLLMBlobTestNPUW, testing::Values(ov::AnyMap{})); +INSTANTIATE_TEST_SUITE_P(NPUW_FOLD, ImportNonLLMBlobTestNPUW, testing::Values(ov::AnyMap{{"NPUW_FOLD", "YES"}})); +INSTANTIATE_TEST_SUITE_P(NPUW_CWAI, ImportNonLLMBlobTestNPUW, testing::Values(ov::AnyMap{{"NPUW_CWAI", "YES"}})); + +INSTANTIATE_TEST_SUITE_P(NPUW_NON_WAI, + ImportNonLLMNonWAIBlobTestNPUW, + testing::Values(ov::AnyMap{}, ov::AnyMap{{"NPUW_FUNCALL_FOR_ALL", "YES"}})); +INSTANTIATE_TEST_SUITE_P(NPUW_WAI, + ImportNonLLMWAIBlobTestNPUW, + testing::Values(ov::AnyMap{{"NPUW_FOLD", "YES"}}, ov::AnyMap{{"NPUW_CWAI", "YES"}})); diff --git a/src/plugins/intel_npu/tests/unit/npuw/kv_cache_block_manager_test.cpp b/src/plugins/intel_npu/tests/unit/npuw/kv_cache_block_manager_test.cpp new file mode 100644 index 000000000000..8fe4ad70edca --- /dev/null +++ b/src/plugins/intel_npu/tests/unit/npuw/kv_cache_block_manager_test.cpp @@ -0,0 +1,266 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "kv_cache_block_manager.hpp" + +#include + +#include +#include + +#include "openvino/core/shape.hpp" +#include "openvino/core/type/element_type.hpp" + +using namespace ov::npuw; + +namespace { + +// Test fixture for KVCacheBlockManager tests +class KVCacheBlockManagerTest : public ::testing::Test { +protected: + void SetUp() override { + // Common test parameters + block_size = 512; + max_blocks = 8; + base_shape = ov::Shape{1, 32, block_size, 128}; // [batch, num_heads, seq_len=block_size, head_dim] + elem_type = ov::element::f16; + device = "CPU"; // Use CPU for unit tests + + // Pass nullptr for plugin since CPU device doesn't need it in allocMem + manager = std::make_unique(block_size, max_blocks, base_shape, elem_type, device, nullptr); + } + + void TearDown() override { + manager.reset(); + } + + uint32_t block_size; + uint32_t max_blocks; + ov::Shape base_shape; + ov::element::Type elem_type; + std::string device; + std::unique_ptr manager; +}; + +// Test 1: Basic Allocation +TEST_F(KVCacheBlockManagerTest, BasicAllocation) { + // Allocate a block + auto block_id = manager->allocate_block(); + ASSERT_TRUE(block_id.has_value()) << "Block allocation should succeed"; + EXPECT_EQ(block_id.value(), 0u) << "First block should have ID 0"; + + // Get block tensor + auto tensor = manager->get_block_tensor(block_id.value()); + ASSERT_NE(tensor, nullptr) << "Block tensor should not be null"; + + // Check tensor shape + auto shape = tensor->get_shape(); + EXPECT_EQ(shape[0], base_shape[0]) << "Batch dimension should match"; + EXPECT_EQ(shape[1], base_shape[1]) << "Num_heads dimension should match"; + EXPECT_EQ(shape[2], block_size) << "Seq_len should be block_size"; + EXPECT_EQ(shape[3], base_shape[3]) << "Head_dim should match"; + + // Check token count + EXPECT_EQ(manager->get_block_tokens(block_id.value()), 0u) << "Newly allocated block should have 0 tokens"; +} + +// Test 2: Multiple Allocations +TEST_F(KVCacheBlockManagerTest, MultipleAllocations) { + std::vector allocated_blocks; + + // Allocate half of available blocks + for (uint32_t i = 0; i < max_blocks / 2; ++i) { + auto block_id = manager->allocate_block(); + ASSERT_TRUE(block_id.has_value()) << "Allocation " << i << " should succeed"; + allocated_blocks.push_back(block_id.value()); + } + + // Verify unique block IDs + std::set unique_ids(allocated_blocks.begin(), allocated_blocks.end()); + EXPECT_EQ(unique_ids.size(), allocated_blocks.size()) << "All allocated blocks should have unique IDs"; + + // Verify counts via get_allocated_blocks() + auto all_allocated = manager->get_allocated_blocks(); + EXPECT_EQ(all_allocated.size(), max_blocks / 2); +} + +// Test 3: Block Exhaustion +TEST_F(KVCacheBlockManagerTest, BlockExhaustion) { + // Allocate all blocks + for (uint32_t i = 0; i < max_blocks; ++i) { + auto block_id = manager->allocate_block(); + ASSERT_TRUE(block_id.has_value()) << "Allocation " << i << " should succeed"; + } + + // All blocks should be allocated + EXPECT_EQ(manager->get_allocated_blocks().size(), static_cast(max_blocks)); + + // Try to allocate one more - should fail + auto extra_block = manager->allocate_block(); + EXPECT_FALSE(extra_block.has_value()) << "Allocation should fail when all blocks are in use"; + + // Clear all, then allocation should succeed again + manager->clear_all(); + EXPECT_TRUE(manager->get_allocated_blocks().empty()); + + auto new_block = manager->allocate_block(); + ASSERT_TRUE(new_block.has_value()) << "Allocation should succeed after clearing all blocks"; +} + +// Test 4: Token Tracking +TEST_F(KVCacheBlockManagerTest, TokenTracking) { + auto block_id = manager->allocate_block(); + ASSERT_TRUE(block_id.has_value()); + + // Initially 0 tokens + EXPECT_EQ(manager->get_block_tokens(block_id.value()), 0u); + + // Update to 256 tokens + manager->update_block_tokens(block_id.value(), 256); + EXPECT_EQ(manager->get_block_tokens(block_id.value()), 256u); + + // Update to full capacity + manager->update_block_tokens(block_id.value(), block_size); + EXPECT_EQ(manager->get_block_tokens(block_id.value()), block_size); + + // Allocate a second block and verify its tokens are tracked independently + auto block_id2 = manager->allocate_block(); + ASSERT_TRUE(block_id2.has_value()); + manager->update_block_tokens(block_id2.value(), 100); + EXPECT_EQ(manager->get_block_tokens(block_id2.value()), 100u); + EXPECT_EQ(manager->get_block_tokens(block_id.value()), block_size) << "First block tokens should be unchanged"; +} + +// Test 5: Token Count Validation +TEST_F(KVCacheBlockManagerTest, TokenCountValidation) { + auto block_id = manager->allocate_block(); + ASSERT_TRUE(block_id.has_value()); + + // Try to set more tokens than capacity - should throw + EXPECT_THROW(manager->update_block_tokens(block_id.value(), block_size + 1), ov::Exception) + << "Setting tokens beyond capacity should throw exception"; +} + +// Test 6: Memory Reuse After Clear +TEST_F(KVCacheBlockManagerTest, MemoryReuseAfterClear) { + // Allocate a block and record its tensor pointer + auto block_id = manager->allocate_block(); + ASSERT_TRUE(block_id.has_value()); + auto tensor_before = manager->get_block_tensor(block_id.value()); + ASSERT_NE(tensor_before, nullptr); + void* ptr_before = tensor_before->data(); + + // Clear and re-allocate — should reuse the same underlying memory + manager->clear_all(); + auto block_id2 = manager->allocate_block(); + ASSERT_TRUE(block_id2.has_value()); + auto tensor_after = manager->get_block_tensor(block_id2.value()); + ASSERT_NE(tensor_after, nullptr); + EXPECT_EQ(tensor_after->data(), ptr_before) << "Should reuse the same memory buffer after clear"; +} + +// Test 7: Allocation and Token State Consistency +TEST_F(KVCacheBlockManagerTest, AllocationAndTokenConsistency) { + // Initially no blocks allocated + EXPECT_TRUE(manager->get_allocated_blocks().empty()); + + // Allocate 2 blocks with different token counts + auto block1 = manager->allocate_block(); + auto block2 = manager->allocate_block(); + ASSERT_TRUE(block1.has_value()); + ASSERT_TRUE(block2.has_value()); + + manager->update_block_tokens(block1.value(), block_size); // full block + manager->update_block_tokens(block2.value(), 256); // half-full block + + EXPECT_EQ(manager->get_allocated_blocks().size(), 2u); + EXPECT_EQ(manager->get_block_tokens(block1.value()), block_size); + EXPECT_EQ(manager->get_block_tokens(block2.value()), 256u); + + // After clear, tokens and allocations reset + manager->clear_all(); + EXPECT_TRUE(manager->get_allocated_blocks().empty()); + // Re-allocate and verify tokens are reset to 0 + auto block3 = manager->allocate_block(); + ASSERT_TRUE(block3.has_value()); + EXPECT_EQ(manager->get_block_tokens(block3.value()), 0u); +} + +// Test 8: Clear All +TEST_F(KVCacheBlockManagerTest, ClearAll) { + // Allocate several blocks with token counts + for (uint32_t i = 0; i < 5; ++i) { + auto block_id = manager->allocate_block(); + ASSERT_TRUE(block_id.has_value()); + manager->update_block_tokens(block_id.value(), 100 * (i + 1)); + } + + // Verify blocks allocated + EXPECT_EQ(manager->get_allocated_blocks().size(), 5u); + + // Clear all + manager->clear_all(); + + // Verify all blocks freed + EXPECT_TRUE(manager->get_allocated_blocks().empty()); + + // Should be able to allocate again, and tokens reset to 0 + auto new_block = manager->allocate_block(); + ASSERT_TRUE(new_block.has_value()); + EXPECT_EQ(manager->get_block_tokens(new_block.value()), 0u); +} + +// Test 9: Get Allocated Blocks +TEST_F(KVCacheBlockManagerTest, GetAllocatedBlocks) { + // Initially empty + auto allocated = manager->get_allocated_blocks(); + EXPECT_TRUE(allocated.empty()); + + // Allocate some blocks + std::vector expected_blocks; + expected_blocks.push_back(manager->allocate_block().value()); + expected_blocks.push_back(manager->allocate_block().value()); + expected_blocks.push_back(manager->allocate_block().value()); + + // Get allocated blocks + allocated = manager->get_allocated_blocks(); + EXPECT_EQ(allocated.size(), expected_blocks.size()); + + // Verify all expected blocks are in the list + for (auto block_id : expected_blocks) { + EXPECT_TRUE(std::find(allocated.begin(), allocated.end(), block_id) != allocated.end()) + << "Block " << block_id << " should be in allocated list"; + } +} + +// Test 10: Invalid Block ID +TEST_F(KVCacheBlockManagerTest, InvalidBlockID) { + // Try to access block with ID >= max_blocks + EXPECT_THROW(manager->get_block_tensor(max_blocks), ov::Exception) << "Accessing invalid block ID should throw"; + + EXPECT_THROW(manager->get_block_tokens(max_blocks + 10), ov::Exception) + << "Accessing invalid block ID should throw"; + + EXPECT_THROW(manager->update_block_tokens(max_blocks, 100), ov::Exception) + << "Updating invalid block ID should throw"; +} + +// Test 11: Get Tensor After Clear (memory kept, state reset) +TEST_F(KVCacheBlockManagerTest, GetTensorAfterClear) { + auto block_id = manager->allocate_block(); + ASSERT_TRUE(block_id.has_value()); + + // Tensor is allocated on first allocate_block() + auto tensor = manager->get_block_tensor(block_id.value()); + EXPECT_NE(tensor, nullptr) << "Tensor should be valid after allocation"; + + // clear_all() resets state but keeps memory for reuse + manager->clear_all(); + auto block_id2 = manager->allocate_block(); + ASSERT_TRUE(block_id2.has_value()); + auto tensor2 = manager->get_block_tensor(block_id2.value()); + EXPECT_NE(tensor2, nullptr) << "Tensor should still be valid after clear + re-allocate"; +} + +} // namespace diff --git a/src/plugins/intel_npu/tests/unit/npuw/lincache_utils_test.cpp b/src/plugins/intel_npu/tests/unit/npuw/lincache_utils_test.cpp new file mode 100644 index 000000000000..e0576ea7240a --- /dev/null +++ b/src/plugins/intel_npu/tests/unit/npuw/lincache_utils_test.cpp @@ -0,0 +1,119 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include + +#include +#include + +#include "util.hpp" + +namespace { +// ===================== matchLinCacheString tests ===================== + +TEST(MatchLinCacheStringTest, MatchesPastConvCache) { + EXPECT_TRUE(ov::npuw::util::matchLinCacheString("cache_params.past.conv.0", "past")); + EXPECT_TRUE(ov::npuw::util::matchLinCacheString("cache_params.past.conv.99", "past")); +} + +TEST(MatchLinCacheStringTest, MatchesPresentConvCache) { + EXPECT_TRUE(ov::npuw::util::matchLinCacheString("cache_params.present.conv.0", "present")); + EXPECT_TRUE(ov::npuw::util::matchLinCacheString("cache_params.present.conv.99", "present")); +} + +TEST(MatchLinCacheStringTest, MatchesPastSsmCache) { + EXPECT_TRUE(ov::npuw::util::matchLinCacheString("cache_params.past.ssm.0", "past")); + EXPECT_TRUE(ov::npuw::util::matchLinCacheString("cache_params.past.ssm.5", "past")); +} + +TEST(MatchLinCacheStringTest, MatchesPresentSsmCache) { + EXPECT_TRUE(ov::npuw::util::matchLinCacheString("cache_params.present.ssm.0", "present")); + EXPECT_TRUE(ov::npuw::util::matchLinCacheString("cache_params.present.ssm.5", "present")); +} + +TEST(MatchLinCacheStringTest, DoesNotMatchKVCacheNames) { + EXPECT_FALSE(ov::npuw::util::matchLinCacheString("past_key_values.0.key", "past")); + EXPECT_FALSE(ov::npuw::util::matchLinCacheString("past_key_values.0.value", "past")); + EXPECT_FALSE(ov::npuw::util::matchLinCacheString("present.0.key", "present")); + EXPECT_FALSE(ov::npuw::util::matchLinCacheString("present.0.value", "present")); + EXPECT_FALSE(ov::npuw::util::matchLinCacheString("cache_params.past.key.0", "past")); + EXPECT_FALSE(ov::npuw::util::matchLinCacheString("cache_params.past.value.0", "past")); + EXPECT_FALSE(ov::npuw::util::matchLinCacheString("cache_params.present.key.0", "present")); + EXPECT_FALSE(ov::npuw::util::matchLinCacheString("cache_params.present.value.0", "present")); +} + +TEST(MatchLinCacheStringTest, PastDoesNotMatchPresentAndViceVersa) { + EXPECT_FALSE(ov::npuw::util::matchLinCacheString("cache_params.present.conv.0", "past")); + EXPECT_FALSE(ov::npuw::util::matchLinCacheString("cache_params.past.conv.0", "present")); +} + +TEST(MatchLinCacheStringTest, DoesNotMatchInvalidStrings) { + EXPECT_FALSE(ov::npuw::util::matchLinCacheString("", "past")); + EXPECT_FALSE(ov::npuw::util::matchLinCacheString("cache_params.past.conv", "past")); // no index + EXPECT_FALSE(ov::npuw::util::matchLinCacheString("cache_params.conv.0", "past")); // missing past/present + EXPECT_FALSE(ov::npuw::util::matchLinCacheString("random_string", "past")); +} + +TEST(MatchLinCacheStringTest, DefaultParameterIsPast) { + EXPECT_TRUE(ov::npuw::util::matchLinCacheString("cache_params.past.conv.0")); // default = "past" + EXPECT_FALSE(ov::npuw::util::matchLinCacheString("cache_params.present.conv.0")); // default = "past" +} + +// ===================== starts_with_past_lincache tests ===================== + +TEST(StartsWithPastLincacheTest, DetectsConvCacheNames) { + EXPECT_TRUE(ov::npuw::util::starts_with_past_lincache("cache_params.past.conv.0")); + EXPECT_TRUE(ov::npuw::util::starts_with_past_lincache("cache_params.past.conv.15")); +} + +TEST(StartsWithPastLincacheTest, DetectsSsmCacheNames) { + EXPECT_TRUE(ov::npuw::util::starts_with_past_lincache("cache_params.past.ssm.0")); + EXPECT_TRUE(ov::npuw::util::starts_with_past_lincache("cache_params.past.ssm.7")); +} + +TEST(StartsWithPastLincacheTest, KVCacheNotDetected) { + EXPECT_FALSE(ov::npuw::util::starts_with_past_lincache("past_key_values.0.key")); + EXPECT_FALSE(ov::npuw::util::starts_with_past_lincache("past_key_values.0.value")); +} + +TEST(StartsWithPastLincacheTest, PresentNamesNotDetected) { + EXPECT_FALSE(ov::npuw::util::starts_with_past_lincache("cache_params.present.conv.0")); + EXPECT_FALSE(ov::npuw::util::starts_with_past_lincache("cache_params.present.ssm.0")); +} + +TEST(StartsWithPastLincacheTest, NonCacheNamesNotDetected) { + EXPECT_FALSE(ov::npuw::util::starts_with_past_lincache("input_ids")); + EXPECT_FALSE(ov::npuw::util::starts_with_past_lincache("attention_mask")); + EXPECT_FALSE(ov::npuw::util::starts_with_past_lincache("")); +} + +TEST(StartsWithPastLincacheTest, ClassifiesInputNamesCorrectly) { + std::vector kvcache_names; + std::vector lincache_names; + + std::vector all_inputs = { + "past_key_values.0.key", + "past_key_values.0.value", + "past_key_values.1.key", + "past_key_values.1.value", + "cache_params.past.conv.0", + "cache_params.past.conv.1", + "cache_params.past.ssm.0", + "cache_params.past.ssm.1", + "input_ids", + "attention_mask" + }; + + for (const auto& name : all_inputs) { + if (ov::npuw::util::starts_with(name, "past_key_values")) { + kvcache_names.push_back(name); + } else if (ov::npuw::util::starts_with_past_lincache(name)) { + lincache_names.push_back(name); + } + } + + EXPECT_EQ(kvcache_names.size(), 4u); // 2 keys + 2 values from layer 1 + EXPECT_EQ(lincache_names.size(), 4u); // conv.0/1, ssm.0/1 +} +} // namespace diff --git a/src/plugins/intel_npu/tests/unit/npuw/llm_compiled_model_factory_options_test.cpp b/src/plugins/intel_npu/tests/unit/npuw/llm_compiled_model_factory_options_test.cpp index db183991ad0c..c15b6c8f2112 100644 --- a/src/plugins/intel_npu/tests/unit/npuw/llm_compiled_model_factory_options_test.cpp +++ b/src/plugins/intel_npu/tests/unit/npuw/llm_compiled_model_factory_options_test.cpp @@ -13,6 +13,7 @@ #include #include "llm_test_helpers.hpp" +#include "openvino/runtime/intel_npu/properties.hpp" #include "whisper/prepare_whisper_model.hpp" #include "openvino/pass/stateful_to_stateless.hpp" #include "unit_test_utils/mocks/openvino/runtime/mock_icore.hpp" @@ -22,6 +23,46 @@ using ov::test::npuw::CompileCall; using ov::test::npuw::NullPlugin; using ov::test::npuw::RecordingFactory; +class ArchAwarePlugin final : public NullPlugin { +public: + ArchAwarePlugin(std::string arch, int64_t max_tiles) : m_arch(std::move(arch)), m_max_tiles(max_tiles) {} + + ov::Any get_property(const std::string& name, const ov::AnyMap&) const override { + if (name == ov::device::architecture.name()) { + return m_arch; + } + if (name == ov::intel_npu::max_tiles.name()) { + return m_max_tiles; + } + if (name == ov::intel_npu::compiler_version.name()) { + return static_cast(0); + } + if (name == ov::supported_properties.name()) { + return std::vector{}; + } + return {}; + } + +private: + std::string m_arch; + int64_t m_max_tiles; +}; + +std::shared_ptr attach_mock_core_with_npu_device(const std::shared_ptr& plugin, + std::vector device_list = std::vector{"0"}) { + auto core = std::make_shared>(); + plugin->set_core(core); + + ON_CALL(*core, + get_property(testing::StrEq("NPU"), + testing::StrEq(ov::available_devices.name()), + testing::An())) + .WillByDefault([device_list](const std::string&, const std::string&, const ov::AnyMap&) -> ov::Any { + return ov::Any(device_list); + }); + return core; +} + class LLMCompiledModelFactoryOptionsTest : public ::testing::Test { protected: void SetUp() override { @@ -32,6 +73,10 @@ class LLMCompiledModelFactoryOptionsTest : public ::testing::Test { return ov::test::npuw::build_llm_test_model(); } + std::shared_ptr build_moe_llm_model() const { + return ov::test::npuw::build_moe_llm_test_model(); + } + std::shared_ptr build_whisper_decoder_model() const { return ov::test::npuw::build_whisper_decoder_test_model(); } @@ -40,6 +85,10 @@ class LLMCompiledModelFactoryOptionsTest : public ::testing::Test { return ov::test::npuw::build_embedding_test_model(); } + std::shared_ptr build_embedding_decoder_model() const { + return ov::test::npuw::build_embedding_decoder_test_model(); + } + static ov::AnyMap base_props() { return {{"NPUW_LLM", "YES"}, {"NPUW_LLM_MAX_PROMPT_LEN", "128"}, {"NPUW_LLM_MIN_RESPONSE_LEN", "64"}}; } @@ -76,6 +125,12 @@ class LLMCompiledModelFactoryOptionsTest : public ::testing::Test { return it->second.as(); } + static int64_t prop_i64(const ov::AnyMap& props, const std::string& key) { + const auto it = props.find(key); + OPENVINO_ASSERT(it != props.end(), "Missing property: ", key); + return it->second.as(); + } + static void expect_prop(const ov::AnyMap& props, const std::string& key, const std::string& expected) { EXPECT_EQ(prop_string(props, key), expected); } @@ -186,7 +241,9 @@ TEST_F(LLMCompiledModelFactoryOptionsTest, AttentionHintsPropagateToStageConfigs ov::AnyMap props = {{"NPUW_LLM_SHARED_HEAD", "NO"}, {"NPUW_LLM_PREFILL_ATTENTION_HINT", "PYRAMID"}, - {"NPUW_LLM_GENERATE_ATTENTION_HINT", "DYNAMIC"}}; + {"NPUW_LLM_GENERATE_ATTENTION_HINT", "DYNAMIC"}, + {"NPUW_LLM_PREFILL_HINT", "DYNAMIC"}, + {"NPUW_LLM_PREFILL_CHUNK_SIZE", "64"}}; ASSERT_NO_THROW(compiled = create_compiled_model(build_llm_model(), props, recorder)); ASSERT_NE(compiled, nullptr); @@ -241,7 +298,11 @@ TEST_F(LLMCompiledModelFactoryOptionsTest, DefaultStageConfigsCarryBaselineNpuwO RecordingFactory recorder; std::unique_ptr compiled; - ASSERT_NO_THROW(compiled = create_compiled_model(build_llm_model(), {{"NPUW_LLM_SHARED_HEAD", "YES"}}, recorder)); + ASSERT_NO_THROW(compiled = create_compiled_model(build_llm_model(), + {{"NPUW_LLM_SHARED_HEAD", "YES"}, + {"NPUW_LLM_PREFILL_HINT", "DYNAMIC"}, + {"NPUW_LLM_PREFILL_CHUNK_SIZE", "64"}}, + recorder)); ASSERT_NE(compiled, nullptr); const auto& prefill = require_call(recorder, "_prefill"); @@ -259,9 +320,16 @@ TEST_F(LLMCompiledModelFactoryOptionsTest, DefaultStageConfigsCarryBaselineNpuwO expect_prop(prefill.props, "NPUW_SLICE_OUT", "YES"); expect_prop(prefill.props, "NPUW_FUNCALL_ASYNC", "YES"); + expect_prop(prefill.props, "NPUW_ATTN", "PYRAMID"); + expect_prop(prefill.props, "NPUW_ONLINE_PIPELINE", "REP"); + expect_prop(prefill.props, "NPUW_ONLINE_ISOLATE", "ATTN"); + expect_prop(prefill.props, "NPUW_ONLINE_KEEP_BLOCK_SIZE", "4"); + expect_prop(prefill.props, "NPUW_UNFOLD_IREQS", "NO"); + expect_missing_prop(prefill.props, "NPUW_FALLBACK_EXEC"); expect_missing_prop(generate.props, "NPUW_SLICE_OUT"); expect_prop(generate.props, "NPUW_FUNCALL_ASYNC", "YES"); + expect_missing_prop(generate.props, "NPUW_ATTN"); expect_missing_prop(head.props, "NPUW_SLICE_OUT"); expect_missing_prop(head.props, "NPUW_FUNCALL_ASYNC"); @@ -271,6 +339,13 @@ TEST_F(LLMCompiledModelFactoryOptionsTest, DefaultStageConfigsCarryBaselineNpuwO EXPECT_EQ(prop_string(prefill.props, "NPUW_WEIGHTS_BANK"), prop_string(head.props, "NPUW_WEIGHTS_BANK")); } +TEST(NPUWAttentionHintOptionDefaultsTest, PrefillAndGenerateAttentionHintsHaveIndependentDefaults) { + EXPECT_EQ(::intel_npu::NPUW_LLM_PREFILL_ATTENTION_HINT::defaultValue(), + ::intel_npu::npuw::llm::AttentionHint::PYRAMID); + EXPECT_EQ(::intel_npu::NPUW_LLM_GENERATE_ATTENTION_HINT::defaultValue(), + ::intel_npu::npuw::llm::AttentionHint::STATIC); +} + TEST_F(LLMCompiledModelFactoryOptionsTest, CommonRuntimeAndDebugOptionsForwardToAllStages) { RecordingFactory recorder; std::unique_ptr compiled; @@ -325,6 +400,198 @@ TEST_F(LLMCompiledModelFactoryOptionsTest, CommonRuntimeAndDebugOptionsForwardTo } } +TEST_F(LLMCompiledModelFactoryOptionsTest, ArchIn5000SeriesSetsNpuTilesInDefaultStageConfigs) { + m_plugin = std::make_shared("5010", 3); + const auto core = attach_mock_core_with_npu_device(m_plugin); + + RecordingFactory recorder; + std::unique_ptr compiled; + + ASSERT_NO_THROW(compiled = create_compiled_model(build_llm_model(), {{"NPUW_LLM_SHARED_HEAD", "YES"}}, recorder)); + ASSERT_NE(compiled, nullptr); + + const auto& prefill = require_call(recorder, "_prefill"); + const auto& generate = require_call_containing(recorder, "_kv"); + const auto& head = require_call(recorder, "_lm_head"); + + for (const auto* call : {&prefill, &generate, &head}) { + EXPECT_EQ(prop_i64(call->props, "NPU_TILES"), 3); + expect_prop(call->props, + "NPU_COMPILATION_MODE_PARAMS", + "compute-layers-with-higher-precision=Sqrt,Power,ReduceMean,Add_RMSNorm"); + } +} + +struct UserStageConfigParam { + std::string arch; + int64_t max_tiles; // reported by the plugin (arch default) + int64_t user_tiles; // NPU_TILES value explicitly provided by the user + // Note: NPU_COMPILATION_MODE_PARAMS is a space-separated token string. + // Supplying it here performs a *full replace* of the arch-computed value + // (which may include tokens like optimization-level=3 or performance-hint-override=latency). + // The production code emits a LOG_WARN when the user value differs from the arch-computed one + // so the user is informed that arch-specific tokens will be lost unless explicitly repeated. + // A proper additive mechanism (analogous to ++NPUW_LLM_PREFILL_CONFIG) does not yet exist + // for this property. + std::string user_compile_params; // NPU_COMPILATION_MODE_PARAMS override; empty = not provided +}; + +class StageConfigUserOverrideTest : public LLMCompiledModelFactoryOptionsTest, + public ::testing::WithParamInterface {}; + +// User-provided NPU_TILES (and optionally NPU_COMPILATION_MODE_PARAMS) must take priority +// over the arch-computed defaults from set_max_tiles_based_on_arch, regardless of the NPU platform. +TEST_P(StageConfigUserOverrideTest, UserSettingsOverrideArchDefaults) { + const auto& param = GetParam(); + m_plugin = std::make_shared(param.arch, param.max_tiles); + const auto core = attach_mock_core_with_npu_device(m_plugin); + + RecordingFactory recorder; + std::unique_ptr compiled; + + ov::AnyMap extra_props = {{"NPUW_LLM_SHARED_HEAD", "YES"}, {"NPU_TILES", param.user_tiles}}; + if (!param.user_compile_params.empty()) { + extra_props["NPU_COMPILATION_MODE_PARAMS"] = param.user_compile_params; + } + + ASSERT_NO_THROW(compiled = create_compiled_model(build_llm_model(), extra_props, recorder)); + ASSERT_NE(compiled, nullptr); + + const auto& prefill = require_call(recorder, "_prefill"); + const auto& generate = require_call_containing(recorder, "_kv"); + const auto& head = require_call(recorder, "_lm_head"); + + for (const auto* call : {&prefill, &generate, &head}) { + EXPECT_EQ(call->props.count("NPU_TILES"), 1u) << "NPU_TILES must appear exactly once in compiled stage config"; + EXPECT_EQ(prop_i64(call->props, "NPU_TILES"), param.user_tiles); + + if (!param.user_compile_params.empty()) { + EXPECT_EQ(call->props.count("NPU_COMPILATION_MODE_PARAMS"), 1u) + << "NPU_COMPILATION_MODE_PARAMS must appear exactly once in compiled stage config"; + // Full replace: the user value entirely replaces the arch-computed string. + expect_prop(call->props, "NPU_COMPILATION_MODE_PARAMS", param.user_compile_params); + } + } +} + +INSTANTIATE_TEST_SUITE_P( + AllArchVariants, + StageConfigUserOverrideTest, + ::testing::Values( + // NPU 2700: arch default sets no tiles; user overrides NPU_TILES with 1 and 2 + UserStageConfigParam{"2700", 2, 1, ""}, + UserStageConfigParam{"2700", 2, 2, ""}, + // NPU 4000: arch default is max_tiles=4 + optimization-level=3; user fully replaces compile params + UserStageConfigParam{"4000", 4, 1, ""}, + UserStageConfigParam{"4000", 4, 2, "compute-layers-with-higher-precision=Sqrt,Power"}, + // NPU 5010: arch default is max_tiles=3; user overrides NPU_TILES and compile params + UserStageConfigParam{"5010", 3, 1, ""}, + UserStageConfigParam{"5010", 3, 2, "compute-layers-with-higher-precision=Sqrt,Power"}, + // NPU 5020: arch default is max_tiles=3; user overrides NPU_TILES and compile params + UserStageConfigParam{"5020", 3, 1, ""}, + UserStageConfigParam{"5020", 3, 2, "compute-layers-with-higher-precision=Sqrt,Power"}, + // AUTO_DETECT: arch default appends performance-hint-override=latency; user fully replaces + UserStageConfigParam{"AUTO_DETECT", 4, 1, ""}, + UserStageConfigParam{"AUTO_DETECT", 4, 2, "compute-layers-with-higher-precision=Sqrt,Power"}), + [](const ::testing::TestParamInfo& info) { + std::string name = info.param.arch; + std::replace(name.begin(), name.end(), '_', 'x'); + name += "_tiles" + std::to_string(info.param.user_tiles); + if (!info.param.user_compile_params.empty()) { + name += "_with_compile_params"; + } + return name; + }); + +TEST_F(LLMCompiledModelFactoryOptionsTest, Arch2700SkipsNpuTilesInDefaultStageConfigs) { + m_plugin = std::make_shared("2700", 2); + const auto core = attach_mock_core_with_npu_device(m_plugin); + + RecordingFactory recorder; + std::unique_ptr compiled; + + ASSERT_NO_THROW(compiled = create_compiled_model(build_llm_model(), {{"NPUW_LLM_SHARED_HEAD", "YES"}}, recorder)); + ASSERT_NE(compiled, nullptr); + + const auto& prefill = require_call(recorder, "_prefill"); + const auto& generate = require_call_containing(recorder, "_kv"); + const auto& head = require_call(recorder, "_lm_head"); + + for (const auto* call : {&prefill, &generate, &head}) { + expect_missing_prop(call->props, "NPU_TILES"); + expect_prop(call->props, + "NPU_COMPILATION_MODE_PARAMS", + "compute-layers-with-higher-precision=Sqrt,Power,ReduceMean,Add_RMSNorm"); + } +} +// avtual config for one of platform with embargo details. +TEST_F(LLMCompiledModelFactoryOptionsTest, ArchAutoDetectSkipsNpuTilesInDefaultStageConfigs) { + m_plugin = std::make_shared("AUTO_DETECT", 4); + const auto core = attach_mock_core_with_npu_device(m_plugin); + + RecordingFactory recorder; + std::unique_ptr compiled; + + ASSERT_NO_THROW(compiled = create_compiled_model(build_llm_model(), {{"NPUW_LLM_SHARED_HEAD", "YES"}}, recorder)); + ASSERT_NE(compiled, nullptr); + + const auto& prefill = require_call(recorder, "_prefill"); + const auto& generate = require_call_containing(recorder, "_kv"); + const auto& head = require_call(recorder, "_lm_head"); + + for (const auto* call : {&prefill, &generate, &head}) { + expect_missing_prop(call->props, "NPU_TILES"); + expect_prop(call->props, + "NPU_COMPILATION_MODE_PARAMS", + "compute-layers-with-higher-precision=Sqrt,Power,ReduceMean,Add_RMSNorm performance-hint-override=latency"); + } +} + +TEST_F(LLMCompiledModelFactoryOptionsTest, Arch4000AddsOptimizationLevelAndSetsNpuTiles) { + m_plugin = std::make_shared("4000", 4); + const auto core = attach_mock_core_with_npu_device(m_plugin); + + RecordingFactory recorder; + std::unique_ptr compiled; + + ASSERT_NO_THROW(compiled = create_compiled_model(build_llm_model(), {{"NPUW_LLM_SHARED_HEAD", "YES"}}, recorder)); + ASSERT_NE(compiled, nullptr); + + const auto& prefill = require_call(recorder, "_prefill"); + const auto& generate = require_call_containing(recorder, "_kv"); + const auto& head = require_call(recorder, "_lm_head"); + + for (const auto* call : {&prefill, &generate, &head}) { + EXPECT_EQ(prop_i64(call->props, "NPU_TILES"), 4); + expect_prop(call->props, + "NPU_COMPILATION_MODE_PARAMS", + "compute-layers-with-higher-precision=Sqrt,Power,ReduceMean,Add_RMSNorm optimization-level=3"); + } +} + +TEST_F(LLMCompiledModelFactoryOptionsTest, ArchAtLeast6000UsesDefaultConfig) { + m_plugin = std::make_shared("7000", 8); + const auto core = attach_mock_core_with_npu_device(m_plugin); + + RecordingFactory recorder; + std::unique_ptr compiled; + + ASSERT_NO_THROW(compiled = create_compiled_model(build_llm_model(), {{"NPUW_LLM_SHARED_HEAD", "YES"}}, recorder)); + ASSERT_NE(compiled, nullptr); + + const auto& prefill = require_call(recorder, "_prefill"); + const auto& generate = require_call_containing(recorder, "_kv"); + const auto& head = require_call(recorder, "_lm_head"); + + for (const auto* call : {&prefill, &generate, &head}) { + expect_missing_prop(call->props, "NPU_TILES"); + expect_prop(call->props, + "NPU_COMPILATION_MODE_PARAMS", + "compute-layers-with-higher-precision=Sqrt,Power,ReduceMean,Add_RMSNorm"); + } +} + + TEST_F(LLMCompiledModelFactoryOptionsTest, FastCompileGenerateHintKeepsCurrentGenerateDefaults) { RecordingFactory recorder; std::unique_ptr compiled; @@ -388,8 +655,8 @@ TEST_F(LLMCompiledModelFactoryOptionsTest, StaticAttentionHintsAvoidAttentionIso ASSERT_NE(compiled, nullptr); const auto& prefill = require_call(recorder, "_prefill"); const auto& generate = require_call_containing(recorder, "_kv"); - expect_prop(prefill.props, "NPUW_ATTN", "STATIC"); - expect_prop(generate.props, "NPUW_ATTN", "STATIC"); + expect_missing_prop(prefill.props, "NPUW_ATTN"); + expect_missing_prop(generate.props, "NPUW_ATTN"); // Static attention should leave partitioning defaults unchanged rather than forcing attention isolation. EXPECT_EQ(prefill.props.count("NPUW_ONLINE_PIPELINE"), 0u); EXPECT_EQ(generate.props.count("NPUW_ONLINE_PIPELINE"), 0u); @@ -405,7 +672,9 @@ TEST_F(LLMCompiledModelFactoryOptionsTest, HfaAttentionHintsEnableAttentionIsola {"NPUW_LLM_GENERATE_ATTENTION_HINT", "HFA"}, {"NPUW_ATTN_HFA_FUSED", "YES"}, {"NPUW_ATTN_DYN", "YES"}, - {"NPUW_ATTN_NO_COPY", "YES"}}, + {"NPUW_ATTN_NO_COPY", "YES"}, + {"NPUW_LLM_PREFILL_HINT", "DYNAMIC"}, + {"NPUW_LLM_PREFILL_CHUNK_SIZE", "64"}}, recorder)); ASSERT_NE(compiled, nullptr); const auto& prefill = require_call(recorder, "_prefill"); @@ -422,6 +691,31 @@ TEST_F(LLMCompiledModelFactoryOptionsTest, HfaAttentionHintsEnableAttentionIsola } } +TEST_F(LLMCompiledModelFactoryOptionsTest, MoeModelKeepsHostRoutedStageIntegration) { + RecordingFactory recorder; + std::unique_ptr compiled; + + ASSERT_NO_THROW(compiled = create_compiled_model(build_moe_llm_model(), + {{"NPUW_LLM_SHARED_HEAD", "NO"}, + {"NPUW_LLM_PREFILL_MOE_HINT", "HOST_ROUTED"}, + {"NPUW_LLM_GENERATE_MOE_HINT", "HOST_ROUTED"}}, + recorder)); + ASSERT_NE(compiled, nullptr); + + const auto& prefill = require_call(recorder, "_prefill"); + const auto& generate = require_call_containing(recorder, "_kv"); + + for (const auto* call : {&prefill, &generate}) { + expect_prop(call->props, "NPUW_ONLINE_PIPELINE", "REP"); + expect_prop(call->props, "NPUW_ONLINE_ISOLATE", "MOE"); + expect_prop(call->props, "NPUW_ONLINE_KEEP_BLOCK_SIZE", "4"); + expect_prop(call->props, "NPUW_UNFOLD_IREQS", "NO"); + } + + EXPECT_EQ(recorder.count_suffix("_prefill"), 1u); + EXPECT_EQ(recorder.count_contains("_kv"), 1u); +} + TEST_F(LLMCompiledModelFactoryOptionsTest, CacheRopeEnabledRoundsTripThroughCompiledModel) { RecordingFactory recorder; std::unique_ptr compiled; @@ -452,6 +746,56 @@ TEST_F(LLMCompiledModelFactoryOptionsTest, CacheRopeDisabledRoundsTripThroughCom EXPECT_EQ(recorder.count_contains("_kv"), 1u); } +// RopeCache replaces Sin/Cos with a Gather-from-LUT. When enabled the prefill +// sub-model must have no Sin/Cos nodes left. +TEST_F(LLMCompiledModelFactoryOptionsTest, CacheRopeEnabledRemovesSinCosFromPrefill) { + RecordingFactory recorder; + std::unique_ptr compiled; + + ASSERT_NO_THROW(compiled = create_compiled_model(build_llm_model(), + {{"NPUW_LLM_SHARED_HEAD", "NO"}, + {"NPUW_LLM_CACHE_ROPE", "YES"}, + {"NPUW_LLM_MAX_PROMPT_LEN", "2048"}}, + recorder)); + ASSERT_NE(compiled, nullptr); + + const auto* prefill = recorder.find_suffix("_prefill"); + ASSERT_NE(prefill, nullptr); + + const auto& ops = prefill->model->get_ops(); + auto sin_count = std::count_if(ops.begin(), ops.end(), + [](const auto& op) { return ov::is_type(op); }); + auto cos_count = std::count_if(ops.begin(), ops.end(), + [](const auto& op) { return ov::is_type(op); }); + EXPECT_EQ(sin_count, 0) << "RopeCache should have replaced all Sin nodes in the prefill model"; + EXPECT_EQ(cos_count, 0) << "RopeCache should have replaced all Cos nodes in the prefill model"; +} + +// When rope caching is disabled the prefill model must still contain Sin/Cos +// (i.e. the RoPE pattern is present but untransformed). +TEST_F(LLMCompiledModelFactoryOptionsTest, CacheRopeDisabledKeepsSinCosInPrefill) { + RecordingFactory recorder; + std::unique_ptr compiled; + + ASSERT_NO_THROW(compiled = create_compiled_model(build_llm_model(), + {{"NPUW_LLM_SHARED_HEAD", "NO"}, + {"NPUW_LLM_CACHE_ROPE", "NO"}, + {"NPUW_LLM_MAX_PROMPT_LEN", "2048"}}, + recorder)); + ASSERT_NE(compiled, nullptr); + + const auto* prefill = recorder.find_suffix("_prefill"); + ASSERT_NE(prefill, nullptr); + + const auto& ops = prefill->model->get_ops(); + auto sin_count = std::count_if(ops.begin(), ops.end(), + [](const auto& op) { return ov::is_type(op); }); + auto cos_count = std::count_if(ops.begin(), ops.end(), + [](const auto& op) { return ov::is_type(op); }); + EXPECT_GT(sin_count, 0) << "Sin nodes must remain when rope caching is disabled"; + EXPECT_GT(cos_count, 0) << "Cos nodes must remain when rope caching is disabled"; +} + TEST_F(LLMCompiledModelFactoryOptionsTest, WhisperOptionCompilesSyntheticDecoderModel) { RecordingFactory recorder; std::unique_ptr compiled; @@ -491,7 +835,8 @@ TEST_F(LLMCompiledModelFactoryOptionsTest, WhisperPrefillPreparationAddsCrossAtt model = model->clone(); EXPECT_TRUE(ov::npuw::util::PrepareWhisperPrefillModel( - 128, static_cast(ov::test::npuw::WhisperConfig{}.max_source_positions)) + 128, static_cast(ov::test::npuw::WhisperConfig{}.max_source_positions), + false /*decompose_sdpa*/) .run_on_model(model)); auto prepared = model; @@ -500,4 +845,17 @@ TEST_F(LLMCompiledModelFactoryOptionsTest, WhisperPrefillPreparationAddsCrossAtt EXPECT_TRUE(has_output_name(prepared, "present")); } +TEST_F(LLMCompiledModelFactoryOptionsTest, TextEmbedOptionCompilesEmbeddingDecoderModel) { + RecordingFactory recorder; + std::unique_ptr compiled; + + ASSERT_NO_THROW(compiled = create_compiled_model(build_embedding_decoder_model(), + {{"NPUW_TEXT_EMBED", "YES"}, + {"NPUW_LLM_SHARED_HEAD", "NO"}}, + recorder)); + ASSERT_NE(compiled, nullptr); + EXPECT_GE(recorder.calls().size(), 1u); + EXPECT_NE(recorder.find_suffix("_prefill"), nullptr); +} + } // namespace diff --git a/src/plugins/intel_npu/tests/unit/npuw/llm_test_helpers.hpp b/src/plugins/intel_npu/tests/unit/npuw/llm_test_helpers.hpp index f949403b9813..ae476dc98a7c 100644 --- a/src/plugins/intel_npu/tests/unit/npuw/llm_test_helpers.hpp +++ b/src/plugins/intel_npu/tests/unit/npuw/llm_test_helpers.hpp @@ -15,6 +15,7 @@ #include "model_builder.hpp" #include "openvino/op/fake_convert.hpp" #include "openvino/op/scaled_dot_product_attention.hpp" +#include "openvino/pass/stateful_to_stateless.hpp" #include "openvino/runtime/iplugin.hpp" #include "serialization.hpp" #include "weights_bank.hpp" @@ -38,6 +39,67 @@ inline std::shared_ptr build_llm_test_model() { return mb.build_llm(make_test_model_config()); } +/// Build an LLM test model configured so that Attention::from() can unambiguously +/// identify the past-KV dimension in the isolated attention function body. +/// +/// The default test config has num_kv_heads == kPastKvLen == 4 which makes the +/// KV-cache shape [1, 4, 4, 16] ambiguous — find_context_dim sees two dims equal +/// to past_len and returns false. By setting num_kv_heads=2 and using a past +/// length of 8, the KV shape becomes [1, 2, 8, 16] where only dim 2 equals +/// past_len=8, so Attention::from() succeeds and f._attention gets set. +inline std::shared_ptr build_dynamic_attention_llm_model() { + ov::test::npuw::LLMConfig cfg; + cfg.num_layers = 2; + cfg.hidden_size = 64; + cfg.num_heads = 4; + cfg.head_dim = 16; + cfg.num_kv_heads = 2; // must differ from kPastKvLen so find_context_dim is unambiguous + cfg.vocab_size = 256; + cfg.force_gqa_broadcast = true; // produces 5-input SDPA needed by the SDPA isolation pattern + + ModelBuilder mb; + auto model = mb.build_llm(cfg); + ov::pass::StatefulToStateless().run_on_model(model); + model = model->clone(); + + constexpr std::size_t kSeq = 4; + constexpr std::size_t kPast = 8; // != num_kv_heads (2), so KV shape [1,2,8,16] is unambiguous + + std::map new_shapes; + for (const auto& input : model->inputs()) { + const auto& name = input.get_any_name(); + const auto& pshape = input.get_partial_shape(); + if (name.find("input_ids") != std::string::npos || + name.find("token_type_ids") != std::string::npos) { + new_shapes[name] = ov::PartialShape{1, kSeq}; + } else if (name.find("attention_mask") != std::string::npos) { + new_shapes[name] = ov::PartialShape{1, kSeq + kPast}; + } else if (name.find("position_ids") != std::string::npos) { + new_shapes[name] = ov::PartialShape{1, kSeq}; + } else { + // KV cache params: fix batch=1, past_len=kPast, leave head/head_dim from pshape + auto static_shape = pshape; + static_shape[0] = 1; + static_shape[2] = kPast; + new_shapes[name] = static_shape; + } + } + model->reshape(new_shapes); + model->validate_nodes_and_infer_types(); + return model; +} + +inline LLMConfig make_test_model_config_gqa() { + auto cfg = make_test_model_config(); + cfg.num_kv_heads = 2; // num_heads=4 / num_kv_heads=2 -> n_rep=2 + return cfg; +} + +inline std::shared_ptr build_llm_gqa_test_model() { + ModelBuilder mb; + return mb.build_llm(make_test_model_config_gqa()); +} + inline std::shared_ptr build_llm_test_model_with_kv_fake_convert(const ov::element::Type fake_convert_type) { auto model = build_llm_test_model(); auto scale = ov::op::v0::Constant::create(ov::element::f32, ov::Shape{}, {1.0f}); @@ -76,6 +138,24 @@ inline std::shared_ptr build_embedding_test_model() { return mb.build_embedding_encoder(make_test_model_config()); } +inline std::shared_ptr build_embedding_decoder_test_model() { + ModelBuilder mb; + auto cfg = make_test_model_config(); + cfg.use_kv_cache = false; + cfg.internal_position_ids = true; + cfg.lm_head_weight = {}; + return mb.build_llm(cfg); +} + +inline std::shared_ptr build_moe_llm_test_model() { + ModelBuilder mb; + auto cfg = make_test_model_config(); + cfg.num_experts = 8; + cfg.num_experts_per_tok = 2; + return mb.build_llm(cfg); +} + + class NullPlugin : public ov::IPlugin { public: std::shared_ptr compile_model(const std::shared_ptr&, diff --git a/src/plugins/intel_npu/tests/unit/npuw/moe_transformation_test.cpp b/src/plugins/intel_npu/tests/unit/npuw/moe_transformation_test.cpp index 5f9cb677bc58..fa5bc7bd05a9 100644 --- a/src/plugins/intel_npu/tests/unit/npuw/moe_transformation_test.cpp +++ b/src/plugins/intel_npu/tests/unit/npuw/moe_transformation_test.cpp @@ -8,6 +8,8 @@ #include +#include "llm_test_helpers.hpp" +#include "model_builder.hpp" #include "openvino/op/ops.hpp" #include "openvino/pass/serialize.hpp" @@ -557,4 +559,47 @@ TEST_F(MoETransformationTest, AWQvsNonAWQComparison) { << "AWQ and non-AWQ models should have identical output shapes"; } +// ============================================================================ +// MoEFFN + build_llm E2E Tests (uses build_moe_llm_test_model from llm_test_helpers.hpp) +// ============================================================================ + +TEST_F(MoETransformationTest, BuildMoELLM_HasExpertAndRouterNodes) { + auto model = ov::test::npuw::build_moe_llm_test_model(); + ASSERT_NE(model, nullptr); + + bool has_tile = false, has_topk = false, has_reduce_sum = false, has_softmax = false; + size_t topk_router_count = 0, scatter_count = 0, softmax_router_count = 0; + + for (const auto& op : model->get_ordered_ops()) { + if (std::dynamic_pointer_cast(op)) + has_tile = true; + if (std::dynamic_pointer_cast(op)) + has_reduce_sum = true; + if (auto topk = std::dynamic_pointer_cast(op)) { + has_topk = true; + if (topk->get_friendly_name().find("router") != std::string::npos) { + EXPECT_EQ(topk->get_mode(), ov::op::v11::TopK::Mode::MAX); + topk_router_count++; + } + } + if (auto softmax = std::dynamic_pointer_cast(op)) { + has_softmax = true; + if (softmax->get_friendly_name().find("router") != std::string::npos) { + softmax_router_count++; + } + } + if (std::dynamic_pointer_cast(op)) + scatter_count++; + } + + EXPECT_TRUE(has_tile) << "Missing Tile (expert)"; + EXPECT_TRUE(has_topk) << "Missing TopK (router)"; + EXPECT_TRUE(has_reduce_sum) << "Missing ReduceSum (aggregation)"; + EXPECT_TRUE(has_softmax) << "Missing Softmax (router)"; + EXPECT_EQ(topk_router_count, 2u) << "One router TopK per layer"; + EXPECT_EQ(softmax_router_count, 2u) << "One router Softmax per layer"; + EXPECT_EQ(scatter_count, 2u) << "One ScatterElementsUpdate per layer"; +} + + } // namespace diff --git a/src/plugins/intel_npu/tests/unit/npuw/moe_transformation_utils_test.cpp b/src/plugins/intel_npu/tests/unit/npuw/moe_transformation_utils_test.cpp new file mode 100644 index 000000000000..59b6a2963ddf --- /dev/null +++ b/src/plugins/intel_npu/tests/unit/npuw/moe_transformation_utils_test.cpp @@ -0,0 +1,113 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "moe_transformations/moe_transformation_utils.hpp" + +#include + +#include "openvino/op/ops.hpp" + +/* + * Unit tests for ov::npuw::moe_utils::is_constant_derived() + * + * Covered cases: + * Positive: + * - Bare Constant node + * - Convert(Constant) + * - Multiply(Constant, Constant) + * - Convert(Multiply(Constant, Constant)) -- nested chain + * Negative: + * - nullptr input + * - Parameter (data-dependent) + * - Convert(Parameter) + * - Multiply(Constant, Parameter) + * - Multiply(Parameter, Constant) -- symmetric check + * - Add(Constant, Constant) -- unrecognized op type + */ + +namespace { + +using ov::npuw::moe_utils::is_constant_derived; + +// ── helpers ────────────────────────────────────────────────────────────────── + +static std::shared_ptr make_const_f32(ov::Shape shape) { + std::vector data(ov::shape_size(shape), 1.0f); + return ov::op::v0::Constant::create(ov::element::f32, shape, data); +} + +static std::shared_ptr make_param_f32(ov::PartialShape shape) { + return std::make_shared(ov::element::f32, shape); +} + +// ── Positive cases ──────────────────────────────────────────────────────────── + +TEST(IsconstantDerivedTest, BareConstant) { + auto c = make_const_f32({4, 16}); + EXPECT_TRUE(is_constant_derived(c)); +} + +TEST(IsconstantDerivedTest, ConvertOfConstant) { + auto c = make_const_f32({4, 16}); + auto conv = std::make_shared(c, ov::element::f16); + EXPECT_TRUE(is_constant_derived(conv)); +} + +TEST(IsconstantDerivedTest, MultiplyTwoConstants) { + auto c0 = make_const_f32({4, 16}); + auto c1 = make_const_f32({4, 1}); + auto mul = std::make_shared(c0, c1); + EXPECT_TRUE(is_constant_derived(mul)); +} + +TEST(IsconstantDerivedTest, NestedConvertMultiplyConstants) { + // Convert(Multiply(Constant, Constant)) — typical NF4 weight dequant chain + auto c0 = make_const_f32({4, 16}); + auto c1 = make_const_f32({4, 1}); + auto mul = std::make_shared(c0, c1); + auto conv = std::make_shared(mul, ov::element::f16); + EXPECT_TRUE(is_constant_derived(conv)); +} + +// ── Negative cases ──────────────────────────────────────────────────────────── + +TEST(IsconstantDerivedTest, NullInput) { + EXPECT_FALSE(is_constant_derived(nullptr)); +} + +TEST(IsconstantDerivedTest, BareParameter) { + auto p = make_param_f32({1, 16}); + EXPECT_FALSE(is_constant_derived(p)); +} + +TEST(IsconstantDerivedTest, ConvertOfParameter) { + auto p = make_param_f32({1, 16}); + auto conv = std::make_shared(p, ov::element::f16); + EXPECT_FALSE(is_constant_derived(conv)); +} + +TEST(IsconstantDerivedTest, MultiplyConstantAndParameter) { + auto c = make_const_f32({4, 1}); + auto p = make_param_f32({4, 16}); + auto mul = std::make_shared(c, p); + EXPECT_FALSE(is_constant_derived(mul)); +} + +TEST(IsconstantDerivedTest, MultiplyParameterAndConstant) { + // Symmetric: swapped operand order must also return false + auto p = make_param_f32({4, 16}); + auto c = make_const_f32({4, 1}); + auto mul = std::make_shared(p, c); + EXPECT_FALSE(is_constant_derived(mul)); +} + +TEST(IsconstantDerivedTest, UnrecognizedOpType) { + // Add is not handled by is_constant_derived — must return false + auto c0 = make_const_f32({4}); + auto c1 = make_const_f32({4}); + auto add = std::make_shared(c0, c1); + EXPECT_FALSE(is_constant_derived(add)); +} + +} // namespace diff --git a/src/plugins/intel_npu/tests/unit/npuw/npuw_options_smoke_test.cpp b/src/plugins/intel_npu/tests/unit/npuw/npuw_options_smoke_test.cpp index fc9982135690..a48cf6af5f5f 100644 --- a/src/plugins/intel_npu/tests/unit/npuw/npuw_options_smoke_test.cpp +++ b/src/plugins/intel_npu/tests/unit/npuw/npuw_options_smoke_test.cpp @@ -100,6 +100,7 @@ std::vector make_cases() { string_case<::intel_npu::NPUW_ONLINE_DUMP_PLAN>("NPUW_ONLINE_DUMP_PLAN", "/tmp/plan.xml", "/tmp/plan.xml"), string_case<::intel_npu::NPUW_PLAN>("NPUW_PLAN", "/tmp/plan.xml", "/tmp/plan.xml"), bool_case<::intel_npu::NPUW_FOLD>("NPUW_FOLD", "YES", true), + string_case<::intel_npu::NPUW_FOLD_ONLY>("NPUW_FOLD_ONLY", "attn,compute", "attn,compute"), bool_case<::intel_npu::NPUW_CWAI>("NPUW_CWAI", "YES", true), bool_case<::intel_npu::NPUW_DQ>("NPUW_DQ", "YES", true), bool_case<::intel_npu::NPUW_DQ_FULL>("NPUW_DQ_FULL", "NO", false), @@ -121,6 +122,7 @@ std::vector make_cases() { bool_case<::intel_npu::NPUW_ATTN_NO_COPY>("NPUW_ATTN_NO_COPY", "YES", true), bool_case<::intel_npu::NPUW_ATTN_HFA_FUSED>("NPUW_ATTN_HFA_FUSED", "YES", true), bool_case<::intel_npu::NPUW_PARALLEL_COMPILE>("NPUW_PARALLEL_COMPILE", "YES", true), + bool_case<::intel_npu::NPUW_ENSURE_COMPATIBILITY>("NPUW_ENSURE_COMPATIBILITY", "YES", true), bool_case<::intel_npu::NPUW_FUNCALL_ASYNC>("NPUW_FUNCALL_ASYNC", "YES", true), bool_case<::intel_npu::NPUW_UNFOLD_IREQS>("NPUW_UNFOLD_IREQS", "YES", true), bool_case<::intel_npu::NPUW_FALLBACK_EXEC>("NPUW_FALLBACK_EXEC", "NO", false), @@ -202,4 +204,11 @@ std::string case_name(const testing::TestParamInfo& info) { INSTANTIATE_TEST_SUITE_P(NPUWOptions, SmokeTest, ::testing::ValuesIn(make_cases()), case_name); +TEST(NPUWConfigOptionsSmokeTest, AttentionHintDefaultsCanDifferPerOption) { + const auto cfg = make_config(); + + EXPECT_EQ(cfg.getString<::intel_npu::NPUW_LLM_PREFILL_ATTENTION_HINT>(), "PYRAMID"); + EXPECT_EQ(cfg.getString<::intel_npu::NPUW_LLM_GENERATE_ATTENTION_HINT>(), "STATIC"); +} + } // namespace diff --git a/src/plugins/intel_npu/tests/unit/npuw/online_partitioning.cpp b/src/plugins/intel_npu/tests/unit/npuw/online_partitioning.cpp index 10a9e761a42a..436bb3d29392 100644 --- a/src/plugins/intel_npu/tests/unit/npuw/online_partitioning.cpp +++ b/src/plugins/intel_npu/tests/unit/npuw/online_partitioning.cpp @@ -125,6 +125,7 @@ TEST(OnlinePartitioningTest, Partitioning_IsTheSame_SmallModel) { } } + TEST(OnlinePartitioningTest, Partitioning_IsTheSame_RepeatedModel) { ModelBuilder mb; auto model = mb.get_model_with_repeated_blocks(); diff --git a/src/plugins/intel_npu/tests/unit/npuw/orc.cpp b/src/plugins/intel_npu/tests/unit/npuw/orc.cpp new file mode 100644 index 000000000000..c17a3dafa02b --- /dev/null +++ b/src/plugins/intel_npu/tests/unit/npuw/orc.cpp @@ -0,0 +1,314 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "orc.hpp" + +#include + +#include +#include +#include + +namespace { + +using namespace ov::npuw::orc; + +// Small sequential TypeIds that fit in u16. +constexpr TypeId TYPE_NCMD = 0x0001u; +constexpr TypeId TYPE_META = 0x0002u; +constexpr TypeId TYPE_PART = 0x0003u; +constexpr TypeId TYPE_WGHT = 0x0004u; +constexpr TypeId TYPE_TEST = 0x0005u; +constexpr TypeId TYPE_UNKNOWN = 0xFFFFu; + +const SchemaUUID TEST_UUID = + {0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF, 0xFE, 0xDC, 0xBA, 0x98, 0x76, 0x54, 0x32, 0x10}; + +struct BlobSummary { + std::uint32_t subgraph_count = 0u; + std::vector devices; +}; + +void serialize(Stream& stream, BlobSummary& value) { + stream & value.subgraph_count & value.devices; +} + +struct MetaV1 { + static constexpr Version kVersion = 1u; + + std::uint32_t subgraph_count = 0u; + std::vector devices; + + void serialize(Stream& stream) { + stream & subgraph_count & devices; + } +}; + +struct MetaV2 : MetaV1 { + ORC_DECLARE_VERSION(MetaV2, MetaV1) + + bool weightless = false; + + void serialize(Stream& stream) { + Prev::serialize(stream); + stream & weightless; + } +}; + +struct MetaV3 : MetaV2 { + ORC_DECLARE_VERSION(MetaV3, MetaV2) + + std::string layout; + + void serialize(Stream& stream) { + Prev::serialize(stream); + stream & layout; + } +}; + +void expect_blob_summary(const BlobSummary& expected, const BlobSummary& actual) { + EXPECT_EQ(expected.subgraph_count, actual.subgraph_count); + EXPECT_EQ(expected.devices, actual.devices); +} + +struct CounterMeta { + static constexpr Version kVersion = 0u; + std::uint32_t value = 0u; + + // Tiny fixed payload used to prove that scoped sections can stream inline metadata + // before nested ORC children without going through the higher-level Section tree API. + void serialize(Stream& stream) { + stream & value; + } +}; + +} // namespace + +TEST(OrcTest, FileRoundTripsNestedSections) { + const BlobSummary meta{2u, {"NPU"}}; + const BlobSummary part{7u, {"prefill", "decode"}}; + const BlobSummary wght{1u, {"lazy"}}; + + const auto root = + Section::container(TYPE_NCMD, + 1u, + {make_payload_section(TYPE_META, 1u, meta), + Section::container(TYPE_PART, 1u, {make_payload_section(TYPE_TEST, 1u, part)}), + make_payload_section(TYPE_WGHT, 1u, wght)}); + + std::stringstream buffer(std::ios::in | std::ios::out | std::ios::binary); + write_file(buffer, root, TEST_UUID); + + const auto decoded = read_file(buffer); + ASSERT_TRUE(decoded.is_container()); + ASSERT_EQ(decoded.type, TYPE_NCMD); + ASSERT_EQ(decoded.children.size(), 3u); + + EXPECT_EQ(decoded.children[0].type, TYPE_META); + expect_blob_summary(meta, decode(decoded.children[0].payload)); + + EXPECT_TRUE(decoded.children[1].is_container()); + ASSERT_EQ(decoded.children[1].children.size(), 1u); + expect_blob_summary(part, decode(decoded.children[1].children[0].payload)); + + EXPECT_EQ(decoded.children[2].type, TYPE_WGHT); + expect_blob_summary(wght, decode(decoded.children[2].payload)); +} + +TEST(OrcTest, RejectsTruncatedFile) { + const auto root = + Section::container(TYPE_NCMD, 1u, {make_payload_section(TYPE_META, 1u, BlobSummary{3u, {"NPU"}})}); + + std::stringstream buffer(std::ios::in | std::ios::out | std::ios::binary); + write_file(buffer, root, TEST_UUID); + + auto bytes = buffer.str(); + ASSERT_FALSE(bytes.empty()); + bytes.pop_back(); + + std::stringstream truncated(bytes, std::ios::in | std::ios::out | std::ios::binary); + EXPECT_THROW(read_file(truncated), ov::Exception); +} + +TEST(OrcTest, ScopedSectionsRoundTripMetadataBeforeChildren) { + std::stringstream buffer(std::ios::in | std::ios::out | std::ios::binary); + write_file_header(buffer, TEST_UUID); + with_section(buffer, TYPE_NCMD, CounterMeta::kVersion, 0u, [&] { + auto root_stream = Stream::writer(buffer); + CounterMeta root_meta{2u}; + root_stream & root_meta; + + with_section(buffer, TYPE_TEST, CounterMeta::kVersion, 0u, [&] { + auto child_stream = Stream::writer(buffer); + CounterMeta child_meta{7u}; + child_stream & child_meta; + }); + }); + + EXPECT_EQ(read_file_header(buffer).schema_uuid, TEST_UUID); + + ScopedReadSection root(buffer); + EXPECT_EQ(root.header().type, TYPE_NCMD); + auto root_stream = Stream::reader(buffer); + CounterMeta root_meta; + root_stream & root_meta; + EXPECT_EQ(root_meta.value, 2u); + + ScopedReadSection child(buffer); + EXPECT_EQ(child.header().type, TYPE_TEST); + auto child_stream = Stream::reader(buffer); + CounterMeta child_meta; + child_stream & child_meta; + EXPECT_EQ(child_meta.value, 7u); + child.expect_end(); + root.expect_end(); +} + +TEST(OrcTest, IsOrcReturnsTrueForValidBlob) { + const auto root = + Section::container(TYPE_NCMD, 1u, {make_payload_section(TYPE_META, 1u, BlobSummary{1u, {"NPU"}})}); + + std::stringstream buffer(std::ios::in | std::ios::out | std::ios::binary); + write_file(buffer, root, TEST_UUID); + + // is_orc must return a header and leave the stream at its original position + const auto header = is_orc(buffer); + ASSERT_TRUE(header.has_value()); + EXPECT_EQ(header->version, 0u); + EXPECT_EQ(header->schema_uuid, TEST_UUID); + // read_file must still work after the probe + EXPECT_NO_THROW(read_file(buffer)); +} + +TEST(OrcTest, IsOrcReturnsFalseForGarbage) { + std::stringstream buffer("not an orc blob", std::ios::in | std::ios::out | std::ios::binary); + EXPECT_FALSE(is_orc(buffer).has_value()); +} + +TEST(OrcTest, TryReadBytesAdvancesOnSuccessAndRollsBackOnFailure) { + std::stringstream buffer(std::ios::in | std::ios::out | std::ios::binary); + buffer.write("abcdef", 6); + + std::array prefix{}; + EXPECT_TRUE(try_read_bytes(buffer, prefix.data(), prefix.size())); + EXPECT_EQ(std::string(prefix.data(), prefix.size()), "abc"); + EXPECT_EQ(buffer.tellg(), std::streampos{3}); + + std::array oversized{}; + const auto pos_before_failure = buffer.tellg(); + EXPECT_FALSE(try_read_bytes(buffer, oversized.data(), oversized.size())); + EXPECT_EQ(buffer.tellg(), pos_before_failure); +} + +TEST(OrcTest, SchemaSkipsUnknownOptionalChildren) { + const auto root = Section::container(TYPE_NCMD, + 1u, + {make_payload_section(TYPE_META, 1u, BlobSummary{2u, {"NPU"}}), + Section::raw(TYPE_UNKNOWN, + 1u, + std::vector{std::byte{0xAB}, std::byte{0xCD}}, + static_cast(SectionFlag::OPTIONAL)), + make_payload_section(TYPE_WGHT, 1u, BlobSummary{1u, {"lazy"}})}); + + Schema schema; + schema.register_loader(TYPE_META, [](const Section& section, const Schema&) { + return decode(section.payload); + }); + schema.register_loader(TYPE_WGHT, [](const Section& section, const Schema&) { + return decode(section.payload); + }); + + const auto children = schema.load_children(root); + ASSERT_EQ(children.size(), 2u); + EXPECT_EQ(children[0].type, TYPE_META); + EXPECT_EQ(children[1].type, TYPE_WGHT); + + expect_blob_summary(BlobSummary{2u, {"NPU"}}, std::any_cast(children[0].value)); + expect_blob_summary(BlobSummary{1u, {"lazy"}}, std::any_cast(children[1].value)); +} + +TEST(OrcTest, SchemaRejectsUnknownRequiredChildren) { + const auto root = Section::container(TYPE_NCMD, + 1u, + {make_payload_section(TYPE_META, 1u, BlobSummary{2u, {"NPU"}}), + Section::raw(TYPE_UNKNOWN, 1u, std::vector{std::byte{0xAA}})}); + + Schema schema; + schema.register_loader(TYPE_META, [](const Section& section, const Schema&) { + return decode(section.payload); + }); + + EXPECT_THROW(schema.load_children(root), ov::Exception); +} + +TEST(OrcTest, SchemaRejectsDuplicateRegistration) { + Schema schema; + schema.register_loader(TYPE_META, [](const Section& section, const Schema&) { + return decode(section.payload); + }); + + EXPECT_THROW( + { + schema.register_loader(TYPE_META, [](const Section& section, const Schema&) { + return decode(section.payload); + }); + }, + ov::Exception); +} + +TEST(OrcTest, SchemaRejectsTypedMismatch) { + const auto section = make_payload_section(TYPE_META, 1u, BlobSummary{2u, {"NPU"}}); + + Schema schema; + schema.register_loader(TYPE_META, [](const Section& payload, const Schema&) { + return decode(payload.payload); + }); + + EXPECT_THROW(schema.load(section), ov::Exception); +} + +TEST(OrcTest, LoadVersionedPayloadMigratesAcrossVersions) { + Schema schema; + schema.register_loader(TYPE_META, [](const Section& section, const Schema&) { + return load_versioned_payload(section); + }); + + const auto meta_v1 = + schema.load(make_payload_section(TYPE_META, MetaV1::kVersion, MetaV1{3u, {"NPU", "CPU"}})); + EXPECT_EQ(meta_v1.subgraph_count, 3u); + EXPECT_EQ(meta_v1.devices, (std::vector{"NPU", "CPU"})); + EXPECT_FALSE(meta_v1.weightless); + EXPECT_TRUE(meta_v1.layout.empty()); + + MetaV2 v2; + v2.subgraph_count = 4u; + v2.devices = {"NPU"}; + v2.weightless = true; + const auto meta_v2 = schema.load(make_payload_section(TYPE_META, MetaV2::kVersion, v2)); + EXPECT_EQ(meta_v2.subgraph_count, 4u); + EXPECT_EQ(meta_v2.devices, (std::vector{"NPU"})); + EXPECT_TRUE(meta_v2.weightless); + EXPECT_TRUE(meta_v2.layout.empty()); + + MetaV3 v3; + v3.subgraph_count = 5u; + v3.devices = {"NPU", "GPU"}; + v3.weightless = true; + v3.layout = "split"; + const auto meta_v3 = schema.load(make_payload_section(TYPE_META, MetaV3::kVersion, v3)); + EXPECT_EQ(meta_v3.subgraph_count, 5u); + EXPECT_EQ(meta_v3.devices, (std::vector{"NPU", "GPU"})); + EXPECT_TRUE(meta_v3.weightless); + EXPECT_EQ(meta_v3.layout, "split"); +} + +TEST(OrcTest, LoadVersionedPayloadRejectsUnsupportedVersion) { + Schema schema; + schema.register_loader(TYPE_META, [](const Section& section, const Schema&) { + return load_versioned_payload(section); + }); + + const auto unsupported = make_payload_section(TYPE_META, 99u, MetaV1{1u, {"NPU"}}); + EXPECT_THROW(schema.load(unsupported), ov::Exception); +} diff --git a/src/plugins/intel_npu/tests/unit/npuw/partitioning_options_test.cpp b/src/plugins/intel_npu/tests/unit/npuw/partitioning_options_test.cpp index 3b6d3fd5c939..45625ae1414c 100644 --- a/src/plugins/intel_npu/tests/unit/npuw/partitioning_options_test.cpp +++ b/src/plugins/intel_npu/tests/unit/npuw/partitioning_options_test.cpp @@ -112,11 +112,112 @@ std::shared_ptr build_static_generate_model() { return build_static_llm_model(1, 2047); } +std::shared_ptr build_static_attention_llm_model() { + LLMConfig config; + config.num_layers = 2; + config.hidden_size = 64; + config.num_heads = 4; + config.head_dim = 16; + config.num_kv_heads = 2; + config.vocab_size = 256; + config.force_gqa_broadcast = true; + + ModelBuilder mb; + auto model = mb.build_llm(config); + + ov::pass::StatefulToStateless().run_on_model(model); + model = model->clone(); + + constexpr int64_t query_len = 4; + constexpr int64_t past_len = 8; + std::map new_shapes; + for (const auto& input : model->inputs()) { + const auto& name = input.get_any_name(); + auto shape = input.get_partial_shape(); + if (name.find("input_ids") != std::string::npos || name.find("token_type_ids") != std::string::npos) { + new_shapes[name] = {1, query_len}; + } else if (name.find("attention_mask") != std::string::npos) { + new_shapes[name] = {1, query_len + past_len}; + } else if (name.find("position_ids") != std::string::npos) { + new_shapes[name] = {1, query_len}; + } else { + shape[0] = 1; + shape[2] = past_len; + new_shapes[name] = shape; + } + } + model->reshape(new_shapes); + model->validate_nodes_and_infer_types(); + return model; +} + +std::shared_ptr build_static_attention_mixed_llm_model() { + LLMConfig config; + config.num_layers = 4; + config.hidden_size = 64; + config.num_heads = 4; + config.head_dim = 16; + config.num_kv_heads = 2; + config.vocab_size = 256; + config.force_gqa_broadcast = true; + + ModelBuilder mb; + auto model = mb.build_llm(config); + + ov::pass::StatefulToStateless().run_on_model(model); + model = model->clone(); + + constexpr int64_t query_len = 4; + constexpr int64_t past_len = 8; + std::map new_shapes; + for (const auto& input : model->inputs()) { + const auto& name = input.get_any_name(); + auto shape = input.get_partial_shape(); + if (name.find("input_ids") != std::string::npos || name.find("token_type_ids") != std::string::npos) { + new_shapes[name] = {1, query_len}; + } else if (name.find("attention_mask") != std::string::npos) { + new_shapes[name] = {1, query_len + past_len}; + } else if (name.find("position_ids") != std::string::npos) { + new_shapes[name] = {1, query_len}; + } else { + shape[0] = 1; + shape[2] = past_len; + new_shapes[name] = shape; + } + } + model->reshape(new_shapes); + model->validate_nodes_and_infer_types(); + return model; +} + std::shared_ptr build_repeated_model(std::size_t repetitions = 10) { ModelBuilder mb; return mb.get_model_with_repeated_blocks(repetitions); } +// Build a model with N repetitions of (Relu -> Sigmoid -> Tanh). +// Each op type forms its own isolated tag so mergeTriangles cannot merge the +// three families into one combined repeating block. +std::shared_ptr build_abc_attn_model(std::size_t repetitions = 30) { + auto input = std::make_shared(ov::element::f32, ov::Shape{1, 32}); + input->set_friendly_name("input"); + + std::shared_ptr prev = input; + for (std::size_t i = 0; i < repetitions; ++i) { + auto relu = std::make_shared(prev); + relu->set_friendly_name("relu_" + std::to_string(i)); + auto sigmoid = std::make_shared(relu); + sigmoid->set_friendly_name("sigmoid_" + std::to_string(i)); + auto tanh = std::make_shared(sigmoid); + tanh->set_friendly_name("tanh_" + std::to_string(i)); + prev = tanh; + } + + auto result = std::make_shared(prev); + result->set_friendly_name("output"); + return std::make_shared(ov::ResultVector{result}, ov::ParameterVector{input}); +} + TEST(PartitioningOptionsTest, PipelineNoneMergesUnaryModelIntoSingleGroup) { auto cfg = make_cfg({{"NPUW_ONLINE_PIPELINE", "NONE"}}); auto ens = ov::npuw::online::buildPartitioning(build_unary_chain_model(), cfg); @@ -270,6 +371,57 @@ TEST(PartitioningOptionsTest, CwaiCreatesFunctionCallsForRepeatedBlocks) { })); } +TEST(PartitioningOptionsTest, FoldOnlyProcessesTaggedRepeatedFamiliesWithoutCwai) { + auto cfg = make_cfg({{"NPUW_ONLINE_PIPELINE", "REP"}, + {"NPUW_ONLINE_ISOLATE", "ATTN"}, + {"NPUW_ATTN", "DYNAMIC"}, + {"NPUW_ONLINE_KEEP_BLOCKS", "2"}, + {"NPUW_ONLINE_KEEP_BLOCK_SIZE", "1"}, + {"NPUW_FOLD_ONLY", "attn"}}); + auto partitioning = ov::npuw::getPartitioning(build_static_attention_llm_model(), cfg); + + EXPECT_FALSE(partitioning.functions.empty()); + EXPECT_TRUE(std::any_of(partitioning.subgraphs.begin(), partitioning.subgraphs.end(), [](const ov::npuw::Subgraph& sg) { + return !sg._funcall.empty(); + })); +} + +TEST(PartitioningOptionsTest, FoldOnlyAndCwaiProcessTaggedAndUntaggedRepeatedFamilies) { + const auto base_cfg = ::intel_npu::Config::ConfigMap{{"NPUW_ONLINE_PIPELINE", "REP"}, + {"NPUW_ONLINE_ISOLATE", "COMPUTE,ATTN"}, + {"NPUW_ONLINE_KEEP_BLOCKS", "2"}, + {"NPUW_ONLINE_KEEP_BLOCK_SIZE", "1"}, + {"NPUW_FOLD_ONLY", "attn"}, + {"NPUW_ATTN", "STATIC"}}; + auto fold_only_cfg = make_cfg(base_cfg); + auto fold_only_partitioning = ov::npuw::getPartitioning(build_static_attention_mixed_llm_model(), fold_only_cfg); + + auto mixed_cfg = base_cfg; + mixed_cfg["NPUW_CWAI"] = "YES"; + auto mixed_cfg_obj = make_cfg(mixed_cfg); + auto mixed_partitioning = ov::npuw::getPartitioning(build_static_attention_mixed_llm_model(), mixed_cfg_obj); + + EXPECT_TRUE(std::any_of(mixed_partitioning.functions.begin(), mixed_partitioning.functions.end(), [](const auto& func) { + return func.second.gettag() == "attn"; + })); + + const auto has_cwai_function = [](const ov::npuw::Partitioning& partitioning) { + return std::any_of(partitioning.functions.begin(), partitioning.functions.end(), [](const auto& func) { + return func.first.find("__") != std::string::npos; + }); + }; + const auto has_cwai_funcall = [](const ov::npuw::Partitioning& partitioning) { + return std::any_of(partitioning.subgraphs.begin(), partitioning.subgraphs.end(), [](const ov::npuw::Subgraph& sg) { + return sg._funcall.find("__") != std::string::npos; + }); + }; + + EXPECT_FALSE(has_cwai_function(fold_only_partitioning)); + EXPECT_FALSE(has_cwai_funcall(fold_only_partitioning)); + EXPECT_TRUE(has_cwai_function(mixed_partitioning)); + EXPECT_TRUE(has_cwai_funcall(mixed_partitioning)); +} + TEST(PartitioningOptionsTest, PlanFileReusesDumpedPartitioningStructure) { const auto plan_path = make_unique_temp_path("npuw_partitioning_effect_plan", ".xml"); @@ -285,6 +437,55 @@ TEST(PartitioningOptionsTest, PlanFileReusesDumpedPartitioningStructure) { std::filesystem::remove(plan_path); } +// Isolate three op families with distinct tags so that mergeTriangles cannot +// collapse them into a single combined repeating block: +// blockA = Relu, blockB = Sigmoid, attn = Tanh +// With N=30 we get 3*N=90 frozen groups after repeatedBlocks. +static const ::intel_npu::Config::ConfigMap abc_attn_base_cfg = { + {"NPUW_ONLINE_PIPELINE", "REP"}, + {"NPUW_ONLINE_ISOLATE", "Op:Relu/blockA,Op:Sigmoid/blockB,Op:Tanh/attn"}, + {"NPUW_ONLINE_KEEP_BLOCKS", "3"}, + {"NPUW_ONLINE_KEEP_BLOCK_SIZE", "1"}, + {"NPUW_FOLD_ONLY", "attn"}, +}; + +TEST(PartitioningOptionsTest, FoldOnlyWithIsolatedTagsProducesExpectedSubgraphCount) { + // Baseline: FOLD_ONLY folds the 30 attn blocks; blockA and blockB remain + // as individual non-folded subgraphs → 3*30 = 90 subgraphs, 30 with funcalls. + constexpr std::size_t N = 30; + auto cfg = make_cfg(abc_attn_base_cfg); + auto partitioning = ov::npuw::getPartitioning(build_abc_attn_model(N), cfg); + + EXPECT_EQ(partitioning.subgraphs.size(), 3u * N); + + std::size_t folded = std::count_if(partitioning.subgraphs.begin(), + partitioning.subgraphs.end(), + [](const ov::npuw::Subgraph& sg) { + return !sg._funcall.empty(); + }); + EXPECT_EQ(folded, N); +} + +TEST(PartitioningOptionsTest, FuseUnfoldedMergesNonFoldOnlyRepeatedBlocks) { + // With NPUW_FUSE_UNFOLDED, blockA (Relu) and blockB (Sigmoid) groups lose + // their reptag and are merged by fuseRemnants (frozen attn blocks act as + // barriers). Result: 30 merged(blockA+blockB) + 30 folded attn = 2*30 = 60. + constexpr std::size_t N = 30; + auto ext_cfg = abc_attn_base_cfg; + ext_cfg["NPUW_FUSE_UNFOLDED"] = "YES"; + auto cfg = make_cfg(ext_cfg); + auto partitioning = ov::npuw::getPartitioning(build_abc_attn_model(N), cfg); + + EXPECT_EQ(partitioning.subgraphs.size(), 2u * N); + + std::size_t folded = std::count_if(partitioning.subgraphs.begin(), + partitioning.subgraphs.end(), + [](const ov::npuw::Subgraph& sg) { + return !sg._funcall.empty(); + }); + EXPECT_EQ(folded, N); +} + #ifdef NPU_PLUGIN_DEVELOPER_BUILD TEST(PartitioningOptionsTest, DumpFullWritesModelXmlIntoCurrentDirectory) { const auto temp_dir = make_unique_temp_path("npuw_dump_full_effect", ""); diff --git a/src/plugins/intel_npu/tests/unit/npuw/per_layer_inputs_copy_test.cpp b/src/plugins/intel_npu/tests/unit/npuw/per_layer_inputs_copy_test.cpp new file mode 100644 index 000000000000..ba6ddaaa2003 --- /dev/null +++ b/src/plugins/intel_npu/tests/unit/npuw/per_layer_inputs_copy_test.cpp @@ -0,0 +1,184 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include + +#include "infer_request_utils.hpp" +#include "openvino/runtime/make_tensor.hpp" +#include "util.hpp" + +namespace { + +// Helper: create an ov::SoPtr with shape [1, seq_len, num_layers, proj_dim] +// and fill with sequential float values starting from `start_val`. +ov::SoPtr make_per_layer_tensor(size_t seq_len, + size_t num_layers, + size_t proj_dim, + float start_val = 0.f) { + ov::Shape shape{1, seq_len, num_layers, proj_dim}; + auto tensor = ov::get_tensor_impl(ov::Tensor(ov::element::f32, shape)); + auto* data = reinterpret_cast(tensor->data()); + for (size_t i = 0; i < tensor->get_size(); ++i) { + data[i] = start_val + static_cast(i); + } + return tensor; +} + +// Helper: read all floats from a tensor into a vector. +std::vector to_vec(const ov::SoPtr& t) { + const auto* data = reinterpret_cast(t->data()); + return std::vector(data, data + t->get_size()); +} + +// --- copy_per_layer_inputs_chunk_to_right tests --------------------------------- + +// Test 1: copy the first chunk of src to dst. +// src shape [1,4,2,2], dst shape [1,2,2,2]. +// Copy chunk_tokens=2 starting at src_offset=0 -> dst should equal src[0:2]. +TEST(PerLayerInputsCopyTest, ChunkAtOffsetZeroCopiesToRight) { + // src: [1, 4, 2, 2], values 0..15 + auto src = make_per_layer_tensor(/*seq_len=*/4, /*num_layers=*/2, /*proj_dim=*/2, /*start_val=*/0.f); + // dst: [1, 2, 2, 2] + auto dst = make_per_layer_tensor(/*seq_len=*/2, /*num_layers=*/2, /*proj_dim=*/2, /*start_val=*/99.f); + + ASSERT_NO_THROW(ov::npuw::util::copy_per_layer_inputs_chunk_to_right(src, dst, /*offset=*/0, /*chunk=*/2)); + + // src tokens 0,1 -> values [0..7] + const auto result = to_vec(dst); + // dst is right-aligned; since chunk==dst_seq_len the entire dst is overwritten + std::vector expected = {0.f, + 1.f, + 2.f, + 3.f, // token 0 + 4.f, + 5.f, + 6.f, + 7.f}; // token 1 + EXPECT_EQ(result, expected); +} + +// Test 2: copy a middle chunk (offset=2, chunk=2) from a src with 6 tokens. +// dst has 4 token slots; chunk fills the right 2 slots, left 2 slots are left unchanged. +TEST(PerLayerInputsCopyTest, ChunkAtOffsetCopiesRightAlignedLeavesLeadingBytesUnchanged) { + // src: [1, 6, 2, 2], values 0..23 + auto src = make_per_layer_tensor(6, 2, 2, 0.f); + // dst: [1, 4, 2, 2], sequential values starting from 99 (99, 100, 101, ...) + auto dst = make_per_layer_tensor(4, 2, 2, 99.f); + + ASSERT_NO_THROW(ov::npuw::util::copy_per_layer_inputs_chunk_to_right(src, dst, /*offset=*/2, /*chunk=*/2)); + + // src tokens at offset 2,3 -> src flat indices [8..15] + const auto result = to_vec(dst); + // Right-aligned: dst tokens 0,1 are unchanged (sequential from start_val=99); dst tokens 2,3 hold src[2],src[3] + std::vector expected = {99.f, + 100.f, + 101.f, + 102.f, // unchanged (token 0) + 103.f, + 104.f, + 105.f, + 106.f, // unchanged (token 1) + 8.f, + 9.f, + 10.f, + 11.f, // token 2 + 12.f, + 13.f, + 14.f, + 15.f}; // token 3 + EXPECT_EQ(result, expected); +} + +// Test 3: chunk_tokens == 1 (generate step). +TEST(PerLayerInputsCopyTest, SingleTokenChunkCopiesToLastSlot) { + // src: [1, 3, 2, 2], values 0..11 + auto src = make_per_layer_tensor(3, 2, 2, 0.f); + // dst: [1, 1, 2, 2] + auto dst = make_per_layer_tensor(1, 2, 2, 99.f); + + ASSERT_NO_THROW(ov::npuw::util::copy_per_layer_inputs_chunk_to_right(src, dst, /*offset=*/0, /*chunk=*/1)); + + const auto result = to_vec(dst); + // src token 0 -> values [0,1,2,3] + std::vector expected = {0.f, 1.f, 2.f, 3.f}; + EXPECT_EQ(result, expected); +} + +// Test 4: chunk_tokens == 0 must throw. +TEST(PerLayerInputsCopyTest, ZeroChunkTokensThrows) { + auto src = make_per_layer_tensor(4, 2, 2); + auto dst = make_per_layer_tensor(4, 2, 2); + EXPECT_ANY_THROW(ov::npuw::util::copy_per_layer_inputs_chunk_to_right(src, dst, 0, 0)); +} + +// Test 5: offset beyond src seq_len must throw. +TEST(PerLayerInputsCopyTest, OffsetExceedsSrcSeqLenThrows) { + auto src = make_per_layer_tensor(4, 2, 2); + auto dst = make_per_layer_tensor(4, 2, 2); + EXPECT_ANY_THROW(ov::npuw::util::copy_per_layer_inputs_chunk_to_right(src, dst, /*offset=*/5, /*chunk=*/1)); +} + +// Test 6: offset+chunk exceeds src seq_len must throw. +TEST(PerLayerInputsCopyTest, ChunkRangeExceedsSrcSeqLenThrows) { + auto src = make_per_layer_tensor(4, 2, 2); + auto dst = make_per_layer_tensor(4, 2, 2); + EXPECT_ANY_THROW(ov::npuw::util::copy_per_layer_inputs_chunk_to_right(src, dst, /*offset=*/3, /*chunk=*/2)); +} + +// Test 7: chunk_tokens > dst_seq_len must throw. +TEST(PerLayerInputsCopyTest, ChunkExceedsDstSeqLenThrows) { + auto src = make_per_layer_tensor(8, 2, 2); + auto dst = make_per_layer_tensor(2, 2, 2); + EXPECT_ANY_THROW(ov::npuw::util::copy_per_layer_inputs_chunk_to_right(src, dst, /*offset=*/0, /*chunk=*/4)); +} + +// Test 8: src and dst have different per-token byte sizes (different num_layers) must throw. +TEST(PerLayerInputsCopyTest, PerTokenByteMismatchThrows) { + auto src = make_per_layer_tensor(4, /*num_layers=*/2, 2); + auto dst = make_per_layer_tensor(4, /*num_layers=*/3, 2); // different num_layers + EXPECT_ANY_THROW(ov::npuw::util::copy_per_layer_inputs_chunk_to_right(src, dst, /*offset=*/0, /*chunk=*/2)); +} + +// --- copy_to_right for per_layer_inputs (inlined path tests) -------------------- + +// Test 9: copy_to_right writes src into the right end of dst; leading bytes are left unchanged. +TEST(PerLayerInputsCopyTest, CopyToRightLeavesLeadingBytesUnchanged) { + // src: [1, 2, 2, 2], values 0..7 + auto src = make_per_layer_tensor(2, 2, 2, 0.f); + // dst: [1, 4, 2, 2], sequential values starting from 99 (99, 100, 101, ...) + auto dst = make_per_layer_tensor(4, 2, 2, 99.f); + + ASSERT_NO_THROW(ov::npuw::util::copy_to_right(src, dst)); + + const auto result = to_vec(dst); + std::vector expected = {99.f, + 100.f, + 101.f, + 102.f, // unchanged (token 0) + 103.f, + 104.f, + 105.f, + 106.f, // unchanged (token 1) + 0.f, + 1.f, + 2.f, + 3.f, // src token 0 + 4.f, + 5.f, + 6.f, + 7.f}; // src token 1 + EXPECT_EQ(result, expected); +} + +// Test 10: copy_to_right when src size == dst size copies everything. +TEST(PerLayerInputsCopyTest, CopyToRightSameSizeCopiesAll) { + auto src = make_per_layer_tensor(2, 2, 2, 1.f); + auto dst = make_per_layer_tensor(2, 2, 2, 0.f); + + ASSERT_NO_THROW(ov::npuw::util::copy_to_right(src, dst)); + + EXPECT_EQ(to_vec(dst), to_vec(src)); +} + +} // namespace diff --git a/src/plugins/intel_npu/tests/unit/npuw/pipeline_passes/add_position_ids_param_test.cpp b/src/plugins/intel_npu/tests/unit/npuw/pipeline_passes/add_position_ids_param_test.cpp new file mode 100644 index 000000000000..69cbc43778ac --- /dev/null +++ b/src/plugins/intel_npu/tests/unit/npuw/pipeline_passes/add_position_ids_param_test.cpp @@ -0,0 +1,285 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include + +#include +#include +#include +#include + +#include "npuw_transformations/add_position_ids_param.hpp" +#include "openvino/op/clamp.hpp" +#include "openvino/op/concat.hpp" +#include "openvino/op/constant.hpp" +#include "openvino/op/convert.hpp" +#include "openvino/op/cos.hpp" +#include "openvino/op/less_eq.hpp" +#include "openvino/op/matmul.hpp" +#include "openvino/op/parameter.hpp" +#include "openvino/op/range.hpp" +#include "openvino/op/result.hpp" +#include "openvino/op/sin.hpp" +#include "openvino/op/squeeze.hpp" +#include "openvino/op/transpose.hpp" +#include "openvino/op/unsqueeze.hpp" + +namespace { +std::shared_ptr build_model_with_lfm2_like_pattern() { + // Range: start=0, stop=seq_len, step=1 (mimics position_ids generation) + auto start = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{}, {0}); + auto stop = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{}, {128}); + auto step = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{}, {1}); + auto range = std::make_shared(start, stop, step, ov::element::i64); + range->set_friendly_name("range"); + + // Unsqueeze: add batch dim [seq_len] → [1, seq_len] + auto unsqueeze_axes = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{1}, {0}); + auto unsqueeze = std::make_shared(range, unsqueeze_axes); + unsqueeze->set_friendly_name("unsqueeze_batch"); + + // Unsqueeze1: add feature dim [1, seq_len] → [1, 1, seq_len] + auto unsqueeze1_axes = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{1}, {1}); + auto unsqueeze1 = std::make_shared(unsqueeze, unsqueeze1_axes); + unsqueeze1->set_friendly_name("unsqueeze_feature"); + + // Convert (always present in real LFM-2 models) + auto convert = std::make_shared(unsqueeze1, ov::element::f32); + convert->set_friendly_name("convert"); + + // MatMul: [inv_freq] × [positions] → freqs + auto inv_freq = std::make_shared(ov::element::f32, ov::PartialShape{1, 8, 1}); + inv_freq->output(0).set_names({"inv_freq"}); + inv_freq->set_friendly_name("inv_freq"); + + auto matmul = std::make_shared(inv_freq, convert); + matmul->set_friendly_name("matmul_rope"); + + // Transpose: [1, 8, 128] → [1, 128, 8] + auto transpose_order = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{3}, {0, 2, 1}); + auto transpose = std::make_shared(matmul, transpose_order); + transpose->set_friendly_name("transpose_rope"); + + // Concat(transpose, transpose) → simulate theta doubling + auto concat = std::make_shared(ov::OutputVector{transpose, transpose}, 2); + concat->set_friendly_name("concat_theta"); + + // Cos / Sin + auto cos = std::make_shared(concat); + cos->set_friendly_name("cos"); + auto sin = std::make_shared(concat); + sin->set_friendly_name("sin"); + + auto cos_result = std::make_shared(cos); + cos_result->set_friendly_name("cos_result"); + auto sin_result = std::make_shared(sin); + sin_result->set_friendly_name("sin_result"); + + ov::ResultVector results = {cos_result, sin_result}; + ov::ParameterVector params = {inv_freq}; + + // Causal mask consumer: + // Real LFM2 path: Range -> Unsqueeze -> Unsqueeze -> Unsqueeze -> LessEqual + auto unsqueeze_causal_axes = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{1}, {2}); + auto unsqueeze_causal = std::make_shared(unsqueeze1, unsqueeze_causal_axes); + unsqueeze_causal->set_friendly_name("unsqueeze_causal"); + + auto stub_k_range_as_const = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{1, 1, 1}, {0}); + auto less_equal = std::make_shared(stub_k_range_as_const, unsqueeze_causal); + less_equal->set_friendly_name("causal_mask_less_equal"); + auto mask_result = std::make_shared(less_equal); + mask_result->set_friendly_name("mask_result"); + results.push_back(mask_result); + + // Optional Clamp consumer on Range (simulates Gated Short Convolution indexing in LFM2). + // Real LFM2 path: Range -> Clamp -> Add -> Mod -> Unsqueeze -> ScatterNDUpdate + auto clamp = std::make_shared(range, 0, 2); + clamp->set_friendly_name("conv_clamp"); + auto clamp_result = std::make_shared(clamp); + clamp_result->set_friendly_name("clamp_result"); + results.push_back(clamp_result); + + return std::make_shared(results, params, "model_with_lfm2_like_pattern"); +} + +bool has_parameter_named(const std::shared_ptr& model, const std::string& name) { + for (const auto& p : model->get_parameters()) { + for (const auto& n : p->output(0).get_names()) { + if (n == name) { + return true; + } + } + } + return false; +} + +size_t count_ops_of_type(const std::shared_ptr& model, const std::string& type_name) { + size_t count = 0; + for (const auto& op : model->get_ops()) { + if (op->get_type_name() == type_name) { + ++count; + } + } + return count; +} + +// ===================== TESTS ===================== +TEST(AddPositionIdsParamTest, AddsPositionIdsParameter) { + auto model = build_model_with_lfm2_like_pattern(); + + EXPECT_FALSE(has_parameter_named(model, "position_ids")); + ASSERT_NO_THROW(ov::npuw::AddPositionIdsParam().run_on_model(model)); + EXPECT_TRUE(has_parameter_named(model, "position_ids")); +} + +TEST(AddPositionIdsParamTest, PositionIdsHasCorrectShapeAndType) { + auto model = build_model_with_lfm2_like_pattern(); + ov::npuw::AddPositionIdsParam().run_on_model(model); + + for (const auto& p : model->get_parameters()) { + for (const auto& n : p->output(0).get_names()) { + if (n == "position_ids") { + EXPECT_EQ(p->get_element_type(), ov::element::i64); + const auto& shape = p->get_partial_shape(); + ASSERT_EQ(shape.rank().get_length(), 2); + EXPECT_TRUE(shape[0].is_dynamic()); + EXPECT_TRUE(shape[1].is_dynamic()); + return; + } + } + } + FAIL() << "position_ids parameter not found"; +} + +// --- Test: RoPE path uses position_ids, not Range --- +// After the pass, the MatMul in the RoPE path should ultimately be fed by position_ids +// (through the new Unsqueeze), not by the original Range. +TEST(AddPositionIdsParamTest, RopePathUsesPositionIds) { + auto model = build_model_with_lfm2_like_pattern(); + ov::npuw::AddPositionIdsParam().run_on_model(model); + + // Find the MatMul node + std::shared_ptr matmul_node; + for (const auto& op : model->get_ops()) { + if (op->get_type_name() == std::string("MatMul")) { + matmul_node = op; + break; + } + } + ASSERT_NE(matmul_node, nullptr) << "MatMul not found in model"; + + // Walk backwards from MatMul's input 1 to find what feeds it + // The chain should be: position_ids → Unsqueeze → Convert → MatMul + auto walk = matmul_node->input_value(1).get_node_shared_ptr(); + bool found_position_ids = false; + int depth = 0; + while (depth < 5) { // limit walk depth + if (walk->get_type_name() == std::string("Parameter")) { + const auto& names = walk->output(0).get_names(); + if (names.count("position_ids") > 0) { + found_position_ids = true; + } + break; + } + if (walk->get_input_size() == 0) { + break; + } + walk = walk->input_value(0).get_node_shared_ptr(); + ++depth; + } + EXPECT_TRUE(found_position_ids) << "MatMul (RoPE path) should be fed by position_ids parameter"; +} + +TEST(AddPositionIdsParamTest, RangePreservedForCausalMaskConsumer) { + auto model = build_model_with_lfm2_like_pattern(); + ov::npuw::AddPositionIdsParam().run_on_model(model); + + // Range should still exist in the graph + EXPECT_GE(count_ops_of_type(model, "Range"), 1u) << "Range node should be preserved for causal mask"; + + // Find the LessEqual node -- Range should still feed it via the Unsqueeze chain + bool found_range_as_le_input = false; + for (const auto& op : model->get_ops()) { + if (op->get_type_name() == std::string("LessEqual")) { + // BFS from all LessEqual inputs to find Range + std::set visited; + std::queue to_visit; + for (size_t inp = 0; inp < op->get_input_size(); ++inp) { + to_visit.push(op->input_value(inp).get_node()); + } + while (!to_visit.empty()) { + auto* n = to_visit.front(); + to_visit.pop(); + if (!visited.insert(n).second) { + continue; + } + if (std::string(n->get_type_name()) == "Range") { + found_range_as_le_input = true; + break; + } + for (size_t i = 0; i < n->get_input_size(); ++i) { + to_visit.push(n->input_value(i).get_node()); + } + } + } + } + EXPECT_TRUE(found_range_as_le_input) << "Range should still feed the causal mask LessEqual"; +} + +TEST(AddPositionIdsParamTest, NoOpWhenPatternDoesNotMatch) { + // Build a model without the RoPE pattern + auto input = std::make_shared(ov::element::f32, ov::PartialShape{1, -1, 64}); + input->output(0).set_names({"input"}); + auto result = std::make_shared(input); + auto model = std::make_shared(ov::ResultVector{result}, ov::ParameterVector{input}, "no_rope_model"); + + size_t params_before = model->get_parameters().size(); + + ASSERT_NO_THROW(ov::npuw::AddPositionIdsParam().run_on_model(model)); + + EXPECT_EQ(model->get_parameters().size(), params_before) + << "No new parameters should be added when pattern doesn't match"; +} + +// --- Test: Clamp consumer on Range gets rewired to position_ids --- +// In real LFM2 models, Range feeds a Clamp for Gated Short Convolution indexing. +// The pass should replace Range->Clamp with Squeeze(position_ids)->Clamp. +TEST(AddPositionIdsParamTest, ClampInputIsReplacedToPositionIds) { + auto model = build_model_with_lfm2_like_pattern(); + + ASSERT_NO_THROW(ov::npuw::AddPositionIdsParam().run_on_model(model)); + EXPECT_TRUE(has_parameter_named(model, "position_ids")); + + // Clamp's input should now come from Squeeze(position_ids), not Range + for (const auto& op : model->get_ops()) { + if (op->get_type_name() != std::string("Clamp")) { + continue; + } + auto producer = op->input_value(0).get_node_shared_ptr(); + ASSERT_EQ(std::string(producer->get_type_name()), "Squeeze") + << "Clamp should be fed by Squeeze(position_ids), not " << producer->get_type_name(); + + auto squeeze_input = producer->input_value(0).get_node_shared_ptr(); + ASSERT_EQ(std::string(squeeze_input->get_type_name()), "Parameter"); + EXPECT_TRUE(squeeze_input->output(0).get_names().count("position_ids") > 0) + << "Squeeze should be fed by position_ids parameter"; + } +} + +// Running the pass a second time must not alter the graph (no duplicate position_ids). +TEST(AddPositionIdsParamTest, ReapplyDoesNotModifyGraph) { + auto model = build_model_with_lfm2_like_pattern(); + ov::npuw::AddPositionIdsParam().run_on_model(model); + + const size_t params_after_first = model->get_parameters().size(); + const size_t ops_after_first = model->get_ops().size(); + + ov::npuw::AddPositionIdsParam().run_on_model(model); + + EXPECT_EQ(model->get_parameters().size(), params_after_first) + << "Second pass should not add duplicate parameters"; + EXPECT_EQ(model->get_ops().size(), ops_after_first) + << "Second pass should not add duplicate operations"; +} +} // namespace diff --git a/src/plugins/intel_npu/tests/unit/npuw/pipeline_passes/convert_kvcache_to_precision_test.cpp b/src/plugins/intel_npu/tests/unit/npuw/pipeline_passes/convert_kvcache_to_precision_test.cpp index b894dac24b94..fb36080fe44b 100644 --- a/src/plugins/intel_npu/tests/unit/npuw/pipeline_passes/convert_kvcache_to_precision_test.cpp +++ b/src/plugins/intel_npu/tests/unit/npuw/pipeline_passes/convert_kvcache_to_precision_test.cpp @@ -2,15 +2,19 @@ // SPDX-License-Identifier: Apache-2.0 // +#include "npuw_transformations/convert_kvcache_to_precision.hpp" + #include #include #include #include -#include "llm_pass_test_fixture.hpp" #include "../util.hpp" +#include "llm_pass_test_fixture.hpp" +#include "openvino/pass/stateful_to_stateless.hpp" #include "openvino/runtime/properties.hpp" +#include "whisper/prepare_whisper_model.hpp" // --- Design note ------------------------------------------------------------------------- // The model builder creates KV cache state with ov::element::f32 (the default @@ -33,24 +37,29 @@ bool any_name_contains(const ov::Output& port, std::string_view return false; } -const std::map>& precision_key_matrix() { +const std::map>& precision_key_input_matrix() { + static const std::map> matrix = { + {ov::element::u8, {{"value", ov::element::u8}, {"scale", ov::element::f32}, {"zero_point", ov::element::u8}}}, + {ov::element::i8, {{"value", ov::element::i8}, {"scale", ov::element::f32}, {"zero_point", ov::element::i8}}}}; + return matrix; +} + +const std::map>& precision_key_output_matrix() { static const std::map> matrix = { {ov::element::u8, {{"value", ov::element::u8}, {"scale", ov::element::f32}, {"zero_point", ov::element::u8}}}, - {ov::element::i8, {{"value", ov::element::i8}, {"scale", ov::element::f32}, {"zero_point", ov::element::i8}}} - }; + {ov::element::i8, {{"value", ov::element::i8}, {"scale", ov::element::f32}, {"zero_point", ov::element::i8}}}}; return matrix; } const std::map>& precision_value_matrix() { static const std::map> matrix = { {ov::element::u8, {{"value", ov::element::i8}, {"scale", ov::element::f32}}}, - {ov::element::i8, {{"value", ov::element::i8}, {"scale", ov::element::f32}}} - }; + {ov::element::i8, {{"value", ov::element::i8}, {"scale", ov::element::f32}}}}; return matrix; } bool is_quantized_kv_type(const ov::element::Type kv_type) { - return precision_key_matrix().count(kv_type) > 0; + return precision_key_input_matrix().count(kv_type) > 0; } bool is_fp8_kv_type(const ov::element::Type kv_type) { @@ -65,8 +74,10 @@ ov::AnyMap make_kv_precision_props(const ov::element::Type kv_type) { return props; } -void expect_kv_cache_input_types(const std::shared_ptr& model, const ov::element::Type kv_type) { - // Key cache: asymmetric quantization -> value tensor + scale (f32) + zero_point (same as quant type). +void expect_kv_cache_input_types(const std::shared_ptr& model, + const ov::element::Type kv_type, + const bool check_quant_aux_ports = true) { + // Key cache: asymmetric quantization -> value tensor + scale (f32) + zero_point. // Value cache: symmetric quantization -> value tensor (i4) + scale (f32), no zero_point. const bool is_quantized = is_quantized_kv_type(kv_type); @@ -97,7 +108,7 @@ void expect_kv_cache_input_types(const std::shared_ptr& model, const if (is_past_key) { found_key_cache_input = true; - const auto expected = is_quantized ? precision_key_matrix().at(kv_type).at("value") : kv_type; + const auto expected = is_quantized ? precision_key_input_matrix().at(kv_type).at("value") : kv_type; EXPECT_EQ(input.get_element_type(), expected) << "past_key_values..key input must have type " << expected; } @@ -109,28 +120,25 @@ void expect_kv_cache_input_types(const std::shared_ptr& model, const << "past_key_values..value input must have type " << expected; } - if (any_name_contains(input, past_key_scale_name)) { + if (check_quant_aux_ports && any_name_contains(input, past_key_scale_name)) { found_key_scale_input = true; - const auto expected = precision_key_matrix().at(kv_type).at("scale"); - EXPECT_EQ(input.get_element_type(), expected) - << "past_key scale input must have type " << expected; + const auto expected = precision_key_input_matrix().at(kv_type).at("scale"); + EXPECT_EQ(input.get_element_type(), expected) << "past_key scale input must have type " << expected; } - if (any_name_contains(input, past_key_zp_name)) { + if (check_quant_aux_ports && any_name_contains(input, past_key_zp_name)) { found_key_zp_input = true; - const auto expected = precision_key_matrix().at(kv_type).at("zero_point"); - EXPECT_EQ(input.get_element_type(), expected) - << "past_key zero-point input must have type " << expected; + const auto expected = precision_key_input_matrix().at(kv_type).at("zero_point"); + EXPECT_EQ(input.get_element_type(), expected) << "past_key zero-point input must have type " << expected; } - if (any_name_contains(input, past_value_scale_name)) { + if (check_quant_aux_ports && any_name_contains(input, past_value_scale_name)) { found_value_scale_input = true; const auto expected = precision_value_matrix().at(kv_type).at("scale"); - EXPECT_EQ(input.get_element_type(), expected) - << "past_value scale input must have type " << expected; + EXPECT_EQ(input.get_element_type(), expected) << "past_value scale input must have type " << expected; } - if (any_name_contains(input, past_value_zp_name)) { + if (check_quant_aux_ports && any_name_contains(input, past_value_zp_name)) { found_value_zp_input = true; } } @@ -138,16 +146,12 @@ void expect_kv_cache_input_types(const std::shared_ptr& model, const EXPECT_TRUE(found_key_cache_input) << "No past_key_values..key input found in model"; EXPECT_TRUE(found_value_cache_input) << "No past_key_values..value input found in model"; - if (is_quantized) { - EXPECT_TRUE(found_key_scale_input) - << "Asymmetric quantized KV key-cache must expose scale input"; - EXPECT_TRUE(found_key_zp_input) - << "Asymmetric quantized KV key-cache must expose zero-point input"; - EXPECT_TRUE(found_value_scale_input) - << "Symmetric quantized KV value-cache must expose scale input"; - EXPECT_FALSE(found_value_zp_input) - << "Symmetric quantized KV value-cache must not expose zero-point input"; - } else { + if (is_quantized && check_quant_aux_ports) { + EXPECT_TRUE(found_key_scale_input) << "Asymmetric quantized KV key-cache must expose scale input"; + EXPECT_TRUE(found_key_zp_input) << "Asymmetric quantized KV key-cache must expose zero-point input"; + EXPECT_TRUE(found_value_scale_input) << "Symmetric quantized KV value-cache must expose scale input"; + EXPECT_FALSE(found_value_zp_input) << "Symmetric quantized KV value-cache must not expose zero-point input"; + } else if (!is_quantized && check_quant_aux_ports) { EXPECT_FALSE(found_key_scale_input) << "Non-quantized KV-cache must not expose key scale input"; EXPECT_FALSE(found_key_zp_input) << "Non-quantized KV-cache must not expose key zero-point input"; EXPECT_FALSE(found_value_scale_input) << "Non-quantized KV-cache must not expose value scale input"; @@ -155,7 +159,9 @@ void expect_kv_cache_input_types(const std::shared_ptr& model, const } } -void expect_kv_cache_present_output_types(const std::shared_ptr& model, const ov::element::Type kv_type) { +void expect_kv_cache_present_output_types(const std::shared_ptr& model, + const ov::element::Type kv_type, + const bool check_quant_aux_ports = true) { const bool is_quantized = is_quantized_kv_type(kv_type); constexpr std::string_view present_key_scale_name = "/present/key/scale"; @@ -185,40 +191,36 @@ void expect_kv_cache_present_output_types(const std::shared_ptr& mode if (is_present_key) { found_present_key = true; - const auto expected = is_quantized ? precision_key_matrix().at(kv_type).at("value") : kv_type; - EXPECT_EQ(output.get_element_type(), expected) - << "present..key output must have type " << expected; + const auto expected = is_quantized ? precision_key_output_matrix().at(kv_type).at("value") : kv_type; + EXPECT_EQ(output.get_element_type(), expected) << "present..key output must have type " << expected; } if (is_present_value) { found_present_value = true; const auto expected = is_quantized ? precision_value_matrix().at(kv_type).at("value") : kv_type; - EXPECT_EQ(output.get_element_type(), expected) - << "present..value output must have type " << expected; + EXPECT_EQ(output.get_element_type(), expected) << "present..value output must have type " << expected; } - if (any_name_contains(output, present_key_scale_name)) { + if (check_quant_aux_ports && any_name_contains(output, present_key_scale_name)) { found_present_key_scale = true; - const auto expected = precision_key_matrix().at(kv_type).at("scale"); - EXPECT_EQ(output.get_element_type(), expected) - << "present key scale output must have type " << expected; + const auto expected = precision_key_output_matrix().at(kv_type).at("scale"); + EXPECT_EQ(output.get_element_type(), expected) << "present key scale output must have type " << expected; } - if (any_name_contains(output, present_key_zp_name)) { + if (check_quant_aux_ports && any_name_contains(output, present_key_zp_name)) { found_present_key_zp = true; - const auto expected = precision_key_matrix().at(kv_type).at("zero_point"); + const auto expected = precision_key_output_matrix().at(kv_type).at("zero_point"); EXPECT_EQ(output.get_element_type(), expected) << "present key zero-point output must have type " << expected; } - if (any_name_contains(output, present_value_scale_name)) { + if (check_quant_aux_ports && any_name_contains(output, present_value_scale_name)) { found_present_value_scale = true; const auto expected = precision_value_matrix().at(kv_type).at("scale"); - EXPECT_EQ(output.get_element_type(), expected) - << "present value scale output must have type " << expected; + EXPECT_EQ(output.get_element_type(), expected) << "present value scale output must have type " << expected; } - if (any_name_contains(output, present_value_zp_name)) { + if (check_quant_aux_ports && any_name_contains(output, present_value_zp_name)) { found_present_value_zp = true; } } @@ -226,22 +228,16 @@ void expect_kv_cache_present_output_types(const std::shared_ptr& mode EXPECT_TRUE(found_present_key) << "No present..key output found in model"; EXPECT_TRUE(found_present_value) << "No present..value output found in model"; - if (is_quantized) { - EXPECT_TRUE(found_present_key_scale) - << "Asymmetric quantized KV key-cache must expose present scale output"; - EXPECT_TRUE(found_present_key_zp) - << "Asymmetric quantized KV key-cache must expose present zero-point output"; - EXPECT_TRUE(found_present_value_scale) - << "Symmetric quantized KV value-cache must expose present scale output"; + if (is_quantized && check_quant_aux_ports) { + EXPECT_TRUE(found_present_key_scale) << "Asymmetric quantized KV key-cache must expose present scale output"; + EXPECT_TRUE(found_present_key_zp) << "Asymmetric quantized KV key-cache must expose present zero-point output"; + EXPECT_TRUE(found_present_value_scale) << "Symmetric quantized KV value-cache must expose present scale output"; EXPECT_FALSE(found_present_value_zp) << "Symmetric quantized KV value-cache must not expose present zero-point output"; - } else { - EXPECT_FALSE(found_present_key_scale) - << "Non-quantized KV-cache must not expose present key scale output"; - EXPECT_FALSE(found_present_key_zp) - << "Non-quantized KV-cache must not expose present key zero-point output"; - EXPECT_FALSE(found_present_value_scale) - << "Non-quantized KV-cache must not expose present value scale output"; + } else if (!is_quantized && check_quant_aux_ports) { + EXPECT_FALSE(found_present_key_scale) << "Non-quantized KV-cache must not expose present key scale output"; + EXPECT_FALSE(found_present_key_zp) << "Non-quantized KV-cache must not expose present key zero-point output"; + EXPECT_FALSE(found_present_value_scale) << "Non-quantized KV-cache must not expose present value scale output"; EXPECT_FALSE(found_present_value_zp) << "Non-quantized KV-cache must not expose present value zero-point output"; } @@ -257,11 +253,7 @@ class ConvertKVCacheHintPrecisionTest : public ov::test::npuw::LLMPassTestFixtur INSTANTIATE_TEST_SUITE_P( KVCachePrecisions, ConvertKVCacheHintPrecisionTest, - ::testing::Values(ov::element::f16, - ov::element::f8e4m3, - ov::element::f8e5m2, - ov::element::i8, - ov::element::u8), + ::testing::Values(ov::element::f16, ov::element::f8e4m3, ov::element::f8e5m2, ov::element::i8, ov::element::u8), [](const ::testing::TestParamInfo& info) -> std::string { std::ostringstream ss; ss << info.param; @@ -309,6 +301,31 @@ TEST_P(ConvertKVCacheHintPrecisionTest, PrefillModelPresentOutputsHaveExpectedPr expect_kv_cache_present_output_types(prefill.model, kv_type); } +// Whisper decoder_with_past model uses names like: +// past_key_values..decoder.key / present..decoder.key +// past_key_values..encoder.key / present..encoder.key +// Ensure KV-cache precision conversion handles those variants too. +TEST_P(ConvertKVCacheHintPrecisionTest, WhisperKVCacheModelPastKeyInputsHaveExpectedPrecision) { + const auto kv_type = GetParam(); + auto model = ov::test::npuw::build_whisper_decoder_test_model(); + ov::pass::StatefulToStateless().run_on_model(model); + model = model->clone(); + ASSERT_TRUE(ov::npuw::util::PrepareWhisperKVCacheModel().run_on_model(model)); + ASSERT_TRUE(ov::npuw::ConvertKVCacheToPrecision(kv_type).run_on_model(model)); + + expect_kv_cache_input_types(model, kv_type, false); +} + +TEST_P(ConvertKVCacheHintPrecisionTest, WhisperKVCacheModelPresentOutputsHaveExpectedPrecision) { + const auto kv_type = GetParam(); + auto model = ov::test::npuw::build_whisper_decoder_test_model(); + ov::pass::StatefulToStateless().run_on_model(model); + model = model->clone(); + ASSERT_TRUE(ov::npuw::util::PrepareWhisperKVCacheModel().run_on_model(model)); + ASSERT_TRUE(ov::npuw::ConvertKVCacheToPrecision(kv_type).run_on_model(model)); + + expect_kv_cache_present_output_types(model, kv_type, false); +} // --- Non-parametric tests ------------------------------------------------------------------------- @@ -359,15 +376,14 @@ TEST_F(ConvertKVCacheToPrecisionPassTest, OptimizeFp8WithPlainModelKeepsF16KvCac expect_kv_cache_present_output_types(prefill.model, ov::element::f16); } - // Chunked-prefill: past_key inputs of the prefill model are converted to f16. TEST_F(ConvertKVCacheToPrecisionPassTest, ChunkedPrefillModelPastKeyInputsAreF16) { RecordingFactory recorder; std::unique_ptr compiled; - ASSERT_NO_THROW(compiled = create_compiled_model({{"NPUW_LLM_PREFILL_HINT", "DYNAMIC"}, - {"NPUW_LLM_PREFILL_CHUNK_SIZE", "32"}}, - recorder)); + ASSERT_NO_THROW( + compiled = create_compiled_model({{"NPUW_LLM_PREFILL_HINT", "DYNAMIC"}, {"NPUW_LLM_PREFILL_CHUNK_SIZE", "32"}}, + recorder)); ASSERT_NE(compiled, nullptr); const auto& prefill = require_sub_model(recorder, "_prefill"); diff --git a/src/plugins/intel_npu/tests/unit/npuw/pipeline_passes/optimize_value_tensors_test.cpp b/src/plugins/intel_npu/tests/unit/npuw/pipeline_passes/optimize_value_tensors_test.cpp index 69ccfadd4b3c..405d7b874840 100644 --- a/src/plugins/intel_npu/tests/unit/npuw/pipeline_passes/optimize_value_tensors_test.cpp +++ b/src/plugins/intel_npu/tests/unit/npuw/pipeline_passes/optimize_value_tensors_test.cpp @@ -116,4 +116,20 @@ TEST_F(OptimizeValueTensorsPassTest, AtLeastOneMatMulHasTransposeBSet) { EXPECT_TRUE(any_matmul_has_transpose_b(generate.model)); } +// Same as above but on a GQA model (num_kv_heads < num_heads), which exercises +// the TransposeValueTensors_GQA pattern instead of the MHA one. +TEST_F(OptimizeValueTensorsPassTest, AtLeastOneMatMulHasTransposeBSet_GQA) { + RecordingFactory recorder; + std::unique_ptr compiled; + + ASSERT_NO_THROW(compiled = create_compiled_model(ov::test::npuw::build_llm_gqa_test_model(), + {{"NPUW_LLM_OPTIMIZE_V_TENSORS", "YES"}}, + recorder)); + ASSERT_NE(compiled, nullptr); + + const auto& generate = require_sub_model_containing(recorder, "_kv"); + + EXPECT_TRUE(any_matmul_has_transpose_b(generate.model)); +} + } // namespace diff --git a/src/plugins/intel_npu/tests/unit/npuw/pipeline_passes/remove_empty_kv_inputs_test.cpp b/src/plugins/intel_npu/tests/unit/npuw/pipeline_passes/remove_empty_kv_inputs_test.cpp index b86b17956405..14a06e056fd2 100644 --- a/src/plugins/intel_npu/tests/unit/npuw/pipeline_passes/remove_empty_kv_inputs_test.cpp +++ b/src/plugins/intel_npu/tests/unit/npuw/pipeline_passes/remove_empty_kv_inputs_test.cpp @@ -171,4 +171,240 @@ TEST_F(RemoveEmptyKVInputsPassTest, SharedParam_TwoConcats_LptSubgraph_BothElimi EXPECT_EQ(outputs[1].get_partial_shape(), ov::PartialShape({1, 4, 1, 16})); } +// --- Tests for the name-based filter (PR #35221 change) --- +// The pass should only remove parameters whose name matches KV-cache naming conventions. +// Linear cache parameters (cache_params.past.conv/ssm) must be preserved even though +// they structurally match the Parameter->Concat pattern. + +// Linear cache conv parameter should NOT be removed by the pass. +TEST_F(RemoveEmptyKVInputsPassTest, LinCacheConvParamPreserved) { + using namespace ov::opset13; + + auto past_conv = std::make_shared(ov::element::f32, ov::Shape{1, 2048, 0}); + past_conv->set_friendly_name("cache_params.past.conv.0"); + past_conv->output(0).set_names({"cache_params.past.conv.0"}); + + auto present_conv = std::make_shared(ov::element::f32, ov::Shape{1, 2048, 3}); + present_conv->set_friendly_name("cache_params.present.0.conv"); + present_conv->output(0).set_names({"cache_params.present.0.conv"}); + + auto concat = std::make_shared(ov::OutputVector{past_conv, present_conv}, 2); + auto result = std::make_shared(concat); + + auto model = std::make_shared(ov::ResultVector{result}, + ov::ParameterVector{past_conv, present_conv}, + "lin_cache_conv_preserved_test"); + + ov::npuw::RemoveEmptyKVInputs pass; + EXPECT_FALSE(pass.run_on_model(model)) << "Pass should not match linear cache conv parameters"; + EXPECT_EQ(model->get_parameters().size(), 2u) << "Both parameters should remain"; + EXPECT_EQ(count_ops(model), 1u) << "Concat should remain"; +} + +// Linear cache SSM parameter should NOT be removed by the pass. +TEST_F(RemoveEmptyKVInputsPassTest, LinCacheSsmParamPreserved) { + using namespace ov::opset13; + + auto past_ssm = std::make_shared(ov::element::f32, ov::Shape{1, 16, 0, 128}); + past_ssm->set_friendly_name("cache_params.past.ssm.0"); + past_ssm->output(0).set_names({"cache_params.past.ssm.0"}); + + auto present_ssm = std::make_shared(ov::element::f32, ov::Shape{1, 16, 128, 128}); + present_ssm->set_friendly_name("cache_params.present.0.ssm"); + present_ssm->output(0).set_names({"cache_params.present.0.ssm"}); + + auto concat = std::make_shared(ov::OutputVector{past_ssm, present_ssm}, 2); + auto result = std::make_shared(concat); + + auto model = std::make_shared(ov::ResultVector{result}, + ov::ParameterVector{past_ssm, present_ssm}, + "lin_cache_ssm_preserved_test"); + + ov::npuw::RemoveEmptyKVInputs pass; + EXPECT_FALSE(pass.run_on_model(model)) << "Pass should not match linear cache SSM parameters"; + EXPECT_EQ(model->get_parameters().size(), 2u) << "Both parameters should remain"; + EXPECT_EQ(count_ops(model), 1u) << "Concat should remain"; +} + +// Plain f32 key+value pair: both should be removed. +// Also verifies 'value' naming (existing tests only use 'key'). +TEST_F(RemoveEmptyKVInputsPassTest, PlainF32KeyValuePairBothRemoved) { + using namespace ov::opset13; + + auto past_k = std::make_shared(ov::element::f32, ov::Shape{1, 4, 0, 16}); + past_k->set_friendly_name("past_key_values.0.key"); + past_k->output(0).set_names({"past_key_values.0.key"}); + + auto present_k = std::make_shared(ov::element::f32, ov::Shape{1, 4, 1, 16}); + present_k->set_friendly_name("present.0.key"); + present_k->output(0).set_names({"present.0.key"}); + + auto past_v = std::make_shared(ov::element::f32, ov::Shape{1, 4, 0, 16}); + past_v->set_friendly_name("past_key_values.0.value"); + past_v->output(0).set_names({"past_key_values.0.value"}); + + auto present_v = std::make_shared(ov::element::f32, ov::Shape{1, 4, 1, 16}); + present_v->set_friendly_name("present.0.value"); + present_v->output(0).set_names({"present.0.value"}); + + auto concat_k = std::make_shared(ov::OutputVector{past_k, present_k}, 2); + auto concat_v = std::make_shared(ov::OutputVector{past_v, present_v}, 2); + + auto result_k = std::make_shared(concat_k); + result_k->output(0).set_names({"present.0.key"}); + auto result_v = std::make_shared(concat_v); + result_v->output(0).set_names({"present.0.value"}); + + auto model = std::make_shared(ov::ResultVector{result_k, result_v}, + ov::ParameterVector{past_k, present_k, past_v, present_v}, + "plain_f32_key_value_pair_test"); + + ov::npuw::RemoveEmptyKVInputs pass; + EXPECT_TRUE(pass.run_on_model(model)); + + // Both past_k and past_v removed; current_k and current_v remain + EXPECT_EQ(model->get_parameters().size(), 2u); + EXPECT_EQ(count_ops(model), 0u); + + const auto outputs = model->outputs(); + ASSERT_EQ(outputs.size(), 2u); + EXPECT_EQ(outputs[0].get_partial_shape(), ov::PartialShape({1, 4, 1, 16})); + EXPECT_EQ(outputs[1].get_partial_shape(), ov::PartialShape({1, 4, 1, 16})); +} + +// Whisper-style "input_restored." naming should be matched and removed. +TEST_F(RemoveEmptyKVInputsPassTest, WhisperRestoredParamRemoved) { + using namespace ov::opset13; + + const std::string var_name = "input_restored.past_key_values.0.decoder.keypresent.0.decoder.key"; + auto past_k = std::make_shared(ov::element::f32, ov::Shape{1, 8, 0, 64}); + past_k->set_friendly_name(var_name); + past_k->output(0).set_names({var_name}); + + auto present_k = std::make_shared(ov::element::f32, ov::Shape{1, 8, 1, 64}); + present_k->set_friendly_name("present.0.key"); + present_k->output(0).set_names({"present.0.key"}); + + auto concat = std::make_shared(ov::OutputVector{past_k, present_k}, 2); + auto result = std::make_shared(concat); + result->output(0).set_names({"output_restored.past_key_values.0.decoder.key"}); + + auto model = std::make_shared(ov::ResultVector{result}, + ov::ParameterVector{past_k, present_k}, + "whisper_restored_param_test"); + + ov::npuw::RemoveEmptyKVInputs pass; + EXPECT_TRUE(pass.run_on_model(model)); + + EXPECT_EQ(model->get_parameters().size(), 1u); + EXPECT_EQ(model->get_parameters().front()->get_friendly_name(), "present.0.key"); + EXPECT_EQ(count_ops(model), 0u); +} + +// Arbitrary non-KV parameter name: should NOT be removed. +TEST_F(RemoveEmptyKVInputsPassTest, NonKVNamedParamPreserved) { + using namespace ov::opset13; + + auto past_other = std::make_shared(ov::element::f32, ov::Shape{1, 4, 0, 16}); + past_other->set_friendly_name("some_other_param"); + past_other->output(0).set_names({"some_other_param"}); + + auto present_other = std::make_shared(ov::element::f32, ov::Shape{1, 4, 1, 16}); + present_other->set_friendly_name("present_other"); + present_other->output(0).set_names({"present_other"}); + + auto concat = std::make_shared(ov::OutputVector{past_other, present_other}, 2); + auto result = std::make_shared(concat); + + auto model = std::make_shared(ov::ResultVector{result}, + ov::ParameterVector{past_other, present_other}, + "non_kv_named_param_test"); + + ov::npuw::RemoveEmptyKVInputs pass; + EXPECT_FALSE(pass.run_on_model(model)) << "Pass should not match non-KV named parameters"; + EXPECT_EQ(model->get_parameters().size(), 2u) << "Both parameters should remain"; + EXPECT_EQ(count_ops(model), 1u) << "Concat should remain"; +} + +// Hybrid cache model: standard KV key+value (should be removed) + linear cache conv/ssm (should be preserved). +TEST_F(RemoveEmptyKVInputsPassTest, HybridCacheKVRemovedLinCachePreserved) { + using namespace ov::opset13; + + // Standard KV cache key – should be removed + auto past_k = std::make_shared(ov::element::f32, ov::Shape{1, 4, 0, 16}); + past_k->set_friendly_name("past_key_values.0.key"); + past_k->output(0).set_names({"past_key_values.0.key"}); + + auto present_k = std::make_shared(ov::element::f32, ov::Shape{1, 4, 1, 16}); + present_k->set_friendly_name("present.0.key"); + present_k->output(0).set_names({"present.0.key"}); + + auto concat_k = std::make_shared(ov::OutputVector{past_k, present_k}, 2); + auto result_k = std::make_shared(concat_k); + result_k->output(0).set_names({"present.0.key"}); + + // Standard KV cache value – should be removed + auto past_v = std::make_shared(ov::element::f32, ov::Shape{1, 4, 0, 16}); + past_v->set_friendly_name("past_key_values.0.value"); + past_v->output(0).set_names({"past_key_values.0.value"}); + + auto present_v = std::make_shared(ov::element::f32, ov::Shape{1, 4, 1, 16}); + present_v->set_friendly_name("present.0.value"); + present_v->output(0).set_names({"present.0.value"}); + + auto concat_v = std::make_shared(ov::OutputVector{past_v, present_v}, 2); + auto result_v = std::make_shared(concat_v); + result_v->output(0).set_names({"present.0.value"}); + + // Linear cache conv – should be preserved + auto past_conv = std::make_shared(ov::element::f32, ov::Shape{1, 2048, 0}); + past_conv->set_friendly_name("cache_params.past.conv.0"); + past_conv->output(0).set_names({"cache_params.past.conv.0"}); + + auto present_conv = std::make_shared(ov::element::f32, ov::Shape{1, 2048, 3}); + present_conv->set_friendly_name("cache_params.present.conv.0"); + present_conv->output(0).set_names({"cache_params.present.conv.0"}); + + auto concat_conv = std::make_shared(ov::OutputVector{past_conv, present_conv}, 2); + auto result_conv = std::make_shared(concat_conv); + + // Linear cache ssm – should be preserved + auto past_ssm = std::make_shared(ov::element::f32, ov::Shape{1, 16, 0, 128}); + past_ssm->set_friendly_name("cache_params.past.ssm.0"); + past_ssm->output(0).set_names({"cache_params.past.ssm.0"}); + + auto present_ssm = std::make_shared(ov::element::f32, ov::Shape{1, 16, 128, 128}); + present_ssm->set_friendly_name("cache_params.present.ssm.0"); + present_ssm->output(0).set_names({"cache_params.present.ssm.0"}); + + auto concat_ssm = std::make_shared(ov::OutputVector{past_ssm, present_ssm}, 2); + auto result_ssm = std::make_shared(concat_ssm); + + auto model = std::make_shared( + ov::ResultVector{result_k, result_v, result_conv, result_ssm}, + ov::ParameterVector{past_k, present_k, past_v, present_v, past_conv, present_conv, past_ssm, present_ssm}, + "hybrid_kv_and_lincache_test"); + + ov::npuw::RemoveEmptyKVInputs pass; + EXPECT_TRUE(pass.run_on_model(model)); + + // past_k and past_v removed; present_k, present_v, conv pair, ssm pair remain + EXPECT_EQ(model->get_parameters().size(), 6u) << "Only past_k and past_v should be removed"; + + // KV Concats eliminated, linear cache Concats remain + EXPECT_EQ(count_ops(model), 2u) << "Only KV Concats should be removed"; + + // Verify linear cache parameters are still present + auto params = model->get_parameters(); + auto has_param = [&](const std::string& name) { + return std::any_of(params.begin(), params.end(), [&](const auto& p) { + return p->output(0).get_names().count(name) > 0; + }); + }; + EXPECT_TRUE(has_param("cache_params.past.conv.0")) << "Conv past should be preserved"; + EXPECT_TRUE(has_param("cache_params.present.conv.0")) << "Conv present should be preserved"; + EXPECT_TRUE(has_param("cache_params.past.ssm.0")) << "SSM past should be preserved"; + EXPECT_TRUE(has_param("cache_params.present.ssm.0")) << "SSM present should be preserved"; +} + } // namespace diff --git a/src/plugins/intel_npu/tests/unit/npuw/pipeline_passes/reshape_to_static_test.cpp b/src/plugins/intel_npu/tests/unit/npuw/pipeline_passes/reshape_to_static_test.cpp index 5b0ddf7f95ff..9c96b5bc9b89 100644 --- a/src/plugins/intel_npu/tests/unit/npuw/pipeline_passes/reshape_to_static_test.cpp +++ b/src/plugins/intel_npu/tests/unit/npuw/pipeline_passes/reshape_to_static_test.cpp @@ -2,9 +2,15 @@ // SPDX-License-Identifier: Apache-2.0 // +#include "npuw_transformations/reshape_to_static.hpp" + #include #include "llm_pass_test_fixture.hpp" +#include "openvino/op/add.hpp" +#include "openvino/op/constant.hpp" +#include "openvino/op/parameter.hpp" +#include "openvino/op/result.hpp" namespace { @@ -12,6 +18,28 @@ using ov::test::npuw::RecordingFactory; class ReshapeToStaticPassTest : public ov::test::npuw::LLMPassTestFixture {}; +std::shared_ptr build_model_with_per_layer_inputs_add() { + auto input_ids = std::make_shared(ov::element::i64, ov::PartialShape{1, -1}); + input_ids->output(0).set_names({"input_ids"}); + + auto attention_mask = std::make_shared(ov::element::i64, ov::PartialShape{1, -1}); + attention_mask->output(0).set_names({"attention_mask"}); + + auto position_ids = std::make_shared(ov::element::i64, ov::PartialShape{1, -1}); + position_ids->output(0).set_names({"position_ids"}); + + auto per_layer_inputs = std::make_shared(ov::element::f32, ov::PartialShape{1, -1, -1, -1}); + per_layer_inputs->output(0).set_names({"per_layer_inputs"}); + + auto sibling = ov::op::v0::Constant::create(ov::element::f32, ov::Shape{1, 1, 42, 256}, {0.0f}); + auto add = std::make_shared(per_layer_inputs, sibling); + auto result = std::make_shared(add); + + return std::make_shared(ov::ResultVector{result}, + ov::ParameterVector{input_ids, attention_mask, position_ids, per_layer_inputs}, + "per_layer_inputs_reshape_model"); +} + // --- Test 1 ------------------------------------------------------------------------- // Every input of the prefill sub-model must be fully static after ReshapeToStatic. // We additionally verify the concrete shapes of the two most representative inputs. @@ -69,10 +97,10 @@ TEST_F(ReshapeToStaticPassTest, AllGenerateInputsAreStatic) { const auto kv_shape = input_shape(generate.model, "past_key_values"); ASSERT_TRUE(kv_shape.has_value()) << "past_key_values not found in generate model"; ASSERT_EQ(kv_shape->size(), 4u); - EXPECT_EQ((*kv_shape)[0], 1u); // batch - EXPECT_EQ((*kv_shape)[1], 4u); // num_kv_heads - EXPECT_EQ((*kv_shape)[2], 191u); // kvcache_size(192) - input_size(1) - EXPECT_EQ((*kv_shape)[3], 16u); // head_dim + EXPECT_EQ((*kv_shape)[0], 1u); // batch + EXPECT_EQ((*kv_shape)[1], 4u); // num_kv_heads + EXPECT_EQ((*kv_shape)[2], 191u); // kvcache_size(192) - input_size(1) + EXPECT_EQ((*kv_shape)[3], 16u); // head_dim } // --- Test 3 ------------------------------------------------------------------------- @@ -84,9 +112,9 @@ TEST_F(ReshapeToStaticPassTest, GenerateModelKVCacheShapeReflectsKVCacheSize) { RecordingFactory recorder; std::unique_ptr compiled; - ASSERT_NO_THROW(compiled = create_compiled_model({{"NPUW_LLM_MAX_PROMPT_LEN", "256"}, - {"NPUW_LLM_MIN_RESPONSE_LEN", "128"}}, - recorder)); + ASSERT_NO_THROW( + compiled = create_compiled_model({{"NPUW_LLM_MAX_PROMPT_LEN", "256"}, {"NPUW_LLM_MIN_RESPONSE_LEN", "128"}}, + recorder)); ASSERT_NE(compiled, nullptr); // kvcache_size = 256 + 128 = 384 @@ -138,10 +166,107 @@ TEST_F(ReshapeToStaticPassTest, MaxGenerationTokenLenDrivesGenerateInputShape) { const auto kv_shape = input_shape(generate.model, "past_key_values"); ASSERT_TRUE(kv_shape.has_value()) << "past_key_values not found in generate model"; ASSERT_EQ(kv_shape->size(), 4u); - EXPECT_EQ((*kv_shape)[0], 1u); // batch - EXPECT_EQ((*kv_shape)[1], 4u); // num_kv_heads - EXPECT_EQ((*kv_shape)[2], 184u); // kvcache_size(192) - input_size(8) - EXPECT_EQ((*kv_shape)[3], 16u); // head_dim + EXPECT_EQ((*kv_shape)[0], 1u); // batch + EXPECT_EQ((*kv_shape)[1], 4u); // num_kv_heads + EXPECT_EQ((*kv_shape)[2], 184u); // kvcache_size(192) - input_size(8) + EXPECT_EQ((*kv_shape)[3], 16u); // head_dim } +// --- Test 5 ------------------------------------------------------------------------- +// per_layer_inputs is consumed by Add with a static sibling tensor. The pass should +// resolve dynamic {num_layers, projection_dim} from that sibling and set seq_len=input_size. +TEST_F(ReshapeToStaticPassTest, PerLayerInputsResolvedToStaticForPrefillAndGenerate) { + const ov::npuw::KVAxesPosition kv_axes_position{0u, 2u}; + + auto prefill_model = build_model_with_per_layer_inputs_add(); + ASSERT_TRUE(ov::npuw::ReshapeToStatic(/*input_size=*/128, + /*kvcache_size=*/192, + kv_axes_position, + /*lora_rank=*/64) + .run_on_model(prefill_model)); + const auto prefill_per_layer_shape = input_shape(prefill_model, "per_layer_inputs"); + ASSERT_TRUE(prefill_per_layer_shape.has_value()) << "per_layer_inputs not found in prefill model"; + EXPECT_EQ(*prefill_per_layer_shape, (ov::Shape{1, 128, 42, 256})); + + auto generate_model = build_model_with_per_layer_inputs_add(); + ASSERT_TRUE(ov::npuw::ReshapeToStatic(/*input_size=*/1, + /*kvcache_size=*/192, + kv_axes_position, + /*lora_rank=*/64) + .run_on_model(generate_model)); + const auto generate_per_layer_shape = input_shape(generate_model, "per_layer_inputs"); + ASSERT_TRUE(generate_per_layer_shape.has_value()) << "per_layer_inputs not found in generate model"; + EXPECT_EQ(*generate_per_layer_shape, (ov::Shape{1, 1, 42, 256})); +} + +// Builds a stateless model with standard LLM inputs plus linear cache inputs +// (cache_params.past.conv.N, cache_params.past.ssm.N) to test the matchLinCacheString +// branch of ReshapeToStatic independently. +std::shared_ptr build_model_with_lincache_inputs() { + auto input_ids = std::make_shared(ov::element::i64, ov::PartialShape{1, -1}); + input_ids->output(0).set_names({"input_ids"}); + + auto attention_mask = std::make_shared(ov::element::i64, ov::PartialShape{1, -1}); + attention_mask->output(0).set_names({"attention_mask"}); + + auto position_ids = std::make_shared(ov::element::i64, ov::PartialShape{1, -1}); + position_ids->output(0).set_names({"position_ids"}); + + // Standard KV cache input + auto kv_key = std::make_shared(ov::element::f32, ov::PartialShape{-1, 4, -1, 64}); + kv_key->output(0).set_names({"past_key_values.0.key"}); + auto kv_value = std::make_shared(ov::element::f32, ov::PartialShape{-1, 4, -1, 64}); + kv_value->output(0).set_names({"past_key_values.0.value"}); + + // Linear cache: conv (e.g. Gated Short Convolution in LFM2/Qwen3.5) + auto conv = std::make_shared(ov::element::f32, ov::PartialShape{-1, 2048, 3}); + conv->output(0).set_names({"cache_params.past.conv.0"}); + + // Linear cache: ssm (e.g. GatedDeltaNet in Qwen3.5) + auto ssm = std::make_shared(ov::element::f32, ov::PartialShape{-1, 16, 128, 128}); + ssm->output(0).set_names({"cache_params.past.ssm.0"}); + + auto result = std::make_shared(input_ids); + return std::make_shared(ov::ResultVector{result}, + ov::ParameterVector{input_ids, attention_mask, position_ids, + kv_key, kv_value, conv, ssm}, + "lincache_reshape_model"); +} + +// --- Test 6 ------------------------------------------------------------------------- +// Linear cache inputs (cache_params.past.conv.N, cache_params.past.ssm.N) should only +// have their batch dimension set to 1 — all other dimensions must be preserved as-is. +// This contrasts with standard KV cache inputs where seq_len = kvcache_size - input_size. +TEST_F(ReshapeToStaticPassTest, LinCacheInputsOnlyBatchIsReshaped) { + const ov::npuw::KVAxesPosition kv_axes_position{0u, 2u}; + + auto model = build_model_with_lincache_inputs(); + ASSERT_TRUE(ov::npuw::ReshapeToStatic(/*input_size=*/1, + /*kvcache_size=*/192, + kv_axes_position, + /*lora_rank=*/0) + .run_on_model(model)); + + EXPECT_TRUE(all_inputs_static(model)) + << "At least one input still has a dynamic dimension after ReshapeToStatic"; + + // Conv linear cache: batch=1, other dims unchanged + const auto conv_shape = input_shape(model, "cache_params.past.conv.0"); + ASSERT_TRUE(conv_shape.has_value()) << "cache_params.past.conv.0 not found"; + EXPECT_EQ(*conv_shape, (ov::Shape{1, 2048, 3})); + + // SSM linear cache: batch=1, other dims unchanged + const auto ssm_shape = input_shape(model, "cache_params.past.ssm.0"); + ASSERT_TRUE(ssm_shape.has_value()) << "cache_params.past.ssm.0 not found"; + EXPECT_EQ(*ssm_shape, (ov::Shape{1, 16, 128, 128})); + + // Standard KV cache: batch=1, seq_len = kvcache_size(192) - input_size(1) = 191 + const auto kv_key_shape = input_shape(model, "past_key_values.0.key"); + ASSERT_TRUE(kv_key_shape.has_value()) << "past_key_values.0.key not found"; + EXPECT_EQ(*kv_key_shape, (ov::Shape{1, 4, 191, 64})); + + const auto kv_value_shape = input_shape(model, "past_key_values.0.value"); + ASSERT_TRUE(kv_value_shape.has_value()) << "past_key_values.0.value not found"; + EXPECT_EQ(*kv_value_shape, (ov::Shape{1, 4, 191, 64})); +} } // namespace diff --git a/src/plugins/intel_npu/tests/unit/npuw/pipeline_passes/stateful_to_stateless_test.cpp b/src/plugins/intel_npu/tests/unit/npuw/pipeline_passes/stateful_to_stateless_test.cpp new file mode 100644 index 000000000000..992aa180cbf5 --- /dev/null +++ b/src/plugins/intel_npu/tests/unit/npuw/pipeline_passes/stateful_to_stateless_test.cpp @@ -0,0 +1,630 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include + +#include +#include + +#include "openvino/op/assign.hpp" +#include "openvino/op/constant.hpp" +#include "openvino/op/gather.hpp" +#include "openvino/op/matmul.hpp" +#include "openvino/op/parameter.hpp" +#include "openvino/op/read_value.hpp" +#include "openvino/op/result.hpp" +#include "openvino/pass/stateful_to_stateless.hpp" + +namespace { +struct CacheStateBlock { + std::shared_ptr read_value; + std::shared_ptr assign; + ov::Output output; +}; + +// Creates a cache block connected to gather with beam_idx, using an explicit variable_id. +CacheStateBlock make_cache_state(const ov::Output& beam_idx, + const std::string& var_name, + ov::element::Type precision, + const ov::PartialShape& var_shape, + const ov::Shape& init_shape) { + auto variable = + std::make_shared(ov::op::util::VariableInfo{var_shape, precision, var_name}); + + auto num_elements = ov::shape_size(init_shape); + auto init = ov::op::v0::Constant::create(precision, init_shape, std::vector(num_elements, 0.0f)); + auto read_value = std::make_shared(init, variable); + + auto gather_axis = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{}, {0}); + auto gather = std::make_shared(read_value, beam_idx, gather_axis); + + auto assign = std::make_shared(gather, variable); + + return {read_value, assign, gather->output(0)}; +} + +// Creates a cache block NOT connected to beam_idx, using an explicit variable_id. +CacheStateBlock make_cache_state(const std::string& var_name, + ov::element::Type precision, + const ov::PartialShape& var_shape, + const ov::Shape& init_shape) { + auto variable = + std::make_shared(ov::op::util::VariableInfo{var_shape, precision, var_name}); + + auto init = ov::op::v0::Constant::create(precision, init_shape, std::vector(ov::shape_size(init_shape), 0.0f)); + auto read_value = std::make_shared(init, variable); + + auto assign = std::make_shared(read_value, variable); + + return {read_value, assign, read_value->output(0)}; +} + +// Standard LLM KV cache: variable_id = "past_key_values.N.keypresent.N.key" +CacheStateBlock make_past_key_values_kv_state(const ov::Output& beam_idx, + size_t layer_idx, + const std::string& key_or_value, + ov::element::Type precision, + const ov::PartialShape& var_shape, + const ov::Shape& init_shape) { + const std::string idx = std::to_string(layer_idx); + const std::string var_name = "past_key_values." + idx + "." + key_or_value + + "present." + idx + "." + key_or_value; + return make_cache_state(beam_idx, var_name, precision, var_shape, init_shape); +} + +// Whisper decoder KV cache: variable_id = "past_key_values.N.decoder.keypresent.N.decoder.key" +CacheStateBlock make_whisper_decoder_kv_state(const ov::Output& beam_idx, + size_t layer_idx, + const std::string& key_or_value, + ov::element::Type precision, + const ov::PartialShape& var_shape, + const ov::Shape& init_shape) { + const std::string idx = std::to_string(layer_idx); + const std::string var_name = "past_key_values." + idx + ".decoder." + key_or_value + + "present." + idx + ".decoder." + key_or_value; + return make_cache_state(beam_idx, var_name, precision, var_shape, init_shape); +} + +// Whisper encoder KV cache: numeric variable_id, NOT connected to beam_idx. +CacheStateBlock make_whisper_encoder_kv_state(const std::string& numeric_id, + ov::element::Type precision, + const ov::PartialShape& var_shape, + const ov::Shape& init_shape) { + return make_cache_state(numeric_id, precision, var_shape, init_shape); +} + +CacheStateBlock make_cache_params_kv_state(const ov::Output& beam_idx, + size_t layer_idx, + const std::string& key_or_value, + ov::element::Type precision, + const ov::PartialShape& var_shape, + const ov::Shape& init_shape) { + const std::string var_name = "cache_params.past." + key_or_value + "." + std::to_string(layer_idx) + + "cache_params.present." + key_or_value + "." + std::to_string(layer_idx); + return make_cache_state(beam_idx, var_name, precision, var_shape, init_shape); +} + +// Creates a linear cache block (conv) NOT connected to beam_idx. +CacheStateBlock make_cache_params_gated_short_conv_state(size_t layer_idx, + ov::element::Type precision, + const ov::Shape& shape) { + const std::string var_name = "cache_params.past.conv." + std::to_string(layer_idx) + + "cache_params.present.conv." + std::to_string(layer_idx); + return make_cache_state(var_name, precision, shape, shape); +} + +// Creates a linear cache block (conv, ssm) connected to beam_idx via Gather. +CacheStateBlock make_cache_params_lin_state(const ov::Output& beam_idx, + size_t layer_idx, + const std::string& cache_type, + ov::element::Type precision, + const ov::PartialShape& var_shape, + const ov::Shape& init_shape) { + const std::string var_name = "cache_params.past." + cache_type + "." + std::to_string(layer_idx) + + "cache_params.present." + cache_type + "." + std::to_string(layer_idx); + return make_cache_state(beam_idx, var_name, precision, var_shape, init_shape); +} + +// Builds a model matching TinyLlama-1.1B caches (scaled down to 2 layers): +// - Standard KV cache naming: past_key_values.N.key/value (f32, {?,4,?,64}) +// - All connected via beam_idx. +std::shared_ptr build_model_with_tinyllama_like_cache(size_t num_layers = 2) { + auto stub_input = std::make_shared(ov::element::f32, ov::PartialShape{-1, 64}); + stub_input->output(0).set_names({"stub_input"}); + + auto beam_idx = std::make_shared(ov::element::i32, ov::PartialShape{-1}); + beam_idx->output(0).set_names({"beam_idx"}); + + ov::SinkVector sinks; + for (size_t layer = 0; layer < num_layers; ++layer) { + auto key_block = make_past_key_values_kv_state( + beam_idx, layer, "key", ov::element::f32, ov::PartialShape{-1, 4, -1, 64}, {1, 4, 0, 64}); + auto value_block = make_past_key_values_kv_state( + beam_idx, layer, "value", ov::element::f32, ov::PartialShape{-1, 4, -1, 64}, {1, 4, 0, 64}); + sinks.push_back(key_block.assign); + sinks.push_back(value_block.assign); + } + + auto result = std::make_shared(stub_input); + return std::make_shared(ov::ResultVector{result}, + sinks, + ov::ParameterVector{stub_input, beam_idx}, + "model_with_tinyllama_like_cache"); +} + +// Builds a model matching Whisper decoder caches (scaled down to 2 layers): +// - 2 decoder KV layers: past_key_values.N.decoder.key/value (f32, {?,8,?,64}), connected to beam_idx +// - 2 encoder KV layers: numeric variable_ids (e.g. "242", "244"), NOT connected to beam_idx +// The decoder naming doesn't match the StatefulToStateless regex, so "input_restored." prefix is expected. +// The encoder numeric ids don't match and they also are NOT connected to beam_idx, so they remain unchanged.. +std::shared_ptr build_model_with_whisper_decoder_like_cache(size_t num_layers = 2) { + auto stub_input = std::make_shared(ov::element::f32, ov::PartialShape{-1, 64}); + stub_input->output(0).set_names({"stub_input"}); + + auto beam_idx = std::make_shared(ov::element::i32, ov::PartialShape{-1}); + beam_idx->output(0).set_names({"beam_idx"}); + + ov::SinkVector sinks; + size_t encoder_id_counter = 242; + for (size_t layer = 0; layer < num_layers; ++layer) { + // Decoder KV cache (connected to beam_idx, uses "decoder" in variable_id) + auto dec_key = make_whisper_decoder_kv_state( + beam_idx, layer, "key", ov::element::f32, ov::PartialShape{-1, 8, -1, 64}, {1, 8, 0, 64}); + auto dec_value = make_whisper_decoder_kv_state( + beam_idx, layer, "value", ov::element::f32, ov::PartialShape{-1, 8, -1, 64}, {1, 8, 0, 64}); + sinks.push_back(dec_key.assign); + sinks.push_back(dec_value.assign); + + // Encoder KV cache (NOT connected to beam_idx, numeric variable_ids) + auto enc_key = make_whisper_encoder_kv_state( + std::to_string(encoder_id_counter++), ov::element::f32, ov::PartialShape{-1, 8, -1, 64}, {1, 8, 0, 64}); + auto enc_value = make_whisper_encoder_kv_state( + std::to_string(encoder_id_counter++), ov::element::f32, ov::PartialShape{-1, 8, -1, 64}, {1, 8, 0, 64}); + sinks.push_back(enc_key.assign); + sinks.push_back(enc_value.assign); + } + + auto result = std::make_shared(stub_input); + return std::make_shared(ov::ResultVector{result}, + sinks, + ov::ParameterVector{stub_input, beam_idx}, + "model_with_whisper_decoder_like_cache"); +} + +// Builds a model matching LFM2-1.2B caches (scaled down to 2 layers): +// - 2 KV-cache layers: cache_params.past.key. / value. (f32, {?,8,?,64}) +// - 2 Gated Short Convolution (conv) layers: cache_params.past.conv. (f32, {?,2048,3}) +// KV caches are connected via beam_idx; conv caches are not. +std::shared_ptr build_model_with_lfm2_like_cache(size_t num_layers = 2) { + auto stub_input = std::make_shared(ov::element::f32, ov::PartialShape{-1, 64}); + stub_input->output(0).set_names({"stub_input"}); + + auto beam_idx = std::make_shared(ov::element::i32, ov::PartialShape{-1}); + beam_idx->output(0).set_names({"beam_idx"}); + + ov::SinkVector sinks; + for (size_t layer = 0; layer < num_layers; ++layer) { + // KV cache (connected to beam_idx) + auto key_block = + make_cache_params_kv_state(beam_idx, layer, "key", ov::element::f32, + ov::PartialShape{-1, 8, -1, 64}, {1, 8, 0, 64}); + auto value_block = + make_cache_params_kv_state(beam_idx, layer, "value", ov::element::f32, + ov::PartialShape{-1, 8, -1, 64}, {1, 8, 0, 64}); + sinks.push_back(key_block.assign); + sinks.push_back(value_block.assign); + + // Gated Short Convolution cache (NOT connected to beam_idx) + auto conv_block = make_cache_params_gated_short_conv_state(layer, ov::element::f32, {1, 2048, 3}); + sinks.push_back(conv_block.assign); + } + + auto result = std::make_shared(stub_input); + return std::make_shared(ov::ResultVector{result}, + sinks, + ov::ParameterVector{stub_input, beam_idx}, + "model_with_lfm2_like_cache"); +} + +// Builds a model matching Qwen3.5-2B caches (scaled down to 2 layers): +// - 2 KV-cache layers: cache_params.past.key. / value. (f32, {?,2,?,256}) +// - 2 GatedDeltaNet (ssm) layers: cache_params.past.ssm. (f32, {?,16,128,128}) +// - 2 Convolution1D (conv) layers: cache_params.past.conv. (f32, {?,6144,4}) +// All cache types are connected via beam_idx Gather in Qwen3.5-2B. +std::shared_ptr build_model_with_qwen35_like_cache(size_t num_layers = 2) { + auto stub_input = std::make_shared(ov::element::f32, ov::PartialShape{-1, 64}); + stub_input->output(0).set_names({"stub_input"}); + + auto beam_idx = std::make_shared(ov::element::i32, ov::PartialShape{-1}); + beam_idx->output(0).set_names({"beam_idx"}); + + ov::SinkVector sinks; + for (size_t layer = 0; layer < num_layers; ++layer) { + // KV cache + auto key_block = + make_cache_params_kv_state(beam_idx, layer, "key", ov::element::f32, ov::PartialShape{-1, 2, -1, 256}, {1, 2, 0, 256}); + auto value_block = + make_cache_params_kv_state(beam_idx, layer, "value", ov::element::f32, ov::PartialShape{-1, 2, -1, 256}, {1, 2, 0, 256}); + sinks.push_back(key_block.assign); + sinks.push_back(value_block.assign); + + // GatedDeltaNet SSM cache + auto ssm_block = + make_cache_params_lin_state(beam_idx, layer, "ssm", ov::element::f32, ov::PartialShape{-1, 16, 128, 128}, {1, 16, 128, 128}); + sinks.push_back(ssm_block.assign); + + // Conv1D cache + auto conv_block = + make_cache_params_lin_state(beam_idx, layer, "conv", ov::element::f32, ov::PartialShape{-1, 6144, 4}, {1, 6144, 4}); + sinks.push_back(conv_block.assign); + } + + auto result = std::make_shared(stub_input); + return std::make_shared(ov::ResultVector{result}, + sinks, + ov::ParameterVector{stub_input, beam_idx}, + "model_with_qwen35_like_cache"); +} + +bool has_input_with_name(const std::shared_ptr& model, const std::string& name) { + for (const auto& input : model->inputs()) { + for (const auto& input_name : input.get_names()) { + if (input_name.find(name) != std::string::npos) { + return true; + } + } + } + return false; +} + +bool has_output_with_name(const std::shared_ptr& model, const std::string& name) { + for (const auto& output : model->outputs()) { + for (const auto& output_name : output.get_names()) { + if (output_name.find(name) != std::string::npos) { + return true; + } + } + } + return false; +} + +std::vector collect_input_names(const std::shared_ptr& model) { + std::vector names; + for (const auto& input : model->inputs()) { + names.push_back(input.get_any_name()); + } + return names; +} + +std::vector collect_output_names(const std::shared_ptr& model) { + std::vector names; + for (const auto& output : model->outputs()) { + names.push_back(output.get_any_name()); + } + return names; +} + +TEST(StatefulToStatelessTest, TinyLlama_ConvertsToStateless) { + auto model = build_model_with_tinyllama_like_cache(); + + ASSERT_EQ(model->get_sinks().size(), 4u); + ASSERT_EQ(model->get_variables().size(), 4u); + + ASSERT_NO_THROW(ov::pass::StatefulToStateless().run_on_model(model)); + + EXPECT_EQ(model->get_sinks().size(), 0u); + EXPECT_EQ(model->get_variables().size(), 0u); + + for (size_t layer = 0; layer < 2; ++layer) { + const auto idx = std::to_string(layer); + EXPECT_TRUE(has_input_with_name(model, "past_key_values." + idx + ".key")); + EXPECT_TRUE(has_input_with_name(model, "past_key_values." + idx + ".value")); + EXPECT_TRUE(has_output_with_name(model, "present." + idx + ".key")); + EXPECT_TRUE(has_output_with_name(model, "present." + idx + ".value")); + } + + // Verify ordering: key(0), value(0), key(1), value(1) + auto input_names = collect_input_names(model); + std::vector kv_inputs; + for (const auto& name : input_names) { + if (name.find("past_key_values") != std::string::npos) { + kv_inputs.push_back(name); + } + } + ASSERT_EQ(kv_inputs.size(), 4u); + EXPECT_NE(kv_inputs[0].find("0.key"), std::string::npos); + EXPECT_NE(kv_inputs[1].find("0.value"), std::string::npos); + EXPECT_NE(kv_inputs[2].find("1.key"), std::string::npos); + EXPECT_NE(kv_inputs[3].find("1.value"), std::string::npos); + + // Verify output ordering: present.0.key, present.0.value, present.1.key, present.1.value + auto output_names = collect_output_names(model); + std::vector kv_outputs; + for (const auto& name : output_names) { + if (name.find("present") != std::string::npos) { + kv_outputs.push_back(name); + } + } + ASSERT_EQ(kv_outputs.size(), 4u); + EXPECT_NE(kv_outputs[0].find("0.key"), std::string::npos); + EXPECT_NE(kv_outputs[1].find("0.value"), std::string::npos); + EXPECT_NE(kv_outputs[2].find("1.key"), std::string::npos); + EXPECT_NE(kv_outputs[3].find("1.value"), std::string::npos); +} + +// Whisper decoder naming ("past_key_values.N.decoder.key") doesn't match the +// StatefulToStateless regex, so converted parameters get "input_restored." prefix. +// Encoder states (numeric variable_ids) are not connected to beam_idx and don't match +// any naming convention, so they remain unchanged. +TEST(StatefulToStatelessTest, WhisperDecoder_DecoderStatesGetRestoredPrefix) { + auto model = build_model_with_whisper_decoder_like_cache(); + + // 2 layers * (dec_key + dec_value + enc_key + enc_value) = 8 sinks + ASSERT_EQ(model->get_sinks().size(), 8u); + ASSERT_EQ(model->get_variables().size(), 8u); + + ASSERT_NO_THROW(ov::pass::StatefulToStateless().run_on_model(model)); + + // Decoder states (connected to beam_idx) get converted with "input_restored." prefix + for (size_t layer = 0; layer < 2; ++layer) { + const auto idx = std::to_string(layer); + const auto decoder_key_var = + "past_key_values." + idx + ".decoder.keypresent." + idx + ".decoder.key"; + const auto decoder_value_var = + "past_key_values." + idx + ".decoder.valuepresent." + idx + ".decoder.value"; + + EXPECT_TRUE(has_input_with_name(model, "input_restored." + decoder_key_var)) + << "Decoder key layer " << layer << " should get input_restored prefix"; + EXPECT_TRUE(has_input_with_name(model, "input_restored." + decoder_value_var)) + << "Decoder value layer " << layer << " should get input_restored prefix"; + EXPECT_TRUE(has_output_with_name(model, "output_restored." + decoder_key_var)) + << "Decoder key layer " << layer << " should get output_restored prefix"; + EXPECT_TRUE(has_output_with_name(model, "output_restored." + decoder_value_var)) + << "Decoder value layer " << layer << " should get output_restored prefix"; + } + + // Encoder states (numeric ids, not connected to beam_idx) remain as sinks/variables + EXPECT_EQ(model->get_sinks().size(), 4u) << "Encoder states should remain as sinks"; + EXPECT_EQ(model->get_variables().size(), 4u) << "Encoder variables should remain"; +} + + +// Verifies full conversion: sinks/variables removed, beam_idx removed, +// KV inputs -> past_key_values naming, conv inputs -> cache_params naming. +TEST(StatefulToStatelessTest, LFM2_ConvertsToStateless) { + auto model = build_model_with_lfm2_like_cache(); + + // 2 layers x (key + value + conv) = 6 sinks + ASSERT_EQ(model->get_sinks().size(), 6u); + ASSERT_EQ(model->get_variables().size(), 6u); + + ASSERT_NO_THROW(ov::pass::StatefulToStateless().run_on_model(model)); + + EXPECT_EQ(model->get_sinks().size(), 0u); + EXPECT_EQ(model->get_variables().size(), 0u); + + // KV cache key/value -> mapped to standard "past_key_values" / "present" naming + for (size_t layer = 0; layer < 2; ++layer) { + const auto idx = std::to_string(layer); + EXPECT_TRUE(has_input_with_name(model, "past_key_values." + idx + ".key")); + EXPECT_TRUE(has_input_with_name(model, "past_key_values." + idx + ".value")); + EXPECT_TRUE(has_output_with_name(model, "present." + idx + ".key")); + EXPECT_TRUE(has_output_with_name(model, "present." + idx + ".value")); + } + + // Conv cache -> keeps cache_params naming + for (size_t layer = 0; layer < 2; ++layer) { + const auto idx = std::to_string(layer); + EXPECT_TRUE(has_input_with_name(model, "cache_params.past.conv." + idx)); + EXPECT_TRUE(has_output_with_name(model, "cache_params.present.conv." + idx)); + } +} + +// Verifies ordering: KV caches first (key before value, layer 0 before 1), then conv in natural order. +TEST(StatefulToStatelessTest, LFM2_KVCacheBeforeConv) { + auto model = build_model_with_lfm2_like_cache(); + ov::pass::StatefulToStateless().run_on_model(model); + + auto input_names = collect_input_names(model); + + std::vector cache_inputs; + for (const auto& name : input_names) { + if (name.find("past_key_values") != std::string::npos || + name.find("cache_params") != std::string::npos) { + cache_inputs.push_back(name); + } + } + + // 2 layers: key(0), value(0), key(1), value(1), conv(0), conv(1) + ASSERT_EQ(cache_inputs.size(), 6u); + EXPECT_NE(cache_inputs[0].find("0.key"), std::string::npos); + EXPECT_NE(cache_inputs[1].find("0.value"), std::string::npos); + EXPECT_NE(cache_inputs[2].find("1.key"), std::string::npos); + EXPECT_NE(cache_inputs[3].find("1.value"), std::string::npos); + + // Conv caches after KV + EXPECT_NE(cache_inputs[4].find("conv"), std::string::npos); + EXPECT_NE(cache_inputs[5].find("conv"), std::string::npos); + + // Verify same ordering for outputs: present.0.key, present.0.value, ..., cache_params.present.conv.0, ... + auto output_names = collect_output_names(model); + std::vector cache_outputs; + for (const auto& name : output_names) { + if (name.find("present") != std::string::npos) { + cache_outputs.push_back(name); + } + } + ASSERT_EQ(cache_outputs.size(), 6u); + EXPECT_NE(cache_outputs[0].find("0.key"), std::string::npos); + EXPECT_NE(cache_outputs[1].find("0.value"), std::string::npos); + EXPECT_NE(cache_outputs[2].find("1.key"), std::string::npos); + EXPECT_NE(cache_outputs[3].find("1.value"), std::string::npos); + EXPECT_NE(cache_outputs[4].find("conv"), std::string::npos); + EXPECT_NE(cache_outputs[5].find("conv"), std::string::npos); +} + +// Verifies that shapes from the real model are preserved through conversion. +TEST(StatefulToStatelessTest, LFM2_ShapesAndElementsTypeArePreserved) { + auto model = build_model_with_lfm2_like_cache(1); // 1 layer for simplicity + ov::pass::StatefulToStateless().run_on_model(model); + + for (const auto& input : model->inputs()) { + const auto& name = input.get_any_name(); + const auto& shape = input.get_partial_shape(); + + if (name.find("past_key_values") != std::string::npos) { + // KV cache: {?,8,?,64}, f32 + ASSERT_EQ(shape.rank().get_length(), 4) << "KV input " << name << " should be rank 4"; + EXPECT_EQ(shape[1].get_length(), 8) << "Num heads mismatch for " << name; + EXPECT_EQ(shape[3].get_length(), 64) << "Head dim mismatch for " << name; + EXPECT_EQ(input.get_element_type(), ov::element::f32) << "Element type mismatch for " << name; + } else if (name.find("cache_params.past.conv") != std::string::npos) { + // Conv cache: {1, 2048, 3}, f32 + ASSERT_EQ(shape.rank().get_length(), 3) << "Conv input " << name << " should be rank 3"; + EXPECT_EQ(shape[1].get_length(), 2048) << "Conv 1st dim mismatch for " << name; + EXPECT_EQ(shape[2].get_length(), 3) << "Conv kernel size mismatch for " << name; + EXPECT_EQ(input.get_element_type(), ov::element::f32) << "Element type mismatch for " << name; + } + } +} + +// Verifies full conversion with all three cache types. +TEST(StatefulToStatelessTest, Qwen35_ConvertsToStateless) { + auto model = build_model_with_qwen35_like_cache(); + + // 2 layers x (key + value + ssm + conv) = 8 sinks + ASSERT_EQ(model->get_sinks().size(), 8u); + ASSERT_EQ(model->get_variables().size(), 8u); + + ASSERT_NO_THROW(ov::pass::StatefulToStateless().run_on_model(model)); + + EXPECT_EQ(model->get_sinks().size(), 0u); + EXPECT_EQ(model->get_variables().size(), 0u); + + for (size_t layer = 0; layer < 2; ++layer) { + const auto idx = std::to_string(layer); + // KV -> standard naming + EXPECT_TRUE(has_input_with_name(model, "past_key_values." + idx + ".key")); + EXPECT_TRUE(has_input_with_name(model, "past_key_values." + idx + ".value")); + EXPECT_TRUE(has_output_with_name(model, "present." + idx + ".key")); + EXPECT_TRUE(has_output_with_name(model, "present." + idx + ".value")); + + // SSM -> cache_params naming + EXPECT_TRUE(has_input_with_name(model, "cache_params.past.ssm." + idx)); + EXPECT_TRUE(has_output_with_name(model, "cache_params.present.ssm." + idx)); + + // Conv -> cache_params naming + EXPECT_TRUE(has_input_with_name(model, "cache_params.past.conv." + idx)); + EXPECT_TRUE(has_output_with_name(model, "cache_params.present.conv." + idx)); + } +} + +// Verifies ordering: KV first (key before value), then ssm/conv in natural order. +TEST(StatefulToStatelessTest, Qwen35_KVCacheBeforeLinearCaches) { + auto model = build_model_with_qwen35_like_cache(); + ov::pass::StatefulToStateless().run_on_model(model); + + auto input_names = collect_input_names(model); + + int kv_last_pos = -1; + int lin_first_pos = static_cast(input_names.size()); + + for (int i = 0; i < static_cast(input_names.size()); ++i) { + if (input_names[i].find("past_key_values") != std::string::npos) { + kv_last_pos = i; + } + if (input_names[i].find("cache_params") != std::string::npos) { + lin_first_pos = std::min(lin_first_pos, i); + } + } + + EXPECT_LT(kv_last_pos, lin_first_pos) + << "KV cache parameters should appear before linear cache parameters"; + + // Same check for outputs + auto output_names = collect_output_names(model); + int kv_out_last_pos = -1; + int lin_out_first_pos = static_cast(output_names.size()); + for (int i = 0; i < static_cast(output_names.size()); ++i) { + if (output_names[i].find("present") != std::string::npos && + output_names[i].find("cache_params") == std::string::npos) { + kv_out_last_pos = i; + } + if (output_names[i].find("cache_params.present") != std::string::npos) { + lin_out_first_pos = std::min(lin_out_first_pos, i); + } + } + EXPECT_LT(kv_out_last_pos, lin_out_first_pos) + << "KV cache outputs should appear before linear cache outputs"; +} + +// Verifies KV key-before-value ordering within each layer. +TEST(StatefulToStatelessTest, Qwen35_KeyBeforeValueOrdering) { + auto model = build_model_with_qwen35_like_cache(); + ov::pass::StatefulToStateless().run_on_model(model); + + auto input_names = collect_input_names(model); + + std::vector kv_inputs; + for (const auto& name : input_names) { + if (name.find("past_key_values") != std::string::npos) { + kv_inputs.push_back(name); + } + } + + // Expected: key(0), value(0), key(1), value(1) + ASSERT_EQ(kv_inputs.size(), 4u); + EXPECT_NE(kv_inputs[0].find("0.key"), std::string::npos); + EXPECT_NE(kv_inputs[1].find("0.value"), std::string::npos); + EXPECT_NE(kv_inputs[2].find("1.key"), std::string::npos); + EXPECT_NE(kv_inputs[3].find("1.value"), std::string::npos); + + // Same ordering for outputs + auto output_names = collect_output_names(model); + std::vector kv_outputs; + for (const auto& name : output_names) { + if (name.find("present") != std::string::npos && + name.find("cache_params") == std::string::npos) { + kv_outputs.push_back(name); + } + } + ASSERT_EQ(kv_outputs.size(), 4u); + EXPECT_NE(kv_outputs[0].find("0.key"), std::string::npos); + EXPECT_NE(kv_outputs[1].find("0.value"), std::string::npos); + EXPECT_NE(kv_outputs[2].find("1.key"), std::string::npos); + EXPECT_NE(kv_outputs[3].find("1.value"), std::string::npos); +} + +// Verifies shapes and element types from the real Qwen3.5 model are preserved. +TEST(StatefulToStatelessTest, Qwen35_ShapesAndElementTypesPreserved) { + auto model = build_model_with_qwen35_like_cache(1); + ov::pass::StatefulToStateless().run_on_model(model); + + for (const auto& input : model->inputs()) { + const auto& name = input.get_any_name(); + const auto& shape = input.get_partial_shape(); + + if (name.find("past_key_values") != std::string::npos) { + // KV cache: {?,2,?,256}, f32 + ASSERT_EQ(shape.rank().get_length(), 4) << "KV input " << name << " should be rank 4"; + EXPECT_EQ(shape[1].get_length(), 2) << "Num heads mismatch for " << name; + EXPECT_EQ(shape[3].get_length(), 256) << "Head dim mismatch for " << name; + EXPECT_EQ(input.get_element_type(), ov::element::f32) << "Element type mismatch for " << name; + } else if (name.find("cache_params.past.ssm") != std::string::npos) { + // SSM cache: {1, 16, 128, 128}, f32 + ASSERT_EQ(shape.rank().get_length(), 4) << "SSM input " << name << " should be rank 4"; + EXPECT_EQ(shape[1].get_length(), 16); + EXPECT_EQ(shape[2].get_length(), 128); + EXPECT_EQ(shape[3].get_length(), 128); + EXPECT_EQ(input.get_element_type(), ov::element::f32) << "Element type mismatch for " << name; + } else if (name.find("cache_params.past.conv") != std::string::npos) { + // Conv cache: {1, 6144, 4}, f32 + ASSERT_EQ(shape.rank().get_length(), 3) << "Conv input " << name << " should be rank 3"; + EXPECT_EQ(shape[1].get_length(), 6144); + EXPECT_EQ(shape[2].get_length(), 4); + EXPECT_EQ(input.get_element_type(), ov::element::f32) << "Element type mismatch for " << name; + } + } +} +} // namespace diff --git a/src/plugins/intel_npu/tests/unit/npuw/pre_compute_test.cpp b/src/plugins/intel_npu/tests/unit/npuw/pre_compute_test.cpp new file mode 100644 index 000000000000..2690cd52d4d2 --- /dev/null +++ b/src/plugins/intel_npu/tests/unit/npuw/pre_compute_test.cpp @@ -0,0 +1,128 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include + +#include +#include +#include +#include + +#include "openvino/core/except.hpp" +#include "openvino/op/ops.hpp" +#include "partitioning/patterns/pre_compute.hpp" + +namespace { + +std::shared_ptr make_longrope_v5_model(const std::vector& short_factor_values, + const std::vector& long_factor_values, + const std::vector& multiply_values, + const std::vector& power_values) { + auto data = std::make_shared(ov::element::f32, ov::Shape{1, 4, 2}); + auto position_ids = std::make_shared(ov::element::i32, ov::Shape{2, 1}); + + auto short_factor = ov::op::v0::Constant::create(ov::element::f32, + ov::Shape{short_factor_values.size()}, + short_factor_values); + auto long_factor = ov::op::v0::Constant::create(ov::element::f32, + ov::Shape{long_factor_values.size()}, + long_factor_values); + auto multiply_const = ov::op::v0::Constant::create(ov::element::f32, + ov::Shape{multiply_values.size()}, + multiply_values); + auto power_const = ov::op::v0::Constant::create(ov::element::f32, ov::Shape{power_values.size()}, power_values); + + auto reduce_axes = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{2}, {0, 1}); + auto red_max = std::make_shared(position_ids, reduce_axes, false); + auto one_i32 = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{}, {1}); + auto add = std::make_shared(red_max, one_i32); + auto max_pos = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{}, {4}); + auto greater = std::make_shared(add, max_pos); + + auto select = std::make_shared(greater, long_factor, short_factor); + auto multiply = std::make_shared(select, multiply_const); + auto power = std::make_shared(multiply, power_const); + + auto unsqueeze_axis0 = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{1}, {0}); + auto unsq0 = std::make_shared(power, unsqueeze_axis0); + auto unsq1 = std::make_shared(unsq0, unsqueeze_axis0); + + auto shape_of = std::make_shared(data); + auto gather_idx0 = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{1}, {0}); + auto axis0 = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{}, {0}); + auto gather = std::make_shared(shape_of, gather_idx0, axis0); + auto seq_len = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{1}, {4}); + auto rotary_dims = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{1}, {2}); + auto concat_1 = std::make_shared(ov::OutputVector{gather, seq_len, rotary_dims}, 0); + + auto broadcast = std::make_shared(unsq1, concat_1); + auto pos_unsq = std::make_shared(position_ids, unsqueeze_axis0); + auto pos_fp32 = std::make_shared(pos_unsq, ov::element::f32); + auto matmul = std::make_shared(broadcast, pos_fp32); + + auto transpose_order = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{3}, {0, 2, 1}); + auto transpose = std::make_shared(matmul, transpose_order); + auto zeros = ov::op::v0::Constant::create(ov::element::f32, ov::Shape{1, 1, 4}, {0.0f, 0.0f, 0.0f, 0.0f}); + auto concat_2 = std::make_shared(ov::OutputVector{transpose, zeros}, 1); + + auto sin = std::make_shared(concat_2); + auto cos = std::make_shared(concat_2); + + sin->set_friendly_name("sin_out"); + cos->set_friendly_name("cos_out"); + + auto sin_res = std::make_shared(sin); + auto cos_res = std::make_shared(cos); + return std::make_shared(ov::ResultVector{sin_res, cos_res}, + ov::ParameterVector{data, position_ids}, + "longrope_v5_test_model"); +} + +bool has_input_name(const std::shared_ptr& model, const std::string& name) { + const auto inputs = model->inputs(); + return std::any_of(inputs.begin(), inputs.end(), [&name](const auto& input) { + const auto& names = input.get_names(); + return std::any_of(names.begin(), names.end(), [&name](const auto& candidate) { + return candidate == name; + }); + }); +} + +TEST(PreComputeTest, RopeCacheTransformsLongRopeV5Pattern) { + auto model = make_longrope_v5_model({1.0f, 2.0f}, {4.0f, 5.0f}, {0.5f, 1.0f}, {2.0f}); + + ov::npuw::patterns::pre_compute::RopeCache pass(/*max_prompt_len=*/16, "longrope_input"); + ASSERT_NO_THROW(pass.run_on_model(model)); + + const auto& ops = model->get_ops(); + const auto sin_count = std::count_if(ops.begin(), ops.end(), [](const auto& op) { + return ov::is_type(op); + }); + const auto cos_count = std::count_if(ops.begin(), ops.end(), [](const auto& op) { + return ov::is_type(op); + }); + + EXPECT_EQ(sin_count, 0); + EXPECT_EQ(cos_count, 0); + EXPECT_TRUE(has_input_name(model, "longrope_input")); +} + +TEST(PreComputeTest, RopeCacheThrowsOnMismatchedFactorSizesInLongRopeV5) { + // multiply has scalar shape {1}: graph is valid by broadcast, but calculate_freq requires exact size match. + auto model = make_longrope_v5_model({1.0f, 2.0f}, {4.0f, 5.0f}, {1.0f}, {1.0f}); + ov::npuw::patterns::pre_compute::RopeCache pass(/*max_prompt_len=*/16, "longrope_input"); + + EXPECT_THROW(pass.run_on_model(model), + ov::AssertFailure); +} + +TEST(PreComputeTest, RopeCacheThrowsOnNonScalarPowerInLongRopeV5) { + auto model = make_longrope_v5_model({1.0f, 2.0f}, {4.0f, 5.0f}, {1.0f, 2.0f}, {1.0f, 2.0f}); + ov::npuw::patterns::pre_compute::RopeCache pass(/*max_prompt_len=*/16, "longrope_input"); + + EXPECT_THROW(pass.run_on_model(model), + ov::AssertFailure); +} + +} // namespace diff --git a/src/plugins/intel_npu/tests/unit/npuw/preserve_const_matmul_pattern_test.cpp b/src/plugins/intel_npu/tests/unit/npuw/preserve_const_matmul_pattern_test.cpp new file mode 100644 index 000000000000..6d8956f99900 --- /dev/null +++ b/src/plugins/intel_npu/tests/unit/npuw/preserve_const_matmul_pattern_test.cpp @@ -0,0 +1,199 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include + +#include +#include +#include + +#include "openvino/op/constant.hpp" +#include "openvino/op/convert.hpp" +#include "openvino/op/matmul.hpp" +#include "openvino/op/multiply.hpp" +#include "openvino/op/parameter.hpp" +#include "openvino/op/result.hpp" +#include "openvino/op/subtract.hpp" +#include "openvino/pass/manager.hpp" +#include "partitioning/patterns/opt.hpp" + +namespace { + +using ResultNodes = std::vector>; + +// Build the subgraph matched by PreserveConstDictMatMulAsymm: +// Const(W:u8) -> Convert(f16) ------+ +// Const(Z:u8) -> Convert(f16) -> Subtract -> Multiply -> [Convert(f32) ->] MatMul -> Result +// Const(S:f16) ----------------------------+ +// Parameter(act:f32) --------------------------------------------> +// The Convert between Multiply and MatMul is optional (convert_before_matmul controls it). +struct SubgraphNodes { + std::shared_ptr qweight; + std::shared_ptr qzerop; + std::shared_ptr qcoeff; + std::shared_ptr model; +}; + +SubgraphNodes build_asymm_matmul_subgraph(const ov::Shape& weight_shape, + const ov::element::Type& weight_type, + const ov::Shape& zerop_shape, + const ov::Shape& scale_shape, + bool transpose_b, + bool convert_before_matmul = true) { + auto qweight = std::make_shared(weight_type, weight_shape, 0); + auto qzerop = std::make_shared(weight_type, zerop_shape, 0); + // When there is no Convert after Multiply, Multiply must produce f32 directly, which + // requires scale (qcoeff) to be f32 — matching real onnx-converted model behaviour. + const auto scale_type = convert_before_matmul ? ov::element::f16 : ov::element::f32; + auto qcoeff = std::make_shared(scale_type, scale_shape, 1.0f); + + // When convert_before_matmul=true (optimum-intel style): + // u8 → Convert(f16) → Subtract → Multiply(f16) → Convert(f32) → MatMul(f32) + // scale (qcoeff) is f16 + // When convert_before_matmul=false (onnx-converted style): + // u8 → Convert(f32) → Subtract → Multiply(f32) → MatMul(f32) [no final Convert] + // scale (qcoeff) is f32 + const auto mid_type = convert_before_matmul ? ov::element::f16 : ov::element::f32; + auto cvtw = std::make_shared(qweight, mid_type); + auto cvtz = std::make_shared(qzerop, mid_type); + auto sub = std::make_shared(cvtw, cvtz); + auto mul = std::make_shared(sub, qcoeff); + + std::shared_ptr mm_weight_input; + if (convert_before_matmul) { + mm_weight_input = std::make_shared(mul, ov::element::f32); + } else { + mm_weight_input = mul; + } + + const size_t IC = transpose_b ? weight_shape[1] : weight_shape[0]; + auto act = std::make_shared(ov::element::f32, ov::Shape{1, IC}); + + auto mm = std::make_shared(act, mm_weight_input, false, transpose_b); + auto result = std::make_shared(mm); + + auto model = std::make_shared(ov::ResultVector{result}, ov::ParameterVector{act}); + return {qweight, qzerop, qcoeff, model}; +} + +void run_preserve_pattern(std::shared_ptr& model, ResultNodes& to_keep) { + ov::pass::Manager manager; + using namespace ov::npuw::patterns::opt; + Context ctx; + ctx.mm_gate = false; + manager.register_pass(std::ref(ctx), std::ref(to_keep)); + manager.run_passes(model); +} + +// ───────────────────────────────────────────── +// Test 1: Standard layout [OC, IC] + scale [OC, 1] + transpose_b=true → 3 consts kept +// ───────────────────────────────────────────── +TEST(OptPatterns, StandardLayout_MatchesAndPreservesConsts) { + const ov::Shape weight_shape{32064, 3072}; + const ov::Shape scale_shape{32064, 1}; + auto [qweight, qzerop, qcoeff, model] = + build_asymm_matmul_subgraph(weight_shape, ov::element::u8, weight_shape, scale_shape, /*transpose_b=*/true); + + ResultNodes to_keep; + run_preserve_pattern(model, to_keep); + + ASSERT_EQ(to_keep.size(), 3u); + EXPECT_TRUE(std::find(to_keep.begin(), to_keep.end(), qweight) != to_keep.end()); + EXPECT_TRUE(std::find(to_keep.begin(), to_keep.end(), qzerop) != to_keep.end()); + EXPECT_TRUE(std::find(to_keep.begin(), to_keep.end(), qcoeff) != to_keep.end()); +} + +// ───────────────────────────────────────────── +// Test 2: Pre-transposed layout [IC, OC] + scale [1, OC] + transpose_b=false → 3 consts kept +// ───────────────────────────────────────────── +TEST(OptPatterns, PreTransposedLayout_MatchesAndPreservesConsts) { + const ov::Shape weight_shape{3072, 32064}; + const ov::Shape scale_shape{1, 32064}; + auto [qweight, qzerop, qcoeff, model] = + build_asymm_matmul_subgraph(weight_shape, ov::element::u8, weight_shape, scale_shape, /*transpose_b=*/false); + + ResultNodes to_keep; + run_preserve_pattern(model, to_keep); + + ASSERT_EQ(to_keep.size(), 3u); + EXPECT_TRUE(std::find(to_keep.begin(), to_keep.end(), qweight) != to_keep.end()); + EXPECT_TRUE(std::find(to_keep.begin(), to_keep.end(), qzerop) != to_keep.end()); + EXPECT_TRUE(std::find(to_keep.begin(), to_keep.end(), qcoeff) != to_keep.end()); +} + +// ───────────────────────────────────────────── +// Test 3: Wrong scale layout [IC, OC] (per-element, not per-channel) → no match +// ───────────────────────────────────────────── +TEST(OptPatterns, WrongLayout_ScaleNotPerChannel_NoMatch) { + const ov::Shape weight_shape{3072, 32064}; + const ov::Shape scale_shape{3072, 32064}; // full per-element: neither [OC,1] nor [1,OC] + auto [qweight, qzerop, qcoeff, model] = + build_asymm_matmul_subgraph(weight_shape, ov::element::u8, weight_shape, scale_shape, /*transpose_b=*/false); + + ResultNodes to_keep; + run_preserve_pattern(model, to_keep); + + EXPECT_TRUE(to_keep.empty()); +} + +// ───────────────────────────────────────────── +// Test 4: Wrong element type (i8 instead of u8) → no match +// ───────────────────────────────────────────── +TEST(OptPatterns, WrongElemType_NotU8_NoMatch) { + const ov::Shape weight_shape{32064, 3072}; + const ov::Shape scale_shape{32064, 1}; + auto [qweight, qzerop, qcoeff, model] = + build_asymm_matmul_subgraph(weight_shape, ov::element::i8, weight_shape, scale_shape, /*transpose_b=*/true); + + ResultNodes to_keep; + run_preserve_pattern(model, to_keep); + + EXPECT_TRUE(to_keep.empty()); +} + +// ───────────────────────────────────────────── +// Test 5: Pre-transposed layout without Convert before MatMul (onnx-exported model pattern) +// ───────────────────────────────────────────── +TEST(OptPatterns, PreTransposedLayout_NoConvert_MatchesAndPreservesConsts) { + const ov::Shape weight_shape{3072, 32064}; + const ov::Shape scale_shape{1, 32064}; + auto [qweight, qzerop, qcoeff, model] = build_asymm_matmul_subgraph(weight_shape, + ov::element::u8, + weight_shape, + scale_shape, + /*transpose_b=*/false, + /*convert_before_matmul=*/false); + + ResultNodes to_keep; + run_preserve_pattern(model, to_keep); + + ASSERT_EQ(to_keep.size(), 3u); + EXPECT_TRUE(std::find(to_keep.begin(), to_keep.end(), qweight) != to_keep.end()); + EXPECT_TRUE(std::find(to_keep.begin(), to_keep.end(), qzerop) != to_keep.end()); + EXPECT_TRUE(std::find(to_keep.begin(), to_keep.end(), qcoeff) != to_keep.end()); +} + +// ───────────────────────────────────────────── +// Test 6: Standard layout without Convert before MatMul +// ───────────────────────────────────────────── +TEST(OptPatterns, StandardLayout_NoConvert_MatchesAndPreservesConsts) { + const ov::Shape weight_shape{32064, 3072}; + const ov::Shape scale_shape{32064, 1}; + auto [qweight, qzerop, qcoeff, model] = build_asymm_matmul_subgraph(weight_shape, + ov::element::u8, + weight_shape, + scale_shape, + /*transpose_b=*/true, + /*convert_before_matmul=*/false); + + ResultNodes to_keep; + run_preserve_pattern(model, to_keep); + + ASSERT_EQ(to_keep.size(), 3u); + EXPECT_TRUE(std::find(to_keep.begin(), to_keep.end(), qweight) != to_keep.end()); + EXPECT_TRUE(std::find(to_keep.begin(), to_keep.end(), qzerop) != to_keep.end()); + EXPECT_TRUE(std::find(to_keep.begin(), to_keep.end(), qcoeff) != to_keep.end()); +} + +} // namespace diff --git a/src/plugins/intel_npu/tests/unit/npuw/resolve_dynamic_quant_storage_types_test.cpp b/src/plugins/intel_npu/tests/unit/npuw/resolve_dynamic_quant_storage_types_test.cpp new file mode 100644 index 000000000000..ab3a77b01c59 --- /dev/null +++ b/src/plugins/intel_npu/tests/unit/npuw/resolve_dynamic_quant_storage_types_test.cpp @@ -0,0 +1,238 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include + +#include +#include +#include + +#include "util.hpp" + +namespace { + +using DQMode = ov::npuw::util::DynamicQuantDecomposeMode; + +struct ResolveStorageTypesParams { + DQMode mode; + bool is_symmetric; + ov::element::Type quant_dt; + ov::element::Type scale_dt; + ov::element::Type expected_quantized_data_type; + ov::element::Type expected_zero_point_type; + ov::element::Type expected_scale_type; +}; + +const char* mode_to_token(DQMode mode) { + switch (mode) { + case DQMode::HandcraftedSymmetricI8: + return "V1"; + case DQMode::OnnxDynamicQuantizeLinear: + return "V2"; + case DQMode::CompilerPatternI8: + return "V3"; + default: + return "Unknown"; + } +} + +const char* type_to_token(const ov::element::Type& type) { + if (type == ov::element::u8) { + return "u8"; + } + if (type == ov::element::i8) { + return "i8"; + } + if (type == ov::element::i4) { + return "i4"; + } + if (type == ov::element::f16) { + return "f16"; + } + if (type == ov::element::f32) { + return "f32"; + } + if (type == ov::element::dynamic) { + return "dyn"; + } + return "other"; +} + +std::string make_test_name(const ResolveStorageTypesParams& p) { + std::ostringstream os; + os << mode_to_token(p.mode) << "_" << (p.is_symmetric ? "Sym" : "Asym") << "_Q" << type_to_token(p.quant_dt) << "_S" + << type_to_token(p.scale_dt) << "_RQ" << type_to_token(p.expected_quantized_data_type) << "_RZ" + << type_to_token(p.expected_zero_point_type) << "_RS" << type_to_token(p.expected_scale_type); + return os.str(); +} + +std::vector make_all_cases() { + return { + {DQMode::HandcraftedSymmetricI8, + true, + ov::element::u8, + ov::element::f32, + ov::element::u8, + ov::element::dynamic, + ov::element::f32}, + {DQMode::HandcraftedSymmetricI8, + true, + ov::element::i8, + ov::element::f32, + ov::element::i8, + ov::element::dynamic, + ov::element::f32}, + {DQMode::HandcraftedSymmetricI8, + true, + ov::element::i4, + ov::element::f32, + ov::element::i4, + ov::element::dynamic, + ov::element::f32}, + {DQMode::HandcraftedSymmetricI8, + false, + ov::element::u8, + ov::element::f32, + ov::element::u8, + ov::element::u8, + ov::element::f32}, + {DQMode::HandcraftedSymmetricI8, + false, + ov::element::i8, + ov::element::f32, + ov::element::i8, + ov::element::i8, + ov::element::f32}, + {DQMode::HandcraftedSymmetricI8, + false, + ov::element::i4, + ov::element::f32, + ov::element::i4, + ov::element::i4, + ov::element::f32}, + + {DQMode::OnnxDynamicQuantizeLinear, + true, + ov::element::u8, + ov::element::f32, + ov::element::u8, + ov::element::dynamic, + ov::element::f32}, + {DQMode::OnnxDynamicQuantizeLinear, + true, + ov::element::i8, + ov::element::f32, + ov::element::i8, + ov::element::dynamic, + ov::element::f32}, + {DQMode::OnnxDynamicQuantizeLinear, + true, + ov::element::i4, + ov::element::f32, + ov::element::i4, + ov::element::dynamic, + ov::element::f32}, + {DQMode::OnnxDynamicQuantizeLinear, + false, + ov::element::u8, + ov::element::f32, + ov::element::u8, + ov::element::u8, + ov::element::f32}, + {DQMode::OnnxDynamicQuantizeLinear, + false, + ov::element::i8, + ov::element::f32, + ov::element::i8, + ov::element::i8, + ov::element::f32}, + {DQMode::OnnxDynamicQuantizeLinear, + false, + ov::element::i4, + ov::element::f32, + ov::element::i4, + ov::element::i4, + ov::element::f32}, + + {DQMode::CompilerPatternI8, + true, + ov::element::u8, + ov::element::f32, + ov::element::u8, + ov::element::dynamic, + ov::element::f32}, + {DQMode::CompilerPatternI8, + true, + ov::element::i8, + ov::element::f32, + ov::element::i8, + ov::element::dynamic, + ov::element::f32}, + {DQMode::CompilerPatternI8, + true, + ov::element::i4, + ov::element::f32, + ov::element::i4, + ov::element::dynamic, + ov::element::f32}, + {DQMode::CompilerPatternI8, + false, + ov::element::u8, + ov::element::f32, + ov::element::u8, + ov::element::u8, + ov::element::f32}, + {DQMode::CompilerPatternI8, + false, + ov::element::i8, + ov::element::f32, + ov::element::u8, + ov::element::u8, + ov::element::f32}, + {DQMode::CompilerPatternI8, + false, + ov::element::i4, + ov::element::f32, + ov::element::i4, + ov::element::i4, + ov::element::f32}, + + {DQMode::CompilerPatternI8, + false, + ov::element::i8, + ov::element::f16, + ov::element::u8, + ov::element::u8, + ov::element::f16}, + {DQMode::OnnxDynamicQuantizeLinear, + true, + ov::element::u8, + ov::element::f16, + ov::element::u8, + ov::element::dynamic, + ov::element::f16}, + }; +} + +class ResolveDynamicQuantStorageTypesTest : public ::testing::TestWithParam {}; + +TEST_P(ResolveDynamicQuantStorageTypesTest, CoversAllQuantDtSymmetryAndDecomposeModeCombinations) { + const auto& p = GetParam(); + + const auto resolved = + ov::npuw::util::resolve_dynamic_quant_storage_types(p.mode, p.is_symmetric, p.quant_dt, p.scale_dt); + + EXPECT_EQ(resolved.quantized_data_type, p.expected_quantized_data_type); + EXPECT_EQ(resolved.zero_point_type, p.expected_zero_point_type); + EXPECT_EQ(resolved.scale_type, p.expected_scale_type); +} + +INSTANTIATE_TEST_SUITE_P(AllCombinations, + ResolveDynamicQuantStorageTypesTest, + ::testing::ValuesIn(make_all_cases()), + [](const ::testing::TestParamInfo& info) { + return make_test_name(info.param); + }); + +} // namespace diff --git a/src/plugins/intel_npu/tests/unit/npuw/serialization.cpp b/src/plugins/intel_npu/tests/unit/npuw/serialization.cpp index f5d062106073..dce1fb740550 100644 --- a/src/plugins/intel_npu/tests/unit/npuw/serialization.cpp +++ b/src/plugins/intel_npu/tests/unit/npuw/serialization.cpp @@ -6,18 +6,161 @@ #include +#include +#include #include +#include "attention.hpp" +#include "common_test_utils/file_utils.hpp" #include "compiled_model.hpp" +#include "host_flash_attention.hpp" #include "intel_npu/config/config.hpp" #include "intel_npu/config/npuw.hpp" +#include "lazy_tensor.hpp" #include "model_builder.hpp" +#include "moe_transformations/moe_transformation.hpp" #include "openvino/core/parallel.hpp" +#include "openvino/core/rt_info/weightless_caching_attributes.hpp" +#include "openvino/op/constant.hpp" #include "openvino/openvino.hpp" +#include "openvino/runtime/shared_buffer.hpp" +#include "openvino/util/mmap_object.hpp" +#include "pyramid_attention.hpp" #include "spatial.hpp" +#include "weights_bank.hpp" using ov::test::npuw::ModelBuilder; +namespace { + +void expect_tensors_equal(const ov::Tensor& expected, const ov::Tensor& actual) { + ASSERT_EQ(static_cast(expected), static_cast(actual)); + if (!expected) { + return; + } + + EXPECT_EQ(expected.get_element_type(), actual.get_element_type()); + EXPECT_EQ(expected.get_shape(), actual.get_shape()); + ASSERT_EQ(expected.get_byte_size(), actual.get_byte_size()); + EXPECT_EQ(std::memcmp(expected.data(), actual.data(), expected.get_byte_size()), 0); +} + +template +std::shared_ptr make_weightless_constant(const ov::element::Type& type, + const ov::Shape& shape, + const std::vector& data, + std::size_t offset) { + auto constant = std::make_shared(type, shape, data); + constant->get_rt_info()[ov::WeightlessCacheAttribute::get_type_info_static()] = + ov::WeightlessCacheAttribute(constant->get_byte_size(), offset, type); + return constant; +} + +ov::npuw::s11n::WeightsContext::ConstsCache make_consts_cache( + const std::vector>& constants) { + ov::npuw::s11n::WeightsContext::ConstsCache cache; + for (const auto& constant : constants) { + const auto attr = constant->get_rt_info() + .at(ov::WeightlessCacheAttribute::get_type_info_static()) + .as(); + cache[{attr.bin_offset, constant->get_byte_size()}] = constant; + } + return cache; +} + +void expect_attention_equal(const ov::npuw::compiled::Attention& expected, + const ov::npuw::compiled::Attention& actual) { + EXPECT_EQ(expected.query_size, actual.query_size); + EXPECT_EQ(expected.context_size, actual.context_size); + EXPECT_EQ(expected.params.size(), actual.params.size()); + for (std::size_t i = 0; i < expected.params.size(); ++i) { + EXPECT_EQ(expected.params[i].idx, actual.params[i].idx); + EXPECT_EQ(expected.params[i].dim, actual.params[i].dim); + } + EXPECT_EQ(expected.mask_idx, actual.mask_idx); + expect_tensors_equal(expected.attend_all, actual.attend_all); +} + +void expect_pyramid_attention_equal(const ov::npuw::compiled::PyramidAttention& expected, + const ov::npuw::compiled::PyramidAttention& actual) { + EXPECT_EQ(expected.query_size, actual.query_size); + EXPECT_EQ(expected.full_context_size, actual.full_context_size); + EXPECT_EQ(expected._context_lengths, actual._context_lengths); + ASSERT_EQ(expected._attention_infos.size(), actual._attention_infos.size()); + for (std::size_t i = 0; i < expected._attention_infos.size(); ++i) { + const auto& lhs = expected._attention_infos[i]; + const auto& rhs = actual._attention_infos[i]; + EXPECT_EQ(lhs.mask_idx, rhs.mask_idx); + EXPECT_EQ(lhs.query_size, rhs.query_size); + EXPECT_EQ(lhs.context_length, rhs.context_length); + EXPECT_EQ(lhs.params.size(), rhs.params.size()); + for (std::size_t j = 0; j < lhs.params.size(); ++j) { + EXPECT_EQ(lhs.params[j].idx, rhs.params[j].idx); + EXPECT_EQ(lhs.params[j].dim, rhs.params[j].dim); + } + } +} + +void expect_host_flash_attention_equal(const ov::npuw::compiled::HostFlashAttention& expected, + const ov::npuw::compiled::HostFlashAttention& actual) { + const auto& lhs = expected._sdpa_attention_info; + const auto& rhs = actual._sdpa_attention_info; + EXPECT_EQ(lhs._query_size, rhs._query_size); + EXPECT_EQ(lhs._context_size, rhs._context_size); + EXPECT_EQ(lhs._k_seq_dim, rhs._k_seq_dim); + EXPECT_EQ(lhs._v_seq_dim, rhs._v_seq_dim); + EXPECT_EQ(lhs._sdpa_indices.query, rhs._sdpa_indices.query); + EXPECT_EQ(lhs._sdpa_indices.past_key, rhs._sdpa_indices.past_key); + EXPECT_EQ(lhs._sdpa_indices.past_value, rhs._sdpa_indices.past_value); + EXPECT_EQ(lhs._sdpa_indices.present_key, rhs._sdpa_indices.present_key); + EXPECT_EQ(lhs._sdpa_indices.present_value, rhs._sdpa_indices.present_value); + EXPECT_EQ(lhs._sdpa_indices.attention_mask, rhs._sdpa_indices.attention_mask); + EXPECT_EQ(lhs._tile_input_indices.q, rhs._tile_input_indices.q); + EXPECT_EQ(lhs._tile_input_indices.k, rhs._tile_input_indices.k); + EXPECT_EQ(lhs._tile_input_indices.v, rhs._tile_input_indices.v); + EXPECT_EQ(lhs._tile_input_indices.mask, rhs._tile_input_indices.mask); + EXPECT_EQ(lhs._tile_input_indices.acc, rhs._tile_input_indices.acc); + EXPECT_EQ(lhs._tile_input_indices.max, rhs._tile_input_indices.max); + EXPECT_EQ(lhs._tile_input_indices.d, rhs._tile_input_indices.d); + EXPECT_EQ(lhs._tile_output_indices.acc, rhs._tile_output_indices.acc); + EXPECT_EQ(lhs._tile_output_indices.max, rhs._tile_output_indices.max); + EXPECT_EQ(lhs._tile_output_indices.d, rhs._tile_output_indices.d); + EXPECT_EQ(expected._tile_size, actual._tile_size); + EXPECT_EQ(expected._can_use_tensor_view, actual._can_use_tensor_view); +} + +void expect_moe_experts_equal(const ov::npuw::compiled::MoEExperts& expected, + const ov::npuw::compiled::MoEExperts& actual) { + EXPECT_EQ(expected.num_experts, actual.num_experts); + EXPECT_EQ(expected.expert_hidden_dim, actual.expert_hidden_dim); + EXPECT_EQ(expected.num_active_experts, actual.num_active_experts); + EXPECT_EQ(expected.input_token_count, actual.input_token_count); + EXPECT_EQ(expected._router_scores.original, actual._router_scores.original); + EXPECT_EQ(expected._router_scores.compiled, actual._router_scores.compiled); + EXPECT_EQ(expected._expert_input.original, actual._expert_input.original); + EXPECT_EQ(expected._expert_input.compiled, actual._expert_input.compiled); + EXPECT_EQ(expected._param_mapping, actual._param_mapping); +} + +void expect_moe_downstream_equal(const ov::npuw::compiled::MoEDownstream& expected, + const ov::npuw::compiled::MoEDownstream& actual) { + EXPECT_EQ(expected.total_experts_num, actual.total_experts_num); + EXPECT_EQ(expected.active_experts_num, actual.active_experts_num); + EXPECT_EQ(expected.expert_output_param_idx, actual.expert_output_param_idx); +} + +void expect_lazy_tensor_transform_types_equal(const ov::npuw::weights::LazyTensor& expected, + const ov::npuw::weights::LazyTensor& actual) { + const auto expected_transforms = expected.get_transformations(); + const auto actual_transforms = actual.get_transformations(); + ASSERT_EQ(expected_transforms.size(), actual_transforms.size()); + for (std::size_t i = 0; i < expected_transforms.size(); ++i) { + EXPECT_EQ(expected_transforms[i].index(), actual_transforms[i].index()); + } +} + +} // namespace + // FIXME: parametrize all the tests below TEST(SerializationTest, BasicTypes_string) { @@ -210,6 +353,34 @@ TEST(SerializationTest, BasicTypes_vector) { EXPECT_EQ(var, res); } +TEST(SerializationTest, BasicTypes_array) { + using namespace ov::npuw::s11n; + + std::array var{1, 2, 3, 4}; + std::array res{}; + + std::stringstream ss; + + write(ss, var); + read(ss, res); + + EXPECT_EQ(var, res); +} + +TEST(SerializationTest, BasicTypes_vector_bool) { + using namespace ov::npuw::s11n; + + std::vector var{true, false, true, true, false}; + std::vector res; + + std::stringstream ss; + + write(ss, var); + read(ss, res); + + EXPECT_EQ(var, res); +} + TEST(SerializationTest, BasicTypes_map) { using namespace ov::npuw::s11n; @@ -258,6 +429,177 @@ TEST(SerializationTest, BasicTypes_optional) { EXPECT_EQ(var2, res2); } +TEST(SerializationTest, OVTypes_AnyMap) { + using namespace ov::npuw::s11n; + + ov::AnyMap var = {{"NPU_USE_NPUW", std::string("YES")}, + {"NPUW_FUNCALL_FOR_ALL", true}, + {"NPUW_ACC_THRESH", 0.125f}, + {"NPUW_LLM_BATCH_DIM", 7}}; + ov::AnyMap res; + + std::stringstream ss; + + write(ss, var); + read(ss, res); + + ASSERT_EQ(var.size(), res.size()); + EXPECT_EQ(res.at("NPU_USE_NPUW").as(), "YES"); + EXPECT_EQ(res.at("NPUW_FUNCALL_FOR_ALL").as(), true); + EXPECT_FLOAT_EQ(res.at("NPUW_ACC_THRESH").as(), 0.125f); + EXPECT_EQ(res.at("NPUW_LLM_BATCH_DIM").as(), 7); +} + +TEST(SerializationTest, OVTypes_ElementType) { + using namespace ov::npuw::s11n; + + ov::element::Type var = ov::element::f16; + ov::element::Type res; + + std::stringstream ss; + + write(ss, var); + read(ss, res); + + EXPECT_EQ(var, res); +} + +TEST(SerializationTest, OVTypes_CacheMode) { + using namespace ov::npuw::s11n; + + ov::CacheMode var = ov::CacheMode::OPTIMIZE_SPEED; + ov::CacheMode res = ov::CacheMode::OPTIMIZE_SIZE; + + std::stringstream ss; + + write(ss, var); + read(ss, res); + + EXPECT_EQ(var, res); +} + +TEST(SerializationTest, OVTypes_PerformanceMode) { + using namespace ov::npuw::s11n; + + ov::hint::PerformanceMode var = ov::hint::PerformanceMode::LATENCY; + ov::hint::PerformanceMode res = ov::hint::PerformanceMode::THROUGHPUT; + + std::stringstream ss; + + write(ss, var); + read(ss, res); + + EXPECT_EQ(var, res); +} + +TEST(SerializationTest, OVTypes_Attention) { + using namespace ov::npuw::s11n; + + ov::npuw::compiled::Attention var; + var.query_size = 3; + var.context_size = 5; + var.params = {{1, 2}, {3, 4}}; + var.mask_idx = 7; + var.attend_all = ov::Tensor(ov::element::f32, ov::Shape{1, 1, 3, 5}); + std::vector mask_data(var.attend_all.get_size()); + std::iota(mask_data.begin(), mask_data.end(), -2.0f); + std::memcpy(var.attend_all.data(), mask_data.data(), var.attend_all.get_byte_size()); + + ov::npuw::compiled::Attention res; + + std::stringstream ss; + + write(ss, var); + read(ss, res); + + expect_attention_equal(var, res); +} + +TEST(SerializationTest, OVTypes_PyramidAttention) { + using namespace ov::npuw::s11n; + + ov::npuw::compiled::PyramidAttention var; + var.query_size = 16; + var.full_context_size = 128; + var._context_lengths = {16, 32, 64, 128}; + var._attention_infos = {{{{0, 2}, {1, 3}}, 4, 16, 32}, {{{2, 1}}, 5, 16, 64}}; + + ov::npuw::compiled::PyramidAttention res; + + std::stringstream ss; + + write(ss, var); + read(ss, res); + + expect_pyramid_attention_equal(var, res); +} + +TEST(SerializationTest, OVTypes_HostFlashAttention) { + using namespace ov::npuw::s11n; + + ov::npuw::compiled::HostFlashAttention var; + var._sdpa_attention_info._query_size = 8; + var._sdpa_attention_info._context_size = 32; + var._sdpa_attention_info._k_seq_dim = 1; + var._sdpa_attention_info._v_seq_dim = 2; + var._sdpa_attention_info._sdpa_indices = {3, 4, 5, 6, 7, 8}; + var._sdpa_attention_info._tile_input_indices = {9, 10, 11, 12, 13, 14, 15}; + var._sdpa_attention_info._tile_output_indices = {16, 17, 18}; + var._tile_size = 64; + var._can_use_tensor_view = true; + + ov::npuw::compiled::HostFlashAttention res; + + std::stringstream ss; + + write(ss, var); + read(ss, res); + + expect_host_flash_attention_equal(var, res); +} + +TEST(SerializationTest, OVTypes_MoEExperts) { + using namespace ov::npuw::s11n; + + ov::npuw::compiled::MoEExperts var; + var.num_experts = 16; + var.expert_hidden_dim = 128; + var.num_active_experts = 2; + var.input_token_count = 64; + var._router_scores.original = 5; + var._router_scores.compiled = 7; + var._expert_input.original = 3; + var._expert_input.compiled = 2; + var._param_mapping = {{0, {1, 2}}, {4, {6, 7, 8}}}; + + ov::npuw::compiled::MoEExperts res; + + std::stringstream ss; + + write(ss, var); + read(ss, res); + + expect_moe_experts_equal(var, res); +} + +TEST(SerializationTest, OVTypes_MoEDownstream) { + using namespace ov::npuw::s11n; + + ov::npuw::compiled::MoEDownstream var; + var.total_experts_num = 16; + var.active_experts_num = 2; + var.expert_output_param_idx = 9; + + ov::npuw::compiled::MoEDownstream res; + + std::stringstream ss; + + write(ss, var); + read(ss, res); + + expect_moe_downstream_equal(var, res); +} + // "with_weights" is an option for read/write_weightless() when Constant in our model is not present in the original // it reads/writes the whole ov::Tensor TEST(SerializationTest, OVTypes_Tensor_with_weights) { @@ -289,4 +631,310 @@ TEST(SerializationTest, OVTypes_Tensor_with_weights) { EXPECT_EQ(data, data_res); } +TEST(SerializationTest, OVTypes_Tensor_empty) { + using namespace ov::npuw::s11n; + + ov::Tensor var; + ov::Tensor res(ov::element::u8, ov::Shape{1}); + + std::stringstream ss; + + write(ss, var); + read(ss, res); + + EXPECT_FALSE(res); +} + +TEST(SerializationTest, OVTypes_Tensor_non_contiguous) { + using namespace ov::npuw::s11n; + + std::vector storage{1.f, 2.f, 100.f, 3.f, 4.f, 200.f}; + ov::Strides strides{3 * sizeof(float), sizeof(float)}; + ov::Tensor var(ov::element::f32, ov::Shape{2, 2}, storage.data(), strides); + ASSERT_FALSE(var.is_continuous()); + + ov::Tensor res; + std::stringstream ss; + + write(ss, var); + read(ss, res); + + ASSERT_TRUE(res.is_continuous()); + std::vector expected_values{1.f, 2.f, 3.f, 4.f}; + ov::Tensor expected(ov::element::f32, ov::Shape{2, 2}, expected_values.data()); + expect_tensors_equal(expected, res); +} + +TEST(SerializationTest, OVTypes_Tensor_allocator) { + using namespace ov::npuw::s11n; + + ov::Tensor var(ov::element::i32, ov::Shape{2, 2}); + std::vector values{1, 2, 3, 4}; + std::memcpy(var.data(), values.data(), var.get_byte_size()); + + std::stringstream ss; + auto output_stream = Stream::writer(ss); + transfer_tensor(output_stream, var); + + bool allocator_called = false; + ov::Tensor res; + auto input_stream = Stream::reader(ss); + transfer_tensor(input_stream, res, [&](const ov::element::Type& type, const ov::Shape& shape) { + allocator_called = true; + return ov::Tensor(type, shape); + }); + + EXPECT_TRUE(allocator_called); + expect_tensors_equal(var, res); +} + +TEST(SerializationTest, OVTypes_Tensor_weightless_bf16_to_f16) { + using namespace ov::npuw::s11n; + + std::vector expected_values = {ov::float16(1.0f), ov::float16(-2.5f), ov::float16(3.25f)}; + ov::Tensor var(ov::element::f16, ov::Shape{3}, expected_values.data()); + + std::stringstream ss; + WeightsContext export_ctx(true, {{var.data(), 0}}); + write_weightless(ss, {var}, export_ctx); + + std::filesystem::path file_path = ov::test::utils::generateTestFilePrefix() + "_npuw_bf16_weights.bin"; + { + std::ofstream os(file_path, std::ios::binary); + std::vector bf16_values = {ov::bfloat16(1.0f), ov::bfloat16(-2.5f), ov::bfloat16(3.25f)}; + os.write(reinterpret_cast(bf16_values.data()), + static_cast(bf16_values.size() * sizeof(ov::bfloat16))); + } + + std::vector res; + { + auto mapped = ov::load_mmap_object(file_path); + ASSERT_NE(mapped, nullptr); + auto weights = std::make_shared(reinterpret_cast(mapped->data()), mapped->size(), mapped); + + WeightsContext import_ctx(weights, file_path.string(), {}, {{0, var.get_byte_size()}}); + read_weightless(ss, res, import_ctx); + } // mapped + weights released here, file handle closed on Windows + + ASSERT_EQ(res.size(), 1); + expect_tensors_equal(var, res.front()); + std::filesystem::remove(file_path); +} + +TEST(SerializationTest, OVTypes_OutputPort_roundtrips_into_parameter_pointer) { + using namespace ov::npuw::s11n; + + auto param = std::make_shared(ov::element::f32, ov::PartialShape{1, 3}); + param->set_friendly_name("input"); + param->output(0).get_tensor().set_names({"input", "alias"}); + + std::shared_ptr res; + std::stringstream ss; + ov::Output output = param->output(0); + + write(ss, output); + read(ss, res); + + ASSERT_NE(res, nullptr); + EXPECT_EQ(res->get_element_type(), param->get_element_type()); + EXPECT_EQ(res->get_partial_shape(), param->get_partial_shape()); + EXPECT_EQ(res->output(0).get_tensor().get_names(), param->output(0).get_tensor().get_names()); +} + +TEST(SerializationTest, OVTypes_OutputPort_roundtrips_into_node_pointer) { + using namespace ov::npuw::s11n; + + auto param = std::make_shared(ov::element::i32, ov::PartialShape{2, 4}); + param->set_friendly_name("node_input"); + param->output(0).get_tensor().set_names({"node_input"}); + + std::shared_ptr res; + std::stringstream ss; + ov::Output output = param->output(0); + + write(ss, output); + read(ss, res); + + ASSERT_NE(res, nullptr); + EXPECT_EQ(res->get_friendly_name(), "node_input"); + EXPECT_EQ(res->output(0).get_tensor().get_names(), param->output(0).get_tensor().get_names()); +} + +TEST(SerializationTest, OVTypes_OutputPort_throws_on_read) { + using namespace ov::npuw::s11n; + + auto param = std::make_shared(ov::element::f32, ov::PartialShape{1}); + std::stringstream ss; + ov::Output output = param->output(0); + write(ss, output); + + EXPECT_THROW(read(ss, output), ov::Exception); +} + +TEST(SerializationTest, OVTypes_ParameterPointer_throws_on_write) { + using namespace ov::npuw::s11n; + + auto param = std::make_shared(ov::element::f32, ov::PartialShape{1}); + std::stringstream ss; + + EXPECT_THROW(write(ss, param), ov::Exception); +} + +TEST(SerializationTest, OVTypes_NodePointer_throws_on_write) { + using namespace ov::npuw::s11n; + + std::shared_ptr node = std::make_shared(ov::element::f32, ov::PartialShape{1}); + std::stringstream ss; + + EXPECT_THROW(write(ss, node), ov::Exception); +} + +TEST(SerializationTest, OVTypes_LazyTensor_uninitialized) { + using namespace ov::npuw::s11n; + + ov::npuw::weights::LazyTensor var; + ov::npuw::weights::LazyTensor res; + + std::stringstream ss; + + write(ss, var); + read(ss, res); + + EXPECT_FALSE(var); + EXPECT_FALSE(res); + EXPECT_EQ(var, res); +} + +TEST(SerializationTest, OVTypes_LazyTensor_const_roundtrip) { + using namespace ov::npuw::s11n; + + auto constant = make_weightless_constant(ov::element::f32, ov::Shape{2}, {1.0f, 2.0f}, 0); + ov::npuw::weights::LazyTensor var(constant); + ov::npuw::weights::LazyTensor res; + + std::stringstream ss; + write(ss, var); + read(ss, res); + res.read_weight(WeightsContext(nullptr, "", make_consts_cache({constant}), {})); + + expect_lazy_tensor_transform_types_equal(var, res); + EXPECT_EQ(var.eval_meta().shape, res.eval_meta().shape); + EXPECT_EQ(var.eval_meta().type, res.eval_meta().type); + expect_tensors_equal(var.eval(), res.eval()); +} + +TEST(SerializationTest, OVTypes_LazyTensor_const_with_embedded_weight_roundtrip) { + using namespace ov::npuw::s11n; + + auto constant = + std::make_shared(ov::element::f32, ov::Shape{2}, std::vector{1.0f, 2.0f}); + ov::npuw::weights::LazyTensor var(constant); + ov::npuw::weights::LazyTensor res; + + std::stringstream ss; + write(ss, var); + read(ss, res); + + expect_lazy_tensor_transform_types_equal(var, res); + EXPECT_EQ(var.eval_meta().shape, res.eval_meta().shape); + EXPECT_EQ(var.eval_meta().type, res.eval_meta().type); + expect_tensors_equal(var.eval(), res.eval()); +} + +TEST(SerializationTest, OVTypes_LazyTensor_concat_permute_convert_roundtrip) { + using namespace ov::npuw::s11n; + + auto first = make_weightless_constant(ov::element::f32, ov::Shape{1, 2}, {1.0f, 2.0f}, 0); + auto second = + make_weightless_constant(ov::element::f32, ov::Shape{1, 2}, {3.0f, 4.0f}, first->get_byte_size()); + ov::npuw::weights::LazyTensor concat( + std::vector{ov::npuw::weights::LazyTensor(first), + ov::npuw::weights::LazyTensor(second)}, + 0); + auto var = concat.permute({1, 0}).convert(ov::element::f16); + ov::npuw::weights::LazyTensor res; + + std::stringstream ss; + write(ss, var); + read(ss, res); + res.read_weight(WeightsContext(nullptr, "", make_consts_cache({first, second}), {})); + + expect_lazy_tensor_transform_types_equal(var, res); + EXPECT_EQ(var.get_hash(), res.get_hash()); + EXPECT_EQ(var.eval_meta().shape, res.eval_meta().shape); + EXPECT_EQ(var.eval_meta().type, res.eval_meta().type); +} + +TEST(SerializationTest, OVTypes_LazyTensor_unpack_roundtrip) { + using namespace ov::npuw::s11n; + + auto w = make_weightless_constant(ov::element::u8, ov::Shape{8}, {1, 2, 3, 4, 5, 6, 7, 8}, 0); + auto s = make_weightless_constant(ov::element::f16, + ov::Shape{8}, + {1.f, 1.f, 1.f, 1.f, 1.f, 1.f, 1.f, 1.f}, + w->get_byte_size()); + ov::npuw::weights::LazyTensor var(ov::npuw::weights::LazyTensor(w), + ov::npuw::weights::LazyTensor(), + ov::npuw::weights::LazyTensor(s), + ov::element::f16, + ov::Shape{8}); + ov::npuw::weights::LazyTensor res; + + std::stringstream ss; + write(ss, var); + read(ss, res); + res.read_weight(WeightsContext(nullptr, "", make_consts_cache({w, s}), {})); + + expect_lazy_tensor_transform_types_equal(var, res); + EXPECT_EQ(var.eval_meta().shape, res.eval_meta().shape); + EXPECT_EQ(var.eval_meta().type, res.eval_meta().type); +} + +TEST(SerializationTest, OVTypes_LazyTensor_gather_roundtrip) { + using namespace ov::npuw::s11n; + + auto w = make_weightless_constant(ov::element::u8, + ov::Shape{16}, + {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}, + 0); + std::vector indices{0, 1, 2, 3}; + ov::Tensor lut(ov::element::f8e4m3, ov::Shape{4}, indices.data()); + ov::npuw::weights::LazyTensor var(ov::npuw::weights::LazyTensor(w), lut, ov::element::f16, ov::Shape{8}); + ov::npuw::weights::LazyTensor res; + + std::stringstream ss; + write(ss, var); + read(ss, res); + res.read_weight(WeightsContext(nullptr, "", make_consts_cache({w}), {})); + + expect_lazy_tensor_transform_types_equal(var, res); + EXPECT_EQ(var.eval_meta().shape, res.eval_meta().shape); + EXPECT_EQ(var.eval_meta().type, res.eval_meta().type); +} + +TEST(SerializationTest, OVTypes_WeightsBank_cpu_roundtrip) { + using namespace ov::npuw::s11n; + + auto first = make_weightless_constant(ov::element::f32, ov::Shape{2}, {1.0f, 2.0f}, 0); + auto second = make_weightless_constant(ov::element::f32, ov::Shape{2}, {3.0f, 4.0f}, first->get_byte_size()); + + ov::npuw::weights::Bank var(nullptr, "CPU", "test-bank"); + const auto uid0 = var.registerLT(ov::npuw::weights::LazyTensor(first), "CPU"); + const auto uid0_dup = var.registerLT(ov::npuw::weights::LazyTensor(first), "CPU"); + const auto uid1 = var.registerLT(ov::npuw::weights::LazyTensor(second), "CPU"); + EXPECT_EQ(uid0, uid0_dup); + EXPECT_NE(uid0, uid1); + + var.evaluate_and_allocate(); + + ov::npuw::weights::Bank res(nullptr, "CPU", "restored-bank"); + std::stringstream ss; + + write(ss, var); + read(ss, res); + + expect_tensors_equal(var.get(uid0, "CPU"), res.get(uid0, "CPU")); + expect_tensors_equal(var.get(uid1, "CPU"), res.get(uid1, "CPU")); +} + // TODO: add tests on CompiledModel and LLMCompiledModel once tests have access to any model to test on diff --git a/src/plugins/intel_npu/tests/unit/npuw/split_kvcache_into_blocks_test.cpp b/src/plugins/intel_npu/tests/unit/npuw/split_kvcache_into_blocks_test.cpp new file mode 100644 index 000000000000..6c31aed9cf8d --- /dev/null +++ b/src/plugins/intel_npu/tests/unit/npuw/split_kvcache_into_blocks_test.cpp @@ -0,0 +1,413 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "npuw_transformations/split_kvcache_into_blocks.hpp" + +#include + +#include "openvino/op/concat.hpp" +#include "openvino/op/convert.hpp" +#include "openvino/op/parameter.hpp" +#include "openvino/op/result.hpp" +#include "openvino/pass/manager.hpp" + +using namespace ov; +using namespace ov::npuw::pass; + +class SplitKVCacheIntoBlocksTest : public ::testing::Test { +protected: + void SetUp() override { + // Common test setup + } + + // Helper: Create a simple model with KV cache parameter -> Concat + std::shared_ptr create_kv_cache_model(const std::string& param_name, + const ov::Shape& shape, + int64_t concat_axis) { + // Create KV cache parameter + auto kv_param = std::make_shared(ov::element::f16, shape); + kv_param->set_friendly_name(param_name); + + // Create new token parameter (to concatenate with) + ov::Shape new_token_shape = shape; + new_token_shape[concat_axis] = 1; // Single token + auto new_token = std::make_shared(ov::element::f16, new_token_shape); + new_token->set_friendly_name("new_token"); + + // Create Concat + auto concat = std::make_shared(ov::OutputVector{kv_param, new_token}, concat_axis); + + // Create Result + auto result = std::make_shared(concat); + + return std::make_shared(ov::ResultVector{result}, ov::ParameterVector{kv_param, new_token}); + } +}; + +TEST_F(SplitKVCacheIntoBlocksTest, TransformKeyParameter) { + // Test: Transform past_key parameter with axis=2 + // Original: past_key [1, 32, 64, 128] + new_token [1, 32, 1, 128] + // After: 4 blocks of 16 tokens each + new_token + const uint32_t block_size = 16; + const ov::Shape orig_shape{1, 32, 64, 128}; // [B, H, S=64, D] + const uint32_t expected_blocks = 64 / block_size; // 4 blocks + + auto model = create_kv_cache_model("past_key_values.0.key", orig_shape, 2); + + // Apply transformation (max_blocks removed, auto-calculated from shape) + ov::pass::Manager manager; + manager.register_pass(block_size, true); + manager.run_passes(model); + + // Verify: Should have expected_blocks + 1 parameters (blocks + new_token) + EXPECT_EQ(model->get_parameters().size(), expected_blocks + 1); + + // Verify: Each block parameter has correct shape [1, 32, 16, 128] + uint32_t block_count = 0; + for (const auto& param : model->get_parameters()) { + if (param->get_friendly_name().find("_block_") != std::string::npos) { + block_count++; + auto block_shape = param->get_shape(); + EXPECT_EQ(block_shape[0], 1); // batch + EXPECT_EQ(block_shape[1], 32); // num_heads + EXPECT_EQ(block_shape[2], block_size); // block_size + EXPECT_EQ(block_shape[3], 128); // head_dim + } + } + EXPECT_EQ(block_count, expected_blocks); + + // Verify: Concat has blocks + new_token input (CRITICAL: present_key preserved!) + bool found_concat = false; + for (const auto& op : model->get_ops()) { + if (ov::is_type(op)) { + found_concat = true; + auto concat = ov::as_type_ptr(op); + EXPECT_EQ(concat->get_axis(), 2); + EXPECT_EQ(concat->get_input_size(), expected_blocks + 1); // blocks + present_key + } + } + EXPECT_TRUE(found_concat); +} + +TEST_F(SplitKVCacheIntoBlocksTest, TransformValueParameterTransposed) { + // Test: Transform past_value parameter with v_transposed=true (axis=3) + const uint32_t block_size = 16; + const ov::Shape orig_shape{1, 32, 128, 64}; // [B, H, D, S] - transposed + + auto model = create_kv_cache_model("past_key_values.0.value", orig_shape, 3); + + // Apply transformation with v_transposed=true + ov::pass::Manager manager; + manager.register_pass(block_size, true); + manager.run_passes(model); + + const uint32_t expected_blocks = 64 / block_size; // 4 blocks + + // Verify: Block parameters have correct shape [1, 32, 128, 16] + uint32_t block_count = 0; + for (const auto& param : model->get_parameters()) { + if (param->get_friendly_name().find("_block_") != std::string::npos) { + block_count++; + auto block_shape = param->get_shape(); + EXPECT_EQ(block_shape[0], 1); // batch + EXPECT_EQ(block_shape[1], 32); // num_heads + EXPECT_EQ(block_shape[2], 128); // head_dim + EXPECT_EQ(block_shape[3], block_size); // block_size + } + } + EXPECT_EQ(block_count, expected_blocks); + + // Verify: Concat axis=3 and has blocks + present_value + for (const auto& op : model->get_ops()) { + if (ov::is_type(op)) { + auto concat = ov::as_type_ptr(op); + EXPECT_EQ(concat->get_axis(), 3); + EXPECT_EQ(concat->get_input_size(), expected_blocks + 1); + } + } +} + +TEST_F(SplitKVCacheIntoBlocksTest, TransformValueParameterNotTransposed) { + // Test: Transform past_value parameter with v_transposed=false (axis=2) + const uint32_t block_size = 16; + const ov::Shape orig_shape{1, 32, 64, 128}; // [B, H, S, D] - same as K + + auto model = create_kv_cache_model("past_key_values.0.value", orig_shape, 2); + + // Apply transformation with v_transposed=false + ov::pass::Manager manager; + manager.register_pass(block_size, false); + manager.run_passes(model); + + const uint32_t expected_blocks = 64 / block_size; // 4 blocks + + // Verify: Block parameters have correct shape [1, 32, 16, 128] + uint32_t block_count = 0; + for (const auto& param : model->get_parameters()) { + if (param->get_friendly_name().find("_block_") != std::string::npos) { + block_count++; + auto block_shape = param->get_shape(); + EXPECT_EQ(block_shape[0], 1); // batch + EXPECT_EQ(block_shape[1], 32); // num_heads + EXPECT_EQ(block_shape[2], block_size); // block_size (at axis=2) + EXPECT_EQ(block_shape[3], 128); // head_dim + } + } + EXPECT_EQ(block_count, expected_blocks); + + // Verify: Concat axis=2 (same as K) and has blocks + present_value + for (const auto& op : model->get_ops()) { + if (ov::is_type(op)) { + auto concat = ov::as_type_ptr(op); + EXPECT_EQ(concat->get_axis(), 2); + EXPECT_EQ(concat->get_input_size(), expected_blocks + 1); + } + } +} + +TEST_F(SplitKVCacheIntoBlocksTest, AlternativeNaming) { + // Test: Transform past_key_values.0.key naming convention + const uint32_t block_size = 16; + const ov::Shape orig_shape{1, 32, 64, 128}; + + auto model = create_kv_cache_model("past_key_values.0.key", orig_shape, 2); + + const uint32_t expected_blocks = 64 / block_size; // 4 blocks + + // Apply transformation + ov::pass::Manager manager; + manager.register_pass(block_size, true); + manager.run_passes(model); + + // Verify: Should have transformed + EXPECT_EQ(model->get_parameters().size(), expected_blocks + 1); + + bool found_blocks = false; + for (const auto& param : model->get_parameters()) { + if (param->get_friendly_name().find("_block_") != std::string::npos) { + found_blocks = true; + } + } + EXPECT_TRUE(found_blocks); +} + +TEST_F(SplitKVCacheIntoBlocksTest, SkipNonKVCacheParameter) { + // Test: Should NOT transform non-KV cache parameters + const uint32_t block_size = 16; + const ov::Shape orig_shape{1, 32, 64, 128}; + + auto model = create_kv_cache_model("hidden_states", orig_shape, 2); + + size_t orig_param_count = model->get_parameters().size(); + + // Apply transformation + ov::pass::Manager manager; + manager.register_pass(block_size, true); + manager.run_passes(model); + + // Verify: Parameter count unchanged (not transformed) + EXPECT_EQ(model->get_parameters().size(), orig_param_count); + + // Verify: No block parameters created + for (const auto& param : model->get_parameters()) { + EXPECT_EQ(param->get_friendly_name().find("_block_"), std::string::npos); + } +} + +TEST_F(SplitKVCacheIntoBlocksTest, InvalidRank) { + // Test: Should skip parameters with invalid rank (not 4D) + const uint32_t block_size = 16; + const ov::Shape invalid_shape{1, 32, 64}; // 3D instead of 4D + + auto kv_param = std::make_shared(ov::element::f16, invalid_shape); + kv_param->set_friendly_name("past_key.0"); + + ov::Shape new_token_shape{1, 32, 1}; + auto new_token = std::make_shared(ov::element::f16, new_token_shape); + + auto concat = std::make_shared(ov::OutputVector{kv_param, new_token}, 2); + auto result = std::make_shared(concat); + auto model = std::make_shared(ov::ResultVector{result}, ov::ParameterVector{kv_param, new_token}); + + size_t orig_param_count = model->get_parameters().size(); + + // Apply transformation + ov::pass::Manager manager; + manager.register_pass(block_size, true); + manager.run_passes(model); + + // Verify: Not transformed due to invalid rank + EXPECT_EQ(model->get_parameters().size(), orig_param_count); +} + +TEST_F(SplitKVCacheIntoBlocksTest, MultipleKVParameters) { + // Test: Transform multiple KV cache parameters in one model + const uint32_t block_size = 16; + const uint32_t expected_blocks = 64 / block_size; // 4 blocks per parameter + + // Create model with both past_key and past_value + auto key_param = std::make_shared(ov::element::f16, ov::Shape{1, 32, 64, 128}); + key_param->set_friendly_name("past_key_values.0.key"); + + auto value_param = std::make_shared(ov::element::f16, ov::Shape{1, 32, 128, 64}); + value_param->set_friendly_name("past_key_values.0.value"); + + auto new_k = std::make_shared(ov::element::f16, ov::Shape{1, 32, 1, 128}); + auto new_v = std::make_shared(ov::element::f16, ov::Shape{1, 32, 128, 1}); + + auto concat_k = std::make_shared(ov::OutputVector{key_param, new_k}, 2); + auto concat_v = std::make_shared(ov::OutputVector{value_param, new_v}, 3); + + auto result_k = std::make_shared(concat_k); + auto result_v = std::make_shared(concat_v); + + auto model = std::make_shared(ov::ResultVector{result_k, result_v}, + ov::ParameterVector{key_param, value_param, new_k, new_v}); + + // Apply transformation + ov::pass::Manager manager; + manager.register_pass(block_size, true); + manager.run_passes(model); + + // Verify: Should have (expected_blocks * 2) + 2 parameters (K blocks + V blocks + new_k + new_v) + EXPECT_EQ(model->get_parameters().size(), (expected_blocks * 2) + 2); + + // Count block parameters + size_t key_blocks = 0, value_blocks = 0; + for (const auto& param : model->get_parameters()) { + if (param->get_friendly_name().find(".key") != std::string::npos && + param->get_friendly_name().find("_block_") != std::string::npos) { + key_blocks++; + } + if (param->get_friendly_name().find(".value") != std::string::npos && + param->get_friendly_name().find("_block_") != std::string::npos) { + value_blocks++; + } + } + + EXPECT_EQ(key_blocks, expected_blocks); + EXPECT_EQ(value_blocks, expected_blocks); +} + +TEST_F(SplitKVCacheIntoBlocksTest, TailBlockHandling) { + // Test: Handle non-evenly divisible seq_len (creates tail block) + // seq_len=70, block_size=16 -> 4 full blocks (64 tokens) + 1 tail block (6 tokens) + const uint32_t block_size = 16; + const ov::Shape orig_shape{1, 32, 70, 128}; // [B, H, S=70, D] + const uint32_t expected_full_blocks = 70 / block_size; // 4 + const uint32_t expected_tail_size = 70 % block_size; // 6 + const uint32_t expected_total_blocks = expected_full_blocks + (expected_tail_size > 0 ? 1 : 0); // 5 + + auto model = create_kv_cache_model("past_key_values.0.key", orig_shape, 2); + + // Apply transformation + ov::pass::Manager manager; + manager.register_pass(block_size, true); + manager.run_passes(model); + + // Verify: Should have 5 block parameters + 1 new_token = 6 total + EXPECT_EQ(model->get_parameters().size(), expected_total_blocks + 1); + + // Verify: 4 full blocks + 1 tail block + uint32_t full_block_count = 0; + bool found_tail_block = false; + for (const auto& param : model->get_parameters()) { + if (param->get_friendly_name().find("_block_tail") != std::string::npos) { + found_tail_block = true; + auto tail_shape = param->get_shape(); + EXPECT_EQ(tail_shape[2], expected_tail_size); // tail block has 6 tokens + } else if (param->get_friendly_name().find("_block_") != std::string::npos) { + full_block_count++; + auto block_shape = param->get_shape(); + EXPECT_EQ(block_shape[2], block_size); // full block has 16 tokens + } + } + EXPECT_EQ(full_block_count, expected_full_blocks); + EXPECT_TRUE(found_tail_block); + + // Verify: Concat has all blocks + present_key + for (const auto& op : model->get_ops()) { + if (ov::is_type(op)) { + auto concat = ov::as_type_ptr(op); + EXPECT_EQ(concat->get_input_size(), expected_total_blocks + 1); // 5 blocks + present_key + } + } +} + +TEST_F(SplitKVCacheIntoBlocksTest, WithConvertNode) { + // Test: Transform KV cache with Convert node between Parameter and Concat + // Pattern: Parameter(f16) -> Convert(f32) -> Concat + const uint32_t block_size = 16; + const ov::Shape orig_shape{1, 32, 64, 128}; // [B, H, S=64, D] + + // Create KV cache parameter + auto kv_param = std::make_shared(ov::element::f16, orig_shape); + kv_param->set_friendly_name("past_key_values.0.key"); + + // Create Convert node (f16 -> f32) + auto convert = std::make_shared(kv_param, ov::element::f32); + + // Create new token parameter + ov::Shape new_token_shape = orig_shape; + new_token_shape[2] = 1; // Single token + auto new_token = std::make_shared(ov::element::f32, new_token_shape); + new_token->set_friendly_name("new_token"); + + // Create Concat with Convert output and new_token + auto concat = std::make_shared(ov::OutputVector{convert, new_token}, 2); + + // Create Result + auto result = std::make_shared(concat); + + auto model = std::make_shared(ov::ResultVector{result}, ov::ParameterVector{kv_param, new_token}); + + // Apply transformation + ov::pass::Manager manager; + manager.register_pass(block_size, true); + manager.run_passes(model); + + // Verify: Should have 4 block parameters + 1 new_token + EXPECT_EQ(model->get_parameters().size(), 5); + + // Verify: Each block parameter should be followed by a Convert node + uint32_t block_count = 0; + uint32_t convert_count = 0; + for (const auto& param : model->get_parameters()) { + if (param->get_friendly_name().find("_block_") != std::string::npos) { + block_count++; + // Check that parameter is f16 + EXPECT_EQ(param->get_element_type(), ov::element::f16); + + // Check that it's followed by a Convert node + for (const auto& output : param->outputs()) { + for (const auto& input : output.get_target_inputs()) { + auto node = input.get_node()->shared_from_this(); + if (ov::is_type(node)) { + auto cvt = ov::as_type_ptr(node); + EXPECT_EQ(cvt->get_destination_type(), ov::element::f32); + convert_count++; + } + } + } + } + } + EXPECT_EQ(block_count, 4); + EXPECT_EQ(convert_count, 4); // Each block should have a Convert + + // Verify: Concat receives f32 inputs from Convert nodes + bool found_concat = false; + for (const auto& op : model->get_ops()) { + if (ov::is_type(op)) { + found_concat = true; + auto concat_node = ov::as_type_ptr(op); + EXPECT_EQ(concat_node->get_input_size(), 5); // 4 blocks + new_token + + // Verify all concat inputs are f32 + for (size_t i = 0; i < concat_node->get_input_size(); ++i) { + EXPECT_EQ(concat_node->get_input_element_type(i), ov::element::f32); + } + } + } + EXPECT_TRUE(found_concat); +} diff --git a/src/plugins/intel_npu/tests/unit/npuw/stored_tokens_state_test.cpp b/src/plugins/intel_npu/tests/unit/npuw/stored_tokens_state_test.cpp new file mode 100644 index 000000000000..66cff261d89c --- /dev/null +++ b/src/plugins/intel_npu/tests/unit/npuw/stored_tokens_state_test.cpp @@ -0,0 +1,61 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include + +#include + +// Forward-declare LLMInferRequest so the friend declaration in StoredTokensState resolves. +namespace ov { namespace npuw { class LLMInferRequest; } } + +#include "llm_stored_tokens_state.hpp" + +namespace { +int64_t read_stored_tokens(const ov::npuw::StoredTokensState& state) { + auto tensor = state.get_state(); + return tensor->data()[0]; +} + +TEST(StoredTokensStateTest, InitialValueIsZero) { + ov::npuw::StoredTokensState state; + EXPECT_EQ(read_stored_tokens(state), 0); +} + +TEST(StoredTokensStateTest, StateNameIsCorrect) { + ov::npuw::StoredTokensState state; + EXPECT_EQ(state.get_name(), "npuw_stored_tokens_state"); +} + +TEST(StoredTokensStateTest, ResetState) { + ov::npuw::StoredTokensState state; + state.reset(); + EXPECT_EQ(read_stored_tokens(state), 0); +} + +// --- set_state(...) should not be called externally --- +TEST(StoredTokensStateTest, SetStateThrows) { + ov::npuw::StoredTokensState state; + auto tensor = ov::Tensor(ov::element::i64, ov::Shape{1}); + EXPECT_ANY_THROW(state.set_state(ov::get_tensor_impl(tensor))); +} + +TEST(StoredTokensStateTest, GetStateReturnsTensor) { + ov::npuw::StoredTokensState state; + auto tensor = state.get_state(); + ASSERT_NE(tensor, nullptr); + EXPECT_EQ(tensor->get_element_type(), ov::element::i64); + EXPECT_EQ(tensor->get_shape(), ov::Shape{1}); +} + +// --- get_state() returns a copy, so external mutation does not affect internal state --- +TEST(StoredTokensStateTest, GetStateReturnsCopyNotReference) { + ov::npuw::StoredTokensState state; + EXPECT_EQ(read_stored_tokens(state), 0); + + auto tensor = state.get_state(); + tensor->data()[0] = 42; + + EXPECT_EQ(read_stored_tokens(state), 0); +} +} // namespace diff --git a/src/plugins/intel_npu/tests/unit/npuw/subgraph_behavior_infer_test.cpp b/src/plugins/intel_npu/tests/unit/npuw/subgraph_behavior_infer_test.cpp new file mode 100644 index 000000000000..065cce5e02e5 --- /dev/null +++ b/src/plugins/intel_npu/tests/unit/npuw/subgraph_behavior_infer_test.cpp @@ -0,0 +1,530 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#define private public +#include "compiled_model.hpp" +#undef private +#include "just_sync_infer_request.hpp" +#include "llm_test_helpers.hpp" +#include "model_builder.hpp" +#include "partitioning/patterns/sdpa.hpp" +#include "unfold_sync_infer_request.hpp" +#include "openvino/op/scaled_dot_product_attention.hpp" +#include "openvino/openvino.hpp" +#include "openvino/pass/stateful_to_stateless.hpp" +#include "unit_test_utils/mocks/openvino/runtime/mock_icore.hpp" + +namespace { + +using ov::test::npuw::build_llm_test_model; + +constexpr std::size_t kSeqLen = 4u; +constexpr std::size_t kPastKvLen = 4u; +constexpr std::size_t kKVCacheSize = kSeqLen + kPastKvLen; +struct BehaviorHits { + std::mutex mutex; + std::vector> values; +}; + +class TestPlugin final : public ov::IPlugin { +public: + std::shared_ptr compile_model(const std::shared_ptr&, + const ov::AnyMap&) const override { + OPENVINO_THROW("Unexpected TestPlugin::compile_model call in subgraph behavior test"); + } + std::shared_ptr compile_model(const std::shared_ptr&, + const ov::AnyMap&, + const ov::SoPtr&) const override { + OPENVINO_THROW("Unexpected TestPlugin::compile_model(context) call in subgraph behavior test"); + } + std::shared_ptr import_model(std::istream&, const ov::AnyMap&) const override { + OPENVINO_THROW("Unexpected TestPlugin::import_model(stream) call in subgraph behavior test"); + } + std::shared_ptr import_model(std::istream&, + const ov::SoPtr&, + const ov::AnyMap&) const override { + OPENVINO_THROW("Unexpected TestPlugin::import_model(stream, context) call in subgraph behavior test"); + } + std::shared_ptr import_model(const ov::Tensor&, const ov::AnyMap&) const override { + OPENVINO_THROW("Unexpected TestPlugin::import_model(blob) call in subgraph behavior test"); + } + std::shared_ptr import_model(const ov::Tensor&, + const ov::SoPtr&, + const ov::AnyMap&) const override { + OPENVINO_THROW("Unexpected TestPlugin::import_model(blob, context) call in subgraph behavior test"); + } + ov::SupportedOpsMap query_model(const std::shared_ptr&, const ov::AnyMap&) const override { + OPENVINO_THROW("Unexpected TestPlugin::query_model call in subgraph behavior test"); + } + void set_property(const ov::AnyMap&) override {} + ov::Any get_property(const std::string&, const ov::AnyMap&) const override { + OPENVINO_THROW("Test plugin does not expose properties"); + } + bool is_property_supported(const std::string&, const ov::AnyMap&) const override { + return false; + } + ov::SoPtr create_context(const ov::AnyMap&) const override { + OPENVINO_THROW("Unexpected TestPlugin::create_context call in subgraph behavior test"); + } + ov::SoPtr get_default_context(const ov::AnyMap&) const override { + OPENVINO_THROW("Unexpected TestPlugin::get_default_context call in subgraph behavior test"); + } +}; + +std::shared_ptr build_static_llm_model() { + auto model = build_llm_test_model(); + ov::pass::StatefulToStateless().run_on_model(model); + model = model->clone(); + + std::map new_shapes; + for (const auto& input : model->inputs()) { + const auto& name = input.get_any_name(); + const auto& pshape = input.get_partial_shape(); + + if (name.find("input_ids") != std::string::npos || name.find("token_type_ids") != std::string::npos) { + new_shapes[name] = ov::PartialShape{1, kSeqLen}; + } else if (name.find("attention_mask") != std::string::npos) { + new_shapes[name] = ov::PartialShape{1, kKVCacheSize}; + } else if (name.find("position_ids") != std::string::npos) { + new_shapes[name] = ov::PartialShape{1, kSeqLen}; + } else { + auto static_shape = pshape; + static_shape[0] = 1; + static_shape[2] = kPastKvLen; + new_shapes[name] = static_shape; + } + } + + model->reshape(new_shapes); + model->validate_nodes_and_infer_types(); + return model; +} + +std::size_t count_sdpa_nodes(const std::shared_ptr& model) { + const auto& ordered_ops = model->get_ordered_ops(); + return std::count_if(ordered_ops.begin(), ordered_ops.end(), [](const std::shared_ptr& op) { + return ov::is_type(op); + }); +} + +std::size_t count_runtime_behaviors(const std::shared_ptr& compiled_model) { + return std::count_if(compiled_model->m_compiled_submodels.begin(), + compiled_model->m_compiled_submodels.end(), + [](const auto& desc) { + return desc.pipeline.runtime_behavior.has_value(); + }); +} + +// Count subgraphs where the attention runtime behavior was attached. The test keys off the +// behavior's registered attn identity rather than only handles_function_prologue, since that +// callback flag is not unique to attention behaviors. +std::size_t count_dyn_attn_behaviors(const std::shared_ptr& compiled_model) { + return std::count_if(compiled_model->m_compiled_submodels.begin(), + compiled_model->m_compiled_submodels.end(), + [](const auto& desc) { + if (!desc.pipeline.runtime_behavior.has_value()) { + return false; + } + const auto& spec = *desc.pipeline.runtime_behavior; + return spec.handles_function_prologue && + spec.registration.group == ov::npuw::patterns::attn::SDPA::group_name() && + spec.registration.name == ov::npuw::patterns::attn::SDPA::pattern_name(); + }); +} + +class FakeSubCompiledModel; + +class FakeSubInferRequest final : public ov::ISyncInferRequest { +public: + explicit FakeSubInferRequest(std::shared_ptr compiled_model); + + void infer() override; + ov::SoPtr get_tensor(const ov::Output& port) const override { + return ov::ISyncInferRequest::get_tensor(port); + } + void set_tensor(const ov::Output& port, const ov::SoPtr& tensor) override { + ov::ISyncInferRequest::set_tensor(port, tensor); + } + void check_tensors() const override {} + std::vector> query_state() const override { + return {}; + } + std::vector get_profiling_info() const override { + return {}; + } +}; + +class FakeSubAsyncInferRequest final : public ov::IAsyncInferRequest { +public: + explicit FakeSubAsyncInferRequest(const std::shared_ptr& request) + : ov::IAsyncInferRequest(nullptr, nullptr, nullptr), + m_request(request) {} + + void start_async() override { + try { + m_request->infer(); + if (m_callback) { + m_callback(nullptr); + } + } catch (...) { + if (m_callback) { + m_callback(std::current_exception()); + return; + } + throw; + } + } + + void wait() override {} + + bool wait_for(const std::chrono::milliseconds&) override { + return true; + } + + void cancel() override {} + + void set_callback(std::function callback) override { + m_callback = std::move(callback); + } + + void infer() override { + m_request->infer(); + } + + std::vector get_profiling_info() const override { + return m_request->get_profiling_info(); + } + + ov::SoPtr get_tensor(const ov::Output& port) const override { + return m_request->get_tensor(port); + } + + void set_tensor(const ov::Output& port, const ov::SoPtr& tensor) override { + m_request->set_tensor(port, tensor); + } + + std::vector> get_tensors(const ov::Output& port) const override { + return m_request->get_tensors(port); + } + + void set_tensors(const ov::Output& port, + const std::vector>& tensors) override { + m_request->set_tensors(port, tensors); + } + + std::vector> query_state() const override { + return m_request->query_state(); + } + + const std::shared_ptr& get_compiled_model() const override { + return m_request->get_compiled_model(); + } + + const std::vector>& get_inputs() const override { + return m_request->get_inputs(); + } + + const std::vector>& get_outputs() const override { + return m_request->get_outputs(); + } + +private: + std::shared_ptr m_request; + std::function m_callback; +}; + + class FakeSubCompiledModel final : public ov::ICompiledModel { +public: + FakeSubCompiledModel(const std::shared_ptr& model, const std::shared_ptr& plugin) + : ov::ICompiledModel(model, plugin, nullptr, nullptr), + m_model(model) {} + + void export_model(std::ostream&) const override {} + std::shared_ptr get_runtime_model() const override { + return m_model; + } + void set_property(const ov::AnyMap&) override {} + ov::Any get_property(const std::string& name) const override { + if (name == ov::execution_devices.name()) { + return std::vector{"CPU"}; + } + OPENVINO_THROW("Unsupported property: ", name); + } + std::shared_ptr create_sync_infer_request() const override { + auto self = std::static_pointer_cast(shared_from_this()); + return std::make_shared(std::move(self)); + } + std::shared_ptr create_infer_request() const override { + return std::make_shared(create_sync_infer_request()); + } + +private: + std::shared_ptr m_model; +}; + +FakeSubInferRequest::FakeSubInferRequest(std::shared_ptr compiled_model) + : ov::ISyncInferRequest(std::move(compiled_model)) { + for (const auto& input : get_compiled_model()->inputs()) { + ov::ISyncInferRequest::set_tensor(input, + ov::get_tensor_impl(ov::Tensor(input.get_element_type(), input.get_shape()))); + } + for (const auto& output : get_compiled_model()->outputs()) { + ov::ISyncInferRequest::set_tensor(output, + ov::get_tensor_impl(ov::Tensor(output.get_element_type(), output.get_shape()))); + } +} + +void FakeSubInferRequest::infer() { + for (const auto& output : get_compiled_model()->outputs()) { + auto tensor = ov::ISyncInferRequest::get_tensor(output); + std::memset(tensor->data(), 0, tensor->get_byte_size()); + } +} + +class SubgraphBehaviorInferTest : public ::testing::Test { +protected: + ov::AnyMap base_props() const { + return {{"NPU_USE_NPUW", "YES"}, + {"NPUW_DEVICES", "CPU"}, + {"NPUW_UNFOLD_IREQS", "NO"}, + {"NPUW_ATTN", "DYNAMIC"}, + {"NPUW_FOLD", "YES"}, + {"NPUW_ONLINE_PIPELINE", "REP"}, + {"NPUW_ONLINE_ISOLATE", "ATTN"}, + // The test model has only 2 layers so repeated blocks appear only twice. + // Lower the thresholds so they survive cleanUpUniquesImpl and ens.repeated + // stays non-empty (required for the FOLD pass to run at all). + {"NPUW_ONLINE_KEEP_BLOCKS", "2"}, + {"NPUW_ONLINE_KEEP_BLOCK_SIZE", "1"}}; + } + + ov::AnyMap unfold_props() const { + auto props = base_props(); + props["NPUW_UNFOLD_IREQS"] = "YES"; + return props; + } + std::shared_ptr> make_core(const std::shared_ptr& plugin) const { + auto core = std::make_shared>(); + + ON_CALL(*core, get_supported_property(testing::_, testing::_, testing::_)) + .WillByDefault([](const std::string&, const ov::AnyMap& properties, const bool) { + return properties; + }); + ON_CALL(*core, get_property(testing::_, testing::_, testing::_)) + .WillByDefault([](const std::string&, const std::string& name, const ov::AnyMap&) -> ov::Any { + if (name == ov::available_devices.name()) { + return std::vector{}; + } + if (name == ov::intel_npu::compiler_version.name()) { + return int64_t{0}; + } + if (name == ov::device::architecture.name()) { + return std::string{}; + } + if (name == ov::supported_properties.name() || name == ov::internal::supported_properties.name()) { + return std::vector{}; + } + return {}; + }); + ON_CALL(*core, get_property(testing::_, testing::_)) + .WillByDefault([](const std::string&, const std::string& name) -> ov::Any { + if (name == ov::available_devices.name()) { + return std::vector{}; + } + if (name == ov::supported_properties.name()) { + return std::vector{}; + } + if (name == ov::intel_npu::compiler_version.name()) { + return static_cast(0); + } + if (name == ov::device::architecture.name()) { + return std::string{}; + } + return {}; + }); + ON_CALL(*core, + compile_model(testing::Matcher&>(testing::_), + testing::Matcher(testing::StrEq("CPU")), + testing::Matcher(testing::_))) + .WillByDefault([plugin](const std::shared_ptr& submodel, const std::string&, const ov::AnyMap&) { + return ov::SoPtr{std::make_shared( + std::const_pointer_cast(submodel), plugin)}; + }); + + return core; + } +}; + +TEST_F(SubgraphBehaviorInferTest, SdpaBehaviorCanOverrideStaticLlmSubgraphExecution) { + auto baseline_model = build_static_llm_model(); + ASSERT_GT(count_sdpa_nodes(baseline_model), 0u) << "The synthesized LLM model must contain SDPA nodes"; + auto hits = std::make_shared(); + + auto plugin = std::make_shared(); + auto core = make_core(plugin); + plugin->set_core(core); + + auto baseline_compiled = std::make_shared(baseline_model, plugin, base_props()); + EXPECT_EQ(count_runtime_behaviors(baseline_compiled), 0u); + auto baseline_request = baseline_compiled->create_infer_request(); + ASSERT_NE(baseline_request, nullptr); + baseline_request->infer(); + EXPECT_TRUE(hits->values.empty()); + + auto behavior_model = build_static_llm_model(); + ASSERT_GT(count_sdpa_nodes(behavior_model), 0u); + ov::npuw::v1::subgraphs::PatternRegistry behavior_registry; + auto behavior_compiled = std::make_shared(behavior_model, plugin, base_props(), &behavior_registry); + bool attached_behavior = false; + for (auto& desc : behavior_compiled->m_compiled_submodels) { + if (!desc.compiled_model) { + continue; + } + + ov::npuw::v1::subgraphs::RuntimeBehaviorSpec spec; + spec.registration.group = "test"; + spec.registration.name = "record-hit"; + spec.context.put>(hits); + spec.factory = [](const ov::npuw::v1::subgraphs::Context& ctx) -> ov::npuw::v1::subgraphs::ISubgraphBehavior::Ptr { + const auto recorder = ctx.get>(); + return std::make_unique( + [recorder](ov::npuw::v1::subgraphs::InferContext& infer_ctx) { + infer_ctx.legacy_infer(); + std::lock_guard lock(recorder->mutex); + recorder->values.emplace_back(infer_ctx.subgraph_idx, infer_ctx.real_subgraph_idx); + }); + }; + desc.pipeline.runtime_behavior = std::move(spec); + attached_behavior = true; + } + ASSERT_TRUE(attached_behavior) << "No compiled subgraph was available for runtime behavior injection"; + auto behavior_request = behavior_compiled->create_infer_request(); + ASSERT_NE(behavior_request, nullptr); + behavior_request->infer(); + + ASSERT_FALSE(hits->values.empty()) << "The SDPA stub behavior was not invoked during inference"; +} + +TEST_F(SubgraphBehaviorInferTest, RuntimeBehaviorForcesJustInferRequestWhenUnfoldIsEnabled) { + auto plugin = std::make_shared(); + auto core = make_core(plugin); + plugin->set_core(core); + + auto baseline_model = build_static_llm_model(); + auto baseline_compiled = std::make_shared(baseline_model, plugin, unfold_props()); + auto baseline_request = baseline_compiled->create_sync_infer_request(); + ASSERT_NE(baseline_request, nullptr); + EXPECT_NE(std::dynamic_pointer_cast(baseline_request), nullptr); + + auto behavior_model = build_static_llm_model(); + auto hits = std::make_shared(); + ov::npuw::v1::subgraphs::PatternRegistry behavior_registry; + auto behavior_compiled = std::make_shared(behavior_model, plugin, unfold_props(), &behavior_registry); + + bool attached_behavior = false; + for (auto& desc : behavior_compiled->m_compiled_submodels) { + if (!desc.compiled_model) { + continue; + } + + ov::npuw::v1::subgraphs::RuntimeBehaviorSpec spec; + spec.registration.group = "test"; + spec.registration.name = "record-hit"; + spec.context.put>(hits); + spec.factory = [](const ov::npuw::v1::subgraphs::Context& ctx) -> ov::npuw::v1::subgraphs::ISubgraphBehavior::Ptr { + const auto recorder = ctx.get>(); + return std::make_unique( + [recorder](ov::npuw::v1::subgraphs::InferContext& infer_ctx) { + infer_ctx.legacy_infer(); + std::lock_guard lock(recorder->mutex); + recorder->values.emplace_back(infer_ctx.subgraph_idx, infer_ctx.real_subgraph_idx); + }); + }; + desc.pipeline.runtime_behavior = std::move(spec); + attached_behavior = true; + } + ASSERT_TRUE(attached_behavior); + + auto behavior_request = behavior_compiled->create_sync_infer_request(); + ASSERT_NE(behavior_request, nullptr); + EXPECT_NE(std::dynamic_pointer_cast(behavior_request), nullptr); + EXPECT_EQ(std::dynamic_pointer_cast(behavior_request), nullptr); +} + +// --- Dynamic-attention behavior gating tests --- +// +// These tests verify two properties: +// +// 1. When NPUW_ATTN=DYNAMIC + NPUW_ONLINE_ISOLATE=ATTN are set and the model has SDPA nodes +// with dynamic KV-cache dimensions, the attn runtime behavior IS attached to the attention +// subgraphs. +// +// 2. When either condition is absent (STATIC mode or no isolation), NO DynAttnBehavior is +// attached. This proves the gate is working correctly. +// +// build_dynamic_llm_model() produces a stateful→stateless-converted LLM whose KV-cache +// parameters retain dynamic dimensions — the exact shape that function::Attention::from() +// requires to succeed in DYNAMIC mode. + +TEST_F(SubgraphBehaviorInferTest, DynAttnBehaviorAttachedWhenDynamicAttentionRequested) { + auto model = ov::test::npuw::build_dynamic_attention_llm_model(); + ASSERT_GT(count_sdpa_nodes(model), 0u) << "Dynamic LLM model must contain SDPA nodes"; + + auto plugin = std::make_shared(); + auto core = make_core(plugin); + plugin->set_core(core); + + // base_props() has NPUW_ATTN=DYNAMIC and NPUW_ONLINE_ISOLATE=ATTN + auto compiled = std::make_shared(model, plugin, base_props()); + EXPECT_GT(count_dyn_attn_behaviors(compiled), 0u) + << "DynAttnBehavior must be attached when NPUW_ATTN=DYNAMIC and NPUW_ONLINE_ISOLATE=ATTN"; +} + +TEST_F(SubgraphBehaviorInferTest, DynAttnBehaviorNotAttachedWithStaticAttentionMode) { + auto model = ov::test::npuw::build_dynamic_attention_llm_model(); + + auto plugin = std::make_shared(); + auto core = make_core(plugin); + plugin->set_core(core); + + auto props = base_props(); + props["NPUW_ATTN"] = std::string("STATIC"); + auto compiled = std::make_shared(model, plugin, props); + EXPECT_EQ(count_dyn_attn_behaviors(compiled), 0u) + << "DynAttnBehavior must NOT be attached when NPUW_ATTN=STATIC"; +} + +TEST_F(SubgraphBehaviorInferTest, DynAttnBehaviorNotAttachedWithoutAttnIsolation) { + auto model = ov::test::npuw::build_dynamic_attention_llm_model(); + + auto plugin = std::make_shared(); + auto core = make_core(plugin); + plugin->set_core(core); + + // Remove NPUW_ONLINE_ISOLATE so no "attn" functions are created by the online partitioner. + ov::AnyMap props = {{"NPU_USE_NPUW", "YES"}, + {"NPUW_DEVICES", "CPU"}, + {"NPUW_UNFOLD_IREQS", "NO"}, + {"NPUW_ATTN", std::string("DYNAMIC")}, + {"NPUW_FOLD", "YES"}, + {"NPUW_ONLINE_PIPELINE", "REP"}, + {"NPUW_ONLINE_KEEP_BLOCKS", "2"}, + {"NPUW_ONLINE_KEEP_BLOCK_SIZE", "1"}}; + auto compiled = std::make_shared(model, plugin, props); + EXPECT_EQ(count_dyn_attn_behaviors(compiled), 0u) + << "DynAttnBehavior must NOT be attached when NPUW_ONLINE_ISOLATE=ATTN is not set"; +} + +} // namespace diff --git a/src/plugins/intel_npu/tests/unit/npuw/subgraph_pipeline.cpp b/src/plugins/intel_npu/tests/unit/npuw/subgraph_pipeline.cpp new file mode 100644 index 000000000000..58d9a10b6749 --- /dev/null +++ b/src/plugins/intel_npu/tests/unit/npuw/subgraph_pipeline.cpp @@ -0,0 +1,210 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "v1/subgraph_pipeline.hpp" + +#include + +#include + +#include "attn/attn_subgraph.hpp" +#include "moe/moe_executor.hpp" +#include "partitioning/partitioning.hpp" +#include "partitioning/patterns/moe.hpp" +#include "partitioning/patterns/sdpa.hpp" + +namespace { + +struct TestPayload { + int value = 0; + std::string name; + + TestPayload() = default; + TestPayload(int value, std::string name) : value(value), name(std::move(name)) {} +}; + +} // namespace + +TEST(SubgraphPipelineContextTest, StoresAndRetrievesTypedValues) { + ov::npuw::v1::subgraphs::Context ctx; + + auto& label = ctx.put("moe"); + auto& payload = ctx.emplace(7, "router"); + + EXPECT_EQ(label, "moe"); + EXPECT_EQ(payload.value, 7); + EXPECT_EQ(payload.name, "router"); + EXPECT_TRUE(ctx.contains()); + EXPECT_TRUE(ctx.contains()); + EXPECT_EQ(ctx.size(), 2u); + EXPECT_EQ(ctx.get(), "moe"); + + const auto* found = ctx.get_if(); + ASSERT_NE(found, nullptr); + EXPECT_EQ(found->value, 7); + EXPECT_EQ(found->name, "router"); +} + +TEST(SubgraphPipelineContextTest, MissingTypeReturnsNull) { + ov::npuw::v1::subgraphs::Context ctx; + + EXPECT_FALSE(ctx.contains()); + EXPECT_EQ(ctx.get_if(), nullptr); + EXPECT_TRUE(ctx.empty()); +} + +TEST(SubgraphPipelineFunctionTest, FunctionTagSeedsPipelineRegistrationPattern) { + ov::npuw::Function function; + + function.settag("GPTOSSExpert"); + + EXPECT_EQ(function.gettag(), "GPTOSSExpert"); + ASSERT_EQ(function._pipeline.registration.patterns.size(), 1u); + EXPECT_EQ(function._pipeline.registration.patterns.front(), "GPTOSSExpert"); +} + +TEST(SubgraphPipelineBehaviorTest, FactoryCreatesBehaviorObject) { + auto behavior = ov::npuw::v1::subgraphs::make_direct_behavior(); + + EXPECT_NE(behavior, nullptr); +} + +TEST(SubgraphPipelineBehaviorTest, RealPatternRegistrationBuildsRuntimeBehaviorForTaggedSubgraph) { + ov::npuw::Subgraph subgraph; + subgraph.settag(ov::npuw::patterns::attn::SDPA::isolation_tag()); + ov::npuw::v1::subgraphs::PatternRegistry registry; + + auto scoped_registration = + registry.on() + .at_compile([](ov::npuw::v1::subgraphs::CompiledPipeline&, ov::npuw::v1::subgraphs::Context& ctx) { + ctx.put("marker"); + }) + .at_runtime( + [](const ov::npuw::v1::subgraphs::Context& ctx) -> ov::npuw::v1::subgraphs::ISubgraphBehavior::Ptr { + EXPECT_EQ(ctx.get(), "marker"); + return ov::npuw::v1::subgraphs::make_direct_behavior(); + }) + .scoped(); + + registry.apply(subgraph); + + ASSERT_TRUE(static_cast(subgraph._pipeline.compile_stage)); + ov::npuw::v1::subgraphs::CompiledPipeline compiled_pipeline; + compiled_pipeline.registration = subgraph._pipeline.registration; + compiled_pipeline.context = subgraph._pipeline.context; + subgraph._pipeline.compile_stage(compiled_pipeline, compiled_pipeline.context); + + ASSERT_TRUE(compiled_pipeline.runtime_behavior.has_value()); + EXPECT_EQ(compiled_pipeline.registration.name, ov::npuw::patterns::attn::SDPA::pattern_name()); + EXPECT_EQ(compiled_pipeline.runtime_behavior->registration.name, ov::npuw::patterns::attn::SDPA::pattern_name()); + auto behavior = compiled_pipeline.runtime_behavior->factory(compiled_pipeline.runtime_behavior->context); + EXPECT_NE(behavior, nullptr); +} + +TEST(SubgraphPipelineBehaviorTest, MoERegistrationBuildsDeferredPartitionPipelineForExpertFunction) { + ov::npuw::Function function; + function.settag(ov::npuw::patterns::moe::GPTOSSExpert::isolation_tag()); + + ov::npuw::v1::subgraphs::PatternRegistry registry; + auto registrations = ov::npuw::moe::register_patterns(registry, 0u); + registry.apply(function); + + ASSERT_TRUE(static_cast(function._pipeline.partition_stage)); + ASSERT_TRUE(static_cast(function._pipeline.compile_stage)); + EXPECT_EQ(function._pipeline.registration.group, ov::npuw::patterns::moe::GPTOSSExpert::group_name()); + EXPECT_EQ(function._pipeline.registration.name, ov::npuw::patterns::moe::GPTOSSExpert::pattern_name()); + EXPECT_NE(std::find(function._pipeline.registration.patterns.begin(), + function._pipeline.registration.patterns.end(), + ov::npuw::patterns::moe::GPTOSSExpert::pattern_name()), + function._pipeline.registration.patterns.end()); +} + +// Verify that attn::register_patterns() chains partition_stage and compile_stage on any +// Function whose isolation tag matches SDPA::isolation_tag() ("attn"). +TEST(SubgraphPipelineBehaviorTest, AttnRegistrationSetsPartitionAndCompileStagesForAttnTaggedFunction) { + ov::npuw::Function function; + function.settag(ov::npuw::patterns::attn::SDPA::isolation_tag()); + + ov::npuw::v1::subgraphs::PatternRegistry registry; + auto registrations = ov::npuw::attn::register_patterns(registry); + registry.apply(function); + + ASSERT_TRUE(static_cast(function._pipeline.partition_stage)); + ASSERT_TRUE(static_cast(function._pipeline.compile_stage)); + // The registration must carry the SDPA pattern name so the compile loop can identify it. + EXPECT_NE(std::find(function._pipeline.registration.patterns.begin(), + function._pipeline.registration.patterns.end(), + ov::npuw::patterns::attn::SDPA::isolation_tag()), + function._pipeline.registration.patterns.end()); +} + +// Verify that the compile_stage does NOT attach a runtime behavior when the partition_stage +// was never given a compiled::Attention context entry — i.e. when f._attention is not set +// (NPUW_ATTN=STATIC, or the model has no dynamic dims in the attention function). +TEST(SubgraphPipelineBehaviorTest, AttnCompileStageSkipsRuntimeBehaviorWhenAttentionNotDynamic) { + ov::npuw::Function function; + function.settag(ov::npuw::patterns::attn::SDPA::isolation_tag()); + + ov::npuw::v1::subgraphs::PatternRegistry registry; + auto registrations = ov::npuw::attn::register_patterns(registry); + registry.apply(function); + + ASSERT_TRUE(static_cast(function._pipeline.partition_stage)); + ASSERT_TRUE(static_cast(function._pipeline.compile_stage)); + + // Run partition_stage WITHOUT setting f._attention — simulates NPUW_ATTN=STATIC or a + // model where function::Attention::from() found no dynamic dims. + function._pipeline.partition_stage(function, function._pipeline.context); + EXPECT_FALSE(function._pipeline.context.contains()) + << "partition_stage must not put compiled::Attention in context when f._attention is unset"; + + // Run compile_stage: with no compiled::Attention in context, no runtime behavior should appear. + ov::npuw::v1::subgraphs::CompiledPipeline compiled; + compiled.registration = function._pipeline.registration; + compiled.context = function._pipeline.context; + function._pipeline.compile_stage(compiled, compiled.context); + + EXPECT_FALSE(compiled.runtime_behavior.has_value()) + << "compile_stage must not attach DynAttnBehavior when compiled::Attention is absent"; +} + +TEST(SubgraphPipelineBehaviorTest, AttnCompileStageAttachesPyramidBehaviorWhenHintIsPresent) { + ov::npuw::Function function; + function.settag(ov::npuw::patterns::attn::SDPA::isolation_tag()); + + ov::npuw::v1::subgraphs::PatternRegistry registry; + auto registrations = ov::npuw::attn::register_patterns(registry); + registry.apply(function); + + function._pipeline.context.put(ov::npuw::attn::BehaviorKind::Pyramid); + + ov::npuw::v1::subgraphs::CompiledPipeline compiled; + compiled.registration = function._pipeline.registration; + compiled.context = function._pipeline.context; + function._pipeline.compile_stage(compiled, compiled.context); + + ASSERT_TRUE(compiled.runtime_behavior.has_value()); + auto behavior = compiled.runtime_behavior->factory(compiled.runtime_behavior->context); + EXPECT_NE(behavior, nullptr); +} + +TEST(SubgraphPipelineBehaviorTest, AttnCompileStageAttachesHFABehaviorWhenHintIsPresent) { + ov::npuw::Function function; + function.settag(ov::npuw::patterns::attn::SDPA::isolation_tag()); + + ov::npuw::v1::subgraphs::PatternRegistry registry; + auto registrations = ov::npuw::attn::register_patterns(registry); + registry.apply(function); + + function._pipeline.context.put(ov::npuw::attn::BehaviorKind::HFA); + + ov::npuw::v1::subgraphs::CompiledPipeline compiled; + compiled.registration = function._pipeline.registration; + compiled.context = function._pipeline.context; + function._pipeline.compile_stage(compiled, compiled.context); + + ASSERT_TRUE(compiled.runtime_behavior.has_value()); + auto behavior = compiled.runtime_behavior->factory(compiled.runtime_behavior->context); + EXPECT_NE(behavior, nullptr); +} diff --git a/src/plugins/intel_npu/thirdparty/level-zero-ext b/src/plugins/intel_npu/thirdparty/level-zero-ext index 42768cc73e74..f9ad3bf89c24 160000 --- a/src/plugins/intel_npu/thirdparty/level-zero-ext +++ b/src/plugins/intel_npu/thirdparty/level-zero-ext @@ -1 +1 @@ -Subproject commit 42768cc73e74f6d371bd9dd51b1860b07774e7ec +Subproject commit f9ad3bf89c2418d714aef2e6b96a5aafb12a1971 diff --git a/src/plugins/intel_npu/tools/opencv_version.json b/src/plugins/intel_npu/tools/opencv_version.json index 81342cd4eed1..87a629608ccf 100644 --- a/src/plugins/intel_npu/tools/opencv_version.json +++ b/src/plugins/intel_npu/tools/opencv_version.json @@ -1,3 +1,3 @@ { - "opencv" : "c1c893ff73581567b6d1a847e8dafc9c052d907c" + "opencv" : "8ec62ad3460fd4f371000157cf98cccaaa933303" } diff --git a/src/plugins/intel_npu/tools/single-image-test/README.md b/src/plugins/intel_npu/tools/single-image-test/README.md index 9801cd3b25ba..ba19a795de30 100644 --- a/src/plugins/intel_npu/tools/single-image-test/README.md +++ b/src/plugins/intel_npu/tools/single-image-test/README.md @@ -62,36 +62,57 @@ Running the application with the `-help` option yields the following usage messa single-image-test.exe: Usage: Release\single-image-test.exe[] Flags from C:\Users\mdoronin\work\applications.ai.vpu-accelerators.vpux-plugin\tools\single-image-test\main.cpp: - -box_tolerance (Box tolerance for 'detection' mode) type: double - default: 0.0001 + -apply_soft_max (Apply SoftMax for 'nrmse' mode) type: bool default: false + -box_tolerance (Box tolerance for 'detection' mode. Can be a single value + or per-layer: 'layer1:0.01;layer2:0.02') type: string default: "0.000100" -classes (Number of classes for Yolo V3) type: int32 default: 80 + -clamp_inf_outputs (Optional. ';'-separated list of output tensor names for + which +/-inf values will be clamped to the representable + [dtype::lowest, dtype::max] range before metric evaluation. Supported + element types: f32, f16, bf16. Unsupported types are skipped with a + warning.) type: string default: "" + -clamp_u8_outputs (Apply clamping when converting FP to U8) type: bool + default: false -color_format (Color format for input: RGB or BGR) type: string default: "BGR" -compiled_blob (Output compiled network file (compiled result blob)) type: string default: "" - -confidence_threshold (Confidence threshold for Detection mode) - type: double default: 0.0001 + -confidence_threshold (Confidence threshold for Detection mode. Can be a + single value or per-layer: 'layer1:0.5;layer2:0.3') type: string + default: "0.000100" -config (Path to the configuration file (optional)) type: string default: "" -coords (Number of coordinates for Yolo V3) type: int32 default: 4 - -cosim_threshold (Threshold for 'cosim' mode) type: double - default: 0.90000000000000002 + -cosim_threshold (Threshold for 'cosim' mode. Can be a single value or + per-layer: 'layer1:0.95;layer2:0.90') type: string default: "0.900000" + -data_shape (Required for models with dynamic shapes. Set shape for input + blobs. Only one shape can be set. In case of one input size: + "[1,3,224,224]") type: string default: "" -dataset (The dataset used to train the model. Useful for instances such as semantic segmentation to visualize the accuracy per-class) type: string default: "NONE" -device (Device to use) type: string default: "" - -il (Input layout) type: string default: "" + -il (Input layout for all inputs, or ';' separated list of pairs + :. Regex in is supported) type: string default: "" -img_as_bin (Force binary input even if network expects an image) type: bool default: false -img_bin_precision (Specify the precision of the binary input files. Eg: 'FP32,FP16,I32,I64,U8') type: string default: "" - -iml (Model input layout) type: string default: "" - -input (Input file(s)) type: string default: "" - -ip (Input precision (default: U8, available: FP32, FP16, I32, I64, U8)) + -iml (Model input layout for all model inputs, or ';' separated list of + pairs :. Regex in is supported) type: string default: "" + -input (Input file(s)) type: string default: "" + -ip (Input precision (default: U8, available: FP32, FP16, I32, I64, U8, + U16, I16, U4, I4, U2, BF8(F8E5M2), HF8(F8E4M3))) type: string + default: "" -is_tiny_yolo (Is it Tiny Yolo or not (true or false)?) type: bool default: false + -l2norm_threshold (Threshold for 'l2norm' mode. Can be a single value or + per-layer: 'layer1:1.0;layer2:2.0') type: string default: "1.000000" -log_level (IE logger level (optional)) type: string default: "" + -map_threshold (mAP score threshold for 'map' mode validation. Can be a + single value or per-layer: 'layer1:0.5;layer2:0.6') type: string + default: "0.500000" -mean_values (Optional. Mean values to be used for the input image per channel. Values to be provided in the [channel1,channel2,channel3] format. Can be defined for desired input of the model, for example: @@ -103,23 +124,45 @@ single-image-test.exe: Usage: Release\single-image-test.exe[] -network (Network file (either XML or pre-compiled blob)) type: string default: "" -normalized_image (Images in [0, 1] range or not) type: bool default: false - -nrmse_loss_threshold (Threshold for 'nrmse' mode) type: double default: 1 + -nrmse_loss_threshold (Threshold for 'nrmse' mode. Can be a single value + or per-layer: 'logits:0.03;pred_boxes:0.05') type: string + default: "0.020000" -num (Number of scales for Yolo V3) type: int32 default: 3 - -ol (Output layout) type: string default: "" - -oml (Model output layout) type: string default: "" - -op (Output precision (default: FP32, available: FP32, FP16, I32, I64, U8)) - type: string default: "" + -ol (Output layout for all outputs, or ';' separated list of pairs + :. Regex in is supported) type: string + default: "" + -oml (Model output layout for all outputs, or ';' separated list of pairs + :. Regex in is supported) type: string + default: "" + -op (Output precision (default: FP32, available: FP32, FP16, I32, I64, U8, + U16, I16, U4, I4, U2, BF8(F8E5M2), HF8(F8E4M3))) type: string + default: "" + -overlap_threshold (IoU threshold for 'map' mode (detection matching). Can + be a single value or per-layer: 'layer1:0.5;layer2:0.6') type: string + default: "0.500000" -override_model_batch_size (Enforce a model to be compiled for batch size) type: uint32 default: 1 -pc (Report performance counters) type: bool default: false - -prob_tolerance (Probability tolerance for 'classification/ssd/yolo' mode) - type: double default: 0.0001 - -psnr_reference (PSNR reference value in dB) type: double default: 30 - -psnr_tolerance (Tolerance for 'psnr' mode) type: double default: 0.0001 - -raw_tolerance (Tolerance for 'raw' mode (absolute diff)) type: double - default: 0.0001 - -rrmse_loss_threshold (Threshold for 'rrmse' mode) type: double - default: 1.7976931348623157e+308 + -prob_tolerance (Probability tolerance for 'classification/ssd/yolo' mode. + Can be a single value or per-layer: 'layer1:0.01;layer2:0.02') + type: string default: "0.000100" + -psnr_reference (PSNR reference value in dB. Can be a single value or + per-layer: 'layer1:30.0;layer2:35.0') type: string default: "30.000000" + -psnr_tolerance (Tolerance for 'psnr' mode. Can be a single value or + per-layer: 'layer1:0.01;layer2:0.02') type: string default: "0.000100" + -raw_tolerance (Tolerance for 'raw' mode (absolute diff). Can be a single + value or per-layer: 'layer1:0.01;layer2:0.02') type: string + default: "0.000100" + -ref_dir (A directory with reference blobs to compare with in run_test + mode. Leave it empty to use the current folder.) type: string default: "" + -ref_results (String of reference result file(s) to be used during + run_test mode. For the same test case, files are separated by comma (,); + for different test cases by semicolon (;). If ref_dir is provided, paths + should be relative to ref_dir; otherwise absolute paths are expected.) + type: string default: "" + -rrmse_loss_threshold (Threshold for 'rrmse' mode. Can be a single value + or per-layer: 'layer1:0.1;layer2:0.2') type: string + default: "1.7976931348623157e+308" -run_test (Run the test (compare current results with previously dumped)) type: bool default: false -scale_border (Scale border) type: uint32 default: 4 @@ -136,8 +179,18 @@ single-image-test.exe: Usage: Release\single-image-test.exe[] default: 12 -sem_seg_ignore_label (The number of the label to be ignored) type: uint32 default: 4294967295 - -sem_seg_threshold (Threshold for 'semantic segmentation' mode) - type: double default: 0.97999999999999998 + -sem_seg_threshold (Threshold for 'semantic segmentation' mode. Can be a + single value or per-layer: 'layer1:0.98;layer2:0.95') type: string + default: "0.98" + -shape (Optional. Set shape for model input. For example, + "input1[1,3,224,224],input2[1,4]" or "[1,3,224,224]" in case of one + input size. This parameter affects model input shape and can be dynamic. + For dynamic dimensions use symbol '?' or '-1'. Ex. [?,3,?,?]. + For bounded dimensions specify range 'min..max'. Ex. [1..10,3,?,?].) + type: string default: "" + -skip_arg_max (Skip ArgMax post processing step) type: bool default: false + -skip_output_layers (Skip output layers from the network. Accept ';' + separated list of output layers) type: string default: "" -top_k (Top K parameter for 'classification' mode) type: uint32 default: 1 ``` @@ -188,6 +241,9 @@ For example, to run inference with mobilenet-v2 model on Intel® Core™ Ultra N Performance counters: 0 Mean_values [channel1,channel2,channel3] Scale_values [channel1,channel2,channel3] + Skip checking output layers: + Clamp U8 outputs: 0 + Clamp inf outputs: Log level: Run single image test @@ -263,6 +319,11 @@ For example, to run inference with mobilenet-v2 model on Intel® Core™ Ultra N Performance counters: 0 Mean_values [channel1,channel2,channel3] Scale_values [channel1,channel2,channel3] + Skip checking output layers: + Clamp U8 outputs: 0 + Clamp inf outputs: + Reference files directory: Current directory + Reference file(s): Mode: classification Top K: 1 Tolerance: 0.6 diff --git a/src/plugins/intel_npu/tools/single-image-test/argument_parse_helpers.cpp b/src/plugins/intel_npu/tools/single-image-test/argument_parse_helpers.cpp index 0713d112ede2..c5cd125d2b27 100644 --- a/src/plugins/intel_npu/tools/single-image-test/argument_parse_helpers.cpp +++ b/src/plugins/intel_npu/tools/single-image-test/argument_parse_helpers.cpp @@ -51,6 +51,10 @@ DEFINE_string(data_shape, "", DEFINE_string(skip_output_layers, "", "Skip output layers from the network." " Accept ';' separated list of output layers"); DEFINE_bool(clamp_u8_outputs, false, "Apply clamping when converting FP to U8"); +DEFINE_string(clamp_inf_outputs, "", + "Optional. ';'-separated list of output tensor names for which +/-inf values will be clamped to the " + "representable [dtype::lowest, dtype::max] range before metric evaluation. " + "Supported element types: f32, f16, bf16. Unsupported types are skipped with a warning."); DEFINE_string(mean_values, "", "Optional. Mean values to be used for the input image per channel. " "Values to be provided in the [channel1,channel2,channel3] format. " @@ -88,6 +92,18 @@ DEFINE_string(rrmse_loss_threshold, std::to_string(metric_defaults::rrmse_loss_t "Threshold for 'rrmse' mode. Can be a single value or per-layer: 'layer1:0.1;layer2:0.2'"); DEFINE_string(nrmse_loss_threshold, std::to_string(metric_defaults::nrmse_loss_threshold), "Threshold for 'nrmse' mode. Can be a single value or per-layer: 'logits:0.03;pred_boxes:0.05'"); +DEFINE_string(nrmse_prefill_seq_len_axis, "", + "Optional. Per-output sequence-length axis used by the 'nrmse' mode to slice prefill LLM " + "outputs (KV cache / hidden state). Semicolon-separated list of 'layer:axis' pairs, e.g. " + "'present.0.key:2;present.0.value:3;Result_logits:1'. Combined with --nrmse_prefill_seq_len_size, " + "the output tensor is sliced to its last N elements along the specified axis before computing " + "NRMSE. If either flag is missing for a given layer, the full tensor is compared."); +DEFINE_string(nrmse_prefill_seq_len_size, "", + "Optional. Per-output number of elements to keep along the sequence-length axis (= prompt " + "length) when computing NRMSE on prefill LLM outputs. Semicolon-separated 'layer:N' pairs, " + "e.g. 'present.0.key:7;present.0.value:7;Result_logits:7'. Used together with " + "--nrmse_prefill_seq_len_axis. If either flag is missing for a given layer, the full tensor " + "is compared."); DEFINE_string(l2norm_threshold, std::to_string(metric_defaults::l2norm_threshold), "Threshold for 'l2norm' mode. Can be a single value or per-layer: 'layer1:1.0;layer2:2.0'"); DEFINE_string(overlap_threshold, std::to_string(metric_defaults::overlap_threshold), @@ -158,6 +174,7 @@ void utils::parseCommandLine(int argc, char* argv[]) { std::cout << " Scale_values [channel1,channel2,channel3] " << FLAGS_scale_values << std::endl; std::cout << " Skip checking output layers: " << FLAGS_skip_output_layers << std::endl; std::cout << " Clamp U8 outputs: " << FLAGS_clamp_u8_outputs << std::endl; + std::cout << " Clamp inf outputs: " << FLAGS_clamp_inf_outputs << std::endl; if (FLAGS_run_test) { std::cout << " Reference files directory: " << (FLAGS_ref_dir.empty() && FLAGS_ref_results.empty() ? "Current directory" : FLAGS_ref_dir) @@ -183,6 +200,10 @@ void utils::parseCommandLine(int argc, char* argv[]) { std::cout << " mAP Threshold: " << FLAGS_map_threshold << std::endl; } else if (strEq(FLAGS_mode, "nrmse")) { std::cout << " Threshold: " << FLAGS_nrmse_loss_threshold << std::endl; + if (!FLAGS_nrmse_prefill_seq_len_axis.empty() || !FLAGS_nrmse_prefill_seq_len_size.empty()) { + std::cout << " Prefill seq-len axis: " << FLAGS_nrmse_prefill_seq_len_axis << std::endl; + std::cout << " Prefill seq-len size: " << FLAGS_nrmse_prefill_seq_len_size << std::endl; + } } else if (strEq(FLAGS_mode, "l2norm")) { std::cout << " Threshold: " << FLAGS_l2norm_threshold << std::endl; } diff --git a/src/plugins/intel_npu/tools/single-image-test/argument_parse_helpers.hpp b/src/plugins/intel_npu/tools/single-image-test/argument_parse_helpers.hpp index 5479267f84f7..4be061f3fa51 100644 --- a/src/plugins/intel_npu/tools/single-image-test/argument_parse_helpers.hpp +++ b/src/plugins/intel_npu/tools/single-image-test/argument_parse_helpers.hpp @@ -70,6 +70,7 @@ DECLARE_string(shape); DECLARE_string(data_shape); DECLARE_string(skip_output_layers); DECLARE_bool(clamp_u8_outputs); +DECLARE_string(clamp_inf_outputs); DECLARE_string(mean_values); DECLARE_string(scale_values); DECLARE_string(img_bin_precision); @@ -83,6 +84,8 @@ DECLARE_string(raw_tolerance); DECLARE_string(cosim_threshold); DECLARE_string(rrmse_loss_threshold); DECLARE_string(nrmse_loss_threshold); +DECLARE_string(nrmse_prefill_seq_len_axis); +DECLARE_string(nrmse_prefill_seq_len_size); DECLARE_string(l2norm_threshold); DECLARE_string(overlap_threshold); DECLARE_string(map_threshold); diff --git a/src/plugins/intel_npu/tools/single-image-test/main.cpp b/src/plugins/intel_npu/tools/single-image-test/main.cpp index 7f0db787bd69..66992927b2f8 100644 --- a/src/plugins/intel_npu/tools/single-image-test/main.cpp +++ b/src/plugins/intel_npu/tools/single-image-test/main.cpp @@ -18,10 +18,12 @@ #include #include +#include #include #include #include #include +#include #include #include #include @@ -30,6 +32,37 @@ using TensorMap = std::map; +// Clamp +/-inf elements to the representable range of the buffer element type. +template +static void clampInfInBuffer(T* data, size_t count) { + const T posMax = std::numeric_limits::max(); + const T negMin = std::numeric_limits::lowest(); + for (size_t i = 0; i < count; ++i) { + const float val = static_cast(data[i]); + if (std::isinf(val)) { + data[i] = (val > 0.0f) ? posMax : negMin; + } + } +} + +// Clamp +/-inf elements in a tensor in-place. +// Returns true on success; returns false for unsupported (non-float) element types. +static bool clampInfInTensor(ov::Tensor& tensor) { + const auto et = tensor.get_element_type(); + const size_t count = tensor.get_size(); + if (et == ov::element::f32) { + clampInfInBuffer(tensor.data(), count); + return true; + } else if (et == ov::element::f16) { + clampInfInBuffer(tensor.data(), count); + return true; + } else if (et == ov::element::bf16) { + clampInfInBuffer(tensor.data(), count); + return true; + } + return false; +} + struct TensorDescriptor { ov::element::Type precision; ov::PartialShape shape; @@ -360,7 +393,7 @@ void cvToOV(const cv::Mat& cvImg, << n << " up to " << N << " with image data from the array: " << cvImgInBatch.to_string() << std::endl; } - cv::Mat batch(static_cast(H), + cv::Mat batch(static_cast(H), static_cast(W), cvType, dataBuffer + n * (out.size().area() * out.elemSize())); @@ -1528,6 +1561,54 @@ bool testRRMSE(const TensorMap& outputs, const TensorMap& references, const Layo // e.g. '--mode nrmse --nrmse_loss_threshold 0.01' // e.g. '--mode nrmse --nrmse_loss_threshold "logits:0.03;pred_boxes:0.05"' // +// Optional per-output prefill slicing for LLM KV cache / hidden state outputs: +// --nrmse_prefill_seq_len_axis ":;..." +// --nrmse_prefill_seq_len_size ":;..." +// Keeps only the last N elements along the specified axis before computing NRMSE. +// + +// Return a new contiguous tensor containing the last `len` elements of `src` along `axis`. +// The element type and all other dimensions are preserved. +static ov::Tensor sliceLastNAlongAxis(const ov::Tensor& src, size_t axis, size_t len) { + const auto& shape = src.get_shape(); + OPENVINO_ASSERT(axis < shape.size(), + "nrmse_prefill_seq_len_axis ", axis, + " is out of range for tensor of rank ", shape.size()); + OPENVINO_ASSERT(len > 0 && len <= shape[axis], + "nrmse_prefill_seq_len_size ", len, + " is out of range for axis ", axis, " of size ", shape[axis]); + if (len == shape[axis]) { + return src; + } + + ov::Shape newShape = shape; + newShape[axis] = len; + ov::Tensor dst(src.get_element_type(), newShape); + + size_t outer = 1; + for (size_t i = 0; i < axis; ++i) { + outer *= shape[i]; + } + const size_t mid = shape[axis]; + size_t inner = 1; + for (size_t i = axis + 1; i < shape.size(); ++i) { + inner *= shape[i]; + } + + const size_t elemSize = src.get_element_type().size(); + const size_t innerBytes = inner * elemSize; + const size_t srcStart = mid - len; + + const auto* srcData = static_cast(src.data()); + auto* dstData = static_cast(dst.data()); + + for (size_t o = 0; o < outer; ++o) { + const uint8_t* srcRow = srcData + (o * mid + srcStart) * innerBytes; + uint8_t* dstRow = dstData + (o * len) * innerBytes; + std::memcpy(dstRow, srcRow, len * innerBytes); + } + return dst; +} bool computeNRMSE(const ov::Tensor& output, const ov::Tensor& reference, double threshold) { if (output.get_shape() != reference.get_shape()) { @@ -1691,13 +1772,13 @@ bool computeMAP(const std::map& outputs, const std::map bool testMAP(const TensorMap& outputs, const TensorMap& references, const LayoutMap& outputLayouts) { if (outputs.size() != references.size()) { - std::cout << "Actual and reference has different number of output blobs" << std::endl; - return false; + std::cout << "Warning: Actual and reference have different number of output blobs (" + << outputs.size() << " vs " << references.size() << ")" << std::endl; } - // For single-image detection models with pred_boxes and logits outputs, - // compute mAP directly from all outputs rather than per-layer - std::cout << "Computing mAP for single-image detection model" << std::endl; + // Compute mAP from detection model outputs. + // Supports: DETR-style (pred_boxes + logits), YOLOv10-style (single [N,6] tensor) + std::cout << "Computing mAP for detection model" << std::endl; std::cout << "Output layers:" << std::endl; for (const auto& [tensorName, tensor] : outputs) { std::cout << " - " << tensorName << " : " << tensor.get_shape() << std::endl; @@ -1745,6 +1826,12 @@ bool testNRMSE(const TensorMap& outputs, const TensorMap& references, const Layo // Parse per-layer thresholds auto thresholdMap = utils::parsePerLayerValues(FLAGS_nrmse_loss_threshold, metric_defaults::nrmse_loss_threshold); + // Parse optional per-layer prefill seq-len axis/size maps used to slice LLM + // KV-cache / hidden-state outputs to the last N positions along the sequence axis. + // Only entries that appear in BOTH maps are honored; otherwise the full tensor is compared. + auto seqLenAxisMap = utils::parsePerLayerValues(FLAGS_nrmse_prefill_seq_len_axis, 0.0); + auto seqLenSizeMap = utils::parsePerLayerValues(FLAGS_nrmse_prefill_seq_len_size, 0.0); + bool allPassed = true; for (auto& [tensorName, output] : outputs) { auto referencesIterator = references.find(tensorName); @@ -1753,6 +1840,26 @@ bool testNRMSE(const TensorMap& outputs, const TensorMap& references, const Layo double layerThreshold = utils::getValueForLayer(thresholdMap, tensorName); + // Optional prefill slicing: applied only when both axis and size are explicitly + // specified for this layer. When only one of the two is given, the full tensor + // is compared and a warning is emitted. + ov::Tensor outputForCompare = output; + ov::Tensor referenceForCompare = referencesIterator->second; + const bool hasAxis = seqLenAxisMap.find(tensorName) != seqLenAxisMap.end(); + const bool hasSize = seqLenSizeMap.find(tensorName) != seqLenSizeMap.end(); + if (hasAxis && hasSize) { + const size_t axis = static_cast(seqLenAxisMap.at(tensorName)); + const size_t keep = static_cast(seqLenSizeMap.at(tensorName)); + std::cout << "Slicing NRMSE input '" << tensorName << "' to last " << keep + << " element(s) along axis " << axis << std::endl; + outputForCompare = sliceLastNAlongAxis(outputForCompare, axis, keep); + referenceForCompare = sliceLastNAlongAxis(referenceForCompare, axis, keep); + } else if (hasAxis != hasSize) { + std::cout << "Warning: only one of --nrmse_prefill_seq_len_axis / " + "--nrmse_prefill_seq_len_size is set for layer '" + << tensorName << "'; comparing full tensor." << std::endl; + } + BlobTestMethod blobComparator = [applySoftMax, layerThreshold](ov::Tensor outputTensor, ov::Tensor referenceTensor) { if (applySoftMax) { std::vector actOutput; @@ -1778,8 +1885,8 @@ bool testNRMSE(const TensorMap& outputs, const TensorMap& references, const Layo }; if (!test_blobs_in_batch(tensorName, - splitBatchedTensor(output, tensorName, outputLayouts), - splitBatchedTensor(referencesIterator->second, tensorName, outputLayouts), + splitBatchedTensor(outputForCompare, tensorName, outputLayouts), + splitBatchedTensor(referenceForCompare, tensorName, outputLayouts), blobComparator)) { allPassed = false; } @@ -2766,6 +2873,32 @@ static int runSingleImageTest() { } } + // Clamp +/-inf values in the specified output tensors before metric evaluation. + if (!FLAGS_clamp_inf_outputs.empty()) { + const auto infClampNames = splitStringList(FLAGS_clamp_inf_outputs, ';'); + for (const auto& name : infClampNames) { + const auto outIt = filteredOutputTensors.find(name); + const auto refIt = filteredReferenceTensors.find(name); + if (outIt == filteredOutputTensors.end() && refIt == filteredReferenceTensors.end()) { + std::cerr << "Warning: tensor '" << name + << "' specified in --clamp_inf_outputs was not found in output tensors" + << std::endl; + continue; + } + if (outIt != filteredOutputTensors.end()) { + if (!clampInfInTensor(outIt->second)) { + std::cerr << "Warning: tensor '" << name << "' has element type " + << outIt->second.get_element_type() + << " which is not supported for inf clamping, skipping" << std::endl; + continue; + } + } + if (refIt != filteredReferenceTensors.end()) { + clampInfInTensor(refIt->second); + } + } + } + using ResultStatus = std::tuple; std::array resultStatuses{{{"FAILED", EXIT_FAILURE}, {"PASSED", EXIT_SUCCESS}}}; diff --git a/src/plugins/intel_npu/tools/single-image-test/map_metric_helpers.cpp b/src/plugins/intel_npu/tools/single-image-test/map_metric_helpers.cpp index 863a8ecabf5e..8d0b5f15a50c 100644 --- a/src/plugins/intel_npu/tools/single-image-test/map_metric_helpers.cpp +++ b/src/plugins/intel_npu/tools/single-image-test/map_metric_helpers.cpp @@ -39,33 +39,93 @@ float calculateIoU(const Detection& detection1, const Detection& detection2, boo return union_area > 0.0f ? intersection / union_area : 0.0f; } -// Parse detections from model outputs (single image) -// Expected outputs: pred_boxes, logits, encoder_hidden_state, last_hidden_state -// pred_boxes: [batch, num_queries, 4] with format [x_center, y_center, width, height] (normalized) -// logits: [batch, num_queries, num_classes] with class probabilities/logits -std::vector parseDetectionsFromOutputs(const std::map& outputs, - float confidence_threshold) { +// Parse detections from a single combined output tensor. +// Supports two formats: +// - [batch, N, 6]: each detection is [x1, y1, x2, y2, score, class_id] (e.g. YOLOv10) +// - [batch, N, 5+C]: each detection is [x1, y1, x2, y2, score, class_0, ..., class_C-1] +// where score is objectness and class scores are per-class confidences +static std::vector parseSingleTensorDetections(const ov::Tensor& tensor, + float confidence_threshold) { std::vector detections; - // Find the pred_boxes and logits tensors - auto pred_boxes_it = outputs.find("pred_boxes"); - auto logits_it = outputs.find("logits"); - - // Use the provided confidence_threshold parameter as the single source of truth - const float confThresh = confidence_threshold; + const ov::Tensor fp32 = npu::utils::toFP32(tensor); + const auto buffer = fp32.data(); + const auto shape = tensor.get_shape(); - if (pred_boxes_it == outputs.end()) { - std::cout << "Warning: 'pred_boxes' output not found" << std::endl; + if (shape.size() != 3) { + std::cout << "Warning: Single-tensor detection requires 3D shape [batch, N, cols], got " + << shape.size() << "D" << std::endl; return detections; } - if (logits_it == outputs.end()) { - std::cout << "Warning: 'logits' output not found" << std::endl; + size_t num_detections = shape[1]; + size_t cols = shape[2]; + + if (cols < 6) { + std::cout << "Warning: Single-tensor detection requires at least 6 columns, got " + << cols << std::endl; return detections; } - const ov::Tensor& pred_boxes_tensor = pred_boxes_it->second; - const ov::Tensor& logits_tensor = logits_it->second; + if (cols == 6) { + // YOLOv10 format: [x1, y1, x2, y2, score, class_id] + for (size_t i = 0; i < num_detections; ++i) { + size_t offset = i * cols; + float x1 = buffer[offset + 0]; + float y1 = buffer[offset + 1]; + float x2 = buffer[offset + 2]; + float y2 = buffer[offset + 3]; + float score = buffer[offset + 4]; + int class_id = static_cast(buffer[offset + 5]); + + // Skip padding detections (score <= 0 or invalid boxes) + if (score > confidence_threshold && x2 > x1 && y2 > y1) { + detections.emplace_back(x1, y1, x2, y2, score, class_id); + } + } + } else { + // Format [x1, y1, x2, y2, objectness_score, class_0_score, ..., class_C-1_score] + size_t num_classes = cols - 5; + for (size_t i = 0; i < num_detections; ++i) { + size_t offset = i * cols; + float x1 = buffer[offset + 0]; + float y1 = buffer[offset + 1]; + float x2 = buffer[offset + 2]; + float y2 = buffer[offset + 3]; + float obj_score = buffer[offset + 4]; + + if (obj_score <= confidence_threshold || x2 <= x1 || y2 <= y1) { + continue; + } + + // Find the class with the highest score + float max_class_score = -std::numeric_limits::infinity(); + int best_class = 0; + for (size_t c = 0; c < num_classes; ++c) { + float class_score = buffer[offset + 5 + c]; + if (class_score > max_class_score) { + max_class_score = class_score; + best_class = static_cast(c); + } + } + + float confidence = obj_score * max_class_score; + if (confidence > confidence_threshold) { + detections.emplace_back(x1, y1, x2, y2, confidence, best_class); + } + } + } + + return detections; +} + +// Parse detections from two separate output tensors (pred_boxes + logits). +// pred_boxes: [batch, num_queries, 4] with format [x_center, y_center, width, height] (normalized) +// logits: [batch, num_queries, num_classes] with class probabilities/logits +static std::vector parseTwoTensorDetections(const ov::Tensor& pred_boxes_tensor, + const ov::Tensor& logits_tensor, + float confidence_threshold) { + std::vector detections; const ov::Tensor boxes_fp32 = npu::utils::toFP32(pred_boxes_tensor); const ov::Tensor logits_fp32 = npu::utils::toFP32(logits_tensor); @@ -76,22 +136,16 @@ std::vector parseDetectionsFromOutputs(const std::map parseDetectionsFromOutputs(const std::map::infinity(); int best_class = -1; - // First pass: find max logit for numerical stability for (size_t c = 0; c < num_classes; ++c) { float logit = logits_buffer[logits_offset + c]; if (logit > max_logit) { @@ -132,19 +182,15 @@ std::vector parseDetectionsFromOutputs(const std::map confThresh && best_class >= 0) { + if (confidence > confidence_threshold && best_class >= 0) { detections.emplace_back(x_min, y_min, x_max, y_max, confidence, best_class); } } @@ -152,6 +198,68 @@ std::vector parseDetectionsFromOutputs(const std::map parseDetectionsFromOutputs(const std::map& outputs, + float confidence_threshold) { + std::vector detections; + + if (outputs.empty()) { + std::cout << "Warning: No output tensors provided" << std::endl; + return detections; + } + + // Strategy 1: Look for named "pred_boxes" and "logits" tensors (DETR-style) + auto pred_boxes_it = outputs.find("pred_boxes"); + auto logits_it = outputs.find("logits"); + + if (pred_boxes_it != outputs.end() && logits_it != outputs.end()) { + return parseTwoTensorDetections(pred_boxes_it->second, logits_it->second, confidence_threshold); + } + + // Strategy 2: Single output tensor so parse as combined detections + if (outputs.size() == 1) { + const auto& [name, tensor] = *outputs.begin(); + const auto shape = tensor.get_shape(); + + if (shape.size() == 3 && shape[2] >= 6) { + return parseSingleTensorDetections(tensor, confidence_threshold); + } + + std::cout << "Warning: Single output '" << name << "' with shape " << shape + << " does not match expected detection format [batch, N, 6+]" << std::endl; + return detections; + } + + // Strategy 3: Two outputs without standard names so infer roles by shape + // The tensor with last dim == 4 is boxes, the other is logits/classes + if (outputs.size() == 2) { + const ov::Tensor* boxes_tensor = nullptr; + const ov::Tensor* classes_tensor = nullptr; + std::string boxes_name, classes_name; + + for (const auto& [name, tensor] : outputs) { + const auto shape = tensor.get_shape(); + if (shape.size() == 3 && shape[2] == 4) { + boxes_tensor = &tensor; + boxes_name = name; + } else { + classes_tensor = &tensor; + classes_name = name; + } + } + + if (boxes_tensor && classes_tensor) { + return parseTwoTensorDetections(*boxes_tensor, *classes_tensor, confidence_threshold); + } + } + + std::cout << "Warning: Could not determine detection format from outputs. Available tensors:" << std::endl; + for (const auto& [name, tensor] : outputs) { + std::cout << " " << name << " : " << tensor.get_shape() << std::endl; + } + return detections; +} + MatchResult matchDetectionsForClass(const std::vector& predictions, const std::vector& ground_truth, int class_id, diff --git a/src/plugins/intel_npu/tools/single-image-test/map_metric_helpers.hpp b/src/plugins/intel_npu/tools/single-image-test/map_metric_helpers.hpp index c19d6b98a450..3f8988314c7d 100644 --- a/src/plugins/intel_npu/tools/single-image-test/map_metric_helpers.hpp +++ b/src/plugins/intel_npu/tools/single-image-test/map_metric_helpers.hpp @@ -6,6 +6,7 @@ #include #include +#include #include @@ -42,6 +43,14 @@ MatchResult matchDetectionsForClass(const std::vector& predictions, float iou_threshold, bool include_boundaries = true); +// Parse detections from model outputs. +// Supports multiple output formats: +// - Two-layer mode: outputs contain "pred_boxes" [batch, N, 4] (cx, cy, w, h normalized) +// and "logits" [batch, N, num_classes] +// - Single-layer mode (e.g. YOLOv10): single output with shape [batch, N, 6] +// where each detection is [x1, y1, x2, y2, score, class_id] +// - Single-layer mode with per-class scores: single output with shape [batch, N, 5+C] +// where each detection is [x1, y1, x2, y2, score, class_0_score, ..., class_C-1_score] std::vector parseDetectionsFromOutputs(const std::map& outputs, float confidence_threshold = 0.0f); } // namespace utils diff --git a/src/plugins/proxy/src/plugin.cpp b/src/plugins/proxy/src/plugin.cpp index 535e494f2d0f..711703b7a3aa 100644 --- a/src/plugins/proxy/src/plugin.cpp +++ b/src/plugins/proxy/src/plugin.cpp @@ -51,13 +51,6 @@ bool compare_containers(const std::unordered_set& c1, const std::un return true; } -size_t string_to_size_t(const std::string& s) { - std::stringstream sstream(s); - size_t idx; - sstream >> idx; - return idx; -} - bool is_device_in_config(const ov::AnyMap& config) { return config.find(ov::device::id.name()) != config.end(); } @@ -200,10 +193,12 @@ void ov::proxy::Plugin::set_property(const ov::AnyMap& properties) { // Biggest number means minimum priority size_t min_priority(0); for (auto&& dev_priority : it->second.as>()) { - auto dev_prior = ov::util::split(dev_priority, ':'); + auto dev_prior = ov::util::split(dev_priority, ":"); OPENVINO_ASSERT(dev_prior.size() == 2, "Cannot set ov::proxy::device_priorities property. Format is incorrect."); - auto priority = string_to_size_t(dev_prior[1]); + const auto priority_opt = util::view_to_number(dev_prior[1]); + OPENVINO_ASSERT(priority_opt, "Cannot parse ov::proxy::device_priorities value from: ", dev_prior[1]); + const auto priority = *priority_opt; if (priority > min_priority) min_priority = priority; priority_order.push_back(std::pair{dev_prior[0], priority}); @@ -587,7 +582,7 @@ std::vector> ov::proxy::Plugin::get_hidden_devices() co // Add fallback devices use device_id for individual fallback property auto fallback = get_internal_property(ov::device::priorities.name(), device_id).as(); if (!fallback.empty()) { - for (const auto& fallback_dev : ov::util::split(fallback, ' ')) { + for (const auto& fallback_dev : ov::util::split(fallback, " ")) { if (fallback_dev != device) devices.emplace_back(fallback_dev); else diff --git a/src/plugins/template/backend/ops/gather_nd.cpp b/src/plugins/template/backend/ops/gather_nd.cpp index 30cf05584739..075ee02e9404 100644 --- a/src/plugins/template/backend/ops/gather_nd.cpp +++ b/src/plugins/template/backend/ops/gather_nd.cpp @@ -5,7 +5,9 @@ #include "openvino/reference/gather_nd.hpp" #include "evaluate_node.hpp" +#include "gather_nd_shape_inference.hpp" #include "openvino/core/type/element_type_traits.hpp" +#include "openvino/core/validation_util.hpp" #include "openvino/op/gather_nd.hpp" template @@ -13,7 +15,9 @@ bool evaluate(const std::shared_ptr& op, ov::TensorVector& outputs, const ov::TensorVector& inputs) { using T = typename ov::element_type_traits::value_type; - if (op->get_input_element_type(1) == ov::element::i64) { + const auto output_shapes = ov::op::v5::shape_infer(op.get(), ov::util::get_tensors_partial_shapes(inputs)); + outputs[0].set_shape(output_shapes[0].get_shape()); + if (const auto& ind_type = op->get_input_element_type(1); ind_type == ov::element::i64) { ov::reference::gather_nd(inputs[0].data(), inputs[1].data(), outputs[0].data(), @@ -21,7 +25,7 @@ bool evaluate(const std::shared_ptr& op, inputs[1].get_shape(), outputs[0].get_shape(), static_cast(op->get_batch_dims())); - } else if (op->get_input_element_type(1) == ov::element::i32) { + } else if (ind_type == ov::element::i32) { ov::reference::gather_nd(inputs[0].data(), inputs[1].data(), outputs[0].data(), @@ -30,7 +34,7 @@ bool evaluate(const std::shared_ptr& op, outputs[0].get_shape(), static_cast(op->get_batch_dims())); } else { - OPENVINO_THROW("Unexpected indices type for GatherND operation"); + OPENVINO_THROW("Unexpected indices type for GatherND operation: ", ind_type); } return true; } @@ -40,7 +44,9 @@ bool evaluate(const std::shared_ptr& op, ov::TensorVector& outputs, const ov::TensorVector& inputs) { using T = typename ov::element_type_traits::value_type; - if (op->get_input_element_type(1) == ov::element::i64) { + const auto output_shapes = ov::op::v8::shape_infer(op.get(), ov::util::get_tensors_partial_shapes(inputs)); + outputs[0].set_shape(output_shapes[0].get_shape()); + if (const auto& ind_type = op->get_input_element_type(1); ind_type == ov::element::i64) { ov::reference::gather_nd(inputs[0].data(), inputs[1].data(), outputs[0].data(), @@ -48,7 +54,7 @@ bool evaluate(const std::shared_ptr& op, inputs[1].get_shape(), outputs[0].get_shape(), static_cast(op->get_batch_dims())); - } else if (op->get_input_element_type(1) == ov::element::i32) { + } else if (ind_type == ov::element::i32) { ov::reference::gather_nd(inputs[0].data(), inputs[1].data(), outputs[0].data(), @@ -57,7 +63,7 @@ bool evaluate(const std::shared_ptr& op, outputs[0].get_shape(), static_cast(op->get_batch_dims())); } else { - OPENVINO_THROW("Unexpected indices type for GatherND operation"); + OPENVINO_THROW("Unexpected indices type for GatherND operation: ", ind_type); } return true; } diff --git a/src/plugins/template/backend/ops/interpolate.cpp b/src/plugins/template/backend/ops/interpolate.cpp index e9074f474d81..f237724ccaa7 100644 --- a/src/plugins/template/backend/ops/interpolate.cpp +++ b/src/plugins/template/backend/ops/interpolate.cpp @@ -170,6 +170,120 @@ std::vector get_scales_vector(const ov::TensorVector& args, return scales; } +namespace v4 { +bool evaluate_interpolate(const std::shared_ptr& op, + ov::TensorVector& outputs, + const ov::TensorVector& inputs) { + using namespace ov; + + constexpr size_t data_port = 0; + constexpr size_t scales_port = 2; + constexpr size_t axes_port = 3; + constexpr size_t max_num_of_ports = 4; + + element::Type input_et = inputs[0].get_element_type(); + size_t type_size = input_et.size(); + + PartialShape input_shape{inputs[data_port].get_shape()}; + auto m_attrs = op->get_attrs(); + + const auto ta = make_tensor_accessor(inputs); + auto input_shapes = std::vector(); + std::transform(inputs.cbegin(), inputs.cend(), std::back_inserter(input_shapes), [](const ov::Tensor& ht) { + return ht.get_shape(); + }); + + // Use v4 shape inference + const auto output_shape = + ov::op::v4::shape_infer(op.get(), input_shapes, m_attrs.pads_begin, m_attrs.pads_end, ta).front(); + + Shape padded_input_shape; + for (size_t i = 0; i < input_shape.size(); ++i) { + padded_input_shape.emplace_back(m_attrs.pads_begin[i] + m_attrs.pads_end[i] + input_shape[i].get_length()); + } + + auto axes = get_axes_vector(inputs, inputs[data_port].get_shape().size(), axes_port, max_num_of_ports); + auto scales = get_scales_vector(inputs, padded_input_shape, m_attrs, axes, scales_port); + + Shape out_shape = output_shape.to_shape(); + outputs[0].set_shape(out_shape); + + size_t bytes_in_padded_input = shape_size(padded_input_shape) * type_size; + std::vector padded_input_data(bytes_in_padded_input, 0); + + auto data_ptr = static_cast(inputs[0].data()); + uint8_t* padded_data_ptr = padded_input_data.data(); + + reference::pad_input_data(data_ptr, + padded_data_ptr, + type_size, + input_shape.to_shape(), + padded_input_shape, + m_attrs.pads_begin); + + switch (input_et) { + case element::f32: + ov::reference::interpolate(reinterpret_cast(padded_data_ptr), + padded_input_shape, + scales, + axes, + outputs[0].data(), + out_shape, + m_attrs); + break; + case element::f64: + ov::reference::interpolate(reinterpret_cast(padded_data_ptr), + padded_input_shape, + scales, + axes, + outputs[0].data(), + out_shape, + m_attrs); + break; + case element::bf16: + ov::reference::interpolate(reinterpret_cast(padded_data_ptr), + padded_input_shape, + scales, + axes, + outputs[0].data(), + out_shape, + m_attrs); + break; + case element::f16: + ov::reference::interpolate(reinterpret_cast(padded_data_ptr), + padded_input_shape, + scales, + axes, + outputs[0].data(), + out_shape, + m_attrs); + break; + case element::i8: + ov::reference::interpolate(reinterpret_cast(padded_data_ptr), + padded_input_shape, + scales, + axes, + outputs[0].data(), + out_shape, + m_attrs); + break; + case element::u8: + ov::reference::interpolate(reinterpret_cast(padded_data_ptr), + padded_input_shape, + scales, + axes, + outputs[0].data(), + out_shape, + m_attrs); + break; + default: + OPENVINO_THROW("Interpolate operation supports only f32, f64, f16, bf16, i8 and u8 types"); + } + + return true; +} +} // namespace v4 + namespace v11 { bool evaluate_interpolate(const std::shared_ptr& op, ov::TensorVector& outputs, @@ -229,6 +343,15 @@ bool evaluate_interpolate(const std::shared_ptr& op, out_shape, m_attrs); break; + case element::f64: + ov::reference::interpolate(reinterpret_cast(padded_data_ptr), + padded_input_shape, + scales, + axes, + outputs[0].data(), + out_shape, + m_attrs); + break; case element::bf16: ov::reference::interpolate(reinterpret_cast(padded_data_ptr), padded_input_shape, @@ -290,6 +413,57 @@ bool evaluate(const std::shared_ptr& op, return eval::interpolate::v11::evaluate_interpolate(op, outputs, inputs); } +template +bool evaluate(const std::shared_ptr& op, + ov::TensorVector& outputs, + const ov::TensorVector& inputs) { + return eval::interpolate::v4::evaluate_interpolate(op, outputs, inputs); +} + +template <> +bool evaluate_node(std::shared_ptr node, + ov::TensorVector& outputs, + const ov::TensorVector& inputs) { + const auto& element_type = node->get_output_element_type(0); + + switch (element_type) { + case ov::element::boolean: + return evaluate(ov::as_type_ptr(node), outputs, inputs); + case ov::element::bf16: + return evaluate(ov::as_type_ptr(node), outputs, inputs); + case ov::element::f16: + return evaluate(ov::as_type_ptr(node), outputs, inputs); + case ov::element::f64: + return evaluate(ov::as_type_ptr(node), outputs, inputs); + case ov::element::f32: + return evaluate(ov::as_type_ptr(node), outputs, inputs); + case ov::element::i4: + return evaluate(ov::as_type_ptr(node), outputs, inputs); + case ov::element::i8: + return evaluate(ov::as_type_ptr(node), outputs, inputs); + case ov::element::i16: + return evaluate(ov::as_type_ptr(node), outputs, inputs); + case ov::element::i32: + return evaluate(ov::as_type_ptr(node), outputs, inputs); + case ov::element::i64: + return evaluate(ov::as_type_ptr(node), outputs, inputs); + case ov::element::u1: + return evaluate(ov::as_type_ptr(node), outputs, inputs); + case ov::element::u4: + return evaluate(ov::as_type_ptr(node), outputs, inputs); + case ov::element::u8: + return evaluate(ov::as_type_ptr(node), outputs, inputs); + case ov::element::u16: + return evaluate(ov::as_type_ptr(node), outputs, inputs); + case ov::element::u32: + return evaluate(ov::as_type_ptr(node), outputs, inputs); + case ov::element::u64: + return evaluate(ov::as_type_ptr(node), outputs, inputs); + default: + OPENVINO_THROW("Unhandled data type ", element_type, " in evaluate_node()"); + } +} + template <> bool evaluate_node(std::shared_ptr node, ov::TensorVector& outputs, diff --git a/src/plugins/template/backend/ops/ops_evaluates.hpp b/src/plugins/template/backend/ops/ops_evaluates.hpp index 97967c3f6dc7..a186fc4a6d9e 100644 --- a/src/plugins/template/backend/ops/ops_evaluates.hpp +++ b/src/plugins/template/backend/ops/ops_evaluates.hpp @@ -5,6 +5,7 @@ #pragma once #include "evaluate_node.hpp" #include "openvino/op/ops.hpp" +#include "openvino/op/paged_attention.hpp" #include "openvino/op/rms_norm.hpp" #include "ov_ops/augru_cell.hpp" #include "ov_ops/augru_sequence.hpp" @@ -62,6 +63,10 @@ extern template bool evaluate_node(std::shared_ptr(std::shared_ptr node, + ov::TensorVector& outputs, + const ov::TensorVector& inputs); + extern template bool evaluate_node(std::shared_ptr node, ov::TensorVector& outputs, const ov::TensorVector& inputs); @@ -572,6 +577,10 @@ extern template bool evaluate_node(std::shared_ptr(std::shared_ptr node, + ov::TensorVector& outputs, + const ov::TensorVector& inputs); + extern template bool evaluate_node(std::shared_ptr node, ov::TensorVector& outputs, const ov::TensorVector& inputs); diff --git a/src/plugins/template/backend/ops/paged_attention.cpp b/src/plugins/template/backend/ops/paged_attention.cpp new file mode 100644 index 000000000000..dfeb01bc2873 --- /dev/null +++ b/src/plugins/template/backend/ops/paged_attention.cpp @@ -0,0 +1,150 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "openvino/op/paged_attention.hpp" + +#include +#include + +#include "evaluate_node.hpp" +#include "openvino/core/type/element_iterator.hpp" +#include "openvino/reference/paged_attention.hpp" +#include "openvino/reference/utils/paged_cache_manager_helper.hpp" +#include "paged_attention_shape_inference.hpp" +#include "tensor_data_accessor.hpp" + +namespace { + +// Resize outputs using shape_infer with runtime tensors. +// PA keeps some inputs dynamic after validate_and_infer_types, +// so we use actual tensor shapes here. +void resize_pa_outputs(const ov::op::PagedAttentionExtension* op, + ov::TensorVector& outputs, + const ov::TensorVector& inputs) { + std::vector input_shapes; + input_shapes.reserve(inputs.size()); + for (const auto& t : inputs) { + input_shapes.emplace_back(t.get_shape()); + } + const auto out_shapes = ov::op::shape_infer(op, input_shapes, ov::make_tensor_accessor(inputs)); + OPENVINO_ASSERT(out_shapes.size() == outputs.size()); + for (size_t i = 0; i < outputs.size(); ++i) { + if (out_shapes[i].is_static()) { + outputs[i].set_shape(out_shapes[i].to_shape()); + } else { + // Diversity output may stay dynamic -- default to {0} + outputs[i].set_shape(ov::Shape{0}); + } + } +} + +template +bool evaluate(const ov::op::PagedAttentionExtension* pa_op, + ov::TensorVector& outputs, + const ov::TensorVector& inputs, + std::uintptr_t node_key, + ov::reference::paged_attention_cache::PagedCacheManager* cache_manager) { + using T = typename ov::element_type_traits::value_type; + + OPENVINO_ASSERT(inputs.size() == 28, "PagedAttentionExtension: expected 28 inputs, got ", inputs.size()); + OPENVINO_ASSERT(outputs.size() == 3, "PagedAttentionExtension: expected 3 outputs"); + + resize_pa_outputs(pa_op, outputs, inputs); + + // Map "present but empty" inputs to nullptr. These are logically optional + // but must be passed as empty tensors since PA has no optional positional inputs. + const void* alibi_ptr = inputs[11].get_size() > 0 ? inputs[11].data() : nullptr; + const auto alibi_et = alibi_ptr ? inputs[11].get_element_type() : ov::element::dynamic; + const void* trig_lut_ptr = inputs[16].get_size() > 0 ? inputs[16].data() : nullptr; + const auto trig_lut_et = trig_lut_ptr ? inputs[16].get_element_type() : ov::element::dynamic; + const void* xattn_thresh_ptr = inputs[17].get_size() > 0 ? inputs[17].data() : nullptr; + const auto xattn_thresh_et = xattn_thresh_ptr ? inputs[17].get_element_type() : ov::element::dynamic; + const void* sinks_ptr = inputs[20].get_size() > 0 ? inputs[20].data() : nullptr; + const auto sinks_et = sinks_ptr ? inputs[20].get_element_type() : ov::element::dynamic; + const int32_t* arkv_evict_ptr = inputs[22].get_size() > 0 ? inputs[22].data() : nullptr; + const int32_t* arkv_indices_ptr = inputs[23].get_size() > 0 ? inputs[23].data() : nullptr; + const int32_t* arkv_begins_ptr = inputs[24].get_size() > 0 ? inputs[24].data() : nullptr; + const int32_t* token_type_ids_ptr = inputs[25].get_size() > 0 ? inputs[25].data() : nullptr; + const std::size_t token_type_ids_count = inputs[25].get_size(); + + ov::reference::paged_attention(node_key, + cache_manager, + outputs[0].data(), + outputs[1].data(), + outputs[2].data(), + inputs[0].data(), // query + inputs[1].data(), // key + inputs[2].data(), // value + inputs[3].data(), // key_cache init + inputs[4].data(), // value_cache init + inputs[5].data(), // past_lens + inputs[6].data(), // subsequence_begins + inputs[7].data(), // block_indices init + inputs[7].get_size(), + inputs[8].data(), // block_indices_begins init + inputs[8].get_size(), + inputs[9].data(), // scale + inputs[9].get_element_type(), + inputs[10].data(), // sliding_window + alibi_ptr, // alibi_slopes + alibi_et, + inputs[11].get_shape(), + inputs[12].data(), // max_context_len + inputs[13].data(), // score_aggregation_window + inputs[13].get_size(), // score_aggregation_window_count + inputs[14].data(), // rotated_block_indices + inputs[14].get_size(), + inputs[15].data(), // rotation_deltas + inputs[15].get_shape(), + trig_lut_ptr, // rotation_trig_lut + trig_lut_et, + inputs[16].get_shape(), + xattn_thresh_ptr, // xattention_threshold + xattn_thresh_et, + inputs[18].data(), // xattention_block_size + inputs[19].data(), // xattention_stride + sinks_ptr, // sinks + sinks_et, + inputs[21].data(), // adaptive_rkv_start_size + arkv_evict_ptr, // adaptive_rkv_evictable_sizes + arkv_indices_ptr, // adaptive_rkv_diversity_block_set_indices + arkv_begins_ptr, // adaptive_rkv_diversity_block_set_indices_begins + token_type_ids_ptr, // token_type_ids + token_type_ids_count, // token_type_ids_count + inputs[0].get_shape(), + inputs[1].get_shape(), + inputs[2].get_shape(), + inputs[3].get_shape(), + inputs[4].get_shape(), + inputs[5].get_shape(), + inputs[6].get_shape()); + return true; +} + +} // namespace + +template <> +bool evaluate_node(std::shared_ptr node, + ov::TensorVector& outputs, + const ov::TensorVector& inputs) { + const auto& pa = std::static_pointer_cast(node); + auto* cache_manager = ov::reference::paged_attention_cache::get_cache_manager_ptr(pa.get()); + OPENVINO_ASSERT(cache_manager != nullptr, "PagedAttentionExtension: cache manager handle is null"); + + const std::uintptr_t node_key = reinterpret_cast(node.get()); + const auto et = node->get_output_element_type(0); + + switch (et) { + case ov::element::bf16: + return evaluate(pa.get(), outputs, inputs, node_key, cache_manager); + case ov::element::f16: + return evaluate(pa.get(), outputs, inputs, node_key, cache_manager); + case ov::element::f32: + return evaluate(pa.get(), outputs, inputs, node_key, cache_manager); + case ov::element::f64: + return evaluate(pa.get(), outputs, inputs, node_key, cache_manager); + default: + OPENVINO_THROW("PagedAttentionExtension: unsupported element type: ", et); + } +} diff --git a/src/plugins/template/backend/opset_int_tbl.hpp b/src/plugins/template/backend/opset_int_tbl.hpp index f056971e5d43..c10ac0808e67 100644 --- a/src/plugins/template/backend/opset_int_tbl.hpp +++ b/src/plugins/template/backend/opset_int_tbl.hpp @@ -82,6 +82,7 @@ _OPENVINO_OP_REG(ScatterNDUpdate, op::v3) _OPENVINO_OP_REG(ShapeOf, op::v3) _OPENVINO_OP_REG(CTCLoss, op::v4) +_OPENVINO_OP_REG(Interpolate, op::v4) _OPENVINO_OP_REG(LSTMCell, op::v4) _OPENVINO_OP_REG(NonMaxSuppression, op::v4) _OPENVINO_OP_REG(Proposal, op::v4) @@ -189,3 +190,4 @@ _OPENVINO_OP_REG(AUGRUCell, ov::op::internal) _OPENVINO_OP_REG(AUGRUSequence, ov::op::internal) _OPENVINO_OP_REG(RMS, ov::op::internal) _OPENVINO_OP_REG(RMSNorm, ov::op::internal) +_OPENVINO_OP_REG(PagedAttentionExtension, ov::op) diff --git a/src/plugins/template/src/attach_cache_manager_to_paged_attention.hpp b/src/plugins/template/src/attach_cache_manager_to_paged_attention.hpp new file mode 100644 index 000000000000..e9d22506d8e2 --- /dev/null +++ b/src/plugins/template/src/attach_cache_manager_to_paged_attention.hpp @@ -0,0 +1,51 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// +// Attaches a shared PagedCacheManager to every PA node via rt_info. + +#pragma once + +#include "openvino/core/model.hpp" +#include "openvino/op/paged_attention.hpp" +#include "openvino/pass/matcher_pass.hpp" +#include "openvino/pass/pattern/op/wrap_type.hpp" +#include "openvino/reference/utils/paged_cache_manager_helper.hpp" + +namespace ov { +namespace pass { + +class AttachCacheManagerToPagedAttention : public ov::pass::MatcherPass { +public: + OPENVINO_MATCHER_PASS_RTTI("AttachCacheManagerToPagedAttention"); + AttachCacheManagerToPagedAttention() { + using namespace ov::reference::paged_attention_cache; + + auto pa_pattern = pattern::wrap_type(); + + auto shared_handle = std::make_shared(); + auto shared_dtype = std::make_shared(); + + ov::matcher_pass_callback callback = [shared_handle, shared_dtype](pattern::Matcher& m) -> bool { + auto pa = std::dynamic_pointer_cast(m.get_match_root()); + if (!pa || get_cache_manager(pa.get()) != nullptr) + return false; + + if (!*shared_handle) { + *shared_dtype = pa->get_input_element_type(3); + *shared_handle = make_cache_handle(*shared_dtype); + } + + OPENVINO_ASSERT(pa->get_input_element_type(3) == *shared_dtype, + "AttachCacheManagerToPagedAttention: incompatible cache data types"); + + set_cache_manager(pa.get(), *shared_handle); + return true; + }; + + auto m = std::make_shared(pa_pattern, "AttachCacheManagerToPagedAttention"); + register_matcher(m, callback); + } +}; + +} // namespace pass +} // namespace ov diff --git a/src/plugins/template/src/compiled_model.cpp b/src/plugins/template/src/compiled_model.cpp index 0f1010156510..af798b218689 100644 --- a/src/plugins/template/src/compiled_model.cpp +++ b/src/plugins/template/src/compiled_model.cpp @@ -132,7 +132,8 @@ ov::Any ov::template_plugin::CompiledModel::get_property(const std::string& name ov::supported_properties, ov::execution_devices, ov::loaded_from_cache, - ov::optimal_number_of_infer_requests}; + ov::optimal_number_of_infer_requests, + ov::runtime_requirements}; return ro_properties; }; const auto& default_rw_properties = []() { @@ -150,6 +151,9 @@ ov::Any ov::template_plugin::CompiledModel::get_property(const std::string& name } else if (ov::optimal_number_of_infer_requests == name) { unsigned int value = m_cfg.streams; return decltype(ov::optimal_number_of_infer_requests)::value_type(value); + } else if (ov::runtime_requirements == name) { + // A real plugin would encode compilation capabilities. + return std::string(get_template_plugin()->get_runtime_requirements()); } else if (ov::supported_properties == name) { auto ro_properties = default_ro_properties(); auto rw_properties = default_rw_properties(); diff --git a/src/plugins/template/src/plugin.cpp b/src/plugins/template/src/plugin.cpp index 3838e0697e25..454e47f34797 100644 --- a/src/plugins/template/src/plugin.cpp +++ b/src/plugins/template/src/plugin.cpp @@ -7,6 +7,7 @@ #include #include +#include "attach_cache_manager_to_paged_attention.hpp" #include "itt.hpp" #include "openvino/op/ops.hpp" #include "openvino/pass/manager.hpp" @@ -25,6 +26,7 @@ #include "transformations/op_conversions/convert_maxpool_downgrade.hpp" #include "transformations/op_conversions/convert_reduce_to_pooling.hpp" #include "transformations/op_conversions/scaled_dot_product_attention_decomposition.hpp" +#include "transformations/paged_attention/convert_pagedattn_inputs.hpp" namespace { static constexpr const char* wait_executor_name = "TemplateWaitExecutor"; @@ -126,6 +128,30 @@ ov::SoPtr ov::template_plugin::Plugin::get_default_context( void transform_model(const std::shared_ptr& model) { // Perform common optimizations and device-specific transformations ov::pass::Manager passManager("Plugin:Template"); + + // Defaults are overwritten below from the actual PA node's input types (inputs 3/4 for + // cache, input 0 for query). f32 is only used if there is no PA node in the model. + ov::pass::ConvertPagedAttnInputs::KVCacheConfig cacheConfig; + cacheConfig.keyCachePrecision = ov::element::f32; + cacheConfig.valueCachePrecision = ov::element::f32; + cacheConfig.inferencePrecision = ov::element::f32; + for (const auto& op : model->get_ops()) { + if (auto pa = std::dynamic_pointer_cast(op)) { + const auto key_et = pa->get_input_element_type(3); + const auto val_et = pa->get_input_element_type(4); + if (!key_et.is_dynamic()) + cacheConfig.keyCachePrecision = key_et; + if (!val_et.is_dynamic()) + cacheConfig.valueCachePrecision = val_et; + const auto qry_et = pa->get_input_element_type(0); + if (!qry_et.is_dynamic()) + cacheConfig.inferencePrecision = qry_et; + break; + } + } + auto noop_shape = [](const ov::element::Type&, const bool, const size_t, int64_t&, int64_t&) {}; + passManager.register_pass(cacheConfig, noop_shape); + // Example: register CommonOptimizations transformation from transformations library passManager.register_pass(); // Disable some transformations @@ -146,6 +172,9 @@ void transform_model(const std::shared_ptr& model) { // Disabled SDPA transformation, since there is ref SDPA op. pass_config->disable(); + // Attach cache manager to PA nodes for KV cache block management + passManager.register_pass(); + // After `run_passes`, we have the transformed function, where operations match device operations, // and we can create device backend-dependent graph passManager.run_passes(model); @@ -353,7 +382,8 @@ ov::Any ov::template_plugin::Plugin::get_property(const std::string& name, const ov::device::capabilities, ov::device::type, ov::range_for_async_infer_requests, - ov::execution_devices}; + ov::execution_devices, + ov::compatibility_check}; return ro_properties; }; const auto& default_rw_properties = []() { @@ -416,6 +446,18 @@ ov::Any ov::template_plugin::Plugin::get_property(const std::string& name, const return decltype(ov::execution_devices)::value_type{get_device_name()}; } else if (ov::range_for_async_infer_requests == name) { return decltype(ov::range_for_async_infer_requests)::value_type{1, 1, 1}; + } else if (ov::compatibility_check == name) { + if (auto it = arguments.find(ov::runtime_requirements.name()); it != arguments.end()) { + const auto& requirements = it->second.as(); + if (!requirements.empty()) { + if (const auto pos = requirements.find(get_runtime_requirements()); pos == 0) { + return ov::CompatibilityCheck::SUPPORTED; + } else { + return ov::CompatibilityCheck::UNSUPPORTED; + } + } + } + return ov::CompatibilityCheck::NOT_APPLICABLE; } else { return m_cfg.Get(name); } @@ -426,3 +468,9 @@ ov::Any ov::template_plugin::Plugin::get_property(const std::string& name, const static const ov::Version version = {CI_BUILD_NUMBER, "openvino_template_plugin"}; OV_DEFINE_PLUGIN_CREATE_FUNCTION(ov::template_plugin::Plugin, version) // ! [plugin:create_plugin_engine] + +// ! [plugin:get_runtime_requirements] +std::string_view ov::template_plugin::Plugin::get_runtime_requirements() const { + return "TEMPLATE_PLUGIN_CAPABILITIES_v1"; +} +// ! [plugin:get_runtime_requirements] diff --git a/src/plugins/template/src/plugin.hpp b/src/plugins/template/src/plugin.hpp index 9c797794e92b..f9c90d6e4548 100644 --- a/src/plugins/template/src/plugin.hpp +++ b/src/plugins/template/src/plugin.hpp @@ -4,6 +4,8 @@ #pragma once +#include + #include "backend.hpp" #include "compiled_model.hpp" #include "config.hpp" @@ -54,6 +56,7 @@ class Plugin : public ov::IPlugin { private: friend class CompiledModel; friend class InferRequest; + std::string_view get_runtime_requirements() const; std::shared_ptr m_backend; Configuration m_cfg; diff --git a/src/plugins/template/tests/functional/op_reference/interpolate.cpp b/src/plugins/template/tests/functional/op_reference/interpolate.cpp index 7bee6948f380..0549d7d94e14 100644 --- a/src/plugins/template/tests/functional/op_reference/interpolate.cpp +++ b/src/plugins/template/tests/functional/op_reference/interpolate.cpp @@ -1557,6 +1557,8 @@ std::vector generateCombinedParamsForInterpolate_v11() const std::vector> allTypeParamsV11{ generateParamsForInterpolate_bilinear_pil_float(), generateParamsForInterpolate_bicubic_pil_float(), + generateParamsForInterpolate_bilinear_pil_float(), + generateParamsForInterpolate_bicubic_pil_float(), generateParamsForInterpolate_bilinear_pil_float(), generateParamsForInterpolate_bicubic_pil_float(), generateParamsForInterpolate_bilinear_pil_float(), diff --git a/src/plugins/template/tests/functional/op_reference/pad.cpp b/src/plugins/template/tests/functional/op_reference/pad.cpp index 31d06b7fc0fe..7fa425604153 100644 --- a/src/plugins/template/tests/functional/op_reference/pad.cpp +++ b/src/plugins/template/tests/functional/op_reference/pad.cpp @@ -2000,6 +2000,13 @@ std::vector generateParamsTooLarge() { using T = typename element_type_traits::value_type; using T_INT = typename element_type_traits::value_type; std::vector params{ + PadParams(reference_tests::Tensor(ET, {3}, std::vector{1, 2, 3}), + reference_tests::Tensor(ET_INT, {1}, std::vector{0}), + reference_tests::Tensor(ET_INT, {1}, std::vector{-5}), + reference_tests::Tensor(ET, {1}, std::vector{0}), + op::PadMode::CONSTANT, + "pad_negative_output_dim"), + PadParams(reference_tests::Tensor(ET, {2, 2}, std::vector{ @@ -2154,4 +2161,153 @@ INSTANTIATE_TEST_SUITE_P(smoke_Pad_With_Hardcoded_Refs, ReferencePadV1TestNonConstPadsBeginPadsEndPadValParamsOk, testing::ValuesIn(generateCombinedParamsOk()), ReferencePadTest::getTestCaseName); + +std::vector generateStringParams() { + const auto ET = element::Type_t::string; + const auto ET_INT = element::Type_t::i64; + using T = typename element_type_traits::value_type; + using T_INT = typename element_type_traits::value_type; + + return { + // 1-D, pad begin and end with explicit pad value + PadParams(reference_tests::Tensor(ET, {4}, std::vector{"a", "bb", "ccc", "dddd"}), + reference_tests::Tensor(ET_INT, {1}, std::vector{2}), + reference_tests::Tensor(ET_INT, {1}, std::vector{1}), + reference_tests::Tensor(ET, {7}, std::vector{"

", "

", "a", "bb", "ccc", "dddd", "

"}), + op::PadMode::CONSTANT, + reference_tests::Tensor(ET, {}, std::vector{"

"}), + "pad_string_1d_begin_end_explicit_value"), + + // 1-D, explicit empty-string pad value + PadParams(reference_tests::Tensor(ET, {3}, std::vector{"x", "yy", "zzz"}), + reference_tests::Tensor(ET_INT, {1}, std::vector{1}), + reference_tests::Tensor(ET_INT, {1}, std::vector{2}), + reference_tests::Tensor(ET, {6}, std::vector{"", "x", "yy", "zzz", "", ""}), + op::PadMode::CONSTANT, + reference_tests::Tensor(ET, {}, std::vector{""}), + "pad_string_1d_empty_pad_value"), + + // 2-D, pad rows and columns + PadParams(reference_tests::Tensor(ET, {2, 2}, std::vector{"a", "bb", "ccc", "dddd"}), + reference_tests::Tensor(ET_INT, {2}, std::vector{1, 2}), + reference_tests::Tensor(ET_INT, {2}, std::vector{0, 1}), + reference_tests::Tensor( + ET, + {3, 5}, + std::vector{".", ".", ".", ".", ".", ".", ".", "a", "bb", ".", ".", ".", "ccc", "dddd", "."}), + op::PadMode::CONSTANT, + reference_tests::Tensor(ET, {}, std::vector{"."}), + "pad_string_2d_rows_cols"), + + // 1-D, negative pads (crop) + PadParams(reference_tests::Tensor(ET, {5}, std::vector{"a", "bb", "ccc", "dddd", "eeeee"}), + reference_tests::Tensor(ET_INT, {1}, std::vector{-1}), + reference_tests::Tensor(ET_INT, {1}, std::vector{-2}), + reference_tests::Tensor(ET, {2}, std::vector{"bb", "ccc"}), + op::PadMode::CONSTANT, + reference_tests::Tensor(ET, {}, std::vector{""}), + "pad_string_1d_negative_crop"), + + // 1-D, pad only at end + PadParams(reference_tests::Tensor(ET, {3}, std::vector{"x", "yy", "zzz"}), + reference_tests::Tensor(ET_INT, {1}, std::vector{0}), + reference_tests::Tensor(ET_INT, {1}, std::vector{3}), + reference_tests::Tensor(ET, {6}, std::vector{"x", "yy", "zzz", "", "", ""}), + op::PadMode::CONSTANT, + reference_tests::Tensor(ET, {}, std::vector{""}), + "pad_string_1d_end_only"), + + // 1-D, pad both sides with non-empty pad value + PadParams(reference_tests::Tensor(ET, {3}, std::vector{"a", "bb", "ccc"}), + reference_tests::Tensor(ET_INT, {1}, std::vector{1}), + reference_tests::Tensor(ET_INT, {1}, std::vector{2}), + reference_tests::Tensor(ET, {6}, std::vector{"FILL", "a", "bb", "ccc", "FILL", "FILL"}), + op::PadMode::CONSTANT, + reference_tests::Tensor(ET, {}, std::vector{"FILL"}), + "pad_string_1d_both_sides_non_empty"), + + // 2-D, pad rows only (begin and end) + PadParams(reference_tests::Tensor(ET, {2, 3}, std::vector{"a", "b", "c", "d", "e", "f"}), + reference_tests::Tensor(ET_INT, {2}, std::vector{2, 0}), + reference_tests::Tensor(ET_INT, {2}, std::vector{1, 0}), + reference_tests::Tensor( + ET, + {5, 3}, + std::vector{"X", "X", "X", "X", "X", "X", "a", "b", "c", "d", "e", "f", "X", "X", "X"}), + op::PadMode::CONSTANT, + reference_tests::Tensor(ET, {}, std::vector{"X"}), + "pad_string_2d_rows_only"), + + // 2-D, pad columns only + PadParams( + reference_tests::Tensor(ET, {2, 3}, std::vector{"a", "b", "c", "d", "e", "f"}), + reference_tests::Tensor(ET_INT, {2}, std::vector{0, 1}), + reference_tests::Tensor(ET_INT, {2}, std::vector{0, 2}), + reference_tests::Tensor(ET, {2, 6}, std::vector{"", "a", "b", "c", "", "", "", "d", "e", "f", "", ""}), + op::PadMode::CONSTANT, + reference_tests::Tensor(ET, {}, std::vector{""}), + "pad_string_2d_cols_only"), + + // 3-D + // slice 2 = original slice 1 + 1 row of "?" at end for dim1 + PadParams(reference_tests::Tensor(ET, {2, 2, 2}, std::vector{"a", "b", "c", "d", "e", "f", "g", "h"}), + reference_tests::Tensor(ET_INT, {3}, std::vector{1, 0, 0}), + reference_tests::Tensor(ET_INT, {3}, std::vector{0, 1, 0}), + reference_tests::Tensor( + ET, + {3, 3, 2}, + std::vector< + T>{"?", "?", "?", "?", "?", "?", "a", "b", "c", "d", "?", "?", "e", "f", "g", "h", "?", "?"}), + op::PadMode::CONSTANT, + reference_tests::Tensor(ET, {}, std::vector{"?"}), + "pad_string_3d"), + }; +} + +class ReferencePadV12StringTest : public ReferencePadTest { +public: + void SetUp() override { + SKIP_IF_CURRENT_TEST_IS_DISABLED(); + BaseConstSetUp(); + function = commonConstPadsCreateFunction(GetParam()); + } +}; + +TEST_P(ReferencePadV12StringTest, CompareWithRefs) { + Exec(); +} + +INSTANTIATE_TEST_SUITE_P(smoke_Pad_String_With_Hardcoded_Refs, + ReferencePadV12StringTest, + testing::ValuesIn(generateStringParams()), + ReferencePadTest::getTestCaseName); + +// Test that non-CONSTANT pad modes are rejected for element::string at construction/validation time. +TEST(ReferencePadV12StringNegative, NonConstantModeThrows) { + const auto data = std::make_shared(element::string, Shape{4}); + const auto pads_begin = op::v0::Constant::create(element::i64, Shape{1}, {1}); + const auto pads_end = op::v0::Constant::create(element::i64, Shape{1}, {1}); + for (const auto mode : {op::PadMode::EDGE, op::PadMode::REFLECT, op::PadMode::SYMMETRIC}) { + EXPECT_THROW(std::make_shared(data, pads_begin, pads_end, mode), ov::Exception) + << "Expected throw for pad mode " << mode; + } +} + +// Test that the 3-input ctor (omitted pad_value) defaults to empty string for element::string. +TEST(ReferencePadV12StringDefaultPadValue, DefaultsToEmptyString) { + const auto data = std::make_shared(element::string, Shape{3}); + const auto pads_begin = op::v0::Constant::create(element::i64, Shape{1}, {1}); + const auto pads_end = op::v0::Constant::create(element::i64, Shape{1}, {1}); + // Must not throw — default pad value for string should be "" + std::shared_ptr pad; + ASSERT_NO_THROW(pad = std::make_shared(data, pads_begin, pads_end, op::PadMode::CONSTANT)); + // Verify output shape: {3} + 1 + 1 = {5} + EXPECT_EQ(pad->get_output_partial_shape(0), PartialShape({5})); + // Verify the default pad_value constant is an empty string + const auto pad_val_const = ov::as_type_ptr(pad->get_input_node_shared_ptr(3)); + ASSERT_NE(pad_val_const, nullptr); + EXPECT_EQ(pad_val_const->get_element_type(), element::string); + EXPECT_EQ(pad_val_const->cast_vector(), std::vector{""}) + << "Default pad value for string Pad should be empty string"; +} } // namespace diff --git a/src/plugins/template/tests/functional/shared_tests_instances/behavior/ov_executable_network/exec_network_base.cpp b/src/plugins/template/tests/functional/shared_tests_instances/behavior/ov_executable_network/exec_network_base.cpp index 9b86b41d766e..42c76ea713a7 100644 --- a/src/plugins/template/tests/functional/shared_tests_instances/behavior/ov_executable_network/exec_network_base.cpp +++ b/src/plugins/template/tests/functional/shared_tests_instances/behavior/ov_executable_network/exec_network_base.cpp @@ -25,4 +25,6 @@ INSTANTIATE_TEST_SUITE_P(smoke_BehaviorTests, ::testing::ValuesIn(configs)), OVCompiledModelBaseTestOptional::getTestCaseName); +GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(CompiledModelSetType); + } // namespace ov::test::behavior diff --git a/src/plugins/template/tests/functional/shared_tests_instances/behavior/ov_plugin/core_integration.cpp b/src/plugins/template/tests/functional/shared_tests_instances/behavior/ov_plugin/core_integration.cpp index d200f5bca3a0..0cc503a3fc16 100644 --- a/src/plugins/template/tests/functional/shared_tests_instances/behavior/ov_plugin/core_integration.cpp +++ b/src/plugins/template/tests/functional/shared_tests_instances/behavior/ov_plugin/core_integration.cpp @@ -1,5 +1,5 @@ // Copyright (C) 2018-2026 Intel Corporation -// SPDX-License-Identifcorer: Apache-2.0 +// SPDX-License-Identifier: Apache-2.0 // #include @@ -55,3 +55,9 @@ INSTANTIATE_TEST_SUITE_P(smoke_OVClassQueryModelTest, ::testing::Values(ov::test::utils::DEVICE_TEMPLATE)); } // namespace + +namespace ov::test::behavior { +GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(OVClassCompileModelWithCondidateDeviceListContainedMetaPluginTest); +GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(OVClassSeveralDevicesTestCompileModel); +GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(OVClassSeveralDevicesTestQueryModel); +} // namespace ov::test::behavior diff --git a/src/plugins/template/tests/functional/shared_tests_instances/single_layer_tests/gather_nd.cpp b/src/plugins/template/tests/functional/shared_tests_instances/single_layer_tests/gather_nd.cpp new file mode 100644 index 000000000000..d7d45463a152 --- /dev/null +++ b/src/plugins/template/tests/functional/shared_tests_instances/single_layer_tests/gather_nd.cpp @@ -0,0 +1,55 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "openvino/op/gather_nd.hpp" + +#include + +#include "openvino/op/non_zero.hpp" +#include "openvino/op/not_equal.hpp" +#include "openvino/op/transpose.hpp" +#include "shared_test_classes/base/ov_subgraph.hpp" + +namespace ov::test { + +class GatherNDLayerTemplateTest : virtual public SubgraphBaseTest { +protected: + void SetUp() override { + targetDevice = utils::DEVICE_TEMPLATE; + } + + template + static std::shared_ptr make_nonzero_gather_model(PartialShape data_shape, int batch_dims) { + const auto data = std::make_shared(element::i32, data_shape); + // zero constant to compare against + const auto zero = op::v0::Constant::create(element::i32, Shape{1, 1}, {0}); + const auto not_equal = std::make_shared(data, zero); + // NonZero: output shape [2, -1] i64 (data-dependent) + const auto non_zero = std::make_shared(not_equal, element::i64); + // Transpose order [1, 0]: output shape [-1, 2] i64 + const auto order = op::v0::Constant::create(element::i64, Shape{2}, {1, 0}); + const auto transpose = std::make_shared(non_zero, order); + const auto gather = std::make_shared(data, transpose, batch_dims); + return std::make_shared(gather->outputs(), ov::ParameterVector{data}, "gatherND"); + } +}; + +TEST_F(GatherNDLayerTemplateTest, smoke_dynamic_v5_nonzero) { + const auto input_shapes = + std::vector{InputShape{ov::PartialShape({1, -1}), std::vector{ov::Shape{1, 101}}}}; + init_input_shapes(input_shapes); + function = make_nonzero_gather_model(inputDynamicShapes.at(0), 0); + + run(); +} + +TEST_F(GatherNDLayerTemplateTest, smoke_dynamic_v8_nonzero) { + const auto input_shapes = + std::vector{InputShape{ov::PartialShape({1, -1}), std::vector{ov::Shape{1, 99}}}}; + init_input_shapes(input_shapes); + function = make_nonzero_gather_model(inputDynamicShapes.at(0), 0); + + run(); +} +} // namespace ov::test diff --git a/src/plugins/template/tests/functional/shared_tests_instances/single_layer_tests/softmax.cpp b/src/plugins/template/tests/functional/shared_tests_instances/single_layer_tests/softmax.cpp index 851f53426127..dab68e2ab915 100644 --- a/src/plugins/template/tests/functional/shared_tests_instances/single_layer_tests/softmax.cpp +++ b/src/plugins/template/tests/functional/shared_tests_instances/single_layer_tests/softmax.cpp @@ -147,3 +147,9 @@ INSTANTIATE_TEST_SUITE_P(smoke_SoftMax5D_dynamic, SoftMax8LayerTest::getTestCaseName); } // namespace + +// TEST_P(SoftMaxLayerTest, ...) is defined in the shared header but the template +// plugin only instantiates SoftMax8LayerTest. +namespace ov::test::subgraph { +GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(SoftMaxLayerTest); +} // namespace ov::test::subgraph diff --git a/src/plugins/template/tests/functional/specific_properties_test.cpp b/src/plugins/template/tests/functional/specific_properties_test.cpp new file mode 100644 index 000000000000..88732cb74152 --- /dev/null +++ b/src/plugins/template/tests/functional/specific_properties_test.cpp @@ -0,0 +1,91 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include + +#include "common_test_utils/ov_plugin_cache.hpp" +#include "openvino/op/parameter.hpp" +#include "openvino/op/relu.hpp" +#include "openvino/op/result.hpp" +#include "openvino/runtime/properties.hpp" +#include "openvino/util/common_util.hpp" + +namespace ov::test { + +std::shared_ptr make_simple_model() { + auto data = std::make_shared(ov::element::f32, ov::Shape{1, 3}); + auto relu = std::make_shared(data); + auto result = std::make_shared(relu); + return std::make_shared(ov::ResultVector{result}, ov::ParameterVector{data}); +} + +TEST(PropertyTest, CompiledModelExposesRuntimeRequirements) { + auto core = ov::test::utils::PluginCache::get().core("TEMPLATE"); + auto compiled = core->compile_model(make_simple_model(), "TEMPLATE"); + + const auto supported = compiled.get_property(ov::supported_properties); + ASSERT_TRUE(ov::util::contains(supported, ov::runtime_requirements.name())); + + const auto reqs = compiled.get_property(ov::runtime_requirements); + EXPECT_FALSE(reqs.empty()); +} + +TEST(PropertyTest, PluginReportsRequirementsMetForValidRequirements) { + auto core = ov::test::utils::PluginCache::get().core("TEMPLATE"); + auto compiled = core->compile_model(make_simple_model(), "TEMPLATE"); + + const auto reqs = compiled.get_property(ov::runtime_requirements); + ASSERT_FALSE(reqs.empty()); + + const auto compat = + core->get_property("TEMPLATE", ov::compatibility_check, std::make_pair(ov::runtime_requirements.name(), reqs)); + EXPECT_EQ(compat, ov::CompatibilityCheck::SUPPORTED); +} + +TEST(PropertyTest, PluginRejectsModifiedRequirements) { + auto core = ov::test::utils::PluginCache::get().core("TEMPLATE"); + auto compiled = core->compile_model(make_simple_model(), "TEMPLATE"); + + const auto reqs = compiled.get_property(ov::runtime_requirements); + ASSERT_FALSE(reqs.empty()); + + EXPECT_EQ(core->get_property("TEMPLATE", + ov::compatibility_check, + ov::AnyMap{{ov::runtime_requirements.name(), "_tampered"}}), + ov::CompatibilityCheck::UNSUPPORTED); +} + +TEST(PropertyTest, PluginAcceptModifiedRequirements) { + auto core = ov::test::utils::PluginCache::get().core("TEMPLATE"); + auto compiled = core->compile_model(make_simple_model(), "TEMPLATE"); + + const auto reqs = compiled.get_property(ov::runtime_requirements); + ASSERT_FALSE(reqs.empty()); + + std::string tampered = "tampered_" + reqs; + + EXPECT_EQ(core->get_property("TEMPLATE", ov::compatibility_check, {{ov::runtime_requirements.name(), tampered}}), + ov::CompatibilityCheck::UNSUPPORTED); +} + +TEST(PropertyTest, PluginRejectsEmptyRequirements) { + auto core = ov::test::utils::PluginCache::get().core("TEMPLATE"); + + const std::string empty_reqs; + EXPECT_EQ(core->get_property("TEMPLATE", ov::compatibility_check, {{ov::runtime_requirements.name(), empty_reqs}}), + ov::CompatibilityCheck::NOT_APPLICABLE); +} + +TEST(PropertyTest, PluginReturnsNotApplicableWithoutArguments) { + auto core = ov::test::utils::PluginCache::get().core("TEMPLATE"); + const auto compat = core->get_property("TEMPLATE", ov::compatibility_check); + EXPECT_EQ(compat, ov::CompatibilityCheck::NOT_APPLICABLE); +} + +TEST(PropertyTest, CompatibilityCheckListedInSupportedProperties) { + auto core = ov::test::utils::PluginCache::get().core("TEMPLATE"); + const auto supported = core->get_property("TEMPLATE", ov::supported_properties); + EXPECT_TRUE(ov::util::contains(supported, ov::compatibility_check.name())); +} +} // namespace ov::test diff --git a/src/plugins/template/tests/functional/subgraph_reference/stateful_model.cpp b/src/plugins/template/tests/functional/subgraph_reference/stateful_model.cpp index fe84855260f3..317976c5b311 100644 --- a/src/plugins/template/tests/functional/subgraph_reference/stateful_model.cpp +++ b/src/plugins/template/tests/functional/subgraph_reference/stateful_model.cpp @@ -11,3 +11,13 @@ namespace { INSTANTIATE_TEST_SUITE_P(smoke, StatefulModelStateInLoopBody, ::testing::Values(ov::test::utils::DEVICE_TEMPLATE)); } // namespace + +// Other stateful model suites are defined via TEST_P in the shared header but +// not instantiated by the template plugin. +namespace ov::test { +GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(StaticShapeStatefulModel); +GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(StaticShapeTwoStatesModel); +GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(DynamicShapeStatefulModelDefault); +GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(DynamicShapeStatefulModelParam); +GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(DynamicShapeStatefulModelStateAsInp); +} // namespace ov::test diff --git a/src/tests/functional/base_func_tests/include/behavior/ov_infer_request/callback.hpp b/src/tests/functional/base_func_tests/include/behavior/ov_infer_request/callback.hpp index 30f107ee34f0..e2af544a50ab 100644 --- a/src/tests/functional/base_func_tests/include/behavior/ov_infer_request/callback.hpp +++ b/src/tests/functional/base_func_tests/include/behavior/ov_infer_request/callback.hpp @@ -4,7 +4,12 @@ #pragma once +#include +#include +#include #include +#include +#include #include "shared_test_classes/base/ov_behavior_test_utils.hpp" @@ -39,9 +44,7 @@ TEST_P(OVInferRequestCallbackTests, syncInferDoesNotCallCompletionCallback) { ASSERT_FALSE(is_called); } -// test that can wait all callbacks on dtor -// Ticket: 151980 -TEST_P(OVInferRequestCallbackTests, DISABLED_canStartSeveralAsyncInsideCompletionCallbackWithSafeDtor) { +TEST_P(OVInferRequestCallbackTests, canStartSeveralAsyncInsideCompletionCallbackWithSafeDtor) { const int NUM_ITER = 10; struct TestUserData { std::atomic numIter = {0}; @@ -65,7 +68,8 @@ TEST_P(OVInferRequestCallbackTests, DISABLED_canStartSeveralAsyncInsideCompletio auto future = data.promise.get_future(); OV_ASSERT_NO_THROW(req.start_async()); OV_ASSERT_NO_THROW(req.wait()); - future.wait(); + ASSERT_EQ(future.wait_for(std::chrono::seconds(30)), std::future_status::ready) + << "Timed out waiting for callback completion"; auto callbackStatus = future.get(); ASSERT_TRUE(callbackStatus); auto dataNumIter = data.numIter - 1; @@ -122,6 +126,66 @@ TEST_P(OVInferRequestCallbackTests, ImplDoesNotCopyCallback) { OV_ASSERT_NO_THROW(req.wait()); } +TEST_P(OVInferRequestCallbackTests, CallbackStartsNextInferWithSleepWithoutMissingCallbacks) { + ov::InferRequest req; + OV_ASSERT_NO_THROW(req = execNet.create_infer_request()); + OV_ASSERT_NO_THROW(req.get_input_tensor()); + + constexpr size_t callback_iterations = 10; + constexpr auto callback_sleep = std::chrono::milliseconds(50); + constexpr auto test_timeout = std::chrono::seconds(30); + + std::atomic checkout_count{0}; + std::atomic callback_count{0}; + std::atomic callback_start_failures{0}; + std::atomic callback_exception_count{0}; + + std::mutex done_mutex; + std::condition_variable done_cv; + bool done = false; + + OV_ASSERT_NO_THROW(req.set_callback([&](std::exception_ptr ex) { + if (ex != nullptr) { + callback_exception_count.fetch_add(1, std::memory_order_relaxed); + } + + const auto current_callback = callback_count.fetch_add(1, std::memory_order_relaxed) + 1; + + if (current_callback < callback_iterations) { + try { + checkout_count.fetch_add(1, std::memory_order_relaxed); + req.start_async(); + std::this_thread::sleep_for(callback_sleep); + } catch (...) { + callback_start_failures.fetch_add(1, std::memory_order_relaxed); + std::lock_guard lock(done_mutex); + done = true; + done_cv.notify_one(); + } + } else { + std::lock_guard lock(done_mutex); + done = true; + done_cv.notify_one(); + } + })); + + checkout_count.fetch_add(1, std::memory_order_relaxed); + OV_ASSERT_NO_THROW(req.start_async()); + + { + std::unique_lock lock(done_mutex); + ASSERT_TRUE(done_cv.wait_for(lock, test_timeout, [&]() { + return done; + })); + } + + OV_ASSERT_NO_THROW(req.wait()); + EXPECT_EQ(callback_start_failures.load(std::memory_order_relaxed), 0); + EXPECT_EQ(callback_exception_count.load(std::memory_order_relaxed), 0); + EXPECT_EQ(checkout_count.load(std::memory_order_relaxed), callback_iterations); + EXPECT_EQ(callback_count.load(std::memory_order_relaxed), callback_iterations); +} + } // namespace behavior } // namespace test } // namespace ov diff --git a/src/tests/functional/base_func_tests/include/behavior/ov_infer_request/properties_tests.hpp b/src/tests/functional/base_func_tests/include/behavior/ov_infer_request/properties_tests.hpp index e2f04f354b4e..f251da68a806 100644 --- a/src/tests/functional/base_func_tests/include/behavior/ov_infer_request/properties_tests.hpp +++ b/src/tests/functional/base_func_tests/include/behavior/ov_infer_request/properties_tests.hpp @@ -108,7 +108,7 @@ TEST_P(InferRequestPropertiesTest, ReusableCPUStreamsExecutor) { execNet = core->compile_model(function, target_device, config); auto req = execNet.create_infer_request(); if (target_device == ov::test::utils::DEVICE_NPU) { - ASSERT_EQ(1u, ov::threading::executor_manager()->get_executors_number()); + ASSERT_EQ(0u, ov::threading::executor_manager()->get_executors_number()); ASSERT_EQ(0u, ov::threading::executor_manager()->get_idle_cpu_streams_executors_number()); } else if ((target_device == ov::test::utils::DEVICE_AUTO) || (target_device == ov::test::utils::DEVICE_MULTI)) { diff --git a/src/tests/functional/base_func_tests/include/shared_test_classes/base/ov_subgraph.hpp b/src/tests/functional/base_func_tests/include/shared_test_classes/base/ov_subgraph.hpp index 42eec56a4415..ad2929d90ff8 100644 --- a/src/tests/functional/base_func_tests/include/shared_test_classes/base/ov_subgraph.hpp +++ b/src/tests/functional/base_func_tests/include/shared_test_classes/base/ov_subgraph.hpp @@ -8,9 +8,12 @@ #include "common_test_utils/ov_plugin_cache.hpp" #include "functional_test_utils/summary/op_summary.hpp" #include "openvino/core/model.hpp" +#include "openvino/runtime/exec_model_info.hpp" #include "transformations/convert_precision.hpp" #include "functional_test_utils/skip_tests_config.hpp" +#include + namespace ov { namespace test { @@ -128,6 +131,52 @@ inline std::vector static_shapes_to_test_representation(const std::v return result; } +inline void CheckNumberOfNodesWithTypes(std::shared_ptr function, + const std::unordered_set& nodeTypes, + size_t expectedCount) { + ASSERT_NE(nullptr, function); + size_t actualNodeCount = 0; + for (const auto& node : function->get_ops()) { + const auto& rtInfo = node->get_rt_info(); + auto getExecValue = [&rtInfo](const std::string& paramName) -> std::string { + auto it = rtInfo.find(paramName); + OPENVINO_ASSERT(rtInfo.end() != it); + return it->second.as(); + }; + + if (nodeTypes.count(getExecValue(ov::exec_model_info::LAYER_TYPE))) { + actualNodeCount++; + } + } + + std::string nodeTypesStr; + for (const auto& t : nodeTypes) { + nodeTypesStr += t + ","; + } + ASSERT_EQ(expectedCount, actualNodeCount) + << "Unexpected count of the node types '{" << nodeTypesStr << "}'"; +} + +inline void CheckNumberOfNodesWithTypes(const ov::CompiledModel& compiledModel, + const std::unordered_set& nodeTypes, + size_t expectedCount) { + if (!compiledModel) + return; + CheckNumberOfNodesWithTypes(compiledModel.get_runtime_model(), nodeTypes, expectedCount); +} + +inline void CheckNumberOfNodesWithType(const ov::CompiledModel& compiledModel, + const std::string& nodeType, + size_t expectedCount) { + CheckNumberOfNodesWithTypes(compiledModel, {nodeType}, expectedCount); +} + +inline void CheckNumberOfNodesWithType(std::shared_ptr function, + const std::string& nodeType, + size_t expectedCount) { + CheckNumberOfNodesWithTypes(function, {nodeType}, expectedCount); +} + class SubgraphBaseStaticTest : public ov::test::SubgraphBaseTest { public: void run() override { diff --git a/src/tests/functional/base_func_tests/src/base/utils/compare_results.cpp b/src/tests/functional/base_func_tests/src/base/utils/compare_results.cpp index 9bf67d524b3e..4cd149451bd1 100644 --- a/src/tests/functional/base_func_tests/src/base/utils/compare_results.cpp +++ b/src/tests/functional/base_func_tests/src/base/utils/compare_results.cpp @@ -8,6 +8,7 @@ #include "ov_ops/augru_cell.hpp" #include "ov_ops/augru_sequence.hpp" #include "ov_ops/rms.hpp" +#include "openvino/op/paged_attention.hpp" #include "shared_test_classes/base/utils/compare_results.hpp" #include diff --git a/src/tests/functional/base_func_tests/src/base/utils/generate_inputs.cpp b/src/tests/functional/base_func_tests/src/base/utils/generate_inputs.cpp index 0f4f38953a8f..7b5053485a84 100644 --- a/src/tests/functional/base_func_tests/src/base/utils/generate_inputs.cpp +++ b/src/tests/functional/base_func_tests/src/base/utils/generate_inputs.cpp @@ -12,6 +12,8 @@ #include "ov_ops/augru_sequence.hpp" #include "ov_ops/rms.hpp" +#include "openvino/op/paged_attention.hpp" + #include "common_test_utils/ov_tensor_utils.hpp" #include "common_test_utils/data_utils.hpp" diff --git a/src/tests/functional/base_func_tests/src/behavior/compiled_model/import_export.cpp b/src/tests/functional/base_func_tests/src/behavior/compiled_model/import_export.cpp index d78ad17defdd..55e5ffacc01b 100644 --- a/src/tests/functional/base_func_tests/src/behavior/compiled_model/import_export.cpp +++ b/src/tests/functional/base_func_tests/src/behavior/compiled_model/import_export.cpp @@ -1,5 +1,5 @@ // Copyright (C) 2018-2026 Intel Corporation -// SPDX-License-Identifcorer: Apache-2.0 +// SPDX-License-Identifier: Apache-2.0 // #include @@ -785,6 +785,10 @@ TEST_P(OVExecGraphSerializationTest, ExecutionGraph) { ASSERT_TRUE(status) << message; } +GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(OVClassCompiledModelImportExportTestP); +GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(OVCompiledModelGraphUniqueNodeNamesTest); +GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(OVExecGraphSerializationTest); + } // namespace behavior } // namespace test } // namespace ov diff --git a/src/tests/functional/base_func_tests/src/behavior/compiled_model/properties.cpp b/src/tests/functional/base_func_tests/src/behavior/compiled_model/properties.cpp index b653da0b6be9..2e2d7918348d 100644 --- a/src/tests/functional/base_func_tests/src/behavior/compiled_model/properties.cpp +++ b/src/tests/functional/base_func_tests/src/behavior/compiled_model/properties.cpp @@ -34,7 +34,7 @@ std::string OVClassCompiledModelPropertiesTests::getTestCaseName(testing::TestPa std::ostringstream result; result << "targetDevice=" << targetDevice << "_"; if (!properties.empty()) { - result << "properties=" << util::join(util::split(util::to_string(properties), ' '), "_"); + result << "properties=" << util::join(util::split(util::to_string(properties), " "), "_"); } return result.str(); } @@ -62,7 +62,7 @@ std::string OVCompileModelGetExecutionDeviceTests::getTestCaseName(testing::Test std::ostringstream result; result << "device_name=" << target_device << "_"; if (!compileModelProperties.empty()) { - result << "_compileModelProp=" << util::join(util::split(util::to_string(compileModelProperties), ' '), "_"); + result << "_compileModelProp=" << util::join(util::split(util::to_string(compileModelProperties), " "), "_"); } result << "_expectedDevice=" << userConfig.second; return result.str(); @@ -488,7 +488,7 @@ TEST_P(OVClassCompiledModelGetPropertyTest_EXEC_DEVICES, CanGetExecutionDeviceIn TEST_P(OVCompileModelGetExecutionDeviceTests, CanGetExecutionDeviceInfo) { ov::CompiledModel exeNetWork; auto deviceList = core->get_available_devices(); - std::vector expected_devices = util::split(expectedDeviceName, ','); + const auto expected_devices = util::split(expectedDeviceName); std::vector updatedExpectDevices; updatedExpectDevices.assign(expected_devices.begin(), expected_devices.end()); for (auto& iter : compileModelProperties) { @@ -532,6 +532,20 @@ TEST_P(OVClassCompiledModelGetConfigTest, CanCompileModelWithCustomLocale) { setlocale(LC_ALL, prev.c_str()); } +// Shared library definitions - not every plugin instantiates all of them. +GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(OVClassCompiledModelGetPropertyTest_DEVICE_PRIORITY); +GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(OVClassCompiledModelGetPropertyTest_MODEL_PRIORITY); +GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(OVClassCompiledModelSetCorrectConfigTest); +GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(OVClassCompileModelWithCorrectPropertiesTest); +GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(OVCompileModelGetExecutionDeviceTests); +GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(OVCompiledModelPropertiesDefaultSupportedTests); +GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(OVClassCompiledModelEmptyPropertiesTests); +GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(OVClassCompiledModelGetPropertyTest_EXEC_DEVICES); +GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(OVClassCompiledModelGetConfigTest); +GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(OVClassCompiledModelSetIncorrectConfigTest); +GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(OVClassCompiledModelPropertiesDefaultTests); +GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(OVCompiledModelIncorrectDevice); + } // namespace behavior } // namespace test } // namespace ov diff --git a/src/tests/functional/base_func_tests/src/behavior/compiled_model/properties_hetero.cpp b/src/tests/functional/base_func_tests/src/behavior/compiled_model/properties_hetero.cpp index 49640b4f6d43..7e6fdbbb51d2 100644 --- a/src/tests/functional/base_func_tests/src/behavior/compiled_model/properties_hetero.cpp +++ b/src/tests/functional/base_func_tests/src/behavior/compiled_model/properties_hetero.cpp @@ -88,6 +88,10 @@ TEST_P(OVClassHeteroCompiledModelGetMetricTest_EXEC_DEVICES, GetMetricNoThrow) { ASSERT_EQ(expectedTargets, exeTargets); } + +GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(OVClassHeteroCompiledModelGetMetricTest_EXEC_DEVICES); +GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(OVClassHeteroCompiledModelGetMetricTest_TARGET_FALLBACK); +GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(OVClassHeteroCompiledModelGetMetricTest_SUPPORTED_CONFIG_KEYS); } // namespace behavior } // namespace test } // namespace ov diff --git a/src/tests/functional/base_func_tests/src/behavior/ov_infer_request/infer_request_dynamic.cpp b/src/tests/functional/base_func_tests/src/behavior/ov_infer_request/infer_request_dynamic.cpp index d2a46b220ab5..d2d2663185a4 100644 --- a/src/tests/functional/base_func_tests/src/behavior/ov_infer_request/infer_request_dynamic.cpp +++ b/src/tests/functional/base_func_tests/src/behavior/ov_infer_request/infer_request_dynamic.cpp @@ -587,6 +587,9 @@ TEST_P(OVNotSupportRequestDynamicTests, InferDynamicNotSupported) { ov::CompiledModel execNet; ASSERT_THROW((execNet = ie->compile_model(function, target_device, configuration)), ov::Exception); } + +GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(OVNotSupportRequestDynamicTests); + } // namespace behavior } // namespace test } // namespace ov diff --git a/src/tests/functional/base_func_tests/src/behavior/ov_infer_request/io_tensor.cpp b/src/tests/functional/base_func_tests/src/behavior/ov_infer_request/io_tensor.cpp index b9d5556f639f..936e530a02d1 100644 --- a/src/tests/functional/base_func_tests/src/behavior/ov_infer_request/io_tensor.cpp +++ b/src/tests/functional/base_func_tests/src/behavior/ov_infer_request/io_tensor.cpp @@ -528,6 +528,9 @@ TEST_P(OVInferRequestCheckTensorPrecision, getOutputsFromSplitFunctionWithSevera EXPECT_TRUE(compareTensors(tensor1, tensor2)); } +GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(OVInferRequestIOTensorSetPrecisionTest); +GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(OVInferRequestIOTensorTest); + } // namespace behavior } // namespace test } // namespace ov diff --git a/src/tests/functional/base_func_tests/src/behavior/ov_infer_request/perf_counters.cpp b/src/tests/functional/base_func_tests/src/behavior/ov_infer_request/perf_counters.cpp index aa1ed6fabe24..f5c54f2bd1bd 100644 --- a/src/tests/functional/base_func_tests/src/behavior/ov_infer_request/perf_counters.cpp +++ b/src/tests/functional/base_func_tests/src/behavior/ov_infer_request/perf_counters.cpp @@ -54,6 +54,7 @@ TEST_P(OVInferRequestPerfCountersTest, NotEmptyAfterSyncInfer) { TEST_P(OVInferRequestPerfCountersExceptionTest, perfCountWereNotEnabledExceptionTest) { EXPECT_ANY_THROW(req.get_profiling_info()); } +GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(OVInferRequestPerfCountersExceptionTest); TEST_P(OVInferRequestPerfCountersTest, CheckOperationInProfilingInfo) { req = execNet.create_infer_request(); diff --git a/src/tests/functional/base_func_tests/src/behavior/ov_plugin/caching_tests.cpp b/src/tests/functional/base_func_tests/src/behavior/ov_plugin/caching_tests.cpp index 45eaa4e013cc..0a1a0075b8f5 100644 --- a/src/tests/functional/base_func_tests/src/behavior/ov_plugin/caching_tests.cpp +++ b/src/tests/functional/base_func_tests/src/behavior/ov_plugin/caching_tests.cpp @@ -766,7 +766,7 @@ std::string CompiledKernelsCacheTest::getTestCaseName(testing::TestParamInfosecond.model_paths.size(), 2); ASSERT_EQ(*meta.get_model_info().begin()->second.model_paths.begin(), test_model_path_1); ASSERT_EQ(*meta.get_model_info().begin()->second.model_paths.rbegin(), test_model_path); - // check occurence + // check occurrence ASSERT_EQ(meta.get_model_info().begin()->second.this_op_cnt, 2); ASSERT_EQ(meta.get_model_info().begin()->second.total_op_cnt, 3); // max opset version for Convert - 1 @@ -141,7 +141,7 @@ TEST_F(OpCacheUnitTest, update_cache_by_model) { ASSERT_EQ(meta.get_model_info().begin()->first, test_model_name); ASSERT_EQ(meta.get_model_info().begin()->second.model_paths.size(), 1); ASSERT_EQ(*meta.get_model_info().begin()->second.model_paths.begin(), test_model_path_1); - // check occurence + // check occurrence ASSERT_EQ(meta.get_model_info().begin()->second.this_op_cnt, 1); ASSERT_EQ(meta.get_model_info().begin()->second.total_op_cnt, 2); // max opset version for ShapeOf - 3 diff --git a/src/tests/functional/plugin/conformance/test_runner/api_conformance_runner/src/ov_compiled_model/exec_network_base.cpp b/src/tests/functional/plugin/conformance/test_runner/api_conformance_runner/src/ov_compiled_model/exec_network_base.cpp index 050e661fac29..2e0d8c309e36 100644 --- a/src/tests/functional/plugin/conformance/test_runner/api_conformance_runner/src/ov_compiled_model/exec_network_base.cpp +++ b/src/tests/functional/plugin/conformance/test_runner/api_conformance_runner/src/ov_compiled_model/exec_network_base.cpp @@ -6,6 +6,10 @@ #include "ov_api_conformance_helpers.hpp" +namespace ov::test::behavior { +GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(CompiledModelSetType); +} // namespace ov::test::behavior + namespace { using namespace ov::test::behavior; using namespace ov::test::conformance; diff --git a/src/tests/functional/plugin/conformance/test_runner/api_conformance_runner/src/ov_plugin/core_integration.cpp b/src/tests/functional/plugin/conformance/test_runner/api_conformance_runner/src/ov_plugin/core_integration.cpp index ed7ecb6c7f7b..f20ae6beda06 100644 --- a/src/tests/functional/plugin/conformance/test_runner/api_conformance_runner/src/ov_plugin/core_integration.cpp +++ b/src/tests/functional/plugin/conformance/test_runner/api_conformance_runner/src/ov_plugin/core_integration.cpp @@ -12,6 +12,10 @@ using namespace ov::test::behavior; using namespace ov::test::conformance; +GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(OVClassCompileModelWithCondidateDeviceListContainedMetaPluginTest); +GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(OVClassSeveralDevicesTestCompileModel); +GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(OVClassSeveralDevicesTestQueryModel); + namespace { // // OV Class Common tests with diff --git a/src/tests/functional/plugin/shared/CMakeLists.txt b/src/tests/functional/plugin/shared/CMakeLists.txt index 682060c72fb0..6d43f7e9ff84 100644 --- a/src/tests/functional/plugin/shared/CMakeLists.txt +++ b/src/tests/functional/plugin/shared/CMakeLists.txt @@ -61,6 +61,7 @@ ov_add_target( "$" PRIVATE "${OpenVINO_SOURCE_DIR}/src/plugins/template/include" + $/include LINK_LIBRARIES PUBLIC openvino::pugixml diff --git a/src/tests/functional/plugin/shared/include/shared_test_classes/single_op/pad.hpp b/src/tests/functional/plugin/shared/include/shared_test_classes/single_op/pad.hpp index 53e1f9590ecd..d632b2088ce0 100644 --- a/src/tests/functional/plugin/shared/include/shared_test_classes/single_op/pad.hpp +++ b/src/tests/functional/plugin/shared/include/shared_test_classes/single_op/pad.hpp @@ -44,5 +44,23 @@ class Pad12LayerTest : public PadLayerTest { const std::shared_ptr&, ov::op::PadMode) const override; }; + +using PadStringLayerTestParamSet = std::tuple< + std::vector, // padsBegin + std::vector, // padsEnd + std::string, // padValue (CONSTANT mode) + std::vector, // Input shapes + std::string // Target device name +>; + +class PadStringLayerTest : public testing::WithParamInterface, + virtual public ov::test::SubgraphBaseTest { +public: + static std::string getTestCaseName(const testing::TestParamInfo& obj); + +protected: + void SetUp() override; + void generate_inputs(const std::vector& targetInputStaticShapes) override; +}; } // namespace test } // namespace ov diff --git a/src/tests/functional/plugin/shared/include/shared_test_classes/single_op/paged_attention.hpp b/src/tests/functional/plugin/shared/include/shared_test_classes/single_op/paged_attention.hpp new file mode 100644 index 000000000000..503829435a5c --- /dev/null +++ b/src/tests/functional/plugin/shared/include/shared_test_classes/single_op/paged_attention.hpp @@ -0,0 +1,517 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#pragma once + +#include +#include +#include +#include +#include + +#include "shared_test_classes/base/ov_subgraph.hpp" +#include "common_test_utils/test_enums.hpp" + +#include "openvino/op/paged_attention.hpp" +#include "transformations/rt_info/keep_const_precision.hpp" + + +namespace ov { +namespace test { + +using InputShapes = std::vector; +using PagedAttentionTestParams = std::tuple; + +class PagedAttentionLayerTest + : public testing::WithParamInterface, + public ov::test::SubgraphBaseTest { +public: + static std::string getTestCaseName(const testing::TestParamInfo& obj) { + const auto& [inType, + inputShapes, + extendBlockIndices, + enableXattn, + sinkInput, + slidingWindow, + useAlibi, + maxContextLen, + additional_config] = obj.param; + std::ostringstream result; + result << "IS="; + for (const auto& shape : inputShapes) { + result << ov::test::utils::partialShape2str({shape.first}) << "_"; + } + result << "TS="; + for (const auto& shape : inputShapes) { + result << "("; + if (!shape.second.empty()) { + for (const auto& itr : shape.second) { + result << ov::test::utils::vec2str(itr); + } + } + result << ")_"; + } + result << "Prc=" << inType << "_"; + result << "ExtendBlockIndices=" << extendBlockIndices << "_"; + result << "EnableXattn=" << enableXattn << "_"; + result << "SinkInput=" << sinkInput << "_"; + result << "SlidingWindow=" << slidingWindow << "_"; + result << "UseAlibi=" << useAlibi << "_"; + result << "MaxContextLen=" << maxContextLen << "_"; + result << "config=("; + for (const auto& configEntry : additional_config) { + result << configEntry.first << ", " << configEntry.second.as() << "_"; + } + result << ")"; + + return result.str(); + } + +protected: + // Model construction + static std::shared_ptr make_param(const ov::PartialShape& ps, + ov::element::Type et, + const std::string& name) { + auto p = std::make_shared(et, ps); + p->set_friendly_name(name); + p->get_output_tensor(0).set_names({name}); + return p; + } + + std::shared_ptr make_paged_attn_model(ov::element::Type data_type, + bool enable_xattn, + int64_t head_size = 64, + int64_t head_num = 8, + bool use_sink_input = false, + int32_t sliding_window = 0, + bool use_alibi = false, + int32_t max_ctx_len = 1024, + bool use_rotation = false, + int64_t block_size = 32, + int32_t adaptive_rkv_eviction_size = 0, + int32_t score_window_val = -1) { + // PA expects q/k/v as [tokens, features] + auto q = make_param({ov::Dimension::dynamic(), ov::Dimension::dynamic()}, data_type, "q"); + auto k = make_param({ov::Dimension::dynamic(), head_num * head_size}, data_type, "k"); + auto v = make_param({ov::Dimension::dynamic(), head_num * head_size}, data_type, "v"); + + // Cache layout: [num_blocks, num_kv_heads, block_size, head_size] + // Use data_type directly so TEMPLATE can allocate real tensors without running ConvertPagedAttnInputs. + // Mark with keep_const_precision so the CPU's ConvertPrecision pass doesn't insert + // Convert nodes around them — the PA kernel modifies caches in-place across steps. + auto key_cache = make_param({ov::Dimension::dynamic(), head_num, block_size, head_size}, data_type, "key_cache.0"); + auto value_cache = make_param({ov::Dimension::dynamic(), head_num, block_size, head_size}, data_type, "value_cache.0"); + ov::enable_keep_const_precision(key_cache); + ov::enable_keep_const_precision(value_cache); + + auto past_lens = make_param({ov::Dimension::dynamic()}, ov::element::i32, "past_lens"); + auto subseq_begins = make_param({ov::Dimension::dynamic()}, ov::element::i32, "subsequence_begins"); + auto block_indices = make_param({ov::Dimension::dynamic()}, ov::element::i32, "block_indices"); + auto block_indices_begins = make_param({ov::Dimension::dynamic()}, ov::element::i32, "block_indices_begins"); + + // Use typed empty vectors to avoid Constant::create overload ambiguity + const float scale_value = 1.0f / std::sqrt(static_cast(head_size)); + auto scale = std::make_shared(ov::element::f32, ov::Shape{}, std::vector{scale_value}); + auto sliding_windows = std::make_shared(ov::element::i32, ov::Shape{}, std::vector{sliding_window}); + std::shared_ptr alibi_slopes; + if (use_alibi) { + // Generate slopes: 1.0 / 2^i for i = 0..head_num-1 + std::vector slopes(static_cast(head_num)); + for (int64_t i = 0; i < head_num; ++i) { + slopes[i] = 1.0f / static_cast(1 << i); + } + alibi_slopes = std::make_shared(ov::element::f32, + ov::Shape{static_cast(head_num)}, slopes); + } else { + alibi_slopes = std::make_shared(ov::element::f32, ov::Shape{0}, std::vector{}); + } + auto max_context_len = std::make_shared(ov::element::i32, ov::Shape{}, std::vector{max_ctx_len}); + // score_window_val < 0 -> use max_ctx_len (all tokens, positive window); 0 -> disabled + const int32_t effective_score_window = (score_window_val < 0) ? max_ctx_len : score_window_val; + auto score_aggregation_window = std::make_shared(ov::element::i32, ov::Shape{}, std::vector{effective_score_window}); + + std::shared_ptr rotated_block_indices; + std::shared_ptr rotation_deltas; + std::shared_ptr rotation_trig_lut; + if (use_rotation) { + // Rotate block 0 with a simple RoPE-style trig LUT + // We use per-block granularity: rotation_deltas shape [1, 1] (1 block, delta = 0 => LUT row 0) + rotated_block_indices = std::make_shared(ov::element::i32, ov::Shape{1}, std::vector{0}); + + // Per-block delta: single block, single delta pointing to LUT row 0 + rotated_block_indices_ = std::vector{0}; + rotation_deltas = std::make_shared(ov::element::i32, ov::Shape{1, 1}, std::vector{0}); + + // Trig LUT: [1, head_size] with layout [cos_0..cos_{half-1}, sin_0..sin_{half-1}] + // Use a gentle rotation: cos ~= 0.9, sin ~= 0.1 (not unit-circle exact, but valid for testing) + const size_t hs = static_cast(head_size); + const size_t half = hs / 2; + std::vector lut(hs, 0.f); + for (size_t d = 0; d < half; ++d) { + // Use RoPE-like frequencies: theta_d = 1 / 10000^(2d/head_size) + // Apply a rotation of delta_position = 1 + const float theta = 1.f / std::pow(10000.f, 2.f * static_cast(d) / static_cast(hs)); + lut[d] = std::cos(theta); // cos part + lut[half + d] = std::sin(theta); // sin part + } + rotation_trig_lut = std::make_shared(ov::element::f32, ov::Shape{1, hs}, lut); + } else { + rotated_block_indices = std::make_shared(ov::element::i32, ov::Shape{0}, std::vector{0}); + rotation_deltas = std::make_shared(ov::element::i32, ov::Shape{0}, std::vector{0}); + rotation_trig_lut = std::make_shared(ov::element::f32, ov::Shape{0}, std::vector{0}); + } + + std::shared_ptr xattention_threshold; + if (enable_xattn) { + xattention_threshold = std::make_shared(ov::element::f32, ov::Shape{1}, std::vector{0.9f}); + } else { + xattention_threshold = std::make_shared(ov::element::f32, ov::Shape{0}, std::vector{0}); + } + auto xattention_block_size = std::make_shared(ov::element::i32, ov::Shape{}, std::vector{64}); + auto xattention_stride = std::make_shared(ov::element::i32, ov::Shape{}, std::vector{8}); + + // Sink input: [1, H, 1, 1] when enabled, empty when disabled + std::shared_ptr sinks; + if (use_sink_input) { + // Use per-head sink values comparable in magnitude to attention logits + // so the effect is clearly visible: 3.0 + 0.5 * h + const size_t hn = static_cast(head_num); + std::vector sink_data(hn); + for (size_t h = 0; h < hn; ++h) { + sink_data[h] = 3.0f + 0.5f * static_cast(h); + } + sinks = std::make_shared(ov::element::f32, ov::Shape{1, hn, 1, 1}, sink_data); + } else { + sinks = std::make_shared(data_type, ov::Shape{0}, std::vector{}); + } + + // adaptive_rkv inputs + std::shared_ptr adaptive_rkv_start_size; + std::shared_ptr adaptive_rkv_evictable_sizes; + std::shared_ptr adaptive_rkv_diversity_block_set_indices; + std::shared_ptr adaptive_rkv_diversity_block_set_indices_begins; + if (adaptive_rkv_eviction_size > 0) { + // start_size = 0 for simplicity + adaptive_rkv_start_size = std::make_shared( + ov::element::i32, ov::Shape{}, std::vector{0}); + // Single sequence: evictable_sizes = [eviction_size] + adaptive_rkv_evictable_sizes = std::make_shared( + ov::element::i32, ov::Shape{1}, std::vector{adaptive_rkv_eviction_size}); + // block_set_indices: list of block indices in the eviction zone (0..num_eviction_blocks-1) + const int32_t num_eviction_blocks = adaptive_rkv_eviction_size / static_cast(block_size); + std::vector block_set(static_cast(num_eviction_blocks)); + for (int32_t i = 0; i < num_eviction_blocks; ++i) block_set[i] = i; + adaptive_rkv_diversity_block_set_indices = std::make_shared( + ov::element::i32, ov::Shape{static_cast(num_eviction_blocks)}, block_set); + // begins: [0, num_eviction_blocks] for single sequence + adaptive_rkv_diversity_block_set_indices_begins = std::make_shared( + ov::element::i32, ov::Shape{2}, std::vector{0, num_eviction_blocks}); + } else { + adaptive_rkv_start_size = std::make_shared( + ov::element::i32, ov::Shape{}, std::vector{0}); + adaptive_rkv_evictable_sizes = std::make_shared( + ov::element::i32, ov::Shape{0}, std::vector{0}); + adaptive_rkv_diversity_block_set_indices = std::make_shared( + ov::element::i32, ov::Shape{0}, std::vector{0}); + adaptive_rkv_diversity_block_set_indices_begins = std::make_shared( + ov::element::i32, ov::Shape{0}, std::vector{0}); + } + + ov::ParameterVector params = {q,k,v,key_cache,value_cache,past_lens,subseq_begins,block_indices,block_indices_begins}; + auto token_type_ids = std::make_shared( + ov::element::i32, ov::Shape{0}, std::vector{}); + auto qq_bias = std::make_shared( + ov::element::u8, ov::Shape{0}, std::vector{}); + auto qq_bias_begins = std::make_shared( + ov::element::i32, ov::Shape{0}, std::vector{}); + ov::OutputVector pa_inputs = {q,k,v,key_cache,value_cache,past_lens,subseq_begins,block_indices,block_indices_begins, + scale,sliding_windows,alibi_slopes,max_context_len,score_aggregation_window, + rotated_block_indices,rotation_deltas,rotation_trig_lut, + xattention_threshold,xattention_block_size,xattention_stride, + sinks, + adaptive_rkv_start_size,adaptive_rkv_evictable_sizes, + adaptive_rkv_diversity_block_set_indices,adaptive_rkv_diversity_block_set_indices_begins, + token_type_ids, + qq_bias,qq_bias_begins}; + + auto pa = std::make_shared(pa_inputs); + + // Head metadata for ConvertPagedAttnInputs transformation + auto& rt = pa->get_rt_info(); + rt["num_k_heads"] = static_cast(head_num); + rt["k_head_size"] = static_cast(head_size); + rt["num_v_heads"] = static_cast(head_num); + rt["v_head_size"] = static_cast(head_size); + + // Only wire output 2 (diversity) when adaptive RKV is active, + // since CPU does not compute it. + ov::OutputVector model_outputs = {pa->output(0), pa->output(1)}; + if (adaptive_rkv_eviction_size > 0) { + model_outputs.push_back(pa->output(2)); + } + auto model = std::make_shared(model_outputs, params, "pa_vsref"); + return model; + } + + // Input generation like in the CPU plugin + template + static void strided_iota(IT first, size_t n, T value, T stride) { + for (size_t i = 0; i < n; i++) { + const float idx = static_cast(n - 1 - i); + *first++ = static_cast(value + stride * static_cast(idx)); + } + } + + struct StepInputs { + std::map, ov::Tensor> tensors; + }; + + StepInputs make_step_inputs(const std::shared_ptr& model, + const ov::Shape& lbhs, + int step_idx, + bool extendBlockIndices, + ov::Tensor& key_cache, + ov::Tensor& value_cache, + int32_t& past_len_count) { + StepInputs out; + auto params = model->get_parameters(); + + const size_t L = lbhs[0], B = lbhs[1], H = lbhs[2], S = lbhs[3]; + const size_t tokens = L * B; + const size_t feats = H * S; + + auto fill_fp = [&](ov::Tensor& t, float base) { + if (t.get_element_type() == ov::element::f32) + strided_iota(static_cast(t.data()), t.get_size(), base, 0.1f); + else if (t.get_element_type() == ov::element::f16) + strided_iota(static_cast(t.data()), t.get_size(), static_cast(base), ov::float16(0.1f)); + else + strided_iota(static_cast(t.data()), t.get_size(), static_cast(base), ov::bfloat16(0.1f)); + }; + + // q,k,v + { + ov::Tensor tq(params[0]->get_element_type(), {tokens, feats}); + ov::Tensor tk(params[1]->get_element_type(), {tokens, feats}); + ov::Tensor tv(params[2]->get_element_type(), {tokens, feats}); + fill_fp(tq, step_idx + 1.f); + fill_fp(tk, step_idx + 2.f); + fill_fp(tv, step_idx + 3.f); + out.tensors[params[0]] = tq; + out.tensors[params[1]] = tk; + out.tensors[params[2]] = tv; + } + + // cache tensors (already allocated outside) + out.tensors[params[3]] = key_cache; + out.tensors[params[4]] = value_cache; + + + // past_lens/subseq/block tables + const size_t block_size = key_cache.get_shape().at(2); + const size_t batch_seq = B; + + // number of blocks needed PER SEQUENCE (batch item) + const int32_t used_blocks_per_seq = std::max( + 1, + static_cast((static_cast(past_len_count) + static_cast(L) + + static_cast(block_size) - 1) / + static_cast(block_size))); + + OPENVINO_ASSERT(static_cast(used_blocks_per_seq) <= max_blocks_per_seq_, + "PagedAttention test: cache plan too small for current step"); + + const int32_t bi_count_per_seq = + extendBlockIndices ? std::max(2, used_blocks_per_seq) : used_blocks_per_seq; + + ov::Tensor past_lens(ov::element::i32, {batch_seq}); + ov::Tensor subseq(ov::element::i32, {batch_seq + 1}); + ov::Tensor bi_begins(ov::element::i32, {batch_seq + 1}); + ov::Tensor bi(ov::element::i32, {batch_seq * static_cast(bi_count_per_seq)}); + + auto* pl = past_lens.data(); + auto* sb = subseq.data(); + auto* bb = bi_begins.data(); + auto* b = bi.data(); + + for (size_t s = 0; s < batch_seq; ++s) { + pl[s] = (step_idx == 0) ? 0 : past_len_count; + sb[s] = static_cast(s * L); + bb[s] = static_cast(s * static_cast(bi_count_per_seq)); + + const int32_t block_base = static_cast(s * max_blocks_per_seq_); + for (int32_t i = 0; i < bi_count_per_seq; ++i) { + b[bb[s] + i] = (i < used_blocks_per_seq) ? (block_base + i) : -1; + } + } + + sb[batch_seq] = static_cast(tokens); + bb[batch_seq] = static_cast(batch_seq * static_cast(bi_count_per_seq)); + + out.tensors[params[5]] = past_lens; + out.tensors[params[6]] = subseq; + out.tensors[params[7]] = bi; + out.tensors[params[8]] = bi_begins; + + past_len_count += static_cast(L); + + return out; + } + +public: + std::vector> run_device(ov::Core& core, + const std::shared_ptr& model, + const std::string& device, + ov::AnyMap cfg, + bool extendBlockIndices, + const std::vector& steps) { + auto compiled = core.compile_model(model, device, cfg); + auto req = compiled.create_infer_request(); + const size_t num_outputs = compiled.outputs().size(); + + std::vector> outs; + outs.reserve(steps.size()); + + for (size_t si = 0; si < steps.size(); ++si) { + const auto& step = steps[si]; + for (const auto& kv : step.tensors) { + req.set_tensor(kv.first, kv.second); + } + + req.infer(); + + std::vector step_outs; + step_outs.reserve(num_outputs); + for (size_t oi = 0; oi < num_outputs; ++oi) { + auto t = req.get_output_tensor(oi); + ov::Tensor copy(t.get_element_type(), t.get_shape()); + t.copy_to(copy); + step_outs.push_back(std::move(copy)); + } + outs.push_back(std::move(step_outs)); + } + return outs; + } + + void SetUp() override { + is_report_stages = true; + const auto& [inType, inputShapes, extendBlockIndices, enableXattn, sinkInput, slidingWindow, useAlibi, maxContextLen, additional_config] = GetParam(); + + init_input_shapes(inputShapes); + + // Derive H (num_heads) and S (head_size) from the first target shape [L,B,H,S] + OPENVINO_ASSERT(!targetStaticShapes.empty() && targetStaticShapes[0][0].size() == 4, + "PagedAttention test expects [L,B,H,S] shapes"); + const int64_t head_num = static_cast(targetStaticShapes[0][0][2]); + const int64_t head_size = static_cast(targetStaticShapes[0][0][3]); + + // Check if rotation is requested via the additional config map + use_rotation_ = false; + { + auto it = additional_config.find("test_use_rotation"); + if (it != additional_config.end()) { + use_rotation_ = it->second.as(); + } + } + + // Check for adaptive RKV parameters + int64_t cfg_block_size = 32; + int32_t cfg_arkv_eviction_size = 0; + { + auto it = additional_config.find("test_block_size"); + if (it != additional_config.end()) { + cfg_block_size = static_cast(it->second.as()); + } + } + { + auto it = additional_config.find("test_adaptive_rkv_eviction_size"); + if (it != additional_config.end()) { + cfg_arkv_eviction_size = static_cast(it->second.as()); + } + } + + pa_model_ = make_paged_attn_model(inType, enableXattn, head_size, head_num, sinkInput, slidingWindow, useAlibi, maxContextLen, use_rotation_, cfg_block_size, cfg_arkv_eviction_size); + + // Pre-generate the step inputs once, so the CPU/TEMPLATE see identical tensors + // Allocate caches once here, and reuse for both runs by copying initial cache state + { + const auto& kc_ps = pa_model_->get_parameters()[3]->get_partial_shape(); + OPENVINO_ASSERT(kc_ps.rank().is_static() && kc_ps.rank().get_length() == 4, + "PagedAttention test: expected key_cache rank 4"); + OPENVINO_ASSERT(kc_ps[2].is_static(), "PagedAttention test: expected static block_size dimension in cache"); + block_size_ = static_cast(kc_ps[2].get_length()); + + // We treat each batch item as an independent sequence in metadata tensors + // Ensure cache has enough blocks per sequence for the entire multi-step run + batch_size_ = targetStaticShapes.empty() ? 1 : targetStaticShapes[0][0][1]; + max_blocks_per_seq_ = 1; + int32_t past_tmp = 0; + + for (size_t i = 0; i < targetStaticShapes.size(); ++i) { + const auto& lbhs = targetStaticShapes[i][0]; + OPENVINO_ASSERT(lbhs.size() == 4, "PagedAttention test expects [L,B,H,S] shapes"); + const size_t L = lbhs[0]; + const size_t B = lbhs[1]; + if (i == 0) { + batch_size_ = B; + } else { + OPENVINO_ASSERT(B == batch_size_, "PagedAttention test expects stable batch across steps"); + } + + const size_t blocks_now = + std::max(1, (static_cast(past_tmp) + L + block_size_ - 1) / block_size_); + max_blocks_per_seq_ = std::max(max_blocks_per_seq_, blocks_now); + past_tmp += static_cast(L); + } + + allocate_caches_for_model(pa_model_, batch_size_ * max_blocks_per_seq_, inType); + } + + int32_t past = 0; + steps_.clear(); + steps_.reserve(targetStaticShapes.size()); + for (size_t i = 0; i < targetStaticShapes.size(); ++i) { + steps_.push_back(make_step_inputs(pa_model_, targetStaticShapes[i][0], static_cast(i), + extendBlockIndices, key_cache_init_, value_cache_init_, past)); + } + } + + void allocate_caches_for_model(const std::shared_ptr& model, + size_t block_nums, + ov::element::Type inType) { + // Use model parameter shapes directly (not compiled model) for determinism + auto params = model->get_parameters(); + auto kc = params[3]; + auto vc = params[4]; + + auto ps_k = kc->get_partial_shape(); + auto ps_v = vc->get_partial_shape(); + OPENVINO_ASSERT(ps_k.rank().is_static() && ps_k.rank().get_length() == 4); + OPENVINO_ASSERT(ps_v.rank().is_static() && ps_v.rank().get_length() == 4); + + ps_k[0] = static_cast(block_nums); + ps_v[0] = static_cast(block_nums); + + // Allocate caches using the same element type as PA expects + key_cache_init_ = ov::Tensor(inType, ps_k.get_shape()); + value_cache_init_ = ov::Tensor(inType, ps_v.get_shape()); + std::memset(key_cache_init_.data(), 0, key_cache_init_.get_byte_size()); + std::memset(value_cache_init_.data(), 0, value_cache_init_.get_byte_size()); + } + +protected: + size_t block_size_ = 32; + size_t max_blocks_per_seq_ = 1; + size_t batch_size_ = 1; + bool use_rotation_ = false; + + std::shared_ptr pa_model_; + ov::Tensor key_cache_init_; + ov::Tensor value_cache_init_; + std::vector steps_; + std::vector rotated_block_indices_; // stored for debug prints +}; + +} // namespace test +} // namespace ov diff --git a/src/tests/functional/plugin/shared/include/shared_test_classes/single_op/paged_attention_token_type.hpp b/src/tests/functional/plugin/shared/include/shared_test_classes/single_op/paged_attention_token_type.hpp index 5146d3a6a80e..24ef7701fb39 100644 --- a/src/tests/functional/plugin/shared/include/shared_test_classes/single_op/paged_attention_token_type.hpp +++ b/src/tests/functional/plugin/shared/include/shared_test_classes/single_op/paged_attention_token_type.hpp @@ -4,46 +4,28 @@ #pragma once -#include -#include -#include - -#include "common_test_utils/test_enums.hpp" #include "shared_test_classes/base/ov_subgraph.hpp" namespace ov { namespace test { -struct TestData { - std::string name; - std::vector tokenTypes; - std::vector qData; - std::vector kData; - std::vector vData; - std::vector expectedOutput; -}; - using PagedAttnTokenTypeParams = std::tuple; class PagedAttentionTokenTypeTest : public testing::WithParamInterface, virtual public ov::test::SubgraphBaseTest { public: static std::string getTestCaseName(const testing::TestParamInfo& obj); - static std::vector GetTestDataForHeadSize32HeadNum1(); - static std::vector GetTestDataForHeadSize32HeadNum1SlidingWindowSize5(); - void run() override; protected: void SetUp() override; - void TearDown() override; - void RunAndValidate(); + void generate_inputs(const std::vector& targetInputStaticShapes) override; }; } // namespace test diff --git a/src/tests/functional/plugin/shared/include/shared_test_classes/single_op/paged_causal_conv1d.hpp b/src/tests/functional/plugin/shared/include/shared_test_classes/single_op/paged_causal_conv1d.hpp new file mode 100644 index 000000000000..5737b2141895 --- /dev/null +++ b/src/tests/functional/plugin/shared/include/shared_test_classes/single_op/paged_causal_conv1d.hpp @@ -0,0 +1,56 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#pragma once + +#include +#include +#include +#include + +#include "gtest/gtest.h" +#include "shared_test_classes/base/ov_subgraph.hpp" + +namespace ov::test { + +struct PagedCausalConv1DLayerParams { + int32_t hidden_size; + int32_t kernel_size; + bool has_bias; + // Multiple sets of seq_lengths/cache_intervals to exercise dynamic shapes. + // Each inner vector represents one inference iteration with different token counts. + std::vector> seq_lengths_sets; + std::vector> cache_intervals_sets; + ov::element::Type element_type; + std::string target_device; +}; + +class PagedCausalConv1DLayerTest : public testing::WithParamInterface, + virtual public ov::test::SubgraphBaseTest { +public: + static std::string getTestCaseName(const testing::TestParamInfo& obj); + void generate_inputs(const std::vector& targetInputStaticShapes) override; + +protected: + std::vector calculate_refs() override; + std::vector get_plugin_outputs() override; + void compare(const std::vector& expected, const std::vector& actual) override; + void SetUp() override; + +private: + struct IterationData { + std::vector subsequence_begins; + std::vector block_indices; + std::vector block_indices_begins; + std::vector past_lens; + std::vector cache_interval; + }; + + std::map, ov::Tensor> host_inputs; + ov::element::Type data_type; + std::vector m_iteration_data; + size_t m_current_iteration = 0; +}; + +} // namespace ov::test diff --git a/src/tests/functional/plugin/shared/include/shared_test_classes/single_op/paged_gated_delta_net.hpp b/src/tests/functional/plugin/shared/include/shared_test_classes/single_op/paged_gated_delta_net.hpp new file mode 100644 index 000000000000..8fb153bfae49 --- /dev/null +++ b/src/tests/functional/plugin/shared/include/shared_test_classes/single_op/paged_gated_delta_net.hpp @@ -0,0 +1,46 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#pragma once + +#include +#include +#include +#include +#include + +#include "gtest/gtest.h" +#include "shared_test_classes/base/ov_subgraph.hpp" + +namespace ov::test { + +using PagedGatedDeltaNetLayerParams = std::tuple, + std::vector, + ov::element::Type, + std::string>; // qk_heads, v_heads, qk_head_size, v_head_size, + // seq_lengths, cache_intervals, element_type, + // target_device + +class PagedGatedDeltaNetLayerTest : public testing::WithParamInterface, + virtual public ov::test::SubgraphBaseTest { +public: + static std::string getTestCaseName(const testing::TestParamInfo& obj); + void generate_inputs(const std::vector& targetInputStaticShapes) override; + +protected: + std::vector calculate_refs() override; + std::vector get_plugin_outputs() override; + void compare(const std::vector& expected, const std::vector& actual) override; + void SetUp() override; + +private: + std::map, ov::Tensor> host_inputs; + ov::element::Type data_type; +}; + +} // namespace ov::test \ No newline at end of file diff --git a/src/tests/functional/plugin/shared/include/shared_test_classes/subgraph/matmul_transpose_to_reshape.hpp b/src/tests/functional/plugin/shared/include/shared_test_classes/subgraph/matmul_transpose_to_reshape.hpp new file mode 100644 index 000000000000..d45b9d4e3700 --- /dev/null +++ b/src/tests/functional/plugin/shared/include/shared_test_classes/subgraph/matmul_transpose_to_reshape.hpp @@ -0,0 +1,27 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#pragma once + +#include + +#include "shared_test_classes/base/ov_subgraph.hpp" + +namespace ov { +namespace test { + +using MatMulTransposeToReshapeParams = std::tuple; // Target device + +class MatMulTransposeToReshape : public testing::WithParamInterface, + virtual public ov::test::SubgraphBaseStaticTest { +public: + static std::string getTestCaseName(const testing::TestParamInfo& obj); + +protected: + void SetUp() override; +}; + +} // namespace test +} // namespace ov diff --git a/src/tests/functional/plugin/shared/include/single_op_tests/pad.hpp b/src/tests/functional/plugin/shared/include/single_op_tests/pad.hpp index 0edd6b5860c9..d1745d0f4d05 100644 --- a/src/tests/functional/plugin/shared/include/single_op_tests/pad.hpp +++ b/src/tests/functional/plugin/shared/include/single_op_tests/pad.hpp @@ -15,5 +15,9 @@ TEST_P(PadLayerTest, Inference) { TEST_P(Pad12LayerTest, Inference) { run(); } + +TEST_P(PadStringLayerTest, Inference) { + run(); +} } // namespace test } // namespace ov diff --git a/src/tests/functional/plugin/shared/include/single_op_tests/paged_attention.hpp b/src/tests/functional/plugin/shared/include/single_op_tests/paged_attention.hpp new file mode 100644 index 000000000000..d524c4dd1639 --- /dev/null +++ b/src/tests/functional/plugin/shared/include/single_op_tests/paged_attention.hpp @@ -0,0 +1,195 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#pragma once + +#include "shared_test_classes/single_op/paged_attention.hpp" +#include "common_test_utils/common_utils.hpp" +#include "common_test_utils/ov_tensor_utils.hpp" +#include "openvino/runtime/system_conf.hpp" + +namespace ov { +namespace test { + +TEST_P(PagedAttentionLayerTest, Inference) { + const auto& [inType, inputShapes, extendBlockIndices, enableXattn, sinkInput, slidingWindow, useAlibi, maxContextLen, additional_config] = GetParam(); + (void)inputShapes; (void)enableXattn; (void)sinkInput; (void)slidingWindow; (void)useAlibi; (void)maxContextLen; + +#ifdef OPENVINO_ARCH_X86_64 + if (inType == ov::element::f16 && !ov::with_cpu_x86_avx512_core_fp16()) + GTEST_SKIP() << "f16 PA requires AVX512-FP16 hardware"; + if (inType == ov::element::bf16 && !ov::with_cpu_x86_bfloat16()) + GTEST_SKIP() << "bf16 PA requires BF16 hardware"; +#endif + + // Use a single ov::Core to avoid re-registration errors + auto& core_ref = *core; + + // Read per-instantiation comparison thresholds (default: 1e-3 abs, 1e-2 rel) + float test_abs_threshold = 1e-3f; + float test_rel_threshold = 1e-2f; + { + auto it = additional_config.find("test_abs_threshold"); + if (it != additional_config.end()) test_abs_threshold = it->second.as(); + it = additional_config.find("test_rel_threshold"); + if (it != additional_config.end()) test_rel_threshold = it->second.as(); + } + + // Strip test-only keys from CPU config. + // inference_precision=f32 keeps ConvertPrecision from converting f32 Constants + // (scale, alibi) to f16/bf16, which would corrupt PA kernel arithmetic. + ov::AnyMap cpu_cfg = additional_config; + cpu_cfg.erase("test_use_rotation"); + cpu_cfg.erase("test_block_size"); + cpu_cfg.erase("test_adaptive_rkv_eviction_size"); + cpu_cfg.erase("test_abs_threshold"); + cpu_cfg.erase("test_rel_threshold"); + cpu_cfg[ov::hint::inference_precision.name()] = ov::element::f32; + + ov::AnyMap tmpl_cfg; + tmpl_cfg[ov::hint::inference_precision.name()] = ov::element::f32; + // Run CPU with fresh cache copies (the plugin mutates them in-place) + ov::Tensor key_cache_cpu(key_cache_init_.get_element_type(), key_cache_init_.get_shape()); + ov::Tensor value_cache_cpu(value_cache_init_.get_element_type(), value_cache_init_.get_shape()); + key_cache_init_.copy_to(key_cache_cpu); + value_cache_init_.copy_to(value_cache_cpu); + + auto steps_cpu = steps_; + for (auto& s : steps_cpu) { + s.tensors[pa_model_->get_parameters()[3]] = key_cache_cpu; + s.tensors[pa_model_->get_parameters()[4]] = value_cache_cpu; + } + auto cpu_out = run_device(core_ref, pa_model_, ov::test::utils::DEVICE_CPU, cpu_cfg, extendBlockIndices, steps_cpu); + + // Run TEMPLATE with an independent fresh cache copy + ov::Tensor key_cache_tmpl(key_cache_init_.get_element_type(), key_cache_init_.get_shape()); + ov::Tensor value_cache_tmpl(value_cache_init_.get_element_type(), value_cache_init_.get_shape()); + key_cache_init_.copy_to(key_cache_tmpl); + value_cache_init_.copy_to(value_cache_tmpl); + + auto steps_tmpl = steps_; + for (auto& s : steps_tmpl) { + s.tensors[pa_model_->get_parameters()[3]] = key_cache_tmpl; + s.tensors[pa_model_->get_parameters()[4]] = value_cache_tmpl; + } + auto tmpl_out = run_device(core_ref, pa_model_, ov::test::utils::DEVICE_TEMPLATE, tmpl_cfg, extendBlockIndices, steps_tmpl); + + OPENVINO_ASSERT(cpu_out.size() == tmpl_out.size(), "PA verify: step count mismatch"); + for (size_t i = 0; i < cpu_out.size(); ++i) { + const size_t n_outs = cpu_out[i].size(); + OPENVINO_ASSERT(n_outs == tmpl_out[i].size(), "PA verify: output count mismatch at step ", i); + for (size_t oi = 0; oi < n_outs; ++oi) { + const auto& ct = cpu_out[i][oi]; + const auto& tt = tmpl_out[i][oi]; + // Guard against empty tensors + if (ct.get_size() == 0 || tt.get_size() == 0) { + continue; + } + // Output 2 (diversity): only computed by TEMPLATE, skip comparison + if (oi > 1) { + continue; + } + ov::test::utils::compare(tt, ct, test_abs_threshold, test_rel_threshold); + } + } +} + +// Verify that score_aggregation_window=0 produces all-zero output 1. +TEST_P(PagedAttentionLayerTest, ScoreWindowZeroZerosOutput1) { + const auto& [inType, inputShapes, extendBlockIndices, enableXattn, sinkInput, slidingWindow, useAlibi, maxContextLen, additional_config] = GetParam(); + (void)inputShapes; (void)enableXattn; (void)sinkInput; (void)slidingWindow; (void)useAlibi; (void)maxContextLen; + +#ifdef OPENVINO_ARCH_X86_64 + if (inType == ov::element::f16 && !ov::with_cpu_x86_avx512_core_fp16()) + GTEST_SKIP() << "f16 PA requires AVX512-FP16 hardware"; + if (inType == ov::element::bf16 && !ov::with_cpu_x86_bfloat16()) + GTEST_SKIP() << "bf16 PA requires BF16 hardware"; +#endif + + int64_t cfg_block_size = 32; + { + auto it = additional_config.find("test_block_size"); + if (it != additional_config.end()) + cfg_block_size = static_cast(it->second.as()); + } + + OPENVINO_ASSERT(!targetStaticShapes.empty() && targetStaticShapes[0][0].size() == 4, + "PagedAttention test expects [L,B,H,S] shapes"); + const int64_t head_num = static_cast(targetStaticShapes[0][0][2]); + const int64_t head_size = static_cast(targetStaticShapes[0][0][3]); + + // Build model with score_aggregation_window = 0 and output 1 wired + auto model_zero = make_paged_attn_model(inType, /*enable_xattn=*/false, head_size, head_num, + /*use_sink=*/false, /*sliding_window=*/0, /*use_alibi=*/false, + /*max_ctx_len=*/maxContextLen, /*use_rotation=*/false, + cfg_block_size, /*arkv=*/0, /*score_window_val=*/0); + + // Allocate fresh zero-initialized caches for this model + ov::Tensor kc0(inType, key_cache_init_.get_shape()); + ov::Tensor vc0(inType, value_cache_init_.get_shape()); + std::memset(kc0.data(), 0, kc0.get_byte_size()); + std::memset(vc0.data(), 0, vc0.get_byte_size()); + + // Build step inputs against the new model's parameters + int32_t past = 0; + std::vector steps_zero; + steps_zero.reserve(targetStaticShapes.size()); + for (size_t i = 0; i < targetStaticShapes.size(); ++i) { + steps_zero.push_back(make_step_inputs(model_zero, targetStaticShapes[i][0], + static_cast(i), extendBlockIndices, + kc0, vc0, past)); + } + + ov::AnyMap tmpl_cfg; + tmpl_cfg[ov::hint::inference_precision.name()] = ov::element::f32; + + // Strip test-only keys; inference_precision=f32 keeps f32 Constants (scale, alibi) unconverted. + ov::AnyMap cpu_cfg = additional_config; + cpu_cfg.erase("test_use_rotation"); + cpu_cfg.erase("test_block_size"); + cpu_cfg.erase("test_adaptive_rkv_eviction_size"); + cpu_cfg.erase("test_abs_threshold"); + cpu_cfg.erase("test_rel_threshold"); + cpu_cfg[ov::hint::inference_precision.name()] = ov::element::f32; + + ov::Tensor kc_cpu(inType, key_cache_init_.get_shape()); + ov::Tensor vc_cpu(inType, value_cache_init_.get_shape()); + std::memset(kc_cpu.data(), 0, kc_cpu.get_byte_size()); + std::memset(vc_cpu.data(), 0, vc_cpu.get_byte_size()); + auto steps_cpu = steps_zero; + for (auto& s : steps_cpu) { + s.tensors[model_zero->get_parameters()[3]] = kc_cpu; + s.tensors[model_zero->get_parameters()[4]] = vc_cpu; + } + + auto tmpl_out = run_device(*core, model_zero, ov::test::utils::DEVICE_TEMPLATE, + tmpl_cfg, extendBlockIndices, steps_zero); + auto cpu_out = run_device(*core, model_zero, ov::test::utils::DEVICE_CPU, + cpu_cfg, extendBlockIndices, steps_cpu); + + for (size_t step = 0; step < tmpl_out.size(); ++step) { + for (const auto* outs : {&tmpl_out[step], &cpu_out[step]}) { + ASSERT_GE(outs->size(), 2u) << "Expected at least 2 outputs at step " << step; + const auto& scores = (*outs)[1]; + ASSERT_GT(scores.get_size(), 0u) << "score output is empty at step " << step; + // Read scores as f32 regardless of storage type + const auto et = scores.get_element_type(); + for (size_t j = 0; j < scores.get_size(); ++j) { + float v = 0.f; + if (et == ov::element::f32) + v = static_cast(scores.data())[j]; + else if (et == ov::element::f16) + v = static_cast(static_cast(scores.data())[j]); + else if (et == ov::element::bf16) + v = static_cast(static_cast(scores.data())[j]); + EXPECT_EQ(v, 0.f) + << "output 1 not zero at step=" << step << " index=" << j + << " (score_aggregation_window=0)"; + } + } + } +} + +} // namespace test +} // namespace ov diff --git a/src/tests/functional/plugin/shared/include/single_op_tests/paged_gated_delta_net.hpp b/src/tests/functional/plugin/shared/include/single_op_tests/paged_gated_delta_net.hpp new file mode 100644 index 000000000000..4a3011ee3b53 --- /dev/null +++ b/src/tests/functional/plugin/shared/include/single_op_tests/paged_gated_delta_net.hpp @@ -0,0 +1,15 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#pragma once + +#include "shared_test_classes/single_op/paged_gated_delta_net.hpp" + +namespace ov::test { + +TEST_P(PagedGatedDeltaNetLayerTest, Inference) { + run(); +} + +} // namespace ov::test diff --git a/src/tests/functional/plugin/shared/include/subgraph_tests/gated_delta_net.hpp b/src/tests/functional/plugin/shared/include/subgraph_tests/gated_delta_net.hpp index 1ec3cf420b40..ad1d82da21e4 100644 --- a/src/tests/functional/plugin/shared/include/subgraph_tests/gated_delta_net.hpp +++ b/src/tests/functional/plugin/shared/include/subgraph_tests/gated_delta_net.hpp @@ -8,30 +8,14 @@ namespace ov::test { -inline void CheckNumberOfNodesWithType(std::shared_ptr function, - const std::unordered_set& nodeTypes, - size_t expectedCount) { - ASSERT_NE(nullptr, function); - int num_ops = 0; - for (const auto& node : function->get_ordered_ops()) { - const auto& rt_info = node->get_rt_info(); - const auto layer_type = rt_info.find("layerType")->second.as(); - if (nodeTypes.count(layer_type)) { - num_ops++; - } - } - ASSERT_EQ(num_ops, expectedCount); -} - TEST_P(GatedDeltaNet, CompareWithRefs) { SKIP_IF_CURRENT_TEST_IS_DISABLED(); run(); auto function = compiledModel.get_runtime_model(); CheckNumberOfNodesWithType(function, {"GatedDeltaNet"}, 1); CheckNumberOfNodesWithType(function, {"Transpose"}, 0); - CheckNumberOfNodesWithType(function, {"Concat"}, 0); CheckNumberOfNodesWithType(function, {"ReduceSum"}, 0); CheckNumberOfNodesWithType(function, {"Multiply"}, 0); CheckNumberOfNodesWithType(function, {"Divide"}, 0); }; -} // namespace ov::test \ No newline at end of file +} // namespace ov::test diff --git a/src/tests/functional/plugin/shared/include/subgraph_tests/matmul_transpose_to_reshape.hpp b/src/tests/functional/plugin/shared/include/subgraph_tests/matmul_transpose_to_reshape.hpp new file mode 100644 index 000000000000..1a1265fd3582 --- /dev/null +++ b/src/tests/functional/plugin/shared/include/subgraph_tests/matmul_transpose_to_reshape.hpp @@ -0,0 +1,23 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#pragma once + +#include "shared_test_classes/subgraph/matmul_transpose_to_reshape.hpp" + +namespace ov { +namespace test { + +TEST_P(MatMulTransposeToReshape, CompareWithRefs) { + SKIP_IF_CURRENT_TEST_IS_DISABLED(); + run(); + + const auto runtime_model = compiledModel.get_runtime_model(); + CheckNumberOfNodesWithTypes(runtime_model, {"FullyConnected"}, 1); + CheckNumberOfNodesWithTypes(runtime_model, {"Transpose"}, 0); + CheckNumberOfNodesWithTypes(runtime_model, {"Permute"}, 0); +} + +} // namespace test +} // namespace ov diff --git a/src/tests/functional/plugin/shared/include/subgraph_tests/rotary_pos_emb.hpp b/src/tests/functional/plugin/shared/include/subgraph_tests/rotary_pos_emb.hpp index 86d62f0ec6f8..1ba90d6eb30b 100644 --- a/src/tests/functional/plugin/shared/include/subgraph_tests/rotary_pos_emb.hpp +++ b/src/tests/functional/plugin/shared/include/subgraph_tests/rotary_pos_emb.hpp @@ -9,21 +9,6 @@ namespace ov { namespace test { -inline void CheckNumberOfNodesWithType(std::shared_ptr function, - const std::unordered_set& nodeTypes, - size_t expectedCount) { - ASSERT_NE(nullptr, function); - int num_ops = 0; - for (const auto& node : function->get_ordered_ops()) { - const auto& rt_info = node->get_rt_info(); - const auto layer_type = rt_info.find("layerType")->second.as(); - if (nodeTypes.count(layer_type)) { - num_ops++; - } - } - ASSERT_EQ(num_ops, expectedCount); -} - TEST_P(RoPETestFlux, CompareWithRefs) { SKIP_IF_CURRENT_TEST_IS_DISABLED(); run(); diff --git a/src/tests/functional/plugin/shared/src/behavior/compiled_model/model_cache.cpp b/src/tests/functional/plugin/shared/src/behavior/compiled_model/model_cache.cpp index e403cd506cd5..2678c5201ae9 100644 --- a/src/tests/functional/plugin/shared/src/behavior/compiled_model/model_cache.cpp +++ b/src/tests/functional/plugin/shared/src/behavior/compiled_model/model_cache.cpp @@ -3,28 +3,30 @@ // #include "behavior/compiled_model/model_cache.hpp" + #include "common_test_utils/ov_tensor_utils.hpp" #include "common_test_utils/subgraph_builders/read_concat_split_assign.hpp" #include "common_test_utils/subgraph_builders/single_concat_with_constant.hpp" #include "common_test_utils/subgraph_builders/ti_with_lstm_cell.hpp" +#include "common_test_utils/subgraph_builders/weights_decompression_builders.hpp" #include "common_test_utils/test_assertions.hpp" #include "openvino/op/matmul.hpp" #include "openvino/runtime/weightless_properties_utils.hpp" #include "openvino/util/codec_xor.hpp" #include "shared_test_classes/subgraph/weights_decompression_params.hpp" -#include "common_test_utils/subgraph_builders/weights_decompression_builders.hpp" namespace ov { namespace test { namespace behavior { -std::string WeightlessCacheAccuracy::get_test_case_name(const ::testing::TestParamInfo& obj) { +std::string WeightlessCacheAccuracy::get_test_case_name( + const ::testing::TestParamInfo& obj) { std::ostringstream result; result << "use_compile_model_api=" << utils::bool2str(std::get<0>(obj.param)); - result << "_do_encryption=" << utils::bool2str(std::get<1>(obj.param)); - result << "_inference_mode=" << std::get<2>(obj.param); - result << "_model_dtype=" << std::get<3>(obj.param); + result << "_do_encryption=" << utils::bool2str(std::get<1>(obj.param)); + result << "_inference_mode=" << std::get<2>(obj.param); + result << "_model_dtype=" << std::get<3>(obj.param); result << "_config="; for (const auto& [name, value] : std::get<4>(obj.param)) { result << name << "[" << value.as() << "]|"; @@ -50,8 +52,8 @@ void WeightlessCacheAccuracy::TearDown() { std::remove(m_bin_path.c_str()); std::remove(m_cache_path.c_str()); - ov::test::utils::removeFilesWithExt(m_cache_dir, "blob"); - ov::test::utils::removeFilesWithExt(m_cache_dir, "cl_cache"); + ov::test::utils::removeFilesWithExt(m_cache_dir, "blob"); + ov::test::utils::removeFilesWithExt(m_cache_dir, "cl_cache"); ov::test::utils::removeDir(m_cache_dir); } diff --git a/src/tests/functional/plugin/shared/src/execution_graph_tests/disable_lowering_precision.cpp b/src/tests/functional/plugin/shared/src/execution_graph_tests/disable_lowering_precision.cpp index 64f46c81b055..c2c8df8e41cb 100644 --- a/src/tests/functional/plugin/shared/src/execution_graph_tests/disable_lowering_precision.cpp +++ b/src/tests/functional/plugin/shared/src/execution_graph_tests/disable_lowering_precision.cpp @@ -15,7 +15,7 @@ #include "common_test_utils/ov_plugin_cache.hpp" #include "functional_test_utils/skip_tests_config.hpp" #include "execution_graph_tests/disable_lowering_precision.hpp" -#include "transformations/rt_info/disable_fp16_compression.hpp" +#include "transformations/rt_info/disable_precision_conversion.hpp" #include "openvino/op/matmul.hpp" #include "openvino/op/convert.hpp" #include "openvino/op/constant.hpp" @@ -62,7 +62,7 @@ void ExecGraphDisableLoweringPrecision::create_model() { auto matmul = std::make_shared(A, weightConvert); matmul->set_friendly_name("Matmul0"); if (disableLoweringPrecision) - ov::disable_fp16_compression(matmul); + ov::disable_conversion(matmul, ov::element::f16); funcPtr = std::make_shared(matmul->outputs(), ov::ParameterVector{A}, "testModel"); } diff --git a/src/tests/functional/plugin/shared/src/low_precision_transformations/eliminate_fake_quantize_transformation.cpp b/src/tests/functional/plugin/shared/src/low_precision_transformations/eliminate_fake_quantize_transformation.cpp index 5388b4ff5c6d..d857322df836 100644 --- a/src/tests/functional/plugin/shared/src/low_precision_transformations/eliminate_fake_quantize_transformation.cpp +++ b/src/tests/functional/plugin/shared/src/low_precision_transformations/eliminate_fake_quantize_transformation.cpp @@ -61,8 +61,9 @@ TEST_P(EliminateFakeQuantizeTransformation, CompareWithRefImpl) { const auto& nameIt = it.second.find("originalLayersNames"); const auto& fused_name = nameIt->second.as(); - const auto names = ov::util::split(fused_name, ',', true); - for (const auto& name : names) { + const auto names = ov::util::split(fused_name, ","); + for (const auto& not_trimmed_name : names) { + const auto name = std::string(ov::util::trim(not_trimmed_name)); ASSERT_TRUE(absent.find(name) == absent.end()); exist.erase(name); } diff --git a/src/tests/functional/plugin/shared/src/main.cpp b/src/tests/functional/plugin/shared/src/main.cpp index b76e75693f47..46e8bf41d24d 100644 --- a/src/tests/functional/plugin/shared/src/main.cpp +++ b/src/tests/functional/plugin/shared/src/main.cpp @@ -15,6 +15,7 @@ int main(int argc, char *argv[]) { ov::test::utils::is_print_rel_influence_coef = false; bool print_custom_help = false; std::string outputFolderPath("."); + ov::test::utils::OpSummary::setSaveReportTimeout(60); for (int i = 0; i < argc; ++i) { if (std::string(argv[i]) == "--disable_tests_skipping") { ov::test::utils::disable_tests_skipping = true; diff --git a/src/tests/functional/plugin/shared/src/single_op/pad.cpp b/src/tests/functional/plugin/shared/src/single_op/pad.cpp index a1502b7ade11..c9d0cf28881f 100644 --- a/src/tests/functional/plugin/shared/src/single_op/pad.cpp +++ b/src/tests/functional/plugin/shared/src/single_op/pad.cpp @@ -4,6 +4,7 @@ #include "shared_test_classes/single_op/pad.hpp" +#include "common_test_utils/ov_tensor_utils.hpp" #include "openvino/op/parameter.hpp" #include "openvino/op/constant.hpp" #include "openvino/op/result.hpp" @@ -71,5 +72,53 @@ std::shared_ptr Pad12LayerTest::create_pad_op(const std::shared_ptr(data, pads_begin, pads_end, arg_pad_value, pad_mode); } + +std::string PadStringLayerTest::getTestCaseName(const testing::TestParamInfo& obj) { + const auto& [pads_begin, pads_end, pad_value, shapes, target_device] = obj.param; + std::ostringstream result; + result << "IS=("; + for (size_t i = 0lu; i < shapes.size(); i++) { + result << ov::test::utils::partialShape2str({shapes[i].first}) << (i < shapes.size() - 1lu ? "_" : ""); + } + result << ")_TS="; + for (size_t i = 0lu; i < shapes.front().second.size(); i++) { + result << "{"; + for (size_t j = 0lu; j < shapes.size(); j++) { + result << ov::test::utils::vec2str(shapes[j].second[i]) << (j < shapes.size() - 1lu ? "_" : ""); + } + result << "}_"; + } + result << "PadsBegin=" << ov::test::utils::vec2str(pads_begin) << "_"; + result << "PadsEnd=" << ov::test::utils::vec2str(pads_end) << "_"; + result << "Value=" << pad_value << "_"; + result << "TrgDev=" << target_device; + return result.str(); +} + +void PadStringLayerTest::SetUp() { + const auto& [pads_begin, pads_end, pad_value, shapes, _targetDevice] = this->GetParam(); + targetDevice = _targetDevice; + init_input_shapes(shapes); + + auto param = std::make_shared(ov::element::string, inputDynamicShapes.front()); + auto pads_begin_const = std::make_shared(ov::element::i64, ov::Shape{pads_begin.size()}, pads_begin.data()); + auto pads_end_const = std::make_shared(ov::element::i64, ov::Shape{pads_end.size()}, pads_end.data()); + auto pad_value_const = std::make_shared(ov::element::string, ov::Shape{}, std::vector{pad_value}); + + auto pad = std::make_shared(param, pads_begin_const, pads_end_const, pad_value_const, ov::op::PadMode::CONSTANT); + auto result_node = std::make_shared(pad); + function = std::make_shared(result_node, ov::ParameterVector{param}, "pad_string"); +} + +void PadStringLayerTest::generate_inputs(const std::vector& targetInputStaticShapes) { + inputs.clear(); + const auto& funcInputs = function->inputs(); + ov::test::utils::InputGenerateData in_data; + in_data.start_from = 0; + in_data.range = 10; + inputs.insert({funcInputs[0].get_node_shared_ptr(), + utils::create_and_fill_tensor(ov::element::string, targetInputStaticShapes[0], in_data)}); +} + } // namespace test } // namespace ov diff --git a/src/tests/functional/plugin/shared/src/single_op/paged_attention_token_type.cpp b/src/tests/functional/plugin/shared/src/single_op/paged_attention_token_type.cpp index 8ae0bc84030f..776bfc3c73d6 100644 --- a/src/tests/functional/plugin/shared/src/single_op/paged_attention_token_type.cpp +++ b/src/tests/functional/plugin/shared/src/single_op/paged_attention_token_type.cpp @@ -4,53 +4,26 @@ #include "shared_test_classes/single_op/paged_attention_token_type.hpp" -#include +#include +#include #include "common_test_utils/include/common_test_utils/ov_tensor_utils.hpp" #include "common_test_utils/node_builders/constant.hpp" #include "common_test_utils/ov_tensor_utils.hpp" -#include "openvino/core/type/float16.hpp" #include "openvino/op/paged_attention.hpp" #include "openvino/op/parameter.hpp" -#include "shared_test_classes/base/ov_subgraph.hpp" -#include "shared_test_classes/base/utils/ranges.hpp" +#include "openvino/reference/utils/paged_cache_manager_helper.hpp" using namespace ov::op; +#define UNUSED(expr) (void)(expr); + namespace ov { namespace test { namespace helpers { -namespace os { -void set_env(const char* name, const char* value) { -#ifdef _WIN32 - _putenv_s(name, value); -#else - ::setenv(name, value, 1); -#endif -} -void unset_env(const char* name) { -#ifdef _WIN32 - _putenv_s(name, ""); -#else - ::unsetenv(name); -#endif -} -} // namespace os - -static std::vector GetOutputAsFloatVec(const ov::Tensor& tensor) { - std::vector result(tensor.get_size()); - if (tensor.get_element_type() == ov::element::f32) { - auto* p = tensor.data(); - std::copy(p, p + tensor.get_size(), result.begin()); - } else if (tensor.get_element_type() == ov::element::f16) { - auto* p = tensor.data(); - for (size_t i = 0; i < tensor.get_size(); i++) { - result[i] = static_cast(p[i]); - } - } - return result; -} +static constexpr size_t MAX_CONTEXT_LEN = 1024; +static constexpr size_t BLOCK_SIZE = 16; //< Default for standard PA on GPU. static std::shared_ptr MakeParam(const PartialShape& pshape, element::Type element_type, @@ -61,6 +34,55 @@ static std::shared_ptr MakeParam(const PartialShape& psha return param; } +static ov::Tensor GenerateTokenTypeTensor(size_t seq_len) { + ov::Tensor tensor(ov::element::i32, {seq_len}); + auto* token_types = tensor.data(); + std::fill(token_types, token_types + seq_len, 0); + + if (seq_len == 0) { + return tensor; + } + + std::mt19937 generator(static_cast(5489U + seq_len)); + const size_t max_group_count = 4; + const size_t image_group_count = std::uniform_int_distribution(1, max_group_count)(generator); + + std::vector group_sizes(image_group_count, 1); + std::vector gaps(image_group_count + 1, 0); + for (size_t i = 1; i < image_group_count; ++i) { + gaps[i] = 1; + } + + const size_t min_sequence_len = 2 * image_group_count - 1; + OPENVINO_ASSERT(seq_len >= min_sequence_len, + "Sequence length must fit at least one token per image group and separator gap between groups. ", + "seq_len=", + seq_len, + ", image_group_count=", + image_group_count, + ", min_sequence_len=", + min_sequence_len); + + size_t remaining_tokens = seq_len - min_sequence_len; + std::uniform_int_distribution bucket_distribution(0, group_sizes.size() + gaps.size() - 1); + while (remaining_tokens-- > 0) { + const size_t bucket = bucket_distribution(generator); + if (bucket < group_sizes.size()) { + ++group_sizes[bucket]; + } else { + ++gaps[bucket - group_sizes.size()]; + } + } + + size_t token_position = gaps.front(); + for (size_t group_index = 0; group_index < image_group_count; ++group_index) { + std::fill(token_types + token_position, token_types + token_position + group_sizes[group_index], 1); + token_position += group_sizes[group_index] + gaps[group_index + 1]; + } + + return tensor; +} + static std::shared_ptr PrepareModel(ov::element::Type data_type, ov::Dimension::value_type head_size, ov::Dimension::value_type head_num, @@ -68,10 +90,11 @@ static std::shared_ptr PrepareModel(ov::element::Type data_type, auto q = MakeParam(PartialShape{ov::Dimension::dynamic(), head_num * head_size}, data_type, "q"); auto k = MakeParam(PartialShape{ov::Dimension::dynamic(), head_num * head_size}, data_type, "k"); auto v = MakeParam(PartialShape{ov::Dimension::dynamic(), head_num * head_size}, data_type, "v"); + // GPU plugin expects 4-dim cache with concrete element type // key_cache: [num_blocks, num_kv_heads, head_size, block_size] // value_cache: [num_blocks, num_kv_heads, block_size, head_size] - const int64_t block_size = 16; + const int64_t block_size = helpers::BLOCK_SIZE; auto key_cache = MakeParam(PartialShape{ov::Dimension::dynamic(), head_num, head_size, block_size}, data_type, "key_cache.0"); auto value_cache = @@ -87,14 +110,15 @@ static std::shared_ptr PrepareModel(ov::element::Type data_type, auto sliding_window = std::make_shared(ov::element::i32, Shape{}, std::vector{sliding_window_size}); auto alibi_slopes = std::make_shared(ov::element::f32, Shape{0}, std::vector{}); - auto max_context_len = std::make_shared(ov::element::i32, Shape{}, std::vector{1024}); + auto max_context_len = + std::make_shared(ov::element::i32, Shape{}, std::vector{MAX_CONTEXT_LEN}); auto score_aggregation_window = std::make_shared(ov::element::i32, Shape{}, std::vector{0}); auto rotated_block_indices = std::make_shared(ov::element::i32, Shape{0}, std::vector{0}); auto rotation_deltas = std::make_shared(ov::element::i32, Shape{0}, std::vector{0}); auto rotation_trig_lut = std::make_shared(ov::element::f32, Shape{0}, std::vector{0}); auto xattention_threshold = std::make_shared(ov::element::f32, Shape{0}, std::vector{0}); - auto xattention_block_size = std::make_shared(ov::element::i32, Shape{}, std::vector{64}); - auto xattention_stride = std::make_shared(ov::element::i32, Shape{}, std::vector{8}); + auto xattention_block_size = std::make_shared(ov::element::i32, Shape{}, std::vector{0}); + auto xattention_stride = std::make_shared(ov::element::i32, Shape{}, std::vector{0}); auto sinks = std::static_pointer_cast(ov::test::utils::make_constant(data_type, Shape{0})); auto adaptive_rkv_start_size = std::make_shared(ov::element::i32, Shape{}, std::vector{0}); auto adaptive_rkv_evictable_sizes = @@ -156,139 +180,143 @@ static std::shared_ptr PrepareModel(ov::element::Type data_type, paged_attn->get_rt_info()["num_v_heads"] = head_num; paged_attn->get_rt_info()["v_head_size"] = head_size; + // WARNING! Cache manger is needed only for template plugin and is attached + // via transformations. BUT func tests disable all transformations for template plugin, + // so it is needed to attach cache manager manually here... + auto shared_handle = std::make_shared(); + *shared_handle = ov::reference::paged_attention_cache::make_cache_handle(data_type); + + OPENVINO_ASSERT(paged_attn->get_input_element_type(3) == data_type, + "AttachCacheManagerToPagedAttention: incompatible cache data types"); + + ov::reference::paged_attention_cache::set_cache_manager(paged_attn.get(), *shared_handle); + // --- return std::make_shared(OutputVector{paged_attn}, params); } } // namespace helpers std::string PagedAttentionTokenTypeTest::getTestCaseName(const testing::TestParamInfo& obj) { - const auto& [inType, head_size, head_num, sliding_window_size, pattern, device, use_flash_attn_v2] = obj.param; + const auto& [inType, head_size, head_num, sliding_window_size, batch_size, seq_len, device] = obj.param; std::ostringstream result; result << "Prc=" << inType << "_"; result << "HS=" << head_size << "_"; result << "HN=" << head_num << "_"; result << "SW=" << sliding_window_size << "_"; - result << "SQ=" << pattern.tokenTypes.size() << "_"; - result << "Device=" << device << "_"; - result << "FlashAttnV2=" << (use_flash_attn_v2 ? "ON" : "OFF") << "_"; - result << "Name=" << pattern.name; + result << "BS=" << batch_size << "_"; + result << "SQ=" << seq_len << "_"; + result << "Device=" << device; return result.str(); } void PagedAttentionTokenTypeTest::SetUp() { - const auto& [inType, head_size, head_num, sliding_window_size, pattern, device, use_flash_attn_v2] = GetParam(); + const auto& [inType, head_size, head_num, sliding_window_size, batch_size, seq_len, device] = GetParam(); + // CPU plugin can be supported - it uses different key and value cache layout + // plus uses different block size, which was not yet implemented in this + // test class. + ASSERT_EQ(device, ov::test::utils::DEVICE_GPU); + ASSERT_LE(seq_len, helpers::MAX_CONTEXT_LEN); configuration[ov::hint::inference_precision.name()] = ov::element::f32; configuration[ov::hint::kv_cache_precision.name()] = ov::element::f32; - helpers::os::set_env("OV_GPU_COULD_USE_FLASHATTN_V2", use_flash_attn_v2 ? "1" : "0"); targetDevice = device; - function = helpers::PrepareModel(inType, head_size, head_num, sliding_window_size); - compile_model(); -} -void PagedAttentionTokenTypeTest::TearDown() { - helpers::os::unset_env("OV_GPU_COULD_USE_FLASHATTN_V2"); - SubgraphBaseTest::TearDown(); -} + init_input_shapes({InputShape{PartialShape::dynamic(1), {{batch_size * seq_len}}}}); -void PagedAttentionTokenTypeTest::run() { - // This is a workaround to provide test data to the tests, since there is no reference implementation for the paged - // attention at the time of writing the test. The test data is generated by a Python script using PyTorch and is - // stored in a separate file. Once there is a reference implementation available, the test should be updated to use - // it instead of the hardcoded test data. - RunAndValidate(); + function = helpers::PrepareModel(inType, head_size, head_num, sliding_window_size); } -void PagedAttentionTokenTypeTest::RunAndValidate() { - const auto& [inType, head_size, head_num, sliding_window_size, data, device, use_flash_attn_v2] = this->GetParam(); - - const size_t seq_len = data.tokenTypes.size(); - const size_t hidden_dim = head_size * head_num; - - ASSERT_EQ(data.qData.size(), seq_len * hidden_dim); - ASSERT_EQ(data.kData.size(), seq_len * hidden_dim); - ASSERT_EQ(data.vData.size(), seq_len * hidden_dim); - ov::Tensor token_type_tensor(ov::element::i32, {seq_len}); - std::memcpy(token_type_tensor.data(), data.tokenTypes.data(), seq_len * sizeof(int32_t)); - - ASSERT_TRUE(inType == ov::element::f32); - ov::Tensor q_tensor(inType, {seq_len, hidden_dim}); - std::memcpy(q_tensor.data(), data.qData.data(), seq_len * hidden_dim * sizeof(float)); - ov::Tensor k_tensor(inType, {seq_len, hidden_dim}); - std::memcpy(k_tensor.data(), data.kData.data(), seq_len * hidden_dim * sizeof(float)); - ov::Tensor v_tensor(inType, {seq_len, hidden_dim}); - std::memcpy(v_tensor.data(), data.vData.data(), seq_len * hidden_dim * sizeof(float)); - - auto infer_request = compiledModel.create_infer_request(); - - // Create cache tensors with known shapes - const size_t block_size = 16; - const size_t block_nums = 1024 / block_size; - ov::Tensor key_cache_tensor(inType, {block_nums, head_num, head_size, block_size}); - ov::Tensor value_cache_tensor(inType, {block_nums, head_num, block_size, head_size}); - - auto params = function->get_parameters(); +void PagedAttentionTokenTypeTest::generate_inputs(const std::vector& targetInputStaticShapes) { + inputs.clear(); + + const auto& [inType, head_size, head_num, sliding_window_size, batch_size, seq_length, device] = this->GetParam(); + + UNUSED(sliding_window_size); + UNUSED(device); + + OPENVINO_ASSERT(!targetInputStaticShapes.empty() && targetInputStaticShapes[0].size() == 1, + "Expected a single 1-D shape representing total token count"); + OPENVINO_ASSERT(targetInputStaticShapes[0][0] == batch_size * seq_length, + "Unexpected total token count for the configured batch and sequence length"); + const size_t seq_len = seq_length; + const size_t total_tokens = targetInputStaticShapes[0][0]; + const size_t hidden_dim = static_cast(head_size) * static_cast(head_num); + + using ov::test::utils::InputGenerateData; + + ov::Tensor q_tensor = + ov::test::utils::create_and_fill_tensor(inType, {total_tokens, hidden_dim}, InputGenerateData(-1, 2, 32, 1)); + ov::Tensor k_tensor = + ov::test::utils::create_and_fill_tensor(inType, {total_tokens, hidden_dim}, InputGenerateData(-1, 2, 32, 2)); + ov::Tensor v_tensor = + ov::test::utils::create_and_fill_tensor(inType, {total_tokens, hidden_dim}, InputGenerateData(-1, 2, 32, 3)); + + ov::Tensor token_type_tensor(ov::element::i32, {total_tokens}); + auto* token_types = token_type_tensor.data(); + for (size_t batch = 0; batch < batch_size; ++batch) { + ov::Tensor sequence_token_types = helpers::GenerateTokenTypeTensor(seq_len); + std::copy(sequence_token_types.data(), + sequence_token_types.data() + seq_len, + token_types + batch * seq_len); + } - // Prefill: past_lens=0, single sequence - size_t batch_size = 1; - int32_t total_blocks = static_cast((seq_len + block_size - 1) / block_size); + // Cache tensors with known shapes (matching PrepareModel layout). + const size_t block_size = helpers::BLOCK_SIZE; + const size_t max_blocks_per_sequence = (helpers::MAX_CONTEXT_LEN + block_size - 1) / block_size; + const size_t block_nums = batch_size * max_blocks_per_sequence; + ov::Tensor key_cache_tensor = ov::test::utils::create_and_fill_tensor( + inType, + {block_nums, static_cast(head_num), static_cast(head_size), block_size}, + InputGenerateData(-1, 2, 32, 5)); + ov::Tensor value_cache_tensor = ov::test::utils::create_and_fill_tensor( + inType, + {block_nums, static_cast(head_num), block_size, static_cast(head_size)}, + InputGenerateData(-1, 2, 32, 6)); + + const int32_t blocks_per_sequence = static_cast((seq_len + block_size - 1) / block_size); + const int32_t total_blocks = static_cast(batch_size) * blocks_per_sequence; ov::Tensor past_lens(ov::element::i32, {batch_size}); ov::Tensor subsequence_begins(ov::element::i32, {batch_size + 1}); ov::Tensor block_indices(ov::element::i32, {static_cast(total_blocks)}); ov::Tensor block_indices_begins(ov::element::i32, {batch_size + 1}); - past_lens.data()[0] = 0; subsequence_begins.data()[0] = 0; - subsequence_begins.data()[1] = static_cast(seq_len); block_indices_begins.data()[0] = 0; - block_indices_begins.data()[1] = total_blocks; + for (size_t batch = 0; batch < batch_size; ++batch) { + past_lens.data()[batch] = 0; + subsequence_begins.data()[batch + 1] = static_cast((batch + 1) * seq_len); + block_indices_begins.data()[batch + 1] = static_cast(batch + 1) * blocks_per_sequence; + } for (int32_t i = 0; i < total_blocks; i++) { block_indices.data()[i] = i; } - for (auto& param : params) { - auto name = param->get_friendly_name(); + for (const auto& param : function->get_parameters()) { + const auto& name = param->get_friendly_name(); if (name == "q") - infer_request.set_tensor(param, q_tensor); + inputs.insert({param, q_tensor}); else if (name == "k") - infer_request.set_tensor(param, k_tensor); + inputs.insert({param, k_tensor}); else if (name == "v") - infer_request.set_tensor(param, v_tensor); + inputs.insert({param, v_tensor}); else if (name == "key_cache.0") - infer_request.set_tensor(param, key_cache_tensor); + inputs.insert({param, key_cache_tensor}); else if (name == "value_cache.0") - infer_request.set_tensor(param, value_cache_tensor); + inputs.insert({param, value_cache_tensor}); else if (name == "past_lens") - infer_request.set_tensor(param, past_lens); + inputs.insert({param, past_lens}); else if (name == "subsequence_begins") - infer_request.set_tensor(param, subsequence_begins); + inputs.insert({param, subsequence_begins}); else if (name == "block_indices") - infer_request.set_tensor(param, block_indices); + inputs.insert({param, block_indices}); else if (name == "block_indices_begins") - infer_request.set_tensor(param, block_indices_begins); + inputs.insert({param, block_indices_begins}); else if (name == "token_type_ids") - infer_request.set_tensor(param, token_type_tensor); - } - - infer_request.infer(); - - auto output = infer_request.get_output_tensor(0); - ov::Tensor output_copy{output.get_element_type(), output.get_shape()}; - output.copy_to(output_copy); - - const std::vector outputVec = helpers::GetOutputAsFloatVec(output_copy); - - const float tolerance = (inType == ElementType::f16) ? 1e-2f : 1e-5f; - - ASSERT_EQ(outputVec.size(), data.expectedOutput.size()); - - for (size_t i = 0; i < outputVec.size(); i++) { - float diff = std::abs(outputVec[i] - data.expectedOutput[i]); - EXPECT_LE(diff, tolerance) << "Output differs from expected at index " << i << ": got " << outputVec[i] - << ", expected " << data.expectedOutput[i]; + inputs.insert({param, token_type_tensor}); } } - } // namespace test -} // namespace ov \ No newline at end of file +} // namespace ov + +#undef UNUSED \ No newline at end of file diff --git a/src/tests/functional/plugin/shared/src/single_op/paged_causal_conv1d.cpp b/src/tests/functional/plugin/shared/src/single_op/paged_causal_conv1d.cpp new file mode 100644 index 000000000000..60cb462e018b --- /dev/null +++ b/src/tests/functional/plugin/shared/src/single_op/paged_causal_conv1d.cpp @@ -0,0 +1,452 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "shared_test_classes/single_op/paged_causal_conv1d.hpp" + +#include +#include +#include +#include +#include + +#include "common_test_utils/ov_tensor_utils.hpp" +#include "openvino/core/type/bfloat16.hpp" +#include "openvino/op/paged_causal_conv1d.hpp" +#include "openvino/op/parameter.hpp" +#include "openvino/op/result.hpp" +#include "openvino/runtime/remote_context.hpp" +#include "openvino/runtime/remote_tensor.hpp" +#include "openvino/runtime/tensor.hpp" + +namespace { + +struct ShapeConfig { + int32_t tokens; + int32_t num_sequences; + int32_t num_blocks; +}; + +ShapeConfig compute_shape_config(const std::vector& seq_lengths, const std::vector& cache_intervals) { + const int32_t tokens = std::accumulate(seq_lengths.begin(), seq_lengths.end(), 0); + const int32_t num_sequences = static_cast(seq_lengths.size()); + + int32_t num_blocks = 0; + for (size_t i = 0; i < seq_lengths.size(); i++) { + const int32_t past_len = 1 + static_cast(i % 3); + if (cache_intervals[i] == 0) { + num_blocks += 2; + } else { + const int32_t prev_nums = past_len % cache_intervals[i]; + const int32_t write_blocks = (prev_nums + seq_lengths[i] + cache_intervals[i] - 1) / cache_intervals[i]; + num_blocks += 1 + write_blocks; + } + } + return {tokens, num_sequences, num_blocks}; +} + +template +void run_reference(const std::vector& input_embeds, + std::vector& conv_state_table, + const std::vector& conv_weight, + const std::vector& conv_bias, + bool has_bias, + const std::vector& subsequence_begins, + const std::vector& block_indices, + const std::vector& block_indices_begins, + const std::vector& past_lens, + const std::vector& cache_interval, + int32_t hidden_size, + int32_t kernel_size, + std::vector& output) { + const size_t state_stride = static_cast(hidden_size) * kernel_size; + const int32_t num_sequences = static_cast(subsequence_begins.size()) - 1; + const size_t total_tokens = input_embeds.size() / hidden_size; + output.resize(total_tokens * hidden_size); + + std::vector local_state(state_stride); + + for (int32_t s = 0; s < num_sequences; s++) { + const int32_t token_begin = subsequence_begins[s]; + const int32_t token_end = subsequence_begins[s + 1]; + const int32_t blk_begin = block_indices_begins[s]; + const int32_t blk_end = block_indices_begins[s + 1]; + const int32_t block_span = blk_end - blk_begin; + if (block_span <= 1) + continue; + + const int32_t seq_interval = cache_interval[s]; + const int32_t prev_nums = (seq_interval > 0) ? (past_lens[s] % seq_interval) : 0; + const int32_t seq_tokens = token_end - token_begin; + + const int32_t read_block = block_indices[blk_begin]; + for (size_t i = 0; i < state_stride; i++) { + local_state[i] = static_cast(conv_state_table[static_cast(read_block) * state_stride + i]); + } + + for (int32_t t = 0; t < seq_tokens; t++) { + const size_t token_idx = static_cast(token_begin + t); + + for (int32_t h = 0; h < hidden_size; h++) { + float* state_h = local_state.data() + static_cast(h) * kernel_size; + for (int32_t k = 0; k + 1 < kernel_size; k++) { + state_h[k] = state_h[k + 1]; + } + state_h[kernel_size - 1] = static_cast(input_embeds[token_idx * hidden_size + h]); + + const size_t weight_off = static_cast(h) * kernel_size; + float sum = has_bias ? static_cast(conv_bias[h]) : 0.0f; + for (int32_t k = 0; k < kernel_size; k++) { + sum += state_h[k] * static_cast(conv_weight[weight_off + k]); + } + output[token_idx * hidden_size + h] = static_cast(sum); + } + + const int32_t cached_tokens = prev_nums + (t + 1); + const bool interval_hit = (seq_interval > 0) && ((cached_tokens % seq_interval) == 0); + const bool is_last_token = (t == seq_tokens - 1); + if (interval_hit || is_last_token) { + const int32_t slot = (seq_interval > 0) ? (1 + (cached_tokens - 1) / seq_interval) : 1; + if (slot < block_span) { + const int32_t phys_block = block_indices[blk_begin + slot]; + for (size_t i = 0; i < state_stride; i++) { + conv_state_table[static_cast(phys_block) * state_stride + i] = + static_cast(local_state[i]); + } + } + } + } + } +} + +template +std::vector tensor_to_vector(const ov::Tensor& tensor) { + const auto* ptr = tensor.data(); + return std::vector(ptr, ptr + tensor.get_size()); +} + +ov::Tensor make_i32_tensor(const std::vector& values) { + ov::Tensor tensor(ov::element::i32, ov::Shape{values.size()}); + std::copy(values.begin(), values.end(), tensor.data()); + return tensor; +} + +template +std::vector calculate_typed_refs(const std::map, ov::Tensor>& host_inputs, + const std::shared_ptr& function, + int32_t hidden_size, + int32_t kernel_size, + bool has_bias, + const ov::element::Type& data_type) { + const auto& params = function->get_parameters(); + + auto input_embeds = tensor_to_vector(host_inputs.at(params[0])); + auto state = tensor_to_vector(host_inputs.at(params[1])); + auto weight = tensor_to_vector(host_inputs.at(params[2])); + auto bias = tensor_to_vector(host_inputs.at(params[3])); + auto subsequence_begins = tensor_to_vector(host_inputs.at(params[4])); + auto block_indices = tensor_to_vector(host_inputs.at(params[5])); + auto block_indices_begins = tensor_to_vector(host_inputs.at(params[6])); + auto past_lens = tensor_to_vector(host_inputs.at(params[7])); + auto cache_interval = tensor_to_vector(host_inputs.at(params[8])); + + std::vector ref_output; + run_reference(input_embeds, + state, + weight, + bias, + has_bias, + subsequence_begins, + block_indices, + block_indices_begins, + past_lens, + cache_interval, + hidden_size, + kernel_size, + ref_output); + + ov::Tensor output_tensor(data_type, host_inputs.at(params[0]).get_shape()); + std::copy(ref_output.begin(), ref_output.end(), output_tensor.data()); + + ov::Tensor state_tensor(data_type, host_inputs.at(params[1]).get_shape()); + std::copy(state.begin(), state.end(), state_tensor.data()); + + return {output_tensor, state_tensor}; +} + +} // namespace + +namespace ov::test { + +std::string PagedCausalConv1DLayerTest::getTestCaseName( + const testing::TestParamInfo& obj) { + const auto& p = obj.param; + std::ostringstream result; + result << "Hidden=" << p.hidden_size; + result << "_Kernel=" << p.kernel_size; + result << "_Bias=" << (p.has_bias ? "yes" : "no"); + for (size_t s = 0; s < p.seq_lengths_sets.size(); s++) { + result << "_SeqSet" << s << "="; + for (size_t i = 0; i < p.seq_lengths_sets[s].size(); i++) { + if (i > 0) + result << "x"; + result << p.seq_lengths_sets[s][i]; + } + } + for (size_t s = 0; s < p.cache_intervals_sets.size(); s++) { + result << "_IntSet" << s << "="; + for (size_t i = 0; i < p.cache_intervals_sets[s].size(); i++) { + if (i > 0) + result << "x"; + result << p.cache_intervals_sets[s][i]; + } + } + result << "_Type=" << p.element_type; + result << "_Target=" << p.target_device; + return result.str(); +} + +void PagedCausalConv1DLayerTest::SetUp() { + const auto& p = GetParam(); + targetDevice = p.target_device; + data_type = p.element_type; + configuration[ov::hint::inference_precision.name()] = p.element_type; + + const size_t num_iters = p.seq_lengths_sets.size(); + OPENVINO_ASSERT(num_iters > 0); + OPENVINO_ASSERT(num_iters == p.cache_intervals_sets.size()); + + m_iteration_data.resize(num_iters); + m_current_iteration = 0; + + const auto hidden = static_cast(p.hidden_size); + const auto kernel = static_cast(p.kernel_size); + const ov::Shape weight_shape{hidden, 1, kernel}; + const ov::Shape bias_shape = p.has_bias ? ov::Shape{hidden} : ov::Shape{0}; + + // Build per-iteration target shapes and metadata + std::vector embeds_targets; + std::vector state_targets; + std::vector subseq_targets; + std::vector blocks_targets; + std::vector block_begins_targets; + std::vector past_lens_targets; + std::vector interval_targets; + + for (size_t iter = 0; iter < num_iters; iter++) { + const auto& sl = p.seq_lengths_sets[iter]; + const auto& ci = p.cache_intervals_sets[iter]; + OPENVINO_ASSERT(sl.size() == ci.size()); + + const auto cfg = compute_shape_config(sl, ci); + + embeds_targets.push_back({static_cast(cfg.tokens), hidden}); + state_targets.push_back({static_cast(cfg.num_blocks), hidden, kernel}); + subseq_targets.push_back({static_cast(cfg.num_sequences + 1)}); + blocks_targets.push_back({static_cast(cfg.num_blocks)}); + block_begins_targets.push_back({static_cast(cfg.num_sequences + 1)}); + past_lens_targets.push_back({static_cast(cfg.num_sequences)}); + interval_targets.push_back({static_cast(cfg.num_sequences)}); + + // Precompute metadata for generate_inputs + auto& data = m_iteration_data[iter]; + const int32_t num_sequences = static_cast(sl.size()); + + data.subsequence_begins.clear(); + data.block_indices.clear(); + data.block_indices_begins.clear(); + data.past_lens.clear(); + data.cache_interval.clear(); + + data.subsequence_begins.push_back(0); + data.block_indices_begins.push_back(0); + + int32_t total_blocks = 0; + for (int32_t seq = 0; seq < num_sequences; seq++) { + const int32_t seq_len = sl[seq]; + const int32_t seq_interval = ci[seq]; + const int32_t seq_past_len = 1 + (seq % 3); + + data.subsequence_begins.push_back(data.subsequence_begins.back() + seq_len); + data.past_lens.push_back(seq_past_len); + data.cache_interval.push_back(seq_interval); + + int32_t required_slots = 2; + if (seq_interval > 0) { + const int32_t prev_nums = seq_past_len % seq_interval; + const int32_t write_blocks = (prev_nums + seq_len + seq_interval - 1) / seq_interval; + required_slots = 1 + write_blocks; + } + for (int32_t i = 0; i < required_slots; i++) { + data.block_indices.push_back(total_blocks + i); + } + total_blocks += required_slots; + data.block_indices_begins.push_back(total_blocks); + } + } + + // Use dynamic partial shapes for dimensions that vary across iterations + init_input_shapes({ + InputShape{ov::PartialShape{-1, static_cast(hidden)}, embeds_targets}, + InputShape{ov::PartialShape{-1, static_cast(hidden), static_cast(kernel)}, state_targets}, + InputShape{ov::PartialShape{static_cast(hidden), 1, static_cast(kernel)}, {weight_shape}}, + InputShape{ov::PartialShape{static_cast(p.has_bias ? p.hidden_size : 0)}, {bias_shape}}, + InputShape{ov::PartialShape{-1}, subseq_targets}, + InputShape{ov::PartialShape{-1}, blocks_targets}, + InputShape{ov::PartialShape{-1}, block_begins_targets}, + InputShape{ov::PartialShape{-1}, past_lens_targets}, + InputShape{ov::PartialShape{-1}, interval_targets}, + }); + + // Build model with dynamic shapes + auto p_embeds = std::make_shared(data_type, inputDynamicShapes[0]); + auto p_state = std::make_shared(data_type, inputDynamicShapes[1]); + auto p_weight = std::make_shared(data_type, inputDynamicShapes[2]); + auto p_bias = std::make_shared(data_type, inputDynamicShapes[3]); + auto p_subseq = std::make_shared(ov::element::i32, inputDynamicShapes[4]); + auto p_blocks = std::make_shared(ov::element::i32, inputDynamicShapes[5]); + auto p_block_begins = std::make_shared(ov::element::i32, inputDynamicShapes[6]); + auto p_past_lens = std::make_shared(ov::element::i32, inputDynamicShapes[7]); + auto p_cache_interval = std::make_shared(ov::element::i32, inputDynamicShapes[8]); + + auto conv1d = std::make_shared(p_embeds, + p_state, + p_weight, + p_bias, + p_subseq, + p_blocks, + p_block_begins, + p_past_lens, + p_cache_interval); + + function = std::make_shared(ov::ResultVector{std::make_shared(conv1d)}, + ov::ParameterVector{p_embeds, + p_state, + p_weight, + p_bias, + p_subseq, + p_blocks, + p_block_begins, + p_past_lens, + p_cache_interval}); +} + +void PagedCausalConv1DLayerTest::generate_inputs(const std::vector& targetInputStaticShapes) { + inputs.clear(); + host_inputs.clear(); + + const auto& p = GetParam(); + const size_t iter = m_current_iteration; + m_current_iteration = (m_current_iteration + 1) % m_iteration_data.size(); + const auto& iter_data = m_iteration_data[iter]; + + const auto& params = function->get_parameters(); + const bool use_remote_tensors = targetDevice == "GPU"; + ov::RemoteContext remote_context; + if (use_remote_tensors) { + remote_context = compiledModel.get_context(); + } + + for (size_t i = 0; i < params.size(); i++) { + const auto& param = params[i]; + const auto& shape = targetInputStaticShapes[i]; + ov::Tensor tensor; + + if (i <= 1) { + // input_embeds and conv_state_table + tensor = ov::test::utils::create_and_fill_tensor(param->get_element_type(), + shape, + ov::test::utils::InputGenerateData(-1, 1, 1000, 1)); + } else if (i == 2) { + // conv_weight + tensor = ov::test::utils::create_and_fill_tensor(param->get_element_type(), + shape, + ov::test::utils::InputGenerateData(-1, 1, 1000, 1)); + } else if (i == 3) { + // conv_bias + if (p.has_bias) { + tensor = ov::test::utils::create_and_fill_tensor(param->get_element_type(), + shape, + ov::test::utils::InputGenerateData(-1, 1, 1000, 1)); + } else { + tensor = ov::Tensor(param->get_element_type(), shape); + } + } else if (i == 4) { + tensor = make_i32_tensor(iter_data.subsequence_begins); + } else if (i == 5) { + tensor = make_i32_tensor(iter_data.block_indices); + } else if (i == 6) { + tensor = make_i32_tensor(iter_data.block_indices_begins); + } else if (i == 7) { + tensor = make_i32_tensor(iter_data.past_lens); + } else if (i == 8) { + tensor = make_i32_tensor(iter_data.cache_interval); + } + + host_inputs[param] = tensor; + + if (use_remote_tensors && i <= 3) { + auto remote_tensor = remote_context.create_tensor(param->get_element_type(), shape); + remote_tensor.copy_from(tensor); + inputs[param] = remote_tensor; + } else { + inputs[param] = tensor; + } + } +} + +std::vector PagedCausalConv1DLayerTest::calculate_refs() { + const auto& p = GetParam(); + + if (data_type == ov::element::f16) { + return calculate_typed_refs(host_inputs, + function, + p.hidden_size, + p.kernel_size, + p.has_bias, + data_type); + } + + if (data_type == ov::element::bf16) { + return calculate_typed_refs(host_inputs, + function, + p.hidden_size, + p.kernel_size, + p.has_bias, + data_type); + } + + return calculate_typed_refs(host_inputs, function, p.hidden_size, p.kernel_size, p.has_bias, data_type); +} + +std::vector PagedCausalConv1DLayerTest::get_plugin_outputs() { + auto outputs = SubgraphBaseTest::get_plugin_outputs(); + + // Read back the state table (input 1) which was modified in-place + const auto& state_param = function->get_parameters().at(1); + const auto actual_state_tensor = inferRequest.get_tensor(state_param); + ov::Tensor host_state_tensor(actual_state_tensor.get_element_type(), actual_state_tensor.get_shape()); + actual_state_tensor.copy_to(host_state_tensor); + outputs.push_back(host_state_tensor); + + return outputs; +} + +void PagedCausalConv1DLayerTest::compare(const std::vector& expected, + const std::vector& actual) { + ASSERT_EQ(expected.size(), actual.size()); + if (data_type == ov::element::bf16) { + abs_threshold = 1e-2f; + rel_threshold = 1e-2f; + } else if (data_type == ov::element::f16) { + abs_threshold = 1e-3f; + rel_threshold = 1e-3f; + } else { + abs_threshold = 1e-5f; + rel_threshold = 1e-5f; + } + ov::test::utils::compare(expected[0], actual[0], abs_threshold, rel_threshold); + ov::test::utils::compare(expected[1], actual[1], abs_threshold, rel_threshold); +} + +} // namespace ov::test diff --git a/src/tests/functional/plugin/shared/src/single_op/paged_gated_delta_net.cpp b/src/tests/functional/plugin/shared/src/single_op/paged_gated_delta_net.cpp new file mode 100644 index 000000000000..b9522e2982d9 --- /dev/null +++ b/src/tests/functional/plugin/shared/src/single_op/paged_gated_delta_net.cpp @@ -0,0 +1,506 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "shared_test_classes/single_op/paged_gated_delta_net.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "common_test_utils/ov_tensor_utils.hpp" +#include "openvino/core/type/bfloat16.hpp" +#include "openvino/op/paged_gated_delta_net.hpp" +#include "openvino/op/parameter.hpp" +#include "openvino/op/result.hpp" +#include "openvino/runtime/remote_context.hpp" +#include "openvino/runtime/remote_tensor.hpp" +#include "openvino/runtime/tensor.hpp" + +namespace { + +template +void normalize_and_scale(const T* src, size_t n, float scale, std::vector& dst) { + dst.resize(n); + float sum = 0.0f; + for (size_t i = 0; i < n; i++) { + const float value = static_cast(src[i]); + dst[i] = value; + sum += value * value; + } + const float inv = 1.0f / std::sqrt(sum + 1e-6f); + for (size_t i = 0; i < n; i++) { + dst[i] *= inv * scale; + } +} + +template +void run_reference(const std::vector& query, + const std::vector& key, + const std::vector& value, + const std::vector& gate, + const std::vector& beta, + std::vector& recurrent_state_table, + const std::vector& subsequence_begins, + const std::vector& block_indices, + const std::vector& block_indices_begins, + const std::vector& past_lens, + const std::vector& cache_interval, + int32_t qk_heads, + int32_t v_heads, + int32_t qk_head_size, + int32_t v_head_size, + std::vector& output) { + const int32_t tokens = static_cast(query.size()) / (qk_heads * qk_head_size); + output.resize(static_cast(tokens) * v_heads * v_head_size); + + const auto state_off = [=](int32_t block, int32_t h, int32_t k_idx, int32_t v_idx) { + return ((block * v_heads + h) * v_head_size + v_idx) * qk_head_size + k_idx; + }; + + const float attn_scale = 1.0f / std::sqrt(static_cast(qk_head_size)); + const int32_t num_sequences = static_cast(subsequence_begins.size()) - 1; + const int32_t group_size = v_heads / qk_heads; + + OPENVINO_ASSERT(static_cast(recurrent_state_table.size()) % (v_heads * qk_head_size * v_head_size) == 0, + "State table size is inconsistent with inferred qk/v head sizes"); + + for (int32_t seq = 0; seq < num_sequences; seq++) { + const int32_t token_begin = subsequence_begins[seq]; + const int32_t token_end = subsequence_begins[seq + 1]; + const int32_t block_begin = block_indices_begins[seq]; + const int32_t block_end = block_indices_begins[seq + 1]; + const int32_t seq_blocks = std::max(block_end - block_begin, 0); + const int32_t past_len = past_lens[seq]; + const int32_t interval = cache_interval[seq]; + const int32_t prev_nums = (interval > 0) ? (past_len % interval) : 0; + + for (int32_t h = 0; h < v_heads; h++) { + const int32_t hk = h / group_size; + std::vector state(static_cast(qk_head_size) * v_head_size, 0.0f); + + const int32_t block_id = block_indices[block_begin]; + + for (int32_t k_idx = 0; k_idx < qk_head_size; k_idx++) { + for (int32_t v_idx = 0; v_idx < v_head_size; v_idx++) { + state[k_idx * v_head_size + v_idx] = + static_cast(recurrent_state_table[state_off(block_id, h, k_idx, v_idx)]); + } + } + + for (int32_t token = token_begin; token < token_end; token++) { + const auto q_ptr = query.data() + (token * qk_heads + hk) * qk_head_size; + const auto k_ptr = key.data() + (token * qk_heads + hk) * qk_head_size; + + std::vector q_norm; + std::vector k_norm; + normalize_and_scale(q_ptr, qk_head_size, attn_scale, q_norm); + normalize_and_scale(k_ptr, qk_head_size, 1.0f, k_norm); + + const float b_g = std::exp(static_cast(gate[token * v_heads + h])); + const float b_beta = static_cast(beta[token * v_heads + h]); + + for (int32_t v_idx = 0; v_idx < v_head_size; v_idx++) { + const float b_v = static_cast(value[(token * v_heads + h) * v_head_size + v_idx]); + + float h_k = 0.0f; + for (int32_t k_idx = 0; k_idx < qk_head_size; k_idx++) { + auto& s = state[k_idx * v_head_size + v_idx]; + s *= b_g; + h_k += s * k_norm[k_idx]; + } + + const float update = (b_v - h_k) * b_beta; + float out_v = 0.0f; + for (int32_t k_idx = 0; k_idx < qk_head_size; k_idx++) { + auto& s = state[k_idx * v_head_size + v_idx]; + s += k_norm[k_idx] * update; + out_v += s * q_norm[k_idx]; + } + + output[(token * v_heads + h) * v_head_size + v_idx] = static_cast(out_v); + } + + const int32_t processed_tokens = (token - token_begin) + 1; + const int32_t cached_tokens = prev_nums + processed_tokens; + const bool reached_interval_boundary = (interval > 0) && ((cached_tokens % interval) == 0); + const bool reached_sequence_end = (token == token_end - 1); + if (reached_interval_boundary || reached_sequence_end) { + const int32_t slot = interval > 0 ? (1 + (cached_tokens - 1) / interval) : 1; + if (slot < seq_blocks) { + const int32_t next_block_id = block_indices[block_begin + slot]; + for (int32_t k_idx = 0; k_idx < qk_head_size; k_idx++) { + for (int32_t v_idx = 0; v_idx < v_head_size; v_idx++) { + recurrent_state_table[state_off(next_block_id, h, k_idx, v_idx)] = + static_cast(state[k_idx * v_head_size + v_idx]); + } + } + } + } + } + } + } +} + +template +std::vector tensor_to_vector(const ov::Tensor& tensor) { + const auto* ptr = tensor.data(); + return std::vector(ptr, ptr + tensor.get_size()); +} + +ov::Tensor make_i32_tensor(const std::vector& values) { + ov::Tensor tensor(ov::element::i32, ov::Shape{values.size()}); + std::copy(values.begin(), values.end(), tensor.data()); + return tensor; +} + +template +std::vector calculate_typed_refs(const std::map, ov::Tensor>& host_inputs, + const std::shared_ptr& function, + int32_t qk_heads, + int32_t v_heads, + int32_t qk_head_size, + int32_t v_head_size, + const ov::element::Type& data_type) { + const auto& params = function->get_parameters(); + + auto query = tensor_to_vector(host_inputs.at(params[0])); + auto key = tensor_to_vector(host_inputs.at(params[1])); + auto value = tensor_to_vector(host_inputs.at(params[2])); + auto state = tensor_to_vector(host_inputs.at(params[3])); + auto gate = tensor_to_vector(host_inputs.at(params[4])); + auto beta = tensor_to_vector(host_inputs.at(params[5])); + auto subsequence_begins = tensor_to_vector(host_inputs.at(params[6])); + auto block_indices = tensor_to_vector(host_inputs.at(params[7])); + auto block_indices_begins = tensor_to_vector(host_inputs.at(params[8])); + auto past_lens = tensor_to_vector(host_inputs.at(params[9])); + auto cache_interval = tensor_to_vector(host_inputs.at(params[10])); + + std::vector ref_output; + run_reference(query, + key, + value, + gate, + beta, + state, + subsequence_begins, + block_indices, + block_indices_begins, + past_lens, + cache_interval, + qk_heads, + v_heads, + qk_head_size, + v_head_size, + ref_output); + + ov::Tensor output_tensor(data_type, host_inputs.at(params[2]).get_shape()); + std::copy(ref_output.begin(), ref_output.end(), output_tensor.data()); + + ov::Tensor state_tensor(data_type, host_inputs.at(params[3]).get_shape()); + std::copy(state.begin(), state.end(), state_tensor.data()); + + return {output_tensor, state_tensor}; +} + +} // namespace + +namespace ov::test { + +std::string PagedGatedDeltaNetLayerTest::getTestCaseName( + const testing::TestParamInfo& obj) { + const auto& [qk_heads, + v_heads, + qk_head_size, + v_head_size, + seq_lengths, + cache_intervals, + element_type, + target_device] = obj.param; + std::ostringstream result; + result << "QKHeads=" << qk_heads; + result << "_VHeads=" << v_heads; + result << "_QKHeadSize=" << qk_head_size; + result << "_VHeadSize=" << v_head_size; + result << "_SeqLens="; + for (size_t i = 0; i < seq_lengths.size(); i++) { + if (i > 0) + result << "x"; + result << seq_lengths[i]; + } + result << "_Intervals="; + for (size_t i = 0; i < cache_intervals.size(); i++) { + if (i > 0) + result << "x"; + result << cache_intervals[i]; + } + result << "_Type=" << element_type; + result << "_Target=" << target_device; + return result.str(); +} + +void PagedGatedDeltaNetLayerTest::SetUp() { + const auto& [qk_heads, v_heads, qk_head_size, v_head_size, seq_lengths, cache_intervals, data_type, device] = + GetParam(); + if (device.find("CPU") != std::string::npos) { + // Skip BF16/F16 tests if not support + if ((data_type == ov::element::bf16) && !with_cpu_x86_avx512_core_amx_bf16()) { + GTEST_SKIP(); + } + if ((data_type == ov::element::f16) && !with_cpu_x86_avx512_core_fp16()) { + GTEST_SKIP(); + } + } + + targetDevice = device; + this->data_type = data_type; + configuration[ov::hint::inference_precision.name()] = data_type; + OPENVINO_ASSERT(!seq_lengths.empty()); + OPENVINO_ASSERT(seq_lengths.size() == cache_intervals.size()); + + const int32_t tokens = std::accumulate(seq_lengths.begin(), seq_lengths.end(), 0); + const int32_t num_sequences = static_cast(seq_lengths.size()); + + int32_t num_blocks = 0; + for (size_t i = 0; i < seq_lengths.size(); i++) { + OPENVINO_ASSERT(cache_intervals[i] >= 0); + const int32_t past_len = 1 + static_cast(i % 3); + if (cache_intervals[i] == 0) { + // interval == 0: use exactly 2 blocks per sequence + // block 0 for read, block 1 for final write. + num_blocks += 2; + } else { + const int32_t prev_nums = past_len % cache_intervals[i]; + const int32_t write_blocks = (prev_nums + seq_lengths[i] + cache_intervals[i] - 1) / cache_intervals[i]; + num_blocks += 1 + write_blocks; + } + } + + const ov::Shape q_shape{static_cast(tokens), + static_cast(qk_heads), + static_cast(qk_head_size)}; + const ov::Shape v_shape{static_cast(tokens), + static_cast(v_heads), + static_cast(v_head_size)}; + const ov::Shape state_shape{static_cast(num_blocks), + static_cast(v_heads), + static_cast(v_head_size), + static_cast(qk_head_size)}; + const ov::Shape gv_shape{static_cast(tokens), static_cast(v_heads)}; + + init_input_shapes(static_shapes_to_test_representation({q_shape, + q_shape, + v_shape, + state_shape, + gv_shape, + gv_shape, + ov::Shape{static_cast(num_sequences + 1)}, + ov::Shape{static_cast(num_blocks)}, + ov::Shape{static_cast(num_sequences + 1)}, + ov::Shape{static_cast(num_sequences)}, + ov::Shape{static_cast(num_sequences)}})); + + auto p_query = std::make_shared(data_type, ov::PartialShape{-1, qk_heads, qk_head_size}); + auto p_key = std::make_shared(data_type, ov::PartialShape{-1, qk_heads, qk_head_size}); + auto p_value = std::make_shared(data_type, ov::PartialShape{-1, v_heads, v_head_size}); + auto p_state = + std::make_shared(data_type, ov::PartialShape{-1, v_heads, v_head_size, qk_head_size}); + auto p_gate = std::make_shared(data_type, ov::PartialShape{-1, v_heads}); + auto p_beta = std::make_shared(data_type, ov::PartialShape{-1, v_heads}); + auto p_subseq = std::make_shared(ov::element::i32, ov::PartialShape{-1}); + auto p_blocks = std::make_shared(ov::element::i32, ov::PartialShape{-1}); + auto p_block_begins = std::make_shared(ov::element::i32, ov::PartialShape{-1}); + auto p_past_lens = std::make_shared(ov::element::i32, ov::PartialShape{-1}); + auto p_cache_interval = std::make_shared(ov::element::i32, ov::PartialShape{-1}); + auto pgdn = std::make_shared(p_query, + p_key, + p_value, + p_state, + p_gate, + p_beta, + p_subseq, + p_blocks, + p_block_begins, + p_past_lens, + p_cache_interval, + true, + 1e-6f, + 1e-6f); + + function = std::make_shared(ov::ResultVector{std::make_shared(pgdn)}, + ov::ParameterVector{p_query, + p_key, + p_value, + p_state, + p_gate, + p_beta, + p_subseq, + p_blocks, + p_block_begins, + p_past_lens, + p_cache_interval}); +} + +void PagedGatedDeltaNetLayerTest::generate_inputs(const std::vector& targetInputStaticShapes) { + inputs.clear(); + host_inputs.clear(); + + const auto& [qk_heads, v_heads, qk_head_size, v_head_size, seq_lengths, cache_intervals, element_type, device] = + GetParam(); + const auto num_sequences = static_cast(seq_lengths.size()); + + std::vector subsequence_begins; + std::vector block_indices; + std::vector block_indices_begins; + std::vector past_lens; + std::vector cache_interval; + + subsequence_begins.reserve(static_cast(num_sequences + 1)); + block_indices_begins.reserve(static_cast(num_sequences + 1)); + past_lens.reserve(static_cast(num_sequences)); + cache_interval.reserve(static_cast(num_sequences)); + + subsequence_begins.push_back(0); + block_indices_begins.push_back(0); + + int32_t total_blocks = 0; + for (int32_t seq = 0; seq < num_sequences; seq++) { + const int32_t seq_len = seq_lengths[seq]; + const int32_t seq_interval = cache_intervals[seq]; + const int32_t seq_past_len = 1 + (seq % 3); + + subsequence_begins.push_back(subsequence_begins.back() + seq_len); + past_lens.push_back(seq_past_len); + cache_interval.push_back(seq_interval); + + int32_t required_slots = 2; + if (seq_interval > 0) { + const int32_t prev_nums = seq_past_len % seq_interval; + const int32_t write_blocks = (prev_nums + seq_len + seq_interval - 1) / seq_interval; + required_slots = 1 + write_blocks; + } + for (int32_t i = 0; i < required_slots; i++) { + block_indices.push_back(total_blocks + i); + } + total_blocks += required_slots; + block_indices_begins.push_back(total_blocks); + } + + const auto& params = function->get_parameters(); + const bool use_remote_tensors = targetDevice == "GPU"; + ov::RemoteContext remote_context; + if (use_remote_tensors) { + remote_context = compiledModel.get_context(); + } + + for (size_t i = 0; i < params.size(); i++) { + const auto& param = params[i]; + const auto& shape = targetInputStaticShapes[i]; + ov::Tensor tensor; + + if (i == 4) { + tensor = ov::test::utils::create_and_fill_tensor(param->get_element_type(), + shape, + ov::test::utils::InputGenerateData(-1, 1, 1000, 1)); + } else if (i == 5) { + tensor = ov::test::utils::create_and_fill_tensor(param->get_element_type(), + shape, + ov::test::utils::InputGenerateData(0, 1, 1000, 1)); + } else if (i <= 3) { + tensor = ov::test::utils::create_and_fill_tensor(param->get_element_type(), + shape, + ov::test::utils::InputGenerateData(-1, 1, 1000, 1)); + } else if (i == 6) { + tensor = make_i32_tensor(subsequence_begins); + } else if (i == 7) { + tensor = make_i32_tensor(block_indices); + } else if (i == 8) { + tensor = make_i32_tensor(block_indices_begins); + } else if (i == 9) { + tensor = make_i32_tensor(past_lens); + } else if (i == 10) { + tensor = make_i32_tensor(cache_interval); + } + + host_inputs[param] = tensor; + + if (use_remote_tensors && i <= 5) { + auto remote_tensor = remote_context.create_tensor(param->get_element_type(), shape); + remote_tensor.copy_from(tensor); + inputs[param] = remote_tensor; + } else { + inputs[param] = tensor; + } + } + + OPENVINO_ASSERT(qk_heads > 0 && v_heads > 0 && qk_head_size > 0 && v_head_size > 0); +} + +std::vector PagedGatedDeltaNetLayerTest::calculate_refs() { + const auto& [qk_heads, v_heads, qk_head_size, v_head_size, seq_lengths, cache_intervals, element_type, device] = + GetParam(); + + if (element_type == ov::element::f16) { + return calculate_typed_refs(host_inputs, + function, + qk_heads, + v_heads, + qk_head_size, + v_head_size, + element_type); + } + + if (element_type == ov::element::bf16) { + return calculate_typed_refs(host_inputs, + function, + qk_heads, + v_heads, + qk_head_size, + v_head_size, + element_type); + } + + return calculate_typed_refs(host_inputs, + function, + qk_heads, + v_heads, + qk_head_size, + v_head_size, + element_type); +} + +std::vector PagedGatedDeltaNetLayerTest::get_plugin_outputs() { + auto outputs = SubgraphBaseTest::get_plugin_outputs(); + + const auto& state_param = function->get_parameters().at(3); + const auto actual_state_tensor = inferRequest.get_tensor(state_param); + ov::Tensor host_state_tensor(actual_state_tensor.get_element_type(), actual_state_tensor.get_shape()); + actual_state_tensor.copy_to(host_state_tensor); + outputs.push_back(host_state_tensor); + + return outputs; +} + +void PagedGatedDeltaNetLayerTest::compare(const std::vector& expected, + const std::vector& actual) { + ASSERT_EQ(expected.size(), actual.size()); + if (data_type == ov::element::bf16) { + abs_threshold = 1e-3f; + rel_threshold = 1e-2f; + } else if (data_type == ov::element::f16) { + abs_threshold = 3e-4f; + rel_threshold = 1e-5f; + } else { + abs_threshold = 2e-4f; + rel_threshold = 1e-5f; + } + ov::test::utils::compare(expected[0], actual[0], abs_threshold, rel_threshold); + ov::test::utils::compare(expected[1], actual[1], abs_threshold, rel_threshold); +} + +} // namespace ov::test \ No newline at end of file diff --git a/src/tests/functional/plugin/shared/src/subgraph/gated_delta_net.cpp b/src/tests/functional/plugin/shared/src/subgraph/gated_delta_net.cpp index 056a2e33a53b..ea40fbf2f16b 100644 --- a/src/tests/functional/plugin/shared/src/subgraph/gated_delta_net.cpp +++ b/src/tests/functional/plugin/shared/src/subgraph/gated_delta_net.cpp @@ -6,6 +6,8 @@ #include #include +#include +#include #include "common_test_utils/ov_tensor_utils.hpp" #include "openvino/core/type/bfloat16.hpp" @@ -19,10 +21,15 @@ #include "openvino/op/exp.hpp" #include "openvino/op/gated_delta_net.hpp" #include "openvino/op/gather.hpp" +#include "openvino/op/gather_nd.hpp" +#include "openvino/op/less.hpp" #include "openvino/op/loop.hpp" #include "openvino/op/multiply.hpp" +#include "openvino/op/non_zero.hpp" #include "openvino/op/parameter.hpp" #include "openvino/op/power.hpp" +#include "openvino/op/range.hpp" +#include "openvino/op/reduce_max.hpp" #include "openvino/op/reduce_prod.hpp" #include "openvino/op/reduce_sum.hpp" #include "openvino/op/reshape.hpp" @@ -34,6 +41,7 @@ #include "openvino/op/subtract.hpp" #include "openvino/op/transpose.hpp" #include "openvino/op/unsqueeze.hpp" +#include "openvino/op/variadic_split.hpp" #include "openvino/runtime/properties.hpp" namespace ov { @@ -48,8 +56,8 @@ std::shared_ptr GatedDeltaNet::buildLoopedGDN(int32_t batch, ov::element::Type dtype) { const ov::PartialShape qk_shape{batch, seq_len, qk_head_num, qk_head_size}; const ov::PartialShape v_tensor_shape{batch, seq_len, v_head_num, v_head_size}; - const ov::PartialShape gv_shape{batch, seq_len, qk_head_num}; - const ov::PartialShape h_shape{batch, qk_head_num, qk_head_size, v_head_size}; + const ov::PartialShape gv_shape{batch, seq_len, v_head_num}; + const ov::PartialShape h_shape{batch, v_head_num, qk_head_size, v_head_size}; auto q = std::make_shared(dtype, qk_shape); auto k = std::make_shared(dtype, qk_shape); @@ -65,6 +73,39 @@ std::shared_ptr GatedDeltaNet::buildLoopedGDN(int32_t batch, g->set_friendly_name("g"); beta->set_friendly_name("beta"); + const bool need_head_repeat = (qk_head_num != v_head_num); + + auto repeat_qk_heads = + [&](const ov::Output& query, + const ov::Output& key) -> std::pair, ov::Output> { + if (!need_head_repeat) { + return {query, key}; + } + + const int64_t group_size = static_cast(v_head_num / qk_head_num); + + std::vector repeated_head_ids; + repeated_head_ids.reserve(static_cast(v_head_num)); + for (int64_t h = 0; h < static_cast(v_head_num); ++h) { + repeated_head_ids.push_back(h / group_size); + } + + auto gather_indices = + ov::op::v0::Constant::create(ov::element::i64, {static_cast(v_head_num)}, repeated_head_ids); + auto gather_axis = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{}, {2}); + + auto repeated_q = std::make_shared(query, gather_indices, gather_axis, 0); + auto repeated_k = std::make_shared(key, gather_indices, gather_axis, 0); + + auto repeated_shape = ov::op::v0::Constant::create( + ov::element::i64, + ov::Shape{4}, + std::vector{0, 0, static_cast(v_head_num), static_cast(qk_head_size)}); + auto repeated_q_reshape = std::make_shared(repeated_q, repeated_shape, true); + auto repeated_k_reshape = std::make_shared(repeated_k, repeated_shape, true); + return {repeated_q_reshape, repeated_k_reshape}; + }; + auto l2norm = [&](const ov::Output& x) { auto sq = std::make_shared(x, x); auto axis = ov::op::v0::Constant::create(ov::element::i32, {1}, {-1}); @@ -76,10 +117,68 @@ std::shared_ptr GatedDeltaNet::buildLoopedGDN(int32_t batch, return std::make_shared(x, inv); }; - auto q_norm = l2norm(q); - auto k_norm = l2norm(k); + ov::Output q_for_attn = q; + ov::Output k_for_attn = k; + ov::Output v_for_attn = v; + + if (need_head_repeat) { + auto flatten_q_shape = ov::op::v0::Constant::create(ov::element::i64, + ov::Shape{4}, + std::vector{static_cast(0), + static_cast(0), + static_cast(1), + static_cast(-1)}); + auto flatten_v_shape = ov::op::v0::Constant::create(ov::element::i64, + ov::Shape{4}, + std::vector{static_cast(0), + static_cast(0), + static_cast(1), + static_cast(-1)}); + + auto q_flat = std::make_shared(q, flatten_q_shape, true); + auto k_flat = std::make_shared(k, flatten_q_shape, true); + auto v_flat = std::make_shared(v, flatten_v_shape, true); + + auto qkv_concat = std::make_shared(ov::OutputVector{q_flat, k_flat, v_flat}, -1); + auto split_axis = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{}, {-1}); + auto split_lengths = + ov::op::v0::Constant::create(ov::element::i64, + {3}, + {static_cast(qk_head_num) * static_cast(qk_head_size), + static_cast(qk_head_num) * static_cast(qk_head_size), + static_cast(v_head_num) * static_cast(v_head_size)}); + auto qkv_split = std::make_shared(qkv_concat, split_axis, split_lengths); + + auto q_shape = ov::op::v0::Constant::create(ov::element::i64, + ov::Shape{4}, + std::vector{static_cast(0), + static_cast(0), + static_cast(qk_head_num), + static_cast(qk_head_size)}); + auto k_shape = ov::op::v0::Constant::create(ov::element::i64, + ov::Shape{4}, + std::vector{static_cast(0), + static_cast(0), + static_cast(qk_head_num), + static_cast(qk_head_size)}); + auto v_shape_split = ov::op::v0::Constant::create(ov::element::i64, + ov::Shape{4}, + std::vector{static_cast(0), + static_cast(0), + static_cast(v_head_num), + static_cast(v_head_size)}); + + q_for_attn = std::make_shared(qkv_split->output(0), q_shape, true); + k_for_attn = std::make_shared(qkv_split->output(1), k_shape, true); + v_for_attn = std::make_shared(qkv_split->output(2), v_shape_split, true); + + std::tie(q_for_attn, k_for_attn) = repeat_qk_heads(q_for_attn, k_for_attn); + } - auto v_shape = std::make_shared(v); + auto q_norm = l2norm(q_for_attn); + auto k_norm = l2norm(k_for_attn); + + auto v_shape = std::make_shared(v_for_attn); auto core_attn_init = std::make_shared(ov::op::v0::Constant::create(dtype, {}, {0.0f}), v_shape); @@ -87,7 +186,7 @@ std::shared_ptr GatedDeltaNet::buildLoopedGDN(int32_t batch, auto perm_bhs = ov::op::v0::Constant::create(ov::element::i64, {3}, {0, 2, 1}); auto q_norm_t = std::make_shared(q_norm, perm_bhsd); - auto shape_of_q = std::make_shared(q); + auto shape_of_q = std::make_shared(q_for_attn); auto gather_q_perm_index = ov::op::v0::Constant::create(ov::element::i64, {4}, {0, 2, 1, 3}); auto gather_0_axis = ov::op::v0::Constant::create(ov::element::i64, {}, {0}); auto gather_q_shape = std::make_shared(shape_of_q, gather_q_perm_index, gather_0_axis, 0); @@ -100,7 +199,7 @@ std::shared_ptr GatedDeltaNet::buildLoopedGDN(int32_t batch, auto q_scaled_t = std::make_shared(q_norm_t, q_scale); auto k_norm_t = std::make_shared(k_norm, perm_bhsd); - auto v_t = std::make_shared(v, perm_bhsd); + auto v_t = std::make_shared(v_for_attn, perm_bhsd); auto g_t = std::make_shared(g, perm_bhs); auto beta_t = std::make_shared(beta, perm_bhs); @@ -275,10 +374,10 @@ void GatedDeltaNet::SetUp() { static_cast(v_head_num), static_cast(v_head_size)}; const ov::Shape h_shape{static_cast(batch), - static_cast(qk_head_num), + static_cast(v_head_num), static_cast(qk_head_size), static_cast(v_head_size)}; - const ov::Shape g_shape{static_cast(batch), static_cast(seq_len), static_cast(qk_head_num)}; + const ov::Shape g_shape{static_cast(batch), static_cast(seq_len), static_cast(v_head_num)}; init_input_shapes(static_shapes_to_test_representation({q_shape, q_shape, v_shape, h_shape, g_shape, g_shape})); diff --git a/src/tests/functional/plugin/shared/src/subgraph/matmul_transpose_to_reshape.cpp b/src/tests/functional/plugin/shared/src/subgraph/matmul_transpose_to_reshape.cpp new file mode 100644 index 000000000000..a12bf0fae017 --- /dev/null +++ b/src/tests/functional/plugin/shared/src/subgraph/matmul_transpose_to_reshape.cpp @@ -0,0 +1,35 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "shared_test_classes/subgraph/matmul_transpose_to_reshape.hpp" + +#include "common_test_utils/node_builders/constant.hpp" +#include "openvino/op/matmul.hpp" + +namespace ov { +namespace test { + +std::string MatMulTransposeToReshape::getTestCaseName(const testing::TestParamInfo& obj) { + const auto& [element_type, target_name] = obj.param; + + std::ostringstream result; + result << "ET=" << element_type << "_"; + result << "targetDevice=" << target_name; + return result.str(); +} + +void MatMulTransposeToReshape::SetUp() { + const auto& [element_type, _targetDevice] = GetParam(); + + targetDevice = _targetDevice; + + const auto data = std::make_shared(element_type, ov::Shape{3, 2, 1}); + const auto weights = ov::test::utils::make_constant(element_type, ov::Shape{1, 2, 1}); + const auto matmul = std::make_shared(data, weights, true, false); + + function = std::make_shared(ov::OutputVector{matmul}, ov::ParameterVector{data}, "MatMulTransposeToReshape"); +} + +} // namespace test +} // namespace ov diff --git a/src/tests/test_utils/common_test_utils/include/common_test_utils/all_close.hpp b/src/tests/test_utils/common_test_utils/include/common_test_utils/all_close.hpp index 45850f48d6f0..3489b6ec664c 100644 --- a/src/tests/test_utils/common_test_utils/include/common_test_utils/all_close.hpp +++ b/src/tests/test_utils/common_test_utils/include/common_test_utils/all_close.hpp @@ -4,6 +4,7 @@ #pragma once +#include #include #include "gtest/gtest.h" diff --git a/src/tests/test_utils/common_test_utils/include/common_test_utils/common_utils.hpp b/src/tests/test_utils/common_test_utils/include/common_test_utils/common_utils.hpp index e166c9463aa8..9d8b25c94102 100644 --- a/src/tests/test_utils/common_test_utils/include/common_test_utils/common_utils.hpp +++ b/src/tests/test_utils/common_test_utils/include/common_test_utils/common_utils.hpp @@ -214,6 +214,9 @@ std::string generateTestFilePrefix(); size_t getVmSizeInKB(); size_t getVmRSSInKB(); + +size_t count_resident_pages(const void* data, size_t size); + } // namespace utils } // namespace test } // namespace ov diff --git a/src/tests/test_utils/common_test_utils/include/common_test_utils/data_utils.hpp b/src/tests/test_utils/common_test_utils/include/common_test_utils/data_utils.hpp index 263f5d431118..566a19c8f310 100644 --- a/src/tests/test_utils/common_test_utils/include/common_test_utils/data_utils.hpp +++ b/src/tests/test_utils/common_test_utils/include/common_test_utils/data_utils.hpp @@ -372,11 +372,7 @@ void inline fill_data_ptr_normal_random_float(T* data, std::normal_distribution<> normal_d{mean, stddev}; for (size_t i = 0; i < size; i++) { auto value = static_cast(normal_d(random)); - if (typeid(T) == typeid(typename ov::fundamental_type_for)) { - data[i] = static_cast(ov::float16(value).to_bits()); - } else { - data[i] = static_cast(value); - } + data[i] = static_cast(value); } } diff --git a/src/tests/test_utils/common_test_utils/include/common_test_utils/file_utils.hpp b/src/tests/test_utils/common_test_utils/include/common_test_utils/file_utils.hpp index da860ebb3924..f375a3350b3e 100644 --- a/src/tests/test_utils/common_test_utils/include/common_test_utils/file_utils.hpp +++ b/src/tests/test_utils/common_test_utils/include/common_test_utils/file_utils.hpp @@ -253,6 +253,7 @@ inline std::vector splitStringByDelimiter(std::string paths, const } std::string getModelFromTestModelZoo(const std::string& relModelPath); +std::string getModelFromTestModelZoo(const std::filesystem::path& relModelPath); std::string getOpenvinoLibDirectory(); std::string getExecutableDirectory(); diff --git a/src/tests/test_utils/common_test_utils/include/common_test_utils/graph_comparator.hpp b/src/tests/test_utils/common_test_utils/include/common_test_utils/graph_comparator.hpp index 6b515fc6355e..76ddfb560adf 100644 --- a/src/tests/test_utils/common_test_utils/include/common_test_utils/graph_comparator.hpp +++ b/src/tests/test_utils/common_test_utils/include/common_test_utils/graph_comparator.hpp @@ -58,6 +58,10 @@ class FunctionsComparator { return fc; } + static FunctionsComparator all_flags_enabled() noexcept { + return FunctionsComparator{static_cast(~0)}; + } + FunctionsComparator& enable(CmpValues f) noexcept { m_comparison_flags = static_cast(m_comparison_flags | f); return *this; diff --git a/src/tests/test_utils/common_test_utils/include/common_test_utils/node_builders/moe_builders.hpp b/src/tests/test_utils/common_test_utils/include/common_test_utils/node_builders/moe_builders.hpp index 9996f8508a95..10676c33d151 100644 --- a/src/tests/test_utils/common_test_utils/include/common_test_utils/node_builders/moe_builders.hpp +++ b/src/tests/test_utils/common_test_utils/include/common_test_utils/node_builders/moe_builders.hpp @@ -30,11 +30,21 @@ enum class MoERoutingType { SIGMOID_BIAS, ///< Sigmoid -> Add(bias) -> TopK routing }; +enum class MoEActivationType { + SWISH, ///< Swish gate activation (SwiGLU) + GELU, ///< Gelu gate activation with Tanh approximation (GeGLU-Tanh) + GELU_ERF, ///< Gelu gate activation with ERF (exact) formula (GeGLU-ERF) +}; + /// Softmax branch: /// routing_weights -> Softmax -> TopK -> ReduceSum -> Divide (norm) +/// [-> Multiply(norm, Gather(per_expert_scale, topk_idx)) when use_per_expert_scale=true] /// -> ScatterElementsUpdate -> Transpose -> Reshape -> Unsqueeze -std::pair, ov::Output> -build_softmax_routing_subgraph(const ov::Output& routing_weights, size_t number_of_experts, size_t topk); +std::pair, ov::Output> build_softmax_routing_subgraph( + const ov::Output& routing_weights, + size_t number_of_experts, + size_t topk, + bool use_per_expert_scale = false); /// Sigmoid+bias branch: /// routing_weights -> Sigmoid -> Add(bias) -> TopK -> Convert(i32) @@ -45,6 +55,7 @@ std::pair, ov::Output> build_sigmoid_bias_routing size_t number_of_experts, size_t topk); +// gate_idx: 0 = gate (swish) at even positions (real gpt-oss), 1 = at odd positions. std::shared_ptr initMoE2GeMMSubgraph( const MoePatternParams& moe_params, const ov::element::Type data_precision, @@ -55,7 +66,8 @@ std::shared_ptr initMoE2GeMMSubgraph( const std::optional decompression_multiply_type = std::nullopt, const std::optional decompression_subtract_type = std::nullopt, const std::optional reshape_on_decompression = std::nullopt, - const std::optional decompression_group_size = std::nullopt); + const std::optional decompression_group_size = std::nullopt, + size_t gate_idx = 0); std::shared_ptr initMoE3GeMMSubgraph( const MoePatternParams& moe_params, @@ -68,7 +80,10 @@ std::shared_ptr initMoE3GeMMSubgraph( const std::optional decompression_subtract_type = std::nullopt, const std::optional reshape_on_decompression = std::nullopt, const std::optional decompression_group_size = std::nullopt, - MoERoutingType routing_type = MoERoutingType::SOFTMAX); + MoERoutingType routing_type = MoERoutingType::SOFTMAX, + MoEActivationType activation_type = MoEActivationType::SWISH, + bool use_per_expert_scale = false, + bool use_layernorm_multiply = false); } // namespace test } // namespace ov diff --git a/src/tests/test_utils/common_test_utils/include/common_test_utils/postgres_helpers.hpp b/src/tests/test_utils/common_test_utils/include/common_test_utils/postgres_helpers.hpp index cc3dad39cf4b..fe3ac3e52c50 100644 --- a/src/tests/test_utils/common_test_utils/include/common_test_utils/postgres_helpers.hpp +++ b/src/tests/test_utils/common_test_utils/include/common_test_utils/postgres_helpers.hpp @@ -127,11 +127,9 @@ extern fnPQclear PQclear; extern fnPQresultErrorMessage PQresultErrorMessage; #endif -extern const char* PGPrefix(const char* text, ::testing::internal::GTestColor color); - -#define PG_ERR PGPrefix("[ PG ERROR ] ", ::testing::internal::COLOR_RED) -#define PG_WRN PGPrefix("[ PG WARN ] ", ::testing::internal::COLOR_YELLOW) -#define PG_INF PGPrefix("[ PG INFO ] ", ::testing::internal::COLOR_GREEN) +#define PG_ERR "[ PG ERROR ] " +#define PG_WRN "[ PG WARN ] " +#define PG_INF "[ PG INFO ] " /// \brief Count of tries when serialization error is detected after query const uint8_t serializationTriesCount = 30; // Pause between each attempt is not less than 50ms diff --git a/src/tests/test_utils/common_test_utils/include/common_test_utils/subgraph_builders/weights_decompression_builders.hpp b/src/tests/test_utils/common_test_utils/include/common_test_utils/subgraph_builders/weights_decompression_builders.hpp index 87a597c89849..223a97971415 100644 --- a/src/tests/test_utils/common_test_utils/include/common_test_utils/subgraph_builders/weights_decompression_builders.hpp +++ b/src/tests/test_utils/common_test_utils/include/common_test_utils/subgraph_builders/weights_decompression_builders.hpp @@ -8,6 +8,7 @@ #include #include +#include "common_test_utils/ov_tensor_utils.hpp" #include "openvino/core/node.hpp" namespace ov { @@ -37,7 +38,8 @@ std::shared_ptr initMatMulDecompressionSubgraph( const std::optional& insert_transpose_node = std::nullopt, const size_t seed = 1); -// do real quantization of a random float tensor to get weights and scales +// Real-quantize a random fp32 tensor. Source range [-0.1, 0.1) matches LLM FFN weights; +// wider ranges sink deep-chain outputs into bf16/f16 noise. std::shared_ptr initMatMulDecompressionSubgraphQuantization( const ov::Shape& weights_shape, const int group_size, diff --git a/src/tests/test_utils/common_test_utils/include/common_test_utils/test_control.hpp b/src/tests/test_utils/common_test_utils/include/common_test_utils/test_control.hpp index 38c2568fc530..9a023159c851 100644 --- a/src/tests/test_utils/common_test_utils/include/common_test_utils/test_control.hpp +++ b/src/tests/test_utils/common_test_utils/include/common_test_utils/test_control.hpp @@ -23,12 +23,20 @@ std::string combine_test_backend_and_case(const std::string& backend_name, const #define OPENVINO_GTEST_TEST_(backend_name, test_case_name, test_name, parent_class, parent_id) \ class OPENVINO_GTEST_TEST_CLASS_NAME_(backend_name, test_case_name, test_name) : public parent_class { \ public: \ - OPENVINO_GTEST_TEST_CLASS_NAME_(backend_name, test_case_name, test_name)() {} \ + OPENVINO_GTEST_TEST_CLASS_NAME_(backend_name, test_case_name, test_name)() = default; \ + ~OPENVINO_GTEST_TEST_CLASS_NAME_(backend_name, test_case_name, test_name)() override = default; \ + OPENVINO_GTEST_TEST_CLASS_NAME_(backend_name, test_case_name, test_name) \ + (const OPENVINO_GTEST_TEST_CLASS_NAME_(backend_name, test_case_name, test_name) &) = delete; \ + OPENVINO_GTEST_TEST_CLASS_NAME_(backend_name, test_case_name, test_name) & operator=( \ + const OPENVINO_GTEST_TEST_CLASS_NAME_(backend_name, test_case_name, test_name) &) = delete; \ + OPENVINO_GTEST_TEST_CLASS_NAME_(backend_name, test_case_name, test_name) \ + (OPENVINO_GTEST_TEST_CLASS_NAME_(backend_name, test_case_name, test_name) &&) noexcept = delete; \ + OPENVINO_GTEST_TEST_CLASS_NAME_(backend_name, test_case_name, test_name) & operator=( \ + OPENVINO_GTEST_TEST_CLASS_NAME_(backend_name, test_case_name, test_name) &&) noexcept = delete; \ \ private: \ void TestBody() override; \ - static ::testing::TestInfo* const test_info_ GTEST_ATTRIBUTE_UNUSED_; \ - GTEST_DISALLOW_COPY_AND_ASSIGN_(OPENVINO_GTEST_TEST_CLASS_NAME_(backend_name, test_case_name, test_name)); \ + [[maybe_unused]] static ::testing::TestInfo* const test_info_; \ }; \ \ ::testing::TestInfo* const OPENVINO_GTEST_TEST_CLASS_NAME_(backend_name, test_case_name, test_name)::test_info_ = \ @@ -39,8 +47,8 @@ std::string combine_test_backend_and_case(const std::string& backend_name, const nullptr, \ ::testing::internal::CodeLocation(__FILE__, __LINE__), \ (parent_id), \ - parent_class::SetUpTestCase, \ - parent_class::TearDownTestCase, \ + ::testing::internal::SuiteApiResolver::GetSetUpCaseOrSuite(__FILE__, __LINE__), \ + ::testing::internal::SuiteApiResolver::GetTearDownCaseOrSuite(__FILE__, __LINE__), \ new ::testing::internal::TestFactoryImpl< \ OPENVINO_GTEST_TEST_CLASS_NAME_(backend_name, test_case_name, test_name)>); \ void OPENVINO_GTEST_TEST_CLASS_NAME_(backend_name, test_case_name, test_name)::TestBody() @@ -85,30 +93,33 @@ std::string combine_test_backend_and_case(const std::string& backend_name, const // Start by defining a class derived from ::testing::TestWithParam, which you'll pass // for the test_case_name parameter. // Then use OPENVINO_INSTANTIATE_TEST_SUITE_P to define each generation of test cases (see below). -#define OPENVINO_TEST_P(backend_name, test_case_name, test_name) \ - class OPENVINO_GTEST_TEST_CLASS_NAME_(backend_name, test_case_name, test_name) : public test_case_name { \ - public: \ - OPENVINO_GTEST_TEST_CLASS_NAME_(backend_name, test_case_name, test_name)() {} \ - void TestBody() override; \ - \ - private: \ - static int AddToRegistry() { \ - ::testing::UnitTest::GetInstance() \ - ->parameterized_test_registry() \ - .GetTestCasePatternHolder(#backend_name "/" #test_case_name, \ - ::testing::internal::CodeLocation(__FILE__, __LINE__)) \ - ->AddTestPattern( \ - #backend_name "/" #test_case_name, \ - ::ov::prepend_disabled(#backend_name "/" #test_case_name, #test_name, s_manifest).c_str(), \ - new ::testing::internal::TestMetaFactory< \ - OPENVINO_GTEST_TEST_CLASS_NAME_(backend_name, test_case_name, test_name)>()); \ - return 0; \ - } \ - static int gtest_registering_dummy_ GTEST_ATTRIBUTE_UNUSED_; \ - GTEST_DISALLOW_COPY_AND_ASSIGN_(OPENVINO_GTEST_TEST_CLASS_NAME_(backend_name, test_case_name, test_name)); \ - }; \ - int OPENVINO_GTEST_TEST_CLASS_NAME_(backend_name, test_case_name, test_name)::gtest_registering_dummy_ = \ - OPENVINO_GTEST_TEST_CLASS_NAME_(backend_name, test_case_name, test_name)::AddToRegistry(); \ +#define OPENVINO_TEST_P(backend_name, test_case_name, test_name) \ + class OPENVINO_GTEST_TEST_CLASS_NAME_(backend_name, test_case_name, test_name) : public test_case_name { \ + public: \ + OPENVINO_GTEST_TEST_CLASS_NAME_(backend_name, test_case_name, test_name)() {} \ + void TestBody() override; \ + \ + private: \ + static int AddToRegistry() { \ + ::testing::UnitTest::GetInstance() \ + ->parameterized_test_registry() \ + .GetTestCasePatternHolder(#backend_name "/" #test_case_name, \ + ::testing::internal::CodeLocation(__FILE__, __LINE__)) \ + ->AddTestPattern( \ + #backend_name "/" #test_case_name, \ + ::ov::prepend_disabled(#backend_name "/" #test_case_name, #test_name, s_manifest).c_str(), \ + new ::testing::internal::TestMetaFactory< \ + OPENVINO_GTEST_TEST_CLASS_NAME_(backend_name, test_case_name, test_name)>()); \ + return 0; \ + } \ + [[maybe_unused]] static int gtest_registering_dummy_; \ + OPENVINO_GTEST_TEST_CLASS_NAME_(backend_name, test_case_name, test_name) \ + (const OPENVINO_GTEST_TEST_CLASS_NAME_(backend_name, test_case_name, test_name) &) = delete; \ + OPENVINO_GTEST_TEST_CLASS_NAME_(backend_name, test_case_name, test_name) & operator=( \ + const OPENVINO_GTEST_TEST_CLASS_NAME_(backend_name, test_case_name, test_name) &) = delete; \ + }; \ + int OPENVINO_GTEST_TEST_CLASS_NAME_(backend_name, test_case_name, test_name)::gtest_registering_dummy_ = \ + OPENVINO_GTEST_TEST_CLASS_NAME_(backend_name, test_case_name, test_name)::AddToRegistry(); \ void OPENVINO_GTEST_TEST_CLASS_NAME_(backend_name, test_case_name, test_name)::TestBody() // Use OPENVINO_INSTANTIATE_TEST_SUITE_P to create a generated set of test case variations. @@ -153,7 +164,7 @@ std::string combine_test_backend_and_case(const std::string& backend_name, const const ::testing::TestParamInfo& info) { \ return ::testing::internal::DefaultParamName(info); \ } \ - static int gtest_##prefix##backend_name##test_suite_name##_dummy_ GTEST_ATTRIBUTE_UNUSED_ = \ + [[maybe_unused]] static int gtest_##prefix##backend_name##test_suite_name##_dummy_ = \ ::testing::UnitTest::GetInstance() \ ->parameterized_test_registry() \ .GetTestCasePatternHolder(#backend_name "/" #test_suite_name, \ diff --git a/src/tests/test_utils/common_test_utils/src/all_close_f.cpp b/src/tests/test_utils/common_test_utils/src/all_close_f.cpp index 0a449d232527..c83c775c87ed 100644 --- a/src/tests/test_utils/common_test_utils/src/all_close_f.cpp +++ b/src/tests/test_utils/common_test_utils/src/all_close_f.cpp @@ -6,6 +6,8 @@ #include +#include + #include "common_test_utils/float_util.hpp" #include "openvino/core/type/element_type_traits.hpp" diff --git a/src/tests/test_utils/common_test_utils/src/common_utils.cpp b/src/tests/test_utils/common_test_utils/src/common_utils.cpp index a432e74bb317..9dfd043d8205 100644 --- a/src/tests/test_utils/common_test_utils/src/common_utils.cpp +++ b/src/tests/test_utils/common_test_utils/src/common_utils.cpp @@ -18,6 +18,11 @@ # include # include "psapi.h" +#else +# include // mincore +# include + +# include #endif namespace ov { @@ -39,8 +44,8 @@ std::ostream& operator<<(std::ostream& os, OpType type) { } std::string generateTestFilePrefix() { - // Generate unique file names based on test name, thread id and timestamp - // This allows execution of tests in parallel (stress mode) + // Generate unique file names based on test name, process id, thread id and timestamp. + // This allows execution of tests in parallel across threads and processes. auto testInfo = ::testing::UnitTest::GetInstance()->current_test_info(); std::string testName = testInfo->test_case_name(); testName += testInfo->name(); @@ -48,7 +53,12 @@ std::string generateTestFilePrefix() { std::stringstream ss; auto ts = std::chrono::duration_cast( std::chrono::high_resolution_clock::now().time_since_epoch()); - ss << testName << "_" << std::this_thread::get_id() << "_" << ts.count(); +#ifdef _WIN32 + const auto pid = static_cast(::GetCurrentProcessId()); +#else + const auto pid = static_cast(::getpid()); +#endif + ss << testName << "_" << pid << "_" << std::this_thread::get_id() << "_" << ts.count(); testName = ss.str(); std::replace(testName.begin(), testName.end(), ':', '_'); return testName; @@ -71,6 +81,10 @@ size_t getVmRSSInKB() { return getMemoryInfo().WorkingSetSize / 1024; } +size_t count_resident_pages(const void* data, size_t size) { + throw std::runtime_error("count_resident_pages is not implemented on Windows"); +} + #else size_t getSystemDataByName(char* name) { @@ -112,6 +126,29 @@ size_t getVmRSSInKB() { return getSystemDataByName(const_cast("VmRSS:")); } +// Returns the number of pages in [data, data+size) that are resident in the page cache. +// Uses mincore(2). Returns 0 if mincore fails (region not mapped or other error). +size_t count_resident_pages(const void* data, size_t size) { + if (!data || size == 0) + return 0; + const size_t page = static_cast(sysconf(_SC_PAGE_SIZE)); + const auto base_addr = reinterpret_cast(data); + const auto aligned = (base_addr / page) * page; + const auto gap = base_addr - aligned; + const size_t aligned_size = size + gap; + const size_t num_pages = (aligned_size + page - 1) / page; + std::vector vec(num_pages, 0); +# ifdef __linux__ + if (mincore(reinterpret_cast(aligned), aligned_size, vec.data()) != 0) +# else + if (mincore(reinterpret_cast(aligned), aligned_size, reinterpret_cast(vec.data())) != 0) +# endif + return 0; + return static_cast(std::count_if(vec.begin(), vec.end(), [](unsigned char v) { + return (v & 1) != 0; + })); +} + #endif } // namespace utils } // namespace test diff --git a/src/tests/test_utils/common_test_utils/src/file_utils.cpp b/src/tests/test_utils/common_test_utils/src/file_utils.cpp index ff7f95105f5e..f97d3439d619 100644 --- a/src/tests/test_utils/common_test_utils/src/file_utils.cpp +++ b/src/tests/test_utils/common_test_utils/src/file_utils.cpp @@ -174,6 +174,10 @@ std::string getModelFromTestModelZoo(const std::string& relModelPath) { return ov::util::path_join({getExecutableDirectory(), relModelPath}).string(); } +std::string getModelFromTestModelZoo(const std::filesystem::path& relModelPath) { + return ov::util::path_to_string(ov::util::make_path(getExecutableDirectory()) / relModelPath); +} + std::string getRelativePath(const std::string& from, const std::string& to) { auto split_path = [](const std::string& path) -> std::vector { std::string sep{FileTraits::file_separator}; @@ -251,9 +255,10 @@ std::filesystem::path to_fs_path(const StringPathVariant& param) { FileHandle open_ro_file(const std::filesystem::path& path) { #ifdef _WIN32 + // FILE_SHARE_DELETE allows std::filesystem::remove() to succeed while the handle is open, matching POSIX unlink() return ::CreateFileW(path.c_str(), GENERIC_READ, - FILE_SHARE_READ, + FILE_SHARE_READ | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, @@ -262,4 +267,5 @@ FileHandle open_ro_file(const std::filesystem::path& path) { return ::open(path.c_str(), O_RDONLY); #endif } + } // namespace ov::test::utils diff --git a/src/tests/test_utils/common_test_utils/src/node_builders/moe_builders.cpp b/src/tests/test_utils/common_test_utils/src/node_builders/moe_builders.cpp index 76ab3d3a6eea..bb7a38092b07 100644 --- a/src/tests/test_utils/common_test_utils/src/node_builders/moe_builders.cpp +++ b/src/tests/test_utils/common_test_utils/src/node_builders/moe_builders.cpp @@ -21,6 +21,7 @@ #include "openvino/op/divide.hpp" #include "openvino/op/gather.hpp" #include "openvino/op/gather_elements.hpp" +#include "openvino/op/gelu.hpp" #include "openvino/op/matmul.hpp" #include "openvino/op/minimum.hpp" #include "openvino/op/multiply.hpp" @@ -43,8 +44,11 @@ namespace ov { namespace test { -std::pair, ov::Output> -build_softmax_routing_subgraph(const ov::Output& routing_weights, size_t number_of_experts, size_t topk) { +std::pair, ov::Output> build_softmax_routing_subgraph( + const ov::Output& routing_weights, + size_t number_of_experts, + size_t topk, + bool use_per_expert_scale) { using namespace ov::op; auto router_softmax = std::make_shared(routing_weights, 1); @@ -60,6 +64,15 @@ build_softmax_routing_subgraph(const ov::Output& routing_weights, size auto scatter_w = ov::Output{std::make_shared(router_topk->output(0), reduce_sm)}; auto topk_idx = router_topk->output(1); + if (use_per_expert_scale) { + const auto elem_type = routing_weights.get_element_type(); + auto pes_data = ov::test::utils::InputGenerateData(0.5, 2, 100, 42); + auto per_expert_scale_const = ov::test::utils::make_constant(elem_type, ov::Shape{number_of_experts}, pes_data); + auto gather_axis = v0::Constant::create(ov::element::i32, ov::Shape{}, {0}); + auto gathered_scales = std::make_shared(per_expert_scale_const, topk_idx, gather_axis); + scatter_w = std::make_shared(scatter_w, gathered_scales); + } + auto shapeof = std::make_shared(topk_idx); auto gather = std::make_shared(shapeof, v0::Constant::create(ov::element::i64, ov::Shape{}, {0}), @@ -239,7 +252,11 @@ std::shared_ptr initMoE2GeMMSubgraph( const std::optional decompression_multiply_type, const std::optional decompression_subtract_type, const std::optional reshape_on_decompression, - const std::optional decompression_group_size) { + const std::optional decompression_group_size, + size_t gate_idx) { + OPENVINO_ASSERT(gate_idx == 0 || gate_idx == 1, "gate_idx must be 0 or 1"); + const int64_t up_start = static_cast(1 - gate_idx); + const int64_t gate_start = static_cast(gate_idx); // Use parameters from shape_params - static shapes only const auto& input_shape = moe_params.data_shape; const size_t intermediate_size = moe_params.intermediate_size; @@ -298,34 +315,42 @@ std::shared_ptr initMoE2GeMMSubgraph( gate_up_matmul->set_friendly_name("GateUpMatMul"); + // Zero-centered bias keeps gate_up away from the clamp cliff (default range [1,10] sits at it). + auto small_bias_data = ov::test::utils::InputGenerateData(-0.5, 1, 1000); auto gate_up_add = std::make_shared( gate_up_matmul, ov::test::utils::make_constant(data_precision, - ov::Shape{number_of_experts, 1, intermediate_size * fusion_factor})); + ov::Shape{number_of_experts, 1, intermediate_size * fusion_factor}, + small_bias_data)); - // Slice the last axis, every second element + // slice1 → clamp → up auto slice1 = std::make_shared( gate_up_add, - ov::op::v0::Constant::create(ov::element::i64, ov::Shape{1}, std::vector{0}), + ov::op::v0::Constant::create(ov::element::i64, ov::Shape{1}, std::vector{up_start}), ov::op::v0::Constant::create(ov::element::i64, ov::Shape{1}, std::vector{std::numeric_limits::max()}), ov::op::v0::Constant::create(ov::element::i64, ov::Shape{1}, std::vector{2}), ov::op::v0::Constant::create(ov::element::i64, ov::Shape{1}, std::vector{2})); auto clamp = std::make_shared(slice1, -expert_beta, expert_beta); - auto add1 = std::make_shared(clamp, ov::test::utils::make_constant(data_precision, ov::Shape{1})); + // SwiGLU "+1" on the up path; fused kernel hardcodes UP_ADD_VAL=1.0. + auto add1 = std::make_shared( + clamp, + ov::op::v0::Constant::create(data_precision, ov::Shape{1}, std::vector{1.0f})); + // slice2 → min → swish → gate auto slice2 = std::make_shared( gate_up_add, - ov::op::v0::Constant::create(ov::element::i64, ov::Shape{1}, std::vector{1}), + ov::op::v0::Constant::create(ov::element::i64, ov::Shape{1}, std::vector{gate_start}), ov::op::v0::Constant::create(ov::element::i64, ov::Shape{1}, std::vector{std::numeric_limits::max()}), ov::op::v0::Constant::create(ov::element::i64, ov::Shape{1}, std::vector{2}), ov::op::v0::Constant::create(ov::element::i64, ov::Shape{1}, std::vector{2})); - auto minimum1 = - std::make_shared(slice2, - ov::op::v0::Constant::create(data_precision, ov::Shape{1}, {10.0f})); + // Swish upper bound = clamp range; fused kernel reuses CLAMP_MAX. + auto minimum1 = std::make_shared( + slice2, + ov::op::v0::Constant::create(data_precision, ov::Shape{1}, {expert_beta})); auto swish_beta = ov::op::v0::Constant::create(data_precision, ov::Shape{}, std::vector{expert_alpha}); auto swish = std::make_shared(minimum1, swish_beta); @@ -350,7 +375,7 @@ std::shared_ptr initMoE2GeMMSubgraph( auto down_proj_add = std::make_shared( down_proj_matmul, - ov::test::utils::make_constant(data_precision, ov::Shape{number_of_experts, 1, hidden_size})); + ov::test::utils::make_constant(data_precision, ov::Shape{number_of_experts, 1, hidden_size}, small_bias_data)); auto router_weights = build_matmul_weights(ov::Shape{hidden_size, number_of_experts}, weights_precision, @@ -369,7 +394,7 @@ std::shared_ptr initMoE2GeMMSubgraph( auto router_bias = std::make_shared( reshape_2nd_consumer_router_matmul, - ov::test::utils::make_constant(data_precision, ov::Shape{1, number_of_experts})); + ov::test::utils::make_constant(data_precision, ov::Shape{1, number_of_experts}, small_bias_data)); auto router_topk_values_and_indices = std::make_shared(router_bias, @@ -448,7 +473,10 @@ std::shared_ptr initMoE3GeMMSubgraph( const std::optional decompression_subtract_type, const std::optional reshape_on_decompression, const std::optional decompression_group_size, - MoERoutingType routing_type) { + MoERoutingType routing_type, + MoEActivationType activation_type, + bool use_per_expert_scale, + bool use_layernorm_multiply) { // Use parameters from shape_params - static shapes only const auto& input_shape = moe_params.data_shape; const size_t intermediate_size = moe_params.intermediate_size; @@ -464,6 +492,14 @@ std::shared_ptr initMoE3GeMMSubgraph( auto input = std::make_shared(data_precision, input_shape); ov::Output input_data = input; + // Gemma4 pattern: layernorm Multiply sits between Parameter and Reshape. + // The fused MOE's hidden_states input must be the Multiply output, not the Parameter. + if (use_layernorm_multiply) { + auto ln_data = ov::test::utils::InputGenerateData(0.5, 2, 100, 99); + auto ln_scale = ov::test::utils::make_constant(data_precision, ov::Shape{1, 1, hidden_size}, ln_data); + input_data = std::make_shared(input_data, ln_scale); + } + // Expert processing path - use -1 for dynamic reshape like in reference auto experts_reshape = std::make_shared( input_data, @@ -505,8 +541,15 @@ std::shared_ptr initMoE3GeMMSubgraph( gate_matmul->set_friendly_name("GateMatMul"); - // Apply Swish activation directly to gate - auto swish = std::make_shared(gate_matmul); + // Apply gate activation (Swish for SwiGLU, Tanh-approximated Gelu for GeGLU, ERF Gelu for GeGLU-ERF) + std::shared_ptr gate_act; + if (activation_type == MoEActivationType::GELU) { + gate_act = std::make_shared(gate_matmul, ov::op::GeluApproximationMode::TANH); + } else if (activation_type == MoEActivationType::GELU_ERF) { + gate_act = std::make_shared(gate_matmul, ov::op::GeluApproximationMode::ERF); + } else { + gate_act = std::make_shared(gate_matmul); + } // Second GEMM (up_projection) auto up_weights = build_matmul_weights(ov::Shape{number_of_experts, hidden_size, intermediate_size}, @@ -525,8 +568,8 @@ std::shared_ptr initMoE3GeMMSubgraph( up_matmul->set_friendly_name("UpMatMul"); - // Join: Multiply (SwiGLU) - auto swiglu = std::make_shared(swish, up_matmul); + // Join: Multiply (SwiGLU or GeGLU) + auto swiglu = std::make_shared(gate_act, up_matmul); // Third GEMM (down_projection) auto down_weights_moe3 = build_matmul_weights(ov::Shape{number_of_experts, intermediate_size, hidden_size}, @@ -558,14 +601,23 @@ std::shared_ptr initMoE3GeMMSubgraph( reshape_on_decompression, decompression_group_size); - auto router_matmul = std::make_shared(experts_reshape, router_weights_moe3, false, true); + // Gemma-4 style: the router uses a separately RMSNorm-scaled hidden state (different node + // from the expert path's experts_reshape). + ov::Output router_input = experts_reshape; + if (use_per_expert_scale) { + auto pes_data = ov::test::utils::InputGenerateData(0.5, 2, 100, 42); + auto router_norm_scale = ov::test::utils::make_constant(data_precision, ov::Shape{1, hidden_size}, pes_data); + router_input = std::make_shared(experts_reshape, router_norm_scale); + } + auto router_matmul = std::make_shared(router_input, router_weights_moe3, false, true); std::pair, ov::Output> routing_outputs; switch (routing_type) { case MoERoutingType::SOFTMAX: - routing_outputs = build_softmax_routing_subgraph(router_matmul, number_of_experts, topk); + routing_outputs = build_softmax_routing_subgraph(router_matmul, number_of_experts, topk, use_per_expert_scale); break; case MoERoutingType::SIGMOID_BIAS: + OPENVINO_ASSERT(!use_per_expert_scale, "Per-expert scale is only supported for SOFTMAX routing"); routing_outputs = build_sigmoid_bias_routing_subgraph(router_matmul, data_precision, number_of_experts, topk); break; default: diff --git a/src/tests/test_utils/common_test_utils/src/ov_tensor_utils.cpp b/src/tests/test_utils/common_test_utils/src/ov_tensor_utils.cpp index 92f5c45570db..6d4a3645058c 100644 --- a/src/tests/test_utils/common_test_utils/src/ov_tensor_utils.cpp +++ b/src/tests/test_utils/common_test_utils/src/ov_tensor_utils.cpp @@ -412,6 +412,11 @@ class Error { std::vector incorrect_values_abs; double abs_threshold, rel_threshold, mvn_threshold, topk_threshold, mvn_results, topk_results; size_t tensor_size; + // Track the coord with the largest |actual - expected| so release builds (which only + // print one failing coord) surface the coord that actually drives the failure — + // otherwise the reader sees the first-in-tensor-order fail, which may be far from worst. + double worst_diff = -1.0; + IncorrectValue worst_value{0.0, 0.0, 0.0, 0}; void emplace_back(double in_actual_value, double in_expected_value, double in_threshold, size_t in_coordinate) { incorrect_values_abs.push_back(IncorrectValue(in_actual_value, in_expected_value, in_threshold, in_coordinate)); @@ -438,6 +443,10 @@ class Error { if (less_or_equal(diff, threshold)) { return true; } + if (diff > worst_diff) { + worst_diff = diff; + worst_value = IncorrectValue(actual, expected, threshold, coordinate); + } emplace_back(actual, expected, threshold, coordinate); return false; } @@ -463,12 +472,22 @@ class Error { constexpr size_t max_num_to_print = 1; #endif size_t i = 0; - for (; i < incorrect_values_abs.size() && i < max_num_to_print; ++i) { - auto val = incorrect_values_abs[i]; - std::cout << "Coordinate: " << std::setw(2) << val.coordinate << " Expected: " << val.expected_value - << " Actual: " << val.actual_value - << " Diff: " << std::fabs(val.expected_value - val.actual_value) - << " abs_threshold: " << val.threshold << "\n"; + if constexpr (max_num_to_print == 1) { + if (worst_diff >= 0.0) { + std::cout << "Coordinate: " << std::setw(2) << worst_value.coordinate + << " Expected: " << worst_value.expected_value << " Actual: " << worst_value.actual_value + << " Diff: " << worst_diff << " abs_threshold: " << worst_value.threshold << " (worst of " + << incorrect_values_abs.size() << ")\n"; + i = 1; + } + } else { + for (; i < incorrect_values_abs.size() && i < max_num_to_print; ++i) { + auto val = incorrect_values_abs[i]; + std::cout << "Coordinate: " << std::setw(2) << val.coordinate << " Expected: " << val.expected_value + << " Actual: " << val.actual_value + << " Diff: " << std::fabs(val.expected_value - val.actual_value) + << " abs_threshold: " << val.threshold << "\n"; + } } if constexpr (max_num_to_print > 1) { diff --git a/src/tests/test_utils/common_test_utils/src/ov_test_utils.cpp b/src/tests/test_utils/common_test_utils/src/ov_test_utils.cpp index fc2871ae3b17..40be0e95cd90 100644 --- a/src/tests/test_utils/common_test_utils/src/ov_test_utils.cpp +++ b/src/tests/test_utils/common_test_utils/src/ov_test_utils.cpp @@ -4,11 +4,15 @@ #include "common_test_utils/ov_test_utils.hpp" +#include + #include "common_test_utils/file_utils.hpp" #include "common_test_utils/ov_plugin_cache.hpp" #include "common_test_utils/test_constants.hpp" #include "openvino/op/tensor_iterator.hpp" +#include "openvino/op/util/node_util.hpp" #include "openvino/runtime/core.hpp" +#include "openvino/util/common_util.hpp" #include "openvino/util/file_util.hpp" #include "template/properties.hpp" @@ -142,7 +146,33 @@ ov::TensorVector infer_on_template(const std::shared_ptr& model, auto infer_request = compiled_model.create_infer_request(); for (auto& input : inputs) { - infer_request.set_tensor(input.first, input.second); + try { + infer_request.set_tensor(input.first, input.second); + } catch (const std::exception& ex) { + const auto& src_param = input.first; + const auto& src_tensor = input.second; + const ov::Output src_port = src_param->output(0); + std::ostringstream ports_dump; + const auto& compiled_inputs = compiled_model.inputs(); + for (size_t i = 0; i < compiled_inputs.size(); ++i) { + const auto& cp = compiled_inputs[i]; + ports_dump << "\n port[" << i << "] " << ov::util::make_default_tensor_name(cp) + << " shape=" << cp.get_shape() << " tensor_names={" + << ov::util::join(cp.get_tensor().get_names(), ",") << "}"; + } + OPENVINO_THROW("infer_on_template: set_tensor failed. Source parameter: ", + ov::util::make_default_tensor_name(src_port), + ", tensor_names={", + ov::util::join(src_port.get_tensor().get_names(), ","), + "}, expected_shape=", + src_port.get_shape(), + ", got_tensor_shape=", + src_tensor.get_shape(), + ". Compiled model inputs:", + ports_dump.str(), + ". Original error: ", + ex.what()); + } } infer_request.infer(); diff --git a/src/tests/test_utils/common_test_utils/src/postgres_helpers.cpp b/src/tests/test_utils/common_test_utils/src/postgres_helpers.cpp index aaee1846b38e..aec4f704b0a9 100644 --- a/src/tests/test_utils/common_test_utils/src/postgres_helpers.cpp +++ b/src/tests/test_utils/common_test_utils/src/postgres_helpers.cpp @@ -4,6 +4,9 @@ #include "common_test_utils/postgres_helpers.hpp" +#include +#include + namespace ov { namespace test { namespace utils { @@ -26,18 +29,6 @@ fnPQgetisnull PQgetisnull; fnPQclear PQclear; fnPQresultErrorMessage PQresultErrorMessage; -const char* PGPrefix(const char* text, ::testing::internal::GTestColor color) { -#ifdef __GNUC__ -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wformat-security" -#endif - ::testing::internal::ColoredPrintf(color, text); -#ifdef __GNUC__ -#pragma GCC diagnostic pop -#endif - return ""; -} - PGresultHolder PostgreSQLConnection::common_query(const char* query) { #ifdef PGQL_DEBUG std::cerr << query << std::endl; diff --git a/src/tests/test_utils/common_test_utils/src/subgraph_builders/weights_decompression_builders.cpp b/src/tests/test_utils/common_test_utils/src/subgraph_builders/weights_decompression_builders.cpp index dd28bfb21e2d..f4a0d762b8a0 100644 --- a/src/tests/test_utils/common_test_utils/src/subgraph_builders/weights_decompression_builders.cpp +++ b/src/tests/test_utils/common_test_utils/src/subgraph_builders/weights_decompression_builders.cpp @@ -236,11 +236,14 @@ std::shared_ptr initMatMulDecompressionSubgraphQuantization( transformed_weights_shape.insert(transformed_weights_shape.begin() + in_channel_idx + 1, group_size); } - // Step 1: Generate FP32 weights for real quantization - auto fp32_weights_tensor = - ov::test::utils::create_and_fill_tensor(ov::element::f32, - transformed_weights_shape, - ov::test::utils::InputGenerateData(1, 6, 8, seed)); + // mt19937 distribution: gtest's LCG repeats every k_range, collapsing per-group min/max diversity. + const float fp32_min = -0.1f; + const float fp32_max = 0.1f; + auto fp32_weights_tensor = ov::test::utils::create_and_fill_tensor_real_distribution(ov::element::f32, + transformed_weights_shape, + fp32_min, + fp32_max, + static_cast(seed)); // Calculate quantization parameters const auto qmin = weights_precision == ov::element::u2 ? 0.0f diff --git a/src/tests/test_utils/common_test_utils/src/test_case.cpp b/src/tests/test_utils/common_test_utils/src/test_case.cpp index 814e5b6f3a75..6b6cd6d19d16 100644 --- a/src/tests/test_utils/common_test_utils/src/test_case.cpp +++ b/src/tests/test_utils/common_test_utils/src/test_case.cpp @@ -194,6 +194,12 @@ testing::AssertionResult TestCase::compare_results_with_tolerance_as_fp(float to case element::Type_t::f32: comparison_result = compare_with_fp_tolerance(exp_result, result_tensor, tolerance); break; + case element::Type_t::f16: { + auto exp_f32 = ov::test::utils::make_tensor_with_precision_convert(exp_result, ov::element::f32); + auto res_f32 = ov::test::utils::make_tensor_with_precision_convert(result_tensor, ov::element::f32); + comparison_result = compare_with_fp_tolerance(exp_f32, res_f32, tolerance); + break; + } case element::Type_t::i32: comparison_result = compare_values(exp_result, result_tensor, 0); break; diff --git a/src/tests/test_utils/common_test_utils/src/test_common.cpp b/src/tests/test_utils/common_test_utils/src/test_common.cpp index 39fdb93e8fa0..a9494b3f3980 100644 --- a/src/tests/test_utils/common_test_utils/src/test_common.cpp +++ b/src/tests/test_utils/common_test_utils/src/test_common.cpp @@ -30,9 +30,12 @@ TestsCommon::TestsCommon() #endif { #ifndef __APPLE__ // TODO: add getVmSizeInKB() for Apple platform - auto memsize = ov::test::utils::getVmSizeInKB(); - if (memsize != 0) { - std::cout << "\nMEM_USAGE=" << memsize << "KB\n"; + static const bool print_mem_usage = std::getenv("OV_TEST_PRINT_MEM_USAGE") != nullptr; + if (print_mem_usage) { + auto memsize = ov::test::utils::getVmSizeInKB(); + if (memsize != 0) { + std::cout << "\nMEM_USAGE=" << memsize << "KB\n"; + } } #endif ov::threading::executor_manager()->clear(); diff --git a/src/tests/test_utils/common_test_utils/src/unicode_utils.cpp b/src/tests/test_utils/common_test_utils/src/unicode_utils.cpp index 58d83ef5a497..a1f7274173a1 100644 --- a/src/tests/test_utils/common_test_utils/src/unicode_utils.cpp +++ b/src/tests/test_utils/common_test_utils/src/unicode_utils.cpp @@ -36,13 +36,4 @@ std::filesystem::path UnicodePathTest::get_path_param() const { GetParam()); } -INSTANTIATE_TEST_SUITE_P(string_paths, UnicodePathTest, testing::Values("test_folder")); -INSTANTIATE_TEST_SUITE_P(u16_paths, UnicodePathTest, testing::Values(u"test_folder")); -INSTANTIATE_TEST_SUITE_P(u32_paths, UnicodePathTest, testing::Values(U"test_folder")); -INSTANTIATE_TEST_SUITE_P(wstring_paths, UnicodePathTest, testing::Values(L"test_folder")); - -#ifdef OPENVINO_ENABLE_UNICODE_PATH_SUPPORT -INSTANTIATE_TEST_SUITE_P(unicode_paths, UnicodePathTest, unicode_paths); -#endif - } // namespace ov::test diff --git a/src/tests/test_utils/common_test_utils/tests/common_util_test.cpp b/src/tests/test_utils/common_test_utils/tests/common_util_test.cpp new file mode 100644 index 000000000000..04399c5ace30 --- /dev/null +++ b/src/tests/test_utils/common_test_utils/tests/common_util_test.cpp @@ -0,0 +1,185 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include + +#include "common_test_utils/common_utils.hpp" +#include "openvino/util/common_util.hpp" +namespace ov::test { + +using CommonUtilsTest = testing::Test; + +TEST_F(CommonUtilsTest, ltrim) { + EXPECT_EQ("space test", util::ltrim(" space test")); + EXPECT_EQ("tab,linefeed test", util::ltrim("\t\ntab,linefeed test")); + EXPECT_EQ("", util::ltrim("")); + EXPECT_EQ("", util::ltrim(" \r ")); + EXPECT_EQ("carriage test ", util::ltrim("\rcarriage test ")); + EXPECT_EQ("vertical tab test ", util::ltrim("\v vertical tab test ")); +} + +TEST_F(CommonUtilsTest, rtrim) { + EXPECT_EQ("space test", util::rtrim("space test ")); + EXPECT_EQ("\ttab,linefeed test", util::rtrim("\ttab,linefeed test\t\n")); + EXPECT_EQ("", util::rtrim("")); + EXPECT_EQ("", util::rtrim(" \r ")); + EXPECT_EQ(" carriage test", util::rtrim(" carriage test \r")); + EXPECT_EQ("\v vertical tab test", util::rtrim("\v vertical tab test \v")); +} + +TEST_F(CommonUtilsTest, trim) { + EXPECT_EQ("space test", util::trim(" space test ")); + EXPECT_EQ("tab,linefeed test", util::trim("\t\ntab,linefeed test\t\n")); + EXPECT_EQ("", util::trim("")); + EXPECT_EQ("", util::trim(" \r ")); + EXPECT_EQ("carriage test", util::trim("\rcarriage test \r")); + EXPECT_EQ("vertical tab test", util::trim("\v vertical tab test \v")); +} + +TEST_F(CommonUtilsTest, parse_views_into_container_empty_fields) { + std::vector res; + util::view_transform(" 1, , 2, 3", std::back_inserter(res), ","); + EXPECT_EQ((std::vector{" 1"," ", " 2", " 3"}), res); +} + +TEST_F(CommonUtilsTest, parse_views_into_container_trailing_separator) { + std::vector res; + util::view_transform("1,2,", std::back_inserter(res), ","); + EXPECT_EQ((std::vector{"1", "2", ""}), res); +} + +TEST_F(CommonUtilsTest, parse_views_into_container_empty_input) { + std::vector res; + util::view_transform("", std::back_inserter(res), ","); + EXPECT_TRUE(res.empty()); +} + +TEST_F(CommonUtilsTest, parse_views_into_container_custom_check) { + std::vector res; + util::view_transform_if(" 1,;2,; 3,; ", std::back_inserter(res), ",;", [](auto&& field) { + OPENVINO_ASSERT(!field.empty(), "Cannot get vector of fields! \" 1,;2,; 3,; \" is incorrect"); + return true; + }); + EXPECT_EQ((std::vector{" 1", "2", " 3", " "}), res); +} + +TEST_F(CommonUtilsTest, parse_views_into_container_custom_check_fail) { + std::vector res; + EXPECT_THROW(util::view_transform_if(" 1,, 2, 3, ", std::back_inserter(res), ",", [](auto&& field) { + OPENVINO_ASSERT(!field.empty(), "Cannot get vector of fields! \" 1, , 2, 3, \" is incorrect"); + return true; + }, [](auto&& field) { + return std::string{field}; + } ), + ov::AssertFailure); +} + +TEST_F(CommonUtilsTest, parse_views_into_container_arithmetic) { + std::vector res{5,5,5}; + util::view_transform("1,2,3", std::back_inserter(res), ",", + [](auto&& field) { return util::view_to_number(field).value_or(0); }); + EXPECT_EQ((std::vector{5,5,5,1, 2, 3}), res); +} + +TEST_F(CommonUtilsTest, parse_views_into_container_arithmetic_fail) { + std::vector res(3); + ASSERT_NO_THROW(util::view_transform("1, a, 3 ", res.begin(),",", + [](auto&& field) { return util::view_to_number(field).value_or(0); })); + EXPECT_EQ((std::vector{1, 0, 0}), res); +} + +TEST_F(CommonUtilsTest, parse_views_into_container_arithmetic_custom_check_fail) { + std::vector res; + EXPECT_THROW(util::view_transform("1,,3", std::back_inserter(res), ",", [](auto&& field) { + OPENVINO_ASSERT(!field.empty(), "Cannot get vector of fields! \" 1, a, 3 \" is incorrect"); + return true; + }), + ov::AssertFailure); +} + +TEST_F(CommonUtilsTest, split_to_views_default_separator) { + EXPECT_EQ((std::vector{"1", "2", "3"}), util::split("1,2,3")); +} + +TEST_F(CommonUtilsTest, split_to_views_custom_separator) { + EXPECT_EQ((std::vector{"1", "2", "3"}), util::split("1;2;3", ";")); +} + +TEST_F(CommonUtilsTest, split_to_views_custom_check) { + EXPECT_EQ((std::vector{"1", "2", "3"}), + util::split("1;2;3", ";", [](auto&& field) { + OPENVINO_ASSERT(!field.empty(), "Cannot get vector of fields! \" 1;2;3 \" is incorrect"); + return true; + })); +} + +TEST_F(CommonUtilsTest, split_to_views_custom_check_fail) { + EXPECT_THROW(util::split( + "1;;3", + ";", + [](auto&& field) { + OPENVINO_ASSERT(!field.empty(), "Cannot get vector of fields! \" 1;;3 \" is incorrect"); + return true; + }), + ov::AssertFailure); +} + +TEST_F(CommonUtilsTest, split_to_views_lower) { + const std::string test_str{"Test1,Test2,Test3"}; + EXPECT_EQ((std::vector{"test1", "test2", "test3"}), util::split(util::to_lower(test_str))); +} + +TEST_F(CommonUtilsTest, split_empty_strings) { + using testing::ElementsAre; + EXPECT_THAT(util::split(""), testing::IsEmpty()); + EXPECT_THAT(util::split(","), ElementsAre("", "")); + EXPECT_THAT(util::split(",,"), ElementsAre("", "", "")); + EXPECT_THAT(util::split("test,"), ElementsAre("test", "")); + EXPECT_THAT(util::split(",test"), ElementsAre("", "test")); +} + +TEST_F(CommonUtilsTest, view_to_integral_number){ + EXPECT_EQ(123, util::view_to_number("123").value()); + EXPECT_EQ(-123, util::view_to_number("-123").value()); + EXPECT_EQ(123, util::view_to_number("123abc").value()); +} + +TEST_F(CommonUtilsTest, view_to_number_invalid_input){ + EXPECT_FALSE(util::view_to_number("abc").has_value()); + EXPECT_FALSE(util::view_to_number("").has_value()); + + EXPECT_FALSE(util::view_to_number("abc").has_value()); + EXPECT_FALSE(util::view_to_number("").has_value()); + + EXPECT_FALSE(util::view_to_number("abc").has_value()); + EXPECT_FALSE(util::view_to_number("").has_value()); + + EXPECT_FALSE(util::view_to_number("abc").has_value()); + EXPECT_FALSE(util::view_to_number("").has_value()); +} + +TEST_F(CommonUtilsTest, view_to_floating_point_number){ + EXPECT_FLOAT_EQ(123.456f, util::view_to_number("123.456").value()); + EXPECT_FLOAT_EQ(-123.456f, util::view_to_number("-123.456").value()); + EXPECT_FLOAT_EQ(-std::numeric_limits::infinity(), util::view_to_number("-inf").value()); + EXPECT_FLOAT_EQ(std::numeric_limits::infinity(), util::view_to_number("inf").value()); + EXPECT_TRUE(std::isnan(util::view_to_number("nan").value())); + + EXPECT_DOUBLE_EQ(123.456, util::view_to_number("123.456").value()); + EXPECT_DOUBLE_EQ(-123.456, util::view_to_number("-123.456").value()); + EXPECT_DOUBLE_EQ(-std::numeric_limits::infinity(), util::view_to_number("-inf").value()); + EXPECT_DOUBLE_EQ(std::numeric_limits::infinity(), util::view_to_number("inf").value()); + EXPECT_TRUE(std::isnan(util::view_to_number("nan").value())); +} + +TEST_F(CommonUtilsTest, ends_with) { + EXPECT_TRUE(util::ends_with("test.cpp", ".cpp")); + EXPECT_TRUE(util::ends_with("test.cpp", "cpp")); + EXPECT_TRUE(util::ends_with("test.cpp", "")); + EXPECT_FALSE(util::ends_with("test.cpp", ".h")); + EXPECT_FALSE(util::ends_with("test.cpp", "cpp ")); + EXPECT_FALSE(util::ends_with("d", ".cpp")); + EXPECT_FALSE(util::ends_with("test.bin.bak", ".bin")); +} +} // namespace ov::test diff --git a/src/tests/test_utils/common_test_utils/tests/file_util_test.cpp b/src/tests/test_utils/common_test_utils/tests/file_util_test.cpp index 5488405ea556..45c64db8ce79 100644 --- a/src/tests/test_utils/common_test_utils/tests/file_util_test.cpp +++ b/src/tests/test_utils/common_test_utils/tests/file_util_test.cpp @@ -88,33 +88,6 @@ TEST(file_util, path_join) { } } -TEST(file_util, sanitize_path) { - { - string path = "../../tensor.data"; - EXPECT_STREQ("tensor.data", ov::util::sanitize_path(path).c_str()); - } - { - string path = "/../tensor.data"; - EXPECT_STREQ("tensor.data", ov::util::sanitize_path(path).c_str()); - } - { - string path = ".."; - EXPECT_STREQ("", ov::util::sanitize_path(path).c_str()); - } - { - string path = "workspace/data/tensor.data"; - EXPECT_STREQ("workspace/data/tensor.data", ov::util::sanitize_path(path).c_str()); - } - { - string path = "..\\..\\tensor.data"; - EXPECT_STREQ("tensor.data", ov::util::sanitize_path(path).c_str()); - } - { - string path = "C:\\workspace\\tensor.data"; - EXPECT_STREQ("workspace\\tensor.data", ov::util::sanitize_path(path).c_str()); - } -} - using namespace testing; class TrimFileTest : public Test { @@ -676,4 +649,48 @@ TEST_P(FileUtilTestP, create_directories) { std::filesystem::remove_all(test_dir); } + +class SanitizePathTest : public ::testing::Test { +protected: + std::filesystem::path base{std::filesystem::temp_directory_path() / "ov_sanitize_test_base"}; +}; + +TEST_F(SanitizePathTest, valid_nested_relative_path) { + const auto result = ov::util::sanitize_path(base, "workspace/data/tensor.data"); + EXPECT_EQ(result, base / "workspace/data/tensor.data"); +} + +TEST_F(SanitizePathTest, invalid_single_dotdot) { + EXPECT_THROW(ov::util::sanitize_path(base, ".."), std::runtime_error); +} + +TEST_F(SanitizePathTest, invalid_dotdot_prefix) { + EXPECT_THROW(ov::util::sanitize_path(base, "../tensors_data/tensor.data"), std::runtime_error); +} + +TEST_F(SanitizePathTest, invalid_absolute_path_outside_base) { + EXPECT_THROW(ov::util::sanitize_path(base, "/../tensor.data"), std::runtime_error); +} + +#ifdef _WIN32 +TEST_F(SanitizePathTest, invalid_windows_absolute_path_different_drive) { + EXPECT_THROW(ov::util::sanitize_path(base, "C:\\workspace\\tensor.data"), std::runtime_error); +} + +TEST_F(SanitizePathTest, invalid_windows_dotdot_backslash) { + EXPECT_THROW(ov::util::sanitize_path(base, "..\\..\\tensor.data"), std::runtime_error); +} +#endif + +TEST_F(SanitizePathTest, empty_dir_uses_cwd) { + const auto result = ov::util::sanitize_path("", "tensors/data.bin"); + const auto expected = ov::util::get_absolute_file_path( + std::filesystem::weakly_canonical(std::filesystem::current_path() / "tensors/data.bin")); + EXPECT_EQ(result, expected); +} + +TEST_F(SanitizePathTest, empty_dir_dotdot_still_rejected) { + EXPECT_THROW(ov::util::sanitize_path("", ".."), std::runtime_error); +} + } // namespace ov::test diff --git a/src/tests/test_utils/common_test_utils/tests/graph_comparator_tests.cpp b/src/tests/test_utils/common_test_utils/tests/graph_comparator_tests.cpp index 07abd7feb146..4927de8154f4 100644 --- a/src/tests/test_utils/common_test_utils/tests/graph_comparator_tests.cpp +++ b/src/tests/test_utils/common_test_utils/tests/graph_comparator_tests.cpp @@ -5,16 +5,19 @@ #include #include "common_test_utils/graph_comparator.hpp" +#include "common_test_utils/ov_test_utils.hpp" #include "openvino/op/add.hpp" #include "openvino/op/convert.hpp" #include "openvino/op/convolution.hpp" #include "openvino/op/gru_cell.hpp" #include "openvino/op/multiply.hpp" #include "openvino/op/negative.hpp" +#include "openvino/op/relu.hpp" #include "openvino/op/squeeze.hpp" #include "openvino/op/tensor_iterator.hpp" #include "openvino/op/unsqueeze.hpp" #include "openvino/op/util/variable.hpp" +#include "openvino/runtime/tensor.hpp" TEST(GraphComparatorTests, AllEnablePositiveCheck) { FunctionsComparator comparator(FunctionsComparator::no_default()); @@ -698,3 +701,30 @@ TEST(GraphComparatorTests, CheckConsumersCountNegative) { auto res = comparator.compare(function, function_ref); ASSERT_FALSE(res.valid) << res.message; } + +TEST(InferOnTemplateTests, ShapeMismatchDiagnosticContainsPortInfo) { + auto input = std::make_shared(ov::element::f32, ov::Shape{4}); + input->set_friendly_name("my_param"); + input->get_output_tensor(0).set_names({"my_tensor"}); + auto relu = std::make_shared(input); + auto model = std::make_shared(ov::OutputVector{relu}, ov::ParameterVector{input}); + + ov::Tensor wrong_shape_tensor(ov::element::f32, ov::Shape{8}); + std::map, ov::Tensor> inputs{{input, wrong_shape_tensor}}; + + std::string what; + try { + ov::test::utils::infer_on_template(model, inputs); + FAIL() << "infer_on_template was expected to throw on shape mismatch"; + } catch (const std::exception& ex) { + what = ex.what(); + } + + EXPECT_NE(what.find("infer_on_template: set_tensor failed"), std::string::npos) << what; + EXPECT_NE(what.find("my_param"), std::string::npos) << what; + EXPECT_NE(what.find("my_tensor"), std::string::npos) << what; + EXPECT_NE(what.find("expected_shape="), std::string::npos) << what; + EXPECT_NE(what.find("got_tensor_shape="), std::string::npos) << what; + EXPECT_NE(what.find("Compiled model inputs:"), std::string::npos) << what; + EXPECT_NE(what.find("Original error:"), std::string::npos) << what; +} diff --git a/src/tests/test_utils/common_test_utils/tests/memory_test.cpp b/src/tests/test_utils/common_test_utils/tests/memory_test.cpp new file mode 100644 index 000000000000..1db1ae773ccf --- /dev/null +++ b/src/tests/test_utils/common_test_utils/tests/memory_test.cpp @@ -0,0 +1,150 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include + +#include + +#include "openvino/util/memory.hpp" + +namespace ov::test { + +// Verify the constexpr contract at compile time for the most common alignments. +static_assert(ov::util::align_size_up(0, 64) == 0); +static_assert(ov::util::align_size_up(1, 64) == 64); +static_assert(ov::util::align_size_up(64, 64) == 64); +static_assert(ov::util::align_size_up(65, 64) == 128); +static_assert(ov::util::align_size_up(100, 16) == 112); +static_assert(ov::util::align_size_up(100, 1) == 100); +static_assert(ov::util::align_size_up(7, 8) == 8); +static_assert(ov::util::align_size_up(8, 8) == 8); +static_assert(ov::util::align_size_up(9, 8) == 16); +static_assert(ov::util::align_size_up(9, alignof(std::max_align_t)) == 16); + +static_assert(ov::util::align_size_down(0, 64) == 0); +static_assert(ov::util::align_size_down(63, 64) == 0); +static_assert(ov::util::align_size_down(64, 64) == 64); +static_assert(ov::util::align_size_down(65, 64) == 64); +static_assert(ov::util::align_size_down(100, 16) == 96); +static_assert(ov::util::align_size_down(100, 1) == 100); + +static_assert(ov::util::align_region(64, 32, 64).m_address == 64); +static_assert(ov::util::align_region(64, 32, 64).m_length == 32); +static_assert(ov::util::align_region(64, 32, 64).m_gap == 0); +static_assert(ov::util::align_region(65, 32, 64).m_address == 64); +static_assert(ov::util::align_region(65, 32, 64).m_length == 33); +static_assert(ov::util::align_region(65, 32, 64).m_gap == 1); + +using AlignSizeUpTest = testing::Test; + +TEST_F(AlignSizeUpTest, already_aligned_value_is_unchanged) { + EXPECT_EQ(64u, util::align_size_up(64, 64)); + EXPECT_EQ(128u, util::align_size_up(128, 64)); + EXPECT_EQ(16u, util::align_size_up(16, 16)); +} + +TEST_F(AlignSizeUpTest, unaligned_value_is_rounded_up) { + EXPECT_EQ(64u, util::align_size_up(1, 64)); + EXPECT_EQ(64u, util::align_size_up(63, 64)); + EXPECT_EQ(128u, util::align_size_up(65, 64)); + EXPECT_EQ(112u, util::align_size_up(100, 16)); +} + +TEST_F(AlignSizeUpTest, alignment_1_never_rounds) { + EXPECT_EQ(0u, util::align_size_up(0, 1)); + EXPECT_EQ(100u, util::align_size_up(100, 1)); + EXPECT_EQ(255u, util::align_size_up(255, 1)); +} + +TEST_F(AlignSizeUpTest, zero_size_returns_zero) { + EXPECT_EQ(0u, util::align_size_up(0, 64)); + EXPECT_EQ(0u, util::align_size_up(0, 4096)); +} + +using AlignSizeDownTest = testing::Test; + +TEST_F(AlignSizeDownTest, already_aligned_value_is_unchanged) { + EXPECT_EQ(64u, util::align_size_down(64, 64)); + EXPECT_EQ(128u, util::align_size_down(128, 64)); + EXPECT_EQ(16u, util::align_size_down(16, 16)); +} + +TEST_F(AlignSizeDownTest, unaligned_value_is_rounded_down) { + EXPECT_EQ(0u, util::align_size_down(1, 64)); + EXPECT_EQ(0u, util::align_size_down(63, 64)); + EXPECT_EQ(64u, util::align_size_down(65, 64)); + EXPECT_EQ(96u, util::align_size_down(100, 16)); +} + +TEST_F(AlignSizeDownTest, alignment_1_never_rounds) { + EXPECT_EQ(0u, util::align_size_down(0, 1)); + EXPECT_EQ(100u, util::align_size_down(100, 1)); + EXPECT_EQ(255u, util::align_size_down(255, 1)); +} + +TEST_F(AlignSizeDownTest, zero_size_returns_zero) { + EXPECT_EQ(0u, util::align_size_down(0, 64)); + EXPECT_EQ(0u, util::align_size_down(0, 4096)); +} + +using AlignRegionTest = testing::Test; + +TEST_F(AlignRegionTest, aligned_base_has_zero_gap) { + const auto r = util::align_region(64, 32, 64); + EXPECT_EQ(64u, r.m_address); + EXPECT_EQ(32u, r.m_length); + EXPECT_EQ(0u, r.m_gap); +} + +TEST_F(AlignRegionTest, unaligned_base_is_rounded_down) { + const auto r = util::align_region(65, 32, 64); + EXPECT_EQ(64u, r.m_address); + EXPECT_EQ(33u, r.m_length); + EXPECT_EQ(1u, r.m_gap); +} + +TEST_F(AlignRegionTest, gap_equals_base_minus_address) { + const auto r = util::align_region(100, 50, 64); + EXPECT_EQ(64u, r.m_address); + EXPECT_EQ(86u, r.m_length); + EXPECT_EQ(36u, r.m_gap); +} + +TEST_F(AlignRegionTest, result_covers_original_range) { + for (uintptr_t base : {0u, 1u, 63u, 64u, 65u, 100u, 127u}) { + const auto r = util::align_region(base, 128, 64); + EXPECT_LE(r.m_address, base) << "base=" << base; + EXPECT_GE(r.m_address + r.m_length, base + 128u) << "base=" << base; + } +} + +using AlignedAllocTest = testing::Test; + +TEST_F(AlignedAllocTest, returns_non_null_for_valid_args) { + void* ptr = util::aligned_alloc(128, 64); + ASSERT_NE(nullptr, ptr); + util::aligned_free(ptr); +} + +TEST_F(AlignedAllocTest, pointer_satisfies_requested_alignment) { + for (auto align : {1u, 2u, 4u, 8u, 16u, 32u, 64u, 128u, 256u}) { + void* ptr = util::aligned_alloc(256, align); + ASSERT_NE(nullptr, ptr) << "align=" << align; + EXPECT_EQ(0u, reinterpret_cast(ptr) % align) << "align=" << align; + util::aligned_free(ptr); + } +} + +TEST_F(AlignedAllocTest, zero_alignment_uses_default_alignment) { + void* ptr = util::aligned_alloc(64, 0); + ASSERT_NE(nullptr, ptr); + EXPECT_EQ(0u, reinterpret_cast(ptr) % alignof(std::max_align_t)); + util::aligned_free(ptr); +} + +TEST_F(AlignedAllocTest, free_nullptr_is_noop) { + EXPECT_NO_FATAL_FAILURE(util::aligned_free(nullptr)); +} + +} // namespace ov::test diff --git a/src/tests/test_utils/functional_test_utils/src/summary/api_summary.cpp b/src/tests/test_utils/functional_test_utils/src/summary/api_summary.cpp index ce9bd6c3c635..6248c58e2120 100644 --- a/src/tests/test_utils/functional_test_utils/src/summary/api_summary.cpp +++ b/src/tests/test_utils/functional_test_utils/src/summary/api_summary.cpp @@ -5,6 +5,7 @@ #include "functional_test_utils/summary/api_summary.hpp" #include +#include #include "common_test_utils/file_utils.hpp" @@ -222,6 +223,9 @@ void ApiSummary::saveReport() { bool result = false; do { result = doc.save_file(outputFilePath.c_str()); + if (!result && std::chrono::system_clock::now() < exitTime) { + std::this_thread::sleep_for(std::chrono::milliseconds(500)); + } } while (!result && std::chrono::system_clock::now() < exitTime); if (!result) { diff --git a/src/tests/test_utils/functional_test_utils/src/summary/op_summary.cpp b/src/tests/test_utils/functional_test_utils/src/summary/op_summary.cpp index f1817b8f7fa7..8d1143c5e177 100644 --- a/src/tests/test_utils/functional_test_utils/src/summary/op_summary.cpp +++ b/src/tests/test_utils/functional_test_utils/src/summary/op_summary.cpp @@ -6,6 +6,7 @@ #include #include +#include #include "common_test_utils/file_utils.hpp" #include "functional_test_utils/summary/op_info.hpp" @@ -363,6 +364,9 @@ void OpSummary::saveReport() { bool result = false; do { result = doc.save_file(outputFilePath.c_str()); + if (!result && std::chrono::system_clock::now() < exitTime) { + std::this_thread::sleep_for(std::chrono::milliseconds(500)); + } } while (!result && std::chrono::system_clock::now() < exitTime); if (!result) { diff --git a/tests/constraints.txt b/tests/constraints.txt index c309407cba78..eacb259adc34 100644 --- a/tests/constraints.txt +++ b/tests/constraints.txt @@ -16,12 +16,12 @@ opencv-python>=4.5 paddlepaddle==3.3.0 protobuf>=3.21.12,<7.0.0 py>=1.9.0 -pytest>=5.0,<9.1 +pytest>=5.0,<9.2 pytest-dependency==0.6.1 pytest-html==4.2.0 pytest-timeout==2.4.0 -kornia==0.8.2 +kornia==0.8.3 --extra-index-url https://download.pytorch.org/whl/cpu -torch~=2.8.0 -torchvision~=0.23.0 +torch~=2.12.0 +torchvision~=0.27.0 diff --git a/tests/e2e_tests/common/logger.py b/tests/e2e_tests/common/logger.py index a5db344c1641..2f1bad910e5d 100644 --- a/tests/e2e_tests/common/logger.py +++ b/tests/e2e_tests/common/logger.py @@ -82,7 +82,7 @@ def __new__(cls) -> 'SensitiveKeysStrippingFilter': return cls.instance @classmethod - def build_sensitive_values_regexp(cls) -> re: + def build_sensitive_values_regexp(cls) -> re.Pattern: return re.compile( "|".join([r"{value}".format(value=var) for var in cls.sensitive_pairs.values()])) diff --git a/tests/e2e_tests/requirements.txt b/tests/e2e_tests/requirements.txt index f89f41528186..30a01c7e9493 100644 --- a/tests/e2e_tests/requirements.txt +++ b/tests/e2e_tests/requirements.txt @@ -17,7 +17,7 @@ scikit-image>=0.25.0; python_version >= '3.13' tabulate==0.10.0 pytest>=5.0,<=7.0.1; python_version < '3.10' -pytest==9.0.2; python_version >= '3.10' +pytest==9.0.3; python_version >= '3.10' pytest-cov==7.1.0 pytest-html==4.2.0 pytest-json-report==1.5.0 diff --git a/tests/fuzz/fuzz-testhelper/fuzz-utils.cc b/tests/fuzz/fuzz-testhelper/fuzz-utils.cc index ef9bbc18155b..bd6998c506a9 100644 --- a/tests/fuzz/fuzz-testhelper/fuzz-utils.cc +++ b/tests/fuzz/fuzz-testhelper/fuzz-utils.cc @@ -1,15 +1,19 @@ // Copyright (C) 2018-2026 Intel Corporation // SPDX-License-Identifier: Apache-2.0 // - #include "fuzz-utils.h" + #include -#include -#include #include +#include +#include + #ifndef _WIN32 #include -#endif // _WIN32 +#include +#include +#endif + MemoryFile::MemoryFile(const void *data, size_t size) { #ifdef _WIN32 @@ -38,3 +42,59 @@ MemoryFile::~MemoryFile() { free(m_name); #endif // _WIN32 } + + +bool write_file(const std::filesystem::path& p, std::string_view data) { + std::ofstream out(p, std::ios::binary); + if (!out) return false; + if (!data.empty()) + out.write(data.data(), static_cast(data.size())); + return out.good(); +} + +uint64_t next_id() { + static std::atomic id{0}; + return ++id; +} + +std::filesystem::path& temp_root() { + static std::filesystem::path root = []{ + std::filesystem::path dir = std::filesystem::temp_directory_path() / "openvino_read_model_fuzz"; + std::error_code ec; + std::filesystem::create_directories(dir, ec); + return ec ? std::filesystem::temp_directory_path() : dir; + }(); + return root; +} + + +std::filesystem::path create_model_file(const uint8_t* data, size_t size, const std::filesystem::path& ext) { + auto name = std::filesystem::path("model_" + std::to_string(next_id())); + name += ext; + const auto path = temp_root() / name; + if (!write_file(path, std::string_view(reinterpret_cast(data), size))) { + throw std::runtime_error("Write to file failed"); + } + return path; +} + + +std::array split_data(std::string_view data, std::string_view delim) { + const auto pos = data.find(delim); + if (pos == std::string_view::npos) + throw std::runtime_error("Problem detected during splitting fuzzer input data"); + return {data.substr(0, pos), data.substr(pos + delim.size())}; +} + + +std::tuple create_ir_model_files(const uint8_t* data, size_t size, std::string_view delim) { + const auto [xml_sv, bin_sv] = split_data({reinterpret_cast(data), size}, delim); + const auto stem = std::string("model_") + std::to_string(next_id()); + const auto xml_path = temp_root() / (stem + ".xml"); + const auto bin_path = temp_root() / (stem + ".bin"); + if (!write_file(xml_path, xml_sv)) + throw std::runtime_error("Write to xml file failed"); + if (!write_file(bin_path, bin_sv)) + throw std::runtime_error("Write to bin file failed"); + return {xml_path, bin_path}; +} \ No newline at end of file diff --git a/tests/fuzz/fuzz-testhelper/fuzz-utils.h b/tests/fuzz/fuzz-testhelper/fuzz-utils.h index 85e34240b73c..ee6aa732ab24 100644 --- a/tests/fuzz/fuzz-testhelper/fuzz-utils.h +++ b/tests/fuzz/fuzz-testhelper/fuzz-utils.h @@ -1,8 +1,15 @@ // Copyright (C) 2018-2026 Intel Corporation // SPDX-License-Identifier: Apache-2.0 // +#pragma once #include +#include +#include +#include +#include +#include +#include class MemoryFile { public: @@ -17,3 +24,18 @@ class MemoryFile { private: char *m_name; }; + + +struct ScopedRemove { + std::filesystem::path path; + ~ScopedRemove() { + std::error_code ec; + if (!path.empty()) + std::filesystem::remove(path, ec); + } +}; + + +std::array split_data(std::string_view data, std::string_view delim); +std::filesystem::path create_model_file(const uint8_t* data, size_t size, const std::filesystem::path& ext); +std::tuple create_ir_model_files(const uint8_t* data, size_t size, std::string_view delim); diff --git a/tests/fuzz/src/read_model_ir-fuzzer.cc b/tests/fuzz/src/read_model_ir-fuzzer.cc new file mode 100644 index 000000000000..1fea1e8d9f67 --- /dev/null +++ b/tests/fuzz/src/read_model_ir-fuzzer.cc @@ -0,0 +1,25 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 + +#include "openvino/openvino.hpp" +#include "fuzz-utils.h" + +constexpr std::string_view kSplitSequence = "FUZZ_NEXT_FIELD"; + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { + ov::Core core; + + try { + const auto& [xml_path, bin_path] = create_ir_model_files(data, size, kSplitSequence); + ScopedRemove cleanup_xml{xml_path}; + ScopedRemove cleanup_bin{bin_path}; + + if (const auto model = core.read_model(xml_path); model) { + model->get_name(); + model->outputs(); + model->inputs(); + } + } + catch (...) {} + return 0; +} \ No newline at end of file diff --git a/tests/fuzz/src/read_model_onnx-fuzzer.cc b/tests/fuzz/src/read_model_onnx-fuzzer.cc new file mode 100644 index 000000000000..a45bd855442a --- /dev/null +++ b/tests/fuzz/src/read_model_onnx-fuzzer.cc @@ -0,0 +1,22 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// +#include "openvino/openvino.hpp" +#include "fuzz-utils.h" + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { + ov::Core core; + + try { + const auto model_file = create_model_file(data, size, ".onnx"); + ScopedRemove cleanup{model_file}; + + if (const auto model = core.read_model(model_file); model) { + model->get_name(); + model->outputs(); + model->inputs(); + } + } catch (...) {} + + return 0; +} diff --git a/tests/fuzz/src/read_model_paddle-fuzzer.cc b/tests/fuzz/src/read_model_paddle-fuzzer.cc new file mode 100644 index 000000000000..83994a0e1806 --- /dev/null +++ b/tests/fuzz/src/read_model_paddle-fuzzer.cc @@ -0,0 +1,25 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "openvino/openvino.hpp" +#include "fuzz-utils.h" + +constexpr std::string_view kSplitSequence = "FUZZ_NEXT_FIELD"; + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { + ov::Core core; + try { + const auto [net_sv, weights_sv] = split_data({reinterpret_cast(data), size}, kSplitSequence); + + std::string net{net_sv}; + ov::Tensor weights(ov::element::u8, {weights_sv.size()}, const_cast(weights_sv.data())); + if (const auto model = core.read_model(net, weights); model) { + model->get_name(); + model->outputs(); + model->inputs(); + } + } catch (...) {} + + return 0; +} diff --git a/tests/fuzz/src/read_model_tflite-fuzzer.cc b/tests/fuzz/src/read_model_tflite-fuzzer.cc new file mode 100644 index 000000000000..2002ab281fb8 --- /dev/null +++ b/tests/fuzz/src/read_model_tflite-fuzzer.cc @@ -0,0 +1,21 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// +#include "openvino/openvino.hpp" +#include "fuzz-utils.h" + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { + ov::Core core; + try { + const auto model_file = create_model_file(data, size, ".tflite"); + ScopedRemove cleanup{model_file}; + + if (const auto model = core.read_model(model_file); model) { + model->get_name(); + model->outputs(); + model->inputs(); + } + } catch (...) {} + + return 0; +} diff --git a/tests/layer_tests/common/utils/common_utils.py b/tests/layer_tests/common/utils/common_utils.py index 37fe8441a155..91c8fa528ab0 100644 --- a/tests/layer_tests/common/utils/common_utils.py +++ b/tests/layer_tests/common/utils/common_utils.py @@ -43,10 +43,9 @@ def generate_ir_python_api(coverage=False, **kwargs): if 'output_dir' in kwargs: del kwargs['output_dir'] - compress_to_fp16 = False + compress_to_fp16 = True if 'compress_to_fp16' in kwargs: - # TODO 132871: fix error with no tensor name in case of compression - # compress_to_fp16 = kwargs['compress_to_fp16'] + compress_to_fp16 = kwargs['compress_to_fp16'] del kwargs['compress_to_fp16'] ov_model = convert_model(**kwargs) diff --git a/tests/layer_tests/ovc_python_api_tests/test_pytorch.py b/tests/layer_tests/ovc_python_api_tests/test_pytorch.py index 5046d3ba29ad..badefabf5809 100644 --- a/tests/layer_tests/ovc_python_api_tests/test_pytorch.py +++ b/tests/layer_tests/ovc_python_api_tests/test_pytorch.py @@ -585,7 +585,11 @@ def create_pytorch_module_convert_pytorch_frontend_oob(tmp_dir): class ConvModel(torch.nn.Module): def __init__(self): super(ConvModel, self).__init__() - self.weights = torch.rand([1, 3, 3, 3]) + # Deterministic weights with zero FP16 round-trip error so the + # CompressFloatConstants decision doesn't depend on a random state. + # 0.5 is exactly representable in FP16 (abs/rel round-trip error = 0), + # so compression always happens and the reference model below matches. + self.weights = torch.full([1, 3, 3, 3], 0.5) def forward(self, x): return F.conv2d(x, self.weights) @@ -1512,3 +1516,188 @@ def test_ovc_for_exported_program_on_disk(self, create_model, graph_ref, compare_tensor_names=False, ovc=True) os.remove(ep_file_name) + + +class TestPytorchModelOnDiskFileTypeDetection: + """Tests for the structural ZIP-based detection used by + get_pytorch_decoder_for_model_on_disk to skip non-PyTorch files + without importing torch or running pickle-based deserialization. + """ + + @staticmethod + def _make_zip(path, entries): + import zipfile + with zipfile.ZipFile(path, "w") as zf: + for name, data in entries.items(): + zf.writestr(name, data) + + @pytest.mark.precommit + @pytest.mark.nightly + def test_is_pytorch_zip_detects_torchscript_archive(self, tmp_path, ie_device, precision, ir_version): + from openvino.tools.ovc.moc_frontend.pytorch_frontend_utils import _is_pytorch_zip + + # Real TorchScript archive saved by torch.jit.save contains data.pkl + # and constants.pkl entries. Emulate the structural shape. + ts_path = str(tmp_path / "ts_like.zip") + self._make_zip(ts_path, {"model/data.pkl": b"\x80\x02", "model/constants.pkl": b""}) + assert _is_pytorch_zip(ts_path) is True + + @pytest.mark.precommit + @pytest.mark.nightly + def test_is_pytorch_zip_detects_legacy_exported_program(self, tmp_path, ie_device, precision, ir_version): + from openvino.tools.ovc.moc_frontend.pytorch_frontend_utils import _is_pytorch_zip + + # ExportedProgram archives in torch <= 2.6 contain serialized_*.json files. + ep_path = str(tmp_path / "ep_legacy.zip") + self._make_zip(ep_path, {"serialized_state_dict.json": b"{}"}) + assert _is_pytorch_zip(ep_path) is True + + @pytest.mark.precommit + @pytest.mark.nightly + def test_is_pytorch_zip_detects_pt2_archive(self, tmp_path, ie_device, precision, ir_version): + from openvino.tools.ovc.moc_frontend.pytorch_frontend_utils import _is_pytorch_zip + + # PT2 archives (torch >= 2.7) contain an "archive_format" entry == b"pt2". + pt2_path = str(tmp_path / "pt2.zip") + self._make_zip(pt2_path, {"archive_format": b"pt2", "data/blob": b"\x00"}) + assert _is_pytorch_zip(pt2_path) is True + + @pytest.mark.precommit + @pytest.mark.nightly + def test_is_pytorch_zip_rejects_non_zip_file(self, tmp_path, ie_device, precision, ir_version): + from openvino.tools.ovc.moc_frontend.pytorch_frontend_utils import _is_pytorch_zip + + path = tmp_path / "plain.bin" + path.write_bytes(b"not a zip file") + assert _is_pytorch_zip(str(path)) is False + + @pytest.mark.precommit + @pytest.mark.nightly + def test_is_pytorch_zip_rejects_unrelated_zip(self, tmp_path, ie_device, precision, ir_version): + from openvino.tools.ovc.moc_frontend.pytorch_frontend_utils import _is_pytorch_zip + + path = str(tmp_path / "unrelated.zip") + self._make_zip(path, {"foo.txt": b"hello", "bar/baz.bin": b"\x00\x01"}) + assert _is_pytorch_zip(path) is False + + @pytest.mark.precommit + @pytest.mark.nightly + def test_is_pytorch_zip_rejects_archive_format_with_other_content(self, tmp_path, ie_device, precision, ir_version): + from openvino.tools.ovc.moc_frontend.pytorch_frontend_utils import _is_pytorch_zip + + # An "archive_format" entry whose content is not b"pt2" must not be + # accepted as a PyTorch archive. + path = str(tmp_path / "other_archive.zip") + self._make_zip(path, {"archive_format": b"other"}) + assert _is_pytorch_zip(path) is False + + @pytest.mark.precommit + @pytest.mark.nightly + def test_is_pytorch_zip_rejects_oversized_archive_format(self, tmp_path, ie_device, precision, ir_version): + from openvino.tools.ovc.moc_frontend.pytorch_frontend_utils import _is_pytorch_zip + + # A crafted ZIP where the "archive_format" entry is larger than the + # legitimate b"pt2" marker must be rejected without being read into + # memory. This guards against CPU/memory amplification on auto-detect. + path = str(tmp_path / "oversized_archive_format.zip") + # 1 MiB of zeros; even highly compressible, must not be decompressed. + self._make_zip(path, {"archive_format": b"\x00" * (1 << 20)}) + assert _is_pytorch_zip(path) is False + + @pytest.mark.precommit + @pytest.mark.nightly + def test_is_pytorch_zip_handles_missing_file(self, tmp_path, ie_device, precision, ir_version): + from openvino.tools.ovc.moc_frontend.pytorch_frontend_utils import _is_pytorch_zip + + assert _is_pytorch_zip(str(tmp_path / "does_not_exist.zip")) is False + + @pytest.mark.precommit + @pytest.mark.nightly + def test_get_pytorch_decoder_skips_non_pytorch_file(self, tmp_path, ie_device, precision, ir_version): + """A non-PyTorch file on disk must short-circuit and return False + without populating argv (i.e. without attempting torch.jit.load / + torch.export.load on it). + """ + from openvino.tools.ovc.moc_frontend.pytorch_frontend_utils import ( + get_pytorch_decoder_for_model_on_disk, + ) + + path = tmp_path / "model.onnx" + path.write_bytes(b"\x08\x01\x12\x00not-a-pytorch-archive") + + class _Argv: + input_model = str(path) + framework = None + + argv = _Argv() + result = get_pytorch_decoder_for_model_on_disk(argv, {}) + + assert result is False + # input_model is left as the original string path; framework not set. + assert argv.input_model == str(path) + assert argv.framework is None + + @pytest.mark.precommit + @pytest.mark.nightly + def test_get_pytorch_decoder_skips_unrelated_zip(self, tmp_path, ie_device, precision, ir_version): + from openvino.tools.ovc.moc_frontend.pytorch_frontend_utils import ( + get_pytorch_decoder_for_model_on_disk, + ) + + path = str(tmp_path / "unrelated.zip") + self._make_zip(path, {"foo.txt": b"hello"}) + + class _Argv: + input_model = path + framework = None + + argv = _Argv() + result = get_pytorch_decoder_for_model_on_disk(argv, {}) + + assert result is False + assert argv.input_model == path + assert argv.framework is None + + @pytest.mark.precommit + @pytest.mark.nightly + def test_get_pytorch_decoder_rejects_non_path_input(self, ie_device, precision, ir_version): + from openvino.tools.ovc.moc_frontend.pytorch_frontend_utils import ( + get_pytorch_decoder_for_model_on_disk, + ) + + class _Argv: + input_model = 12345 + framework = None + + argv = _Argv() + # A non-(str/Path) input_model must early-return False. + assert get_pytorch_decoder_for_model_on_disk(argv, {}) is False + + @pytest.mark.precommit + @pytest.mark.nightly + def test_get_pytorch_decoder_loads_real_torchscript_file(self, tmp_path, ie_device, precision, ir_version): + """End-to-end: a genuine torch.jit-saved file must be picked up by + get_pytorch_decoder_for_model_on_disk and populate argv. + """ + from openvino.tools.ovc.moc_frontend.pytorch_frontend_utils import ( + get_pytorch_decoder_for_model_on_disk, + ) + + model = make_pt_model_one_input() + scripted = torch.jit.script(model) + path = str(tmp_path / "scripted.pt") + scripted.save(path) + + class _Argv: + input_model = path + framework = None + + argv = _Argv() + result = get_pytorch_decoder_for_model_on_disk( + argv, {"example_input": (torch.zeros(1, 3, 10, 10),)} + ) + + assert result is True + assert argv.framework == "pytorch" + # On success, argv.input_model is replaced with a TorchScriptPythonDecoder. + assert not isinstance(argv.input_model, str) diff --git a/tests/layer_tests/py_frontend_tests/test_torch_frontend.py b/tests/layer_tests/py_frontend_tests/test_torch_frontend.py index 035a71ca8f6c..cda59c2a7bdf 100644 --- a/tests/layer_tests/py_frontend_tests/test_torch_frontend.py +++ b/tests/layer_tests/py_frontend_tests/test_torch_frontend.py @@ -311,6 +311,15 @@ def forward(self, x): return torch.cos(x.to(torch.float32)) +class ModelWithCosModule(torch.nn.Module): + def __init__(self): + super().__init__() + self.cos_module = CosModel() + + def forward(self, x): + return self.cos_module(x) + + def test_op_extension(): from openvino.frontend.pytorch.ts_decoder import TorchScriptPythonDecoder from openvino.frontend.pytorch import OpExtension @@ -471,6 +480,365 @@ def forward(self, x): "Parameter", "Sin", "Tan", "Add", "Result"] +@pytest.mark.parametrize("match_key_fn", [ + pytest.param(lambda m: type(m.cos_module), id="by_class"), + pytest.param(lambda m: m.cos_module, id="by_instance"), + pytest.param(lambda m: "cos_module", id="by_name"), +]) +def test_module_extension_dynamo_match(match_key_fn): + """ModuleExtension matches by class, instance, or name with dynamo=True.""" + from openvino.frontend.pytorch import ModuleExtension, ConversionExtension + from openvino import convert_model + + def sin_op(context): + return ops.sin(context.get_input(0)).outputs() + + model = ModelWithCosModule() + converted_model = convert_model( + model, example_input=[torch.randn(100)], dynamo=True, + extension=[ + ModuleExtension(match_key_fn(model), "MySinOp"), + ConversionExtension("MySinOp", sin_op)]) + assert converted_model + assert [n.get_type_name() for n in converted_model.get_ordered_ops()] == [ + "Parameter", "Sin", "Result"] + assert converted_model.get_results()[0].get_output_partial_shape(0) == \ + PartialShape([100]) + + +def test_module_extension_dynamo_condition(): + """ModuleExtension condition callback controls whether extension is applied.""" + from openvino.frontend.pytorch import ModuleExtension, ConversionExtension + from openvino import convert_model + + def sin_op(context): + return ops.sin(context.get_input(0)).outputs() + + me = ModuleExtension(CosModel, "MySinOp", + condition=lambda m: getattr(m, "flag", False)) + + # condition False → extension skipped, original Cos runs + model = ModelWithCosModule() + model.cos_module.flag = False + converted_model = convert_model( + model, example_input=[torch.randn(100)], dynamo=True, + extension=[me, ConversionExtension("MySinOp", sin_op)]) + assert [n.get_type_name() for n in converted_model.get_ordered_ops()] == [ + "Parameter", "Cos", "Result"] + assert converted_model.get_results()[0].get_output_partial_shape(0) == \ + PartialShape([100]) + + # condition True → extension applies, Sin replaces Cos + model = ModelWithCosModule() + model.cos_module.flag = True + converted_model = convert_model( + model, example_input=[torch.randn(100)], dynamo=True, + extension=[me, ConversionExtension("MySinOp", sin_op)]) + assert [n.get_type_name() for n in converted_model.get_ordered_ops()] == [ + "Parameter", "Sin", "Result"] + assert converted_model.get_results()[0].get_output_partial_shape(0) == \ + PartialShape([100]) + + +def test_module_extension_dynamo_unpatch(): + """Model is unpatched after conversion (no leftover attributes).""" + from openvino.frontend.pytorch import ModuleExtension, ConversionExtension + from openvino import convert_model + + def sin_op(context): + return ops.sin(context.get_input(0)).outputs() + + model = ModelWithCosModule() + convert_model( + model, example_input=[torch.randn(100)], dynamo=True, + extension=[ + ModuleExtension(CosModel, "MySinOp"), + ConversionExtension("MySinOp", sin_op)]) + + for _, m in model.named_modules(): + assert not hasattr(m, "_openvino_module_extension_patch_orig_forward") + + # Verify model still produces correct output after unpatching + x = torch.randn(100) + result = model(x) + expected = torch.cos(x.to(torch.float32)) + assert torch.allclose(result, expected) + + +def test_module_extension_dynamo_fx_op_reuse(): + """FX-style op name as target_op reuses built-in FX translator.""" + from openvino.frontend.pytorch import ModuleExtension + from openvino import convert_model + + model = ModelWithCosModule() + converted_model = convert_model( + model, example_input=[torch.randn(100)], dynamo=True, + extension=[ModuleExtension(CosModel, "aten.sin.default")]) + assert converted_model + assert [n.get_type_name() for n in converted_model.get_ordered_ops()] == [ + "Parameter", "Sin", "Result"] + assert converted_model.get_results()[0].get_output_partial_shape(0) == \ + PartialShape([100]) + + +def test_module_extension_dynamo_multi_input(): + """Multi-input module: schema generated with correct arity.""" + from openvino.frontend.pytorch import ModuleExtension, ConversionExtension + from openvino import convert_model + + class AddModule(torch.nn.Module): + def forward(self, x, y): + return x + y + + class ModelWithAdd(torch.nn.Module): + def __init__(self): + super().__init__() + self.add_mod = AddModule() + + def forward(self, a, b): + return self.add_mod(a, b) + + def mul_op(context): + return ops.multiply(context.get_input(0), + context.get_input(1)).outputs() + + model = ModelWithAdd() + converted_model = convert_model( + model, example_input=[torch.randn(100), torch.randn(100)], + dynamo=True, + extension=[ + ModuleExtension(AddModule, "MyMulOp"), + ConversionExtension("MyMulOp", mul_op)]) + assert converted_model + assert [n.get_type_name() for n in converted_model.get_ordered_ops()] == [ + "Parameter", "Parameter", "Multiply", "Result"] + assert converted_model.get_results()[0].get_output_partial_shape(0) == \ + PartialShape([100]) + + +def test_module_extension_dynamo_weight_carrying(): + """convert() passes extra module params (weight, bias) to target_op. + + The auto-registered schema must match what convert() actually calls + (3 args: input, weight, bias), not the forward signature (1 arg: x). + """ + from openvino.frontend.pytorch import ModuleExtension, ConversionExtension + from openvino import convert_model + + class WeightedModule(torch.nn.Module): + def __init__(self, out_features): + super().__init__() + self.weight = torch.nn.Parameter(torch.randn(out_features, 10)) + self.bias = torch.nn.Parameter(torch.randn(out_features)) + + def forward(self, x): + return x @ self.weight.t() + self.bias + + class ModelWithWeighted(torch.nn.Module): + def __init__(self): + super().__init__() + self.linear = WeightedModule(5) + + def forward(self, x): + return self.linear(x) + + def custom_convert(module, target_op, *args, **kwargs): + return target_op(args[0], module.weight, module.bias) + + def linear_op(context): + mm = ops.matmul(context.get_input(0), + context.get_input(1), False, True) + return ops.add(mm, context.get_input(2)).outputs() + + model = ModelWithWeighted() + ref_weight = model.linear.weight.detach().numpy().copy() + ref_bias = model.linear.bias.detach().numpy().copy() + + converted_model = convert_model( + model, example_input=[torch.randn(2, 10)], dynamo=True, + extension=[ + ModuleExtension(WeightedModule, "WeightedLinear", + convert=custom_convert), + ConversionExtension("WeightedLinear", linear_op)]) + assert converted_model + op_types = [n.get_type_name() for n in converted_model.get_ordered_ops()] + assert "MatMul" in op_types + assert "Add" in op_types + # Weight is [5, 10], input is [2, 10], MatMul(input, weight^T) → [2, 5] + assert converted_model.get_results()[0].get_output_partial_shape(0) == \ + PartialShape([2, 5]) + + # Verify weight and bias constant values match the original parameters + found_weight = False + found_bias = False + for n in converted_model.get_ordered_ops(): + if n.get_type_name() != "Constant": + continue + data = n.get_data() + if data.shape == ref_weight.shape and np.allclose(data, ref_weight): + found_weight = True + if data.size == ref_bias.size and np.allclose(data.flatten(), ref_bias.flatten()): + found_bias = True + assert found_weight, "Weight constant not found or values don't match" + assert found_bias, "Bias constant not found or values don't match" + + +def test_module_extension_dynamo_scalar_args(): + """convert() passes scalar args (int, bool) alongside tensors.""" + from openvino.frontend.pytorch import ModuleExtension, ConversionExtension + from openvino import convert_model + + class ScalarArgModule(torch.nn.Module): + def __init__(self): + super().__init__() + self.scale = 2 + self.negate = True + + def forward(self, x): + return x * self.scale * (-1 if self.negate else 1) + + class ModelWithScalar(torch.nn.Module): + def __init__(self): + super().__init__() + self.mod = ScalarArgModule() + + def forward(self, x): + return self.mod(x) + + def scalar_convert(module, target_op, *args, **kwargs): + return target_op(args[0], module.scale, module.negate) + + def scalar_op(context): + inp = context.get_input(0) + scale = context.get_values_from_const_input(1) + negate = context.get_values_from_const_input(2) + assert scale == 2, f"Expected scale=2, got {scale}" + assert negate == True, f"Expected negate=True, got {negate}" + # Apply: x * scale * (-1 if negate else 1) + scale_const = ops.constant(np.array([scale], dtype=np.float32)) + result = ops.multiply(inp, scale_const) + if negate: + neg_one = ops.constant(np.array([-1], dtype=np.float32)) + result = ops.multiply(result, neg_one) + return result.outputs() + + model = ModelWithScalar() + converted_model = convert_model( + model, example_input=[torch.randn(100)], dynamo=True, + extension=[ + ModuleExtension(ScalarArgModule, "ScalarOp", + convert=scalar_convert), + ConversionExtension("ScalarOp", scalar_op)]) + assert converted_model + op_types = [n.get_type_name() for n in converted_model.get_ordered_ops()] + assert "Multiply" in op_types, f"Expected Multiply in ops, got {op_types}" + assert converted_model.get_results()[0].get_output_partial_shape(0) == \ + PartialShape([100]) + + +def test_module_extension_dynamo_shape_changing(): + """Shape-changing module: input [2,10] → output [2,5]. + + The Meta impl must use evaluate to infer correct output shape; + otherwise downstream ops see the wrong shape and export fails. + """ + from openvino.frontend.pytorch import ModuleExtension, ConversionExtension + from openvino import convert_model + + class ShapeChangingModule(torch.nn.Module): + def __init__(self, in_features, out_features): + super().__init__() + self.weight = torch.nn.Parameter( + torch.randn(out_features, in_features)) + self.out_features = out_features + + def forward(self, x): + return x @ self.weight.t() + + class ModelWithShapeChange(torch.nn.Module): + def __init__(self): + super().__init__() + self.proj = ShapeChangingModule(10, 5) + + def forward(self, x): + y = self.proj(x) + # Downstream op depends on correct output shape (2, 5). + return y + torch.ones(5) + + def proj_convert(module, target_op, *args, **kwargs): + return target_op(args[0], module.weight) + + def proj_op(context): + return ops.matmul( + context.get_input(0), context.get_input(1), + False, True).outputs() + + model = ModelWithShapeChange() + converted_model = convert_model( + model, example_input=[torch.randn(2, 10)], dynamo=True, + extension=[ + ModuleExtension(ShapeChangingModule, "ProjOp", + convert=proj_convert), + ConversionExtension("ProjOp", proj_op)]) + assert converted_model + op_types = [n.get_type_name() for n in converted_model.get_ordered_ops()] + assert "MatMul" in op_types + assert "Add" in op_types + # Final output shape must be [2, 5] (proj [2,10]→[2,5], then + ones(5)) + assert converted_model.get_results()[0].get_output_partial_shape(0) == \ + PartialShape([2, 5]) + + +def test_module_extension_dynamo_custom_callbacks(): + """Custom evaluate, convert, and condition callbacks.""" + from openvino.frontend.pytorch import ModuleExtension, ConversionExtension + from openvino import convert_model + + # Custom convert: negate input before calling target_op. + def custom_convert(module, target_op, *args, **kwargs): + return target_op(-args[0]) + + def custom_evaluate(module, *args, **kwargs): + return torch.zeros_like(args[0]) + + def custom_condition(module): + return getattr(module, "apply_ext", False) + + def sin_op(context): + return ops.sin(context.get_input(0)).outputs() + + me = ModuleExtension(CosModel, "CustomNegOp", + evaluate=custom_evaluate, + convert=custom_convert, + condition=custom_condition) + ce = ConversionExtension("CustomNegOp", sin_op) + + # condition True → custom_convert negates input, then Sin + model = ModelWithCosModule() + model.cos_module.apply_ext = True + converted_model = convert_model( + model, example_input=[torch.randn(100)], dynamo=True, + extension=[me, ce]) + assert converted_model + op_types = [n.get_type_name() for n in converted_model.get_ordered_ops()] + assert "Sin" in op_types + assert "Multiply" in op_types # negation: x * -1 + assert "Cos" not in op_types + assert converted_model.get_results()[0].get_output_partial_shape(0) == \ + PartialShape([100]) + + # condition False → extension skipped, original Cos runs + model = ModelWithCosModule() + model.cos_module.apply_ext = False + converted_model = convert_model( + model, example_input=[torch.randn(100)], dynamo=True, + extension=[me, ce]) + assert [n.get_type_name() for n in converted_model.get_ordered_ops()] == [ + "Parameter", "Cos", "Result"] + assert converted_model.get_results()[0].get_output_partial_shape(0) == \ + PartialShape([100]) + + def verify_model(model, example_input, expected_ops): import numpy as np import openvino as ov @@ -1949,6 +2317,104 @@ def forward(self, x): assert not hasattr(m, "_openvino_quantized_patch_orig_forward") +def _make_torch_fused_gptq_model(in_features=32, out_features=64, group_size=32): + """Build a minimal GPTQ model whose linear layer mimics gptqmodel's + ``TorchFusedQuantLinear`` backend (``QUANT_TYPE == "torch_fused"``), using the + standard 4-bit/int32 weight packing the OpenVINO GPTQ patcher expects. The + layer's own ``forward`` is a placeholder — OpenVINO replaces it with its + decompression forward before tracing/export, so only the packed buffers and + attributes need to be realistic. + """ + bits = 4 + pack_num = 32 // bits # 8 nibbles per int32 + + class FakeQuantConfig: + quant_method = "gptq" + sym = True + + class FakeConfig: + quantization_config = FakeQuantConfig() + + class TorchFusedLinear(torch.nn.Module): + QUANT_TYPE = "torch_fused" + + def __init__(self): + super().__init__() + self.bits = bits + self.group_size = group_size + # Real GPTQ backends register the packed tensors as buffers (not + # parameters); the OpenVINO patcher re-assigns plain tensors to them. + self.register_buffer("qweight", torch.randint( + 0, 2 ** 31, (in_features // pack_num, out_features), + dtype=torch.int32)) + self.register_buffer("qzeros", torch.randint( + 0, 2 ** 31, (in_features // group_size, out_features // pack_num), + dtype=torch.int32)) + self.register_buffer("scales", torch.randn( + in_features // group_size, out_features, dtype=torch.float16)) + self.bias = None + + def forward(self, x): + return torch.zeros(*x.shape[:-1], out_features, dtype=x.dtype, device=x.device) + + class GPTQModel(torch.nn.Module): + def __init__(self): + super().__init__() + self.config = FakeConfig() + self.linear = TorchFusedLinear() + + def forward(self, x): + return self.linear(x) + + return GPTQModel(), torch.randn(2, in_features) + + +def test_gptq_torch_fused_convert_keeps_u4(): + """A GPTQ model whose layers report ``QUANT_TYPE == "torch_fused"`` must convert + via the TorchScript path and keep its 4-bit weight packing: the resulting + ov::Model must contain a 4-bit (i4/u4) Constant and no live ``BitwiseRightShift`` + weight-unpacking op.""" + from openvino.frontend.pytorch.ts_decoder import TorchScriptPythonDecoder + + model, x = _make_torch_fused_gptq_model() + model.eval() + + # Convert through the frontend directly: TorchScriptPythonDecoder traces the + # model and auto-applies the GPTQ patch, and FrontEnd.convert keeps the u4 + # weight constant produced by the u4_compression_stack fold. The full + # openvino.convert_model MOC pipeline would constant-fold the all-constant + # dequant subgraph of this tiny fixture, hiding the packing under test. + decoder = TorchScriptPythonDecoder(model, example_input=(x,)) + fe = FrontEndManager().load_by_framework("pytorch") + ov_model = fe.convert(fe.load(decoder)) + assert ov_model + + ops = ov_model.get_ops() + type_names = [o.get_type_name() for o in ops] + # The GPTQ unpacking must have been folded away (no runtime bit-shift unpacking). + assert "BitwiseRightShift" not in type_names + # ...and the weights must be stored as a packed 4-bit constant. + four_bit_consts = [o for o in ops + if o.get_type_name() == "Constant" + and o.get_output_element_type(0) in (Type.i4, Type.u4)] + assert four_bit_consts, "expected a packed 4-bit (i4/u4) weight constant" + + +def test_gptq_torch_fused_export_supported(): + """``patch_quantized_for_export`` must accept ``QUANT_TYPE == "torch_fused"`` + rather than raising ``ValueError`` for the unsupported quant type.""" + from openvino.frontend.pytorch.quantized import ( + patch_quantized_for_export, unpatch_quantized_for_export) + + model, _ = _make_torch_fused_gptq_model() + + patch_quantized_for_export(model) # must not raise + try: + assert hasattr(model.linear, "_openvino_quantized_patch_orig_forward") + finally: + unpatch_quantized_for_export(model) + + # ────────────────────────────────────────────────────────────────────── # Tests for dynamo=True auto-patching of quantized models # ────────────────────────────────────────────────────────────────────── diff --git a/tests/layer_tests/pytorch_tests/test_as_strided.py b/tests/layer_tests/pytorch_tests/test_as_strided.py index 685eb8839291..32968e1f6f5b 100644 --- a/tests/layer_tests/pytorch_tests/test_as_strided.py +++ b/tests/layer_tests/pytorch_tests/test_as_strided.py @@ -73,6 +73,8 @@ def forward(self, x): ], ) @pytest.mark.parametrize("offset", [None, 1, 3, 7]) + @pytest.mark.nightly + @pytest.mark.precommit @pytest.mark.precommit_fx_backend def test_as_strided_copy(self, size, stride, offset, ie_device, precision, ir_version): self._test(*self.create_model(size, stride, offset), ie_device, precision, ir_version, trace_model=True) diff --git a/tests/layer_tests/pytorch_tests/test_expand.py b/tests/layer_tests/pytorch_tests/test_expand.py index 307cedbeb375..e50c773cc26c 100644 --- a/tests/layer_tests/pytorch_tests/test_expand.py +++ b/tests/layer_tests/pytorch_tests/test_expand.py @@ -59,6 +59,8 @@ def forward(self, x): return aten_expand_copy(dim), f"aten::expand_copy" @pytest.mark.parametrize("dims", [(4, 3), (-1, -1), (1, 2, 3), (1, 2, 2, 3)]) + @pytest.mark.nightly + @pytest.mark.precommit @pytest.mark.precommit_fx_backend def test_expand_copy(self, dims, ie_device, precision, ir_version): self._test(*self.create_model(dims), ie_device, precision, ir_version) diff --git a/tests/layer_tests/pytorch_tests/test_getitem_oob_index.py b/tests/layer_tests/pytorch_tests/test_getitem_oob_index.py new file mode 100644 index 000000000000..6fd3b6aa7acc --- /dev/null +++ b/tests/layer_tests/pytorch_tests/test_getitem_oob_index.py @@ -0,0 +1,432 @@ +# Copyright (C) 2018-2026 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +""" +Regression tests for out-of-bounds index validation in aten::__getitem__ +replacer transforms (CVE fix). + +These tests verify that ov.convert_model raises a proper exception (instead +of crashing with SIGSEGV) when a TorchScript model contains an +aten::__getitem__ node whose constant index is out of bounds for the +producer collection (aten::split outputs, SequenceMark inputs, or +append-list inputs). +""" + +import pytest +import torch +import openvino as ov + +from pytorch_layer_test_class import PytorchLayerTest + +# --------------------------------------------------------------------------- +# GI1: aten::split (scalar split_size) + aten::__getitem__ with OOB index +# --------------------------------------------------------------------------- + +class SplitGetitemOOB(torch.nn.Module): + """Model that indexes split results with an out-of-bounds constant.""" + + def __init__(self, split_size, dim, index): + super().__init__() + self.split_size = split_size + self.dim = dim + self.index = index + + def forward(self, x): + splits = torch.split(x, self.split_size, self.dim) + return splits[self.index] + + +class SplitGetitemNegativeOOB(torch.nn.Module): + """Model that indexes split results with a large negative OOB constant.""" + + def __init__(self, split_size, dim, index): + super().__init__() + self.split_size = split_size + self.dim = dim + self.index = index + + def forward(self, x): + splits = torch.split(x, self.split_size, self.dim) + return splits[self.index] + + +class TestSplitGetitemOOBIndex(PytorchLayerTest): + """ + Tests for aten::split + aten::__getitem__ with out-of-bounds index. + + When split_size is a scalar, torch.split produces aten::split which is + not in op_table.cpp and stays as a FrameworkNode. The AtenGetItemReplacer + transform then processes it. An OOB index must be caught gracefully. + """ + + @pytest.mark.precommit + @pytest.mark.nightly + @pytest.mark.parametrize("oob_index", [999, 100, 5]) + def test_positive_oob_index_no_crash(self, oob_index, ie_device, precision): + """Positive OOB index must not cause SIGSEGV.""" + # Input shape [1, 10], split_size=2, dim=1 -> 5 chunks (indices 0-4) + model = SplitGetitemOOB(split_size=2, dim=1, index=oob_index) + sample_input = self.random.randn(1, 10) + + try: + scripted = torch.jit.script(model) + except Exception: + pytest.skip("torch.jit.script rejected the model") + + # The conversion must NOT segfault. It should raise a clean + # exception for out-of-bounds index. + # Static input shape is required so the OOB check can compute num_splits. + with pytest.raises(ov.frontend.OpConversionFailure): + ov.convert_model(scripted, example_input=(sample_input,), input=[1, 10]) + + @pytest.mark.precommit + @pytest.mark.nightly + @pytest.mark.parametrize("oob_index", [-999, -100, -6]) + def test_negative_oob_index_no_crash(self, oob_index, ie_device, precision): + """Large negative OOB index must not cause SIGSEGV.""" + model = SplitGetitemNegativeOOB(split_size=2, dim=1, index=oob_index) + sample_input = self.random.randn(1, 10) + + try: + scripted = torch.jit.script(model) + except Exception: + pytest.skip("torch.jit.script rejected the model") + + with pytest.raises(ov.frontend.OpConversionFailure): + ov.convert_model(scripted, example_input=(sample_input,), input=[1, 10]) + + @pytest.mark.precommit + @pytest.mark.nightly + @pytest.mark.parametrize("valid_index", [-5, -1, 0, 1, 4]) + def test_valid_index_still_works(self, valid_index, ie_device, precision): + """Valid indices (including negative wrapping) must still convert.""" + model = SplitGetitemOOB(split_size=2, dim=1, index=valid_index) + sample_input = self.random.randn(1, 10) + + try: + scripted = torch.jit.script(model) + except Exception: + pytest.skip("torch.jit.script rejected the model") + + # Valid indices should convert successfully + ov_model = ov.convert_model(scripted, example_input=(sample_input,), input=[1, 10]) + assert ov_model is not None + + +# --------------------------------------------------------------------------- +# GI1 variant: aten::split (list split_sizes) + aten::__getitem__ with OOB +# --------------------------------------------------------------------------- + +class SplitSizesGetitemOOB(torch.nn.Module): + """Model using list split_sizes with OOB getitem index.""" + + def __init__(self, split_sizes, dim, index): + super().__init__() + self.split_sizes = split_sizes + self.dim = dim + self.index = index + + def forward(self, x): + splits = torch.split(x, self.split_sizes, self.dim) + return splits[self.index] + + +class TestSplitSizesGetitemOOBIndex(PytorchLayerTest): + """ + Tests for aten::split with list split_sizes + OOB __getitem__. + + When split_sizes is a list, the VariadicSplit branch is taken in + AtenGetItemReplacer. OOB index on split->outputs() must be caught. + """ + + @pytest.mark.precommit + @pytest.mark.nightly + @pytest.mark.parametrize("oob_index", [3, 10, 999]) + def test_positive_oob_index_no_crash(self, oob_index, ie_device, precision): + """Positive OOB index on list split must not crash.""" + # split_sizes=[2,3,5] -> 3 chunks (indices 0-2) + model = SplitSizesGetitemOOB(split_sizes=[2, 3, 5], dim=1, index=oob_index) + sample_input = self.random.randn(1, 10) + + try: + scripted = torch.jit.script(model) + except Exception: + pytest.skip("torch.jit.script rejected the model") + + with pytest.raises(ov.frontend.OpConversionFailure): + ov.convert_model(scripted, example_input=(sample_input,)) + + @pytest.mark.precommit + @pytest.mark.nightly + @pytest.mark.parametrize("oob_index", [-4, -100]) + def test_negative_oob_index_no_crash(self, oob_index, ie_device, precision): + """Large negative OOB index on list split must not crash.""" + model = SplitSizesGetitemOOB(split_sizes=[2, 3, 5], dim=1, index=oob_index) + sample_input = self.random.randn(1, 10) + + try: + scripted = torch.jit.script(model) + except Exception: + pytest.skip("torch.jit.script rejected the model") + + with pytest.raises(ov.frontend.OpConversionFailure): + ov.convert_model(scripted, example_input=(sample_input,)) + + @pytest.mark.precommit + @pytest.mark.nightly + @pytest.mark.parametrize("valid_index", [-3, -1, 0, 1, 2]) + def test_valid_list_split_index_still_works(self, valid_index, ie_device, precision): + """Valid indices for list split_sizes must still convert.""" + model = SplitSizesGetitemOOB(split_sizes=[2, 3, 5], dim=1, index=valid_index) + sample_input = self.random.randn(1, 10) + + try: + scripted = torch.jit.script(model) + except Exception: + pytest.skip("torch.jit.script rejected the model") + + ov_model = ov.convert_model(scripted, example_input=(sample_input,)) + assert ov_model is not None + + +# --------------------------------------------------------------------------- +# Chunk + getitem OOB (related pattern in AtenGetItemReplacer) +# --------------------------------------------------------------------------- + +class ChunkGetitemOOB(torch.nn.Module): + """Model that indexes chunk results with an OOB constant.""" + + def __init__(self, chunks, dim, index): + super().__init__() + self.chunks = chunks + self.dim = dim + self.index = index + + def forward(self, x): + parts = torch.chunk(x, self.chunks, self.dim) + return parts[self.index] + + +class TestChunkGetitemOOBIndex(PytorchLayerTest): + """Chunk + getitem OOB index tests (uses dynamic Slice, no vector OOB).""" + + @pytest.mark.precommit + @pytest.mark.nightly + @pytest.mark.parametrize("valid_index", [-3, -1, 0, 1, 2]) + def test_valid_chunk_index(self, valid_index, ie_device, precision): + """Valid chunk indices must still work.""" + model = ChunkGetitemOOB(chunks=3, dim=1, index=valid_index) + sample_input = self.random.randn(1, 12) + + try: + scripted = torch.jit.script(model) + except Exception: + pytest.skip("torch.jit.script rejected the model") + + ov_model = ov.convert_model(scripted, example_input=(sample_input,)) + assert ov_model is not None + + +# --------------------------------------------------------------------------- +# SequenceMark (prim::ListConstruct) + aten::__getitem__ with OOB index +# --------------------------------------------------------------------------- + +class ListConstructGetitemOOB(torch.nn.Module): + """Model that indexes a list-constructed sequence with an OOB constant. + + prim::ListConstruct becomes a SequenceMark during conversion; the + aten::__getitem__ on it is handled by the SequenceMark branch of + AtenGetItemReplacer. + """ + + def __init__(self, index): + super().__init__() + self.index = index + + def forward(self, x): + a = x[:, :2] + b = x[:, 2:5] + c = x[:, 5:] + lst = [a, b, c] + return lst[self.index] + + +class TestListConstructGetitemOOBIndex(PytorchLayerTest): + """ + Tests for SequenceMark (prim::ListConstruct) + aten::__getitem__ with + out-of-bounds index. + + The SequenceMark branch in AtenGetItemReplacer validates idx.size() == 1 + and checks bounds against the number of sequence inputs before accessing. + """ + + @pytest.mark.precommit + @pytest.mark.nightly + @pytest.mark.parametrize("oob_index", [3, 10, 999]) + def test_positive_oob_index_no_crash(self, oob_index, ie_device, precision): + """Positive OOB index on list construct must not crash.""" + model = ListConstructGetitemOOB(index=oob_index) + sample_input = self.random.randn(1, 10) + + try: + scripted = torch.jit.script(model) + except Exception: + pytest.skip("torch.jit.script rejected the model") + + with pytest.raises(ov.frontend.OpConversionFailure): + ov.convert_model(scripted, example_input=(sample_input,)) + + @pytest.mark.precommit + @pytest.mark.nightly + @pytest.mark.parametrize("oob_index", [-4, -100]) + def test_negative_oob_index_no_crash(self, oob_index, ie_device, precision): + """Large negative OOB index on list construct must not crash.""" + model = ListConstructGetitemOOB(index=oob_index) + sample_input = self.random.randn(1, 10) + + try: + scripted = torch.jit.script(model) + except Exception: + pytest.skip("torch.jit.script rejected the model") + + with pytest.raises(ov.frontend.OpConversionFailure): + ov.convert_model(scripted, example_input=(sample_input,)) + + @pytest.mark.precommit + @pytest.mark.nightly + @pytest.mark.parametrize("valid_index", [-3, -2, -1, 0, 1, 2]) + def test_valid_list_index_still_works(self, valid_index, ie_device, precision): + """Valid indices (including negative wrapping) must still convert.""" + model = ListConstructGetitemOOB(index=valid_index) + sample_input = self.random.randn(1, 10) + + try: + scripted = torch.jit.script(model) + except Exception: + pytest.skip("torch.jit.script rejected the model") + + ov_model = ov.convert_model(scripted, example_input=(sample_input,)) + assert ov_model is not None + + +# --------------------------------------------------------------------------- +# AppendListUnpackReplacer: prim::ListUnpack + aten::__getitem__ with OOB +# --------------------------------------------------------------------------- + +class ListUnpackGetitemOOB(torch.nn.Module): + """Model that triggers prim::ListUnpack after aten::__getitem__ on a + list-of-tensors. + + This exercises the AppendListUnpackReplacer path where a list construct + is indexed before being unpacked. + """ + + def __init__(self, index): + super().__init__() + self.index = index + + def forward(self, x): + # Build a list of 2D tensors (Tensor[][]-like pattern): + # each element is [N, features] stacked along a new dim 0. + a = x[:, :3].unsqueeze(0) # [1, N, 3] + b = x[:, 3:7].unsqueeze(0) # [1, N, 4] + c = x[:, 7:].unsqueeze(0) # [1, N, 3] + lst = [a, b, c] + selected = lst[self.index] + # Unpack along dim 0 (prim::ListUnpack pattern) + result = selected.squeeze(0) + return result + + +class ListUnpackGetitemMultiOutput(torch.nn.Module): + """Model triggering AppendListUnpackReplacer with multiple unpack outputs. + + Uses indexing into a list built from stacked tensor slices. + """ + + def __init__(self, index): + super().__init__() + self.index = index + + def forward(self, x): + # Create a list of stacked tensors + a = x[:, :3] + b = x[:, 3:6] + c = x[:, 6:9] + stacked = torch.stack([a, b, c], dim=0) # [3, N, 3] + parts = [stacked[0], stacked[1], stacked[2]] + return parts[self.index] + + +class TestListUnpackGetitemOOBIndex(PytorchLayerTest): + """ + Tests for AppendListUnpackReplacer with aten::__getitem__ OOB index. + + The AppendListUnpackReplacer handles prim::ListUnpack optionally + preceded by aten::__getitem__. An OOB index into the list must be + caught gracefully without crashing. + """ + + @pytest.mark.precommit + @pytest.mark.nightly + @pytest.mark.parametrize("oob_index", [3, 10, 999]) + def test_positive_oob_index_no_crash(self, oob_index, ie_device, precision): + """Positive OOB index on list unpack must not crash.""" + model = ListUnpackGetitemOOB(index=oob_index) + sample_input = self.random.randn(2, 10) + + try: + scripted = torch.jit.script(model) + except Exception: + pytest.skip("torch.jit.script rejected the model") + + with pytest.raises(ov.frontend.OpConversionFailure): + ov.convert_model(scripted, example_input=(sample_input,)) + + @pytest.mark.precommit + @pytest.mark.nightly + @pytest.mark.parametrize("oob_index", [-4, -100]) + def test_negative_oob_index_no_crash(self, oob_index, ie_device, precision): + """Large negative OOB index on list unpack must not crash.""" + model = ListUnpackGetitemOOB(index=oob_index) + sample_input = self.random.randn(2, 10) + + try: + scripted = torch.jit.script(model) + except Exception: + pytest.skip("torch.jit.script rejected the model") + + with pytest.raises(ov.frontend.OpConversionFailure): + ov.convert_model(scripted, example_input=(sample_input,)) + + @pytest.mark.precommit + @pytest.mark.nightly + @pytest.mark.parametrize("valid_index", [-3, -2, -1, 0, 1, 2]) + def test_valid_list_unpack_index(self, valid_index, ie_device, precision): + """Valid indices for list unpack must still convert.""" + model = ListUnpackGetitemOOB(index=valid_index) + sample_input = self.random.randn(2, 10) + + try: + scripted = torch.jit.script(model) + except Exception: + pytest.skip("torch.jit.script rejected the model") + + ov_model = ov.convert_model(scripted, example_input=(sample_input,)) + assert ov_model is not None + + @pytest.mark.precommit + @pytest.mark.nightly + @pytest.mark.parametrize("valid_index", [-3, -1, 0, 1, 2]) + def test_multi_output_valid_index(self, valid_index, ie_device, precision): + """Multi-output list pattern with valid index must convert.""" + model = ListUnpackGetitemMultiOutput(index=valid_index) + sample_input = self.random.randn(2, 9) + + try: + scripted = torch.jit.script(model) + except Exception: + pytest.skip("torch.jit.script rejected the model") + + ov_model = ov.convert_model(scripted, example_input=(sample_input,)) + assert ov_model is not None diff --git a/tests/layer_tests/pytorch_tests/test_hardtanh.py b/tests/layer_tests/pytorch_tests/test_hardtanh.py index 1ad2e992a18d..3a2cd7460765 100644 --- a/tests/layer_tests/pytorch_tests/test_hardtanh.py +++ b/tests/layer_tests/pytorch_tests/test_hardtanh.py @@ -26,9 +26,9 @@ def forward(self, x): return F.hardtanh(x, min_val=self.min_val, max_val=self.max_val, inplace=self.inplace) - return aten_hardtanh(min_val, max_val, inplace), "aten::hardtanh" + return aten_hardtanh(min_val, max_val, inplace), "aten::hardtanh_" if inplace else "aten::hardtanh" - @pytest.mark.parametrize(("min_val", "max_val"), [[-1.0,1.0], [0, 1.0], [-2.0, 2.0]]) + @pytest.mark.parametrize(("min_val", "max_val"), [[-1.0,1.0], [0.0, 1.0], [-2.0, 2.0]]) @pytest.mark.parametrize("inplace", [True, False]) @pytest.mark.parametrize("input_dtype", ['float32', 'int32', 'int64', 'float64']) @pytest.mark.parametrize("input_shape", [(1, 3, 10, 10), (100,), (24, 24)]) diff --git a/tests/layer_tests/pytorch_tests/test_isinf.py b/tests/layer_tests/pytorch_tests/test_isinf.py index b6dc2f9e6ec4..cbcd242f8a82 100644 --- a/tests/layer_tests/pytorch_tests/test_isinf.py +++ b/tests/layer_tests/pytorch_tests/test_isinf.py @@ -8,7 +8,9 @@ from pytorch_layer_test_class import PytorchLayerTest -@pytest.mark.parametrize('input_tensor', (np.array([1, 0, -1]),)) +@pytest.mark.parametrize('input_tensor', + (np.array([float("inf"), float("nan"), -float("inf"), 0.0, 1.0, -1.0], + dtype=np.float32),)) class TestIsInf(PytorchLayerTest): def _prepare_input(self): @@ -19,7 +21,7 @@ def create_model(self): class aten_isinf(torch.nn.Module): def forward(self, input_tensor): - return torch.isinf(input_tensor * float("inf")) + return torch.isinf(input_tensor) return aten_isinf(), "aten::isinf" diff --git a/tests/layer_tests/pytorch_tests/test_permute.py b/tests/layer_tests/pytorch_tests/test_permute.py index a3dbd54f5fdc..dcc292cd240a 100644 --- a/tests/layer_tests/pytorch_tests/test_permute.py +++ b/tests/layer_tests/pytorch_tests/test_permute.py @@ -60,6 +60,8 @@ def forward(self, x): return aten_permute_copy(order), "aten::permute_copy" @pytest.mark.parametrize("order", [[0, 2, 3, 1], [0, 3, 1, 2], [0, -1, 1, -2]]) + @pytest.mark.nightly + @pytest.mark.precommit @pytest.mark.precommit_fx_backend def test_permute_copy(self, order, ie_device, precision, ir_version): self._test(*self.create_model(order), ie_device, precision, ir_version) diff --git a/tests/layer_tests/pytorch_tests/test_scalar_tensor.py b/tests/layer_tests/pytorch_tests/test_scalar_tensor.py index cce20ac011fc..5681568941a1 100644 --- a/tests/layer_tests/pytorch_tests/test_scalar_tensor.py +++ b/tests/layer_tests/pytorch_tests/test_scalar_tensor.py @@ -13,21 +13,27 @@ class TestScalarTensor(PytorchLayerTest): def _prepare_input(self): return (np.array(self.random.randn(), dtype=np.float32),) - def create_model(self): + def create_model(self, dtype): class aten_scalar_tensor(torch.nn.Module): - def __init__(self) -> None: + def __init__(self, dtype) -> None: super().__init__() + self.dtype = dtype def forward(self, lhs): - return torch.scalar_tensor(lhs.item()) + if self.dtype is None: + return torch.scalar_tensor(lhs.item()) + return torch.scalar_tensor(lhs.item(), dtype=self.dtype) - return aten_scalar_tensor(), f"aten::scalar_tensor" + return aten_scalar_tensor(dtype), f"aten::scalar_tensor" + @pytest.mark.parametrize("dtype", [None, torch.float32, torch.float64, torch.int32, torch.int64]) + @pytest.mark.nightly + @pytest.mark.precommit @pytest.mark.precommit_torch_export @pytest.mark.precommit_fx_backend - def test_scalar_tensor(self, ie_device, precision, ir_version): - self._test(*self.create_model(), ie_device, precision, ir_version, use_convert_model=True) + def test_scalar_tensor(self, dtype, ie_device, precision, ir_version): + self._test(*self.create_model(dtype), ie_device, precision, ir_version, use_convert_model=True) diff --git a/tests/layer_tests/pytorch_tests/test_scaled_dot_product_attention.py b/tests/layer_tests/pytorch_tests/test_scaled_dot_product_attention.py index c1b9fc965e8f..9d48f83d916b 100644 --- a/tests/layer_tests/pytorch_tests/test_scaled_dot_product_attention.py +++ b/tests/layer_tests/pytorch_tests/test_scaled_dot_product_attention.py @@ -83,7 +83,7 @@ def test_scaled_dot_product_atten_fp64(self, ie_device, precision, if PytorchLayerTest.use_torch_export() and not mask and is_causal: pytest.xfail(reason="Unsupported case for torch.export") dtype = np.float64 - self._test(*self.create_model(mask, is_causal, dtype, mask_shape), + self._test(*self.create_model(mask, is_causal, dtype, mask_shape, enable_gqa=False), ie_device, precision, ir_version, dynamic_shapes=dyn_shapes, kwargs_to_prepare_input={"dtype": dtype}) diff --git a/tests/layer_tests/pytorch_tests/test_select.py b/tests/layer_tests/pytorch_tests/test_select.py index 3fc862e72a16..d08f309696b5 100644 --- a/tests/layer_tests/pytorch_tests/test_select.py +++ b/tests/layer_tests/pytorch_tests/test_select.py @@ -57,6 +57,8 @@ def forward(self, input_tensor): return aten_select_copy(input_dim, input_index), "aten::select_copy" + @pytest.mark.nightly + @pytest.mark.precommit @pytest.mark.precommit_fx_backend def test_select_copy(self, ie_device, precision, ir_version, input_dim, input_index): self._test(*self.create_model(input_dim, input_index), diff --git a/tests/layer_tests/pytorch_tests/test_split.py b/tests/layer_tests/pytorch_tests/test_split.py index e242d7d46871..c9f3406ebcdd 100644 --- a/tests/layer_tests/pytorch_tests/test_split.py +++ b/tests/layer_tests/pytorch_tests/test_split.py @@ -109,8 +109,10 @@ def forward(self, x, y): return torch.split_with_sizes_copy(x, [y.shape[0]], dim=0) - return aten_split_with_sizes_copy(), ["aten::split_with_sizes", "prim::ListConstruct"] + return aten_split_with_sizes_copy(), ["aten::split_with_sizes_copy", "prim::ListConstruct"] + @pytest.mark.nightly + @pytest.mark.precommit @pytest.mark.precommit_torch_export @pytest.mark.precommit_fx_backend def test_split_with_sizes_copy(self, ie_device, precision, ir_version): diff --git a/tests/layer_tests/pytorch_tests/test_squeeze.py b/tests/layer_tests/pytorch_tests/test_squeeze.py index 0e2415b59e27..38459b174df8 100644 --- a/tests/layer_tests/pytorch_tests/test_squeeze.py +++ b/tests/layer_tests/pytorch_tests/test_squeeze.py @@ -63,6 +63,8 @@ def forward(self, x): return aten_squeeze_copy(dim), "aten::squeeze_copy" @pytest.mark.parametrize("dim,dynamic_shapes", [(-2, True), (0, True), (None, False)]) + @pytest.mark.nightly + @pytest.mark.precommit @pytest.mark.precommit_fx_backend def test_squeeze_copy(self, dim, dynamic_shapes, ie_device, precision, ir_version): if PytorchLayerTest.use_torch_export() and dim is None: diff --git a/tests/layer_tests/pytorch_tests/test_unary_ops.py b/tests/layer_tests/pytorch_tests/test_unary_ops.py index f99644923f5a..3dd509608a85 100644 --- a/tests/layer_tests/pytorch_tests/test_unary_ops.py +++ b/tests/layer_tests/pytorch_tests/test_unary_ops.py @@ -283,8 +283,6 @@ def test_unary_op_float(self, op_type, dtype, ie_device, precision, ir_version): ]) def test_unary_op_out(self, op_type, dtype, ie_device, precision, ir_version): self.dtype = dtype - if ie_device == "GPU" and op_type == "aten::erfinv": - pytest.xfail(reason="erfinv is not supported on GPU") self._test(unary_op_out_net(OPS[op_type], dtype), op_type, ie_device, precision, ir_version, kwargs_to_prepare_input={"unit_range": op_type in ("aten::atanh", "aten::erfinv")}) diff --git a/tests/layer_tests/pytorch_tests/test_unsqueeze.py b/tests/layer_tests/pytorch_tests/test_unsqueeze.py index f9fd4df5205d..7322a5ed93e1 100644 --- a/tests/layer_tests/pytorch_tests/test_unsqueeze.py +++ b/tests/layer_tests/pytorch_tests/test_unsqueeze.py @@ -64,6 +64,8 @@ def forward(self, x): return model_class(dim), op @pytest.mark.parametrize("dim", [0, 1, -1]) + @pytest.mark.nightly + @pytest.mark.precommit @pytest.mark.precommit_fx_backend def test_unsqueeze_copy(self, dim, ie_device, precision, ir_version): self._test(*self.create_model(dim), ie_device, precision, ir_version) diff --git a/tests/layer_tests/pytorch_tests/test_view.py b/tests/layer_tests/pytorch_tests/test_view.py index 8934e2530952..98316e63ae91 100644 --- a/tests/layer_tests/pytorch_tests/test_view.py +++ b/tests/layer_tests/pytorch_tests/test_view.py @@ -179,6 +179,8 @@ def forward(self, input_tensor): return aten_view_copy(self.input_data), "aten::view_copy" + @pytest.mark.nightly + @pytest.mark.precommit @pytest.mark.precommit_fx_backend def test_view_copy(self, ie_device, precision, ir_version, input_shapes): self.input_data = [] diff --git a/tests/layer_tests/pytorch_tests/test_where.py b/tests/layer_tests/pytorch_tests/test_where.py index d377824f4b23..f8409e7f2322 100644 --- a/tests/layer_tests/pytorch_tests/test_where.py +++ b/tests/layer_tests/pytorch_tests/test_where.py @@ -142,7 +142,8 @@ def test_where_as_nonzero_export(self, mask_fill, mask_dtype, x_dtype, ie_device 'mask_dtype': mask_dtype, 'return_x_y': False, "x_dtype": x_dtype, - }) + }, + trace_model=True) @pytest.mark.parametrize("cond_val", ['zeros', 'ones']) @pytest.mark.parametrize("x_dtype", ["float32", "int32"]) diff --git a/tests/layer_tests/tensorflow_lite_tests/test_tfl_Conv3D.py b/tests/layer_tests/tensorflow_lite_tests/test_tfl_Conv3D.py new file mode 100644 index 000000000000..0877c79268b2 --- /dev/null +++ b/tests/layer_tests/tensorflow_lite_tests/test_tfl_Conv3D.py @@ -0,0 +1,44 @@ +# Copyright (C) 2018-2026 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +import numpy as np +import pytest +import tensorflow as tf + +from common.tflite_layer_test_class import TFLiteLayerTest + +np.random.seed(42) + +test_params = [ + {'shape': [1, 16, 28, 28, 3], 'ksize': [1, 3, 3, 3, 1], 'strides': (1, 1, 1, 1, 1), 'padding': 'SAME', 'dilations': [1, 1, 1, 1, 1]}, + {'shape': [1, 18, 230, 230, 3], 'ksize': [1, 7, 7, 3, 1], 'strides': (1, 1, 1, 1, 1), 'padding': 'SAME', 'dilations': [1, 2, 2, 2, 1]}, + {'shape': [1, 18, 230, 230, 3], 'ksize': [1, 7, 7, 3, 1], 'strides': (1, 1, 2, 2, 1), 'padding': 'SAME', 'dilations': [1, 1, 1, 1, 1]}, + {'shape': [1, 18, 230, 230, 3], 'ksize': [1, 7, 7, 3, 1], 'strides': (1, 2, 2, 2, 1), 'padding': 'VALID', 'dilations': [1, 1, 1, 1, 1]}, + {'shape': [1, 16, 112, 112, 64], 'ksize': [1, 1, 1, 64, 1], 'strides': (1, 1, 1, 1, 1), 'padding': 'SAME', 'dilations': [1, 1, 1, 1, 1]}, + {'shape': [1, 16, 112, 112, 64], 'ksize': [1, 1, 1, 64, 128], 'strides': (1, 1, 2, 2, 1), 'padding': 'VALID', 'dilations': [1, 1, 1, 1, 1]}, +] + + +class TestTFLiteConv3DLayerTest(TFLiteLayerTest): + inputs = ["Input"] + outputs = ["Conv3D"] + allowed_ops = ['CONV_3D'] + + def make_model(self, params): + assert len(set(params.keys()).intersection({'shape', 'ksize', 'strides', + 'padding', 'dilations'})) == 5, \ + 'Unexpected parameters for test: ' + ','.join(params.keys()) + tf.compat.v1.reset_default_graph() + with tf.compat.v1.Session() as sess: + weights = tf.constant(np.random.randn(*params['ksize']), dtype=tf.float32) + place_holder = tf.compat.v1.placeholder(params.get('dtype', tf.float32), params['shape'], + name=self.inputs[0]) + tf.nn.conv3d(place_holder, weights, params['strides'], params['padding'], 'NDHWC', + params['dilations'], name=self.outputs[0]) + net = sess.graph_def + return net + + @pytest.mark.parametrize("params", test_params) + @pytest.mark.nightly + def test_conv3d(self, params, ie_device, precision, temp_dir): + self._test(ie_device, precision, temp_dir, {**params, 'custom_eps': 0.5}) diff --git a/tests/layer_tests/tensorflow_lite_tests/test_tfl_Relu0To1.py b/tests/layer_tests/tensorflow_lite_tests/test_tfl_Relu0To1.py new file mode 100644 index 000000000000..a8c777145dc1 --- /dev/null +++ b/tests/layer_tests/tensorflow_lite_tests/test_tfl_Relu0To1.py @@ -0,0 +1,66 @@ +# Copyright (C) 2018-2026 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +import os + +import numpy as np +import pytest +import tensorflow as tf +from tensorflow.lite.python import schema_py_generated as schema_fb +from tensorflow.lite.tools import flatbuffer_utils as utils + +from common.tflite_layer_test_class import TFLiteLayerTest + +test_params = [ + {'shape': [2, 10]}, + {'shape': [2, 10, 10, 3]}, +] + + +class TestTFLiteRelu0To1LayerTest(TFLiteLayerTest): + inputs = ["Input"] + outputs = ["Relu0To1"] + allowed_ops = ['RELU_0_TO_1'] + + def _prepare_input(self, inputs_dict, generator=None): + inputs_dict['Input'] = np.float32(4 * np.random.random_sample(inputs_dict['Input']) - 2) + return inputs_dict + + def make_model(self, params): + assert 'shape' in params, 'Unexpected parameters for test: ' + ','.join(params.keys()) + tf.compat.v1.reset_default_graph() + with tf.compat.v1.Session() as sess: + placeholder = tf.compat.v1.placeholder(tf.float32, params['shape'], + name=self.inputs[0]) + # Use RELU as a placeholder; produce_tflite_model will replace it with RELU_0_TO_1 + tf.nn.relu(placeholder, name=self.outputs[0]) + net = sess.graph_def + return net + + def produce_tflite_model(self, framework_model, save_path): + # Convert the TF graph (with RELU) to TFLite + tflite_model_path = super().produce_tflite_model(framework_model, save_path) + + # Post-process: replace RELU with RELU_0_TO_1 in the flatbuffer. + model_obj = utils.read_model(tflite_model_path) + replaced = False + relu0to1_op = schema_fb.BuiltinOperator.RELU_0_TO_1 + placeholder = schema_fb.BuiltinOperator.PLACEHOLDER_FOR_GREATER_OP_CODES + for oc in model_obj.operatorCodes: + if (oc.builtinCode == schema_fb.BuiltinOperator.RELU or + oc.deprecatedBuiltinCode == schema_fb.BuiltinOperator.RELU): + oc.builtinCode = relu0to1_op + oc.deprecatedBuiltinCode = min(relu0to1_op, placeholder) + replaced = True + + assert replaced, "Failed to find RELU operator to replace with RELU_0_TO_1" + + relu0to1_path = os.path.join(save_path, 'relu_0_to_1.tflite') + utils.write_model(model_obj, relu0to1_path) + return relu0to1_path + + @pytest.mark.parametrize("params", test_params) + @pytest.mark.nightly + @pytest.mark.precommit + def test_relu_0_to_1(self, params, ie_device, precision, temp_dir): + self._test(ie_device, precision, temp_dir, params) diff --git a/tests/layer_tests/tensorflow_tests/test_tf_AsString.py b/tests/layer_tests/tensorflow_tests/test_tf_AsString.py new file mode 100644 index 000000000000..0664645563e4 --- /dev/null +++ b/tests/layer_tests/tensorflow_tests/test_tf_AsString.py @@ -0,0 +1,71 @@ +# Copyright (C) 2018-2026 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +import numpy as np +import pytest +import tensorflow as tf +from common.tf_layer_test_class import CommonTFLayerTest +from common.utils.tf_utils import run_in_jenkins + +rng = np.random.default_rng() + + +class TestAsString(CommonTFLayerTest): + def _prepare_input(self, inputs_info): + assert 'input:0' in inputs_info + input_shape = inputs_info['input:0'] + inputs_data = {} + if self.input_dtype == np.bool_: + inputs_data['input:0'] = rng.choice([True, False], size=input_shape) + elif np.issubdtype(self.input_dtype, np.floating): + inputs_data['input:0'] = rng.uniform(-100, 100, input_shape).astype(self.input_dtype) + elif np.issubdtype(self.input_dtype, np.unsignedinteger): + inputs_data['input:0'] = rng.integers(0, 100, input_shape).astype(self.input_dtype) + else: + inputs_data['input:0'] = rng.integers(-100, 100, input_shape).astype(self.input_dtype) + return inputs_data + + def create_as_string_net(self, input_shape, input_dtype): + self.input_dtype = input_dtype + + tf_type_map = { + np.int32: tf.int32, + np.int64: tf.int64, + np.int16: tf.int16, + np.int8: tf.int8, + np.uint8: tf.uint8, + np.uint16: tf.uint16, + np.uint32: tf.uint32, + np.uint64: tf.uint64, + np.float32: tf.float32, + np.float64: tf.float64, + np.bool_: tf.bool, + } + + tf.compat.v1.reset_default_graph() + with tf.compat.v1.Session() as sess: + input = tf.compat.v1.placeholder(tf_type_map[input_dtype], input_shape, 'input') + tf.raw_ops.AsString(input=input, name='AsString') + + tf.compat.v1.global_variables_initializer() + tf_net = sess.graph_def + + return tf_net, None + + # Note: float32/float64 are excluded because TF uses fixed-point formatting + # (e.g. "1.000000") while the tokenizers extension uses default stream formatting + # (e.g. "1"). The numeric values are correct but string representations differ. + # Note: bool is excluded because TF bool maps to u8 in OpenVINO element types, + # so NumericToString treats it as an integer rather than "true"/"false". + @pytest.mark.parametrize("input_shape", [[3], [2, 3], [1, 2, 3]]) + @pytest.mark.parametrize("input_dtype", [ + np.int32, np.int64, np.int16, np.int8, + np.uint8, np.uint16, np.uint32, np.uint64, + ]) + @pytest.mark.precommit + @pytest.mark.nightly + def test_as_string(self, input_shape, input_dtype, ie_device, precision, ir_version, temp_dir): + if ie_device == 'GPU' or run_in_jenkins(): + pytest.skip("operation extension is not supported on GPU") + self._test(*self.create_as_string_net(input_shape=input_shape, input_dtype=input_dtype), + ie_device, precision, ir_version, temp_dir=temp_dir) diff --git a/tests/layer_tests/tensorflow_tests/test_tf_ReverseSequence.py b/tests/layer_tests/tensorflow_tests/test_tf_ReverseSequence.py index b04cfb4a9370..522f9287dfc7 100644 --- a/tests/layer_tests/tensorflow_tests/test_tf_ReverseSequence.py +++ b/tests/layer_tests/tensorflow_tests/test_tf_ReverseSequence.py @@ -49,3 +49,56 @@ def create_reverse_sequence_net(self, input_shape, input_type, seq_lengths_type, def test_reverse_sequence_basic(self, params, ie_device, precision, ir_version, temp_dir): self._test(*self.create_reverse_sequence_net(**params), ie_device, precision, ir_version, temp_dir=temp_dir) + + +class TestComplexReverseSequence(CommonTFLayerTest): + def _prepare_input(self, inputs_info): + rng = np.random.default_rng() + assert 'param_real:0' in inputs_info + assert 'param_imag:0' in inputs_info + assert 'seq_lengths:0' in inputs_info + param_real_shape = inputs_info['param_real:0'] + param_imag_shape = inputs_info['param_imag:0'] + seq_lengths_shape = inputs_info['seq_lengths:0'] + inputs_data = {} + inputs_data['param_real:0'] = 4 * rng.random(param_real_shape).astype(np.float32) - 2 + inputs_data['param_imag:0'] = 4 * rng.random(param_imag_shape).astype(np.float32) - 2 + inputs_data['seq_lengths:0'] = np.random.randint( + 0, self.max_seq_length + 1, seq_lengths_shape).astype(np.int32) + return inputs_data + + def create_complex_reverse_sequence_net(self, input_shape, seq_dim, batch_dim): + assert 0 <= batch_dim < len(input_shape), "Incorrect batch_dim in the test case" + assert 0 <= seq_dim < len(input_shape), "Incorrect seq_dim in the test case" + self.max_seq_length = input_shape[seq_dim] + batch_size = input_shape[batch_dim] + tf.compat.v1.reset_default_graph() + with tf.compat.v1.Session() as sess: + param_real = tf.compat.v1.placeholder(np.float32, input_shape, 'param_real') + param_imag = tf.compat.v1.placeholder(np.float32, input_shape, 'param_imag') + seq_lengths = tf.compat.v1.placeholder(np.int32, [batch_size], 'seq_lengths') + complex_tensor = tf.raw_ops.Complex(real=param_real, imag=param_imag) + reverse_sequence = tf.raw_ops.ReverseSequence( + input=complex_tensor, + seq_lengths=seq_lengths, + seq_dim=seq_dim, + batch_dim=batch_dim) + tf.raw_ops.Real(input=reverse_sequence) + tf.raw_ops.Imag(input=reverse_sequence) + tf.compat.v1.global_variables_initializer() + tf_net = sess.graph_def + return tf_net, None + + test_data_complex = [ + dict(input_shape=[2, 3], seq_dim=1, batch_dim=0), + dict(input_shape=[3, 6, 4], seq_dim=2, batch_dim=0), + dict(input_shape=[6, 3, 4, 2], seq_dim=0, batch_dim=3), + ] + + @pytest.mark.parametrize("params", test_data_complex) + @pytest.mark.precommit + @pytest.mark.nightly + def test_complex_reverse_sequence(self, params, ie_device, precision, ir_version, temp_dir): + self._test( + *self.create_complex_reverse_sequence_net(**params), + ie_device, precision, ir_version, temp_dir=temp_dir) diff --git a/tests/layer_tests/tensorflow_tests/test_tf_UnsortedSegmentMax.py b/tests/layer_tests/tensorflow_tests/test_tf_UnsortedSegmentMax.py new file mode 100644 index 000000000000..e167105815fc --- /dev/null +++ b/tests/layer_tests/tensorflow_tests/test_tf_UnsortedSegmentMax.py @@ -0,0 +1,80 @@ +# Copyright (C) 2018-2026 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +import numpy as np +import pytest +import tensorflow as tf +from common.tf_layer_test_class import CommonTFLayerTest + +rng = np.random.default_rng(56234) + + +class TestUnsortedSegmentMax(CommonTFLayerTest): + def _prepare_input(self, inputs_info): + assert 'data:0' in inputs_info, "Test error: inputs_info must contain `data`" + assert 'segment_ids:0' in inputs_info, "Test error: inputs_info must contain `segment_ids`" + data_shape = inputs_info['data:0'] + segment_ids_shape = inputs_info['segment_ids:0'] + inputs_data = {} + inputs_data['data:0'] = rng.integers(-50, 50, data_shape).astype(self.data_type) + num_ids = segment_ids_shape[0] + if num_ids >= self.num_segments_val: + ids = list(range(self.num_segments_val)) + ids += rng.integers(0, self.num_segments_val, num_ids - self.num_segments_val).tolist() + rng.shuffle(ids) + else: + ids = rng.integers(0, self.num_segments_val, num_ids).tolist() + inputs_data['segment_ids:0'] = np.array(ids, dtype=self.segment_ids_type) + return inputs_data + + def create_unsorted_segment_max_net(self, data_shape, segment_ids_shape, num_segments_val, data_type, + segment_ids_type, num_segments_type): + self.data_type = data_type + self.segment_ids_type = segment_ids_type + self.num_segments_val = num_segments_val + tf.compat.v1.reset_default_graph() + with tf.compat.v1.Session() as sess: + data = tf.compat.v1.placeholder(data_type, data_shape, 'data') + segment_ids = tf.compat.v1.placeholder(segment_ids_type, segment_ids_shape, 'segment_ids') + num_segments = tf.constant(num_segments_val, dtype=num_segments_type, shape=[]) + tf.raw_ops.UnsortedSegmentMax(data=data, segment_ids=segment_ids, num_segments=num_segments) + tf.compat.v1.global_variables_initializer() + + tf_net = sess.graph_def + + return tf_net, None + + test_data_basic = [ + dict(data_shape=[8], segment_ids_shape=[8], num_segments_val=5), + dict(data_shape=[10, 4], segment_ids_shape=[10], num_segments_val=5), + dict(data_shape=[8, 6, 7], segment_ids_shape=[8], num_segments_val=8), + ] + + @pytest.mark.parametrize("params", test_data_basic) + @pytest.mark.parametrize("data_type", [np.float32, np.int32]) + @pytest.mark.parametrize("segment_ids_type", [np.int32, np.int64]) + @pytest.mark.parametrize("num_segments_type", [np.int32, np.int64]) + @pytest.mark.precommit + @pytest.mark.nightly + def test_unsorted_segment_max_basic(self, params, data_type, segment_ids_type, num_segments_type, ie_device, + precision, ir_version, temp_dir): + self._test(*self.create_unsorted_segment_max_net(**params, + data_type=data_type, segment_ids_type=segment_ids_type, + num_segments_type=num_segments_type), + ie_device, precision, ir_version, temp_dir=temp_dir) + + test_data_empty_segments = [ + dict(data_shape=[4], segment_ids_shape=[4], num_segments_val=10), + dict(data_shape=[3, 5], segment_ids_shape=[3], num_segments_val=8), + ] + + @pytest.mark.parametrize("params", test_data_empty_segments) + @pytest.mark.precommit + @pytest.mark.nightly + def test_unsorted_segment_max_empty_segments(self, params, ie_device, precision, ir_version, temp_dir): + if precision == 'FP16': + pytest.skip("FP16 lowest fill value differs from FP32 for empty segments") + self._test(*self.create_unsorted_segment_max_net(**params, + data_type=np.float32, segment_ids_type=np.int32, + num_segments_type=np.int32), + ie_device, precision, ir_version, temp_dir=temp_dir) diff --git a/tests/memory_tests/src/memory_test.hpp b/tests/memory_tests/src/memory_test.hpp index b3a273d9f3de..810a3363e381 100644 --- a/tests/memory_tests/src/memory_test.hpp +++ b/tests/memory_tests/src/memory_test.hpp @@ -153,10 +153,10 @@ struct Context { << "\"samples\": {"; for (auto &sample: samples) { std::cout << "\"" << sample.first << "\": {" - << "\"vmsize\": " << sample.second.virtual_size << ", " - << "\"vmpeak\": " << sample.second.virtual_peak << ", " - << "\"vmrss\": " << sample.second.resident_size << ", " - << "\"vmhwm\": " << sample.second.resident_peak << ", " + << "\"system_size\": " << sample.second.virtual_size << ", " + << "\"system_peak\": " << sample.second.virtual_peak << ", " + << "\"system_rss\": " << sample.second.resident_size << ", " + << "\"system_hwm\": " << sample.second.resident_peak << ", " << "\"threads\": " << sample.second.thread_count << ", " << "\"gpu_local_used\": " << sample.second.gpu_local_used << ", " << "\"gpu_local_total\": " << sample.second.gpu_local_total << ", " diff --git a/tests/memory_tests/tools/run_tests.py b/tests/memory_tests/tools/run_tests.py index 071dc373edce..c65902385f6c 100644 --- a/tests/memory_tests/tools/run_tests.py +++ b/tests/memory_tests/tools/run_tests.py @@ -2,6 +2,7 @@ # SPDX-License-Identifier: Apache-2.0 # +import platform import argparse import glob import os @@ -10,11 +11,11 @@ import subprocess import json import time +import sys +from typing import Any from pathlib import Path -from datetime import datetime, timedelta from dataclasses import dataclass, asdict -from collections import defaultdict try: import requests @@ -22,6 +23,102 @@ requests = None +INTEL_FAMILIES = { + (0x06, 0x5E): "skylake", + (0x06, 0x55): "skylake", + (0x06, 0x8E): "kabylake", + (0x06, 0x9E): "kabylake", + (0x06, 0xA5): "cometlake", + (0x06, 0xA6): "cometlake", + (0x06, 0x66): "cannonlake", + (0x06, 0x6A): "icelake", + (0x06, 0x6C): "icelake", + (0x06, 0x7D): "icelake", + (0x06, 0x7E): "icelake", + (0x06, 0x9D): "icelake", + (0x06, 0xA7): "rocketlake", + (0x06, 0x8C): "tigerlake", + (0x06, 0x8D): "tigerlake", + (0x06, 0x8F): "sapphirerapids-server", + (0x06, 0xCF): "emeraldrapids-server", + (0x06, 0xAD): "graniterapids", + (0x06, 0xAE): "graniterapids", + (0x13, 0x01): "diamondrapids", + (0x06, 0xD7): "bartlettlake", + (0x06, 0x8A): "lakefield", + (0x06, 0x97): "alderlake", + (0x06, 0x9A): "alderlake", + (0x06, 0xB7): "raptorlake", + (0x06, 0xBA): "raptorlake", + (0x06, 0xBF): "raptorlake", + (0x06, 0xAC): "meteorlake", + (0x06, 0xAA): "meteorlake", + (0x06, 0xC5): "arrowlake", + (0x06, 0xC6): "arrowlake", + (0x06, 0xB5): "arrowlake", + (0x06, 0xBD): "lunarlake", + (0x06, 0xCC): "pantherlake", + (0x06, 0xD5): "wildcatlake", + (0x12, 0x01): "novalake", + (0x12, 0x03): "novalake" +} + + +def get_cpu_family(): + arch = platform.machine().lower() + system = platform.system() + if arch in ("arm64", "aarch64"): + if system == "Darwin": + return subprocess.check_output( + ["sysctl", "machdep.cpu.brand_string"]).decode().strip() + else: + # not implemented + return "Unknown arm64" + elif arch in ("x86_64", "amd64"): + if system == "Windows": + cpuinfo = subprocess.check_output( + ["powershell", "(Get-WmiObject -Class Win32_Processor).Caption"]).decode().strip() + infomatch = re.match(r".* Family (\d+) Model (\d+) .*", + cpuinfo.splitlines()[-1]) + if not infomatch: + return "Unknown x86_64" + try: + family, model = map(int, infomatch.groups()) + except ValueError: + return "Unknown x86_64" + elif system == "Linux": + with open("/proc/cpuinfo") as cpuinfofile: + cpuinfo = cpuinfofile.read().strip().split("\n\n")[0] + cpuinfo = ( + map(str.strip, line.split(":", 1)) + for line in cpuinfo.split("\n") + ) + cpuinfo = {k: v for k, v in cpuinfo} + try: + family = int(cpuinfo.get("cpu family", "")) + model = int(cpuinfo.get("model", "")) + except ValueError: + return "Unknown x86_64" + elif system == "Darwin": + family = subprocess.check_output( + ["sysctl", "machdep.cpu.family"]).decode().strip() + model = subprocess.check_output( + ["sysctl", "machdep.cpu.model"]).decode().strip() + try: + family = int(family) + model = int(model) + except ValueError: + return "Unknown x86_64" + else: + return "Unknown x86_64" + return INTEL_FAMILIES.get((family, model), "Unknown x86_64") + else: + return "Unknown" + + +CPU_FAMILY = get_cpu_family() + + def value_diff(value, reference): difference = value - reference diff_ratio = difference / reference @@ -42,10 +139,10 @@ def attempt(func, *args, **kwargs): @dataclass class MemSample: - vmsize: int - vmpeak: int - vmrss: int - vmhwm: int + system_size: int + system_peak: int + system_rss: int + system_hwm: int threads: int gpu_local_used: int = -1 gpu_local_total: int = -1 @@ -76,7 +173,7 @@ def __repr__(self): def run_test_executable_extract_result(command): - proc = subprocess.run(command, stdout=subprocess.PIPE) + proc = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE) run_out = proc.stdout.decode() if run_out.startswith("TEST_RESULTS: "): results_json = run_out.splitlines()[0].removeprefix("TEST_RESULTS: ") @@ -137,7 +234,7 @@ def get_test_info(self): return result def api(self, method, data=None, **kwargs): - extra_args = {"timeout": 30} + extra_args: dict[str, Any] = {"timeout": 30} extra_args.update(kwargs) if self.report_api is None: raise Exception("Report API was not specified") @@ -149,7 +246,7 @@ def api(self, method, data=None, **kwargs): print(f"API Error: {response.text}") return response.json() - def api_push_test_result(self, source, modelid, device, result): + def api_push_test_result(self, model_path, modelid, weights_size, device, result): if not self.report_metadata: print("No job metadata found, no report will be made.") return @@ -163,17 +260,19 @@ def api_push_test_result(self, source, modelid, device, result): sample_report.update({ "test_name": f"{result.get('test', self.test_name)}:{sname}", "status": "failed" if "error" in result else "passed", - "source": source, + "source": model_path, "log": result.get("stderr", ""), "model_name": modelname, "model": modelid, "device": result.get("device") or device, "framework": framework, "precision": precision, - "metrics": sample.as_dict() + "metrics": sample.as_dict(), + "cpu_family": CPU_FAMILY, + "model_size": weights_size }) test_report.append(sample_report) - response = attempt(self.api, "v1/memory/push-2-db-facade", {"data": test_report}) + response = attempt(self.api, "v2/memory/push-2-db-facade", {"data": test_report}) if response: print(f"Push result to API: {response}") @@ -189,7 +288,7 @@ def detect_report_metadata(self): "branch": os.environ.get("sourceBranch", "unknown"), "target_branch": os.environ.get("targetBranch", "unknown"), "log_path": os.environ.get("SHARED_LOG_PATH", ""), - "dldt_version": build_number, + "version": build_number, "ext": {} } except KeyError as err: @@ -219,9 +318,21 @@ def scan_directory(self, directory: Path): yield from ((path.removeprefix(cache_dir).replace("\\", "/"), path) for path in new_files) def generate_test_cases(self): + def _with_filesize(paths): + for (modelid, path) in paths: + weights_path, _ = os.path.splitext(path) + weights_path = f"{weights_path}.bin" + if os.path.isfile(weights_path): + weights_size = os.path.getsize(weights_path) + else: + # weights file does not exist -> invalid test case + continue + yield modelid, path, weights_size for ir_cache_dir in self.ir_cache_dirs: yield from itertools.product( - self.scan_directory(ir_cache_dir), self.devices) + _with_filesize(self.scan_directory(ir_cache_dir)), + self.devices + ) def run_test_case(self, model_path, device): try: @@ -230,9 +341,21 @@ def run_test_case(self, model_path, device): print(f" When running test an unexpected error happened: {ex}") return {"error": "unexpected error", "exception": ex} - def handle_test_result(self, modelid, device, result): + def handle_test_result(self, modelid, weights_size, device, result): + base2_suffixes = ["bytes", "KiB", "MiB", "GiB", "TiB", "PiB"] + + def _base2_human_readable(number): + order_of_magnitude = 0 + one_order_higher = 1 << 10 + while number > one_order_higher and order_of_magnitude < len(base2_suffixes) - 1: + number >>= 10 + order_of_magnitude += 1 + suffix = base2_suffixes[order_of_magnitude] + return f"{number} {suffix}" + status = "error" if "error" in result else "ok" - print(f"TEST {modelid} x {device}: {status}") + weights_size_human_read = _base2_human_readable(weights_size) + print(f"TEST {modelid} ({weights_size_human_read}) x {device}: {status}") if status == "error": error = result.get("error") stdout = result.get("stdout") @@ -245,16 +368,18 @@ def handle_test_result(self, modelid, device, result): print(f" stderr: {stderr.strip()}\n === END OF STDERR ===") if exception: print(f" {repr(exception)}") - return - for sname, sample in result["samples"].items(): - print(f" {sname:>15}: {sample}") + else: + for sname, sample in result["samples"].items(): + print(f" {sname:>15}: {sample}") + sys.stdout.flush() + sys.stderr.flush() def run(self): - for (modelid, model_path), device in self.generate_test_cases(): + for (modelid, model_path, weights_size), device in self.generate_test_cases(): result = self.run_test_case(model_path, device) test_name = result.get("test", self.test_name) - self.api_push_test_result(model_path, modelid, device, result) - self.handle_test_result(modelid, device, result) + self.api_push_test_result(model_path, modelid, weights_size, device, result) + self.handle_test_result(modelid, weights_size, device, result) if __name__ == "__main__": @@ -282,7 +407,7 @@ def run(self): TestSession( args.test_executable, args.ir_cache, - args.devices.split(","), + [device.upper() for device in args.devices.split(",")], args.api, args.upload_reference ).run() diff --git a/tests/model_hub_tests/pytorch/envs/llm.txt b/tests/model_hub_tests/pytorch/envs/llm.txt index 31fddc1dbf04..aa1de5617bbc 100644 --- a/tests/model_hub_tests/pytorch/envs/llm.txt +++ b/tests/model_hub_tests/pytorch/envs/llm.txt @@ -1,13 +1,27 @@ # Extra dependencies for test_llm.py (LLM quantized models) # These are NOT needed by test_hf_transformers.py +# +# Versions below are hard-pinned (==) intentionally. transformers/gptqmodel are bumped +# deliberately, not automatically: a silent upgrade changes the GPTQ backend selection +# (e.g. gptqmodel auto-selecting TorchFusedQuantLinear -> QUANT_TYPE "torch_fused") and the +# generated graph, which previously broke conversion. Bump these together and re-validate +# the opt_gptq entry in test_llm.py before raising them. transformers==5.5.3 huggingface-hub==1.10.1 +# kernels (and kernels-data) are pulled transitively by transformers and gptqmodel, neither of +# which caps the version. kernels>=0.15 made LayerRepository require a version/revision, which +# transformers 5.5.3's hub_kernels.py constructs without -> ImportError at "import transformers". +# Keep at the validated 0.14.1. +kernels==0.14.1 +kernels-data==0.14.1 + # quantized model deps autoawq==0.2.9; platform_system == "Linux" and platform_machine == "x86_64" triton==3.6.0; platform_system == "Linux" and platform_machine == "x86_64" gptqmodel==6.0.3; platform_system == "Linux" and platform_machine == "x86_64" and python_version < "3.12" + peft==0.18.1; platform_system == "Linux" and platform_machine == "x86_64" and python_version < "3.12" # optimum (required by transformers' GptqHfQuantizer for GPTQ model loading) diff --git a/tests/model_hub_tests/pytorch/test_torchvision_models.py b/tests/model_hub_tests/pytorch/test_torchvision_models.py index 66a425d7308e..75dcfae8b964 100644 --- a/tests/model_hub_tests/pytorch/test_torchvision_models.py +++ b/tests/model_hub_tests/pytorch/test_torchvision_models.py @@ -3,12 +3,10 @@ import os import platform -import tempfile import pytest import torch -import torchvision.transforms.functional as F -from torchvision.models import list_models, get_model, get_model_weights +from torchvision.models import list_models, get_model from models_hub_common.utils import get_models_list, retry from torch_utils import TestTorchConvertModel @@ -19,35 +17,6 @@ def get_all_models() -> list: return m_list -def get_video(): - """ - Download video and return frames. - Using free video from pexels.com, credits go to Pavel Danilyuk. - Initially used in https://pytorch.org/vision/stable/auto_examples/plot_optical_flow.html - """ - from pathlib import Path - from urllib.request import urlretrieve - from torchvision.io import read_video - - video_url = "https://download.pytorch.org/tutorial/pexelscom_pavel_danilyuk_basketball_hd.mp4" - with tempfile.TemporaryDirectory() as tmp: - video_path = Path(tmp) / "basketball.mp4" - _ = urlretrieve(video_url, video_path) - - frames, _, _ = read_video(str(video_path), output_format="TCHW") - return frames - - -def prepare_frames_for_raft(name, frames1, frames2): - w = get_model_weights(name).DEFAULT - img1_batch = torch.stack(frames1) - img2_batch = torch.stack(frames2) - img1_batch = F.resize(img1_batch, size=[520, 960], antialias=False) - img2_batch = F.resize(img2_batch, size=[520, 960], antialias=False) - img1_batch, img2_batch = w.transforms()(img1_batch, img2_batch) - return (img1_batch, img2_batch) - - # To make tests reproducible we seed the random generator torch.manual_seed(0) @@ -65,13 +34,28 @@ def load_model(self, model_name, model_link): self.example = (torch.randn(2, 3, 16, 224, 224),) self.inputs = (torch.randn(3, 3, 16, 224, 224),) elif "raft" in model_name: - frames = get_video() - self.example = prepare_frames_for_raft(model_name, - [frames[100], frames[150]], - [frames[101], frames[151]]) - self.inputs = prepare_frames_for_raft(model_name, - [frames[75], frames[125]], - [frames[76], frames[126]]) + # torchvision.io.read_video was removed in torchvision 0.27.0 and the + # CPU wheel has no video-reading backend. Use smooth synthetic frames + # (sine/cosine patterns with a small periodic shift) instead: structured + # inputs give RAFT a well-conditioned optical-flow problem so the FP32 + # differences between OV and PyTorch stay within the 0.05 tolerance. + # The shape (2, 3, 520, 960) matches what the old resize+normalise + # pipeline produced from the basketball clip. + import math + h, w = 520, 960 + y = torch.linspace(-math.pi, math.pi, h).view(h, 1) + x = torch.linspace(-math.pi, math.pi, w).view(1, w) + freq = 4.0 + frame = torch.stack([ + torch.sin(freq * x) * torch.cos(freq * y), + torch.cos(freq * x) * torch.sin(freq * y), + torch.sin(freq * (x + y) * 0.5), + ], dim=0) # [3, 520, 960], values in [-1, 1] + # 2-pixel cyclic roll along width = stable constant horizontal flow + frame1 = frame.unsqueeze(0).expand(2, -1, -1, -1).contiguous() + frame2 = torch.roll(frame, shifts=2, dims=2).unsqueeze(0).expand(2, -1, -1, -1).contiguous() + self.example = (frame1, frame2) + self.inputs = (frame1.clone(), frame2.clone()) elif "vit_h_14" in model_name: self.example = (torch.randn(1, 3, 518, 518),) self.inputs = (torch.randn(1, 3, 518, 518),) diff --git a/tests/requirements_pytorch b/tests/requirements_pytorch index debf84e24ffc..ac1b2bbe324a 100644 --- a/tests/requirements_pytorch +++ b/tests/requirements_pytorch @@ -6,11 +6,11 @@ # test against NumPy 1.x with older Python versions numpy==1.26.4; python_version < "3.12" numpy==2.1.1; python_version >= "3.12" -torch==2.9.0 + --extra-index-url https://download.pytorch.org/whl/cpu +torch==2.12.0 +torchvision==0.27.0 -torchvision==0.24.0 -torchaudio==2.9.0 pytest==7.0.1; python_version < '3.10' pytest==7.2.0; python_version >= '3.10' pytest-html==4.2.0 diff --git a/tests/stress_tests/scripts/memcheck_upload.py b/tests/stress_tests/scripts/memcheck_upload.py index 0a58ed121424..ec1b3e99bc5d 100644 --- a/tests/stress_tests/scripts/memcheck_upload.py +++ b/tests/stress_tests/scripts/memcheck_upload.py @@ -1,8 +1,7 @@ -#!/usr/bin/env python3 - # Copyright (C) 2018-2026 Intel Corporation # SPDX-License-Identifier: Apache-2.0 + """ Upload metrics gathered by MemCheckTests into Mongo DB Usage: ./scrips/memcheck_upload.py https://ci.intel.com/job/memchek/1234/ \ @@ -34,8 +33,7 @@ PRODUCT_NAME = 'dldt' # product name from build manifest RE_GTEST_MODEL_XML = re.compile(r']*>') RE_GTEST_CUR_MEASURE = re.compile(r'\[\s*MEASURE\s*\]') -RE_GTEST_REF_MEASURE = re.compile( - r'Reference values of virtual memory consumption') +RE_GTEST_REF_MEASURE = re.compile(r'Reference values of virtual memory consumption') RE_GTEST_PASSED = re.compile(r'\[\s*PASSED\s*\]') RE_GTEST_FAILED = re.compile(r'\[\s*FAILED\s*\]') GTEST_INFO = '[ INFO ]' diff --git a/thirdparty/dependencies.cmake b/thirdparty/dependencies.cmake index ac5ebe105c81..5e1073d8025f 100644 --- a/thirdparty/dependencies.cmake +++ b/thirdparty/dependencies.cmake @@ -405,8 +405,8 @@ if(ENABLE_OV_PADDLE_FRONTEND OR ENABLE_OV_ONNX_FRONTEND OR ENABLE_OV_TF_FRONTEND if(ENABLE_THREAD_SANITIZER AND OV_COMPILER_IS_CLANG) foreach(proto_target protoc libprotobuf libprotobuf-lite) if(TARGET ${proto_target}) - target_compile_options(${proto_target} PUBLIC -fno-sanitize=thread) - target_link_options(${proto_target} PUBLIC -fno-sanitize=thread) + target_compile_options(${proto_target} PRIVATE -fno-sanitize=thread) + target_link_options(${proto_target} PRIVATE -fno-sanitize=thread) endif() endforeach() endif() @@ -477,6 +477,13 @@ if(ENABLE_OV_TF_LITE_FRONTEND OR ENABLE_INTEL_NPU) add_executable(flatc ALIAS flatbuffers::flatc) endif() endif() + # disable TSan for flatc binary, only used as a build time tool, not in runtime. + if(ENABLE_THREAD_SANITIZER AND TARGET flatc) + target_compile_options(flatc PRIVATE -fno-sanitize=thread) + target_link_options(flatc PRIVATE -fno-sanitize=thread) + string(REPLACE "-shared-libsan" "" _flatc_exe_flags "${CMAKE_EXE_LINKER_FLAGS}") + set_target_properties(flatc PROPERTIES LINK_FLAGS "${_flatc_exe_flags}") + endif() endif() # set additional variables, used in other places of our cmake scripts diff --git a/thirdparty/flatbuffers/CMakeLists.txt b/thirdparty/flatbuffers/CMakeLists.txt index c0367c48cfac..60c5cf716322 100644 --- a/thirdparty/flatbuffers/CMakeLists.txt +++ b/thirdparty/flatbuffers/CMakeLists.txt @@ -24,14 +24,29 @@ if(FLATBUFFERS_BUILD_FLATC) set_target_properties(flatc PROPERTIES COMPILE_OPTIONS "-Wno-shadow") endif() + # we dont want to build flatc with ThreadSanitizer since it causes issues during our build. + # this might be incorrect if flatbuffers::flatc gets used / linked in the NPU Plugin + # current assumption is that it is only a build tool + if(ENABLE_THREAD_SANITIZER AND TARGET flatc) + target_compile_options(flatc PRIVATE -fno-sanitize=thread) + target_link_options(flatc PRIVATE -fno-sanitize=thread) + string(REPLACE "-shared-libsan" "" _flatc_exe_flags "${CMAKE_EXE_LINKER_FLAGS}") + set_target_properties(flatc PROPERTIES LINK_FLAGS "${_flatc_exe_flags}") + endif() + set(flatbuffers_COMPILER $ PARENT_SCOPE) set(flatbuffers_DEPENDENCY flatc PARENT_SCOPE) else() set(HOST_FLATC_INSTALL_DIR "${CMAKE_CURRENT_BINARY_DIR}/install") + if(ENABLE_THREAD_SANITIZER) + set(flatc_extra_flags "-fno-sanitize=thread") + endif() + ov_native_compile_external_project( TARGET_NAME host_flatc NATIVE_INSTALL_DIR "${HOST_FLATC_INSTALL_DIR}" + EXTRA_COMPILE_FLAGS "${flatc_extra_flags}" CMAKE_ARGS "-DFLATBUFFERS_BUILD_TESTS=${FLATBUFFERS_BUILD_TESTS}" "-DFLATBUFFERS_BUILD_FLATLIB=OFF" "-DFLATBUFFERS_BUILD_FLATC=ON" diff --git a/thirdparty/gtest/CMakeLists.txt b/thirdparty/gtest/CMakeLists.txt index c2dac29f2bfd..bcf3a36f3458 100644 --- a/thirdparty/gtest/CMakeLists.txt +++ b/thirdparty/gtest/CMakeLists.txt @@ -20,11 +20,14 @@ function(_ov_gtest_filter_install_interface TARGET TYPE) get_target_property(include_dirs ${TARGET} INTERFACE_INCLUDE_DIRECTORIES) foreach(include_dir IN LISTS include_dirs) if(NOT include_dir MATCHES ".*INSTALL_INTERFACE.*") - # remove leading and trailing parts of generator expressions - string(REPLACE "$" "" include_dir "${include_dir}") - # wrap to BUILD_INTERFACE again - list(APPEND final_include_dirs "$") + # unwrap the outer BUILD_INTERFACE, preserving inner generator + # expressions such as $ introduced by upstream gtest. + string(REGEX REPLACE "^\\$$" "\\1" include_dir "${include_dir}") + # expand $ back into a real list so we can rewrap each entry + string(REPLACE "$" ";" include_dir "${include_dir}") + foreach(dir IN LISTS include_dir) + list(APPEND final_include_dirs "$") + endforeach() endif() endforeach() @@ -54,4 +57,12 @@ foreach(target gtest gtest_main gmock gmock_main) # disable warnings ov_disable_all_warnings(${target}) set_target_properties(${target} PROPERTIES FOLDER thirdparty) + + # thirdparty/dependencies.cmake enables CMAKE_INTERPROCEDURAL_OPTIMIZATION_RELEASE for the whole thirdparty scope + # when ENABLE_LTO=ON. Override it per-target for gtest: GCC 15's lto1 emits a false-positive + # -Wstringop-overflow through the std::unordered_set insertion path in UnitTestFilter + # gtest does not benefit from LTO; + if(CMAKE_COMPILER_IS_GNUCXX AND CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL 15) + set_target_properties(${target} PROPERTIES INTERPROCEDURAL_OPTIMIZATION_RELEASE FALSE) + endif() endforeach() diff --git a/thirdparty/gtest/gtest b/thirdparty/gtest/gtest index 99760ac17764..03e2ed1291b4 160000 --- a/thirdparty/gtest/gtest +++ b/thirdparty/gtest/gtest @@ -1 +1 @@ -Subproject commit 99760ac1776430f3df65947992bf4e8ebc0d7660 +Subproject commit 03e2ed1291b40062be24df240f597d995aaaa962 diff --git a/thirdparty/level_zero/level-zero b/thirdparty/level_zero/level-zero index d562046e7266..b77ced661af9 160000 --- a/thirdparty/level_zero/level-zero +++ b/thirdparty/level_zero/level-zero @@ -1 +1 @@ -Subproject commit d562046e7266120e8be3533597b65f34a00524c1 +Subproject commit b77ced661af97104f5cda03d49596274392f6aa4 diff --git a/thirdparty/onnx/CMakeLists.txt b/thirdparty/onnx/CMakeLists.txt index 7626cd76dc49..8e95d1e49010 100644 --- a/thirdparty/onnx/CMakeLists.txt +++ b/thirdparty/onnx/CMakeLists.txt @@ -18,11 +18,6 @@ else() set(ONNX_USE_LITE_PROTO_DEFAULT ON) endif() -if(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") - # 4244 conversion from 'XXX' to 'YYY', possible loss of data - ov_add_compiler_flags(/wd4244) -endif() - set(PYTHON_EXECUTABLE "${Python3_EXECUTABLE}") set(ONNX_USE_PROTOBUF_SHARED_LIBS OFF CACHE BOOL "Use dynamic protobuf by ONNX library" FORCE) set(ONNX_NAMESPACE ${OV_ONNX_NAMESPACE}) diff --git a/thirdparty/protobuf/CMakeLists.txt b/thirdparty/protobuf/CMakeLists.txt index a16bd56d11ce..5d73c6aef987 100644 --- a/thirdparty/protobuf/CMakeLists.txt +++ b/thirdparty/protobuf/CMakeLists.txt @@ -6,6 +6,11 @@ # Configure Google Protobuf ... #------------------------------------------------------------------------------ +if(ENABLE_THREAD_SANITIZER AND OV_COMPILER_IS_CLANG) + string(REPLACE "-ltsan" "" CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS}") + string(REPLACE "-shared-libsan" "" CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS}") +endif() + set(BUILD_SHARED_LIBS OFF) if(CMAKE_COMPILER_IS_GNUCXX OR OV_COMPILER_IS_CLANG OR (OV_COMPILER_IS_INTEL_LLVM AND UNIX)) @@ -64,6 +69,12 @@ if(CMAKE_COMPILER_IS_GNUCXX OR OV_COMPILER_IS_CLANG OR OV_COMPILER_IS_INTEL_LLVM C_VISIBILITY_PRESET default VISIBILITY_INLINES_HIDDEN OFF INTERPROCEDURAL_OPTIMIZATION_RELEASE OFF) + if(ENABLE_THREAD_SANITIZER) + foreach(_lib ${_protoc_libs}) + target_compile_options(${_lib} PRIVATE -fno-sanitize=thread) + endforeach() + target_link_options(protoc PRIVATE -fno-sanitize=thread) + endif() endif() ov_disable_all_warnings(${_protoc_libs} libprotobuf-lite) set_target_properties(libprotobuf-lite PROPERTIES @@ -79,9 +90,14 @@ endif() if(NOT protobuf_BUILD_PROTOC_BINARIES) set(HOST_PROTOC_INSTALL_DIR "${CMAKE_CURRENT_BINARY_DIR}/install") + if(ENABLE_THREAD_SANITIZER) + set(protoc_extra_flags "-fno-sanitize=thread") + endif() + ov_native_compile_external_project( TARGET_NAME host_protoc NATIVE_INSTALL_DIR "${HOST_PROTOC_INSTALL_DIR}" + EXTRA_COMPILE_FLAGS "${protoc_extra_flags}" CMAKE_ARGS "-Dprotobuf_VERBOSE=${protobuf_VERBOSE}" "-Dprotobuf_BUILD_TESTS=${protobuf_BUILD_TESTS}" "-Dprotobuf_WITH_ZLIB=${protobuf_WITH_ZLIB}" diff --git a/tools/commit_slider/utils/log_parser.py b/tools/commit_slider/utils/log_parser.py index 7cc48fa511be..0341bb59904c 100644 --- a/tools/commit_slider/utils/log_parser.py +++ b/tools/commit_slider/utils/log_parser.py @@ -32,7 +32,7 @@ def extractPatterns(dirName): with open(os.path.join(dirName, "logcommon_log.log")) as file: data = file.read() - intervalPattern = "[A-Za-z0-9]*\.\.[A-Za-z0-9]*" + intervalPattern = r'[A-Za-z0-9]*\.\.[A-Za-z0-9]*' pattern = "Check commits {}".format(intervalPattern) stats_re = re.compile(pattern, re.MULTILINE | re.DOTALL) @@ -45,7 +45,7 @@ def extractPatterns(dirName): return hashPatternList, intervalPatternList def prepareCSVData(hashMap, dirName): - throughputPattern = "Throughput:\s*([0-9]*[.][0-9]*)\s*FPS" + throughputPattern = r'Throughput:\s*([0-9]*[.][0-9]*)\s*FPS' csvData = [] for k, v in hashMap.items(): diff --git a/tools/ovc/openvino/tools/ovc/moc_frontend/pytorch_frontend_utils.py b/tools/ovc/openvino/tools/ovc/moc_frontend/pytorch_frontend_utils.py index 1522f4284a17..833e6026a571 100644 --- a/tools/ovc/openvino/tools/ovc/moc_frontend/pytorch_frontend_utils.py +++ b/tools/ovc/openvino/tools/ovc/moc_frontend/pytorch_frontend_utils.py @@ -156,7 +156,8 @@ def get_pytorch_decoder(model, example_inputs, args): decoder = TorchFXPythonDecoder.from_model( model, example_inputs=inputs, - dynamic_shapes=dynamic_shapes) + dynamic_shapes=dynamic_shapes, + module_extensions=extract_module_extensions(args)) else: decoder = TorchScriptPythonDecoder( model, @@ -175,14 +176,53 @@ def get_pytorch_decoder(model, example_inputs, args): return args -def get_pytorch_decoder_for_model_on_disk(argv, args): +def _is_pytorch_zip(path): + """Check if a file looks like a PyTorch archive (TorchScript or ExportedProgram). + + Both formats are ZIP archives with specific internal entries. + This lightweight check avoids expensive torch.jit.load / torch.export.load + calls (and their warnings/side effects) on non-PyTorch files. + """ + import zipfile + # Maximum bytes to read from the "archive_format" entry. The expected + # content is the literal b"pt2" (3 bytes); cap at a small constant so a + # crafted ZIP cannot trigger large/expensive decompression during + # auto-detection. + _ARCHIVE_FORMAT_MAX_BYTES = 16 try: - from openvino.frontend.pytorch.ts_decoder import TorchScriptPythonDecoder - from openvino.frontend.pytorch.fx_decoder import TorchFXPythonDecoder - import torch - except: + if not zipfile.is_zipfile(path): + return False + with zipfile.ZipFile(path, 'r') as zf: + for info in zf.infolist(): + name = info.filename + basename = name.rsplit('/', 1)[-1] if '/' in name else name + # TorchScript archives contain data.pkl and constants.pkl + if basename == 'data.pkl' or basename == 'constants.pkl': + return True + # Older ExportedProgram format (PyTorch ≤2.6) + if basename.startswith('serialized_') and basename.endswith('.json'): + return True + # Newer PT2 archive format (PyTorch ≥2.7): contains an + # "archive_format" entry whose content is b"pt2". Reject + # entries with unexpected uncompressed size up-front and + # only read a small bounded prefix to avoid CPU/memory + # blow-ups on crafted archives. + if basename == 'archive_format': + if info.file_size > _ARCHIVE_FORMAT_MAX_BYTES: + continue + try: + with zf.open(info, 'r') as fh: + data = fh.read(_ARCHIVE_FORMAT_MAX_BYTES) + if data == b'pt2': + return True + except Exception: + pass + return False + except Exception: return False + +def get_pytorch_decoder_for_model_on_disk(argv, args): example_inputs = None if 'example_input' in args and args['example_input'] is not None: example_inputs = args['example_input'] @@ -195,9 +235,27 @@ def get_pytorch_decoder_for_model_on_disk(argv, args): if not isinstance(input_model, (str, pathlib.Path)): return False + # Quick structural check: only attempt PyTorch loading for ZIP archives + # that contain PyTorch-specific entries. This avoids importing torch, + # emitting misleading warnings, and running pickle-based deserialization + # on non-PyTorch files (e.g. ONNX, TF, TFLite). + if not _is_pytorch_zip(input_model): + return False + + try: + from openvino.frontend.pytorch.ts_decoder import TorchScriptPythonDecoder + from openvino.frontend.pytorch.fx_decoder import TorchFXPythonDecoder + import torch + except Exception: + # Auto-detection must not fail hard if the PyTorch frontend or torch + # itself cannot be imported (missing shared libs, init errors, etc.). + # Mirror the broad-except pattern used in convert_impl.py. + return False + + inputs = prepare_torch_inputs(example_inputs) + # attempt to load scripted model try: - inputs = prepare_torch_inputs(example_inputs) model = torch.jit.load(input_model) model.eval() decoder = TorchScriptPythonDecoder( @@ -208,17 +266,19 @@ def get_pytorch_decoder_for_model_on_disk(argv, args): argv.input_model = decoder argv.framework = 'pytorch' return True - except: + except Exception: pass + # attempt to load exported model try: exported_program = torch.export.load(input_model) - if hasattr(torch, "export") and isinstance(exported_program, (torch.export.ExportedProgram)): + if isinstance(exported_program, torch.export.ExportedProgram): argv.input_model = TorchFXPythonDecoder.from_exported_program(exported_program) argv.framework = 'pytorch' return True - except: + except Exception: pass + return False