Ticket: openvinotoolkit/omega#66
Reported symptom: INT8 weight-compressed inference of Qwen/Qwen3.6-27B (base model for
prism-ml/Bonsai-27B-gguf) on GPU.1 (Intel Arc Pro B60, discrete, Xe2/"Battlemage") is
~75x slower (~3.35 s/token) than INT4 on the same device (~44.75 ms/token), despite
both being functionally correct (WWB similarity 0.962 for INT8, 0.911 for INT4, both ≥ 0.9
threshold). CPU and GPU.0 (integrated Xe-LPG) show no such anomaly.
Bottom line up front: the dominant root cause is GPU memory (VRAM) capacity
exhaustion, not a computational/kernel inefficiency. The INT8 IR for this model is ~26 GB
of weights; GPU.1 has ~24.4 GB of physical VRAM (confirmed via clinfo:
CL_DEVICE_GLOBAL_MEM_SIZE = 24,385,683,456 bytes ≈ 22.7 GiB). The model does not fit in
device memory at INT8, forcing the GPU driver into a memory‑oversubscription/eviction regime
that is functionally transparent (hence "correct" output) but catastrophically slow (hence the
~75x latency cliff). INT4 (~15 GB) fits comfortably with ~9 GB of headroom for KV-cache and
activations, and is fast. This is corroborated by:
- The ticket's own data (FP16 = 51 GB fails outright on GPU.1 with
CL_OUT_OF_RESOURCES; INT8 = 26 GB "succeeds" but is anomalously slow; INT4 = 15 GB is fast). - An independent, controlled reproduction built in this investigation (below), which
triggers the identical
CL_OUT_OF_RESOURCESerror once a purely-synthetic model's total weight footprint crosses the same ~24 GB physical ceiling. - OpenVINO GPU-plugin source code that explicitly documents this exact failure mode
(
src/plugins/intel_gpu/src/runtime/engine.cpp), including a code comment that performance "might drop due to memory swap" when device memory is exceeded.
A secondary, smaller, independently-confirmed effect also exists and is documented below: a real, reproducible, Xe2-specific FC-kernel dequantization/activation-dynamic-quantization slowdown of ~1.7–2.7x for INT8 vs INT4 that scales with hidden dimension. This is real and worth addressing, but it is roughly two orders of magnitude too small to explain a 75x gap on its own — it is a compounding factor, not the primary cause.
Prototype delivered: beyond the report, a narrowly-scoped, safe diagnostic fix has been
implemented, built, unit-tested, and validated end-to-end on real hardware (§7), including
through a from-source build of OpenVINO GenAI running real generation on the real proxy
model (§7.4) — a proactive warning that fires when a model's weights approach or exceed a
discrete GPU's physical VRAM, replacing today's silence (or cryptic CL_OUT_OF_RESOURCES)
with an actionable message pointing at the real cause.
- Model:
Qwen/Qwen3.6-27B(qwen3_5architecture family), the disclosed base model behindprism-ml/Bonsai-27B-gguf. Dense (not MoE), hybrid linear/full attention:hidden_size=5120,intermediate_size=17408,num_hidden_layers=64,full_attention_interval=4(3 GatedDeltaNet/linear-attention layers per 1 full-attention layer),linear_num_value_heads=48,linear_key_head_dim=128, plus a 1-layer MTP head. Confirmed via the model's publicconfig.json. - Proxy model used for hands-on reproduction:
Qwen/Qwen3.5-0.8B— same architecture family/op set (confirmed identical GatedDeltaNet kernel selection, same dynamic-quantization transformation logic), 24 layers, small enough to run natively in this environment. Exported viaoptimum-cli export openvinoto both INT8 (1.1 GB) and INT4 (866 MB) IRs. - Hardware: Intel Core Ultra 9 285K; GPU.0 = integrated Xe-LPG iGPU (arch v12.70.4,
shares system RAM, 188 GB); GPU.1 = discrete Intel Arc Pro B60 (Xe2/"Battlemage", arch
v20.1.0, PCI
8086:e211, driverxeon Linux 6.17, NEO userspace driver 26.9.37435) — 24,385,683,456 bytes (≈22.7 GiB, ≈24.4 GB) of dedicated VRAM, confirmed viaclinfo(CL_DEVICE_GLOBAL_MEM_SIZEandCL_DEVICE_MAX_MEM_ALLOC_SIZE, the latter being equal to the former — a single allocation may span the whole device memory, i.e. there is no separate "single-buffer" ceiling below the total VRAM). - Software: stock
openvino/openvino-genai2026.3.1 release wheels for end-to-end benchmarking; a from-source,-DENABLE_DEBUG_CAPS=ONcustom build (openvino 2026.5.0) used exclusively forOV_VERBOSEtransformation-decision tracing (not used for performance-sensitive timing, since debug-caps builds carry extra instrumentation overhead).
Because the real 27B model cannot be run end-to-end in this environment in reasonable time (51 GB FP16 baseline; export/compression/compile times of tens of minutes each; and the target failure only manifests on genuinely large models), the investigation combined:
- Source-code archaeology of the OpenVINO GPU plugin's dynamic-quantization and memory management code paths, including full git-blame/PR-history tracing of the relevant transformations.
- Real, small-scale end-to-end reproduction using the
Qwen/Qwen3.5-0.8Bproxy model (same architecture family) across CPU / GPU.0 / GPU.1, INT8 vs INT4, using the officialopenvino.genaillm_benchtool andVLMPipelinedirectly. - Runtime ground-truth via
OV_VERBOSE=4transformation-decision tracing on a custom debug-caps OpenVINO build, to directly observe which dynamic-quantization group size, kernel implementation, and code path each precision/model combination actually selects (rather than inferring it from source code alone). - Isolated, controlled micro-benchmarks of a single
FullyConnectedlayer atQwen3.6-27B's exact real dimensions (hidden=5120,intermediate=17408), sweeping size and precision configuration to isolate individual variables (weight bit-width, activation dynamic-quantization group size, symmetric-vs-asymmetric quantization). - A purpose-built synthetic "deep MLP stack" model (repeated
gate/up/downSwiGLU-style blocks at the model's real hidden/intermediate dimensions) sized so that its total compressed-weight footprint crosses the GPU's physical VRAM budget at INT8 while staying safely under it at INT4 — used to directly test the VRAM-capacity hypothesis without needing the real, full 27B model.
All benchmarks were run on an otherwise-idle system; results were cross-checked against
system load (uptime) to rule out contention artifacts (see §5.4).
| Precision | Weight file size (ticket data) | Fits in 24.4 GB VRAM? | Observed behavior (ticket) |
|---|---|---|---|
| FP16 | 51 GB | No (2.1x over) | Hard failure: CL_OUT_OF_RESOURCES |
| INT8 | 26 GB | No (1.6 GB over) | "Succeeds" but ~75x slower (~3.35 s/token) |
| INT4 (group-size 128) | 15 GB | Yes (~9 GB headroom) | Fast (~44.75 ms/token) |
This table alone — taken directly from the ticket's own Step-1/Step-3 progress comments — is the single strongest piece of evidence: the only precision that is both "large enough to be tight against the VRAM ceiling" and "anomalously slow" is INT8; the only one that hard-fails outright is the one that is far over budget (FP16); the only one that is fast is the one with comfortable headroom (INT4). The pattern lines up exactly with a capacity-driven effect, not a precision-specific kernel bug.
To confirm this is a genuine capacity effect and not a coincidence of the specific model, a
synthetic model was constructed: repeated SwiGLU-style MLP blocks (gate/up/down
MatMuls) at the exact real hidden_size=5120 / intermediate_size=17408 dimensions,
stacked to a chosen layer count, then weight-compressed with NNCF exactly as the real model is
(INT8_ASYM / INT4_ASYM, group-size 128) and compiled for GPU.1.
| Layers | Weight footprint (INT8) | Weight footprint (INT4) | INT8 result | INT4 result |
|---|---|---|---|---|
| 60 | 16.04 GB (fits) | 8.02 GB (fits) | 39.09 ms/inference | 19.85 ms/inference (ratio 1.97x — matches isolated-kernel baseline, §4) |
| 88 | 23.53 GB (fits, ~0.9 GB headroom) | 11.77 GB (fits) | 57.23 ms/inference (perfectly linear vs the 60-layer point) | not needed (already established linear) |
| 96 | 25.67 GB (exceeds 24.4 GB by ~1.3 GB) | 12.83 GB (fits) | Hard crash: onednn_verbose,...,error,ocl,errcode -5,CL_OUT_OF_RESOURCES,src/gpu/intel/ocl/kernel.cpp:240 |
not applicable (int8-only test) |
| 110 | 29.41 GB (exceeds by ~5 GB) | 14.71 GB (fits, ~9.7 GB headroom) | Hard crash, same CL_OUT_OF_RESOURCES |
36.30 ms/inference — perfectly linear with the 60-layer point (110/60 × 19.85 = 36.4 ms, measured 36.30 ms) |
Key observations:
- Below the VRAM ceiling, INT8 and INT4 both scale perfectly linearly with layer count (i.e., with total weight bytes moved) — there is no hidden slowdown building up gradually.
- The exact same
CL_OUT_OF_RESOURCESOpenCL error code that the ticket's own Step-3 validation observed for the (grossly oversized) FP16 case appears in this from-scratch, independently-built reproduction, at a model size within ~1 GB of the real INT8 model's documented 26 GB footprint. - INT4 never crashes at any tested size (up to 14.7 GB), and scales linearly throughout.
This investigation's synthetic model crashes outright once VRAM is exceeded, while the real
Qwen3.6-27B INT8 pipeline reportedly runs (just ~75x slower). Both are legitimate
manifestations of the same underlying resource-exhaustion phenomenon; the difference in
failure mode is explained by GPU-plugin source code:
// src/plugins/intel_gpu/src/runtime/engine.cpp — engine::check_allocatable()
auto used_mem = get_used_device_memory(allocation_type::usm_device)
+ get_used_device_memory(allocation_type::usm_host);
auto exceed_available_mem_size = (layout.bytes_count() + used_mem > get_max_memory_size());
...
#ifdef __unix__
// Prevent from being killed by Ooo Killer of Linux
OPENVINO_ASSERT(!exceed_available_mem_size, "[GPU] Exceeded max size of memory allocation: ...");
#else
if (exceed_available_mem_size) {
GPU_DEBUG_COUT << "[Warning] [GPU] Exceeded max size of memory allocation: ...";
GPU_DEBUG_COUT << "Please note that performance might drop due to memory swap." << std::endl;
}
#endif- The book-keeping budget checked here (
get_max_memory_size()) is actually host RAM + device VRAM for a discrete GPU (188 GB + 24.4 GB ≈ 212 GB on this system) — a coarse guard against truly impossible requests, not a precise VRAM check, so it does not explain the crash by itself at ~26 GB. - The crash observed in this investigation (
CL_OUT_OF_RESOURCESfromsrc/gpu/intel/ocl/kernel.cpp:240, i.e. at kernel-execution time, not at the book-keeping check above) comes from a lower level: the actual OpenCL/oneDNN runtime failing to satisfy a real device-memory (usm_device) request once physical VRAM is genuinely full. This is sensitive to exact allocation pattern/fragmentation (number and order of constants, whether the OS/driver has other consumers, etc.), which plausibly explains why the real pipeline's specific allocation pattern (a full GenAI stateful model with KV-cache, vs. this investigation's simpler stack of raw constants) can behave differently at a similar total size — sometimes it still fails allocation outright, sometimes the driver instead falls back to page migration / host-visible USM memory and keeps functioning (validated by the ticket's own comment: "Process is confirmed healthy… steady forward progress… ~100-200x slower than every other device/precision combination" — i.e. it is not hung, it is genuinely computing, just extremely slowly, exactly consistent with constant eviction/re-fetch of weights over PCIe rather than local VRAM bandwidth). - Crucially, on non-Linux systems the code above only ever prints a debug-only warning
(
GPU_DEBUG_COUT, which compiles to a complete no-op —if (0) ...— in any build without-DENABLE_DEBUG_CAPS=ON, i.e. every standard release build users actually run). This means in a real production/release build, there is currently no user-visible diagnostic at all for this condition on the "graceful but catastrophically slow" path — the user just observes an inexplicable, undocumented 75x slowdown. See the proposed fix in §6.
Qwen3.5-0.8BINT8 is only ~1.1 GB — nowhere near a 24.4 GB ceiling, so no VRAM pressure exists at that scale; end-to-end re-benchmarking (5 runs each, idle system) showed a stable ~1.05x INT8-vs-INT4 ratio, not the reported 75x. This is fully consistent with (not contradictory to) the VRAM hypothesis: the effect is scale-gated.- A single isolated
FullyConnectedlayer, even at the model's real 5120×17408 dimensions, is only ~89 MB of weights — orders of magnitude below any VRAM ceiling, so isolated micro-benchmarks (§4) never trigger this effect either. They correctly measure a different, smaller, always-present kernel-level cost.
Independently of the VRAM effect, a real, reproducible, Xe2-specific performance gap between
INT8 and INT4 FullyConnected execution was isolated and confirmed. This will still matter
once the VRAM issue is addressed (e.g. on a larger-memory GPU, or for smaller models of this
family), so it is documented and should be tracked, but it is not the explanation for the
75x figure.
Two earlier hypotheses were tested and falsified using OV_VERBOSE=4 ground-truth tracing
on both the real INT8 and INT4 Qwen3.5-0.8B IRs:
DynamicQuantizeFullyConnectedGS128 hybrid-attention workaround (PR #35961, "[GPU] Robust INT8 dynamic quantization handling for hybrid linear-attn (GatedDeltaNet) models") is not a differential cause: verbose logs show both INT8 and INT4 in the real hybrid model get forced to identicalgroup_sizes: 1,1,128(97DynamicQuantizeops each). This WA exists because "hybrid linear-attention blocks... produce SSM-gated outputs with a wide dynamic range... per-token INT8 dynamic activation quantization... can cause severe output collapse" (from the PR description) — it is an accuracy fix, empirically validated on Qwen3.5/3.6 dense and MoE models, and it applies uniformly to both precisions in a hybrid model, so it cannot explain an INT8-vs-INT4 differential.- GatedDeltaNet kernel selection is identical for both precisions: verbose logs confirm
the exact same
ocl::gated_delta_net::ref_kernel and kernel hash for INT8 and INT4 (18 ops, matchingnum_hidden_layers − num_full_attention_layers). Note: this kernel has no optimized implementation at all — only a reference implementation exists — a legitimate, separate performance-improvement opportunity (it accounted for ~29% of decode step time in PERF_COUNT profiling) but again equally for both precisions.
A related, separate, and important discovery: use_gs128_for_int8_per_token
(transformations_pipeline.cpp) forces group_size=128 for any INT8-weight
FullyConnected on any Xe2+ device, unconditionally — not gated on whether the model is a
hybrid-attention model. This predates the hybrid-attention WA (PR #32493, "enable gs128 by
default for int8 models in xe2+ platforms... It is not enabled for other cases because of
performance concern"). Because it is unconditional, it means every INT8 model — not just
hybrid-attention ones — pays the GS128 grouping cost on Xe2, whereas INT4 only pays it in
hybrid-attention models. This was investigated as a possible explanation for the FC-level
delta below (§4.2) but was ruled out as the primary driver of the scaling effect (§4.3):
forcing INT4 to the same group_size=128 explicitly (DYNAMIC_QUANTIZATION_GROUP_SIZE=128)
reproduces virtually the same ratio curve as leaving INT4 at its default per-token grouping,
proving the grouping granularity itself is nearly free — the actual cost is elsewhere (weight
bit-width/dequantization), as detailed next.
Because the real model's INT4 weights are actually INT4_ASYM (confirmed via direct IR
inspection: .../weight tensors are uint4_t and 100% of them have a paired
/zero_point constant, also uint4_t) — not INT4_SYM as an earlier draft of this
benchmark mistakenly used — the final, representative comparison below uses INT8_ASYM vs
INT4_ASYM, both explicitly forced to the same group_size=128 activation
dynamic-quantization granularity (matching what the real hybrid model does for both
precisions):
| hidden | intermediate | INT8 median (ms) | INT4 median (ms) | Ratio |
|---|---|---|---|---|
| 1024 | 3584 | 0.0935 | 0.0857 | 1.09x |
| 2048 | 7168 | 0.1017 | 0.0931 | 1.09x |
| 4096 | 14336 | 0.2234 | 0.1506 | 1.48x |
| 5120 | 17408 | 0.3131 | 0.1884 | 1.66x ← real model's exact dims |
| 8192 | 28672 | 0.7510 | 0.3702 | 2.03x |
| 12288 | 32768 | 1.2450 | 0.5770 | 2.16x |
| 16384 | 43008 | 2.4612 | 0.9733 | 2.53x |
(GPU.1, PERF_COUNT node-level timing corroborates the same trend at the FC-node level,
peaking at ~2.7x.)
At the model's actual real dimensions (hidden=5120, intermediate=17408), the isolated FC
effect is only ~1.7x — not 75x. The ratio grows with size and appears to plateau around
2.5–2.7x at the largest tested dimension (16384); it does not explode.
- Since matching the activation-quantization group size does not remove the gap, and using
the representative asymmetric (zero-point) quantization for both precisions does not
meaningfully change it either (
INT4_SYMvsINT4_ASYMgave nearly identical ratios), the remaining, isolated cause is the weight bit-width itself: INT8 weight storage is 2x that of INT4, meaning 2x the bytes must be streamed from device memory per FC invocation. The observed ratio (up to ~2.5-2.7x, somewhat above the "naive" 2x memory-bandwidth expectation) suggests the FC/dequantization GEMM kernel on Xe2 is not perfectly bandwidth-proportional — plausibly extra per-element unpack/dequant instruction overhead, or GEMM tiling/occupancy that favors the smaller (INT4) weight footprint at large sizes (better cache/register reuse). This was not traced further into the OpenCL kernel source itself due to time constraints, and is flagged as a good target for focused kernel-level profiling (e.g. withcliloader/kernel-level GPU timing) as a follow-up. - This effect is real, Xe2-specific (gated by
device_info.supports_immad && use_onednn, i.e. does not apply to GPU.0 — confirmed separately: GPU.0 uses a completely differentfully_connected_gpu_bf_tiledkernel path with no oneDNN/dynamic-quantization at all, showing an expected ~1.55x ratio consistent with plain 2x-bandwidth minus reuse benefits), and worth incremental optimization — but it is two orders of magnitude short of explaining the reported 75x, and should not be conflated with the VRAM-capacity finding in §3.
For completeness/audit trail, the following were investigated and explicitly falsified before arriving at the VRAM-capacity conclusion:
- GS128 hybrid-attention accuracy WA (PR #35961) as a differential cause — falsified; applies identically to both precisions in a real hybrid model (§4.1).
- GatedDeltaNet kernel selection differing by precision — falsified; identical kernel/hash for both precisions (§4.1).
- Dynamic-quantization grouping granularity (GS128 vs per-token) as the FC-scaling cause — falsified; forcing matched grouping reproduces the same ratio curve (§4.1/4.2).
- Symmetric vs. asymmetric weight quantization as the FC-scaling cause — falsified;
INT4_SYMvs. representativeINT4_ASYMgive effectively the same ratios (§4.2). - PERF_COUNT-based op-level profiling as a methodology — found to be misleading: it
forces serialization that erased a previously-observed wall-clock gap and showed nearly
identical INT8/INT4 op-level breakdowns regardless of ground truth;
OV_VERBOSEtransformation-decision tracing was far more informative for root-causing than runtime op-counters. - CPU contention / thermal / driver kernel-cache cold-start artifacts — investigated at
length (synthetic
yes-loop CPU stress, real concurrentcc1plus/ninjacompile load, deliberately cleared NEO GPU kernel cache) as an explanation for an earlier, non-reproducible 0.8B-scale measurement; none reproduced any anomaly. This confirms environmental noise was not a factor in the (separately explained, VRAM-driven) real 27B anomaly, and that the originally-recorded 0.8B-scale "3.84x" data point was most likely a one-off measurement artifact unrelated to the true (VRAM-driven) 27B phenomenon — the two are unrelated findings from different scales. - ATTENTION_BACKEND (PagedAttention vs SDPA) choice — affects absolute latency substantially (5 ms vs 103 ms/token in one comparison) but affects INT8 and INT4 equally; not a differentiator.
On a ~24 GB-class GPU, INT4 (or lower) is the only practical precision for a ~27B dense model. INT8 is expected to be unusable-at-acceptable-speed by design, not by bug, on this class of hardware — this should be explicitly documented for the Bonsai-27B / Qwen3.6-27B enablement (e.g., in the model card / notebook produced by this enablement pipeline), so users do not attempt INT8-on-24GB-GPU expecting good performance. Deployment on a GPU with ≥32 GB VRAM (headroom for KV-cache/activations beyond the ~26 GB weights) would be expected to remove the effect entirely — this is a hardware-sizing recommendation, not a code fix.
The core problem today is silence: neither Linux (hard, somewhat generic assert) nor
Windows (release-mode no-op due to GPU_DEBUG_COUT) surfaces a clear, actionable message in a
normal release build explaining why a model is about to be extremely slow or fail.
This has been implemented and validated as a prototype (not just proposed) — see §7 for the
full implementation, test, and end-to-end validation results. In summary, it adds an
always-on (not gated behind ENABLE_DEBUG_CAPS), best-effort diagnostic at compile time
that:
- Sums the total constant/weight byte count for the compiled network (a lightweight, private
pre-pass over
datanodes inprogram::processing_order, deliberately independent ofget_estimated_device_mem_usage(), which is wired only into the unrelated auto-batch-sizing feature and not invoked on the normal single-request LATENCY-hint compile path). - Compares it against the device's actual physical memory
(
device_info.max_global_mem_size, not the inflated host+deviceget_max_memory_size()figure used for the Linux OOM-killer guard). - If the total is within a configurable margin of (default 90%), or exceeds, that figure, logs
a clear, user-visible warning via
std::cerr, e.g.:[GPU] [WARNING] Model weights (~23.92 GiB) are close to or exceed device 'Intel(R) Arc(TM) Pro B60 Graphics' physical memory capacity (~22.71 GiB). This can force the driver into a memory-oversubscription fallback that remains functionally correct but may be dramatically slower than expected (up to ~75x observed in practice for a similarly-sized model). Consider a smaller or more compressed model (e.g. INT4 instead of INT8), or a device with more memory. Background: openvinotoolkit/omega#66.
This is purely additive/diagnostic (no numerical, allocation, or kernel behavior changes),
touches no other model's runtime performance, and directly turns a mysterious 75x slowdown (or
a cryptic CL_OUT_OF_RESOURCES/could not execute a primitive exception) into an explained,
actionable one. It is implemented directly against src/plugins/intel_gpu/ (see §7 for exact
files/diff), placed alongside the existing get_estimated_device_mem_usage() machinery in
.../graph/program.cpp and .../include/intel_gpu/graph/program.hpp, and covered by a new
unit test suite. It is offered as a starting point for a properly-scoped, reviewed upstream PR
— the GPU plugin team should still weigh in on the exact logging channel/verbosity conventions
and default margin before merging, but the detection logic itself is implemented, tested, and
validated end-to-end (§7).
- Weight streaming / layer-wise paging for models whose weights exceed device memory (keep only N layers resident, prefetch the next while computing the current one) — a substantial architecture investment, common in other inference stacks for oversized models, but well beyond a "narrow, safe fix."
- Kernel-level investigation of the ~1.7–2.7x Xe2 FC dequantization scaling (§4) via
cliloader/kernel-timing tools, to see whether the INT8 dequant/GEMM kernel can be brought closer to the theoretical ~2x bandwidth ratio at large sizes. - An optimized (non-reference) GatedDeltaNet kernel — currently only
ref_exists, consuming ~29% of decode time for both precisions; a fused/optimized implementation would benefit this whole model family regardless of the VRAM finding.
Per the task's explicit framing, a working code fix is optional/best-effort and secondary to this report. Given the primary root cause (§3) is a genuine hardware-capacity constraint without a safe, narrowly-scoped performance fix (the model must either be smaller, more compressed, or the device must have more memory — none of which are code changes to propose safely within this task's scope), the actionable prototype pursued is the proactive VRAM-pressure diagnostic warning from §6.2. Unlike a performance fix, this is safe, narrowly scoped, beneficial to any model/user hitting this class of problem, and fully testable — and it has been implemented, built, and validated end-to-end.
Three files changed/added directly against the openvino source tree (full unified diff saved
at repro/vram_pressure_diagnostic.patch):
| File | Change |
|---|---|
src/plugins/intel_gpu/include/intel_gpu/graph/program.hpp |
Declares two new program methods: static bool is_device_memory_pressure_risk(uint64_t total_weights_bytes, uint64_t device_max_global_mem_bytes, double margin_ratio = 0.9) (pure, side-effect-free, unit-testable without a device) and void check_device_memory_pressure() (computes real weight bytes + logs). |
src/plugins/intel_gpu/src/graph/program.cpp |
Implements both methods. check_device_memory_pressure() early-returns for integrated GPUs (dev_type != discrete_gpu — no separate VRAM tier to exceed), otherwise sums bytes_count() over all non-shape-infer data (constant) nodes in processing_order and compares against device_info.max_global_mem_size. Wired into program::build_program() immediately after the existing transfer_memory_to_device() call (i.e. only for real, non-internal program builds — internal/sub-graph builds are unaffected, matching the existing gating pattern for weight transfer). |
src/plugins/intel_gpu/tests/unit/test_cases/test_device_memory_pressure_warning.cpp (new) |
9 unit tests: 8 pure-logic tests of is_device_memory_pressure_risk() covering fits-comfortably, exceeds-capacity, default/custom margin behavior, and zero-input edge cases (no GPU device needed); 1 integration smoke test that builds a real (tiny) program end-to-end via program::build_program() on the actual test device and asserts it does not throw. |
The change is ~63 lines of new production code + ~112 lines of new test code; zero existing lines modified (only two call-sites/declarations added). No other function, model, or code path is touched.
- Full incremental rebuild of the affected static library succeeded cleanly:
ninja openvino_intel_gpu_graph(797 targets, no new warnings/errors introduced). - New unit tests: all 9/9 pass, including the real-device smoke test:
[==========] 9 tests from test_device_memory_pressure_warning ran. (160 ms total) [ PASSED ] 9 tests. - Regression check, pre-existing related test:
test_device_mem_usage_estimation.*(3/3 pass, unchanged behavior). - Broader regression sample (chosen for direct relevance to
build_program()and to the FC/weight-compression code paths central to this investigation):network_test.*,basic.*,basic_memory_dependencies.*,swiglu_gpu_test.*(15/15 pass) and the fullfully_connected_gpu_testssuite (137/137 pass, grep-confirmed zero spurious warnings emitted for any of these — none of their models approach the discrete GPU's capacity threshold).
The Python wheel was rebuilt from the same modified source tree (ninja pyopenvino ie_wheel)
and reinstalled into venv_debug, then exercised on the real GPU.1 (Arc Pro B60, discrete)
in three scenarios:
| Scenario | Weights vs. capacity | Result |
|---|---|---|
Real 0.8B proxy model (models/qwen3.5-0.8b-int8, ~1.1 GB) |
~4.8% of 22.71 GiB | Compiles and runs correctly in 1.3s. No warning printed — confirms zero false positives for models nowhere near the risk zone. |
| Synthetic 88-layer stack, INT8 (23.53 GB ≈ 21.92 GiB) | ~96.5% of capacity | Warning correctly fires: "Model weights (~21.9239 GiB) are close to or exceed device ... physical memory capacity (~22.7109 GiB)." Inference still completes and runs with stable, linear-scaling latency (57ms/iter) — matching §3.2's original finding that this configuration is "near-limit but still functional." The diagnostic proactively flags real risk before any failure occurs. |
| Synthetic 96-layer stack, INT8 (25.67 GB ≈ 23.92 GiB) | ~105% of capacity (genuinely exceeds) | Warning correctly fires during compilation: "Model weights (~23.917 GiB) are close to or exceed device ... physical memory capacity (~22.7109 GiB)." Immediately afterward, the same underlying CL_OUT_OF_RESOURCES (src/gpu/intel/ocl/kernel.cpp:240) failure from §3.2 still occurs at req.infer() — but now the user sees the actionable, root-caused warning before the cryptic low-level driver exception, rather than only the raw RuntimeError: ... could not execute a primitive with no explanation. |
This confirms the diagnostic behaves correctly in all three directions: silent when safe, a proactive warning when near the edge (including cases that end up working, consistent with the conservative 90% margin being an early-warning threshold rather than a strict failure predictor), and a proactive warning immediately ahead of an actual hard failure — directly addressing the "no user-visible warning exists today" gap identified in §3 and §6.2.
Per the task's request to validate beyond the same performance-analysis tooling used for the
initial evaluation, openvino.genai (already forked/cloned as mlukasze/openvino.genai) was
built from source against this modified OpenVINO core, to prove the diagnostic is exercised
correctly by a real, production-shaped inference pipeline rather than only by raw
ov::Core/Python-API calls or unit tests:
-
Configured with CMake pointed directly at the modified core's build tree (
-DOpenVINO_DIR=.../openvino/build), initialized the missingopenvino_tokenizerssubmodule, built with Ninja (ninja py_openvino_genai, 335/335 targets, succeeded cleanly). -
Loaded the resulting
py_openvino_genaimodule (built against, and linked to,libopenvino.sofrom this session's modified core) viaPYTHONPATH/LD_LIBRARY_PATH, alongside the matching rebuiltopenvinoPython wheel. -
Ran
ov_genai.VLMPipeline(...)(the correct pipeline class for this VLM-family proxy model, which exports separate text/vision embedding sub-models alongside the language model) on GPU.1 for both the INT8 and INT4 exports of the 0.8B proxy model, generating real text:Model Prompt Generated output qwen3.5-0.8b-int8"The capital of France is" "The capital of France is Paris. It is the largest city in France and the seat of" qwen3.5-0.8b-int4"The capital of France is" "The capital of France is Paris. It is also the largest city in Europe." Both are factually correct, coherent completions — confirming the modified OpenVINO core produces no functional/correctness regression through the full GenAI pipeline (tokenizer → embeddings → language model → detokenizer, all compiled through the same
build_program()path that now includes the new diagnostic check). Neither run printed a spurious warning (both models are ~4.8%/~2.9% of device capacity), confirming the diagnostic remains silent and non-intrusive for real, well-provisioned GenAI workloads, not just synthetic ones.
OVMS was scoped out of hands-on validation (a deliberate, reasoned decision, not an
omission): OVMS is fundamentally a serving wrapper around the same ov::Core::compile_model()
→ program::build_program() call chain already exercised and validated at three independent
levels above (C++ unit test, raw Python API, and now full GenAI VLMPipeline). Building it
from source would mean standing up its separate Bazel-based build system for a change that (a)
touches no public API surface OVMS or any consumer depends on, and (b) would exercise the exact
same internal code path already proven correct — a disproportionate time cost for near-zero
incremental risk coverage given the change's narrow scope. If desired, a follow-up could still
run the model through an OVMS Docker image built against a packaged wheel from
repro/vram_pressure_diagnostic.patch applied to a stock OpenVINO checkout.
This prototype is offered as a starting point for a properly-scoped, reviewed PR, not a final
upstream-ready patch. Before merging into openvinotoolkit/openvino, the GPU plugin team
should weigh in on:
- Preferred logging channel/verbosity conventions (currently a direct
std::cerrwrite, clearly marked in code comments as a prototype choice — a real PR should likely route through whatever the plugin's standard user-facing warning mechanism is, if one exists, or establish one). - Whether the default 90% margin is the right balance between early warning and false-positive risk across the full range of supported discrete GPUs and workloads (this investigation only validated it against one device, Arc Pro B60/24GB-class).
- Whether a one-time-per-process warning (vs. current unconditional per-
build_program()-call) is preferred for pipelines that compile many sub-networks.
No changes to optimum-intel were required or made, consistent with the task's constraints.
repro/fc_bench.py,repro/fc_bench_scaling.py,repro/fc_bench_matched_grouping.py,repro/fc_bench_matched_grouping_asym.py— isolated FC-kernel scaling benchmarks (§4).repro/vram_oversubscription_test.py— synthetic deep-MLP-stack VRAM-capacity reproduction (§3.2), also reused for prototype validation (§7.3).repro/vram_pressure_diagnostic.patch— full unified diff of the implemented and validated proactive VRAM-pressure diagnostic prototype (§6.2, §7).repro/prototype_validation/— validation logs and scripts for the prototype: real-model no-warning compile log, 88-/96-layer synthetic VRAM-pressure warning logs (§7.3), and the full GenAIVLMPipelineend-to-end script + logs for both INT8/INT4 (§7.4).repro/llm_bench.py,repro/llm_bench_backend.py— real end-to-endVLMPipelinebenchmarks on the 0.8B proxy model (§3.4, §5)./tmp/int8_verbose4.log,/tmp/int4_verbose4.log—OV_VERBOSE=4ground-truth transformation-decision traces for the real proxy model (§4.1).models/qwen3.5-0.8b-int8/,models/qwen3.5-0.8b-int4/— exported, verified-correct OpenVINO IR for the proxy model.
- The real
Qwen/Qwen3.6-27Bmodel was not run end-to-end in this environment (infeasible in the available time/disk/compute budget); all 27B-scale conclusions are derived from (a) the ticket's own previously-collected data on the real model, and (b) an independent synthetic reproduction at the model's exact real dimensions and representative quantization modes, cross-validated against GPU-plugin source code. This is a strong but indirect evidence chain; direct confirmation on the real model (e.g. instrumenting an actual run with GPU memory-usage telemetry, if tooling becomes available) would further strengthen it. - No GPU frequency/VRAM-usage telemetry tool (
intel_gpu_top,xpu-smi) was available/ installable in this environment (no root access), so live VRAM occupancy during the failing synthetic run could not be directly observed; the conclusion rests on file-size arithmetic, device capability queries (clinfo), and the matchingCL_OUT_OF_RESOURCESerror signature rather than a live memory-usage graph. - The exact micro-mechanism that lets the real 27B pipeline degrade gracefully (slow) rather than hard-fail (as this investigation's simpler synthetic model does) was not traced to a specific line of driver or runtime code; §3.3 gives a well-evidenced but not 100%-pinpointed explanation for this difference in failure mode (both are the same root cause — VRAM exhaustion — manifesting differently based on allocation pattern specifics).