Skip to content

Commit 8496669

Browse files
wsmosesclaude
andcommitted
Linkable runtime: compile a kernel again in chunks when it would not fit
XLA allocates the values an executable keeps as one temporary, and a raised kernel's batched parallel loops keep a value per iteration of many of them: past some problem size the executable cannot run (MFEM ceed BP3 order 4 at side 240: 35.79 GiB; BP4: 47.29 GiB). After compiling, the runtime now reads the executable's temporary bytes; when they exceed what the allocator has left (90%; else half the device; or REACTANT_EXEC_TEMP_BUDGET), it compiles the module again from the start asking the pattern list for parallel_while_to_batched_scatter_chunked(N) -- the loops batched N chunks at a time -- and without greedy_while_loop_batch_fission, which lifts ops out of a loop over all its iterations at once. N grows with the overshoot, again while the temporary still shrinks. Modules that fit are compiled exactly as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016zErYp7upmqr4NHfhod9UD
1 parent 3f7c6de commit 8496669

2 files changed

Lines changed: 134 additions & 4 deletions

File tree

‎deps/ReactantExtra/API.cpp‎

Lines changed: 131 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,10 @@ void reactantReleaseAddressRange(void *base, size_t nbytes);
7676
#include "stablehlo/dialect/StablehloOps.h"
7777
#include "stablehlo/transforms/Passes.h"
7878
#include "stablehlo/transforms/optimization/Passes.h"
79+
#if defined(REACTANT_CUDA) || defined(REACTANT_ROCM)
80+
#include "xla/service/gpu/gpu_executable.h"
81+
#endif
82+
#include "xla/stream_executor/integrations/tf_allocator_adapter.h"
7983

