MimIR
MimIR is my Intermediate Representation
Loading...
Searching...
No Matches
nvptx.cpp
Go to the documentation of this file.
2
3#include <algorithm>
4#include <format>
5
6#include <fe/log.h>
7#include <fe/sys.h>
8
9#include <mim/driver.h>
10#include <mim/lam.h>
11
12#include <mim/plug/core/core.h>
13#include <mim/plug/gpu/gpu.h>
14#include <mim/plug/mem/mem.h>
15
17
18using namespace std::string_literals;
19
20namespace mim::plug::nvptx {
21
22namespace core = mim::plug::core;
23namespace ll = mim::plug::ll;
24namespace mem = mim::plug::mem;
25namespace gpu = mim::plug::gpu;
26
27class HostEmitter : public ll::Emitter {
28public:
30
31 HostEmitter(World& world, std::ostream& ostream, std::optional<std::string> device_fatbin_file)
32 : Super(world, "llvm_nvptx_host_emitter", ostream)
33 , device_fatbin_file_(device_fatbin_file) {}
34
35 void start() final;
36 void find_kernels(const Def*);
37
38 std::string prepare() override;
39 void emit_epilogue(Lam*) override;
40
41 std::optional<std::string> isa_targetspecific_intrinsic(ll::BB&, const Def*) final;
42
43protected:
44 std::string convert(const Def*, bool simd = true) override;
45
46private:
47 static constexpr std::string_view mod_name_ = "@.mimir_cu_mod";
48 static constexpr std::string_view ctx_name_ = "@.mimir_cu_ctx";
49 static constexpr std::string_view fatbin_name_ = "@.fatbin";
50 static constexpr std::string_view kernel_array_name_ = "@.mimir_kernels";
51 static constexpr std::string_view kernel_name_prefix = "@.kname.";
52
53 void emit_cu_error_handling(ll::BB&, const std::string&);
54 void emit_gpu_setup(ll::BB&, const std::string& name);
55 void emit_gpu_teardown(ll::BB&, const std::string& name);
56
57 std::optional<std::string> device_fatbin_file_;
58 LamMap<int> kernel_ids_;
59 bool cu_globals_declared_ = false;
60
61 DefSet analyzed_;
62 /// Externals wrapped in their own GPU setup/teardown - see HostEmitter::start().
63 LamSet gpu_externals_;
64};
65
66class DeviceEmitter : public ll::Emitter {
67public:
69
71 : Super(world, "llvm_nvptx_device_emitter", ostream) {
72 simd_ = false;
73 }
74
75 void start() final;
76
77 std::string prepare() override;
78
79 std::optional<std::string> isa_targetspecific_intrinsic(ll::BB&, const Def*) final;
80
81 bool is_using_libdevice() const { return uses_libdevice; }
82 const std::string& get_extra_flags() const { return extra_flags; }
83
84private:
85 /// Device slots live in a module-scope global in their requested address space, not on the stack.
86 std::string emit_slot(ll::BB&, const App* app, const Def* pointee, const Def* addr_space) override {
87 auto v_ptr = "@" + app->unique_name() + ".slot";
88 std::println(vars_decls_, "{} = internal addrspace({}) global {} undef", v_ptr, addr_space,
89 convert(pointee, false));
90 return v_ptr;
91 }
92
93 LamSet kernels_;
94
95 bool uses_libdevice = false;
96 std::string extra_flags;
97};
98
99namespace {
100bool reaches_if(const Def* def, DefSet& seen, std::predicate<const Def*> auto pred) {
101 if (auto [_, ins] = seen.emplace(def); !ins) return false;
102 if (pred(def)) return true;
103 for (auto d : def->deps())
104 if (reaches_if(d, seen, pred)) return true;
105 return false;
106}
107
108bool is_gpu_auto_init(const Def* def) { return Axm::isa<gpu::auto_init>(def) != nullptr; }
109} // namespace
110
112 for (auto def : world().externals().muts())
113 find_kernels(def);
114
115 for (auto [kernel, kid] : kernel_ids_) {
116 auto name = id(kernel).substr(1);
117 std::println(vars_decls_, "{}{} = private constant [{} x i8] c\"{}\\00\"", kernel_name_prefix, kid,
118 name.size() + 1, name);
119 }
120 std::println(vars_decls_, "{} = dso_local global [{} x ptr] zeroinitializer", kernel_array_name_,
121 kernel_ids_.size());
122
123 LamSet gpu_touching;
124 for (auto mut : world().externals().muts()) {
125 auto lam = mut->isa_mut<Lam>();
126 if (!lam || !lam->ret_pi()) continue;
127 DefSet seen;
128 if (reaches_if(lam, seen, is_gpu_auto_init)) gpu_touching.emplace(lam);
129 }
130 // Only wrap the ones not themselves called by another GPU-touching external: a caller's own
131 // setup/teardown already covers whatever GPU-touching external(s) it calls.
132 for (auto lam : gpu_touching) {
133 auto called_by_other = std::ranges::any_of(gpu_touching, [&](auto other) {
134 if (other == lam) return false;
135 DefSet seen;
136 return reaches_if(other, seen, [lam](const Def* d) { return d == lam; });
137 });
138 if (!called_by_other) gpu_externals_.emplace(lam);
139 }
140
141 Super::start();
142}
143
145 if (auto [_, ins] = analyzed_.emplace(def); !ins) return;
146
147 for (auto d : def->deps())
148 find_kernels(d);
149
150 if (auto launch = Axm::isa<gpu::launch>(def)) {
151 auto kernel = launch->decurry()->decurry()->arg();
152 auto kernel_lam = kernel->expect_mut<Lam>("the kernel passed to `gpu.launch` to be a mutable lambda");
153 if (kernel_ids_.contains(kernel_lam)) return;
154 auto kid = kernel_ids_.size();
155 kernel_ids_[kernel_lam] = kid;
156 }
157}
158
159constexpr auto Cu_Init = "cuInit";
160constexpr auto Cu_Ctx_Create = "cuCtxCreate_v4";
161constexpr auto Cu_Ctx_Destroy = "cuCtxDestroy_v2";
162constexpr auto Cu_Device_Get = "cuDeviceGet";
163constexpr auto Cu_Launch_Kernel = "cuLaunchKernel_ptsz";
164constexpr auto Cu_Mem_Alloc = "cuMemAlloc_v2";
165constexpr auto Cu_Mem_Alloc_Async = "cuMemAllocAsync_ptsz";
166constexpr auto Cu_Mem_Free = "cuMemFree_v2";
167constexpr auto Cu_Mem_Free_Async = "cuMemFreeAsync_ptsz";
168constexpr auto Cu_Memcpy_Htod = "cuMemcpyHtoD_v2";
169constexpr auto Cu_Memcpy_Htod_Async = "cuMemcpyHtoDAsync_v2_ptsz";
170constexpr auto Cu_Memcpy_Dtoh = "cuMemcpyDtoH_v2";
171constexpr auto Cu_Memcpy_Dtoh_Async = "cuMemcpyDtoHAsync_v2_ptsz";
172constexpr auto Cu_Module_Load_Fatbin = "cuModuleLoadFatBinary";
173constexpr auto Cu_Module_Get_Function = "cuModuleGetFunction";
174constexpr auto Cu_Module_Unload = "cuModuleUnload";
175constexpr auto Cu_Stream_Create = "cuStreamCreate";
176constexpr auto Cu_Stream_Destroy = "cuStreamDestroy_v2";
177constexpr auto Cu_Stream_Sync = "cuStreamSynchronize_ptsz";
178
179void HostEmitter::emit_cu_error_handling(ll::BB& bb, const std::string& cu_result) {
180 // Offload the CUresult check to the C runtime wrapper `mim_cu_check` (see rt/mim_cuda_rt.c)
181 // instead of open-coding it here; showcases the C-runtime system on the nvptx backend.
182 declare_rt("void @mim_cu_check(i32)");
183 std::print(bb.body().emplace_back(), "call void @mim_cu_check(i32 {})", cu_result);
184}
185
186void HostEmitter::emit_gpu_setup(ll::BB& bb, const std::string& name) {
187 auto dev_num = 0; // TODO: consider parameterizing this
188 auto ctx_flags = 0; // TODO: consider parameterizing this
189
190 declare("i32 @{}(i32)", Cu_Init);
191 auto init_res = bb.assign(name + "_init_res", "call i32 @{}(i32 0)", Cu_Init);
192 emit_cu_error_handling(bb, init_res);
193
194 declare("i32 @{}(ptr, i32)", Cu_Device_Get);
195 auto dev_ptr = bb.assign(name + "_dev_ptr", "alloca i32");
196 auto dev_get_res = bb.assign(name + "_get_res", "call i32 @{}(ptr {}, i32 {})", Cu_Device_Get, dev_ptr, dev_num);
197 emit_cu_error_handling(bb, dev_get_res);
198
199 declare("i32 @{}(ptr, ptr, i32, i32)", Cu_Ctx_Create);
200 if (!cu_globals_declared_) std::println(vars_decls_, "{} = global ptr null", ctx_name_);
201 auto dev = bb.assign(name + "_dev", "load i32, ptr {}", dev_ptr);
202 auto ctx_res = bb.assign(name + "_ctx_res", "call i32 @{}(ptr {}, ptr null, i32 {}, i32 {})", Cu_Ctx_Create,
203 ctx_name_, ctx_flags, dev);
204 emit_cu_error_handling(bb, ctx_res);
205
206 declare("i32 @{}(ptr, ptr)", Cu_Module_Load_Fatbin);
207 if (!cu_globals_declared_) {
208 std::println(vars_decls_, "{} = global ptr null", mod_name_);
209 if (device_fatbin_file_.has_value()) {
210 std::ifstream fatbin_file(device_fatbin_file_.value(), std::ios::binary);
211 if (!fatbin_file)
212 fe::throwf(MIM_NVPTX_BE "could not open `{}` as binary file", device_fatbin_file_.value());
213
214 auto start = std::istreambuf_iterator<char>(fatbin_file);
215 auto end = std::istreambuf_iterator<char>();
216 std::vector<u8> fatbin_bytes(start, end);
217
218 std::print(vars_decls_, "{} = private constant [{} x i8] c\"", fatbin_name_, fatbin_bytes.size());
219 for (auto byte : fatbin_bytes) {
220 bool invalid_cstr_char = byte == '"' || byte == '\\';
221 if (std::isprint(byte) && !invalid_cstr_char)
222 std::print(vars_decls_, "{:c}", byte);
223 else
224 std::print(vars_decls_, "\\{:02x}", byte);
225 }
226 std::println(vars_decls_, "\"");
227 } else {
228 std::println(vars_decls_, "; Add the bytes of your compiled nvptx fatbin binary here:");
229 std::println(vars_decls_,
230 "{} = private constant [YOUR_FATBIN_DATA_SIZE_GOES_HERE x i8] YOUR_FATBIN_DATA_GOES_HERE",
231 fatbin_name_);
232 }
233 cu_globals_declared_ = true;
234 }
235 auto mod_res
236 = bb.assign(name + "_mod_res", "call i32 @{}(ptr {}, ptr {})", Cu_Module_Load_Fatbin, mod_name_, fatbin_name_);
237 emit_cu_error_handling(bb, mod_res);
238 auto mod_inner = bb.assign(name + "_mod_inner", "load ptr, ptr {}", mod_name_);
239
240 declare("i32 @{}(ptr, ptr, ptr)", Cu_Module_Get_Function);
241 for (auto [kernel, kid] : kernel_ids_) {
242 auto kname = id(kernel).substr(1);
243 auto func_ptr = bb.assign(name + "_" + kname + "_funcptr", "getelementptr inbounds ptr, ptr {}, i64 {}",
244 kernel_array_name_, kid);
245 auto func_res = bb.assign(name + "_" + kname + "_getfuncres", "call i32 @{}(ptr {}, ptr {}, ptr {}{})",
246 Cu_Module_Get_Function, func_ptr, mod_inner, kernel_name_prefix, kid);
247 emit_cu_error_handling(bb, func_res);
248 }
249}
250
251void HostEmitter::emit_gpu_teardown(ll::BB& bb, const std::string& name) {
252 declare("i32 @{}(ptr)", Cu_Module_Unload);
253 std::print(bb.body().emplace_back(), "{}_mod = load ptr, ptr {}", name, mod_name_);
254 std::print(bb.body().emplace_back(), "{}_mod_unload_res = call i32 @{}(ptr {}_mod)", name, Cu_Module_Unload, name);
255 emit_cu_error_handling(bb, name + "_mod_unload_res");
256
257 declare("i32 @{}(ptr)", Cu_Ctx_Destroy);
258 std::print(bb.body().emplace_back(), "{}_ctx = load ptr, ptr {}", name, ctx_name_);
259 std::print(bb.body().emplace_back(), "{}_ctx_destroy_res = call i32 @{}(ptr {}_ctx)", name, Cu_Ctx_Destroy, name);
260 emit_cu_error_handling(bb, name + "_ctx_destroy_res");
261}
262
263std::string HostEmitter::prepare() {
264 auto name = Super::prepare();
265 // Append to root()'s own BB, not func_impls_: Emitter::finalize_impl writes it out only once.
266 if (gpu_externals_.contains(root())) emit_gpu_setup(lam2bb_[root()], "%" + root()->unique_name());
267 return name;
268}
269
271 // Must run first to force emission of the return value's own dependencies (e.g. gpu.free) into bb.body().
273 if (gpu_externals_.contains(root())) {
274 // lam, not root(): a function can have several return blocks, and LLVM names are function-scoped.
275 if (auto app = lam->body()->isa<App>(); app && app->callee() == root()->ret_var())
276 emit_gpu_teardown(lam2bb_[lam], "%" + lam->unique_name());
277 }
278}
279
280std::string HostEmitter::convert(const Def* type, bool simd) {
281 if (auto ptr = Axm::isa<mem::Ptr>(type)) {
282 auto [_, addr_space] = ptr->args<2>();
283 auto lit = Lit::isa(addr_space);
284 if (lit.value_or(0L) != 0) {
285 // NVIDIA treats all device pointers as i64s in host code
286 return "i64";
287 }
288 }
289 return Super::convert(type, simd);
290}
291
292std::optional<std::string> HostEmitter::isa_targetspecific_intrinsic(ll::BB& bb, const Def* def) {
293 auto name = id(def);
294
295 if (auto default_stream = Axm::isa<gpu::default_stream>(def)) {
296 return "null";
297 } else if (auto init = Axm::isa<gpu::init>(def)) {
298 auto mem_val = emit_unsafe(init->arg());
299 emit_gpu_setup(bb, name);
300 return mem_val;
301 } else if (auto deinit = Axm::isa<gpu::deinit>(def)) {
302 emit_unsafe(deinit->arg(3, 0));
303 emit_unsafe(deinit->arg(3, 1));
304 emit_gpu_teardown(bb, name);
305 return ""s;
306 } else if (auto auto_init = Axm::isa<gpu::auto_init>(def)) {
307 return emit_unsafe(auto_init->arg());
308 } else if (auto auto_deinit = Axm::isa<gpu::auto_deinit>(def)) {
309 // emit_unsafe on `global` keeps any gpu.free threaded through it reachable.
310 emit_unsafe(auto_deinit->arg(3, 1));
311 emit_unsafe(auto_deinit->arg(3, 2));
312 return emit_unsafe(auto_deinit->arg(3, 0));
313 } else if (auto stream_init = Axm::isa<gpu::stream_init>(def)) {
314 declare("i32 @{}(ptr, i32)", Cu_Stream_Create);
315
316 emit_unsafe(stream_init->arg(3, 0));
317 emit_unsafe(stream_init->arg(3, 1));
318 auto stream_ptr = emit(stream_init->arg(3, 2));
319
320 auto res = bb.assign(name, "call i32 @{}(ptr {}, i32 0)", Cu_Stream_Create, stream_ptr);
321 emit_cu_error_handling(bb, res);
322 return res;
323 } else if (auto stream_deinit = Axm::isa<gpu::stream_deinit>(def)) {
324 declare("i32 @{}(ptr)", Cu_Stream_Destroy);
325
326 emit_unsafe(stream_deinit->arg(3, 0));
327 emit_unsafe(stream_deinit->arg(3, 1));
328 auto stream = emit(stream_deinit->arg(3, 2));
329
330 auto res = bb.assign(name, "call i32 @{}(ptr {})", Cu_Stream_Destroy, stream);
331 emit_cu_error_handling(bb, res);
332 return res;
333 } else if (auto stream_sync = Axm::isa<gpu::stream_sync>(def)) {
334 declare("i32 @{}(ptr)", Cu_Stream_Sync);
335
336 emit_unsafe(stream_sync->arg(3, 0));
337 emit_unsafe(stream_sync->arg(3, 1));
338 auto stream = emit(stream_sync->arg(3, 2));
339
340 auto res = bb.assign(name, "call i32 @{}(ptr {})", Cu_Stream_Sync, stream);
341 emit_cu_error_handling(bb, res);
342 return res;
343 } else if (auto alloc = Axm::isa<gpu::alloc>(def)) {
344 bool is_async;
345 switch (alloc.id()) {
346 case gpu::alloc::block: is_async = false; break;
347 case gpu::alloc::asyn: is_async = true; break;
348 default: fe::throwf(MIM_NVPTX_BE "unhandled `gpu.alloc` id in `{}`", def);
349 }
350
351 // `.block` and `.asyn` differ in arity, so the count has to follow the variant.
352 auto n = is_async ? 2_n : 1_n;
353
354 if (is_async)
355 declare("i32 @{}(ptr, i64, ptr)", Cu_Mem_Alloc_Async);
356 else
357 declare("i32 @{}(ptr, i64)", Cu_Mem_Alloc);
358
359 emit_unsafe(alloc->arg(n, 0));
360 auto alloc_t = alloc->decurry()->arg();
361 World& w = alloc_t->world();
362 auto type_size = w.call(core::trait::size, alloc_t);
363 auto alloc_size = emit(type_size);
364
365 auto ptr_t = convert(Axm::expect<mem::Ptr>(def->proj(2, 1)->type(), "a `mem.Ptr`"));
366
367 auto alloc_ptr = bb.assign(name + "ptr", "alloca {}", ptr_t);
368 std::string alloc_res;
369 if (is_async) {
370 auto stream = emit(alloc->arg(n, 1));
371 alloc_res = bb.assign(name + "res", "call i32 @{}(ptr {}, i64 {}, ptr {})", Cu_Mem_Alloc_Async, alloc_ptr,
372 alloc_size, stream);
373 } else
374 alloc_res = bb.assign(name + "res", "call i32 @{}(ptr {}, i64 {})", Cu_Mem_Alloc, alloc_ptr, alloc_size);
375
376 emit_cu_error_handling(bb, alloc_res);
377 return bb.assign(name, "load {}, {} addrspace(0)* {}", ptr_t, ptr_t, alloc_ptr);
378 } else if (auto free = Axm::isa<gpu::free>(def)) {
379 bool is_async;
380 switch (free.id()) {
381 case gpu::free::block: is_async = false; break;
382 case gpu::free::asyn: is_async = true; break;
383 default: fe::throwf(MIM_NVPTX_BE "unhandled `gpu.free` id in `{}`", def);
384 }
385
386 // `.block` and `.asyn` differ in arity, so the count has to follow the variant.
387 auto n = is_async ? 3_n : 2_n;
388
389 if (is_async)
390 declare("i32 @{}(i64, ptr)", Cu_Mem_Free_Async);
391 else
392 declare("i32 @{}(i64)", Cu_Mem_Free);
393
394 emit_unsafe(free->arg(n, 0));
395 auto ptr = emit(free->arg(n, 1));
396
397 std::string free_res;
398 if (is_async) {
399 auto stream = emit(free->arg(n, 2));
400 free_res = bb.assign(name + "res", "call i32 @{}(i64 {}, ptr {})", Cu_Mem_Free_Async, ptr, stream);
401 } else
402 free_res = bb.assign(name + "res", "call i32 @{}(i64 {})", Cu_Mem_Free, ptr);
403
404 emit_cu_error_handling(bb, free_res);
405 return free_res;
406 } else if (auto copy_to_device = Axm::isa<gpu::copy_to_device>(def)) {
407 bool is_async;
408 switch (copy_to_device.id()) {
409 case gpu::copy_to_device::block: is_async = false; break;
410 case gpu::copy_to_device::asyn: is_async = true; break;
411 default: fe::throwf(MIM_NVPTX_BE "unhandled `gpu.copy_to_device` id in `{}`", def);
412 }
413
414 // `.block` and `.asyn` differ in arity, so the count has to follow the variant.
415 auto n = is_async ? 5_n : 4_n;
416
417 if (is_async)
418 declare("i32 @{}(i64, ptr, i64, ptr)", Cu_Memcpy_Htod_Async);
419 else
420 declare("i32 @{}(i64, ptr, i64)", Cu_Memcpy_Htod);
421
422 auto type = copy_to_device->decurry()->arg();
423 World& w = type->world();
424 auto type_size = w.call(core::trait::size, type);
425
426 emit_unsafe(copy_to_device->arg(n, 0));
427 emit_unsafe(copy_to_device->arg(n, 1));
428 auto host_ptr = emit(copy_to_device->arg(n, 2));
429 auto dev_ptr = emit(copy_to_device->arg(n, 3));
430 auto size = emit(type_size);
431
432 std::string copy_res;
433 if (is_async) {
434 auto stream = emit(copy_to_device->arg(n, 4));
435 copy_res = bb.assign(name + "res", "call i32 @{}(i64 {}, ptr {}, i64 {}, ptr {})", Cu_Memcpy_Htod_Async,
436 dev_ptr, host_ptr, size, stream);
437 } else
438 copy_res = bb.assign(name + "res", "call i32 @{}(i64 {}, ptr {}, i64 {})", Cu_Memcpy_Htod, dev_ptr,
439 host_ptr, size);
440
441 emit_cu_error_handling(bb, copy_res);
442 return copy_res;
443 } else if (auto copy_to_host = Axm::isa<gpu::copy_to_host>(def)) {
444 bool is_async;
445 switch (copy_to_host.id()) {
446 case gpu::copy_to_host::block: is_async = false; break;
447 case gpu::copy_to_host::asyn: is_async = true; break;
448 default: fe::throwf(MIM_NVPTX_BE "unhandled `gpu.copy_to_host` id in `{}`", def);
449 }
450
451 // `.block` and `.asyn` differ in arity, so the count has to follow the variant.
452 auto n = is_async ? 5_n : 4_n;
453 if (is_async)
454 declare("i32 @{}(ptr, i64, i64, ptr)", Cu_Memcpy_Dtoh_Async);
455 else
456 declare("i32 @{}(ptr, i64, i64)", Cu_Memcpy_Dtoh);
457
458 auto type = copy_to_host->decurry()->arg();
459 World& w = type->world();
460 auto type_size = w.call(core::trait::size, type);
461
462 emit_unsafe(copy_to_host->arg(n, 0));
463 emit_unsafe(copy_to_host->arg(n, 1));
464 auto dev_ptr = emit(copy_to_host->arg(n, 2));
465 auto host_ptr = emit(copy_to_host->arg(n, 3));
466 auto size = emit(type_size);
467
468 std::string copy_res;
469 if (is_async) {
470 auto stream = emit(copy_to_host->arg(n, 4));
471 copy_res = bb.assign(name + "res", "call i32 @{}(ptr {}, i64 {}, i64 {}, ptr {})", Cu_Memcpy_Dtoh_Async,
472 host_ptr, dev_ptr, size, stream);
473 } else
474 copy_res = bb.assign(name + "res", "call i32 @{}(ptr {}, i64 {}, i64 {})", Cu_Memcpy_Dtoh, host_ptr,
475 dev_ptr, size);
476
477 emit_cu_error_handling(bb, copy_res);
478 return copy_res;
479 } else if (auto launch = Axm::isa<gpu::launch>(def)) {
480 // TODO: rewrite to use modern cuLaunchKernelEx instead
481 declare("i32 @{}(ptr, i32, i32, i32, i32, i32, i32, i32, ptr, ptr, ptr)", Cu_Launch_Kernel);
482
483 auto [implicits, launch_config, kernel_def, arg_def, func_args] = launch->uncurry_args<5>();
484 auto [n_groups_def, n_items_def, stream_def, m, MT] = launch_config->projs<5>();
485 auto [mem, ret_lam_def] = func_args->projs<2>();
486
487 Lam* lam = kernel_def->isa_mut<Lam>();
488 if (!lam) fe::throwf(MIM_NVPTX_BE "kernel `{}` is not a lambda", kernel_def);
489 if (!kernel_ids_.contains(lam)) fe::throwf(MIM_NVPTX_BE "unknown kernel `{}`", lam);
490 auto kid = kernel_ids_[lam];
491
492 auto shared_mem_bytes = 0;
493 if (auto smem_count = Lit::expect(m, "a shared-memory allocation count")) {
494 if (smem_count != 1)
495 fe::throwf(MIM_NVPTX_BE "only one dynamic shared-memory allocation is allowed per kernel");
496 shared_mem_bytes = Lit::expect(world().call(core::trait::size, MT), "a shared-memory size");
497 }
498
500 auto n_groups = emit(n_groups_def);
501 auto n_items = emit(n_items_def);
502 auto stream = emit(stream_def);
503 auto kernel = emit(kernel_def);
504 auto arg = emit(arg_def);
505 auto arg_type = convert(arg_def->type());
506 auto ret_lam = emit(ret_lam_def);
507
508 auto func_ptr = bb.assign(name + "_kernptr", "getelementptr inbounds [{} x ptr], [{} x ptr]* {}, i64 0, i64 {}",
509 kernel_ids_.size(), kernel_ids_.size(), kernel_array_name_, kid);
510 auto func_inner = bb.assign(name + "_kernel", "load ptr, ptr {}", func_ptr);
511
512 auto arg_wrap = bb.assign(name + "_arg_wrap", "alloca {}", arg_type);
513 std::print(bb.body().emplace_back(), "store {} {}, ptr {}", arg_type, arg, arg_wrap);
514
515 auto args_ptr = bb.assign(name + "_args_ptr", "alloca [1 x ptr]");
516 std::print(bb.body().emplace_back(), "store ptr {}, ptr {}", arg_wrap, args_ptr);
517 auto args_inner
518 = bb.assign(name + "_args_inner", "getelementptr inbounds [1 x ptr], ptr {}, i64 0, i64 0", args_ptr);
519 auto launch_res
520 = bb.assign(name,
521 "call i32 @{}(ptr {}, i32 {}, i32 1, i32 1, i32 {}, i32 1, i32 1, "
522 "i32 {}, ptr {}, ptr {}, ptr null)",
523 Cu_Launch_Kernel, func_inner, n_groups, n_items, shared_mem_bytes, stream, args_inner);
524 emit_cu_error_handling(bb, launch_res);
525 return ret_lam;
526 }
527 return std::nullopt;
528}
529
531 for (auto kernel : world().externals().muts()) {
532 auto kernel_lam = kernel->expect_mut<Lam>("an external kernel to be a mutable lambda");
533 kernels_.emplace(kernel_lam);
534 }
535 Super::start();
536}
537
539 auto is_kern = kernels_.contains(root());
540 if (!is_kern) return Super::prepare();
541 auto kernel = root();
542
543 std::print(func_impls_, "define ptx_kernel {} {}(", convert_ret_pi(kernel->type()->ret_pi()), id(kernel));
544
545 auto [m1, m3, m4, m5, group_id, item_id, smem, arg, ret_lam] = kernel->vars<9>();
546
547 auto arg_name = id(arg);
548 locals_[arg] = arg_name;
549 std::println(func_impls_, "{} {}) {{", convert(arg->type()), arg_name);
550
551 auto& bb = lam2bb_[kernel];
552
553 auto register_sreg_idx = [&](const Def* def, std::string_view sreg) {
554 auto name = id(def);
555 auto type = def->type();
556 auto type_name = convert(type);
557 auto opt_idx_lit = Idx::isa_lit(type);
558 if (!opt_idx_lit)
559 fe::throwf(MIM_NVPTX_BE "type of `{}` must be a statically-sized `Idx` but is `{}`", def, type);
560 auto idx_lit = opt_idx_lit.value();
561 locals_[def] = name;
562 declare("i32 @llvm.nvvm.read.ptx.sreg.{}()", sreg);
563 if (type_name == "i0") {
564 locals_[def] = "0";
565 } else if (type_name == "i32") {
566 bb.assign(name, "call i32 @llvm.nvvm.read.ptx.sreg.{}()", sreg);
567 } else if (idx_lit < (1u << 31)) {
568 auto i32 = bb.assign(name + "i32", "call i32 @llvm.nvvm.read.ptx.sreg.{}()", sreg);
569 bb.assign(name, "trunc i32 {} to {}", i32, type_name);
570 } else {
571 fe::throwf(MIM_NVPTX_BE "warp ID too large; must fit into `I32`");
572 }
573 };
574 register_sreg_idx(group_id, "ctaid.x");
575 register_sreg_idx(item_id, "tid.x");
576
577 auto shared_as = Lit::expect(world().annex<gpu::addr_space_shared>(), "the shared address space");
578 if (auto sigma = smem->type()->isa<Sigma>()) {
579 if (sigma->num_ops() != 0)
580 fe::throwf(MIM_NVPTX_BE "shared-memory variable must be an empty sigma, but got `{}`", smem->type());
581 } else {
582 auto ptr = Axm::expect<mem::Ptr>(smem->type(), "a shared-memory pointer type");
583 auto [T, a] = ptr->args<2>();
584 if (Lit::expect(a, "an address space") != shared_as)
585 fe::throwf(MIM_NVPTX_BE "shared-memory variable must live in the shared address space, but got `{}`",
586 smem->type());
587 auto name = "@" + smem->unique_name();
588 locals_[smem] = name;
589 std::println(vars_decls_, "{} = internal addrspace({}) global {} undef", name, a, convert(T, false));
590 }
591
592 return kernel->unique_name();
593}
594
595std::optional<std::string> DeviceEmitter::isa_targetspecific_intrinsic(ll::BB& bb, const Def* def) {
596 auto name = id(def);
597
598 if (auto sync_work_items = Axm::isa<gpu::sync_work_items>(def)) {
599 declare("void @llvm.nvvm.barrier0()");
600
601 emit_unsafe(sync_work_items->arg(2, 0));
602 emit_unsafe(sync_work_items->arg(2, 1));
603 std::print(bb.body().emplace_back(), "call void @llvm.nvvm.barrier0()");
604 return name;
605 } else if (auto tri = Axm::isa<math::tri>(def)) {
606 auto arg = emit(tri->arg());
607 auto type = convert(tri->arg()->type());
608 auto func_name = ""s;
609 switch (tri.id()) {
610 case math::tri::ahff: func_name = "sin"; break;
611 case math::tri::ahfF: func_name = "cos"; break;
612 case math::tri::ahFf: func_name = "tan"; break;
613 case math::tri::ahFF: break;
614 case math::tri::aHff: func_name = "sinh"; break;
615 case math::tri::aHfF: func_name = "cosh"; break;
616 case math::tri::aHFf: func_name = "tanh"; break;
617 case math::tri::aHFF: break;
618 case math::tri::Ahff: func_name = "asin"; break;
619 case math::tri::AhfF: func_name = "acos"; break;
620 case math::tri::AhFf: func_name = "atan"; break;
621 case math::tri::AhFF: break;
622 case math::tri::AHff: func_name = "asinh"; break;
623 case math::tri::AHfF: func_name = "acosh"; break;
624 case math::tri::AHFf: func_name = "atanh"; break;
625 case math::tri::AHFF: break;
626 }
627 if (func_name.empty()) fe::throwf("Trigonometric tag used by {} is currently unused", def);
628 func_name = func_name + ll::detail::math_suffix(tri->arg()->type());
629 auto libdevice_func_name = "__nv_" + func_name;
630 declare("{} @{}({})", type, libdevice_func_name, type);
631 uses_libdevice = true;
632 bb.assign(name, "call {} @{}({} {})", type, libdevice_func_name, type, arg);
633 return name;
634 } else if (auto exp = Axm::isa<math::exp>(def)) {
635 auto arg = emit(exp->arg());
636 auto type = convert(exp->arg()->type());
637 auto func_name = ""s;
638 switch (exp.id()) {
639 case math::exp::lbb: func_name = "exp"; break;
640 case math::exp::lbB: func_name = "exp2"; break;
641 case math::exp::lBb: func_name = "exp10"; break;
642 case math::exp::lBB: break;
643 case math::exp::Lbb: func_name = "log"; break;
644 case math::exp::LbB: func_name = "log2"; break;
645 case math::exp::LBb: func_name = "log10"; break;
646 case math::exp::LBB: break;
647 }
648 if (func_name.empty()) fe::throwf("Exponential tag used by {} is currently unused", def);
649 func_name = func_name + ll::detail::math_suffix(exp->arg()->type());
650 auto libdevice_func_name = "__nv_" + func_name;
651 declare("{} @{}({})", type, libdevice_func_name, type);
652 uses_libdevice = true;
653 bb.assign(name, "call {} @{}({} {})", type, libdevice_func_name, type, arg);
654 return name;
655 }
656 return std::nullopt;
657}
658
659void emit_host(World& world, std::ostream& ostream, std::optional<std::string> device_fatbin_file, ll::Emitter::Rt rt) {
660 HostEmitter emitter(world, ostream, device_fatbin_file);
661 emitter.rt_mode(rt);
662 // Same one-liner the `ll` backend uses; each backend just names its own runtime module.
663 if (rt == ll::Emitter::Rt::embed) emitter.load_rt_module("nvptx_rt.ll");
664 emitter.run();
665}
666
668 DeviceEmitter emitter(world, ostream);
669 emitter.run();
670
671 return DeviceEmitFlags{
672 .uses_libdevice = emitter.is_using_libdevice(),
673 };
674}
675
676} // namespace mim::plug::nvptx
const Def * callee() const
Definition lam.h:237
static auto isa(const Def *def)
Definition axm.h:112
static auto expect(const Def *def, fe::cite_string< Args... > fmt, Args &&... args)
Like Axm::as but - instead of merely asserting in Debug builds - bails via Def::bail_expected when de...
Definition axm.h:142
M * root() const
Definition phase.h:530
Base class for all Defs.
Definition def.h:289
const Def * proj(nat_t a, nat_t i) const
Similar to World::extract while assuming an arity of a, but also works on Sigmas and Arrays.
Definition def.cpp:647
Defs deps() const noexcept
Definition def.cpp:486
T * isa_mut() const
If this is mutable, it will cast constness away and perform a dynamic_cast to T.
Definition def.h:588
const Def * type() const noexcept
Yields the "raw" type of this Def (maybe nullptr).
Definition def.h:1119
std::string unique_name() const
name + "_" + Def::gid
Definition def.cpp:634
static std::optional< nat_t > isa_lit(const Def *def)
Definition def.cpp:677
A function.
Definition lam.h:91
const Def * body() const
Definition lam.h:104
static std::optional< T > isa(const Def *def)
Definition def.h:976
static T expect(const Def *def, fe::cite_string< Args... > fmt, Args &&... args)
Like Lit::as but bails via Def::bail_expected instead of merely asserting in Debug; see Def::expect.
Definition def.h:987
flags_t annex() const
Definition phase.h:62
virtual void run()
Entry point and generates some debug output; invokes Phase::start.
Definition phase.cpp:51
std::string_view name() const
Definition phase.h:61
World & world()
Definition phase.h:58
A dependent tuple type.
Definition tuple.h:39
The World represents the whole program and manages creation of MimIR nodes (Defs).
Definition world.h:43
virtual std::string prepare()
Definition ll.h:513
Emitter(World &world, std::string name, std::ostream &ostream)
Definition ll.h:130
std::string emit(const Def *def)
Recursively emits code.
Definition ll.h:197
std::ostream & ostream() const
Definition ll.h:193
void rt_mode(Rt rt)
Definition ll.h:172
std::string convert_ret_pi(const Pi *)
Definition ll.h:389
void declare(std::format_string< Args... > s, Args &&... args)
Definition ll.h:162
bool load_rt_module(std::string_view filename)
Locates the runtime module rt/<filename> (produced by add_mim_runtime) in the driver's search paths,...
Definition ll.h:485
LamMap< BB > lam2bb_
Definition ll.h:270
std::string id(const Def *, bool force_bb=false) const
Definition ll.h:377
void declare_rt(std::format_string< Args... > sig, Args &&... args)
Declares a runtime wrapper sig (implemented in a C runtime, see add_mim_runtime) and records that the...
Definition ll.h:187
std::ostringstream vars_decls_
Definition ll.h:274
std::ostringstream func_impls_
Definition ll.h:276
Rt
How the C runtime wrappers (compiled to a <name>.ll via add_mim_runtime) reach the output.
Definition ll.h:167
@ embed
Splice the wrapper IR into the emitted module so it is self-contained.
Definition ll.h:168
void start() override
Actual entry.
Definition ll.h:399
std::string emit_unsafe(const Def *def)
As above but yielding an empty string is permitted.
Definition ll.h:204
DefMap< std::string > locals_
Definition ll.h:268
virtual void emit_epilogue(Lam *lam)
Definition ll.h:151
virtual std::string convert(const Def *type, bool simd=true)
Converts type to its LLVM representation as a value (simd) or in memory (!simd).
Definition ll.h:220
std::string emit_slot(ll::BB &, const App *app, const Def *pointee, const Def *addr_space) override
Device slots live in a module-scope global in their requested address space, not on the stack.
Definition nvptx.cpp:86
std::string prepare() override
Definition nvptx.cpp:538
void start() final
Actual entry.
Definition nvptx.cpp:530
bool is_using_libdevice() const
Definition nvptx.cpp:81
std::optional< std::string > isa_targetspecific_intrinsic(ll::BB &, const Def *) final
Definition nvptx.cpp:595
DeviceEmitter(World &world, std::ostream &ostream)
Definition nvptx.cpp:70
const std::string & get_extra_flags() const
Definition nvptx.cpp:82
void emit_epilogue(Lam *) override
Definition nvptx.cpp:270
HostEmitter(World &world, std::ostream &ostream, std::optional< std::string > device_fatbin_file)
Definition nvptx.cpp:31
std::string convert(const Def *, bool simd=true) override
Converts type to its LLVM representation as a value (simd) or in memory (!simd).
Definition nvptx.cpp:280
void start() final
Actual entry.
Definition nvptx.cpp:111
std::optional< std::string > isa_targetspecific_intrinsic(ll::BB &, const Def *) final
Definition nvptx.cpp:292
void find_kernels(const Def *)
Definition nvptx.cpp:144
std::string prepare() override
Definition nvptx.cpp:263
The core Plugin
Definition core.h:8
The gpu Plugin
The ll Plugin
Definition ll.h:41
The mem Plugin
The nvptx Plugin
Definition nvptx.h:12
constexpr auto Cu_Memcpy_Htod
Definition nvptx.cpp:168
constexpr auto Cu_Memcpy_Htod_Async
Definition nvptx.cpp:169
constexpr auto Cu_Init
Definition nvptx.cpp:159
DeviceEmitFlags emit_device(World &, std::ostream &)
Definition nvptx.cpp:667
constexpr auto Cu_Stream_Sync
Definition nvptx.cpp:177
constexpr auto Cu_Memcpy_Dtoh
Definition nvptx.cpp:170
constexpr auto Cu_Mem_Free_Async
Definition nvptx.cpp:167
constexpr auto Cu_Launch_Kernel
Definition nvptx.cpp:163
constexpr auto Cu_Mem_Alloc_Async
Definition nvptx.cpp:165
constexpr auto Cu_Module_Unload
Definition nvptx.cpp:174
constexpr auto Cu_Ctx_Create
Definition nvptx.cpp:160
constexpr auto Cu_Ctx_Destroy
Definition nvptx.cpp:161
constexpr auto Cu_Mem_Free
Definition nvptx.cpp:166
constexpr auto Cu_Mem_Alloc
Definition nvptx.cpp:164
constexpr auto Cu_Memcpy_Dtoh_Async
Definition nvptx.cpp:171
void emit_host(World &, std::ostream &, std::optional< std::string >, ll::Emitter::Rt rt=ll::Emitter::Rt::embed)
Definition nvptx.cpp:659
constexpr auto Cu_Stream_Destroy
Definition nvptx.cpp:176
constexpr auto Cu_Stream_Create
Definition nvptx.cpp:175
constexpr auto Cu_Device_Get
Definition nvptx.cpp:162
constexpr auto Cu_Module_Get_Function
Definition nvptx.cpp:173
constexpr auto Cu_Module_Load_Fatbin
Definition nvptx.cpp:172
GIDSet< Lam * > LamSet
Definition lam.h:211
GIDMap< Lam *, To > LamMap
Definition lam.h:210
GIDSet< const Def * > DefSet
Definition def.h:94
#define MIM_NVPTX_BE
Prefix for this backend's fe::throwf messages; see MIM_LL_BE.
Definition nvptx.h:15
std::deque< std::ostringstream > & body()
Definition ll.h:88
std::string assign(std::string_view name, std::format_string< Args... > s, Args &&... args)
Definition ll.h:92