Skip to content

feat(bench): --llm-backend ollama|llama-server for TurboQuant runs - #62

Merged
jaylfc merged 2 commits into
masterfrom
feat/llama-server-backend
May 2, 2026
Merged

feat(bench): --llm-backend ollama|llama-server for TurboQuant runs#62
jaylfc merged 2 commits into
masterfrom
feat/llama-server-backend

Conversation

@jaylfc

@jaylfc jaylfc commented May 2, 2026

Copy link
Copy Markdown
Owner

Summary

Phase 1 of the TurboQuant benchmark spec. Wires `locomo_runner.py` so a single `--llm-backend` flag swaps the generator between Ollama (default, today) and llama-server (TurboQuant fork, for Qwen3.6-35B-A3B with CPU-MoE offload).

The qwen3.6-35B-A3B model only fits on the 3060 12 GB card with TurboQuant KV-cache offload, which lives in the `TheTom/llama-cpp-turboquant` fork — not in Ollama. That fork serves OpenAI-compatible `/v1/chat/completions`; this PR teaches the runner to speak it.

What changed

  • `_llama_server_generate()` — new function. POSTs to `{url}/v1/chat/completions` with `messages=[{"role":"user","content":prompt}]` and reads `choices[0].message.content`. Honours `temperature`, `thinking_mode`, and `no_think_prefix` (the same `/no_think` prefix works inside the llama-server chat template).
  • `_generate(backend, ...)` — dispatcher. Routes to `_ollama_generate` (default) or `_llama_server_generate`.
  • `_judge` — now goes through the dispatcher, defaults to ollama. The in-runner self-judge stays on the same backend as the generator so a llama-server-only model isn't required to also serve via Ollama just for self-judging.
  • `_process_qa` — gains `llm_backend` and `llama_server_url` params; `gen_url` resolved once at the top so both retrieval and full-context branches reach a defined value (the naïve draft put it inside the `else` and would NameError on `--full-context`).
  • HyDE and main answer callsites — switched from `_ollama_generate` to `_generate(llm_backend, client, gen_url, ...)`.
  • New CLI flags: `--llm-backend {ollama,llama-server}` (default `ollama`), `--llm-server-url` (default `http://localhost:8085\`).
  • JSON meta records `llm_backend` and `llm_server_url` for reproducibility.

What does NOT change

  • Default behaviour: every existing chain script that doesn't set `--llm-backend` keeps hitting Ollama exactly as before.
  • `_decompose_query` still calls `_ollama_generate` directly — uses `gemma4:e2b` as a small utility model, kept on Ollama regardless of the main generator's backend.
  • The external rescore (`locomo_rescore_streaming.py`) is untouched; `qwen3:4b` external judge always runs on Ollama.

Status of the surrounding TurboQuant work

  • ✅ Disk freed for it (~198 GB reclaimed in two cleanup passes today)
  • ✅ Qwen3.6-35B-A3B-UD-Q4_K_M GGUF downloaded (21 GB)
  • 🔄 `llama-cpp-turboquant` Docker build in progress on Fedora (at 31% — `turbo2_0/turbo3_0/turbo4_0` template instances compiling, so the TurboQuant cache types are still in this fork)
  • ⏳ Once build finishes: verify `--cache-type-k turbo3` in `--help`, smoke test `--limit 5 --llm-backend llama-server` against a running llama-server, then wait-and-run the full bench after thinking_on subset frees the GPU

Test plan

  • `python3 -c "import ast; ast.parse(...)"` — syntax OK
  • `pytest tests/ -q` — 128 passing (no regression on the retrieval/api/rescore surface)
  • `python3 benchmarks/locomo_runner.py --help` shows both new flags
  • Smoke test `--limit 5 --llm-backend llama-server` once llama-server is built + running
  • Phase 2 baseline + Phase 3 full-stack runs land in a follow-up commit with the actual numbers

Summary by CodeRabbit

  • New Features

    • Added support for an alternate LLM backend (llama-server) alongside the existing Ollama backend for generation and evaluation
    • New CLI option to select an expansion model via --expansion-model
  • Improvements

    • CLI now exposes backend selection and server URL; run metadata records backend, server URL, and expansion model
    • Self-judging and generation consistently use the configured backend
  • Bug Fixes

    • Multihop decomposition failures now emit an error message instead of failing silently

Phase 1 of docs/specs/2026-04-29-qwen36-turboquant-benchmark-design.md.
The qwen3.6-35B-A3B model fits on a 12 GB card only with TurboQuant
KV-cache offload, served by the llama-cpp-turboquant fork's llama-server
on its OpenAI-compatible /v1/chat/completions endpoint. This wires
locomo_runner so a single --llm-backend flag swaps the generator over.

What changed:

- New `_llama_server_generate()` POSTs to `{url}/v1/chat/completions`
  with `messages=[{"role":"user","content":prompt}]` and reads
  `choices[0].message.content`. Honours `temperature`, `thinking_mode`,
  and `no_think_prefix` (the same `/no_think` prefix trick works inside
  the llama-server chat template).
- New `_generate(backend, ...)` dispatcher routes to either
  `_ollama_generate` (default) or `_llama_server_generate`.
- `_judge` now also goes through the dispatcher, defaulting to ollama
  for back-compat. The in-runner self-judge stays on the same backend
  as the generator so a llama-server-only model isn't required to also
  serve via Ollama just for self-judging.
- `_process_qa` gains `llm_backend` and `llama_server_url` params;
  `gen_url` is resolved once at the top of the function (covering both
  the retrieval and full-context branches — the original draft put it
  inside the else and would have NameError'd on --full-context).
- HyDE call (line ~599) and main answer call (line ~649) now go
  through `_generate(llm_backend, client, gen_url, ...)`.
- New CLI args: `--llm-backend {ollama,llama-server}` (default
  ollama) and `--llm-server-url` (default http://localhost:8085).
- JSON meta now records `llm_backend` and `llm_server_url` for
  reproducibility.

What does NOT change:

- Default behaviour: every existing chain script that doesn't set
  `--llm-backend` keeps hitting Ollama exactly as before.
- `_decompose_query` still calls `_ollama_generate` directly — it
  uses `gemma4:e2b` as a small utility model, kept on Ollama
  regardless of the main generator's backend.
- The external rescore (`locomo_rescore_streaming.py`) is untouched;
  the qwen3:4b external judge always runs on Ollama.

Test plan:
- [x] `python3 -c "import ast; ast.parse(open('benchmarks/locomo_runner.py').read())"` — syntax OK
- [x] `pytest tests/ -q` — 128 passing (no regression on the
      retrieval/api/rescore test surface)
- [x] `--help` shows the new flags
- [ ] Smoke test: `--limit 5 --llm-backend llama-server --model
      Qwen3.6-35B-A3B-UD-Q4_K_M --llm-server-url http://localhost:8085`
      once the llama-server build finishes and is running. Will run
      on the Fedora host before firing the full bench.
@coderabbitai

coderabbitai Bot commented May 2, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 1f68bfd4-c31f-4f8f-aeab-b6df2e681444

📥 Commits

Reviewing files that changed from the base of the PR and between c8f89da and a53c47c.

📒 Files selected for processing (1)
  • benchmarks/locomo_runner.py

📝 Walkthrough

Walkthrough

Adds llama-server as an OpenAI-compatible generation backend and generalizes generation and self-judging to dispatch to either Ollama or llama-server; extends _process_qa, CLI, and run metadata to accept and propagate backend, server URL, and expansion-model options.

Changes

LLM Backend Support

Layer / File(s) Summary
Backend Implementation
benchmarks/locomo_runner.py (_llama_server_generate, lines ~196–225)
Adds _llama_server_generate() that POSTs OpenAI-style chat completion requests to {url}/v1/chat/completions and extracts choices[0].message.content, preserving /no_think behavior.
Dispatcher
benchmarks/locomo_runner.py (_generate, lines ~227–251)
Adds _generate() that dispatches to _llama_server_generate when backend=="llama-server" or to existing Ollama generation otherwise.
Judge Integration
benchmarks/locomo_runner.py (_judge, lines ~253–261)
_judge() now accepts backend and calls _generate() to obtain model outputs for scoring instead of always using Ollama.
QA Processing Signature & URL Resolution
benchmarks/locomo_runner.py (_process_qa, lines ~617–631)
Extends _process_qa() with llm_backend, llama_server_url, and expansion_model; computes a single gen_url so HyDE, final generation, and judging use the same backend endpoint.
Query Expansion & Decomposition
benchmarks/locomo_runner.py (lines ~555–676)
Query-expansion now uses expansion_model (defaults to main model) for Ollama-based expansion; _decompose_query() and expansion failures now log errors to stderr instead of silently continuing.
Generation Call Sites
benchmarks/locomo_runner.py (lines ~687–744)
HyDE and predicted-answer generation calls updated to use _generate(llm_backend, client, gen_url, ...) and maintain existing fallback formatting on error.
Judge Call & Task Wiring
benchmarks/locomo_runner.py (lines ~747–870)
Self-judge invoked with backend=llm_backend; task wrapper and guarded tasks forward args.llm_backend, args.llm_server_url, and args.expansion_model.
Configuration & Metadata
benchmarks/locomo_runner.py (lines ~929–1012)
Run metadata records llm_backend, conditionally llm_server_url when using llama-server, and expansion_model; CLI parser adds --llm-backend {ollama,llama-server}, --llm-server-url, and --expansion-model.

Sequence Diagram(s)

sequenceDiagram
  participant Runner as Locomo Runner
  participant Client as httpx.AsyncClient
  participant Ollama as Ollama Server
  participant LlamaSrv as llama-server

  Note over Runner: Request generation (HyDE / final / judge)
  Runner->>Client: prepare request
  alt llm_backend == "llama-server"
    Client->>LlamaSrv: POST {url}/v1/chat/completions (OpenAI format)
    LlamaSrv-->>Client: choices[0].message.content
  else
    Client->>Ollama: Ollama API request
    Ollama-->>Client: response
  end
  Client-->>Runner: generated text
  Runner->>Runner: judge uses same dispatch (backend) to score prediction
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰 A rabbit hops through backends two,
Ollama old, llama-server new,
One dispatcher guides each call,
Judges and answers share the hall,
Benchmarks leap — the logs accrue!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically summarizes the main change: adding a --llm-backend CLI option to switch between ollama and llama-server backends for TurboQuant benchmark runs.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/llama-server-backend

Review rate limit: 9/10 reviews remaining, refill in 6 minutes.

Comment @coderabbitai help to get the list of available commands and usage tips.

@kilo-code-bot

kilo-code-bot Bot commented May 2, 2026

Copy link
Copy Markdown

Code Review Summary

Status: 1 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 0
SUGGESTION 1
Issue Details (click to expand)

SUGGESTION

File Line Issue
benchmarks/locomo_runner.py 224 Refactor suggestion
Files Reviewed (1 files)
  • benchmarks/locomo_runner.py - 1 issues

Reviewed by grok-code-fast-1:optimized:free · 157,312 tokens

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
benchmarks/locomo_runner.py (1)

647-679: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Backend selection is bypassed for query expansion and multihop decomposition

Line 647 still hardcodes llm_url=ollama_url, and Line 679 always calls _decompose_query(..., ollama_url, ...). With --llm-backend llama-server, these paths can silently degrade via broad except blocks instead of using the selected backend.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@benchmarks/locomo_runner.py` around lines 647 - 679, The code is hardcoding
ollama_url for query expansion and multihop decomposition, bypassing the
selected backend and silently swallowing failures; update the calls to use the
chosen backend URL/identifier (use the existing llm_url or llm_backend variable
instead of ollama_url) for expand_query_llm(...) and _decompose_query(...), and
change the broad except Exception: pass handlers to at minimum log the exception
(e.g., logger.exception or process_logger.error with the exception) so backend
failures are visible rather than silently degraded.
🧹 Nitpick comments (1)
benchmarks/locomo_runner.py (1)

217-224: Consider archive instrumentation for the OpenAI-compatible API call (line 217)

Per coding guidelines, OpenAI-compatible API calls should wrap archive recording to capture request/response pairs. However, since benchmarks/locomo_runner.py is a pure benchmark utility (not production code) and intentionally avoids Archive initialization to isolate test data, this may be optional. If this benchmark will be used to measure production-like scenarios or long-term evaluation runs, archive wrapping would be valuable for audit and replay; otherwise, it can remain uninstrumented.

If you decide to add it: initialize Archive alongside VectorMemory in the main setup, pass it through to _process_qa(), and wrap the client.post() call at line 217 with await archive.record() to capture the prompt and model response.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@benchmarks/locomo_runner.py` around lines 217 - 224, The OpenAI-compatible
POST at client.post(...) in _process_qa should be wrapped with archive
recording: initialize an Archive instance alongside VectorMemory in the main
setup, thread that Archive into _process_qa (add an archive parameter), and
replace the direct await client.post(...) with an await archive.record(...) call
that executes the POST and captures the request payload and response body;
ensure the archive.record invocation returns the same resp object/response body
so the subsequent resp.raise_for_status(), body parsing, and message extraction
(choices/msg/content) keep working unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@benchmarks/locomo_runner.py`:
- Around line 196-224: In _llama_server_generate, the thinking_mode parameter is
ignored; update the payload_prompt construction so thinking_mode overrides
no_think_prefix (i.e., when thinking_mode is True use the raw prompt so the
model can emit CoT, otherwise apply the existing no_think prefix logic), then
continue to build payload and post as before using payload_prompt, message role
handling, and temperature/stream fields.

---

Outside diff comments:
In `@benchmarks/locomo_runner.py`:
- Around line 647-679: The code is hardcoding ollama_url for query expansion and
multihop decomposition, bypassing the selected backend and silently swallowing
failures; update the calls to use the chosen backend URL/identifier (use the
existing llm_url or llm_backend variable instead of ollama_url) for
expand_query_llm(...) and _decompose_query(...), and change the broad except
Exception: pass handlers to at minimum log the exception (e.g., logger.exception
or process_logger.error with the exception) so backend failures are visible
rather than silently degraded.

---

Nitpick comments:
In `@benchmarks/locomo_runner.py`:
- Around line 217-224: The OpenAI-compatible POST at client.post(...) in
_process_qa should be wrapped with archive recording: initialize an Archive
instance alongside VectorMemory in the main setup, thread that Archive into
_process_qa (add an archive parameter), and replace the direct await
client.post(...) with an await archive.record(...) call that executes the POST
and captures the request payload and response body; ensure the archive.record
invocation returns the same resp object/response body so the subsequent
resp.raise_for_status(), body parsing, and message extraction
(choices/msg/content) keep working unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 498eb30a-6845-401c-9e5f-cc104c4effab

📥 Commits

Reviewing files that changed from the base of the PR and between afab872 and c8f89da.

📒 Files selected for processing (1)
  • benchmarks/locomo_runner.py

Comment on lines +196 to +224
async def _llama_server_generate(client: httpx.AsyncClient, url: str, model: str,
prompt: str, temperature: float = 0.2,
thinking_mode: bool = False,
no_think_prefix: bool = False) -> str:
"""OpenAI-compatible generator for llama-server (TurboQuant fork).

Used when ``--llm-backend llama-server`` is set so Qwen3.6-35B-A3B can
serve answers via llama.cpp's chat completions endpoint (the model
doesn't fit in Ollama at this quant on a 12 GB card without TurboQuant
KV-cache offload). ``thinking_mode`` and ``no_think_prefix`` keep their
semantics: with ``no_think_prefix`` we prepend ``/no_think`` so the
chat template can suppress reasoning tokens; with ``thinking_mode`` we
leave both off and let the generator emit CoT.
"""
payload_prompt = ("/no_think\n\n" + prompt) if no_think_prefix else prompt
payload = {
"model": model,
"messages": [{"role": "user", "content": payload_prompt}],
"temperature": temperature,
"stream": False,
}
resp = await client.post(f"{url}/v1/chat/completions", json=payload)
resp.raise_for_status()
body = resp.json()
choices = body.get("choices") or []
if not choices:
return ""
msg = choices[0].get("message") or {}
return (msg.get("content") or "").strip()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

--thinking-mode has no effect on the llama-server path

thinking_mode is accepted but never used in _llama_server_generate, so behavior does not change between enabled/disabled states for this backend.

Suggested minimal fix
 async def _llama_server_generate(client: httpx.AsyncClient, url: str, model: str,
                                  prompt: str, temperature: float = 0.2,
                                  thinking_mode: bool = False,
                                  no_think_prefix: bool = False) -> str:
@@
-    payload_prompt = ("/no_think\n\n" + prompt) if no_think_prefix else prompt
+    use_no_think = no_think_prefix or not thinking_mode
+    payload_prompt = ("/no_think\n\n" + prompt) if use_no_think else prompt
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@benchmarks/locomo_runner.py` around lines 196 - 224, In
_llama_server_generate, the thinking_mode parameter is ignored; update the
payload_prompt construction so thinking_mode overrides no_think_prefix (i.e.,
when thinking_mode is True use the raw prompt so the model can emit CoT,
otherwise apply the existing no_think prefix logic), then continue to build
payload and post as before using payload_prompt, message role handling, and
temperature/stream fields.

log failures instead of swallowing them

CodeRabbit caught a real issue (rated 🟠 Major) on PR #62: with
--llm-backend=llama-server + --llm-query-expansion, the expansion
call would hit Ollama with a GGUF-named model the Ollama daemon
doesn't have, fail inside `expand_query_llm`, and silently fall
through the bare `except: pass` block. The bench would run with no
expansion at all and the user would never know — exactly the
silent-failure pattern the project's policy memory warns against.

Same shape on `_decompose_query`: small utility-model call on Ollama,
also silently swallowed exceptions.

What changed:

- New `--expansion-model` arg defaulting to "" (uses --model for
  back-compat). Required when --llm-backend=llama-server because the
  generator's GGUF name is meaningless to Ollama in that mode.
- Plumbed `expansion_model` through `_process_qa` and the `_guarded`
  callsite. Recorded in the JSON meta block.
- Replaced both `except Exception: pass` handlers with explicit
  prints to stderr. The expansion failure log includes the model and
  URL so a misconfigured backend is obvious. The decompose failure
  log includes the URL.
- expand_query_llm always uses `ollama_url` (because that's the only
  endpoint shape it speaks); the model is now configurable.

What does NOT change:

- Default behaviour for existing chains: --expansion-model unset →
  uses --model → identical Ollama call shape as before.
- 128 tests still pass.
@jaylfc

jaylfc commented May 2, 2026

Copy link
Copy Markdown
Owner Author

Pushed `a53c47c` addressing the major finding. Real bug — confirmed:

  • With `--llm-backend=llama-server --llm-query-expansion`, the expansion call would have hit Ollama with the GGUF model name (e.g. `Qwen3.6-35B-A3B-UD-Q4_K_M`) which Ollama doesn't have, then failed inside `expand_query_llm`, then been swallowed by the bare `except: pass`. The bench would have run with no expansion and the user would have had no signal.

Fixes:

  • New `--expansion-model` arg (default empty → falls back to `--model` for back-compat). Required when `--llm-backend=llama-server` since `expand_query_llm` only speaks Ollama's `/api/generate`. Recorded in JSON meta.
  • Both bare `except Exception: pass` blocks (query expansion + multihop decompose) replaced with explicit `print(..., file=sys.stderr)` including the offending URL/model so misconfigurations are visible in the bench log instead of silently degraded.

Pre-existing chains that don't set the new flag get identical Ollama call shape as before. 128 tests still pass.

@jaylfc
jaylfc merged commit d421f75 into master May 2, 2026
2 checks passed
@jaylfc
jaylfc deleted the feat/llama-server-backend branch May 2, 2026 14:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant