Skip to content

feat(agents): consolidate every agent onto Gemma-4-E4B at one context size#2284

Merged
kovtcharov merged 6 commits into
mainfrom
claudia/task-357442cb
Jul 19, 2026
Merged

feat(agents): consolidate every agent onto Gemma-4-E4B at one context size#2284
kovtcharov merged 6 commits into
mainfrom
claudia/task-357442cb

Conversation

@kovtcharov

@kovtcharov kovtcharov commented Jul 18, 2026

Copy link
Copy Markdown
Collaborator

Switching agents used to evict and cold-reload the model. Agents that left model_id unset fell back to Qwen3.5-35B-A3B-GGUF while chat and email used Gemma-4-E4B-it-GGUF, so moving between them forced a full unload/reload — 7.9s measured on a warm Lemonade. Every agent now shares one model id and one context window per device profile, so the resident model is reused: three consecutive agent switches cost 0.00s each with the llama-server pid unchanged.

Important

Depends on #2283, and the eval gate is not green yet. The eval workflow now lives in #2283 (this branch previously added the same path — that copy was dropped to avoid a hard conflict). #2283 must merge first, then dispatch Agent Eval — Gemma-4-E4B consolidation against this branch as the merge gate. CLAUDE.md requires that run for LLM-affecting changes. Partial results so far: budget_query baseline 9.95 → 9.90 (flat). No full category has completed.

What was checked empirically, not assumed

max_loaded_models was going to be added to keep the LLM and embedder co-resident. Measuring /api/v1/health on Lemonade 10.10.0 showed it is unnecessary:

"max_models":    {"llm": 1, "embedding": 1, "reranking": 1, ...}   <- PER TYPE
"pinned_models": {"llm": 0, "embedding": 0, ...}

Capacity is accounted per model type, so an LLM and an embedder are already co-resident at default config; setting the knob to 2 would license a second LLM, not the pair. Forcing an LLM swap confirmed it — the LLM pid turned over while the embedder held the same pid throughout. So the knob was not added, and the behaviour is pinned by tests instead.

Related: Lemonade runs one llama-server subprocess per model on its own port (8001/8002 observed), so there is no single global "slot" to raise.

Notes for review

  • A flat rag_quality will not mean "no impact." Most of its scenarios run the doc agent, which comes from hub/agents/python/chat and was already on Gemma. The scenarios that actually exercise this change are csv_analysis (the data/Analyst agent, previously Qwen 35B) and the tool_selection set. Regressions there are the accepted 35B→4B tradeoff.
  • Context is per device profile, not one global number. GPU_CTX_SIZE (65536) and NPU_CTX_SIZE (32768) — the NPU's FLM build cannot load above 32768, so collapsing them would either break NPU or cap GPU doc-Q&A at 32K and reopen [Bug]: 1.4MB file indexed no response to queries in the UI #1030. Measured no latency cost from the larger window: median 1.4s @65536 vs 1.5s @32768 on an identical 1631-token prompt.
  • The feat(npu): NPU-native FLM embedder (embed-gemma-300m-FLM) to end FLM↔Vulkan thrash #1744 NPU backend-matching is preserved — the NPU device config keeps embed-gemma-300m-FLM so chat and embeddings stay on the same backend.
  • Out of scope, unchanged: Summarizer's Qwen3-4B-Instruct-2507-GGUF, SD's SDXL-Turbo, voice models, Qwen3-VL-4B. The two legacy Qwen entries in the MODELS registry stay so existing pinned sessions keep resolving.

Two findings from the first runner execution

Worth carrying into #2283, since they are properties of the eval, not of one workflow file:

  1. The eval needs the chat and analyst hub agents installed. Scenarios select their agent via agent_type — 15 request doc, one requests data. doc/chat/file come from the chat package's entry points, not from docqa (which registers as docqa and no scenario requests). Without them every scenario fails to resolve an agent and the run reports a catastrophic regression that never happened. A pre-flight assertion that doc and data are registered turns that into a 60-second loud failure.
  2. Windows PowerShell 5.1 chokes on non-ASCII in inline run: blocks — em dashes produced The string is missing the terminator. Keep inline PowerShell ASCII-only.

