Threading
High performance inference requires that we use multiple threads when executing on a multicore processor. llama.cpp has functions to calculate how many threads to use per model. yzma then uses this information to set how many threads it uses, and on which cores they run.
How llama.cpp uses processor cores
Each token is a graph of many matrix operations. llama.cpp divides each operation into equal parts, one for each thread. All threads must finish an operation before the next operation starts. So the slowest thread sets the speed.
flowchart TD
token["One token"] --> ops["Graph of matrix operations"]
ops --> split["Divide one operation into equal parts"]
subgraph cpu["CPU"]
subgraph p1["Performance core"]
t1["Thread 1"]
end
subgraph p2["Performance core"]
t2["Thread 2"]
end
subgraph p3["Performance core"]
t3["Thread 3"]
end
subgraph e1["Efficiency core"]
t4["Thread 4, slow"]
end
end
split --> t1
split --> t2
split --> t3
split --> t4
t1 --> wait["Wait for all threads"]
t2 --> wait
t3 --> wait
t4 --> wait
wait -- "next operation" --> split
Two conditions make a thread slow:
- The thread is on an efficiency core.
- Two threads share one physical core. The two CPUs of one core share the same arithmetic units.
Too few threads also make inference slow, because cores stay idle. So the best number is one thread for each physical performance core.
How yzma uses threads
flowchart TD
subgraph os["Operating system"]
cores["Core data<br/>sysfs on Linux, sysctl on macOS"]
end
subgraph yzma["pkg/llama"]
threads["Threads()"]
perf["PerformanceCPUs()"]
mthreads["ModelThreads()"]
params["InitFromModel()<br/>NThreads, NThreadsBatch"]
pool["NewPerformanceThreadpool()"]
attach["AttachThreadpool()"]
end
subgraph native["llama.cpp"]
ctx["Context"]
free["Threads that the system puts on a core"]
pinned["One thread pinned to each performance core"]
end
cores --> threads
cores --> perf
threads --> mthreads
mthreads --> params
threads --> params
params --> ctx
perf --> pool
pool --> attach
attach --> ctx
ctx -- "no pool" --> free
ctx -- "pool attached" --> pinned
The default number of threads
By default llama.cpp requests four threads. This is slow on a machine with many cores. yzma calculates a more efficient number based on the available system resources.
llama.Threads() returns one thread for each core that does the arithmetic well. That is one thread for each physical core. On a machine with performance cores and efficiency cores, it counts only the performance cores.
| System | How yzma counts the cores |
|---|---|
| Linux | It reads sysfs. /sys/devices/cpu_core/cpus gives the performance cores, and thread_siblings_list removes each core’s second CPU. |
| macOS | It reads hw.perflevel0.physicalcpu, which is the number of performance cores on Apple Silicon. An Intel Mac gives hw.physicalcpu. |
| Windows | It calls GetLogicalProcessorInformationEx, which gives one record for each physical core, and leaves out the efficiency cores. |
| Other systems | Half of the logical CPUs. A machine with four logical CPUs or fewer gets all of them. |
On Linux, macOS and Windows, yzma also uses half of the logical CPUs when the system does not return a specific value.
The prompt runs well on all of these threads. Generating one token is different, because memory speed limits it. So a small model does not need as many threads.
So llama.ModelThreads(model) gives a count for generation that comes from the model size. It is one thread for each 80 MiB of weights that a token reads, at least 4 and at most llama.Threads(). A MoE model reads only the experts that it uses, so only those count.
These calls set the defaults:
llama.ContextDefaultParams()setsNThreadsBatchtollama.Threads()and leavesNThreadsat 0.llama.InitFromModel()sets anNThreadsof 0 tollama.ModelThreads(model).mtmd.ContextParamsDefault()setsThreadstollama.Threads().
Changing the number of threads
If you want to change the number of threads used, set these two values before creating the context.
params := llama.ContextDefaultParams()
params.NThreads = 8
params.NThreadsBatch = 8
ctx, err := llama.InitFromModel(model, params)
NThreads controls the generation per token. NThreadsBatch controls the prompt, and is used for batches with multiple tokens.
To change these values after you have created the context, use llama.SetNThreads(ctx, nThreads, nThreadsBatch). The functions llama.NThreads(ctx) and llama.NThreadsBatch(ctx) return the current values for an existing context.
For a multimodal model, set Threads in the parameters of mtmd as well.
The native examples have a -t flag. The value 0 uses the count from the model size.
Pin each thread to a core
The operating system can move a thread from one core to a different core. On a machine with two kinds of cores, it can put a thread on an efficiency core. If this happens, that thread will be slow for inference. A thread pool with a CPU mask prevents this.
llama.NewPerformanceThreadpool() creates a pool with one thread for each performance core. Each thread stays on a CPU of its own.
mparams := llama.ModelDefaultParams()
mparams.SetCPUOnly()
model, err := llama.ModelLoadFromFile(modelFile, mparams)
if err != nil {
return err
}
ctx, err := llama.InitFromModel(model, llama.ContextDefaultParams())
if err != nil {
return err
}
tp, err := llama.NewPerformanceThreadpool()
if err == nil {
llama.AttachThreadpool(ctx, uintptr(tp), uintptr(tp))
}
// Use the context.
llama.DetachThreadpool(ctx)
llama.Free(ctx)
llama.ThreadpoolFree(tp)
Follow these rules:
- Call
llama.Init()before you create a pool. The CPU backend supplies the pool calls, and they exist only after the backend starts. - Use
ModelParams.SetCPUOnly()for the model. Do not set the CPU in a device list withSetDevices. Using a device list makesllama.cppstart a second CPU backend, and the pool then gets no work. - Detach the pool from each context before you free the pool.
The call can return two errors.
| Error | Meaning |
|---|---|
llama.ErrNoPerformanceCPUs |
The system cannot tell which CPUs to use. |
llama.ErrNoThreadpool |
This build of llama.cpp cannot create pools. |
Only Linux returns info for the CPUs. macOS does not let a thread select a CPU. Windows gives the cores, but yzma does not pin threads there. On those systems, just use the default threads.
Create your own thread pool
To select the CPUs yourself, set the parameters and then create the pool.
params := llama.ThreadpoolParamsDefault(4)
params.SetCPUs([]int32{0, 2, 4, 6})
params.Prio = llama.SchedPriorityHigh
tp, err := llama.ThreadpoolNew(¶ms)
SetCPUs sets the CPUs in the mask and calls StrictCPU, which gives each thread one CPU. llama.PerformanceCPUs() returns the list that NewPerformanceThreadpool uses.
Goroutines
The mtmd package knows which calls are safe to use from more than one goroutine.
| Call | Safe from multiple goroutines |
|---|---|
mtmd.Tokenize |
Yes, with a shared context. |
mtmd.EncodeChunk |
No. |
mtmd.HelperEvalChunks |
yzma lets only one call run at a time for each context. Calls on different contexts run at the same time. |
In the browser
There are three builds of llama.cpp for WebAssembly. The loader selects one of them.
| Build | Threads |
|---|---|
| WebGPU | 1. The GPU does the work. |
| CPU with multithreading | The logical CPUs of the machine, from 1 to 16. |
| CPU with single threading | 1. |
The multithreaded build needs SharedArrayBuffer. The browser passes it only to a page that has the Cross-Origin-Opener-Policy header and the Cross-Origin-Embedder-Policy header. Without them, the loader selects the single threaded build.
These llamawasm calls tell you what the loader selected.
Threaded()tells if the module uses multithreading.Threads()returns the number of threads.ContextDefaultParamsandMtmdContextParamsDefaultset this value.Backend()returnswebgpu,cpu-threads, orcpu.
llamawasm.SetNThreads changes the thread count of a context after you create it.
Only one goroutine can call into the module at a time, because the package keeps scratch memory in the module. Each call is synchronous, so put the code in a Web Worker and not on the main thread. For more, see the browser guide.
See the result on your machine
The systeminfo example shows the logical CPUs, the number from llama.Threads(), the performance CPUs, and if a thread pool is possible.