8084
#include "absl/log/globals.h"
8185
#include "absl/log/initialize.h"
@@ -4095,7 +4099,8 @@ static void rawDeviceToHost(LinkableRuntime *__restrict__ lrt, void *dst,
40954099

40964100
// The exec path's optimization of a kernel's module, before XLA: the
40974101
// transform list (or REACTANT_EXEC_OPT=hlo-opt, the plain pass).
4098-
static void addExecOptimizationPasses(mlir::PassManager &pm) {
4102+
static void addExecOptimizationPasses(mlir::PassManager &pm,
4103+
int64_t chunks = 1) {
40994104
// REACTANT_EXEC_OPT=hlo-opt runs the plain enzyme-hlo-opt pass instead.
41004105
// REACTANT_EXEC_UNROLL sets the trip count up to which while loops are
41014106
// unrolled. Unrolling the raised kernels' short loops (threshold 16)
@@ -4143,6 +4148,19 @@ static void addExecOptimizationPasses(mlir::PassManager &pm) {
41434148
{"convert_mul_convert;", "associative_binary_op_reordering<1>;"})
41444149
for (size_t at; (at = patterns.find(drop)) != std::string::npos;)
41454150
patterns.erase(at, strlen(drop));
4151+
// Asked for chunks (see reactantXLAExec): a tagged parallel loop is
4152+
// batched a chunk at a time, and fission, which lifts ops out of a loop
4153+
// over all its iterations at once, is left out.
4154+
if (chunks > 1) {
4155+
const std::string whole = "parallel_while_to_batched_scatter;";
4156+
if (size_t at = patterns.find(whole); at != std::string::npos)
4157+
patterns.replace(at, whole.size(),
4158+
"parallel_while_to_batched_scatter_chunked(" +
4159+
std::to_string(chunks) + ");");
4160+
const std::string fission = "greedy_while_loop_batch_fission;";
4161+
if (size_t at = patterns.find(fission); at != std::string::npos)
4162+
patterns.erase(at, fission.size());
4163+
}
41464164
// The main list may introduce enzymexla ops (rotate, wrap, extend)
41474165
// that XLA does not take; the Julia compiler lowers them before export
41484166
// with this second list.
@@ -4250,9 +4268,16 @@ static std::vector<std::vector<int64_t>> returnLoopBounds(func::FuncOp funcOp) {
42504268
// The executable `exec` specialized to the bound values `values`: the
42514269
// bounds (the module's last results) become constants, and are returned
42524270
// no more.
4271+
static int64_t tempBytes(xla::LocalExecutable *exec);
4272+
static int64_t tempBudget(LinkableRuntime *lrt);
4273+
// Asking for more chunks than this, the attempt with the smallest temporary
4274+
// is taken.
4275+
static constexpr int64_t kMaxChunkRequest = 1024;
4276+
42534277
static std::pair<xla::LocalExecutable *, bool>
42544278
specializeRaw(LinkableRuntime *__restrict__ lrt,
4255-
LinkableRuntime::CachedExec &exec, ArrayRef<int64_t> values) {
4279+
LinkableRuntime::CachedExec &exec, ArrayRef<int64_t> values,
4280+
int64_t chunks = 1) {
42564281
MLIRContext context(lrt->registry);
42574282
RegisterDialects(wrap(&context));
42584283
mlir::ParserConfig config(&context, false);
@@ -4281,10 +4306,25 @@ specializeRaw(LinkableRuntime *__restrict__ lrt,
42814306
// Optimized again with the bounds known: the loops they bound unroll
42824307
// (REACTANT_EXEC_UNROLL), and what that exposes simplifies.
42834308
mlir::PassManager pm(&context);
4284-
addExecOptimizationPasses(pm);
4309+
addExecOptimizationPasses(pm, chunks);
42854310
if (!mlir::succeeded(pm.run(*module)))
42864311
ReactantThrowError("specialization: the passes failed");
42874312
auto [lexec, shapes, tupleResult] = compileRaw(lrt, *module);
4313+
// Too large to run (see reactantXLAExec): again with the parallel loops in
4314+
// more chunks, up to kMaxChunkRequest, keeping the attempt that takes the
4315+
// least.
4316+
if (int64_t temp = tempBytes(lexec), budget = tempBudget(lrt);
4317+
temp > budget) {
4318+
int64_t more = chunks * std::max<int64_t>(2, (temp + budget - 1) / budget);
4319+
if (more <= kMaxChunkRequest) {
4320+
auto other = specializeRaw(lrt, exec, values, more);
4321+
if (tempBytes(other.first) < temp) {
4322+
delete lexec;
4323+
return other;
4324+
}
4325+
delete other.first;
4326+
}
4327+
}
42884328
return {lexec, tupleResult};
42894329
}
42904330

@@ -4813,6 +4853,54 @@ REACTANT_ABI void reactantXLAFree(LinkableRuntime **__restrict__ lrtP,
48134853
lrt->allocationBase.erase(id);
48144854
}
48154855

4856+
// The bytes of the temporary allocation an executable makes when it runs
4857+
// (every value XLA keeps that is neither a parameter nor a result).
4858+
static int64_t tempBytes(xla::LocalExecutable *exec) {
4859+
#if defined(REACTANT_CUDA) || defined(REACTANT_ROCM)
4860+
auto *gpu = dynamic_cast<xla::gpu::GpuExecutable *>(exec->executable());
4861+
if (!gpu)
4862+
return 0;
4863+
int64_t bytes = 0;
4864+
for (const xla::BufferAllocation *a : gpu->GetAllocations())
4865+
if (a->IsPreallocatedTempBuffer())
4866+
bytes += a->size();
4867+
return bytes;
4868+
#else
4869+
return 0;
4870+
#endif
4871+
}
4872+
4873+
// Executables whose temporary allocation would not fit (in what the
4874+
// allocator has left, else half the device's memory;
4875+
// REACTANT_EXEC_TEMP_BUDGET bytes to override) are compiled again with the
4876+
// kernel's parallel loops batched in chunks: per module and arguments, the
4877+
// chunks asked for and the temporary bytes it last took.
4878+
struct ChunkRequest {
4879+
int64_t chunks = 1;
4880+
// the fewest temporary bytes an attempt took, and its chunks
4881+
int64_t bestTemp = INT64_MAX, bestChunks = 1;
4882+
bool settled = false;
4883+
};
4884+
static std::mutex chunkRequestsLock;
4885+
static std::map<std::pair<const char *, std::vector<std::vector<int64_t>>>,
4886+
ChunkRequest>
4887+
chunkRequests;
4888+
4889+
static int64_t tempBudget(LinkableRuntime *lrt) {
4890+
if (const char *env = getenv("REACTANT_EXEC_TEMP_BUDGET"))
4891+
return atoll(env);
4892+
// What the allocator has left, the arguments already in it.
4893+
if (auto *multi = dynamic_cast<se::MultiDeviceAdapter *>(lrt->allocator))
4894+
if (auto a = multi->GetAllocator(lrt->ordinal); a.ok())
4895+
if (auto stats = (*a)->GetStats(); stats && stats->bytes_limit)
4896+
return (*stats->bytes_limit - stats->bytes_in_use) / 10 * 9;
4897+
int64_t free = 0, total = 0;
4898+
if (!lrt->local->backend().default_stream_executor()->DeviceMemoryUsage(
4899+
&free, &total))
4900+
return INT64_MAX;
4901+
return total / 2;
4902+
}
4903+
48164904
REACTANT_ABI void reactantXLAExec(LinkableRuntime **__restrict__ lrtP,
48174905
const char *modstr, int64_t argcnt,
48184906
void **args, int64_t constcnt,
@@ -4969,7 +5057,14 @@ REACTANT_ABI void reactantXLAExec(LinkableRuntime **__restrict__ lrtP,
49695057
stablehlo::createStablehloCanonicalizeDynamismPass());
49705058
// The exec-time optimizer runs the pattern list the Julia compiler runs
49715059
// on a traced program (the transform-dialect list with its defaults);
4972-
addExecOptimizationPasses(pm);
5060+
int64_t chunks = 1;
5061+
{
5062+
std::lock_guard<std::mutex> guard(chunkRequestsLock);
5063+
if (auto found = chunkRequests.find({modstr, sizeKey});
5064+
found != chunkRequests.end())
5065+
chunks = found->second.chunks;
5066+
}
5067+
addExecOptimizationPasses(pm, chunks);
49735068
if (getenv("REACTANT_EXEC_DUMP")) {
49745069
llvm::errs() << "EXEC_DUMP before\n";
49755070
for (int64_t i = 0; i < argcnt; i++)
@@ -5131,6 +5226,38 @@ REACTANT_ABI void reactantXLAExec(LinkableRuntime **__restrict__ lrtP,
51315226
module->print(os);
51325227
}
51335228
std::tie(lexec, paramShapes, tupleResult) = compileRaw(lrt, *module);
5229+
// Too large to run: compiled again from the start with the parallel
5230+
// loops in chunks (more chunks may also batch loops a whole batch could
5231+
// not, so a larger request can take more), up to kMaxChunkRequest;
5232+
// past it, the attempt that took the least.
5233+
if (int64_t temp = tempBytes(lexec), budget = tempBudget(lrt);
5234+
temp > budget) {
5235+
bool again = false;
5236+
{
5237+
std::lock_guard<std::mutex> guard(chunkRequestsLock);
5238+
auto &request = chunkRequests[{modstr, sizeKey}];
5239+
if (temp < request.bestTemp) {
5240+
request.bestTemp = temp;
5241+
request.bestChunks = chunks;
5242+
}
5243+
if (!request.settled) {
5244+
int64_t more =
5245+
chunks * std::max<int64_t>(2, (temp + budget - 1) / budget);
5246+
if (more <= kMaxChunkRequest) {
5247+
request.chunks = more;
5248+
} else {
5249+
request.chunks = request.bestChunks;
5250+
request.settled = true;
5251+
}
5252+
again = request.chunks != chunks;
5253+
}
5254+
}
5255+
if (again) {
5256+
delete lexec;
5257+
free(written);
5258+
return reactantXLAExec(lrtP, modstr, argcnt, args, constcnt, consts);
5259+
}
5260+
}
51345261
} else {
51355262
exec =
51365263
ClientCompileWithProto(lrt->client, wrap(module.get()), nullptr, 0);

‎deps/ReactantExtra/BUILD‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1427,6 +1427,7 @@ cc_library(
14271427
"@xla//xla/tsl/protobuf:histogram_proto_cc",
14281428
"@xla//xla/tsl/protobuf:histogram_proto_cc_impl",
14291429
"@xla//xla:xla_data_proto_cc_impl",
1430+
"@xla//xla/stream_executor/integrations:tf_allocator_adapter",
14301431
"@xla//xla/tsl/platform:env",
14311432
"@xla//xla/tsl/platform:errors",
14321433
"@xla//xla/service:hlo_proto_cc_impl",
@@ -1443,6 +1444,7 @@ cc_library(
14431444
"@xla//xla/backends/profiler/gpu:device_tracer",
14441445
"@xla//xla/pjrt/c:pjrt_c_api_gpu_internal",
14451446
"@xla//xla/service/gpu:gpu_transfer_manager",
1447+
"@xla//xla/service/gpu:gpu_executable",
14461448
"@xla//xla/service/gpu:nvptx_compiler",
14471449
"@xla//xla/service/gpu/model:hlo_op_profile_proto_cc_impl",
14481450
"@xla//xla/service/gpu/model:hlo_op_profiles",
@@ -1456,6 +1458,7 @@ cc_library(
14561458
"@xla//xla/pjrt/plugin/xla_gpu:xla_gpu_pjrt_client",
14571459
"@xla//xla/stream_executor:rocm_platform",
14581460
"@xla//xla/service/gpu:amdgpu_compiler",
1461+
"@xla//xla/service/gpu:gpu_executable",
14591462
"@xla//xla/backends/profiler/gpu:device_tracer",
14601463
]) + select({
14611464
# gloo tcp transport only builds on linux

0 commit comments

Comments
 (0)