@@ -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+
42534277static std::pair<xla::LocalExecutable *, bool >
42544278specializeRaw (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+
48164904REACTANT_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 );
0 commit comments