Describe the bug
Reproduced with the stock openvino/model_server:2026.2-gpu and 2026.3-gpu images and the public OpenVINO/Qwen3.5-9B-int8-ov export — no vendor components involved. Under a sustained agent-style workload, the VLM legacy pipeline enters a permanent failure state on a discrete Intel GPU. One request triggers CL_OUT_OF_RESOURCES inside pipe->generate(); from that point on every subsequent request — including a 60-token one with no tools — fails immediately with
400 {"code":"ovms_400","message":"Mediapipe execution failed. MP status - INVALID_ARGUMENT: CalculatorGraph::Run() failed:
Calculator::Process() for node \"LLMExecutor\" failed: Request processing failed, check its correctness.","type":"upstream_error"}
The process stays alive, /v1/config still reports the servable AVAILABLE, and the llm_executor heartbeat ([legacy_executor.cpp:76] All requests: N;) goes silent. Only restarting the server recovers it.
Two things look wrong on the OVMS side, independent of whatever the driver is doing:
- A context-fatal GPU error is not treated as fatal. After
CL_OUT_OF_RESOURCES the OCL context/queue is unusable, but the servable is neither marked unavailable nor rebuilt, so it serves errors indefinitely while advertising itself as ready. A load balancer or orchestrator has no way to route around it.
- The error is reported as a client error.
INVALID_ARGUMENT / HTTP 400 with "check its correctness" points the user at their own request, while the actual cause is a server-side unrecoverable GPU failure. HTTP 503 (or at least a distinct code) would be correct.
To Reproduce
- Models repository — the public export, unmodified:
huggingface-cli download OpenVINO/Qwen3.5-9B-int8-ov --local-dir ./repro/ir
./repro/config.json:
{ "model_config_list": [],
"mediapipe_config_list": [ { "name": "qwen3.5-9b-public", "base_path": "/models/graph" } ] }
./repro/graph/graph.pbtxt (the parts that matter; LLMExecutor node wiring is the standard one):
node_options: {
[type.googleapis.com / mediapipe.LLMCalculatorOptions]: {
pipeline_type: VLM,
tool_parser: "qwen3coder",
reasoning_parser: "qwen3",
enable_tool_guided_generation: true,
models_path: "/models/ir",
plugin_config: '{"CACHE_DIR":"/opt/cache"}',
enable_prefix_caching: true,
cache_size: 2,
max_num_seqs: 256,
device: "GPU.1",
}
}
- OVMS launch command
docker run -d --name ovms-pub --device /dev/dri --network host \
-v $PWD/repro:/models -v $PWD/repro/cache:/opt/cache \
openvino/model_server:2026.2-gpu \
--config_path /models/config.json --rest_port 8123 \
--cache_dir /opt/cache --file_system_poll_wait_seconds 1 --log_level DEBUG
- Client — the attached four JSON bodies posted sequentially to
/v3/chat/completions, one connection at a time, no think time, in this cycle:
| step |
prompt tokens |
tools |
note |
tiny |
148 |
0 |
short "generate a conversation title" request |
big01 |
~12.0k |
46 |
tool-calling turn, one tool round trip in history |
big02 |
~12.6k |
47 |
two round trips |
big03 |
~14.3k |
47 |
three round trips, last tool result is a ~2.7 KB structured dump |
ENDPOINT=http://127.0.0.1:8123/v3/chat/completions ./wedge-loop.sh 150
All four bodies carry stream: true, stream_options.include_usage: true and chat_template_kwargs: {"enable_thinking": false}.
- See error — the loop stops at the first non-200.
With the public export on the stock image (the configuration above): wedged after 25 requests, on the tiny step.
With the same payloads against our own export of the same base model (9 runs total, fresh server process each): wedged after 13, 21, 21, 25, 29, 37, 57, 65 and 89 requests, median 29. Every one of the nine failed on the tiny step — the small tool-less request that immediately follows the large tool-calling turns.
After the failure, the server keeps reporting itself ready:
$ curl -s http://127.0.0.1:8123/v1/config
{"qwen3.5-9b-public":{"model_version_status":[{"version":"1","state":"AVAILABLE","status":{"error_code":"OK","error_message":"OK"}}]}}
$ curl -s -d '{"model":"qwen3.5-9b-public","max_tokens":8,"messages":[{"role":"user","content":"1+1?"}]}' \
-H 'Content-Type: application/json' http://127.0.0.1:8123/v3/chat/completions
{"error":"Mediapipe execution failed. MP status - INVALID_ARGUMENT: CalculatorGraph::Run() failed: \nCalculator::Process() for node \"LLMExecutor\" failed: Request processing failed, check its correctness."}
Expected behavior
- A GPU error that invalidates the inference context should mark the servable unavailable and rebuild the pipeline (or terminate the process so the orchestrator can restart it) instead of serving
INVALID_ARGUMENT forever while reporting AVAILABLE.
- The health endpoints should reflect it. In the wedged state we measured, every discovery and health endpoint still answers 200 —
/v3/models, /v1/config ("state":"AVAILABLE", "error_code":"OK"), /v2/health/ready, /v2/health/live and /v2/models/<name>/ready — while an 8-token inference request returns 400. A readiness probe that returns 200 for a servable that cannot serve anything makes every orchestrator, load balancer and monitor blind to the failure; today the only way to detect it is to send a real inference request.
- The response for a server-side fatal error should not be a 4xx that blames the request.
- Ideally, the workload above should not provoke the GPU error in the first place.
Logs
Server, at the moment of the first failure (--log_level DEBUG):
[llm_executor][error][legacy_executor.cpp:46] VLM pipeline generation failed: Exception from src/inference/src/cpp/infer_request.cpp:224:
[GPU] clFinish, error code: -5 CL_OUT_OF_RESOURCES
[GPU] CL_OUT_OF_RESOURCES exception.
The CL_OUT_OF_RESOURCES error typically occurs in two cases:
...
Immediately after, the executor heartbeat stops:
[llm_executor][info][legacy_executor.cpp:76] All requests: 1
[llm_executor][info][legacy_executor.cpp:76] All requests: 0 <-- last line, then silence
Every later request (including {"messages":[{"role":"user","content":"1+1?"}],"max_tokens":8}) returns the 400 quoted above, with no further ERROR lines.
Kernel side, same instant:
xe 0000:05:00.0: [drm] Tile0: GT0: Engine reset: engine_class=ccs, logical_mask: 0x1, guc_id=2
xe 0000:05:00.0: [drm] Xe device coredump has been created
xe 0000:05:00.0: [drm] Check your /sys/class/drm/card1/device/devcoredump/data
Configuration
-
OVMS version: reproduced on both unmodified upstream images (heartbeat line legacy_executor.cpp:76 in both):
openvino/model_server:2026.2-gpu — wedged after 25 requests
openvino/model_server:2026.3-gpu (sha256:2a52cd2b…, OpenVINO backend 2026.3.0-22451-8a17657b995-releases/2026/3) — 11 runs, 11 wedges, after 5–13 requests (9,9,9,5,9,5,13,9,9,9,13), same model, same graph, same payloads, identical prompt-token counts
The exception text is identical apart from the OCL source line (ocl_stream.cpp:395 on 2026.3). On 2026.3 the failure is close to deterministic — the three observed values are exactly the boundary request of cycles 2, 3 and 4 — which makes it the configuration we suggest starting from. We are not claiming 2026.3 is worse than 2026.2: we only have one 2026.2 run with the public export, and the two were not interleaved.
-
config.json: single mediapipe_config_list entry pointing at the graph above.
-
Accelerator and driver stack. The workload runs on GPU.1; an integrated GPU is present as GPU.0 and stays idle.
Host:
|
|
| GPU |
Intel discrete, PCI 8086:E223 rev 0 (subsystem 8086:1701), Battlemage class, 32.5 GB, arch=v20.2.0, 256 EUs |
| Kernel |
Linux 6.6.129 x86_64 |
| DRM driver |
xe (not i915) |
| GuC firmware |
xe/bmg_guc_70.bin, release version 70.58.0 |
| HuC firmware |
not loaded (status: N/A) — this workload does not use it |
Inside the OVMS container (the image ships its own compute runtime):
| package |
version |
intel-opencl-icd |
26.18.38308.1-0 |
libze-intel-gpu1 |
26.18.38308.1-0 (libze_intel_gpu.so.1.15.38308) |
intel-ocloc |
26.18.38308.1-0 |
intel-igc-core-2 / intel-igc-opencl-2 |
2.34.4 |
libze1 (Level Zero loader) |
1.27.0-124.04ppa2 |
-
Model repository: /models/<model>/{ir/,g_gpu.1/graph.pbtxt} plus config.json.
-
Model: OpenVINO/Qwen3.5-9B-int8-ov (public, Apache-2.0), downloaded unmodified from the Hugging Face hub. The failure was first seen with our own export of the same base model and then reproduced with this public one, so nothing vendor-specific is required to hit it.
Additional context
What we ruled out (each experiment is a full run of the cycle above with one variable changed; "no wedge" means the run completed without a single failure):
| change |
result |
cache_size 2 → 8 GB |
still wedges (after 101 and 245 requests) — not a capacity problem |
remove the tiny step, keep only the tool-calling turns |
180 requests, no wedge |
remove the tool-calling turns, alternate tiny with a 16.8k-token tool-less request |
240 requests, no wedge |
| give the boundary request 3 small tools (659 tokens) |
240 requests, no wedge |
| keep the boundary request tool-less but enlarge it to 1.9k tokens |
240 requests, no wedge |
| boundary request the same size but sharing the tool turns' prefix |
240 requests, no wedge |
So the trigger needs both halves: large tool-schema-heavy prefills alternating with a very small tool-less prefill. Enlarging the KV cache does not remove it.
One more observation that may narrow it down: we built a second payload set with the same token counts, same tool counts and same request structure, but whose text is prose instead of machine data (the real payloads are API schemas plus structured result dumps — short indented lines, opaque ids, timestamps; roughly 10 % punctuation and 13 % digits in the tool results). The prose variant ran 1,440 requests with no wedge; the data-shaped variant wedges in tens of requests. Pushing punctuation density well above the original (18–23 %) also stopped reproducing (600 requests each, twice). That suggests the trigger depends on the shape of the prefill, not only on its length or on the number of tools.
Timing note for anyone reproducing: on our host the public export is slow and erratic per request on both images (big requests 4–40 s, versus ~3–4 s for our own export of the same base model), with a cold plugin cache. This changes how long a run takes but not the outcome.
One more thing you may hit while reproducing: with these payloads (no max_tokens, tool-guided generation on, 46–47 tools) the model occasionally enters a degenerate loop, streaming function_calls tokens for tens of minutes. That is not the wedge — the heartbeat still shows All requests: 1, there is no exception and no engine reset. Our driver caps each request at 300 s to keep the loop moving.
Impact: a single occurrence takes the servable out of service permanently while it continues to advertise itself as ready; recovery requires a restart plus model reload (~3 minutes for us). In production we observed six occurrences, each after 12–129 requests.
Notes: the wedge itself may well be a driver/firmware issue (the engine reset points that way), and we are raising that separately. What we would like from OVMS regardless is the handling: detect CL_OUT_OF_RESOURCES as context-fatal, stop reporting the servable as AVAILABLE, and return a 5xx rather than a 400 that blames the caller.
mock-tiny.json
mock-big01.json
mock-big02.json
mock-big03.json
wedge-loop.sh
Describe the bug
Reproduced with the stock
openvino/model_server:2026.2-gpuand2026.3-gpuimages and the publicOpenVINO/Qwen3.5-9B-int8-ovexport — no vendor components involved. Under a sustained agent-style workload, the VLM legacy pipeline enters a permanent failure state on a discrete Intel GPU. One request triggersCL_OUT_OF_RESOURCESinsidepipe->generate(); from that point on every subsequent request — including a 60-token one with no tools — fails immediately withThe process stays alive,
/v1/configstill reports the servableAVAILABLE, and thellm_executorheartbeat ([legacy_executor.cpp:76] All requests: N;) goes silent. Only restarting the server recovers it.Two things look wrong on the OVMS side, independent of whatever the driver is doing:
CL_OUT_OF_RESOURCESthe OCL context/queue is unusable, but the servable is neither marked unavailable nor rebuilt, so it serves errors indefinitely while advertising itself as ready. A load balancer or orchestrator has no way to route around it.INVALID_ARGUMENT/ HTTP 400 with "check its correctness" points the user at their own request, while the actual cause is a server-side unrecoverable GPU failure. HTTP 503 (or at least a distinct code) would be correct.To Reproduce
./repro/config.json:{ "model_config_list": [], "mediapipe_config_list": [ { "name": "qwen3.5-9b-public", "base_path": "/models/graph" } ] }./repro/graph/graph.pbtxt(the parts that matter;LLMExecutornode wiring is the standard one):/v3/chat/completions, one connection at a time, no think time, in this cycle:tinybig01big02big03All four bodies carry
stream: true,stream_options.include_usage: trueandchat_template_kwargs: {"enable_thinking": false}.With the public export on the stock image (the configuration above): wedged after 25 requests, on the
tinystep.With the same payloads against our own export of the same base model (9 runs total, fresh server process each): wedged after 13, 21, 21, 25, 29, 37, 57, 65 and 89 requests, median 29. Every one of the nine failed on the
tinystep — the small tool-less request that immediately follows the large tool-calling turns.After the failure, the server keeps reporting itself ready:
Expected behavior
INVALID_ARGUMENTforever while reportingAVAILABLE./v3/models,/v1/config("state":"AVAILABLE","error_code":"OK"),/v2/health/ready,/v2/health/liveand/v2/models/<name>/ready— while an 8-token inference request returns 400. A readiness probe that returns 200 for a servable that cannot serve anything makes every orchestrator, load balancer and monitor blind to the failure; today the only way to detect it is to send a real inference request.Logs
Server, at the moment of the first failure (
--log_level DEBUG):Immediately after, the executor heartbeat stops:
Every later request (including
{"messages":[{"role":"user","content":"1+1?"}],"max_tokens":8}) returns the 400 quoted above, with no further ERROR lines.Kernel side, same instant:
Configuration
OVMS version: reproduced on both unmodified upstream images (heartbeat line
legacy_executor.cpp:76in both):openvino/model_server:2026.2-gpu— wedged after 25 requestsopenvino/model_server:2026.3-gpu(sha256:2a52cd2b…,OpenVINO backend 2026.3.0-22451-8a17657b995-releases/2026/3) — 11 runs, 11 wedges, after 5–13 requests (9,9,9,5,9,5,13,9,9,9,13), same model, same graph, same payloads, identical prompt-token countsThe exception text is identical apart from the OCL source line (
ocl_stream.cpp:395on 2026.3). On 2026.3 the failure is close to deterministic — the three observed values are exactly the boundary request of cycles 2, 3 and 4 — which makes it the configuration we suggest starting from. We are not claiming 2026.3 is worse than 2026.2: we only have one 2026.2 run with the public export, and the two were not interleaved.config.json: singlemediapipe_config_listentry pointing at the graph above.Accelerator and driver stack. The workload runs on
GPU.1; an integrated GPU is present asGPU.0and stays idle.Host:
8086:E223rev 0 (subsystem8086:1701), Battlemage class, 32.5 GB,arch=v20.2.0, 256 EUsxe(noti915)xe/bmg_guc_70.bin, release version 70.58.0status: N/A) — this workload does not use itInside the OVMS container (the image ships its own compute runtime):
intel-opencl-icdlibze-intel-gpu1libze_intel_gpu.so.1.15.38308)intel-oclocintel-igc-core-2/intel-igc-opencl-2libze1(Level Zero loader)24.04ppa2Model repository:
/models/<model>/{ir/,g_gpu.1/graph.pbtxt}plusconfig.json.Model:
OpenVINO/Qwen3.5-9B-int8-ov(public, Apache-2.0), downloaded unmodified from the Hugging Face hub. The failure was first seen with our own export of the same base model and then reproduced with this public one, so nothing vendor-specific is required to hit it.Additional context
What we ruled out (each experiment is a full run of the cycle above with one variable changed; "no wedge" means the run completed without a single failure):
cache_size2 → 8 GBtinystep, keep only the tool-calling turnstinywith a 16.8k-token tool-less requestSo the trigger needs both halves: large tool-schema-heavy prefills alternating with a very small tool-less prefill. Enlarging the KV cache does not remove it.
One more observation that may narrow it down: we built a second payload set with the same token counts, same tool counts and same request structure, but whose text is prose instead of machine data (the real payloads are API schemas plus structured result dumps — short indented lines, opaque ids, timestamps; roughly 10 % punctuation and 13 % digits in the tool results). The prose variant ran 1,440 requests with no wedge; the data-shaped variant wedges in tens of requests. Pushing punctuation density well above the original (18–23 %) also stopped reproducing (600 requests each, twice). That suggests the trigger depends on the shape of the prefill, not only on its length or on the number of tools.
Timing note for anyone reproducing: on our host the public export is slow and erratic per request on both images (big requests 4–40 s, versus ~3–4 s for our own export of the same base model), with a cold plugin cache. This changes how long a run takes but not the outcome.
One more thing you may hit while reproducing: with these payloads (no
max_tokens, tool-guided generation on, 46–47 tools) the model occasionally enters a degenerate loop, streamingfunction_callstokens for tens of minutes. That is not the wedge — the heartbeat still showsAll requests: 1, there is no exception and no engine reset. Our driver caps each request at 300 s to keep the loop moving.Impact: a single occurrence takes the servable out of service permanently while it continues to advertise itself as ready; recovery requires a restart plus model reload (~3 minutes for us). In production we observed six occurrences, each after 12–129 requests.
Notes: the wedge itself may well be a driver/firmware issue (the engine reset points that way), and we are raising that separately. What we would like from OVMS regardless is the handling: detect
CL_OUT_OF_RESOURCESas context-fatal, stop reporting the servable asAVAILABLE, and return a 5xx rather than a 400 that blames the caller.mock-tiny.json
mock-big01.json
mock-big02.json
mock-big03.json
wedge-loop.sh