Follow-up (not in this PR)

  • docs(models): point consolidated-agent defaults at Gemma-4-E4B #2279's branch held a further docs pass — measured download sizes (Gemma is 5.56 GB: 4.64 GB weights + 0.92 GB vision projector; nomic-embed 0.48 GB), correcting stale ~3 GB/18.6 GB/25 GB claims across the EMR guide, profile tables and hardware-advisor playbook. That PR is now closed, so this work is stranded on a closed branch and should be re-landed.
  • device.py's _MIN_GPU_VRAM_GB = 24.0 is deliberately untouched. Its rationale — "24 GB to fit Qwen3.5-35B (~17 GB)" — is now obsolete: Gemma-4-E4B measures 5.56 GB, and LITE_MIN_MEMORY_GB = 5.0 is precedent for a lower bar. Lowering it changes the hardware support matrix, which is a product call rather than a side effect of a model swap.

Test plan

  • Merge ci(eval): gate the Gemma-4-E4B consolidation on agent evals #2283 first, then dispatch its Agent Eval — Gemma-4-E4B consolidation workflow against this branch. This is the merge gate.
  • In that run, confirm the log shows doc and data registered before any scenario executes.
  • Review the per-category diff, focusing on csv_analysis and tool_selection for the expected 35B→4B regression; decide whether to refresh baselines with --save-baseline and re-dispatch with enforce: true.
  • python -m pytest tests/unit/ -q — 6786 passed post-rebase; 5 failures are pre-existing on a clean main (test_file_write_guardrails, 3x test_init_command, test_memory_router).
  • python util/lint.py --all — passes.
  • On a machine with Lemonade: python -m pytest tests/integration/test_lemonade_model_residency.py -v — 4 passed against live Lemonade 10.10.0.

@github-actions github-actions Bot added documentation Documentation changes devops DevOps/infrastructure changes mcp MCP integration changes rag RAG system changes llm LLM backend changes cli CLI changes tests Test changes performance Performance-critical changes agents agent::analyst Analyst agent changes agent::fileio FileIO agent changes agent::emr Medical-intake (EMR) agent changes agent::docqa DocumentQA agent changes agent::browser Browser agent changes agent::connectors-demo Connectors-demo agent changes agent::code Code agent changes agent::blender Blender agent changes agent::docker Docker agent changes agent::routing Routing agent changes agent::jira Jira agent changes agent::examples Hub tutorial/example agents (hello-world, word-count, doc-search) labels Jul 18, 2026
@kovtcharov
kovtcharov marked this pull request as ready for review July 18, 2026 23:19
@github-actions

Copy link
Copy Markdown
Contributor

Verdict: Request changes (small, mostly docs — the code is solid)

This puts every agent on Gemma-4-E4B-it-GGUF and pins one context window per device profile (65536 GPU/CPU, 32768 NPU) so switching agents no longer evicts and cold-reloads the model — a genuinely nice latency win, backed by careful measurement and strong tests. Two things should be settled before merge:

  • The eval gate isn't green yet, and this is an LLM-affecting change. Moving Analyst/Code/Jira/Docker/Browser from a 35B MoE to a ~4B model can shift agent behavior, and CLAUDE.md requires a passing gaia eval agent run vs. the committed baselines. You've flagged this and wired the Strix Halo CI job — just hold merge until that run is green (or baselines are intentionally refreshed with the regression called out).
  • One doc still contradicts the code. The EMR guide still documents the EMR agent's LLM as Qwen3.5-35B (18.6 GB), but this PR switches the EMR code to Gemma-4-E4B. Same-release doc drift — worth fixing here.

One thing to confirm, not necessarily block: several agents that were deliberately lightweight (MCP bridge 4096, SD/VLM 8192, minimal 4096) now request 65536 on GPU. That's inherent to the consolidation, but it raises the memory floor 8–16× for those paths and isn't called out in the description — worth a sentence confirming it's intended.

Nits (docs/comments) are in the details below.

🔍 Technical details

🟡 Important

EMR guide contradicts the EMR code changedocs/guides/emr.mdx:46,71,145 still describe the LLM as Qwen3.5-35B-A3B-GGUF (with an "18.6 GB" table row), but this PR changes hub/agents/python/emr/gaia_agent_emr/cli.py:304 and dashboard/server.py:1265,1421 to Gemma-4-E4B-it-GGUF. Per CLAUDE.md, a model change must update every doc that describes it in the same change. Update the guide's model name, the size row, and the "activates only 3B parameters" MoE description.

Eval gate pending on an LLM-affecting change — base agent default flips from Qwen 35B to Gemma-4-E4B (src/gaia/agents/base/agent.py:516), which is exactly the surface CLAUDE.md requires an eval for. You've handled this correctly by wiring the stx CI job and asking reviewers to wait; noting it here only so it's an explicit merge condition, especially for csv_analysis (the data/Analyst agent) and the tool_selection set.

