Skip to content

Commit fcbd7ea

Browse files
committed
auto: whole-batch GPU bias so matmul-heavy blocks don't thrash devices
The per-op auto threshold is greedy: it places each op in isolation, so a transformer block's small projection gemms (seq*d*d, below the matmul threshold) stay on the CPU while its one large FFN gemm goes to the GPU. The block then thrashes CPU<->GPU and every hand-off is a blocking flush. graph::run_ now sums M*N*K over every dot in an evaluation batch and, when the block as a whole earns the GPU (batch_matmul_bias_threshold_, types.h), pins the whole batch to the GPU via a thread_local flag read by gpu_mode_. Ops with no GPU kernel still fall back per-op, so results are unchanged; only device placement shifts. The flag is scoped to the run_ call by an RAII guard. Calibrated on the pipelined transformer bench (position-rotated, per-config min, M1 Pro): the Metal threshold sits between d768's ~7e8 attention batch (pinning it is a net loss) and d1024's ~1.2e9 attention batch (pinning it is a large win). auto d_model=1024 drops 8.73 -> 5.51 ms/iter (gpu 4.90); d512 stays on the CPU. env TL_BATCH_MATMUL_BIAS re-calibrates per target.
1 parent a756625 commit fcbd7ea

2 files changed

Lines changed: 58 additions & 0 deletions

File tree

‎include/array.h‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,12 @@ inline void (*run_noflush_hook)(const std::vector<node_ptr>&) = nullptr;
188188
// nodes are single-threaded like the rest of evaluation).
189189
inline uint64_t visit_counter = 0;
190190

191+
// Whole-batch GPU bias for auto_ mode: set by graph::run_ for the span of one
192+
// evaluation batch when its total matmul work crosses batch_matmul_bias_
193+
// threshold_ (types.h), read by gpu_mode_. thread_local because each eval
194+
// thread runs its own batch.
195+
inline thread_local bool batch_gpu_bias_ = false;
196+
191197
} // namespace detail
192198

193199
class array {
@@ -1381,6 +1387,7 @@ struct graph {
13811387
if (device_ == device_type::gpu) return true;
13821388
if (device_ != device_type::auto_) return false;
13831389
if (gpu::pending()) return true;
1390+
if (batch_gpu_bias_) return true; // batch pinned to GPU (see run_)
13841391
return n >= auto_threshold_(kc);
13851392
}
13861393

@@ -1756,6 +1763,24 @@ struct graph {
17561763
}
17571764
}
17581765
}
1766+
// Auto-mode whole-batch device bias: sum this batch's matmul work and, if
1767+
// the block as a whole earns the GPU, pin every op in it there so the
1768+
// sub-threshold projection gemms don't strand on the CPU and thrash the
1769+
// pipeline (types.h batch_matmul_bias_threshold_). cpu/gpu modes and a
1770+
// pipeline already in flight short-circuit gpu_mode_, so skip the scan.
1771+
struct BiasGuard {
1772+
~BiasGuard() { batch_gpu_bias_ = false; }
1773+
} bias_guard;
1774+
if (device_ == device_type::auto_ && gpu::available() && !gpu::pending()) {
1775+
int64_t work = 0;
1776+
for (const node* n : order) {
1777+
if (n->op != node::op_t::dot || n->inputs.size() != 2) continue;
1778+
const auto& sa = n->inputs[0]->shape;
1779+
const auto& sb = n->inputs[1]->shape;
1780+
if (sa.size() == 2 && sb.size() == 2) work += sa[0] * sa[1] * sb[1];
1781+
}
1782+
if (work >= batch_matmul_bias_threshold_()) batch_gpu_bias_ = true;
1783+
}
17591784
for (auto* n : order) eval_one(*n);
17601785
if (do_flush) gpu::flush(); // blocking eval: batch done when run() returns
17611786
}

‎include/types.h‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
// backends, no kernel changes). Activations, results and scalars stay F32.
88

99
#include <cstdint>
10+
#include <cstdlib>
1011
#include <cstring>
1112

1213
namespace tl {
@@ -120,4 +121,36 @@ inline int64_t auto_threshold_(kernel_class kc) {
120121
return 1'000'000'000'000; // unreachable
121122
}
122123

124+
// Batch device bias (auto_ mode). The per-op auto_threshold_ is greedy: it
125+
// decides each op in isolation, so a transformer block's small projection
126+
// gemms (e.g. seq·d·d = 256·768·768 = 1.5e8, below the matmul threshold)
127+
// strand on the CPU while its one big FFN gemm goes to the GPU — the batch
128+
// then thrashes CPU⇄GPU (each hand-off is a blocking flush). This threshold
129+
// is compared against the *sum* of M·N·K over every dot in one evaluation
130+
// batch (graph::run_); above it the whole batch is pinned to the GPU so the
131+
// projections ride along and the pipeline stays on one device. Env override
132+
// TL_BATCH_MATMUL_BIAS (element-product units) for host calibration, mirroring
133+
// the misc/census.cpp re-measure note above.
134+
inline int64_t batch_matmul_bias_threshold_() {
135+
static const int64_t v = []() -> int64_t {
136+
if (const char* e = std::getenv("TL_BATCH_MATMUL_BIAS")) {
137+
char* end = nullptr;
138+
long long x = std::strtoll(e, &end, 10);
139+
if (end != e && x > 0) return static_cast<int64_t>(x);
140+
}
141+
#if defined(TENSORLIB_CUDA)
142+
return 4'000'000; // GPU wins early; a couple of small gemms
143+
#else
144+
// Metal / M1 Pro, calibrated 2026-07-18 on the pipelined transformer bench
145+
// (position-rotated, per-config min). The batch bias helps only when the
146+
// whole block is GPU-favourable: pinning d768's ~7e8 attention batch to
147+
// the GPU is a net loss (encode/flush > the small gemms' savings), while
148+
// pinning d1024's ~1.2e9 attention batch is a large win (8.73→5.6 ms/iter).
149+
// Sit the threshold between them; env TL_BATCH_MATMUL_BIAS re-calibrates.
150+
return 800'000'000;
151+
#endif
152+
}();
153+
return v;
154+
}
155+
123156
} // namespace tl

0 commit comments

Comments
 (0)