self-hosted/ai
§01·recipe · multimodal

Fara1.5-4B on Apple M4 Max: Browser Computer-Use Agent at Full 262K Context

multimodaladvanced24GB+ VRAMJul 31, 2026

This advanced recipe sets up Fara1.5-4B on the Apple M4 Max, needing about 24 GB of VRAM.

models
tools
prerequisites
  • Apple M4 Max (48GB unified memory); the config below also fits any Apple Silicon Mac with 24GB+ unified memory
  • macOS 14 Sonoma or newer
  • Python 3.10+
  • llama.cpp built for Metal (release b10087 or newer)
  • A disposable browser profile — fara-cli drives a plain local Playwright Chromium with no isolation

What You'll Build

A local browser computer-use agent (CUA) running entirely on Apple Silicon: Microsoft's Fara1.5-4B served by llama-server on Metal with the vision projector loaded, so the model actually sees browser screenshots, driven by Microsoft's own fara-cli harness against a local Playwright Chromium.

Hardware data: Apple M4 Max (48GB unified memory) · Q8_0 + f16 projector = 4.812 GiB of weights, 13.008 GiB total at the model's full 262,144-token context (derived below) · See benchmark data

⚠️ This model clicks, types and submits in a real browser — and fara-cli does 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.

Be clear about what the tooling gives you. fara-cli drives a plain local Playwright Chromium — no container, no allow-list, no pause control. The Docker sandbox belongs to Magentic-UI, which the harness README describes as "a sandboxed browser environment with auditable action logging and user prompts at critical points" and offers as what you use "instead of fara-cli you can use Magentic-UI". The vendor's position is a recommendation, not a guarantee shipped in the CLI: "We recommend running it in a sandboxed environment, monitoring its execution, and avoiding sensitive data or high-risk domains."

On a Mac this matters more than usual, because the Mac you would run this on is very likely your daily machine — the same one holding your mail, your keychain and your logged-in sessions. If you follow the fara-cli path below, you are the sandbox. Practically: run it under a throwaway macOS user account or a VM, let Playwright use its own fresh browser profile (never your daily Chrome), 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 --mmproj step below is mandatory, and Step 4 exists to prove an image really reached the model before you let it drive anything. This is also the single reason the MLX build a reader would reach for first is the wrong one; see Alternative runtime.

⚠️ Runtime status — read before you commit an evening to this. Metal support for every piece of this model is verifiable statically and this recipe shows that evidence, but no published report shows Fara1.5 specifically captioning an image under llama-server on Apple Silicon. Treat the first run as the test. There is also no published speed measurement for this pair, and none is invented here (see Results).

Requirements

ComponentMinimumThis recipe
GPU24GB unified memory (Apple Silicon, Metal)Apple M4 Max, 48GB unified — not measured; the budget below is derived (/contribute)
RAMunified with the GPU — see budget below
Storage5.17 GB (Q8_0 + f16 projector)5.17 GB decimal / 4.812 GiB
SoftwaremacOS 14+, llama.cpp b10087+ (Metal), Python 3.10+, Playwright

Why 48GB does not change this recipe, and why "does it fit" is the wrong question

Apple has no dedicated VRAM. Metal's recommendedMaxWorkingSetSize caps what the GPU may address, and below 64GB that lands near 66% of unified memory — roughly 32 GiB safe on this 48GB machine, perhaps 36 GiB optimistically. That figure is extrapolated from the documented rule rather than read off a 48GB machine's own log, so it is the conservative end that gets used below. Either way /check's raw 48 GB is optimistic by about a third.

Even so, a 4B model is nowhere near that wall: the heaviest configuration on this page is 16.7 GiB, about half the conservative pool. No iogpu.wired_limit_mb raise is needed for anything here, and if a Fara recipe tells you to raise it, it has copied a caveat that does not apply.

