Exp 2026 06 13 Thread Count Big Cores - oRKLLM/ork-driver GitHub Wiki

Experiment โ€” Thread count & big-core scheduling (the integration prefill/decode bottleneck)

Date: 2026-06-13 ยท Status: ๐Ÿ† BEATS librkllmrt on decode at -t 4 ยท Path: ggml-ork integration

Hypothesis (Tier 1b)

CPU-affinity pinning fixed the standalone multi-core (mc_prof/rknn_vs_ork prefill 7665โ†’4365 ยตs/matmul, scaling 1.92ร—), but llama-bench pp512 stayed ~136. Why doesn't the integration benefit?

Diagnosis

Profiled the integration's prefill matmul (ORK_PROFILE=1, real long prompt, affinity ON):

ork profile: prefill (M>1) 193 matmuls, avgM 512.0, run 7596 us/matmul
ork profile: run_multicore 193 calls | setup 796 (10%) submit 6309 (83%) copy 490 (6%)

7596 ยตs/matmul โ€” the slow number, not the 4365 the standalone gets with the same code. So the ork worker threads are slow inside the integration specifically. Cause: llama.cpp's CPU threadpool busy-waits (spins) on all 8 cores while the single NPU matmul node runs, starving the 4 ork NPU-driver threads (calling thread + 3 pinned workers) that need the big cores. Plus the 4 little A55 cores in an -t 8 pool drag every CPU-side op (the threadpool waits on its slowest thread).

Benchmark (standardized โ€” 2026-06-13)

Qwen3-1.7B-w8a8, llama-bench, warm, DDR 2112 / CPU 2.4 GHz. Sweeping -t (CPU thread count):

-t pp512 (prefill) tg128 (decode)
8 (default, 4ร—A76+4ร—A55) 136.2 9.24
6 ~156 10.28
4 (big A76 cluster only) 155.1 12.84
2 132.8 โ€”
librkllmrt (closed, same model) 183.9 11.71

Findings

  1. -t 4 (big cores only) is optimal for both phases: prefill 136โ†’155 (+14%), decode 9.24โ†’12.84 (+39%).
  2. Decode at -t 4 (12.84) BEATS librkllmrt (11.71) โ€” the open stack is faster than the closed runtime on token generation when it isn't dragged by the little cores. Prefill reaches 84% of librkllmrt (155 vs 184).
  3. The integration's prefill was never NPU-matmul-bound โ€” it was CPU-thread contention + little-core drag. The affinity fix is necessary (workers must be on big cores) but insufficient while llama.cpp also spins 8 threads across all cores.
  4. -t 8 is a poor default on big.LITTLE: the 4 slow A55 cores hold back the threadpool barrier and fight the NPU-driver threads for the big cores.

Can it be automatic? (Tier 1d โ€” โŒ ruled out)

Tried making it automatic: pin the calling/main thread to the big cluster at ork_npu_init (before llama.cpp spawns its threadpool, so the pool inherits the mask โ€” the taskset -c <big> effect without the flag). Measured (no taskset):

-t pp512 tg128
4 156.6 12.72 (โœ“ unchanged โ€” already optimal)
8 132.3 2.28 (โœ— cratered)

It doesn't help -t 4 (already optimal) and craters -t 8 โ€” restricting 8 threads onto 4 cores oversubscribes 2:1 and thrashes (taskset's freely-migratable set behaves differently from our inherited-mask + single-core-worker mix). Reverted (ork-driver b9a5c4e); the driver pins only its own dedicated NPU-driver threads, never the whole process. Thread count is the user's lever.

Verdict

๐Ÿ† Run the open stack with -t <big-core-count> (4 on RK3588), not -t 8. That alone makes decode beat librkllmrt and lifts prefill to 84% โ€” and forcing it from the driver backfires (above), so it stays a serving/CLI choice. The remaining prefill gap is ork's single-core large-M matmul kernel (3.3ร— slower than RKNN's per core โ€” see Matmul apples-to-apples), now the main lever. The proper automatic fix is heterogeneous scheduling โ€” the CPU threadpool should sleep (not spin) while the NPU matmul runs, and the thread count should match the big cluster โ€” but that belongs in the llama.cpp threadpool / serving layer, not a process-wide affinity hammer. See Optimization Roadmap.

โš ๏ธ **GitHub.com Fallback** โš ๏ธ