-
Notifications
You must be signed in to change notification settings - Fork 216
perf(cpu): raise GDN recurrence lane cap 4 -> 8 #694
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,8 +18,11 @@ using mllm::cpu::gdn::gatedDeltaRuleF32; | |
| class ScopedCpuOpThreads { | ||
| public: | ||
| explicit ScopedCpuOpThreads(int32_t thread_count) : original_thread_count_(mllm::Context::instance().getCpuOpThreads()) { | ||
| // initializeContext() registers the CPU backend; SymbolTable::reg aborts on | ||
| // a duplicate key, so call it exactly once (the tests have no fixture init). | ||
| static const bool kContextInitialized = [] { mllm::initializeContext(); return true; }(); | ||
| (void)kContextInitialized; | ||
| mllm::Context::instance().setCpuOpThreads(thread_count); | ||
|
Comment on lines
20
to
25
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
rg -n -C 8 '\bsetCpuOpThreads\s*\(' mllm tests
rg -n -C 8 '\binitThreadPool\s*\(' mllmRepository: UbiquitousLearning/mllm Length of output: 7100 🏁 Script executed: #!/bin/bash
set -euo pipefail
fd -a 'CPUBackend.hpp|CPUBackend.cpp|mllm.hpp|Qwen35GDNTest.cpp' . | sed 's#^\./##' | sort
printf '\n--- mllm.cpp nearby ---\n'
sed -n '1,60p' mllm/mllm.cpp
printf '\n--- CPUBackend.h/cpp pool usage ---\n'
sed -n '1,180p' mllm/backends/cpu/CPUBackend.hpp
sed -n '1,180p' mllm/backends/cpu/CPUBackend.cpp
printf '\n--- locate HpcThreadPool definitions/usages ---\n'
rg -n -C 5 'class HpcThreadPool|struct HpcThreadPool|HpcThreadPool|--set-cpu-op-threads|cpu_op_thread|CpuOpThreads|eight|8 workers|workers' .Repository: UbiquitousLearning/mllm Length of output: 50380 🏁 Script executed: #!/bin/bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
import re
files = {
"CPUBackend.hpp": Path("mllm/backends/cpu/CPUBackend.hpp").read_text(),
"CPUBackend.cpp": Path("mllm/backends/cpu/CPUBackend.cpp").read_text(),
"mllm.hpp": Path("mllm/mllm.hpp").read_text(),
"Context.cpp": Path("mllm/engine/Context.cpp").read_text(),
"Qwen35GDNTest.cpp": Path("tests/cpu/Qwen35GDNTest.cpp").read_text(),
}
for name, text in files.items():
print(f"\n--- {name} relevant lines ---")
for n,line in enumerate(text.splitlines(), 1):
if re.search(r'\b(setCpuOpThreads|getCpuOpThreads|initThreadPool|initializeContext|MLLM_CONDITIONAL_PARALLEL_FOR|scoped_thread_pool|thread_pool_|__threadPoolDestroy)', line):
print(f"{name}:{n}: {line}")
for n,line in enumerate(files["Qwen35GDNTest.cpp"].splitlines(), 1):
if "ScopedCpuOpThreads" in line or "kEightThreads" in line or "kCpuOpThreads" in line or "ASSERT_FLOAT_EQ" in line:
print(f"{name}:{n}: {line}")
PYRepository: UbiquitousLearning/mllm Length of output: 2950 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- CPU backend thread-count API definitions/usages ---'
rg -n -C 4 'thread_count|thread_cnt|num_threads|cpu_op_thread|CpuOpThreads|ConditionalParallel|conditional|parallel|thread_pool_->|getThreadPool\(\)' mllm/backends mllm/common mllm/engine mllm/mllm.hpp
printf '%s\n' '--- HpcThreadPool definitions/usages ---'
rg -n -C 4 'class HpcThreadPool|struct HpcThreadPool|HpcThreadPool' mllm
printf '%s\n' '--- relevant Qwen35GDNTest setup ---'
sed -n '1,60p' tests/cpu/Qwen35GDNTest.cpp
sed -n '250,330p' tests/cpu/Qwen35GDNTest.cppRepository: UbiquitousLearning/mllm Length of output: 50380 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- test thread count macros/constants ---'
sed -n '1,40p' tests/cpu/Qwen35GDNTest.cpp
printf '%s\n' '--- MLLM_CONDITIONAL_PARALLEL_FOR definition/usages (top-level files) ---'
rg -n '\bMLLM_(CONDITIONAL_)?PARALLEL_FOR|MLLM_AUTO_PARALLEL_FOR|MLLM_KERNEL_USE_THREADS_VENDOR_MLLM|struct\s+HpcThreadPool|class\s+HpcThreadPool' \
-g '*.{h,hpp,cpp,h.in,cmake,txt,md}' \
mllm tests | head -n 200
printf '%s\n' '--- CPUBackend exact implementation ---'
sed -n '88,102p' mllm/backends/cpu/CPUBackend.cpp
printf '%s\n' '--- Context setCpuOpThreads exact implementation ---'
sed -n '158,164p' mllm/engine/Context.cpp
printf '%s\n' '--- initializeContext exact implementation ---'
sed -n '145,158p' mllm/mllm.hpp
printf '%s\n' '--- Qwen35GDNTest relevant constructor and scoped-threads calls ---'
sed -n '18,28p' tests/cpu/Qwen35GDNTest.cpp
sed -n '256,269p' tests/cpu/Qwen35GDNTest.cpp
sed -n '312,346p' tests/cpu/Qwen35GDNTest.cppRepository: UbiquitousLearning/mllm Length of output: 22515 🏁 Script executed: #!/bin/bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
import re
text = Path("mllm/core/Parallel.hpp").read_text()
needle = "MLLM_KERNEL_USE_THREADS_VENDOR_MLLM"
idx = text.find(needle)
print("MLLM_KERNEL_USE_THREADS_VENDOR_MLLM at", idx)
if idx != -1:
print(text[max(0, idx-400):idx+700])
print("\nmacro branch with MLLM_KERNEL_USE_THREADS_VENDOR_MLLM:")
for i,line in enumerate(text.splitlines(), 1):
if re.search(r'#(?:ifndef|define) ' + re.escape(needle)|MLLM_CONDITIONAL_PARALLEL_FOR|`#define` MLLM_KERNEL_USE_THREADS_VENDOR_MLLM|`#endif`\s*(//)?\s*?{}'.format(re.escape(needle).replace("_", r"\_")), line):
print(f"{i}: {line}")
for i,line in enumerate(Path("tests/cpu/Qwen35GDNTest.cpp").read_text().splitlines(), 1):
if re.match(r'\s*constexpr int kThreadCount\s*=', line) or re.search(r'mllm\.cmake|MLLM_KERNEL_USE_THREADS_VENDOR_MLLM|kThreadCount', line):
print(f"tests/cpu/Qwen35GDNTest.cpp:{i}: {line}")
PY
printf '%s\n' '--- GDN implementation around parallel macro ---'
sed -n '340,380p' mllm/backends/cpu/kernels/common/gdn/gated_delta_net.cpp
printf '%s\n' '--- exact hpc threadpool definition excerpt ---'
sed -n '33,120p' mllm/engine/HpcThreadPool.hppRepository: UbiquitousLearning/mllm Length of output: 403 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- Parallel.hpp around MLLM_KERNEL_USE_THREADS_VENDOR_MLLM ---'
rg -n -C 20 'MLLM_KERNEL_USE_THREADS_VENDOR_MLLM|MLLM_CONDITIONAL_PARALLEL_FOR' mllm/core/Parallel.hpp
printf '%s\n' '--- GDN parallel loop macro usage ---'
sed -n '340,380p' mllm/backends/cpu/kernels/common/gdn/gated_delta_net.cpp
printf '%s\n' '--- HpcThreadPool definition excerpt ---'
sed -n '20,120p' mllm/engine/HpcThreadPool.hpp
printf '%s\n' '--- Qwen35GDNTest constants and flag references ---'
rg -n -C 6 'kThreadCount|kThread|MLLM_KERNEL_USE_THREADS_VENDOR_MLLM|CPU backend|eight' tests/cpu/Qwen35GDNTest.cppRepository: UbiquitousLearning/mllm Length of output: 15390 Set the CPU pool count before creating the context.
🤖 Prompt for AI Agents |
||
| mllm::initializeContext(); | ||
| } | ||
|
|
||
| ~ScopedCpuOpThreads() { mllm::Context::instance().setCpuOpThreads(original_thread_count_); } | ||
|
|
@@ -218,7 +221,10 @@ TEST(Qwen35GDNTest, ParallelBatchValueHeadsMatchSerialBitwise) { | |
| constexpr int kValueHeads = 32; | ||
| constexpr int kKeyDim = 128; | ||
| constexpr int kValueDim = 128; | ||
| constexpr int kThreadCount = 4; | ||
| // Exercises the parallel lane partition up to the 8-lane cap | ||
| // (kMaxParallelGDNLanes); tasks are disjoint so output must be bitwise | ||
| // identical regardless of how many lanes the scheduler picks. | ||
| constexpr int kThreadCount = 8; | ||
|
|
||
| std::vector<float> q(kBatch * kSequence * kKeyHeads * kKeyDim); | ||
| std::vector<float> k(q.size()); | ||
|
|
@@ -266,4 +272,78 @@ TEST(Qwen35GDNTest, ParallelBatchValueHeadsMatchSerialBitwise) { | |
| } | ||
| } | ||
|
|
||
| // 4B real geometry (B=1, S=69, 16 key heads, 32 value heads, 128 dims) at the | ||
| // 8-lane cap — exercises the full task fan-out (32 tasks) that the small | ||
| // geometry above does not. Guards against the device crash observed on | ||
| // OnePlus with the 8-lane product build. | ||
| TEST(Qwen35GDNTest, FourBGeometry8LaneDoesNotCrash) { | ||
| constexpr int kBatch = 1; | ||
| constexpr int kSequence = 69; | ||
| constexpr int kKeyHeads = 16; | ||
| constexpr int kValueHeads = 32; | ||
| constexpr int kKeyDim = 128; | ||
| constexpr int kValueDim = 128; | ||
| constexpr int kThreadCount = 8; | ||
|
|
||
| std::vector<float> q(kBatch * kSequence * kKeyHeads * kKeyDim); | ||
| std::vector<float> k(q.size()); | ||
| std::vector<float> v(kBatch * kSequence * kValueHeads * kValueDim); | ||
| std::vector<float> a(kBatch * kSequence * kValueHeads); | ||
| std::vector<float> b(a.size()); | ||
| std::vector<float> a_log(kValueHeads); | ||
| std::vector<float> dt_bias(kValueHeads); | ||
|
|
||
| for (std::size_t i = 0; i < q.size(); ++i) { | ||
| q[i] = 0.01F * static_cast<float>(static_cast<int>(i % 7) - 3); | ||
| k[i] = 0.01F * static_cast<float>(static_cast<int>(i % 5) - 2); | ||
| } | ||
| for (std::size_t i = 0; i < v.size(); ++i) { v[i] = 0.01F * static_cast<float>(static_cast<int>(i % 11) - 5); } | ||
| for (std::size_t i = 0; i < a.size(); ++i) { | ||
| a[i] = 0.001F * static_cast<float>(static_cast<int>(i % 3)); | ||
| b[i] = 0.001F * static_cast<float>(static_cast<int>(i % 9)); | ||
| } | ||
| for (int i = 0; i < kValueHeads; ++i) { a_log[i] = -1.0F; dt_bias[i] = 0.0F; } | ||
|
|
||
| std::vector<float> state(kBatch * kValueHeads * kValueDim * kKeyDim, 0.0F); | ||
| std::vector<float> output(v.size()); | ||
| std::vector<float> ref_output(v.size()); | ||
| std::vector<float> ref_state = state; | ||
|
|
||
| // Serial reference, then 8-lane parallel — must be bitwise identical. | ||
| gatedDeltaRuleF32(q.data(), k.data(), v.data(), a.data(), b.data(), a_log.data(), dt_bias.data(), ref_state.data(), | ||
| ref_output.data(), kBatch, kSequence, kKeyHeads, kValueHeads, kKeyDim, kValueDim, | ||
| /*thread_count=*/1); | ||
| const ScopedCpuOpThreads scoped_threads(kThreadCount); | ||
| gatedDeltaRuleF32(q.data(), k.data(), v.data(), a.data(), b.data(), a_log.data(), dt_bias.data(), state.data(), | ||
| output.data(), kBatch, kSequence, kKeyHeads, kValueHeads, kKeyDim, kValueDim, kThreadCount); | ||
|
|
||
| for (std::size_t i = 0; i < output.size(); ++i) { | ||
| ASSERT_EQ(ref_output[i], output[i]) << "output index " << i; | ||
| } | ||
| for (std::size_t i = 0; i < state.size(); ++i) { | ||
| ASSERT_EQ(ref_state[i], state[i]) << "state index " << i; | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| // Repeat the full 4B GDN pass 24 times (one per layer) to mimic the real | ||
| // model's layer loop, which interleaves the recurrence with other parallel | ||
| // ops on the shared thread pool. Context init is now once-only (see | ||
| // ScopedCpuOpThreads), so this exercises multi-call thread-pool reuse. | ||
| // Run the recurrence 24 times on a FRESH copy of the initial state each | ||
| // time (mirroring one GDN layer per model layer from the same prefill input), | ||
| // and compare each run's output to the serial reference for that same input. | ||
| // This exercises repeated thread-pool push/acquire/release cycles — the | ||
| // multi-call reuse pattern that crashed on device. | ||
| for (int layer = 0; layer < 24; ++layer) { | ||
| std::vector<float> layer_state(state.size(), 0.0F); | ||
| std::vector<float> layer_ref_state(state.size(), 0.0F); | ||
| gatedDeltaRuleF32(q.data(), k.data(), v.data(), a.data(), b.data(), a_log.data(), dt_bias.data(), layer_ref_state.data(), | ||
| ref_output.data(), kBatch, kSequence, kKeyHeads, kValueHeads, kKeyDim, kValueDim, /*thread_count=*/1); | ||
| gatedDeltaRuleF32(q.data(), k.data(), v.data(), a.data(), b.data(), a_log.data(), dt_bias.data(), layer_state.data(), | ||
| output.data(), kBatch, kSequence, kKeyHeads, kValueHeads, kKeyDim, kValueDim, kThreadCount); | ||
| for (std::size_t i = 0; i < output.size(); ++i) { | ||
| ASSERT_EQ(ref_output[i], output[i]) << "layer " << layer << " output index " << i; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| } // namespace | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: UbiquitousLearning/mllm
Length of output: 7671
🏁 Script executed:
Repository: UbiquitousLearning/mllm
Length of output: 10765
🏁 Script executed:
Repository: UbiquitousLearning/mllm
Length of output: 11179
Clarify the lifetime explanation.
splitTaskwaits until worker callbacks complete before it returns, and each worker flag is cleared aftertasks_[i].first.func(thread_idx)returns. This path does not make the captured references dangling. The value capture is still useful for ownership, but update the comment to avoid saying the lifetime problem is caused by thesplitTaskreturn path.🤖 Prompt for AI Agents