So fit is not the axis, and the honest headline is that nothing material differs from the 64GB configuration — the model's full native context and the top of the quality ladder both still fit, at the same settings, with roughly 19 GiB of the addressable pool untouched. What this particular chip contributes is not capacity but memory bandwidth: at 546 GB/s the 48GB M4 Max is the highest-bandwidth part of the four Apple configurations this site covers, and token generation is bandwidth-bound, so this is the Mac on which the byte-streaming argument below pays off best. The M4 Max ships in two bins (Apple tech specs) — a 14-core-CPU/32-core-GPU part at 410 GB/s and a 16-core-CPU/40-core-GPU part at 546 GB/s — and Apple sells 48 GB only on the latter, so a 48GB M4 Max is unambiguously the 546 GB/s die. That is a published specification, not a measurement of this model — see Results.

Weights — byte counts from the bartowski GGUF tree:

ComponentBytesGiB
Fara1.5-4B-Q8_0.gguf4,493,954,1444.185
mmproj-Fara1.5-4B-f16.gguf672,423,5840.626
Q8_0 pair5,166,377,7284.812
Fara1.5-4B-bf16.gguf8,424,393,5687.846
mmproj-Fara1.5-4B-bf16.gguf675,569,3120.629
bf16 pair9,099,962,8808.475

KV cache — and this is where Fara1.5 is unusual, and where the context decision gets cheap. Its qwen3_5 architecture is a hybrid. The source config.json spells the split out layer by layer in text_config.layer_types: 24 of 32 layers are linear_attention, only 8 are full_attention (full_attention_interval: 4). llama.cpp models this as a hybrid memory — llm_arch_is_hybrid() in src/llama-arch.cpp returns true for LLM_ARCH_QWEN35, and llama-memory-hybrid.cpp filters the growing cache with !hparams.is_recr(il). Only those 8 layers carry a cache that scales with context.

With num_key_value_heads: 4 and head_dim: 256 from the same config.json:

