What You'll Build
A local browser computer-use agent (CUA) on AMD hardware: Microsoft's Fara1.5-9B served by llama-server built against ROCm/HIP on an RX 7900 XTX, with the vision projector loaded so the model actually sees browser screenshots, driven by Microsoft's own fara-cli harness against a plain local Playwright Chromium. You give it a task in English; it takes a screenshot, reasons, and emits a click, a keystroke or a URL visit, then loops.
This card is the one place in the 9B's consumer matrix where the interesting question is not what to quantise to. 24 GB of GDDR6 clears the unquantised BF16 build with the full projector and a 65,536-token window — the precision the vendor asks for, on a die llama.cpp special-cases in BF16's favour. Every 16 GB card in this family, AMD or NVIDIA, is excluded from that by arithmetic.
Hardware data: RX 7900 XTX (24GB VRAM, gfx1100) · BF16 weights + f16 mmproj = 17.545 GiB, 19.741 GiB total at 65,536 context (derived below) · See benchmark data
⚠️ This model clicks, types and submits in a real browser — and
fara-clidoes not sandbox it. That is not boilerplate; it is the operating requirement, and it is the one thing to get right before anything else on this page. The model card is blunt: "Don't run this model with unrestricted browser access on a machine that has anything sensitive on it." Fara also reads whatever is on the page, so a hostile page is a prompt-injection surface.The vendor asks for protections the
fara-clipath does not provide. The harness README says "We recommend running it in a sandboxed environment, monitoring its execution, and avoiding sensitive data or high-risk domains." — butfara-cliitself drives a plain local Playwright Chromium: no container, no allow-list, no pause control. Those controls belong to Magentic-UI / MagenticLite. The same README describes Magentic-UI as "a sandboxed browser environment with auditable action logging and user prompts at critical points" and offers it as what you use "instead of fara-cli you can use Magentic-UI", while the model card's Responsible AI section asks you to "run Fara in an isolated container with no access to host files, environment variables, or sensitive credentials" and then says plainly: "If you integrate Fara1.5-9B directly, you're responsible for these controls."So if you follow the
fara-clipath below, you are the boundary. Practically: run it on a throwaway OS user or VM, let Playwright use its own fresh browser profile (never your daily one), stay logged out of everything, keep tasks on low-stakes public sites, and watch the run rather than leaving it unattended. If you want the container, allow-lists, watch-mode and pause the model card asks for, use Magentic-UI instead — see Running.
ℹ️ Vision is the product, not a bonus. Per the model card, "The model is vision-only at perception time: it sees the browser through screenshots, not the DOM or accessibility tree." A Fara install that only serves text is a broken install — the
--mmprojstep below is mandatory, and Step 4 exists to prove an image really reached the model, on the GPU, before you let it drive anything.
⚠️ What is established about this model on ROCm, and what is not. The primary argument is structural, and it is strong. llama.cpp's multimodal layer is backend-agnostic by construction:
tools/mtmd/clip.cppcontains zero matches forcuda,hip,rocmor anyGGML_USE_*guard. It asks ggml for whatever GPU backend exists —ggml_backend_init_by_type(GGML_BACKEND_DEVICE_TYPE_GPU, nullptr)— and always puts the CPU backend into the same scheduler as a per-op fallback. The backend HIP compiles,ggml/src/ggml-cuda/ggml-cuda.cu, does carryGGML_USE_HIPguards beyond the vendor-API shim — a VMM/ROCR-Runtime unmap workaround, an NCCL warning suppression, host-buffer registration — but none in the op dispatch or in thesupports_optable, which is the half that decides whether an operation runs at all. There is no separate "HIP op set"; it is one backend compiled twice.There is runtime corroboration, and it is narrower than it first looks. llama.cpp issue #13694 contains the line
clip_ctx: CLIP using ROCm0 backend, with ROCm buffer sizes throughout — a real local HIP run of the mtmd path, no RPC involved. Three qualifications, all of which matter. The log is fromllama-mtmd-cli, notllama-server. The silicon is an RX 7800 XT (Navi 32), not this card, and its run reportsgfx1100 (0x1100)only because the command line setsHSA_OVERRIDE_GFX_VERSION="11.0.0"— so the ISA matches this card's native target while the die does not. And the projector there isqwen2.5v(PROJECTOR_TYPE_QWEN25VL), not theqwen3vl_mergerthis model ships; the sameclip.cppcase block handles both, which is why it transfers at all. All 85 comments were checked and none contains anotherCLIP usingline, so that is the thread's only runtime datapoint.For our exact projector on ROCm the evidence is weaker still and should be read as attested once, informally: in issue #20081 a community reporter writes "ROCm (over RPC) works perfectly fine with the same image as aforementioned" about a
qwen3vl_mergerprojector — over the RPC backend, on different hardware, as a passing remark rather than a measurement.Net position: rest on the structure, treat the runs as corroboration. What nobody has published is a run of Fara1.5 on any AMD GPU, or a speed number for it on any consumer GPU at all (see Results). Treat your first run as the test and use Step 4, which reads llama.cpp's own load-time self-report rather than asking you to trust this paragraph.
Requirements
| Component | Minimum | This recipe |
|---|---|---|
| GPU | 24GB VRAM, officially ROCm-supported RDNA3 | RX 7900 XTX (24GB, gfx1100) — not measured; budget below is derived (/contribute) |
| RAM | 24GB (weights are mmap'd on their way to VRAM; Playwright's Chromium sits on top) | — |
| Storage | 18.84 GB (BF16 + f16 mmproj) | 18,838,863,264 B = 17.545 GiB, plus the harness and Chromium |
| Software | Linux, ROCm, llama.cpp b10087+ (HIP), Python 3.10+, Playwright | — |
Two notes on that table. min_vram_gb is 24 because that is the smallest real card capacity the install documented here runs on — 19.741 GiB accounted before a single compute buffer, which no 16 GB card holds. The 4B recipe for this same GPU sets 16, and both are correct: they are filter floors for different installs, not statements about this card. And the GPU cell is not a measurement — /check/fara1-5-9b/rx-7900-xtx returns verdict: unknown with zero benchmark rows, and nothing on this page was run on hardware by us.
Why the 9B, at BF16, on this card
Microsoft's hardware line for this size asks for "A GPU with enough memory for a 9B model in bf16 (A6000, A100, H100, and B200 have been tested)". Every consumer recipe for this model in our catalogue has had to answer that request with "no, and here is the quantisation tier I took instead". This card answers it with yes.
That matters more for Fara than it would for a chat model, and the reason is in the card's own description of the job: it predicts "the next action with grounded arguments (e.g., pixel coordinates for a click)", trained on "Curated datasets for predicting actions and pixel coordinates from screenshots". No public evaluation measures what any quantisation tier costs grounding accuracy. Every rung down the ladder is therefore an unmeasured risk, not a priced one — which is precisely the kind of risk worth spending idle VRAM to avoid.
BF16 is specifically favoured on this die, and here is the code
llama.cpp names RDNA3 explicitly in its BF16 matrix-multiply path. At ggml/src/ggml-cuda/ggml-cuda.cu a BF16 compute type sets
prefer_f32_output = !GGML_CUDA_CC_IS_RDNA3(cc) && !GGML_CUDA_CC_IS_CDNA(cc);
so RDNA3 and CDNA are the only two architecture families that keep BF16 output instead of being downgraded to the F32-output fallback the next branch installs. And the constant behind that predicate, in ggml/src/ggml-cuda/common.cuh, is this card's exact gfx target:
#define GGML_CUDA_CC_RDNA3 (GGML_CUDA_CC_OFFSET_AMD + 0x1100) // RX 7000, minimum for WMMA
GGML_CUDA_CC_IS_RDNA3 is a half-open range test spanning [0x1100, 0x1200) rather than an equality test, so the favour extends across RDNA3 generally — but gfx1100 is the constant it is anchored on, and the comment "RX 7000, minimum for WMMA" is describing this generation. RDNA3's WMMA units take FP16 and BF16 natively; BF16 is not merely tolerated here, it is one of two architectures special-cased in its favour.
Which files, and why
The text weights are BF16 because the source model is BF16, so Fara1.5-9B-bf16.gguf is a file-format conversion rather than a lossy one — bartowski's card labels that row "Full BF16 weights." The projector is the f16 build rather than the bf16 one, on precision grounds: at the same two bytes per value, IEEE binary16 spends three more bits on the mantissa and three fewer on the exponent — a finer grid over a narrower range — and the projector is the stage that turns patch embeddings into the coordinates this model clicks on. The bf16 projector is also 3 MiB larger; substitute it and add 3 MiB to every figure below.
The VRAM budget, derived
There is no benchmark for this pair — /check reports unknown with zero rows — so every number here is arithmetic from the artifacts' own metadata and from llama.cpp's source, not a measurement. Reproduce it rather than trusting it. The card holds 24,576 MiB, i.e. 24.000 GiB, and the whole derivation is in GiB so the comparison is like-for-like.
Weights — byte counts read from the bartowski GGUF tree:
| File | Bytes | GiB |
|---|---|---|
Fara1.5-9B-Q4_K_M.gguf | 5,910,783,104 | 5.505 |
Fara1.5-9B-Q5_K_M.gguf | 6,852,928,640 | 6.382 |
Fara1.5-9B-Q6_K.gguf | 7,700,259,968 | 7.171 |
Fara1.5-9B-Q8_0.gguf | 9,545,983,104 | 8.890 |
Fara1.5-9B-bf16.gguf | 17,920,697,216 | 16.690 |
mmproj-Fara1.5-9B-f16.gguf | 918,166,048 | 0.855 |
mmproj-Fara1.5-9B-bf16.gguf | 921,704,992 | 0.858 |
(HuggingFace displays these as decimal GB — 17.92 GB for the BF16 file — which is the same bytes, not a different number.)
KV cache — smaller than you expect, because this is a hybrid model. The source config.json carries a layer_types array of 24 linear_attention + 8 full_attention entries, and llama.cpp agrees: LLM_ARCH_QWEN35 maps to "qwen35" in src/llama-arch.cpp, and src/models/qwen35.cpp assigns layers with is_recr_impl[i] = (i + 1) % full_attn_interval != 0. With full_attention_interval = 4 and 32 blocks that leaves layers 3, 7, … 31 — 8 attention layers. Only those carry a cache that grows with context:
K+V per token per attention layer = 2 × n_head_kv(4) × head_dim(256) × 2 B (f16) = 4,096 B
× 8 full-attention layers = 32 KiB / token
n_ctx = 32,768 → 1.000 GiB
n_ctx = 65,536 → 2.000 GiB
n_ctx = 131,072 → 4.000 GiB
n_ctx = 262,144 → 8.000 GiB
A non-hybrid 32-layer model with the same head geometry would cost four times that. Context on this model is cheap — every doubling costs exactly 1.000 GiB more than the last.
Recurrent state — fixed, and four times a single sequence's worth. The other 24 layers hold a context-independent state, sized by n_embd_r() / n_embd_s() in src/llama-hparams.cpp from this file's own GGUF keys (ssm.conv_kernel 4, ssm.inner_size 4096, ssm.group_count 16, ssm.state_size 128):
conv state : (4−1) × (4096 + 2×16×128) = 24,576 elems
ssm state : 128 × 4096 = 524,288 elems
per layer : 548,864 × 4 B (F32) = 2.094 MiB
× 24 recurrent layers = 50.25 MiB ← per sequence
× n_seq_max = 4 = 201 MiB = 0.196 GiB
That ×4 is not optional and it is easy to miss. src/llama-model.cpp sizes the recurrent cache with /* recurrent_rs_size */ std::max((uint32_t) 1, cparams.n_seq_max), and llama-server does not leave n_seq_max at 1: common/arg.cpp passes n_parallel = -1 for the server, and tools/server/server.cpp then resolves that with if (n_parallel < 0) { n_parallel = 4; kv_unified = true; }. So the recurrent state is allocated once per slot — four times — while the KV cache is not multiplied, because that same default makes it unified: a unified cache holds -c tokens in total rather than per sequence. The same line is why -c 65536 gives a single trajectory the whole 65,536 tokens rather than a quarter of them.
The whole trade, in one table. Accountable allocations (text weights + f16 projector + KV + the 0.196 GiB recurrent state) in GiB, against a 24.000 GiB card. Each cell is the sum of the exact byte-derived inputs, rounded once at the end — rounding the inputs first moves the third decimal:
| Context | Q4_K_M | Q5_K_M | Q6_K | Q8_0 | bf16 |
|---|---|---|---|---|---|
| 16,384 | 7.056 | 7.934 | 8.723 | 10.442 | 18.241 |
| 32,768 | 7.556 | 8.434 | 9.223 | 10.942 | 18.741 |
| 65,536 | 8.556 | 9.434 | 10.223 | 11.942 | 19.741 |
| 131,072 | 10.556 | 11.434 | 12.223 | 13.942 | 21.741 |
| 262,144 (native) | 14.556 | 15.434 | 16.223 | 17.942 | 25.741 |
The decision: BF16 at 65,536, not Q8_0 at a bigger window
Two configurations in that table are defensible leads and it is worth saying why this recipe picked one of them. BF16 @ 65,536 accounts for 19.741 GiB and leaves 4.259 GiB. Q8_0 @ 262,144 — the model's full native window — accounts for 17.942 GiB and leaves 6.058 GiB. Both fit. They are not equivalent.
The tiebreak is which side of the trade is measured.
- Quantisation cost is unmeasured. Nothing public says what Q8_0 does to a model whose output is pixel coordinates. It is probably small. "Probably small" is not a number, and this is the one card in the matrix that never has to find out.
- Context need is measured, and bounded. The harness README asks for "at least 15000 tokens". Images are capped: the card states "We only keep the most recent 3 screenshots", and at 1,260 tokens each (derived below) that is 3,780 tokens resident, permanently. What actually grows is the text trajectory, and
--max_roundsdefaults to 100 inrun_fara.py. So at-c 65536the loop has(65,536 − 3,780) / 100 ≈ 620tokens per round for the model's own reasoning and action history — against≈ 290at 32,768 and≈ 1,270at 131,072. 620 tokens of reasoning per step, a hundred steps deep, is a real budget rather than a token gesture.
So the 2.000 GiB that would buy 131,072 is not free headroom being wasted; it is being spent on the half of the trade nobody has priced. That also keeps this recipe aligned with the 4B AMD recipe for this card, which leads BF16 at the same 65,536 for the same reason.
Two configurations declined explicitly, so you can disagree with the arithmetic in front of you rather than in the dark:
- BF16 @ 131,072 (21.741 GiB) does fit, and doubles the per-round budget to ~1,270 tokens. It leaves 2.259 GiB for the HIP context, compute buffers, the vision encode graph and a desktop compositor — none of which this recipe derives. If you run headless and have measured your own buffer sizes, this is the upgrade to make before a long unattended run. It is not the default here because the margin is thinner than the evidence justifying it.
- BF16 @ 262,144 (25.741 GiB) does not fit at all, and no flag rescues it.
Nothing in that table is free space you can plan against to the last byte. Read the buffer-size lines llama-server prints on startup for the real figures on your build, and report what you see.
How much context one screenshot costs
A CUA sends a screenshot every step, so the per-image cost decides whether the budget above is honest. It is not a fixed number baked into the projector: llama.cpp treats this one as dynamic-resolution, which is what --image-min-tokens exists to floor.
The harness fixes the browser viewport at 1440×900 (fara15_agent.py: viewport_width: int = 1440, viewport_height: int = 900), matching the card's note that "The screen resolution Fara is most commonly trained with is" 1440×900. It pre-resizes with Qwen's smart_resize at factor = patch_size × merge_size = 32, so 900 becomes 896. llama.cpp counts patches the same way in tools/mtmd/clip.cpp and clamps this projector to 8–4096 merged tokens (hparams.set_limit_image_tokens(8, 4096) in the PROJECTOR_TYPE_QWEN3VL branch), which 1,290,240 px sits inside — so nothing is rescaled:
(1440 / 32) × (896 / 32) = 45 × 28 = 1,260 tokens per screenshot
× 3 retained screenshots = 3,780 tokens resident
The harness's own conservative default image_token_estimate: int = 1500 sits just above the derived 1,260, a useful sanity check on the arithmetic.
Keep that 1,260 in mind for the ROCm troubleshooting below: it means every single agent step submits a prompt-processing batch well over a thousand tokens. This workload never sits in the "small request" regime, which is exactly the regime where one RDNA3 ROCm bug reported working behaviour.
Installation
1. Build llama.cpp with ROCm/HIP for gfx1100
Build from a release at or after b10087 — that is the release the GGUFs used here were quantised on, per the quantiser's card. The gfx target is not optional to get right, and llama.cpp's own build documentation names this card for it: "The above example uses gfx1100 that corresponds to Radeon RX 7900XTX/XT/GRE."
git clone https://github.com/ggml-org/llama.cpp
cd llama.cpp
HIPCXX="$(hipconfig -l)/clang" HIP_PATH="$(hipconfig -R)" \
cmake -S . -B build -DGGML_HIP=ON -DGPU_TARGETS=gfx1100 -DCMAKE_BUILD_TYPE=Release
cmake --build build --config Release -- -j 16
AMD's ROCm system-requirements table lists AMD Radeon RX 7900 XTX · RDNA3 · gfx1100 · Supported, alongside the RX 7900 XT and RX 7900 GRE on the same target. Rather than trust any table, you can ask your own card — llama.cpp documents the self-check: "Find your gpu version string by matching the most significant version information from rocminfo | grep gfx | head -1 | awk '{print $2}' with the list of processors". On an RX 7900 XTX that prints gfx1100.
Both halves of the model are first-class in mainline — worth confirming before you download 19 GB:
grep -n 'LLM_ARCH_QWEN35,' src/llama-arch.cpp # -> { LLM_ARCH_QWEN35, "qwen35" }
grep -n 'qwen3vl_merger' tools/mtmd/clip-impl.h # -> PROJECTOR_TYPE_QWEN3VL
Three things this build deliberately does not do, all of them habits carried over from CUDA recipes:
- No
flash-attnpackage, noxformers. llama.cpp's attention lives inside ggml; there is no Python attention library to install, and the CK build of the standalone FlashAttention project does not target consumer RDNA3 anyway. The vision context probes its own flash-attention support against whichever backend it got and reports the result — Step 4 shows you where. - No quantisation-kernel extras. No CUDA-only INT4 kernel path applies here, and none is needed: this recipe runs unquantised weights, and BF16 is a first-class ggml type on this architecture for the reason derived above.
-DCMAKE_BUILD_TYPE=Release, notDebug. See the first troubleshooting entry; a Debug build has been reported to emit invalid device code on Radeon under some ROCm versions.
There is also an official prebuilt image, ghcr.io/ggml-org/llama.cpp:server-rocm, if you would rather not build a toolchain. Read llama.cpp's own caveat before relying on it — "The GPU enabled images are not currently tested by CI beyond being built." — and note that it pins its own ROCm version (ARG ROCM_VERSION=7.2.1 in .devops/rocm.Dockerfile), which is the variable the first troubleshooting entry is about.
2. Download the weights and the projector
Both files are required. The projector is a separate artifact — downloading only the text GGUF gives you a model that cannot see.
pip install -U huggingface_hub
hf download bartowski/Fara1.5-9B-GGUF \
Fara1.5-9B-bf16.gguf mmproj-Fara1.5-9B-f16.gguf \
--local-dir ~/models/fara1.5-9b
Microsoft publishes no first-party GGUF for Fara1.5 — its org ships Fara1.5-4B, Fara1.5-9B and Fara1.5-27B as safetensors only, plus an ONNX repo for the previous-generation Fara-7B, which is a different model. Several independent community quantisers have converted the 9B including its vision tower, each publishing a matching mmproj; independent conversions succeeding is the strongest available evidence that the vision path converts cleanly, and it is still not a runtime test on this backend.
You can confirm the artifact you just downloaded declares the projector llama.cpp implements:
strings ~/models/fara1.5-9b/mmproj-Fara1.5-9B-f16.gguf | grep -m1 qwen3vl_merger
3. Serve it
./build/bin/llama-server \
-m ~/models/fara1.5-9b/Fara1.5-9B-bf16.gguf \
--mmproj ~/models/fara1.5-9b/mmproj-Fara1.5-9B-f16.gguf \
-ngl 99 -c 65536 --host 127.0.0.1 --port 5000 \
--image-min-tokens 1024 \
--reasoning-format none \
--temp 0
Three of those flags are load-bearing and none of them is a default:
--reasoning-format none— mandatory. Without it the agent loop silently receives nothing; see Troubleshooting for the mechanism. The server documents this value as "none: leaves thoughts unparsed inmessage.content".--image-min-tokens 1024— this projector ships no minimum-pixel key, so llama.cpp falls back to the floor set byhparams.set_limit_image_tokens(8, 4096)in thePROJECTOR_TYPE_QWEN3VLbranch oftools/mtmd/clip.cpp, and warns four lines later in the same branch: "Qwen-VL models require at minimum 1024 image tokens to function correctly on grounding tasks". Fara is a grounding model, so that warning is aimed precisely at this case. At the harness's viewport the image is already 1,260 tokens and the floor never binds; it protects you if you shrink the viewport.-c 65536— see the budget table. Do not leave it at the model's native 262,144: at BF16 that is 25.741 GiB and will not load.
Do not pass --parallel. Leaving it alone keeps llama-server's unified KV cache; setting it divides your context window between sequences, which is the opposite of what you want at 65,536.
Leave --flash-attn alone. The vision context probes flash-attention support against the backend it actually got and falls back with a warning if the backend cannot serve it, which is the behaviour you want on a platform where that answer is build-dependent.
If you would rather not manage files, -hf bartowski/Fara1.5-9B-GGUF:bf16 fetches the model, and per the server docs "mmproj is also downloaded automatically if available".
4. Prove the vision path is running, and running on the GPU
This step matters more on AMD than on CUDA, because it is where the open question of this recipe gets answered on your machine in about ten seconds. llama.cpp reports its own multimodal wiring at load. In the startup log, find three things:
- A line of the form
clip_ctx: CLIP using <backend> backend—clip.cppemits this as"CLIP using %s backend"with the backend's own name. If it saysCPU, the projector is not on the GPU at all and image encoding will be slow. flash attention is enabledorflash attention is disabled. Either is fine for correctness — but they do not cost the same, and on a platform where the answer is build-dependent it is worth knowing the number before you see the line. Without flash attention, llama.cpp materialises the attention scores as an explicit F32 tensor bounded byn_kv × n_ubatch × n_head × 4 B; at this recipe's-c 65536, llama.cpp's default physical batch (n_ubatch= 512) and this model's 16 attention heads, that is 2.000 GiB on top of every figure in the budget table above. It takes the 4.259 GiB of slack down to 2.259 GiB — the same margin this page declines BF16 @ 131,072 over, and it deserves the same scepticism.min_vram_gb: 24holds either way: the configuration still fits a 24 GiB card with that term added, so what adisabledline costs you is headroom, not the floor.- Whether the warning block "WARNING: the CLIP graph uses unsupported operators by the backend" appears.
clip.cppwalks every op in the vision graph after warmup and lists any the chosen backend cannot accelerate, naming the backend. Absence of that block is a direct, first-party statement that the ROCm backend accelerates the wholeqwen3vl_mergergraph on your card — which is the claim this recipe deliberately does not make on your behalf. If the block appears, it also prints the exact op names and shapes, and asks you to file them upstream; please do, and tell us too.
Then check the documented capability and send a real image. llama.cpp's server README instructs clients to check /models or /v1/models for the multimodal capability before a multimodal request; /props is a useful supplement because it names which modality loaded, which is the signal you want when a projector fails quietly.
curl -s http://127.0.0.1:5000/v1/models | grep -o multimodal
# supplementary — names the modality that actually loaded
curl -s http://127.0.0.1:5000/props | python3 -c "import sys,json; print(json.load(sys.stdin)['modalities'])"
# expect: {'vision': True}
curl -s http://127.0.0.1:5000/v1/chat/completions -H "Content-Type: application/json" -d '{
"model": "fara",
"temperature": 0,
"messages": [{"role": "user", "content": [
{"type": "text", "text": "Describe this image in one sentence."},
{"type": "image_url", "image_url": {"url": "https://raw.githubusercontent.com/ggml-org/llama.cpp/master/media/llama0-logo.png"}}
]}]
}' | python3 -m json.tool
If the reply describes the picture, the projector is wired up. If it answers as though no image were present, stop here — the agent loop below will silently produce nonsense actions rather than fail loudly.
5. Install the harness
The harness is hardware-neutral: a Python client that speaks to any OpenAI-compatible endpoint over HTTP and drives Playwright locally, with nothing vendor-specific in it. Its documented primary platform is also Linux — the README's Installation section leads with "The following instructions are for Linux systems, see the Windows section below for Windows instructions." — the same platform ROCm requires, so there is no impedance mismatch to work around.
git clone https://github.com/microsoft/fara.git
cd fara
python3 -m venv .venv && source .venv/bin/activate
pip install -e .
playwright install
Skip the README's [vllm] extra. You are hosting with llama-server, and it pulls a large dependency tree you will not use.
One convenience specific to this size: run_fara.py sets DEFAULT_ENDPOINT_CONFIG = {"model": "Fara1.5-9B", ...}, so the 9B is the harness's default model name. The --model argument below is explicit anyway, because the name you pass has to match what your server advertises.
Running
Two clients share the same llama-server endpoint. Path A (fara-cli) is the smoke test, with no isolation of any kind. Path B (Magentic-UI) is what the vendor recommends for anything real. Pick deliberately; they are not equivalent in risk.
Path A — fara-cli, for a first smoke test
source .venv/bin/activate
fara-cli \
--base_url http://127.0.0.1:5000/v1 \
--api_key sk-no-key-required \
--model fara \
--headful \
--task "how many pages does wikipedia have" \
--output_folder ./runs/first
⚠️
--headfulis not a convenience flag.fara-clidefaults to headless:run_fara.pypassesheadless=not args.headfulat both call sites, and its argparse help reads "Run the browser in headful mode (show GUI, default is headless)". Drop the flag and a real Chromium still opens, still clicks and still submits — you simply cannot see it, which makes the supervision advice above impossible to follow.
With that flag the command opens a real Chromium on your desktop with no confinement around it — the playwright install in step 5 is what put it there. Each run writes per-step screenshots and a data_point.json recording the task, actions, observations and outcome to --output_folder; watch that folder on the first few runs, because it is how you tell a grounding failure (clicks landing in the wrong place) from a reasoning failure. Keep the task on a low-stakes public site and stay signed out. Fara is trained to stop and ask before entering personal information, making payments, submitting forms, signing in or sending messages; if you see it sail through one of those, stop the run — that is a wiring problem in your prompt path, not a model quirk to tolerate.
Note that --max_rounds defaults to 100. That is the number the context budget above is sized against; pass a smaller value for a first smoke test if you would rather not leave a browser driving itself a hundred steps deep.
Path B — Magentic-UI, for the protections the vendor asks for
Everything the model card asks for — the containerised browser, domain allow-lists, watch-mode and a pause control — lives in Magentic-UI, not in fara-cli. That is also where a Docker requirement would come from; there is no Docker step in Path A because Path A genuinely does not use one.
Magentic-UI takes the same OpenAI-compatible endpoint you started above, so nothing on this page changes except the client. Its setup is documented in its own repository rather than reproduced here, because this recipe has not verified those steps end to end on this GPU — give it http://127.0.0.1:5000/v1 as the model endpoint. Whichever client you use, match the trained resolution: the card is explicit — "Match this in your sandbox for the most reliable grounding."
Results
- Speed: omitted — no published measurement exists for Fara1.5-9B on this card, on any AMD GPU, or on any consumer GPU at all. The only hardware the vendor names is datacenter class, tested against its bf16 serving path, which says nothing about a llama.cpp/HIP build on RDNA3. Borrowing a number from it would be misleading. If you run this, please contribute your numbers so this section can be replaced with a measurement.
- VRAM usage: 17.545 GiB of weights and projector; 19.741 GiB accountable including KV and the four-slot recurrent state at 65,536 context, derived above from the GGUF's own metadata and llama.cpp's memory formulas. This is a derivation, not an observed peak — compute buffers, the HIP context and the vision encoder's working set sit on top, and 4.259 GiB of the card is left for them. See /check/fara1-5-9b/rx-7900-xtx.
- Quality notes: nothing here is quantised, so the usual "does 4-bit hurt a coordinate-predicting model" caveat does not apply — this is the vendor's own BF16 precision, and this card is one of the few consumer parts that can say so for the 9B. The vendor's published scores for this size — 86.6 WebVoyager, 63.4 Online-Mind2Web, 32.3 WebTailBench, from the evaluation table on the model card, with full methodology in the Fara-1.5 paper — are therefore at the same precision you are running, which is not something the quantised recipes for this model can say. Neither source states which hardware produced them, so treat the precision match as the only transferable part. Nothing about those numbers speaks to an RDNA3 card.
- How this card differs from its 16GB RDNA3 sibling. Same instruction-set family, same BF16 favourability, different capacity — and at 9B the capacity difference is decisive rather than incremental. Per AMD's own ROCm hardware table the RX 7900 XTX is Navi 31 with 96 compute units on gfx1100 against the RX 7800 XT's 60 on gfx1101, and 24 GB against 16. The BF16 build is 16.690 GiB of weights alone, so the 16GB card cannot run this configuration at any context setting; its recipe leads Q8_0 instead. That is a hard consequence of capacity, not a tuning preference. Throughput will also be higher here on CU count alone, but that is a directional expectation from the specifications rather than a measurement — which is why the Speed line above is empty rather than estimated.
- What ROCm changes, and what it does not. The architecture is fully registered in mainline llama.cpp and its multimodal layer contains no backend-specific code, so nothing about this model is CUDA-dependent by design. What ROCm changes is the maturity of the stack under ggml — see Troubleshooting, where the one RDNA3-adjacent failure this workload can actually hit is a ROCm toolchain issue rather than anything to do with Fara.
- Language: English only, per the model card's out-of-scope list.
For the full benchmark data, see /check/fara1-5-9b/rx-7900-xtx.
Troubleshooting
The agent does nothing — every step logs an empty response or list index out of range
This is the failure everyone hits, and it is not a bug in your setup. Fara opens a <think> block and never closes it — readable straight out of the chat template embedded in the GGUF you downloaded, whose generation prompt emits <think>\n with no closing tag unless enable_thinking is explicitly false. Under llama-server's default reasoning extraction the parser therefore treats the whole output — reasoning and the <tool_call> block together — as reasoning, returns it in reasoning_content, and leaves message.content empty. The harness parses message.content for <tool_call>, finds nothing, and the loop dies on round one.
Launch with --reasoning-format none, documented as "none: leaves thoughts unparsed in message.content". The raw output then begins with a stray unclosed <think> tag, which is cosmetic.
Reported for Fara1.5-27B by a community user on build b10107 in the model's HF discussions and acknowledged there by Microsoft — swhitehead, whose account carries the org-member flag on that repo, replied "Would you mind posting this as an issue on the Fara1.5 github repo so we can mention this in the hosting instructions?", which became microsoft/fara#82. The same reporter adds: "If you have a router/proxy in front doing its own reasoning extraction, disable it there too."
ROCm error: device kernel image is invalid — a ROCm-version trap this workload is shaped to trigger
Worth knowing about before you start, and it has nothing to do with Fara. llama.cpp issue #23934 — opened on an RX 7900 XTX (gfx1100, ROCm 7.11, clang 22, Ubuntu 22.04), closed 2026-07-02 — reports that small requests completed fine while larger ones aborted in the HIP runtime.
Read the scoping in both directions. Reasons it may not apply: the model that reproduced it was a quantised one, and the closing comment scopes its own fix to "with quantized models" — this recipe leads unquantised BF16, so it sits outside the condition the thread names. Reasons not to dismiss it: the card that reproduced it is exactly this one, and a second reporter hit the same error on an entirely different die using official Windows HIP builds, naming no model at all — so the common factor is the ROCm/clang toolchain version, which is die-agnostic. All participants are community accounts and no llama.cpp maintainer engaged. Treat it as a trap to recognise rather than one to expect.
Two community answers, in order:
- Build Release, not Debug. A commenter identified "with ROCm 7.11 on Radeon GPUs where the LLVM backend emits invalid instructions at -O0" as a known ROCm-side defect, hitting only larger matrix kernels — which is why small requests looked healthy. Use
RelWithDebInfoif you need symbols. - Upgrade ROCm. Release alone did not fix it for the reporter, who later closed the issue with: "If you're hitting
ROCm error: device kernel image is invalidon gfx1100 (RX 7900 XTX) with quantized models, this is the fix." — the fix being a move from ROCm 7.11 / clang 22 to a newer ROCm and clang, plus a current amdgpu DKMS driver.
Why this matters more here than for a chat model: a CUA submits a 1,260-token screenshot on every single step, so this workload is permanently outside the "small request" regime where the bug stays hidden. If you are going to hit it, you will hit it on step one rather than discovering it a week later. A current ROCm is the cheapest insurance on this page.
Don't reach for Vulkan to fix the vision half
For text generation on gfx1100 the Vulkan backend is a legitimate thing to A/B against ROCm, and sometimes wins. That advice does not transfer to the projector. llama.cpp issue #20081 reports vision output badly degraded on Vulkan versus CUDA for specific images with exactly this projector — the reporter's log shows projector: qwen3vl_merger under clip_ctx: CLIP using Vulkan0 backend. Read it as a caution rather than a verdict: the Vulkan backend maintainer could not reproduce it, and the reporter's Vulkan device was an RDNA3 integrated GPU, not this card. Still, if you are going to move the vision half off ROCm, check its output rather than assuming parity. You can move the projector independently of the text model with the MTMD_BACKEND_DEVICE environment variable, which clip.cpp reads to pick a named backend for the CLIP context only.
You do not need HSA_OVERRIDE_GFX_VERSION on this card
It appears in a great many copy-pasted AMD configs, sometimes with a comment claiming it is critical for the 7900 XTX. It is not. The override exists to make ROCm present one gfx target as another, for libraries that shipped kernels only for a different die; gfx1100 is an officially supported target in AMD's own system-requirements table and ships real kernels, and the build in Step 1 compiles llama.cpp for it directly. Setting it on top of a correctly-targeted build asks the runtime to pretend your card is something else while your binary already contains the right code — at best a no-op. If you have inherited it from a shell profile or a prebuilt ROCm image, unset it before diagnosing anything.
Do not enable MTP speculative decoding
The qwen35 architecture carries a multi-token-prediction layer, and llama.cpp can use it as a self-draft with --spec-type draft-mtp. Don't, for now: issue #23244, "Eval bug: Always OOM with mtp model + rocm", collects OOM-on-first-decode reports with MTP enabled, including one naming gfx1100. Note carefully that this is not an AMD bug despite the issue's title — the same thread has reporters hitting it on NVIDIA hardware too, and the suspected cause is the memory-fit estimator undercounting MTP. The issue is closed not planned rather than fixed, so treat the advice as current. Nothing in this recipe enables it, and at BF16 you have less slack to absorb an estimator error than the quantised recipes for this model do.
The model answers but ignores the screenshot
Almost always the projector. Confirm --mmproj is on the command line, that the server logged an mmproj load stage and a CLIP using … line, that /v1/models advertises the multimodal capability, and that Step 4's /props call reports vision: True. A text-only GGUF paired with no projector loads and runs perfectly happily — it simply cannot see, which in a CUA presents as confidently wrong actions rather than an error. Note the automatic projector download only happens on the -hf path; with -m you must pass --mmproj yourself.
Clicks land in the wrong place
Check the viewport first: the harness defaults to 1440×900 because that is what the model was mostly trained at, and a different window size changes both the grounding distribution and the token cost derived above. Then confirm --image-min-tokens 1024 is set, since llama.cpp's own warning ties sub-1024-token images to degraded grounding. On this card there is no third suspect on the precision side — you are already running the vendor's own BF16 weights, which is the step every smaller-VRAM install has to hold in reserve.
rope.dimension_sections has wrong array length; expected 4, got 3
This is a real llama.cpp bug (PR #25334), but it belongs to Ollama-exported qwen35 blobs, not to the GGUF this recipe uses. If you see this error, you are loading a different file than the one downloaded in Step 2.
ollama run fara does nothing — there is no Ollama entry
There is no Ollama library entry for Fara1.5 in any form. The GGUF/LM Studio/Ollama line in the harness README refers to Fara-7B, the previous generation, reached with a --fara-7b flag — it is not about Fara1.5, and the two should never be mixed. That is a slightly bigger loss on AMD than elsewhere, since Ollama is usually the cleanest ROCm on-ramp — but it does not exist for this model, so llama-server built from source is the path this recipe documents.
Out of memory at load
In order of what to try, with what each actually frees:
| Change | Frees | Cost |
|---|---|---|
-c 65536 → 32768 | 1.000 GiB | ~290 tokens per round instead of ~620, at --max_rounds 100 |
bf16 → Fara1.5-9B-Q8_0.gguf | 7.800 GiB | the first rung down an unmeasured grounding ladder |
bf16 → Fara1.5-9B-Q6_K.gguf | 9.519 GiB | two rungs down, and far more than you need on this card |
--no-mmproj-offload | 0.855 GiB | slower image encoding, once per agent turn |
Take the context first — it is the cheapest and the best-understood. Keep the f16 projector: at 0.855 GiB it is not where the savings are, and trading vision fidelity in a model whose entire job is looking at pixels is the worst rung on this list. If you are running a desktop on the same card, the compositor's own allocation is a real term in this budget; a headless box gives you most of a gigabyte back.
Running it against your own accounts
Don't. The model is trained to pause before sign-ins, payments and submissions, and that training is a safety net, not a guarantee — the card lists prompt injection embedded in page content among its known risks, and a vision-only agent has no way to distinguish a legitimate button from a hostile one that renders like it. Keep it in a container, on an allow-list, with no credentials in reach.