MimIR
MimIR is my Intermediate Representation
Loading...
Searching...
No Matches
ll_nvptx.cpp
Go to the documentation of this file.
2
3#include <mim/driver.h>
4#include <mim/plugin.h>
5
6#include <mim/util/sys.h>
7
8#include <mim/plug/gpu/gpu.h>
9
11
12using namespace std::string_literals;
13using namespace std::string_view_literals;
14
15namespace mim::plug::ll_nvptx {
16
17namespace {
18
19struct NvptxCompileArgs {
20 std::string host_ll_name, dev_ll_name, dev_ptx_name, dev_cubin_name, dev_fatbin_name, dev_bc_raw_name,
21 dev_bc_opt_name;
22#ifdef __linux__
23 bool embed_device_code = true;
24#else
25 bool embed_device_code = false;
26#endif
27 bool embed_ptx = true;
28 bool embed_cubin = true;
29 std::string compute_cap, libdevice_path;
30 std::string link_llvm_args, opt_args = R"(-passes="default<O2>,nvvm-reflect")", llc_args, ptxas_args,
31 fatbinary_args;
32};
33
34constexpr auto Default_Compute_Cap = "75";
35
36std::string get_compute_capability() {
37 auto nvidia_smi = sys::require_cmd("nvidia-smi");
38 auto out = sys::exec(std::format("{} --query-gpu=compute_cap --format=csv,noheader", nvidia_smi));
39 std::erase_if(out, ::isspace);
40 // out should now have form "7.5" referencing the compute capability "sm_75"
41
42 auto dot_pos = out.find('.');
43 if (dot_pos == std::string::npos) {
44 std::println(std::cerr, "Could not determine compute capability, continuing with default: '{}'.",
45 Default_Compute_Cap);
46 return Default_Compute_Cap;
47 }
48
49 for (size_t i = 0; i < out.size(); ++i) {
50 if (i == dot_pos) continue;
51 if (!std::isdigit(out[i])) {
52 std::println(std::cerr, "Could not determine compute capability, continuing with default: '{}'.",
53 Default_Compute_Cap);
54 return Default_Compute_Cap;
55 }
56 }
57
58 auto compute_cap = std::format("{}{}", out.substr(0, dot_pos), out.substr(dot_pos + 1));
59 std::println(std::cout, "Determined compute capability to be '{}'", compute_cap);
60 return compute_cap;
61}
62
63constexpr auto Libdevice_Name = "libdevice.10.bc"sv;
64
65std::optional<std::filesystem::path> parse_nvcc_profile(const std::filesystem::path& cuda_bin_path) {
66 auto profile_path = cuda_bin_path / "nvcc.profile";
67 if (!std::filesystem::exists(profile_path)) return std::nullopt;
68
69 std::ifstream file(profile_path);
70 if (!file.is_open()) return std::nullopt;
71
72 std::string line, top_dir, lib_dir;
73
74 while (std::getline(file, line)) {
75 std::erase_if(line, ::isspace);
76 if (line.starts_with("TOP=")) {
77 auto macro_pos = line.find("$(_HERE_)/");
78 if (macro_pos == std::string::npos) break;
79 top_dir = line.substr(macro_pos + 10);
80 }
81 if (line.starts_with("NVVMIR_LIBRARY_DIR=")) {
82 auto macro_pos = line.find("$(TOP)/");
83 if (macro_pos == std::string::npos) break;
84 lib_dir = line.substr(macro_pos + 7);
85 }
86 }
87 if (top_dir.empty() || lib_dir.empty()) return std::nullopt;
88 auto path = cuda_bin_path / top_dir / lib_dir / Libdevice_Name;
89 auto resolved_path = path.lexically_normal();
90 if (!std::filesystem::exists(resolved_path)) return std::nullopt;
91 return resolved_path;
92}
93
94std::string find_libdevice() {
95 auto nvcc = sys::find_cmd("nvcc");
96 if (std::filesystem::exists(nvcc)) {
97 auto nvcc_path = std::filesystem::canonical(nvcc);
98 auto cuda_bin_path = nvcc_path.parent_path();
99 if (auto libdevice_path = parse_nvcc_profile(cuda_bin_path)) return libdevice_path->string();
100 }
101 if (const char* cuda_home_env = std::getenv("CUDA_HOME")) {
102 auto libdevice_path = std::filesystem::path(cuda_home_env) / "nvvm" / "libdevice" / Libdevice_Name;
103 if (std::filesystem::exists(libdevice_path)) return libdevice_path.string();
104 }
105 auto debian_fallback = std::filesystem::path("/usr/lib/nvidia-cuda-toolkit/libdevice/") / Libdevice_Name;
106 if (std::filesystem::exists(debian_fallback)) return debian_fallback.string();
107
108 fe::throwf<sys::CmdNotFound>("Unable to find '{}'. Try setting the CUDA_HOME environment variable.",
109 Libdevice_Name);
110}
111
112void link_libdevice(const NvptxCompileArgs& c) {
113 if (!std::filesystem::exists(c.libdevice_path)) fe::throwf("libdevice path does not exist: {}", c.libdevice_path);
114 auto llvm_link = sys::require_cmd("llvm-link");
115 sys::require_run(std::format("{} {} {} {} -o {}", llvm_link, c.link_llvm_args, c.dev_ll_name, c.libdevice_path,
116 c.dev_bc_raw_name));
117}
118
119void optimize_bytecode(const NvptxCompileArgs& c) {
120 auto opt = sys::require_cmd("opt");
121 sys::require_run(std::format("{} {} {} -o {}", opt, c.opt_args, c.dev_bc_raw_name, c.dev_bc_opt_name));
122}
123
124void compile2ptx(const NvptxCompileArgs& c, bool uses_libdevice) {
125 auto compile_input = uses_libdevice ? c.dev_bc_opt_name : c.dev_ll_name;
126 auto llc = sys::require_cmd("llc");
127 sys::require_run(std::format("{} -march=nvptx64 -mcpu=sm_{} {} {} -o {}", llc, c.compute_cap, c.llc_args,
128 compile_input, c.dev_ptx_name));
129}
130
131void compile2cubin(const NvptxCompileArgs& c) {
132 auto ptxas = sys::require_cmd("ptxas");
133 sys::require_run(std::format("{} -arch=sm_{} {} {} -o {}", ptxas, c.compute_cap, c.ptxas_args, c.dev_ptx_name,
134 c.dev_cubin_name));
135}
136
137void compile2fatbin(const NvptxCompileArgs& c) {
138 auto fatbinary = sys::require_cmd("fatbinary");
139 auto ptx_args = ""s;
140 if (c.embed_ptx) {
141 ptx_args = std::format("--image3=kind=ptx,sm={},file={}", c.compute_cap, c.dev_ptx_name);
142 if (!c.ptxas_args.empty()) ptx_args += std::format(" --cmdline={}", c.ptxas_args);
143 }
144 auto cubin_args = ""s;
145 if (c.embed_cubin) cubin_args = std::format("--image3=kind=elf,sm={},file={}", c.compute_cap, c.dev_cubin_name);
146 sys::require_run(std::format("{} --create={} -64 {} {} {}", fatbinary, c.dev_fatbin_name, c.fatbinary_args,
147 ptx_args, cubin_args));
148}
149
150} // namespace
151
152class Emit : public Phase {
153public:
156
157 void start() override {
158 auto name = world().name() ? std::string(world().name().view()) : "a"s;
159
160 auto c = NvptxCompileArgs{};
161 c.host_ll_name = name + ".ll"s;
162 c.dev_ll_name = name + "_dev.ll"s;
163 c.dev_ptx_name = name + "_dev.ptx"s;
164 c.dev_cubin_name = name + "_dev.cubin"s;
165 c.dev_fatbin_name = name + "_dev.fatbin"s;
166 c.dev_bc_raw_name = name + "_dev_raw.bc"s;
167 c.dev_bc_opt_name = name + "_dev_opt.bc"s;
168
169 auto rt = ll::Emitter::Rt::embed;
170 for (const auto& arg : args()) {
171 world().DLOG("ll backend arg: `{}`", arg);
172 // clang-format off
173 if (false) {}
174 else if (arg.starts_with("o=")) c.host_ll_name = arg.substr(2);
175 else if (arg.starts_with("output=")) c.host_ll_name = arg.substr(7);
176 else if (arg.starts_with("o-dev=")) c.dev_ll_name = arg.substr(6);
177 else if (arg.starts_with("output-dev=")) c.dev_ll_name = arg.substr(11);
178 else if (arg.starts_with("sm=")) c.compute_cap = arg.substr(3);
179 else if (arg.starts_with("libdevice=")) c.libdevice_path = arg.substr(10);
180 else if (arg.starts_with("Xlink_llvm=")) c.link_llvm_args = arg.substr(11);
181 else if (arg.starts_with("Xopt=")) c.opt_args = arg.substr(5);
182 else if (arg.starts_with("Xllc=")) c.llc_args = arg.substr(5);
183 else if (arg.starts_with("Xptxas=")) c.ptxas_args = arg.substr(7);
184 else if (arg.starts_with("Xfatbinary=")) c.fatbinary_args = arg.substr(11);
185 else if (arg == "no-embed") c.embed_device_code = false;
186 else if (arg == "no-ptx-embed") c.embed_ptx = false;
187 else if (arg == "no-cubin-embed") c.embed_cubin = false;
188 else if (arg == "rt=embed") rt = ll::Emitter::Rt::embed;
189 else if (arg == "rt=extern") rt = ll::Emitter::Rt::ext;
190 // clang-format on
191 }
192
193 auto split_apply_phase = Phase::create(world().driver().phases(), world().annex<gpu::split_apply>());
194 auto setup_phase = split_apply_phase.get()->expect<RWPhase>("%gpu.split_apply to be an RWPhase");
195 setup_phase->run();
196
197 DeviceEmitFlags device_flags;
198 {
199 auto dev_ofs = std::ofstream(c.dev_ll_name);
200 device_flags = emit_device(setup_phase->new_world(), dev_ofs);
201 }
202 if (c.embed_device_code) {
203 if (!c.embed_ptx && !c.embed_cubin)
204 fe::throwf("Embedding requested with no images (neither PTX nor CUBIN).");
205 try {
206 if (c.compute_cap.empty()) c.compute_cap = get_compute_capability();
207 if (device_flags.uses_libdevice) {
208 if (c.libdevice_path.empty()) c.libdevice_path = find_libdevice();
209 link_libdevice(c);
210 optimize_bytecode(c);
211 }
212 compile2ptx(c, device_flags.uses_libdevice);
213 compile2cubin(c);
214 compile2fatbin(c);
215 } catch (const sys::CmdNotFound& e) {
216 WLOG("{}", e.what());
217 WLOG("Falling back to not embedding device code.");
218 c.embed_device_code = false;
219 }
220 }
221 auto device_fatbin_file = c.embed_device_code ? std::optional(c.dev_fatbin_name) : std::nullopt;
222 auto host_ofs = std::ofstream(c.host_ll_name);
223 emit_host(setup_phase->old_world(), host_ofs, device_fatbin_file, rt);
224
225 if (c.embed_device_code) {
226 std::println(std::cout, "Unified (Fat) LLVM IR written to {}", c.host_ll_name);
227 } else {
228 std::println(std::cout, "Host-only LLVM IR written to {}", c.host_ll_name);
229 std::println(std::cout, "Device-only LLVM IR written to {}", c.dev_ll_name);
230 }
231 }
232};
233
234} // namespace mim::plug::ll_nvptx
235
236using namespace mim;
237
239
240extern "C" MIM_EXPORT Plugin mim_get_plugin() { return {"ll_nvptx", MIM_VERSION, nullptr, reg_phases}; }
void reg_phases(Flags2Phases &phases)
Definition affine.cpp:12
static void hook(Flags2Phases &phases)
Definition phase.h:70
flags_t annex() const
Definition phase.h:81
Phase(World &world, std::string name)
Definition phase.h:29
static std::unique_ptr< Phase > create(const Flags2Phases &phases, const Def *def)
Definition phase.h:50
Driver & driver()
Definition phase.h:78
virtual void run()
Entry point and generates some debug output; invokes Phase::start.
Definition phase.cpp:34
std::string_view name() const
Definition phase.h:80
const Vector< std::string > & args()
Command-line arguments passed to this Phase's plugin via -X <plugin>:<arg>.
Definition phase.cpp:23
World & world()
Definition phase.h:77
Rebuilds old_world() into new_world() and then swaps them.
Definition phase.h:312
The World represents the whole program and manages creation of MimIR nodes (Defs).
Definition world.h:36
Sym name() const
Definition world.h:97
@ embed
Splice the wrapper IR into the emitted module so it is self-contained.
Definition ll.h:160
@ ext
Only declare the wrappers; the runtime is linked in externally.
Definition ll.h:161
void start() override
Actual entry.
Definition ll_nvptx.cpp:157
Emit(World &world, flags_t annex)
Definition ll_nvptx.cpp:154
Thrown by sys::require_cmd when a command cannot be located on the system.
Definition sys.h:32
#define MIM_EXPORT
Definition config.h:19
static void reg_phases(Flags2Phases &phases)
Definition ll_nvptx.cpp:238
#define WLOG(...)
Definition log.h:89
The ll_nvptx Plugin
Definition ll_nvptx.h:12
DeviceEmitFlags emit_device(World &, std::ostream &)
Definition ll_nvptx.cpp:539
void emit_host(World &, std::ostream &, std::optional< std::string >, ll::Emitter::Rt rt=ll::Emitter::Rt::embed)
Definition ll_nvptx.cpp:531
void require_run(const std::string &cmd)
Runs cmd via sys::system and throws if it exits with a non-zero status.
Definition sys.cpp:113
std::string find_cmd(std::string)
Definition sys.cpp:95
std::string require_cmd(std::string_view name)
Locates name on the system or throws CmdNotFound.
Definition sys.cpp:101
std::string exec(std::string cmd)
Executes command cmd.
Definition sys.cpp:83
Definition ast.h:14
u64 flags_t
Definition types.h:39
absl::flat_hash_map< flags_t, std::function< std::unique_ptr< Phase >(World &)> > Flags2Phases
Maps an axiom of a Phase to a function that creates one.
Definition plugin.h:25
mim::Plugin mim_get_plugin()
#define MIM_VERSION
Definition plugin.h:54
Basic info and registration function pointer to be returned from a specific plugin.
Definition plugin.h:59