per layer per token = (4 x 256) + (4 x 256)      = 2,048 elements
x 8 full-attention layers                        = 16,384 elements/token
at f16                                           = 32 KiB/token
x 262,144 tokens (the model's full context)      = 8.00 GiB

The other 24 layers hold a fixed-size recurrent state, sized by llama.cpp's n_embd_r() / n_embd_s() in src/llama-hparams.cpp from linear_conv_kernel_dim: 4, linear_num_value_heads: 32 x linear_value_head_dim: 128 (= 4096), linear_num_key_heads: 16 and linear_key_head_dim: 128:

n_embd_r = (4-1) x (4096 + 2 x 16 x 128)  =    24,576
n_embd_s = 128 x 4096                     =   524,288
(24,576 + 524,288) x 24 layers x 4 bytes  = 50.25 MiB per sequence
x 4 sequences (llama-server's default)     = 201.0 MiB = 0.196 GiB

That state is allocated per sequence, not once: src/llama-model.cpp passes /* recurrent_rs_size */ std::max((uint32_t) 1, cparams.n_seq_max), and tools/server/server.cpp resolves its auto default with params.n_parallel = 4, so a plain llama-server reserves four slots' worth. It is a rounding error against this machine's pool; it is stated at its real size because it costs nothing to be right.

Totals against a ~32 GiB addressable pool:

ConfigWeights + projectorKV @ 262,144Recurrent stateTotal
Q8_0 + f16 mmproj4.812 GiB8.000 GiB0.196 GiB13.008 GiB
bf16 + bf16 mmproj8.475 GiB8.000 GiB0.196 GiB16.671 GiB
Q8_0, context halved to 131,0724.812 GiB4.000 GiB0.196 GiB9.008 GiB

Compute buffers and the vision graph are not in the table — they are allocated at load time and this recipe does not derive them. Read the buffer-size lines llama-server prints on startup for the real figures on your build, and report what you see.

Why Q8_0 and not bf16, when both fit twice over. Token generation is bandwidth-bound, and even 546 GB/s is a fixed budget. The bf16 pair streams 1.87x the bytes of the Q8_0 pair (8,424,393,568 / 4,493,954,144) for a step that is conventionally treated as near-lossless. So Q8_0 leads. bf16 stays documented above as the reference-fidelity option — and unlike the 8 GB sibling, where stepping up a quant tier is a rationing decision, here it costs you nothing but throughput. That is the concrete thing 48GB buys on this model: the full 262K context and the top of the quality ladder at the same time, instead of one or the other.

How much context one screenshot costs

A CUA sends a screenshot every step, so this is the number that decides whether the context budget above is honest. It is not the 768 you may see quoted for this projector — that value derives from num_position_embeddings: 2304 (√2304 x 16 = 768) as a reference for position-embedding interpolation, and llama.cpp treats this projector as dynamic-resolution.

The harness fixes the browser viewport at 1440x900 (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" 1440x900. It then applies Qwen's smart_resize with patch_size: 16 and spatial_merge_size: 2, so the alignment factor is 32 and each 2x2 patch block merges to one token:

900 -> round(900/32)*32   = 896        1440 -> round(1440/32)*32 = 1440
merged grid = (1440/16)/2 x (896/16)/2 = 45 x 28
image tokens                           = 1,260 tokens per screenshot

The harness keeps max_n_images: int = 3, so 3,780 tokens of any prompt are screenshots; its own conservative default image_token_estimate: int = 1500 sits just above the derived 1,260, which is a useful sanity check on the arithmetic. Against the 262,144-token window above, screenshots are rounding error — the reason to keep the window wide is the accumulated reasoning and action history on a long task, not the images. The repo's floor still applies as a lower bound: "Please ensure that context length is set to at least 15000 tokens and temperature to 0 for best results."

Installation

1. Install llama.cpp with Metal

Metal needs no flags. Per docs/build.md: "On MacOS, Metal is enabled by default. Using Metal makes the computation run on the GPU." There is no CUDA toolkit, no ROCm, and no flash-attn wheel in this recipe — none of them exist on Apple Silicon.

brew install llama.cpp

Homebrew is a documented install channel for macOS in docs/install.md and its formula tracks releases. If you would rather build from source — worth doing here, because the Metal path for this architecture is young and still being tuned:

git clone https://github.com/ggml-org/llama.cpp
cd llama.cpp
cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build --config Release -j

Use b10087 or newer — that is the release the GGUF below was quantised on, per the quantiser's card.

What makes this work on Metal, since nobody has published a Fara-on-Mac run. Every op class this model needs has a Metal kernel in ggml-metal.metal: kernel_rope_multi and kernel_rope_vision for the interleaved multimodal RoPE this architecture uses, kernel_gated_delta_net_impl for the 24 linear-attention layers, kernel_ssm_conv_* / kernel_ssm_scan_f32 for their recurrent state, and kernel_im2col / kernel_conv_2d for the vision patch embedding. The projector is not stranded on the CPU either: clip.cpp asks for a generic GPU backend at context creation and logs which one it got (CLIP using <backend> backend) — on a Metal build that line should read Metal, and it is the first thing to check in the startup log. The gated-delta-net kernels landed in PR #20361 (metal : add GDN kernel, merged by a maintainer, superseding the unmerged community PR #20244), and a maintainer is still optimising that path in the open PR #25788, whose benchmark table runs qwen35 models on an Apple M3 — an older and much lower-bandwidth chip than this one, so read that table for direction only. For the vision half specifically, a community contributor's open PR #21443 profiles the image-encoding path of a Qwen3.5 sibling on the Metal backend and reports where the time goes, so that encoder demonstrably runs there.

That is static and circumstantial evidence, not a tested Fara run. Step 4 below is the test.

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-4B-GGUF \
  Fara1.5-4B-Q8_0.gguf mmproj-Fara1.5-4B-f16.gguf \
  --local-dir ~/models/fara1.5-4b

Microsoft publishes no first-party GGUF for Fara1.5 — the microsoft org ships safetensors for all three sizes (and an ONNX repo only for the previous-generation Fara-7B, a different model). Four independent community quantisers have converted the 4B including its vision tower — bartowski, prithivMLmods, DevQuasar and runanywhere all ship an mmproj file. Four independent conversions succeeding is the strongest available evidence that the vision path converts cleanly; it is still not a runtime test.

3. Serve it

./build/bin/llama-server \
  -m ~/models/fara1.5-4b/Fara1.5-4B-Q8_0.gguf \
  --mmproj ~/models/fara1.5-4b/mmproj-Fara1.5-4B-f16.gguf \
  -ngl 99 -c 262144 --port 5000 \
  --image-min-tokens 1024 \
  --reasoning-format none \
  --temp 0

Four of those flags are load-bearing and worth understanding rather than pasting:

  • --reasoning-format none. This is the one that decides whether the agent loop works at all, and it is maintainer-acknowledged. Fara opens a <think> tag and never closes it, so the server's default reasoning extraction files the whole reply — thoughts and the <tool_call> block — as reasoning, leaving message.content empty; harnesses that read content then get nothing. none is documented in common/arg.cpp as "none: leaves thoughts unparsed in message.content", which is what the harness expects. The report is Fara1.5-27B discussion #4, reproduced on b10107, where the reporter states plainly that "It emits a <think> opening tag but never a closing </think>." and that with the default the whole reply — thoughts and tool call together — "lands in reasoning_content, and message.content comes back as" an empty string. A Microsoft team member replied there asking for it to be filed upstream; it now sits as the open fara#82. Nothing about this is Apple-specific — it is a property of the model's chat template, so it bites every serving stack, including the MLX one below.
  • --image-min-tokens 1024. The projector ships no image_min_pixels key, so llama.cpp falls back to a low default floor and warns: "Qwen-VL models require at minimum 1024 image tokens to function correctly on grounding tasks" (clip.cpp). Fara is a grounding model — it emits pixel coordinates — so this is precisely the case the warning is about. At the harness's 1440x900 viewport the image is already 1,260 tokens and the floor never binds; it protects you if you shrink the viewport.
  • -c 262144. The model's native window, costing 8.00 GiB of KV per the table above. The 8 GB NVIDIA sibling has to cap this at 32,768 and the 16GB M2 Pro at 65,536; you do not. Halve it to 131072 if you would rather keep the total under 9.1 GiB.
  • -ngl 99. Explicit rather than necessary — a Metal build already runs the computation on the GPU by default, per docs/build.md above.

Two things you will notice are missing, deliberately. There is no --cache-type-k q8_0 KV-quantisation step: it exists, but at 8 GiB of KV against ~32 GiB addressable it buys nothing and costs precision on a model whose job is coordinates. And there is no sudo sysctl iogpu.wired_limit_mb raise, for the reason given in Requirements.

4. Prove the vision path works before going further

This is the step that turns the static Metal evidence above into a fact about your machine. Check the server advertises multimodal, confirm the CLIP backend line in the startup log, then send a real image. The server README instructs clients to check for the capability this way, and documents image_url as a first-class content part whose URL "can be a remote URL, base64 (raw or URI-encoded via" a data URI "or path to local file".

curl -s http://localhost:5000/v1/models | grep -o multimodal

curl -s http://localhost: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 and Metal is running it. If it answers as though no image were present, or the server never logged an mmproj load, stop here — the agent loop below will silently produce nonsense actions rather than fail loudly. And if content comes back empty while a reasoning field is full, you missed --reasoning-format none.

5. Install the harness — and skip the extra the README hands you

git clone https://github.com/microsoft/fara.git
cd fara
python3 -m venv .venv && source .venv/bin/activate
pip install -e .
playwright install chromium

Use pip install -e ., not the README's pip install -e .[vllm]. That extra pins a CUDA serving stack — vllm==0.19.1 plus nvidia-cutlass-dsl==4.5.2, both visible in pyproject.toml — which is neither installable nor useful on Apple Silicon. The README notes the plain install as the option for people not self-hosting on their own GPU: "If you plan on hosting with Microsoft Foundry only, you can skip the [vllm] and just do pip install -e .". You are self-hosting, just not with vLLM, so take the plain install and point the client at llama-server.

The rest of the harness is genuinely portable: pyproject.toml declares Operating System :: OS Independent and the runtime dependencies are playwright==1.51, openai, pillow and other pure-Python packages, with no CUDA anywhere. Playwright 1.51 publishes a macosx_11_0_arm64 wheel, so Chromium runs native on Apple Silicon with no Rosetta. Screenshots are captured through the browser (page.screenshot() in playwright_controller.py), not off the desktop, so macOS Screen Recording permission is never requested and never needed. The harness's Xvfb code — the usual source of Linux-only breakage in headful browser agents — is gated behind platform.system() == "Linux" in environment.py, so --headful on macOS simply opens a Chromium window.

Running

Two client paths share the same llama-server endpoint. Path A (fara-cli) is the smoke test — unsandboxed. 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

fara-cli speaks to any OpenAI-compatible endpoint. Point it at llama-server:

fara-cli \
  --base_url http://localhost:5000/v1 \
  --api_key sk-no-key-required \
  --model fara \
  --task "how many pages does wikipedia have" \
  --output_folder ./runs/first

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 — it is how you tell a grounding failure (clicks landing in the wrong place) from a reasoning failure.

Runs are headless by default; --headful opens the window. Either way this drives a real, unsandboxed Chromium on your Mac — the playwright install chromium in step 5 is what put it there. Keep the task on a low-stakes public site, stay signed out, and stay at the keyboard. 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.

Path B — Magentic-UI, for the sandbox

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. This is 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: per the harness README, "instead of fara-cli you can use Magentic-UI". 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 machine — follow microsoft/magentic-ui and give it http://localhost:5000/v1 as the model endpoint.

Alternative runtime: MLX-VLM

Apple recipes on this site normally lead with MLX. This one leads with llama.cpp-Metal, and the reason is worth stating precisely, because it is not a framework gap: mlx-vlm implements this architecture natively — mlx_vlm/models/qwen3_5/ exists, and its vision.py is a one-line subclass of the Qwen3-VL vision model. It also ships a real OpenAI-compatible server (mlx_vlm.server) whose /v1/chat/completions accepts image_url content parts, which is exactly what fara-cli --base_url needs. The call here is about build quality and provenance, not capability:

  • The MLX build a reader would reach for first is text-only. mlx-community/Fara1.5-4B-OptiQ-4bit keeps its vision tower in a separate optiq/optiq_vision.safetensors and says so on its own card: "The one repo loads text-only under stock mlx-lm and full image+text under OptiQ." Vision requires installing the third-party optiq package. For a model whose entire job is looking at a screenshot, a text-only load is not a degraded install, it is a broken one — and it carries the mlx-community name that normally signals "safe default".
  • The one clean MLX build is very new and very small. runanywhere/Fara1.5-4B-mlx-4bit is made with the right converter and passes a tensor census: 1,221 tensors of which 297 are vision, matching the 297 vision tensors in Microsoft's own model.safetensors.index.json exactly, all resolving to its single model.safetensors rather than a side artifact, with the vision tower left unquantised. Its card states it "Loads through MLX VLM stacks that support qwen3_5". But it is a single-author repo days old with double-digit downloads, it offers one quant tier, and its accuracy claims are the author's own self-report with no independent check.
  • The GGUF side has a size ladder and a vetted redistributor. bartowski ships 24 quant tiers from IQ2_M up to full bf16, each with a matching projector — which is precisely what makes the "run it at Q8_0, or at bf16 if you want the reference" choice on this page possible.

If you would rather stay in MLX, the parallel path is real and short:

pip install -U mlx-vlm
mlx_vlm.server --model runanywhere/Fara1.5-4B-mlx-4bit --host 127.0.0.1 --port 8080

Then point the harness at http://127.0.0.1:8080/v1. Two things to know before you do:

  • --host 127.0.0.1 is not optional. The server's own default is 0.0.0.0, which publishes an unauthenticated model endpoint — one that drives a browser — to your whole network.
  • The <think> problem exists here too, and the default already handles it. mlx-vlm's server splits thinking out of the reply exactly as llama.cpp does, and its splitter returns an empty content when it finds an opening tag with no closing one — the same dead agent loop. What saves you is the default: enable_thinking defaults to False, and Fara's chat template branches on it to emit a pre-closed <think>\n\n</think> block in the prompt, so the generated text contains no thinking tag to strip. Do not pass --enable-thinking — it re-opens the exact failure that --reasoning-format none exists to prevent on the llama.cpp side.

One more thing that makes both servers safe here: fara-cli sends the tool schema inside the system prompt and passes no OpenAI tools array (the agent's call args are just {"temperature": 0} plus your extras), and it parses the raw text itself, splitting on <tool_call>. Servers that would otherwise rewrite <tool_call> blocks into a structured tool_calls field leave the text alone when no tools array is sent — which is why the harness's raw-text parser keeps working against a stack Microsoft never tested.

Results

  • Speed: omitted. No published measurement exists for Fara1.5-4B on an M4 Max, on any Apple Silicon chip, or on any consumer GPU; the only hardware the vendor names is datacenter class ("A6000, A100, H100, and B200 have been tested"), and forwarding a number from it would be misleading. Nor can one be forwarded across Apple chips: token generation is bandwidth-bound and this chip's 546 GB/s is a substantially larger budget than the ~400 GB/s M2 Max and M3 Max parts or the ~200 GB/s M2 Pro, so a figure from any of them would understate this machine — by an amount nobody has measured, which is exactly why no number appears here. If you run this, please contribute your numbers so this section can be replaced with a measurement.
  • Unified memory usage: 4.812 GiB of weights and projector; 13.008 GiB including KV and recurrent state at the full 262,144-token context, derived above from config.json and llama.cpp's memory formulas. That sits comfortably inside the ~32 GiB a 48GB M4 Max makes GPU-addressable. This is a derivation, not an observed peak — compute and vision buffers are additional. See /check/fara1-5-4b/m4-max.
  • Quality notes: the vendor's published scores are for the bf16 model on datacenter hardware. Q8_0 is a lossy quantisation of a model whose main job is predicting exact pixel coordinates, and no published evaluation measures how much grounding accuracy that costs at any tier. The advantage of this machine is that the fix is free: if clicks land near-but-not-on targets, switch to Fara1.5-4B-bf16.gguf with mmproj-Fara1.5-4B-bf16.gguf and re-test before blaming the model.
  • Language: English only — the card's out-of-scope list names "Languages other than English (training data is English-only)".

For the full benchmark data, see /check/fara1-5-4b/m4-max.

Troubleshooting

The server starts but the log says CLIP using CPU backend

The projector fell back to the CPU. Vision will still work, but slowly, and it means the GPU backend failed to initialise. Confirm you are on a Metal build (a source build without -DGGML_METAL=OFF, or the Homebrew formula) and that --mmproj points at a file that exists. clip.cpp prints this line at context creation, so it is visible before the first request — check it during step 4 rather than after a confusing agent run.

llama-server exits immediately when you add --mmproj

Check whether you have taken the GPU out of the picture. The one report in llama.cpp's tracker of an Apple M4 llama-server refusing to start with a --mmproj on the command line — issue #23631, a different vision-language model, exiting with code -5 — runs its repro with --device none, i.e. deliberately CPU-only, and the reporter is chasing a KleidiAI CPU path rather than Metal. It was closed by a staleness bot with no maintainer answer, so it is not evidence about the Metal path either way. The actionable inverse is simply: do not force a CPU-only device selection on a Mac. The command in step 3 passes no --device, which lets Metal be chosen, and the CLIP using <backend> backend line in the startup log is how you confirm it was.

ollama run fara does nothing — there is no Ollama entry

There is no Ollama library entry for Fara1.5 in any form: fara1.5, fara, fara1-5, fara1.5-4b and fara-4b all 404. 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. That older path is not trouble-free either: fara#58 is an open community report of fara-cli failing on its first model call against an Ollama-hosted Fara-7B, with the reporter suspecting the OpenAI image_url content type. Whatever server you choose, step 4's image test is the check that matters.

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 that step 4's curl describes the image. A text-only build 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. If you came here from MLX, check first that you are not on the OptiQ build under a stock loader, which is text-only by construction.

The agent dies on round one with an empty response

message.content is empty and everything is in a reasoning field. On llama.cpp that is the missing --reasoning-format none; on mlx_vlm.server it is --enable-thinking being passed. Both are the same underlying behaviour and both are described in step 3.

Clicks land in the wrong place

Check the viewport first. The harness defaults to 1440x900 because that is what the model was mostly trained at; 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. Only after both, suspect the quantisation — and on this machine, testing that is just a re-download at bf16.

Keyboard shortcuts do nothing on pages that expect them

A macOS-only asymmetry. The harness intercepts a fixed set of Control-based chords itself before they reach the browser — refresh, back, forward and find, in _BROWSER_CHROME_DISPATCH and _FIND_CHORD in environment.py — so those work on any OS. Other editing chords the model emits as Control+key are passed through to Chromium, where macOS expects Meta (Cmd) instead, and quietly do nothing. The key map in key_mapping.py does translate cmd to Meta, so a run that phrases the chord that way works; there is no setting to flip. Prefer tasks that do not lean on copy/paste shortcuts, and read data_point.json when an action seems to have been swallowed.

The output is a wall of @@@@ instead of an answer

Worth knowing what this is not. There is one Metal-specific defect in llama.cpp's multimodal path that produces exactly this symptom — issue #23986, where a pipeline/dispatch mismatch in Metal's im2col broke audio encoders from build b9433 onward — and it does not apply to Fara. A second contributor reproduced it at the op level on an M1 Pro and scoped it precisely: "it really is isolated to large-IC 1D conv", and "The 2D path already agrees so it stays unaffected." Fara's vision patch embedding is a 2D convolution, and the original reporter checked the vision half explicitly: "Image / vision inputs are also unaffected (verified with the same mmproj path on the latest build, b9453)." The issue was closed by a staleness bot rather than by a fix, so it is still live for audio models — but it is also the closest thing to independent community confirmation that llama.cpp's Metal mmproj path serves images correctly, which is worth having on a page that otherwise rests on a static kernel census.

Generation is slower than a 4B model should be

The Metal path for this architecture's linear-attention layers is newer than the CUDA one and still being tuned — an open maintainer PR (#25788) reports gains on qwen35 models on an Apple M3 from a single cache-fusion change, with a mixed table that includes regressions on some quants, and llama.cpp's own op-support table still lists GATED_DELTA_NET on Metal as "Partially supported by this backend". Build from a recent master before drawing conclusions, and compare two builds with llama-bench rather than trusting a single impression. This chip's bandwidth advantage does not help if the kernel is the bottleneck. No measurement for this chip exists either way — contribute one.

common questions
How much VRAM does Fara1.5-4B need?

About 24 GB — the minimum this recipe targets.

Which GPUs is Fara1.5-4B tested on?

Apple M4 Max (48 GB).

How hard is this setup?

Advanced — follow the steps above.