feat: add Granite 4.2 model defaults - #1587
Conversation
c798894 to
2eabb25
Compare
Quality-matrix runs on PR generative-computing#1587 hit two Ollama stall failure shapes the existing retry net could not handle: - test_litellm_token_metrics_integration[non-streaming] died to the 900 s pytest-timeout watchdog: LiteLLMBackend passes no request deadline, so litellm falls back to a 600 s per-attempt timeout and the OpenAI SDK silently retries a stalled attempt (verified: 3 attempts on a stalled socket), exhausting the budget with no retryable error. - The flaky marker from generative-computing#1369 matched only the native httpx.ReadTimeout shape; the litellm path raises litellm.exceptions.Timeout whose APITimeoutError message never matched, so its retry was dead code. Changes: - Bound the litellm live test to the same 300 s the native OllamaModelBackend uses, with num_retries: 0 so one pytest attempt can never exceed the watchdog budget (verified end-to-end through LiteLLMBackend against a stalled socket: 1 request, 4.1 s at timeout=4). - Extend the conftest flaky marker to a shared OLLAMA_TIMEOUT_RERUN_PATTERNS constant covering both timeout shapes, and pin the match behaviour in test/test_flaky_ollama_rerun.py (including that the watchdog kill itself is NOT retried). - Capture the Ollama server log in the quality workflow and dump it on failure: server-side stalls were previously undiagnosable from the job log (zero server lines across a 90-minute test window). Local verification: new regression tests + litellm, native-Ollama metrics and tracing live tests pass against Ollama 0.33.0 / granite4.2:3b; ruff, mypy clean. Signed-off-by: Nigel Jones <jonesn@uk.ibm.com>
|
Temporarily closing to re-trigger the quality matrix on c027873 (the synchronize event for that push was not dispatched during the GitHub Actions outage). Reopening now. |
|
Still persuing CI failures - above commits will be squashed, but wanted to push the changes to probe at and debug the CI failures which differ from local testing. |
|
Status update: the root cause of the prolonged Ollama CI stalls is identified and fixed — the telemetry tests were inadvertently sending an empty conversation to the model, and granite4.2 generates runaway output on it. The instrumented re-run now completes the full quality matrix in ~18 min (previously ~2 h) with the previously-stalling /v1 tests passing; one remaining flaky streaming chunk-timing assertion is being chased down and will be re-run. |
|
Update: the re-run is green — 33166578641 on |
Quality-matrix runs on PR generative-computing#1587 hit two Ollama stall failure shapes the existing retry net could not handle: - test_litellm_token_metrics_integration[non-streaming] died to the 900 s pytest-timeout watchdog: LiteLLMBackend passes no request deadline, so litellm falls back to a 600 s per-attempt timeout and the OpenAI SDK silently retries a stalled attempt (verified: 3 attempts on a stalled socket), exhausting the budget with no retryable error. - The flaky marker from generative-computing#1369 matched only the native httpx.ReadTimeout shape; the litellm path raises litellm.exceptions.Timeout whose APITimeoutError message never matched, so its retry was dead code. Changes: - Bound the litellm live test to the same 300 s the native OllamaModelBackend uses, with num_retries: 0 so one pytest attempt can never exceed the watchdog budget (verified end-to-end through LiteLLMBackend against a stalled socket: 1 request, 4.1 s at timeout=4). - Extend the conftest flaky marker to a shared OLLAMA_TIMEOUT_RERUN_PATTERNS constant covering both timeout shapes, and pin the match behaviour in test/test_flaky_ollama_rerun.py (including that the watchdog kill itself is NOT retried). - Capture the Ollama server log in the quality workflow and dump it on failure: server-side stalls were previously undiagnosable from the job log (zero server lines across a 90-minute test window). Local verification: new regression tests + litellm, native-Ollama metrics and tracing live tests pass against Ollama 0.33.0 / granite4.2:3b; ruff, mypy clean. Signed-off-by: Nigel Jones <jonesn@uk.ibm.com>
f675e0b to
1eebfb4
Compare
…4.2-* Add GRANITE42_MODEL-driven size selection for the HF and Ollama live tests (defaults to 3B; set it to test the 8B/30B sizes on LSF or locally), align every fixture and test-runner script to Granite 4.2, and bound the CI e2e context window. Assisted-by: Codex Signed-off-by: Nigel Jones <jonesn@uk.ibm.com>
Assisted-by: Codex Signed-off-by: Nigel Jones <jonesn@uk.ibm.com>
Assisted-by: Codex Signed-off-by: Nigel Jones <jonesn@uk.ibm.com>
Assisted-by: Codex Signed-off-by: Nigel Jones <jonesn@uk.ibm.com>
Assisted-by: Codex Signed-off-by: Nigel Jones <jonesn@uk.ibm.com>
Assisted-by: Codex Signed-off-by: Nigel Jones <jonesn@uk.ibm.com>
Assisted-by: Codex Signed-off-by: Nigel Jones <jonesn@uk.ibm.com>
Quality-matrix runs on PR generative-computing#1587 hit two Ollama stall failure shapes the existing retry net could not handle: - test_litellm_token_metrics_integration[non-streaming] died to the 900 s pytest-timeout watchdog: LiteLLMBackend passes no request deadline, so litellm falls back to a 600 s per-attempt timeout and the OpenAI SDK silently retries a stalled attempt (verified: 3 attempts on a stalled socket), exhausting the budget with no retryable error. - The flaky marker from generative-computing#1369 matched only the native httpx.ReadTimeout shape; the litellm path raises litellm.exceptions.Timeout whose APITimeoutError message never matched, so its retry was dead code. Changes: - Bound the litellm live test to the same 300 s the native OllamaModelBackend uses, with num_retries: 0 so one pytest attempt can never exceed the watchdog budget (verified end-to-end through LiteLLMBackend against a stalled socket: 1 request, 4.1 s at timeout=4). - Extend the conftest flaky marker to a shared OLLAMA_TIMEOUT_RERUN_PATTERNS constant covering both timeout shapes, and pin the match behaviour in test/test_flaky_ollama_rerun.py (including that the watchdog kill itself is NOT retried). - Capture the Ollama server log in the quality workflow and dump it on failure: server-side stalls were previously undiagnosable from the job log (zero server lines across a 90-minute test window). Local verification: new regression tests + litellm, native-Ollama metrics and tracing live tests pass against Ollama 0.33.0 / granite4.2:3b; ruff, mypy clean. Signed-off-by: Nigel Jones <jonesn@uk.ibm.com>
The first use of the failure-time log dump (run 32991484037) showed that a plain tail is useless for diagnosis: Ollama logs slot print_timing lines every ~25 tokens, so the last 100 lines covered ~30 s of a 1 h 43 m run and missed the 31-minute stall window entirely. Dump the request-level story instead (one GIN line per HTTP request with its duration, plus error/warning and model load/unload lines) capped at 500 lines, and keep a short raw tail for the final moment. Signed-off-by: Nigel Jones <jonesn@uk.ibm.com>
…l shape
Run 33006850028 (3.13 lane) failed on test_openai_token_metrics_integration
[streaming] via the 900 s watchdog, and its server log (now captured
per-request) shows why: the runner's Ollama degraded mid-run (one request
took 15 m 0 s) and the 'Say hello' prompt is not reliably obeyed by the
model -- generations of 1865-2295 tokens were observed, which at ~9 t/s
CPU decode is a 3-15 minute single request. The streaming tests have no
output bound and no total-time bound, so one such generation can consume
the entire pytest-timeout budget.
Changes:
- Cap the five live 'say hello' telemetry tests at 64 output tokens via
ModelOption.MAX_NEW_TOKENS (natively mapped to num_predict /
max_tokens / max_completion_tokens by the Ollama, OpenAI and LiteLLM
backends). 64 tokens is far more than a 'hello' answer needs and caps
the worst case at seconds even on a degraded runner.
- Extend OLLAMA_TIMEOUT_RERUN_PATTERNS with 'TimeoutError': when a stalled
stream goes quiet for the 120 s chunk guard, send_to_queue aborts it and
the builtin TimeoutError is raised verbatim at the consumer. That is the
streaming path's bounded form of the same stall; without it the test
rides the per-chunk guard to the 900 s watchdog. The watchdog kill
string ('Failed: Timeout (>900.0s) ...') does not match any pattern --
pinned by the regression tests, which now cover four shapes.
Local verification: 14 passed (all live ollama/openai/litellm telemetry
tests, streaming and non-streaming, plus the four regression tests)
against Ollama 0.33.0 / granite4.2:3b; ruff + mypy clean.
Signed-off-by: Nigel Jones <jonesn@uk.ibm.com>
Run 33015176815 (3.13 lane) isolated the last unbounded client path:
test_openai_token_metrics_integration[streaming] ran for the full 900 s
pytest watchdog even though the 64-token output cap landed. The server
log shows why: a /v1/chat/completions stream that stayed open for
exactly 15m0s (the request completed at the same second the watchdog
fired). The 120 s per-chunk stream guard only bounds inter-chunk gaps;
a stalled or queued stream that keeps the connection alive keeps the
guard re-armed and has no total-time backstop, so it rides to the
watchdog and kills the job. The non-streaming paths already have the
300 s httpx bound -- streaming did not.
Changes:
- Wrap the mot.astream() consumption in the four live streaming telemetry
tests with asyncio.wait_for(..., timeout=300.0), mirroring the
non-streaming bound. A genuine stall now surfaces as a bounded builtin
TimeoutError (empty message), which the OLLAMA_TIMEOUT_RERUN_PATTERNS
net already retries; a healthy stream finishes in seconds so the
wrapper is a no-op on a good runner.
- Pin the wait_for error shape ('TimeoutError: ', empty message, verified
empirically) in the flaky-rerun regression tests.
Local verification: 17 passed (all live ollama/openai/litellm/HF
streaming tests with the wrapper plus the five regression tests) against
Ollama 0.33.0 / granite4.2:3b; ruff + mypy clean.
Signed-off-by: Nigel Jones <jonesn@uk.ibm.com>
Run 33039206380 (rerun, 3.11 lane) exposed the last unbounded path: test_openai_token_metrics_integration[non-streaming] was watchdog-killed at 900 s. The GIN timeline shows why: the request stalled, the OpenAI SDK's 600 s read timeout fired at 10m0s (server logged the client disconnect as a 500), the SDK then auto-retried the 5xx with its default max_retries=2, and the second attempt was still in flight when the 900 s pytest watchdog killed the test. The SDK's retry chain multiplies the per-attempt bound past the per-attempt budget. Set max_retries=0 and timeout=300.0 on the test's OpenAIBackend client (passthrough to openai.OpenAI kwargs, verified on the live client), mirroring the litellm test's timeout=300.0 + num_retries=0 and the 300 s bound the other live paths use. A stalled server now yields one bounded APITimeoutError/500 per attempt that the flaky marker retries. The other two OpenAIBackend constructions in this file are error-path tests (nonexistent model -> fast 4xx, which the SDK does not retry) and are unchanged. Local verification: 15 passed (all live ollama/openai/litellm telemetry tests plus the five regression tests) against Ollama 0.33.0 / granite4.2:3b; ruff + mypy clean. Signed-off-by: Nigel Jones <jonesn@uk.ibm.com>
E.12 lane of run 33048969379 root-caused a fifth failure shape on test_litellm_token_metrics_integration[streaming] (900-s watchdog). EWO-fold root cause, both pinned empirically: 1. Cap: this PR's 64-token bound used ModelOption.MAX_NEW_TOKENS, which mellea maps to max_completion_tokens on the OpenAI/LiteLLM paths. The CI-pinned Ollama 0.32.2 /v1 handler (openai/openai.go at v0.32.2, verified in source) only maps max_tokens -> num_predict and silently ignores max_completion_tokens, so the say-hello request ran uncapped on CI for 15 m (GIN: 200 | 15m0s; the raw tail shows the server's decode running to a 2549-token runaway at ~9 t/s). Local Ollama 0.33.0 accepts both fields, which masked the problem locally. Both /v1 live tests now send the raw "max_tokens": 64 key (it passes through ModelOption.replace_keys and the SDK's create() filter unchanged). The native-ollama test keeps the sentinel, which maps to num_predict on the native API. 2. Consumption: wait_for(300) wrapped only the first astream() call, but astream() returns as soon as its queue drains, so a long stream is finished by avalue() — unbounded — and rode to the 900-s pytest watchdog. All four live tests now wrap avalue() too (worst case 300 s + 300 s < 900 s). Two regression source-pins in test_flaky_ollama_rerun.py guard both bounds so they cannot be silently 'tidied' away. Local verification against Ollama 0.33.0 / granite4.2:3b: 17 passed in 3m32s (previously 6m47s — the cap now bites locally too); ruff format + ruff check + mypy clean. Signed-off-by: Nigel Jones <jonesn@uk.ibm.com>
The docstring quality gate (test_validate.py:: test_repository_docstrings_use_markdown_backticks[test]) rejects RST double-backtick inline code; run 33063265303 (3.13 lane) caught the two occurrences added in 83774ae. Switch to single backticks per the project docstring convention. Signed-off-by: Nigel Jones <jonesn@uk.ibm.com>
The published granite4.2:3b tag ships num_ctx 131072 in its Modelfile (granite4.1:3b carries no num_ctx, so CI ran the 4096 default). On the 16 GB CPU runner the 131K context allocates a ~6 GB KV cache at load time (8.35 GB total vs 2.75 GB at 8192, measured locally); the memory pressure wedges inference for 15-30 minute windows that outlast the 3x300 s retry budget. Cross-PR data: 268 pull_request runs on other branches since 2026-08-14 have zero Ollama-timeout reds, while every red lane of this PR ran granite4.2:3b. Re-point the local tag at an 8192-context build of the same weights after the pull (tests request the same tag name, so no test changes), and pin the override in test_flaky_ollama_rerun.py. Signed-off-by: Nigel Jones <jonesn@uk.ibm.com>
The granite4.2:3b live-test stalls persist with the 8192-context constraint in place (run 33093698028: ~47-minute wedge on the 3.13 lane, requests hanging at the 300 s cap on both the native and /v1 paths), which refutes the KV-allocation hypothesis. The native tests already send thinking:false with a 2048 per-request context, so thinking mode is not a sufficient explanation either. The remaining differentiator versus main's green runs is the Ollama version itself (0.32.2, a week old) exercising the 4.2 model on slow CPU runners. 0.33.1 (2026-08-26) is the current stable and still carries the GCC-13 AMX fix the pin exists for (generative-computing#1388). Bumping is the minimal controlled experiment; if the wedge persists the issue goes upstream to Ollama/Granite. Signed-off-by: Nigel Jones <jonesn@uk.ibm.com>
granite4.2 thinks by default (model-intrinsic; the Ollama-served build uses a raw prompt template, so the HF chat template's enable_thinking variable is not in play). Ollama's /v1 endpoint ignores chat_template_kwargs — the vLLM convention mellea sent for ModelOption.THINKING: False — so the openai/litellm live tests ran 4.2 with thinking permanently on: a 64-token generation that is ~45 thinking tokens, i.e. minutes on a 4-vCPU CI runner, blowing the 300 s request cap whenever the runner was 2-3x slower than nominal. That is the CI wedge (runs 33093698028, 33104419835); native-path tests already sent think=false and were unaffected. Ollama >= 0.33.1 (the CI pin) maps the OpenAI reasoning_effort param on /v1 to its think control: "none" -> think=false (verified locally: completion_tokens 47 -> 2). Send it for THINKING: False on both OpenAI-compatible backends, alongside the existing chat_template_kwargs.enable_thinking so vLLM-served models keep their mechanism (mirrors the THINKING: True case, which already sends both). Tests: openai live /v1 test now uses ModelOption.THINKING: False instead of the ignored manual extra_body; unit pins updated from "reasoning_effort absent for False" to "reasoning_effort == 'none'"; new OpenAI-backend mapping tests for True/False. Assisted-by: opencode Signed-off-by: Nigel Jones <jonesn@uk.ibm.com>
Run 33122730668 (c9172e0) shows the thinking-off fix working (early /v1 granite4.2 requests complete in 4-46 s, openai /v1 test passes) but a second, engine-level stall remains: from ~mid-run, every 4.2 request on both /v1 and native /api/chat degrades to exactly 300 s -> 500 for ~1.6 h on the 4-vCPU/16 GB runner (5 test failures on 3.11), while the same tests pass locally and on 4.1. The degradation-over-time signature points at accumulation (memory pressure / swapping with several models resident, or engine state), but the serve log at default verbosity records nothing around the hangs. Three additions, all feeding the existing failure-gated dump step so green runs are unaffected: - OLLAMA_DEBUG=1: slog debug level (envconfig/config.go in v0.33.1) for engine-level detail during the hang. - OLLAMA_DEBUG_LOG_REQUESTS=1: per-request bodies + replay curl commands (server/inference_request_log.go) to confirm what the server actually receives and processes. - 30 s resource sampler (vmstat, with a free/loadavg fallback) from server start through the tests: memory, swap in/out, CPU wait. Dump step gains the sampler log and the newest request-debug files. Assisted-by: opencode Signed-off-by: Nigel Jones <jonesn@uk.ibm.com>
The remaining quality-matrix failures had two distinct causes, both proven by the instrumented runs 33122730668 / 33152524235: 1. The telemetry tests built their prompt on SimpleContext, a stateless context whose view_for_generation() always returns [] - so the user message never reached Ollama and the model received an empty conversation. granite4.2 rambles for 2000+ tokens on empty input (4.1 stopped quickly, which is why this never surfaced), so uncapped requests ran into the client's 300 s cap. Those tests now use ChatContext and cap output at 64 tokens. 2. A context-size mismatch forces Ollama to reload the per-model runner, and Ollama 0.33.1 on x86 has a bug where such a reloaded runner ignores num_predict (fresh runners enforce it). All CI 4.2 requests now align on num_ctx=8192 (the Modelfile-constrained default) and the group warmup pins each model at the context its tests use, so the mismatched reload can no longer happen. Assisted-by: opencode Signed-off-by: Nigel Jones <jonesn@uk.ibm.com>
test_ollama_token_metrics_integration[streaming] asserts that gen_ai.client.operation.time_per_output_chunk is recorded, but that histogram only records inter-chunk intervals - it needs >=2 streamed chunks. The 'say hello' prompt can produce a single-chunk reply (the model emits one token, e.g. 'hello'), which leaves the histogram empty and fails the assertion (run 33163851256). Use a counting prompt that reliably spans many tokens/chunks. Assisted-by: opencode Signed-off-by: Nigel Jones <jonesn@uk.ibm.com>
…ndings reasoning_effort="none" was being sent unconditionally to every OpenAI-compatible server, including real api.openai.com, where it is rejected by most current reasoning models. Gate it behind self._server_type != _ServerType.OPENAI on the OpenAI backend and the existing Ollama-provider-prefix check on the LiteLLM backend, matching the mechanism each backend already uses for equivalent divergence. Also, from the same review pass: - Add granite-4.2 entries to BASE_MODEL_TO_CANONICAL_NAME so intrinsic adapter-path resolution stays consistent with the 4.0/4.1 siblings. - Update onboarding docs (quickstart, installation, troubleshooting, tutorials, examples index) that still told new users to pull granite4.1:3b, now stale since start_session()'s default moved to granite4.2:3b. - Fix the CI context-window guard to check the exact num_ctx value instead of just line presence, so a silently-failed re-point to 8192 is actually caught. - Replace the test asserting the raw "max_tokens" key reaches the wire unmapped (it doesn't — both backends remap it to ModelOption.MAX_NEW_TOKENS and back out to max_completion_tokens regardless) with one that reflects actual behaviour. - Stop test_reasoning_effort_conditional_passing from being confounded by the shared fixture's default_extra_body, which forced enable_thinking=False regardless of what the mapping code did. - Fix test_thinking_false_sets_reasoning_effort_none_and_disable in test_litellm_thinking.py, which had been flipped to assert the regression itself against a vLLM target; split it into a non-Ollama case (reasoning_effort absent) and a new Ollama-routed case (sent). Assisted-by: Claude Code Signed-off-by: Nigel Jones <jonesn@uk.ibm.com>
1eebfb4 to
b667812
Compare
Cuts the incident-report-style comments in quality.yml (run IDs, refuted hypotheses, PR cross-refs) and the duplicated reasoning_effort comment in openai.py down to what/why, no history. Assisted-by: Claude Code Signed-off-by: Nigel Jones <jonesn@uk.ibm.com>
Same pass as 0f7a350: cuts run-ID/incident-report detail from the rerun-pattern and warmup-model comments down to what/why. Assisted-by: Claude Code Signed-off-by: Nigel Jones <jonesn@uk.ibm.com>
The chat_template_kwargs/reasoning_effort mapping was duplicated verbatim across _generate_from_intrinsic and _generate_from_chat_context_standard. Extract _map_thinking_option() as the single implementation both call sites use. Assisted-by: Claude Code Signed-off-by: Nigel Jones <jonesn@uk.ibm.com>
Pull Request
Issue
Fixes #1583
Description
Make Granite 4.2 3B the default local text model, and add the public 3B, 8B,
and 30B identifiers. The PR also aligns CI, the GPU runner, and existing
Ollama/vLLM tests with the published model tags so the same model is selected
locally and in automation.
What changed
start_session,OllamaModelBackend, andLiteLLMBackend.the same Granite 4.2 text model.
ModelOption.THINKING: Falsenow sendsreasoning_effort="none"on theOpenAI and LiteLLM backends, scoped to non-
api.openai.comtargets(alongside the existing
chat_template_kwargs.enable_thinkingforvLLM-served models) — real OpenAI reasoning models reject
"none", sothis only fires for Ollama and other self-hosted OpenAI-compatible
servers. The /v1 live tests pass it. Root cause of the CI stalls — see
below.
granite-4.2-{3b,8b,30b}entries to the intrinsics adaptercanonical-name table so adapter-function path resolution stays
consistent with the 4.0/4.1 siblings, and update the onboarding docs
(quickstart, installation, troubleshooting, tutorials, examples index)
that still told new users to pull the previous default model.
catalogues do not provide Granite 4.2 weights.
dense text-model update; this PR does not introduce a Granite 4.2 vision
model.
than require a running Ollama service.
CI stability fixes
Changing the default model means CI's Ollama runner now provisions and serves
Granite 4.2 instead of 4.1, which surfaced several latent bugs — all
test-only, no changes to the library under test. CI lanes running the 4.2
live tests wedged for 15–70 minutes, past the 900 s pytest watchdog, until
these were found and fixed (each now pinned by a regression test in
test/test_flaky_ollama_rerun.py):/v1: Ollama'sOpenAI-compatible endpoint ignores the vLLM
enable_thinkingconventionmellea was sending, so the
/v1live tests ran with reasoning always on —a 64-token reply became ~45 thinking tokens, occasionally pushing a
request over the per-call time bound on a slow runner. This was the
primary cause of the stalls. Fix:
ModelOption.THINKING: Falsenowalso sends
reasoning_effort="none"on OpenAI-compatible backends(
mellea/backends/openai.py,litellm.py,model_options.py; Ollama's/v1only accepts this from 0.33.1, so the CI pin was bumped), scoped tonon-
api.openai.comtargets since real OpenAI reasoning models reject"none".hang a live request, its stream, or the tail of stream consumption
indefinitely. Fix: every live request/stream/value-consumption path in
the test suite is now bounded to 300 s, and the test HTTP client uses
max_retries=0, timeout=300.0(the SDK's own retry+timeout defaults couldotherwise outlast the pytest watchdog before a test-level retry ever saw
the failure).
on
SimpleContext, whoseview_for_generation()always returns[], sothe model received an empty conversation. Granite 4.2 rambles for 2000+
tokens on empty input (4.1 stopped quickly, which hid this). Fix:
switched those tests to
ChatContextand capped output at 64 tokens.num_predictafter acontext-size reload (e.g. 2048→8192), generating 1000+ tokens per request
instead of the requested cap. Fix: CI now pins
num_ctx=8192everywhere and warms up each model at the context its tests use, so the
mismatched reload can't happen (upstream Ollama issue to follow).
time_per_output_chunkonly records inter-chunk intervals, and a short"say hello" reply could produce a single chunk, leaving the histogram
empty. Fix: switched to a counting prompt that reliably spans multiple
chunks.
Result: the quality matrix is green on
b6678123(run 33207685575);
the full suite runs in ~18 min instead of wedging for up to ~2 h. Investigation
detail (refuted hypotheses, intermediate failing runs, instrumentation used)
is in the commit history and prior review discussion on this PR.
Verification
uv run pytest test/ -m "not qualitative"(4,159passed locally) and the GitHub Actions quality matrix both green on
b6678123.verified on GPU hardware.
Known, non-blocking limitations:
ModelOption.THINKING=Falsetothe HF backend's chat-template variable).
tests.
update.
Attribution
Adding a new component, requirement, sampling strategy, or tool?