Confirm the lost small windows are intendedsrc/gaia/cli.py:174 replaces the per-agent context map with profile_ctx_size(_configured_device()), so mcp (was 4096), sd/vlm (was 8192), and minimal (was 4096) all jump to 65536 on GPU/CPU. This is unavoidable given the "one resident (model, ctx_size)" goal, but it materially raises the memory floor for those paths and could make the MCP bridge / SD helper fail to load on constrained hardware where they used to work. The GAIA_CTX_SIZE override mitigates it — just worth an explicit line in the description.

🟢 Minor

Same model, two download sizesdocs/guides/jira.mdx:30 says Gemma-4-E4B is "around 3 GB"; docs/guides/docker.mdx:39 says "about 5.6 GB (4.6 GB weights + 0.9 GB vision projector)" for the same model. Pick one.

   The download is about 5.6 GB, so it takes a little while on most connections. It is selected automatically when you run Jira commands.

Stale "larger/coding model" comments — now that the id resolves to a 4B general model, these comments mislead: hub/agents/python/jira/gaia_agent_jira/agent.py:959 ("Use the larger coding model by default") and hub/agents/python/code/gaia_agent_code/agent.py:678 ("Use the coding model for better code understanding"). One-line tidy so the comment matches the value below it.

CI runner label routing never firestest_eval_agent_gemma_consolidation.yml:113 keys runs-on on github.event.pull_request.labels, but the workflow triggers only on push/workflow_dispatch, where github.event.pull_request is null — so the stx-test branch is dead and it always lands on stx. Either add a pull_request trigger or drop the conditional.

Pattern note (not itemized): a broader sweep of Qwen3.5-35B references remains across docs/reference/cli.mdx, docs/guides/hardware-advisor.mdx, docs/guides/agent-ui.mdx, and playbooks. Most are illustrative and out of this PR's stated scope (you noted the #2279 docs overlap) — flagging once so they're on the radar for the docs pass, not as individual blockers.

device.py's _MIN_GPU_VRAM_GB = 24.0 Qwen rationale is correctly left as an explicit open decision per the description — no action needed here.

Strengths

  • The integration tests are the standout. tests/integration/test_lemonade_model_residency.py pins the exact Lemonade capacity assumption the whole design rests on, and the LLM-swap test guards against a vacuous pass by asserting the LLM pid actually turned over before checking the embedder held — that's the kind of "prove the call was valid, not just made" rigor CLAUDE.md asks for.
  • Clean constant extraction. GPU_CTX_SIZE/NPU_CTX_SIZE/profile_ctx_size() in lemonade_client.py replace scattered 32768/65536 literals with one source of truth, and the callers in registry.py and cli.py reference them rather than re-hardcoding.
  • Fail-loud config resolution. _configured_device() (cli.py:145) re-raises GaiaConfigError with an actionable message instead of silently defaulting a device — matches the No-Silent-Fallbacks rule.

@kovtcharov
kovtcharov force-pushed the claudia/task-357442cb branch from bbd102a to 0470fad Compare July 18, 2026 23:32
@github-actions

Copy link
Copy Markdown
Contributor

🟡 One doc update was missed that directly contradicts the new GPU context-size behavior.

docs/reference/api-spec.mdx got the model name updated to Gemma-4-E4B-it-GGUF but the adjacent --ctx-size 32768 instruction for the Code agent was left untouched. On a GPU/CPU machine, initialize_lemonade_for_agent now requests GPU_CTX_SIZE=65536, not 32768 — so a user who follows this page to start Lemonade at 32768 will trigger an immediate reload to 65536, defeating the "one resident pair" goal this PR exists to achieve.

🔍 Technical details

docs/reference/api-spec.mdx:200–202 currently reads:

**Requirements:**
- Lemonade Server with `--ctx-size 32768`
- Model: `Gemma-4-E4B-it-GGUF`

The diff updated the Model: line but left --ctx-size 32768. The new code path:

  • cli.pyinitialize_lemonade_for_agentrequired_ctx = profile_ctx_size(_configured_device())
  • profile_ctx_size("gpu") == GPU_CTX_SIZE == 65536

The instruction should read --ctx-size 65536 on GPU/CPU (only NPU stays at 32768).

docs/guides/routing.mdx:151 also still says --ctx-size 32768 in its troubleshooting guidance, but that line predates this push; the api-spec change was touched in this push and left inconsistent.

@github-actions

Copy link
Copy Markdown
Contributor

🟡 Two new issues worth fixing before merge.

