Repository navigation
Step 20: Multi-threaded CPU inference (#36) - #39
Conversation
Custom zero-alloc ComputeThreadPool with delegate* dispatch for compute-bound parallelism. Row-partitioned GEMV/GEMM, head-partitioned attention, --threads CLI option. Bit-identical output at any thread count. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
@codex please review this PR thoroughly - performance issues and possible improvements, potential bugs, code quality and architecture |
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
@gemini please review this PR thoroughly - performance issues and possible improvements, potential bugs, code quality and architecture |
|
Gemini ✦ This is an exceptionally well-engineered PR. The architectural choices—specifically the zero-allocation I have reviewed the code for correctness, performance, and .NET best practices. Below are a few critical safety catches and minor performance suggestions. 🚨 Critical: Worker Exception DeadlockIn Fix: Wrap the worker execution in a // src/DotLLM.Cpu/Threading/ComputeThreadPool.cs - Line 126
try
{
_workFn(_context, threadIdx, _threadCount);
}
catch (Exception ex)
{
// Optional: Store 'ex' in a shared array to rethrow on the caller thread
Environment.FailFast("Compute worker crashed", ex);
}
finally
{
_completion.Signal();
}⚡ Performance & Architecture Notes
🧹 Minor Code Quality Tweaks
Verdict: Outstanding work. Add the |
Wrap _workFn call in try/catch/finally so _completion.Signal() is always reached. Catch uses Environment.FailFast since any exception in unmanaged kernel code indicates corrupted state. Addresses Gemini review feedback. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Closes #36
Summary
ComputeThreadPool— N-1 background workers + caller = N total threads,delegate*<nint, int, int, void>function pointer dispatch with stack-allocated context structs,ManualResetEventSlim+CountdownEventsynchronization. Zero managed allocations on the hot path.ComputeThreadPool?. GEMV partitions M rows across threads (4-row aligned for VecDot batching). GEMM parallelizes the tile loop. Falls back to single-threaded when pool is null or below threshold (m < 32).Executeoverload distributes heads across threads, each with its own scores buffer from per-worker scratch memory. Falls back when pool is null or numHeads < 2.--threadsCLI option — Default 1 (single-threaded), 0 = auto (Environment.ProcessorCount). Wired throughThreadingConfig→InferenceOptions→LlamaModel.Benchmarks
Machine: AMD Ryzen 7 5800HS (8C/16T), .NET 10.0.3, AVX2
GEMV (M=K=4096, single-token decode)
Q8_0 scales near-linearly — compute-bound, data fits L3 cache. F32 is memory-bandwidth bound at 64MB weight matrix.
GEMM (M=K=4096, N=64, prefill)
Tile parallelism gives each thread a cache-friendly working set — both formats scale well.
Attention (32 heads, 8 KV heads, headDim=64, seqKv=128)
End-to-end model inference
SmolLM-135M Q8_0 (
--threads 8, temperature 0, 50 tokens):Llama-3.2-1B Q8_0 (
--threads 8, temperature 0, 20 tokens):Files Changed
src/DotLLM.Core/Configuration/ThreadingConfig.csreadonly record structfor thread count configsrc/DotLLM.Core/Configuration/InferenceOptions.csThreadingpropertysrc/DotLLM.Cpu/Threading/ComputeThreadPool.csdelegate*dispatchsrc/DotLLM.Cpu/Kernels/MatMul.cssrc/DotLLM.Cpu/Kernels/Attention.cssrc/DotLLM.Models/Architectures/LlamaModel.cssrc/DotLLM.Cli/Commands/RunCommand.cs--threadsCLI optiontests/.../MatMulParallelTests.cstests/.../AttentionParallelTests.csbenchmarks/.../ParallelBenchmarks.csdocs/ROADMAP.mdREADME.mdTest plan
dotnet test) — 263 existing + 26 new parallel correctness testsnullproduces identical results to explicit single-threaded--threads 1vs--threads 8produce identical token output (temperature 0)🤖 Generated with Claude Code