self-hosted/ai
§01·recipe · multimodal

Fara1.5-4B on Apple M2 Pro: Browser Computer-Use Agent in 16GB Unified Memory

multimodaladvanced16GB+ VRAMJul 31, 2026

This advanced recipe sets up Fara1.5-4B on the Apple M2 Pro, needing about 16 GB of VRAM.

models
tools
prerequisites
  • Apple M2 Pro (16GB unified memory) or any Apple Silicon Mac with 16GB+ 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) on the smallest Apple Silicon configuration this site covers: 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 M2 Pro (16GB unified memory) · Q8_0 + f16 projector = 4.812 GiB of weights, 7.007 GiB total at a 65,536-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. And a 16GB Mac is even more likely to be somebody's only machine. 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
GPU16GB unified memory (Apple Silicon, Metal)Apple M2 Pro, 16GB 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

On a 16GB Mac fit IS the axis — and the sibling configuration does not fit

This is the tightest Apple envelope on the site, and it is the one Apple configuration where the fit question is real rather than rhetorical. Two facts have to be put together honestly.

First, Apple has no dedicated VRAM, and the GPU does not get all of unified memory. Metal's recommendedMaxWorkingSetSize is the practical ceiling that MLX, llama.cpp-Metal and Ollama treat as a hard cap, and below 64GB it lands near 66% of unified memory. Memory is sold in binary units, so this machine's 16 GB is 16 GiB of physical RAM and roughly 10.5 GiB is GPU-addressable by default. So /check's raw 16 GB figure is optimistic by about a third, and every number below is compared against ~10.5 GiB, not against 16. Every budget on this page is stated in GiB so that the comparison is like-for-like; where a decimal GB figure appears it is labelled.

Second, the 64GB M2 Max recipe for this model runs Q8_0 at the model's full 262,144-token context, which is 13.008 GiB. That does not fit here, and no amount of tuning makes it fit. The interesting question is therefore not "does Fara1.5-4B run on an M2 Pro" — a 4B model obviously runs — but which two of quant tier, context window and reference precision you keep.

Weights — byte counts from the bartowski GGUF tree:

ComponentBytesGiB
Fara1.5-4B-Q4_K_M.gguf2,884,850,7842.687
Fara1.5-4B-Q8_0.gguf4,493,954,1444.185
Fara1.5-4B-bf16.gguf8,424,393,5687.846
mmproj-Fara1.5-4B-f16.gguf672,423,5840.626
mmproj-Fara1.5-4B-bf16.gguf675,569,3120.629
Q4_K_M pair3,557,274,3683.313
Q8_0 pair5,166,377,7284.812
bf16 pair9,099,962,8808.475