1. Doc mismatch on download sizedocs/guides/jira.mdx says Gemma-4-E4B-it-GGUF is "around 3 GB", but docs/guides/docker.mdx (in the same PR) says "about 5.6 GB (a 4.6 GB Q4_K_M weights file plus a 0.9 GB vision projector)". Same model ID, very different numbers. Users planning around the Jira guide will be surprised by the actual download. Pick one accurate figure and use it consistently across both guides.

2. Preflight ctx check still uses the old 32K threshold_maybe_load_expected_model in src/gaia/ui/_chat_helpers.py (see the function docstring "32K+ context window" and the ctx_size=DEFAULT_CONTEXT_SIZE at the load call) was not updated. DEFAULT_CONTEXT_SIZE is still 32768, but GPU now requires GPU_CTX_SIZE = 65536 per this PR. On a GPU machine where Lemonade happens to be holding the model at 32768, the preflight's guard (active_ctx >= DEFAULT_CONTEXT_SIZE) passes silently — it won't trigger the reload to 65536. The main request path then detects ctx < 65536 and does the reload anyway, so behaviour is eventually correct, but the preflight fires a wasted 32K load first (extra ~8 s cold-load delay for GPU UI users). The fix is to pass the resolved device_ctx (already available upstream) through to _maybe_load_expected_model instead of relying on the module-level constant.

🔍 Technical details

Doc size inconsistency:

  • docs/guides/jira.mdx line introduced in this diff: "The model is around 3 GB, so the download is quick…"
  • docs/guides/docker.mdx line introduced in this diff: "The download is about 5.6 GB (a 4.6 GB Q4_K_M weights file plus a 0.9 GB vision projector)."

Preflight threshold mismatch:

  • src/gaia/ui/_chat_helpers.py:1211active_ctx == 0 or active_ctx < DEFAULT_CONTEXT_SIZE uses 32768 as the reload trigger
  • src/gaia/ui/_chat_helpers.py:1273client.load_model(model_id, ctx_size=DEFAULT_CONTEXT_SIZE, …) loads at 32768

DEFAULT_CONTEXT_SIZE is unchanged at 32768 while GPU_CTX_SIZE is now 65536. The PR updated resolve_device_model to return GPU_CTX_SIZE for GPU (confirmed by the updated test in tests/unit/test_multi_device_wiring.py), and callers of _apply_device_model pass device_ctx=65536 as min_context_size to the Lemonade start path — but _maybe_load_expected_model is a separate preflight that never receives that context. Updating its signature to accept an optional required_ctx: int = DEFAULT_CONTEXT_SIZE and threading the resolved device_ctx through the one call site would fix both the check threshold and the load call.

kovtcharov-amd
kovtcharov-amd previously approved these changes Jul 19, 2026
kovtcharov-amd
kovtcharov-amd previously approved these changes Jul 19, 2026
… size

Switching agents evicted and cold-reloaded the model. Agents that left
model_id unset fell back to Qwen3.5-35B-A3B-GGUF while chat and email used
Gemma-4-E4B, so moving between them forced a full unload/reload — measured at
7.9s on a warm local Lemonade. Every agent now shares DEFAULT_MODEL_NAME, so
the resident model is reused instead: three consecutive agent switches now cost
0.00s each with the llama-server pid unchanged.

Context size follows the device profile rather than the agent. GPU_CTX_SIZE
(65536) and NPU_CTX_SIZE (32768) replace the per-agent literals that ranged
from 4096 to 65536; profile_ctx_size() keys on device because the NPU's FLM
build cannot load above 32768 and would fail outright at the GPU window.
A machine runs one profile, so exactly one (model, ctx_size) pair is resident.

Measured no latency cost from the larger window: median 1.4s at 65536 vs 1.5s
at 32768 on an identical 1631-token prompt.

Out of scope and unchanged: the Summarizer's Qwen3-4B-Instruct-2507-GGUF, SD's
SDXL-Turbo, the voice models, and Qwen3-VL-4B. The two legacy Qwen entries in
the MODELS registry stay so existing pinned sessions keep resolving.
Adds the regression guards for the consolidation's two load-bearing
assumptions, both verified against a live Lemonade 10.10.0.

