
## Why ollama used 4 threads — the cause, with evidence

**Cause: llama.cpp's own default thread count. Ollama never sets one. llama.cpp's
`common_cpu_get_num_math()` walks the logical CPUs of a hybrid part, skips the efficiency
cores, and after each core it counts performs an *unconditional* `++cpu` to "skip the
hyperthread sibling". On a hybrid CPU with no SMT that step steps over a real performance
core, halving the count.**

### The four receipts

**1. Ollama passes no thread flag.** The argv it launches for a request with no
`num_thread` option contains zero occurrences of `-t` or `--threads`:

```
llama-server --model <blob> --port 43183 --host 127.0.0.1 --no-webui --offline
  -c 4096 -np 1 --log-verbosity 4 --no-log-prefix --no-log-timestamps --no-jinja
  --chat-template chatml --mmproj <blob> --load-mode none --flash-attn auto
  -b 512 -ub 512 --context-shift --keep 4
```

Give the same server `options.num_thread` and it appends `-t N`. So the option reaches the
runner, and the *default* is llama.cpp's own.

**2. The runner's own line**, from that default request on Rig B:

```
cmn  common_param: system_info: n_threads = 4 (n_threads_batch = 4) / 24 | CPU : ... AVX2 = 1 ...
cmn          init: llama threadpool init, n_threads = 4
```

**3. The deciding function is in the shipped binary.** `libllama-common.so.0.1.2` on both
boxes exports `_Z23common_cpu_get_num_mathv` — `common_cpu_get_num_math()`.

**4. Its inputs, read from the hardware.** `coretype.c` pins to every logical CPU, reads
`CPUID.7.EDX[15]` (hybrid) and `CPUID.0x1A EAX[31:24]` (0x40 = Core/P, 0x20 = Atom/E),
then replays the heuristic:

| box | hybrid | P logical | E logical | SMT on P | **physical P-cores** | heuristic returns | ollama used |
|---|---|---|---|---|---|---|---|
| test Rig B laptop | yes | 8 (cpu0–7) | 16 (cpu8–23) | **none** | **8** | **4** | **4** |
| the mini PC | yes | 8 (cpu0–7, 4 sibling pairs) | 8 (cpu8–15) | yes | **4** | **4** | **4** |
| the server | **no** | — | — | yes | 32 | not hybrid → physical cores = **32** | **32** |

### The discriminating test

Two hypotheses explain the mini PC's 4 — "it counts performance cores" and "it counts
performance cores, then halves for SMT". They disagree on Rig B, which has **8** physical
performance cores and **no SMT at all**: the first predicts 8, the second predicts 4.
Ollama used 4. Only the second survives. The server, not hybrid, never enters that path and
lands on its physical core count — confirming the fallback branch independently.

So the same number, 4, arrives on the two Intel boxes by two different routes: on the mini
PC it is the correct performance-core count (the loss is only the 8 efficiency cores it
declines to use), and on Rig B it is half the performance-core count before the efficiency
cores are even considered.

### Ruled out, each with a receipt

| candidate cause | Rig B | the mini PC | verdict |
|---|---|---|---|
| cgroup `cpu.max`, every level above the serve process | `max 100000` | `max 100000` | no quota anywhere |
| CPU affinity of the serve process | `0-23` (all 24) | `0-15` (all 16) | unrestricted |
| systemd `CPUQuotaPerSecUSec` / `AllowedCPUs` / `CPUAffinity` | `infinity` / empty / empty | n/a (plain `setsid` process) | no unit limit |
| `OLLAMA_NUM_THREAD`, `OMP_NUM_THREADS`, `GOMP_*` in the serve environment | none set | none set | no env override |

Full text of receipt 4: `receipts/coretype-cpu-laptop.txt`, `receipts/coretype-cpu-mini.txt`
— both shipped here byte for byte.

**Two receipts named above are NOT in this kit, and this says so rather than pointing at
nothing.** The `diagnosis-cpu-*.txt` files behind receipts 1 and 3 (the cgroup, affinity,
systemd and environment ruling-out table, the launched argv, and the runner's own
`n_threads = 4` line) are raw shell captures from the two machines: they carry hostnames,
absolute filesystem paths, a serving port, and each box's full `lscpu` model string. Nothing
in them could be published without rewriting every line, at which point they would stop being
raw captures. Their *content* — the argv, the runner line, the symbol, and the ruled-out table
— is reproduced verbatim in the four receipts above and in this file. If you want to redo the
check on your own machine, the recipe is in `README.md`.