KV cache — and this is where Fara1.5 is unusual, and where the context decision becomes the cheap lever rather than the expensive one. 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 65,536 tokens                                  = 2.00 GiB
x 262,144 tokens (the model's full context)      = 8.00 GiB

That 4x difference between 65,536 and 262,144 is 6 GiB on a 10.5 GiB budget, which is the whole story of this page.

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 small term either way; it is stated at its real size here because on this machine small terms are the ones that decide a borderline configuration.

Totals against a ~10.5 GiB addressable pool:

ConfigWeights + projectorKVRecurrentTotalFits
Q4_K_M + f16 mmproj @ 65,5363.3132.0000.1965.509yes
Q4_K_M + f16 mmproj @ 131,0723.3134.0000.1967.509yes
Q8_0 + f16 mmproj @ 65,5364.8122.0000.1967.007yes — this recipe
Q8_0 + f16 mmproj @ 131,0724.8124.0000.1969.008too tight
Q8_0 + f16 mmproj @ 262,1444.8128.0000.19613.008no
bf16 + bf16 mmproj @ 65,5368.4752.0000.19610.671no

Compute buffers and the vision graph are not in the table — they are allocated at load time and this recipe does not derive them. That omission is why 9.008 GiB against ~10.5 GiB is listed as "too tight" rather than "yes": roughly 1.5 GiB of unaccounted headroom on a machine that is also running macOS is not a margin worth spending an evening on. The lead configuration leaves about 3.5 GiB instead. Read the buffer-size lines llama-server prints on startup for the real figures on your build, and report what you see.

What this machine keeps, and what it gives up. It keeps the quant tier: Q8_0, the same one the 64GB sibling leads with, so nothing about grounding quality is being rationed. What it gives up is the other two axes — the full 262,144-token window drops to 65,536, and the bf16 reference build is simply unavailable, since its weights alone are 8.475 GiB before a single KV byte. On a big Mac, "switch to bf16 and re-test" is the free fix for a suspected quantisation problem; here it is not an option, which is worth knowing before you hit a grounding failure and go looking for it.

Is 65,536 enough for this harness? For the agent loop, yes, with room to spare. The repo's own floor is the lower bound: "Please ensure that context length is set to at least 15000 tokens and temperature to 0 for best results." Per-round user text is small and bounded — a URL, one fixed instruction, and at most three screenshots — so the variable term is the model's own accumulated reasoning and action history. 65,536 is over four times the documented floor. If you run very long trajectories and start seeing the context fill, the cheaper trade on this machine is Q4_K_M at 131,072 (7.509 GiB) rather than pushing Q8_0 up — but understand what you are buying: Q4_K_M is a coarser quantisation of a model whose output is pixel coordinates, and no published evaluation measures that cost. Prefer the shorter window at the higher quant.

No sudo sysctl iogpu.wired_limit_mb raise is needed, and reaching for one here is a mistake. The lead configuration sits ~3.5 GiB under the default cap. It is tempting to raise the cap to chase the full 262,144-token window, but that needs ~13 GiB wired out of 16 GB, leaving about 3 GB for macOS and every other process — which is the swapping-and-instability regime the raise is documented to avoid. On a 16GB machine the honest move is the shorter context, not a bigger wired limit.

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 65,536-token window this recipe sets, screenshots are under 6% of the budget — so shortening the window costs you history, not eyes.

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 — which is why a recent build is worth the extra five minutes. 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, 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 65536 --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 65536. The context this machine can actually afford, costing 2.00 GiB of KV per the table above. This is the one number that differs from the 64GB Apple sibling, which runs the model's native 262,144. Do not raise it to 131072 on a 16GB Mac unless you have read the startup buffer lines and know you have the room; do not raise it to 262144 at all.
  • -ngl 99. Explicit rather than necessary — a Metal build already runs the computation on the GPU by default, per docs/build.md above.

One thing you will notice is missing, deliberately: there is no --cache-type-k q8_0 KV-quantisation step. It exists, and on a tighter machine it is a real lever — but the reason to reach for it would be to buy back context, and at 2.00 GiB of KV the cheaper trade here is the context table above, at no cost to the precision of a model whose job is coordinates. Keep it in reserve for the day you genuinely need a longer window.

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.

One 16GB-specific note on this step: Chromium is not free. A browser with a couple of tabs open is comfortably a gigabyte of system memory, and on a unified-memory machine that is the same pool the model is using. Close what you do not need before a run, and prefer the headless default while you are testing.

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. Do budget for it on this machine: a container runtime plus Chromium plus a 7 GiB model is a lot to ask of 16 GB.

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, and on this machine that matters more than anywhere else. bartowski ships 24 quant tiers from IQ2_M up to full bf16, each with a matching projector. On a 64GB Mac the ladder is a nicety; on 16 GB it is the mechanism by which the context table above exists at all.

If you would rather stay in MLX, the parallel path is real and short — and its 4-bit weights are the smallest complete artifact for this model at 3.69 GiB on disk:

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. Three 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.
  • The memory budget on this page does not transfer to it. Every figure above is llama.cpp's memory plan for a GGUF, derived from llama.cpp's own formulas. MLX allocates its cache differently and this recipe has not derived it, so treat the smaller weight file as an invitation to measure rather than a proven win — and tell us what you see.

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 M2 Pro, 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. Neither does a figure from another Mac transfer: the M2 Pro's ~200 GB/s of unified-memory bandwidth is roughly half what the Max parts have, and token generation is bandwidth-bound, so a Max measurement would flatter this machine by about a factor of two. 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; 7.007 GiB including KV and recurrent state at the 65,536-token context this recipe sets, derived above from config.json and llama.cpp's memory formulas. That sits about 3.5 GiB inside the ~10.5 GiB a 16GB M2 Pro makes GPU-addressable. This is a derivation, not an observed peak — compute and vision buffers are additional. See /check/fara1-5-4b/m2-pro.
  • 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. Unlike the larger Macs, this one cannot answer the question by switching to bf16 — those weights are 8.475 GiB before any KV — so if clicks land near-but-not-on targets, the diagnostic order is viewport first, --image-min-tokens second, and quantisation only as a hypothesis you cannot cheaply test here.
  • 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/m2-pro.

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.

It loads, then the machine starts swapping

The most likely 16GB-specific failure, and it is a system-level symptom rather than a model error: the model fits the GPU-addressable pool but the machine has nothing left. Watch Activity Monitor's Memory-Pressure gauge during a run. Close Chromium tabs you are not using, quit other heavy apps, and if it persists, drop to Fara1.5-4B-Q4_K_M.gguf (2.687 GiB instead of 4.185 — same projector) or halve the context to -c 32768, which is still twice the harness's documented 15,000-token floor. Raising iogpu.wired_limit_mb is the wrong instrument here: it moves the boundary in the direction that makes swapping more likely, not less.

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 note that on this machine you cannot cheaply rule it out by stepping up to bf16, so treat it as the last hypothesis rather than the first.

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

Two things, in this order. First, this chip: at ~200 GB/s (Apple tech specs) the M2 Pro has roughly half the unified-memory bandwidth of the Max parts, and token generation is bandwidth-bound, so "slower than the Mac I read about" is often just correct. Second, the software: 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. No measurement for this chip exists either way — contribute one.

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

About 16 GB — the minimum this recipe targets.

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

Apple M2 Pro (16 GB).

How hard is this setup?

Advanced — follow the steps above.