Residency (tests/integration/test_lemonade_model_residency.py): /health reports
max_models PER TYPE ({"llm": 1, "embedding": 1, ...}), so one LLM and one
embedder are already co-resident at default config — GAIA sets
max_loaded_models nowhere, and setting it to 2 would license a second LLM
rather than the pair. Observed directly: swapping the LLM changed its pid while
the embedder held the same pid throughout, so RAGSDK's scoped-unload assumption
(#1544) still holds. The swap test asserts the LLM pid actually turned over
before checking the embedder, because an earlier version passed in 0.32s
without triggering any load at all — _ensure_model_loaded early-returns when
the resident ctx already satisfies GAIA, so a same-model reload proves nothing.

Context: profile_ctx_size() is covered for gpu/cpu/npu, case and whitespace
normalisation, and the unknown-device default. The NPU case is the reason the
constants are not collapsed into one number.
`gaia eval agent` needs a real Lemonade on AMD hardware, so this runs on the
Windows `stx` pool — PowerShell throughout, Python via setup-venv and Lemonade
via install-lemonade, per the lab-runner convention. Running it on a dev
machine is not a substitute: a 4B model elsewhere takes ~7 min/scenario and the
committed baselines were recorded on AMD, so only this pool yields comparable
numbers.

Reports by default rather than failing. --compare exits 2 on any regression,
and some regressions here are expected and accepted (Analyst and the
tool_selection set dropped from a 35B MoE to a ~4B dense model), so a hard gate
would sit permanently red and gate nothing. `enforce: true` makes it blocking
once accepted regressions are folded into refreshed baselines.

The paths filter covers every LLM-affecting surface, not just the files this
change happened to touch — rag/sdk.py carries the RAG default model and cli.py
resolves the context size, and a narrower filter would silently skip the gate
on exactly the PR that needed it.

Installs the chat and analyst hub agents explicitly and asserts `doc` and
`data` resolve before evaluating: without them every scenario fails to find an
agent and the run reports a catastrophic regression that never happened.
Documents the new single-model default and the per-profile context constants,
and corrects the download sizes that referenced the retired 35B model.

Sizes are measured from a real Lemonade install, not estimated: the Gemma
download is 5.56 GB (4.64 GB Q4_K_M weights plus a 0.92 GB vision projector),
which also corrects the Docker guide's ctx-size guidance — 8192 is below the
window every agent now requests.
Both branches added the identical path, so whichever merged second would
hard-conflict. #2283 is the CI-only PR that owns this workflow and is being
reworked to a weekly schedule; keeping a copy here would overwrite that.

This PR now depends on #2283 landing first, then dispatches that workflow as
its merge gate.
#2244 gave BuilderAgent a preference list resolved against what is actually
installed, which is better than a hardcoded default — but it listed
Qwen3.5-35B-A3B-GGUF first. On a machine that still has the 35B installed the
builder would pick it, evicting and cold-reloading the model every other agent
shares, which is exactly the churn this branch removes. Gemma now leads; the
35B stays last so an existing install keeps working.

Also points the context-overflow probe's threshold at GPU_CTX_SIZE instead of
repeating 65536, so it tracks the profile window rather than drifting from it.
@kovtcharov
kovtcharov added this pull request to the merge queue Jul 19, 2026
Merged via the queue into main with commit 0a30844 Jul 19, 2026
82 checks passed
@kovtcharov
kovtcharov deleted the claudia/task-357442cb branch July 19, 2026 00:14
@github-actions

Copy link
Copy Markdown
Contributor

🟡 docs/guides/jira.mdx quotes the wrong download size for Gemma-4-E4B-it-GGUF — it says "around 3 GB" while docs/guides/docker.mdx (same PR) correctly breaks it down as 5.6 GB (4.6 GB Q4_K_M weights + 0.9 GB vision projector). Both docs are newly updated in this push, so the inconsistency is fresh. A Jira user who budgets 3 GB will hit a failed download.

Fix the Jira guide to match the Docker guide: "about 5.6 GB (a 4.6 GB Q4_K_M weights file plus a 0.9 GB vision projector)".

🔍 Technical details

docs/guides/jira.mdx:30:

The model is around 3 GB, so the download is quick on most connections.

docs/guides/docker.mdx:39 (same PR, same model):

Note: The download is about 5.6 GB (a 4.6 GB Q4_K_M weights file plus a 0.9 GB vision projector).

Both describe Gemma-4-E4B-it-GGUF. The Docker breakdown is the authoritative one; the Jira number appears to be a rough estimate that didn't get updated to match.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agent::analyst Analyst agent changes agent::blender Blender agent changes agent::browser Browser agent changes agent::code Code agent changes agent::connectors-demo Connectors-demo agent changes agent::docker Docker agent changes agent::docqa DocumentQA agent changes agent::emr Medical-intake (EMR) agent changes agent::examples Hub tutorial/example agents (hello-world, word-count, doc-search) agent::fileio FileIO agent changes agent::jira Jira agent changes agent::routing Routing agent changes agents cli CLI changes devops DevOps/infrastructure changes documentation Documentation changes llm LLM backend changes mcp MCP integration changes performance Performance-critical changes rag RAG system changes tests Test changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants