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